My Resumes history, modern M3 redesign, authenticated exports #2

Merged
armistace merged 10 commits from feature/my-resumes-modern into master 2026-08-04 23:58:13 +10:00
21 changed files with 789 additions and 213 deletions
Showing only changes of commit 3c9553ad39 - Show all commits

View File

@ -4,6 +4,13 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-07-30T23:29:27.007440701Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=5C051JEA313902" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>

1
.idea/gradle.xml generated
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

2
.idea/misc.xml generated
View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

133
GOOGLE_AUTH_SETUP.md Normal file
View File

@ -0,0 +1,133 @@
# Google Authentication Setup
## OAuth Credentials Summary
You now have **TWO** OAuth credentials configured in Google Cloud Console:
### 1. Web Application (for backend)
- **Client ID**: `416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com`
- **Type**: Web application
- **Purpose**: Backend token exchange (`POST /auth/google-token`)
- **Authorized redirect URIs**: Configured on your backend
### 2. Android Application (for mobile app)
- **Client ID**: `416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com`
- **Type**: Android
- **Package name**: `com.example.resbuilder`
- **SHA-1 fingerprint**: `38:A9:E7:27:69:40:77:77:A7:3D:A1:AC:21:F2:8E:50:5D:C9:AC:7F`
- **Purpose**: Android Credential Manager + Google Sign-In
## What Was Changed
### Dependencies Added
```kotlin
// gradle/libs.versions.toml
credentialManager = "1.5.0-beta01"
googleid = "1.1.1"
kotlinxCoroutines = "1.9.0"
androidx-credential-manager
androidx-credential-manager-play-services
googleid
kotlinx-coroutines-android
```
### Code Migration
The `LoginActivity.kt` now uses:
1. **Credential Manager API** (primary) - Modern, recommended approach
2. **Legacy Google Sign-In** (fallback) - For devices that don't support Credential Manager
Key changes:
- Uses `androidClientId` for Credential Manager flow
- Uses `webClientId` for backend token exchange
- Automatic fallback to legacy API if Credential Manager fails
- Coroutines for suspend function support
- Nonce generation for enhanced security
## Verification Steps
1. **Build the app**:
```bash
./gradlew assembleDebug
```
APK: `app/build/outputs/apk/debug/app-debug.apk`
2. **Install on device/emulator** with your debug keystore SHA-1
3. **Test sign-in flow**:
- Tap "Sign in with Google"
- Account picker should appear
- Select an account
- Should authenticate and navigate to main screen
## Troubleshooting
### Error 10: Developer Error
- SHA-1 fingerprint doesn't match Google Cloud Console
- Package name doesn't match
- **Fix**: Verify SHA-1 in Google Cloud Console matches `38:A9:E7:27:69:40:77:77:A7:3D:A1:AC:21:F2:8E:50:5D:C9:AC:7F`
### Error 16: Sign In Cancelled
- User cancelled the account picker
- Normal behavior, not an error
### Credential Manager not working
- Device may not support Credential Manager
- **Fix**: App automatically falls back to legacy Google Sign-In
## API Flow
```
┌─────────────────┐
│ User taps │
│ Sign In │
└────────┬────────┘
v
┌─────────────────┐
│ Credential │
│ Manager │◄─── Uses androidClientId
└────────┬────────┘
v
┌─────────────────┐
│ Google returns │
│ ID Token │
└────────┬────────┘
v
┌─────────────────┐
│ App sends ID │
│ Token to backend│
│ POST /auth/ │
│ google-token │◄─── Backend validates with webClientId
└────────┬────────┘
v
┌─────────────────┐
│ Backend returns │
│ session token │
└────────┬────────┘
v
┌─────────────────┐
│ App stores │
│ session token │
│ (Bearer auth) │
└─────────────────┘
```
## Next Steps (Optional)
For production release:
1. Get **release SHA-1** from Play Console or your release keystore
2. Add release SHA-1 to Google Cloud Console (same Android OAuth client)
3. Build signed release APK/AAB
4. Test with release credentials
## References
- [Credential Manager Documentation](https://developer.android.com/identity/sign-in/credential-manager)
- [Google Sign-In Migration Guide](https://developers.google.com/identity/openid-connect/openid-connect)
- [OAuth 2.0 for Android](https://developers.google.com/identity/protocols/oauth2/native-app)

View File

@ -41,6 +41,10 @@ dependencies {
implementation(libs.gson) implementation(libs.gson)
implementation(libs.browser) implementation(libs.browser)
implementation(libs.google.play.services.auth) implementation(libs.google.play.services.auth)
implementation(libs.androidx.credential.manager)
implementation(libs.androidx.credential.manager.play.services)
implementation(libs.googleid)
implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -62,3 +62,15 @@ data class AdminUser(
data class AdminUsersResponse( data class AdminUsersResponse(
val users: List<AdminUser> val users: List<AdminUser>
) )
data class JobSummary(
val job_id: String,
val type: String,
val status: String,
val title: String? = null,
val created_at: String? = null
)
data class JobListResponse(
val jobs: List<JobSummary>
)

View File

@ -5,6 +5,7 @@ import com.example.resbuilder.data.model.BuildRequest
import com.example.resbuilder.data.model.BuildResponse import com.example.resbuilder.data.model.BuildResponse
import com.example.resbuilder.data.model.HealthResponse import com.example.resbuilder.data.model.HealthResponse
import com.example.resbuilder.data.model.Job import com.example.resbuilder.data.model.Job
import com.example.resbuilder.data.model.JobListResponse
import com.example.resbuilder.data.model.ScrapeResponse import com.example.resbuilder.data.model.ScrapeResponse
import com.example.resbuilder.data.model.UploadResponse import com.example.resbuilder.data.model.UploadResponse
import com.example.resbuilder.data.model.User import com.example.resbuilder.data.model.User
@ -17,6 +18,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -51,6 +53,8 @@ object ApiClient {
bearerToken?.let { builder.header("Authorization", "Bearer $it") } bearerToken?.let { builder.header("Authorization", "Bearer $it") }
chain.proceed(builder.build()) chain.proceed(builder.build())
} }
// HTTP/1.1 only: backend/proxy resets HTTP/2 streams mid-response.
.protocols(listOf(Protocol.HTTP_1_1))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS)
@ -115,6 +119,24 @@ object ApiClient {
} }
} }
@Throws(IOException::class)
fun getMyJobs(): JobListResponse {
val req = Request.Builder()
.url("$API_BASE/jobs")
.build()
client.newCall(req).execute().use { res ->
if (!res.isSuccessful) {
val errBody = res.body?.string()
val detail = try {
gson.fromJson(errBody, Map::class.java)["detail"] as? String
} catch (_: Exception) { null }
throw IOException(detail ?: "Failed to load your documents")
}
val body = res.body?.string() ?: throw IOException("Empty response")
return gson.fromJson(body, JobListResponse::class.java)
}
}
fun getLoginUrl(): String = "$API_BASE/auth/login" fun getLoginUrl(): String = "$API_BASE/auth/login"
fun getLogoutUrl(): String = "$API_BASE/auth/logout" fun getLogoutUrl(): String = "$API_BASE/auth/logout"

View File

@ -2,34 +2,52 @@ package com.example.resbuilder.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.util.Log
import android.os.Looper
import android.view.View import android.view.View
import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialException
import androidx.lifecycle.lifecycleScope
import com.example.resbuilder.R import com.example.resbuilder.R
import com.example.resbuilder.data.remote.ApiClient import com.example.resbuilder.data.remote.ApiClient
import com.google.android.gms.auth.api.signin.GoogleSignIn 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.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.SignInButton
import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Task
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.CircularProgressIndicator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.security.SecureRandom
import java.util.Base64
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private lateinit var googleSignInButton: SignInButton private lateinit var googleSignInButton: MaterialButton
private lateinit var progress: ProgressBar private lateinit var progress: CircularProgressIndicator
private lateinit var credentialManager: CredentialManager
private val handler = Handler(Looper.getMainLooper()) private lateinit var googleSignInLauncher: ActivityResultLauncher<IntentSenderRequest>
private lateinit var googleSignInLauncher: ActivityResultLauncher<Intent> // Client IDs from Google Cloud Console
// Same client ID registered in Google Cloud Console for the backend OAuth
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com" private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.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)
@ -37,110 +55,144 @@ class LoginActivity : AppCompatActivity() {
googleSignInButton = findViewById(R.id.googleSignInButton) googleSignInButton = findViewById(R.id.googleSignInButton)
progress = findViewById(R.id.loginProgress) progress = findViewById(R.id.loginProgress)
credentialManager = CredentialManager.create(this)
googleSignInButton.setOnClickListener { launchGoogleSignIn() } googleSignInButton.setOnClickListener { launchGoogleSignIn() }
googleSignInLauncher = registerForActivityResult( googleSignInLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartIntentSenderForResult()
) { result -> ) { result ->
if (result.resultCode == RESULT_OK && result.data != null) { Log.w(TAG, "Unexpected activity result: ${result.resultCode}")
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
handleSignInResult(task)
} else {
showLoading(false) showLoading(false)
Toast.makeText(this, "Sign in cancelled", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Sign-in interrupted", Toast.LENGTH_SHORT).show()
}
} }
showLoading(true) showLoading(true)
checkExistingAuth() checkExistingAuth()
} }
private fun checkExistingAuth() {
lifecycleScope.launch {
try {
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
showLoading(false)
if (user.authenticated) {
launchMain()
}
} catch (_: Exception) {
showLoading(false)
}
}
}
private fun showLoading(loading: Boolean) { private fun showLoading(loading: Boolean) {
progress.visibility = if (loading) View.VISIBLE else View.GONE progress.visibility = if (loading) View.VISIBLE else View.GONE
googleSignInButton.isEnabled = !loading googleSignInButton.isEnabled = !loading
} }
private fun checkExistingAuth() {
Thread {
try {
val user = ApiClient.getMe()
handler.post {
showLoading(false)
if (user.authenticated) {
launchMain()
}
}
} catch (_: Exception) {
handler.post { showLoading(false) }
}
}.start()
}
private fun launchGoogleSignIn() { private fun launchGoogleSignIn() {
showLoading(true) showLoading(true)
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) lifecycleScope.launch {
.requestIdToken(webClientId) Log.d(TAG, "Starting Credential Manager flow")
.requestEmail() Log.d(TAG, "Web client ID: $webClientId")
Log.d(TAG, "Android client ID: $androidClientId")
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setAutoSelectEnabled(false)
.setNonce(generateNonce())
.build() .build()
val client = GoogleSignIn.getClient(this, gso)
// Clear any previous sign-in to force account picker val request = GetCredentialRequest.Builder()
client.signOut().addOnCompleteListener { .addCredentialOption(googleIdOption)
val signInIntent = client.signInIntent .build()
googleSignInLauncher.launch(signInIntent)
try {
Log.d(TAG, "Calling getCredential...")
val result = credentialManager.getCredential(
request = request,
context = this@LoginActivity
)
Log.d(TAG, "Credential retrieved successfully")
handleCredentialResponse(result)
} 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)
val errorMsg = when (e) {
is androidx.credentials.exceptions.NoCredentialException -> "No Google accounts found on device"
is androidx.credentials.exceptions.GetCredentialCancellationException -> "Sign-in cancelled"
else -> "Sign-in failed: ${e.message}"
}
Toast.makeText(this@LoginActivity, errorMsg, Toast.LENGTH_LONG).show()
}
} }
} }
private fun handleSignInResult(task: Task<GoogleSignInAccount>) { private fun handleCredentialResponse(response: GetCredentialResponse) {
val credential = response.credential
when {
credential is CustomCredential && credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> {
try { try {
val account = task.getResult(ApiException::class.java) val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
val idToken = account?.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, "No ID token from Google", Toast.LENGTH_LONG).show()
return return
} }
exchangeToken(idToken) exchangeToken(idToken)
} catch (e: ApiException) { } catch (e: GoogleIdTokenParsingException) {
showLoading(false) showLoading(false)
Toast.makeText(this, "Sign in failed: ${e.message}", Toast.LENGTH_LONG).show() Toast.makeText(this, "Invalid Google ID token: ${e.message}", Toast.LENGTH_LONG).show()
}
}
else -> {
showLoading(false)
Toast.makeText(this, "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) { private fun exchangeToken(idToken: String) {
Thread { lifecycleScope.launch {
try { try {
val token = ApiClient.exchangeGoogleToken(idToken) val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
ApiClient.setToken(token) ApiClient.setToken(token)
val user = ApiClient.getMe() val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
handler.post {
showLoading(false) showLoading(false)
if (user.authenticated) { if (user.authenticated) {
Toast.makeText( Toast.makeText(
this, this@LoginActivity,
"Welcome ${user.name ?: user.email}", "Welcome ${user.name ?: user.email}",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
launchMain() launchMain()
} else { } else {
Toast.makeText( Toast.makeText(
this, this@LoginActivity,
"Server rejected the token", "Server rejected the token",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
}
} catch (e: Exception) { } catch (e: Exception) {
handler.post {
showLoading(false) showLoading(false)
Log.e(TAG, "Token exchange failed", e)
Toast.makeText( Toast.makeText(
this, this@LoginActivity,
e.message ?: "Token exchange failed", e.message ?: "Token exchange failed",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
} }
}.start()
} }
private fun launchMain() { private fun launchMain() {
@ -150,6 +202,12 @@ class LoginActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
handler.removeCallbacksAndMessages(null) }
private fun generateNonce(): String {
val secureRandom = SecureRandom()
val bytes = ByteArray(32)
secureRandom.nextBytes(bytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
} }
} }

View File

@ -142,6 +142,13 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.menuMyResumes -> {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, MyResumesFragment())
.addToBackStack(null)
.commitAllowingStateLoss()
true
}
R.id.menuSignOut -> { R.id.menuSignOut -> {
signOut() signOut()
true true

View File

@ -0,0 +1,158 @@
package com.example.resbuilder.ui
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.example.resbuilder.R
import com.example.resbuilder.data.model.Job
import com.example.resbuilder.data.model.JobSummary
import com.example.resbuilder.data.remote.ApiClient
import com.google.android.material.progressindicator.LinearProgressIndicator
class MyResumesFragment : Fragment() {
private val handler = Handler(Looper.getMainLooper())
private lateinit var emptyView: TextView
private lateinit var progress: LinearProgressIndicator
private lateinit var listView: ListView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_my_resumes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emptyView = view.findViewById(R.id.myResumesEmpty)
progress = view.findViewById(R.id.myResumesProgress)
listView = view.findViewById(R.id.myResumesList)
listView.setOnItemClickListener { _, _, position, _ ->
val item = listView.adapter.getItem(position) as JobSummary
openJob(item.job_id)
}
loadJobs()
}
private fun loadJobs() {
showLoading(true)
Thread {
try {
val resp = ApiClient.getMyJobs()
handler.post {
showLoading(false)
bindJobs(resp.jobs)
}
} catch (e: Exception) {
handler.post {
showLoading(false)
emptyView.text = getString(R.string.my_resumes_error)
emptyView.visibility = View.VISIBLE
Toast.makeText(requireContext(), e.message ?: getString(R.string.my_resumes_error), Toast.LENGTH_LONG).show()
}
}
}.start()
}
private fun bindJobs(jobs: List<JobSummary>) {
if (jobs.isEmpty()) {
emptyView.text = getString(R.string.my_resumes_empty)
emptyView.visibility = View.VISIBLE
listView.visibility = View.GONE
return
}
listView.visibility = View.VISIBLE
listView.adapter = JobListAdapter(requireContext(), jobs)
}
private fun openJob(jobId: String) {
showLoading(true)
Thread {
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
}
when (job.status.uppercase()) {
"COMPLETED" -> {
val type = job.type ?: JobFormFragment.TYPE_RESUME
replaceWith(ResultFragment.newInstance(job, type))
}
"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))
}
}
} 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) {
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, fragment)
.addToBackStack(null)
.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(
context: android.content.Context,
private val jobs: List<JobSummary>
) : ArrayAdapter<JobSummary>(context, R.layout.item_job_summary, jobs) {
private val inflater = android.view.LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = convertView ?: inflater.inflate(R.layout.item_job_summary, parent, false)
val job = jobs[position]
v.findViewById<TextView>(R.id.itemTitle).text =
job.title?.takeIf { it.isNotBlank() } ?: getKindLabel(job.type)
v.findViewById<TextView>(R.id.itemSubtitle).text = buildSubtitle(job)
return v
}
private fun getKindLabel(type: String?): String =
if (type == JobFormFragment.TYPE_COVER_LETTER) context.getString(R.string.type_cover_letter)
else context.getString(R.string.type_resume)
private fun buildSubtitle(job: JobSummary): String {
val kind = getKindLabel(job.type)
val created = job.created_at?.let(::formatDate)?.let { " · $it" }.orEmpty()
return "$kind$created"
}
private fun formatDate(iso: String): String {
return try {
val date = java.time.OffsetDateTime.parse(iso)
val local = date.atZoneSameInstant(java.time.ZoneId.systemDefault())
java.time.format.DateTimeFormatter.ofPattern("d MMM yyyy").format(local)
} catch (_: Exception) {
iso
}
}
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:startColor="?attr/colorPrimary"
android:endColor="?attr/colorPrimaryContainer"
android:type="linear" />
</shape>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Google G icon -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#4285F4"
android:pathData="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path
android:fillColor="#34A853"
android:pathData="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path
android:fillColor="#FBBC05"
android:pathData="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path
android:fillColor="#EA4335"
android:pathData="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</vector>

View File

@ -5,22 +5,39 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:padding="16dp"
tools:context=".ui.LoginActivity"> tools:context=".ui.LoginActivity">
<!-- Skeleton loading placeholder visible while auth is checked --> <!-- Decorative background element -->
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_gradient_primary"
app:layout_constraintBottom_toTopOf="@id/guidelineTop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.35" />
<!-- Main content card -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/loginSkeletonCard" android:id="@+id/loginCard"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="24dp"
android:visibility="gone" app:cardBackgroundColor="?attr/colorSurfaceContainer"
app:cardCornerRadius="12dp" app:cardCornerRadius="24dp"
app:cardElevation="0dp" app:cardElevation="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -29,91 +46,101 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
android:padding="24dp"> android:padding="32dp">
<View <!-- App icon with elevation -->
android:layout_width="80dp" <com.google.android.material.card.MaterialCardView
android:layout_height="80dp" android:layout_width="88dp"
android:background="?attr/colorSurfaceContainerHigh" /> android:layout_height="88dp"
app:cardBackgroundColor="?attr/colorPrimaryContainer"
<View app:cardCornerRadius="22dp"
android:layout_width="160dp" app:cardElevation="4dp">
android:layout_height="24dp"
android:layout_marginTop="16dp"
android:background="?attr/colorSurfaceContainerHigh" />
<View
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:background="?attr/colorSurfaceContainerHigh" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<ImageView <ImageView
android:id="@+id/appIcon" android:layout_width="match_parent"
android:layout_width="96dp" android:layout_height="match_parent"
android:layout_height="96dp"
android:layout_marginBottom="24dp"
android:background="?attr/colorPrimaryContainer"
android:contentDescription="@string/cd_app_logo" android:contentDescription="@string/cd_app_logo"
android:importantForAccessibility="yes" android:padding="20dp"
android:padding="24dp" android:scaleType="centerInside"
android:src="@drawable/ic_launcher_foreground" android:src="@drawable/ic_launcher_foreground"
app:layout_constraintBottom_toTopOf="@id/loginTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:tint="?attr/colorOnPrimaryContainer" /> app:tint="?attr/colorOnPrimaryContainer" />
</com.google.android.material.card.MaterialCardView>
<!-- Title -->
<TextView <TextView
android:id="@+id/loginTitle" android:id="@+id/loginTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginTop="24dp"
android:text="@string/app_title" android:text="@string/app_title"
android:textAppearance="?attr/textAppearanceHeadlineLarge" android:textAlignment="center"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface"
app:layout_constraintBottom_toTopOf="@id/loginSubtitle" android:textStyle="bold" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<!-- Subtitle -->
<TextView <TextView
android:id="@+id/loginSubtitle" android:id="@+id/loginSubtitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="24dp" android:layout_marginTop="8dp"
android:text="@string/checking_auth" android:text="@string/welcome_subtitle"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAlignment="center"
android:textColor="?attr/colorOnSurfaceVariant" android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/googleSignInButton" android:textColor="?attr/colorOnSurfaceVariant" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginTitle" />
<ProgressBar <!-- Divider -->
android:id="@+id/loginProgress" <View
android:layout_width="48dp" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="1dp"
android:layout_marginBottom="24dp" android:layout_marginVertical="24dp"
android:contentDescription="@string/cd_loading" android:background="?attr/colorOutlineVariant" />
android:indeterminateTint="?attr/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/loginTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.gms.common.SignInButton <!-- Sign in button container -->
<com.google.android.material.button.MaterialButton
android:id="@+id/googleSignInButton" android:id="@+id/googleSignInButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="56dp"
android:fontFamily="sans-serif-medium"
android:text="@string/sign_in_with_google"
android:textAllCaps="false"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
app:cornerRadius="14dp"
app:icon="@drawable/ic_google"
app:iconGravity="textStart"
app:iconPadding="12dp"
app:iconSize="24dp"
app:iconTint="@null"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1.5dp" />
<!-- Helper text -->
<TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="16dp"
android:contentDescription="@string/cd_google_sign_in" android:text="@string/sign_in_terms"
android:minWidth="200dp" android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent" android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Progress indicator -->
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="?attr/colorOnPrimary"
app:indicatorSize="48dp"
app:layout_constraintBottom_toTopOf="@id/loginCard"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginSubtitle" /> app:layout_constraintTop_toTopOf="@id/guidelineTop"
app:trackColor="?attr/colorPrimaryContainer" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:background="?attr/colorSurface"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/myResumesTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="4dp"
android:paddingBottom="8dp"
android:text="@string/my_resumes_title"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/myResumesEmpty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="@string/my_resumes_empty"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/myResumesProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="?attr/colorPrimary"
app:trackColor="?attr/colorSurfaceContainerHighest"
tools:visibility="gone" />
<ListView
android:id="@+id/myResumesList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:divider="@null"
android:listSelector="@android:color/transparent"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:layout_marginVertical="6dp"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/itemTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
tools:text="Senior Android Engineer" />
<TextView
android:id="@+id/itemSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
tools:text="Resume · 12 Aug 2026" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menuMyResumes"
android:title="@string/menu_my_resumes" />
<item <item
android:id="@+id/menuAdminPanel" android:id="@+id/menuAdminPanel"
android:title="@string/menu_admin_panel" android:title="@string/menu_admin_panel"

View File

@ -1,41 +1,41 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary color roles (dark) --> <!-- Primary color roles (dark) -->
<item name="colorPrimary">#D0BCFF</item> <item name="colorPrimary">#4DD9DB</item>
<item name="colorOnPrimary">#381E72</item> <item name="colorOnPrimary">#003738</item>
<item name="colorPrimaryContainer">#4F378B</item> <item name="colorPrimaryContainer">#004F51</item>
<item name="colorOnPrimaryContainer">#EADDFF</item> <item name="colorOnPrimaryContainer">#6FF6F8</item>
<item name="colorPrimaryInverse">#6750A4</item> <item name="colorPrimaryInverse">#006A6C</item>
<!-- Secondary color roles (dark) --> <!-- Secondary color roles (dark) -->
<item name="colorSecondary">#CCC2DC</item> <item name="colorSecondary">#B0CCCB</item>
<item name="colorOnSecondary">#332D41</item> <item name="colorOnSecondary">#1B3535</item>
<item name="colorSecondaryContainer">#4A4458</item> <item name="colorSecondaryContainer">#324C4B</item>
<item name="colorOnSecondaryContainer">#E8DEF8</item> <item name="colorOnSecondaryContainer">#CCE8E7</item>
<!-- Tertiary color roles (dark) --> <!-- Tertiary color roles (dark) -->
<item name="colorTertiary">#EFB8C8</item> <item name="colorTertiary">#B1C8E8</item>
<item name="colorOnTertiary">#492532</item> <item name="colorOnTertiary">#19324C</item>
<item name="colorTertiaryContainer">#633B48</item> <item name="colorTertiaryContainer">#324963</item>
<item name="colorOnTertiaryContainer">#FFD8E4</item> <item name="colorOnTertiaryContainer">#D2E4FF</item>
<!-- Surface color roles (dark) --> <!-- Surface color roles (dark) -->
<item name="android:colorBackground">#1D1B20</item> <item name="android:colorBackground">#0E1515</item>
<item name="colorSurface">#1D1B20</item> <item name="colorSurface">#0E1515</item>
<item name="colorOnSurface">#E6E1E5</item> <item name="colorOnSurface">#DDE4E4</item>
<item name="colorSurfaceVariant">#49454F</item> <item name="colorSurfaceVariant">#3F4949</item>
<item name="colorOnSurfaceVariant">#CAC4D0</item> <item name="colorOnSurfaceVariant">#BEC9C8</item>
<item name="colorSurfaceContainerLowest">#16141C</item> <item name="colorSurfaceContainerLowest">#090F0F</item>
<item name="colorSurfaceContainerLow">#1D1B20</item> <item name="colorSurfaceContainerLow">#161D1D</item>
<item name="colorSurfaceContainer">#211F26</item> <item name="colorSurfaceContainer">#1A2121</item>
<item name="colorSurfaceContainerHigh">#27252D</item> <item name="colorSurfaceContainerHigh">#242B2B</item>
<item name="colorSurfaceContainerHighest">#36343B</item> <item name="colorSurfaceContainerHighest">#2F3636</item>
<item name="colorSurfaceInverse">#E6E0E9</item> <item name="colorSurfaceInverse">#DDE4E4</item>
<item name="colorOnSurfaceInverse">#322F35</item> <item name="colorOnSurfaceInverse">#2D3131</item>
<!-- Outline --> <!-- Outline -->
<item name="colorOutline">#938F99</item> <item name="colorOutline">#889393</item>
<item name="colorOutlineVariant">#49454F</item> <item name="colorOutlineVariant">#3F4949</item>
<!-- Error (dark) --> <!-- Error (dark) -->
<item name="colorError">#F2B8B5</item> <item name="colorError">#F2B8B5</item>

View File

@ -3,37 +3,37 @@
<!-- Material Design 3 semantic color tokens --> <!-- Material Design 3 semantic color tokens -->
<!-- Primary palette --> <!-- Primary palette -->
<color name="primary">#6750A4</color> <color name="primary">#006A6C</color>
<color name="on_primary">#FFFFFF</color> <color name="on_primary">#FFFFFF</color>
<color name="primary_container">#EADDFF</color> <color name="primary_container">#6FF6F8</color>
<color name="on_primary_container">#21005D</color> <color name="on_primary_container">#002021</color>
<!-- Secondary palette --> <!-- Secondary palette -->
<color name="secondary">#625B71</color> <color name="secondary">#4A6363</color>
<color name="on_secondary">#FFFFFF</color> <color name="on_secondary">#FFFFFF</color>
<color name="secondary_container">#E8DEF8</color> <color name="secondary_container">#CCE8E7</color>
<color name="on_secondary_container">#1D192B</color> <color name="on_secondary_container">#051F1F</color>
<!-- Tertiary palette --> <!-- Tertiary palette -->
<color name="tertiary">#7D5260</color> <color name="tertiary">#4B617C</color>
<color name="on_tertiary">#FFFFFF</color> <color name="on_tertiary">#FFFFFF</color>
<color name="tertiary_container">#FFD8E4</color> <color name="tertiary_container">#D2E4FF</color>
<color name="on_tertiary_container">#31111D</color> <color name="on_tertiary_container">#051E35</color>
<!-- Surface colors --> <!-- Surface colors -->
<color name="surface">#FFFBFE</color> <color name="surface">#F4FBFB</color>
<color name="on_surface">#1D1B20</color> <color name="on_surface">#161D1D</color>
<color name="surface_variant">#E7E0EC</color> <color name="surface_variant">#DAE5E4</color>
<color name="on_surface_variant">#49454F</color> <color name="on_surface_variant">#3F4949</color>
<color name="surface_container_lowest">#FFFFFF</color> <color name="surface_container_lowest">#FFFFFF</color>
<color name="surface_container_low">#F5EFF7</color> <color name="surface_container_low">#EEF6F6</color>
<color name="surface_container">#F3EDF7</color> <color name="surface_container">#E8F0F0</color>
<color name="surface_container_high">#ECE6F0</color> <color name="surface_container_high">#E2EAEA</color>
<color name="surface_container_highest">#E6E0E9</color> <color name="surface_container_highest">#DDE5E5</color>
<!-- Outline --> <!-- Outline -->
<color name="outline">#79747E</color> <color name="outline">#6F7979</color>
<color name="outline_variant">#CAC4D0</color> <color name="outline_variant">#BEC9C8</color>
<!-- Status / Semantic colors --> <!-- Status / Semantic colors -->
<color name="success">#4CAF50</color> <color name="success">#4CAF50</color>
@ -57,9 +57,9 @@
<color name="on_info_container">#0B4576</color> <color name="on_info_container">#0B4576</color>
<!-- Inverse surface --> <!-- Inverse surface -->
<color name="inverse_surface">#322F35</color> <color name="inverse_surface">#2D3131</color>
<color name="inverse_on_surface">#F5EFF7</color> <color name="inverse_on_surface">#DFE4E4</color>
<color name="inverse_primary">#D0BCFF</color> <color name="inverse_primary">#4DD9DB</color>
<!-- Status indicator palettes (kept for programmatic tint compatibility) --> <!-- Status indicator palettes (kept for programmatic tint compatibility) -->
<color name="status_healthy">@color/success</color> <color name="status_healthy">@color/success</color>
@ -70,8 +70,8 @@
<color name="purple_200">@color/primary_container</color> <color name="purple_200">@color/primary_container</color>
<color name="purple_500">@color/primary</color> <color name="purple_500">@color/primary</color>
<color name="purple_700">@color/on_primary_container</color> <color name="purple_700">@color/on_primary_container</color>
<color name="teal_200">@color/secondary_container</color> <color name="teal_200">@color/primary_container</color>
<color name="teal_700">@color/secondary</color> <color name="teal_700">@color/primary</color>
<!-- Legacy neutral aliases kept for programmatic compatibility in unchanged Kotlin code --> <!-- Legacy neutral aliases kept for programmatic compatibility in unchanged Kotlin code -->
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>

View File

@ -4,6 +4,8 @@
<!-- Login --> <!-- Login -->
<string name="sign_in_with_google">Sign in with Google</string> <string name="sign_in_with_google">Sign in with Google</string>
<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="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
<!-- Main --> <!-- Main -->
<string name="app_title">resBuilder</string> <string name="app_title">resBuilder</string>
@ -75,4 +77,12 @@
<string name="error_state_message">We could not load this screen. Please try again.</string> <string name="error_state_message">We could not load this screen. Please try again.</string>
<string name="usage_limit_title">Free limit reached</string> <string name="usage_limit_title">Free limit reached</string>
<string name="result_preview">Result preview</string> <string name="result_preview">Result preview</string>
<!-- My Resumes -->
<string name="menu_my_resumes">My Resumes</string>
<string name="my_resumes_title">My Documents</string>
<string name="my_resumes_empty">No generated documents yet. Submit a job to see your results here.</string>
<string name="my_resumes_error">Could not load your documents. Please try again.</string>
<string name="type_resume">Resume</string>
<string name="type_cover_letter">Cover letter</string>
</resources> </resources>

View File

@ -10,6 +10,9 @@ okhttp = "4.12.0"
gson = "2.10.1" gson = "2.10.1"
browser = "1.7.0" browser = "1.7.0"
googlePlayServicesAuth = "20.7.0" googlePlayServicesAuth = "20.7.0"
credentialManager = "1.5.0-beta01"
googleid = "1.1.1"
kotlinxCoroutines = "1.9.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" }
@ -22,6 +25,10 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
browser = { group = "androidx.browser", name = "browser", version.ref = "browser" } browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayServicesAuth" } google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayServicesAuth" }
androidx-credential-manager = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" }
androidx-credential-manager-play-services = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialManager" }
googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleid" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }