Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c9553ad39
|
||
|
|
c589e5f9e4
|
Generated
+7
@@ -4,6 +4,13 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<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 />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
|
||||
Generated
+1
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<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" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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)
|
||||
@@ -41,6 +41,10 @@ dependencies {
|
||||
implementation(libs.gson)
|
||||
implementation(libs.browser)
|
||||
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)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
||||
@@ -62,3 +62,15 @@ data class AdminUser(
|
||||
data class AdminUsersResponse(
|
||||
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>
|
||||
)
|
||||
@@ -5,6 +5,7 @@ import com.example.resbuilder.data.model.BuildRequest
|
||||
import com.example.resbuilder.data.model.BuildResponse
|
||||
import com.example.resbuilder.data.model.HealthResponse
|
||||
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.UploadResponse
|
||||
import com.example.resbuilder.data.model.User
|
||||
@@ -17,6 +18,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
@@ -51,6 +53,8 @@ object ApiClient {
|
||||
bearerToken?.let { builder.header("Authorization", "Bearer $it") }
|
||||
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)
|
||||
.readTimeout(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 getLogoutUrl(): String = "$API_BASE/auth/logout"
|
||||
|
||||
|
||||
@@ -2,34 +2,52 @@ package com.example.resbuilder.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.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.SignInButton
|
||||
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.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() {
|
||||
|
||||
private lateinit var googleSignInButton: SignInButton
|
||||
private lateinit var progress: ProgressBar
|
||||
private lateinit var googleSignInButton: MaterialButton
|
||||
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>
|
||||
|
||||
// Same client ID registered in Google Cloud Console for the backend OAuth
|
||||
// Client IDs from Google Cloud Console
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -37,110 +55,144 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
googleSignInButton = findViewById(R.id.googleSignInButton)
|
||||
progress = findViewById(R.id.loginProgress)
|
||||
credentialManager = CredentialManager.create(this)
|
||||
|
||||
googleSignInButton.setOnClickListener { launchGoogleSignIn() }
|
||||
|
||||
googleSignInLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
|
||||
handleSignInResult(task)
|
||||
} else {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "Sign in cancelled", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Log.w(TAG, "Unexpected activity result: ${result.resultCode}")
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "Sign-in interrupted", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
showLoading(true)
|
||||
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) {
|
||||
progress.visibility = if (loading) View.VISIBLE else View.GONE
|
||||
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() {
|
||||
showLoading(true)
|
||||
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.requestIdToken(webClientId)
|
||||
.requestEmail()
|
||||
.build()
|
||||
val client = GoogleSignIn.getClient(this, gso)
|
||||
// Clear any previous sign-in to force account picker
|
||||
client.signOut().addOnCompleteListener {
|
||||
val signInIntent = client.signInIntent
|
||||
googleSignInLauncher.launch(signInIntent)
|
||||
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()
|
||||
.setFilterByAuthorizedAccounts(false)
|
||||
.setServerClientId(webClientId)
|
||||
.setAutoSelectEnabled(false)
|
||||
.setNonce(generateNonce())
|
||||
.build()
|
||||
|
||||
val request = GetCredentialRequest.Builder()
|
||||
.addCredentialOption(googleIdOption)
|
||||
.build()
|
||||
|
||||
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>) {
|
||||
try {
|
||||
val account = task.getResult(ApiException::class.java)
|
||||
val idToken = account?.idToken
|
||||
if (idToken.isNullOrEmpty()) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "No ID token from Google", Toast.LENGTH_LONG).show()
|
||||
return
|
||||
private fun handleCredentialResponse(response: GetCredentialResponse) {
|
||||
val credential = response.credential
|
||||
when {
|
||||
credential is CustomCredential && credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> {
|
||||
try {
|
||||
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
|
||||
val idToken = googleIdTokenCredential.idToken
|
||||
if (idToken.isNullOrEmpty()) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "No ID token from Google", Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
exchangeToken(idToken)
|
||||
} catch (e: GoogleIdTokenParsingException) {
|
||||
showLoading(false)
|
||||
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()
|
||||
}
|
||||
exchangeToken(idToken)
|
||||
} catch (e: ApiException) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, "Sign in failed: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSignInError(message: String) {
|
||||
showLoading(false)
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun exchangeToken(idToken: String) {
|
||||
Thread {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val token = ApiClient.exchangeGoogleToken(idToken)
|
||||
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
|
||||
ApiClient.setToken(token)
|
||||
val user = ApiClient.getMe()
|
||||
handler.post {
|
||||
showLoading(false)
|
||||
if (user.authenticated) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Welcome ${user.name ?: user.email}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
launchMain()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Server rejected the token",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
handler.post {
|
||||
showLoading(false)
|
||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||
showLoading(false)
|
||||
if (user.authenticated) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
e.message ?: "Token exchange failed",
|
||||
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()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchMain() {
|
||||
@@ -150,6 +202,12 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
override fun 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,13 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menuMyResumes -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, MyResumesFragment())
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
true
|
||||
}
|
||||
R.id.menuSignOut -> {
|
||||
signOut()
|
||||
true
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/banner_warning" />
|
||||
<corners android:radius="8dp" />
|
||||
<shape xmlns:android="http://schemas.android.com/apk"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?attr/colorWarningContainer" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?attr/colorSurfaceContainerHighest" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="12dp"
|
||||
android:height="12dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" />
|
||||
</vector>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,25 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/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:orientation="vertical">
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.AdminActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:elevation="0dp"
|
||||
app:liftOnScroll="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/adminToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||
android:background="?attr/colorSurface"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:title="@string/admin_title"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
app:titleCentered="true"
|
||||
app:titleTextAppearance="?attr/textAppearanceTitleLarge"
|
||||
app:titleTextColor="?attr/colorOnSurface" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/adminRecyclerView"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_weight="1"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/adminRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:contentDescription="@string/admin_title"
|
||||
android:padding="8dp" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,42 +1,146 @@
|
||||
<?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="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
android:background="?attr/colorSurface"
|
||||
tools:context=".ui.LoginActivity">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loginProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/loginTitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/app_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline3"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
app:layout_constraintBottom_toTopOf="@id/googleSignInButton"
|
||||
<!-- 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"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.gms.common.SignInButton
|
||||
android:id="@+id/googleSignInButton"
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guidelineTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.35" />
|
||||
|
||||
<!-- Main content card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/loginCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainer"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:cardElevation="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginTitle" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp">
|
||||
|
||||
<!-- App icon with elevation -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
app:cardBackgroundColor="?attr/colorPrimaryContainer"
|
||||
app:cardCornerRadius="22dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/cd_app_logo"
|
||||
android:padding="20dp"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_launcher_foreground"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@+id/loginTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/app_title"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
android:id="@+id/loginSubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/welcome_subtitle"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="24dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<!-- Sign in button container -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
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_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/sign_in_terms"
|
||||
android:textAlignment="center"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/guidelineTop"
|
||||
app:trackColor="?attr/colorPrimaryContainer" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:orientation="vertical"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true"
|
||||
app:elevation="0dp"
|
||||
app:liftOnScroll="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Dark"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap"
|
||||
app:title="@string/app_title"
|
||||
app:titleTextColor="@color/white">
|
||||
app:titleCentered="true"
|
||||
app:titleTextAppearance="?attr/textAppearanceTitleLarge"
|
||||
app:titleTextColor="?attr/colorOnSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/healthContainer"
|
||||
@@ -27,14 +35,18 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/colorSurfaceContainer"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:minHeight="32dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/healthIndicator"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:contentDescription="@string/cd_health_indicator"
|
||||
android:src="@drawable/ic_circle_status" />
|
||||
|
||||
<TextView
|
||||
@@ -43,35 +55,51 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/unknown_user"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usageBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:background="@drawable/bg_banner_warning"
|
||||
android:drawableStart="@drawable/ic_circle_status"
|
||||
android:drawableTint="?attr/colorWarning"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:padding="12dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/usage_limit_banner"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:contentDescription="@string/app_title"
|
||||
app:tabBackground="?attr/colorSurface"
|
||||
app:tabGravity="fill"
|
||||
app:tabIndicatorColor="?attr/colorPrimary"
|
||||
app:tabMode="fixed"
|
||||
app:tabTextAppearance="?attr/textAppearanceLabelLarge"
|
||||
app:tabTextColor="?attr/colorOnSurfaceVariant"
|
||||
app:tabSelectedTextColor="?attr/colorOnSurface" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usageBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/bg_banner_warning"
|
||||
android:padding="12dp"
|
||||
android:text="@string/usage_limit_banner"
|
||||
android:textColor="@color/banner_warning_text"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabGravity="fill"
|
||||
app:tabMode="fixed" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -1,111 +1,187 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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:fillViewport="true"
|
||||
android:padding="16dp">
|
||||
tools:context=".ui.JobFormFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_job_title"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/jobTitleInput"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hint_job_title"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/hint_seek_url"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/seekUrlInput"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_job_title"
|
||||
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/scrapeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/button_scrape" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/jobTitleInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:minHeight="48dp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/hint_seek_url"
|
||||
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/seekUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:minHeight="48dp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/scrapeButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/cd_scrape_job"
|
||||
android:minWidth="64dp"
|
||||
android:text="@string/button_scrape" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/hint_job_description"
|
||||
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/jobDescriptionInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="4" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_job_description"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/jobDescriptionInput"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="4" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/uploadButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/button_upload" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/docInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_your_resume"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
<TextView
|
||||
android:id="@+id/docInputTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/hint_your_resume"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/docInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="6" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/uploadButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:contentDescription="@string/cd_upload_document"
|
||||
android:text="@string/button_upload" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/docInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_your_resume"
|
||||
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/docInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="6" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/submitButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/generate_resume" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:contentDescription="@string/cd_loading"
|
||||
android:indeterminateTint="?attr/colorPrimary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<?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:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="4dp">
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
@@ -24,45 +30,53 @@
|
||||
android:id="@+id/statusBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:background="@drawable/bg_banner_warning"
|
||||
android:background="@drawable/bg_status_badge"
|
||||
android:contentDescription="@string/status_queued"
|
||||
android:gravity="center"
|
||||
android:minHeight="32dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:text="@string/status_queued"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textStyle="bold" />
|
||||
android:textAppearance="?attr/textAppearanceLabelLarge"
|
||||
android:textColor="?attr/colorOnPrimary" />
|
||||
|
||||
<ProgressBar
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/statusProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="24dp" />
|
||||
android:layout_marginTop="24dp"
|
||||
android:contentDescription="@string/content_generation_running"
|
||||
android:indeterminate="true"
|
||||
app:indicatorColor="?attr/colorPrimary"
|
||||
app:trackColor="?attr/colorSurfaceContainerHighest" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/job_wait_message"
|
||||
android:textAppearance="?attr/textAppearanceBody1" />
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusDetail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tryAgainButton"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:minWidth="120dp"
|
||||
android:text="@string/try_again"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,21 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/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:orientation="vertical">
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.ResultFragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="12dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
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:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
@@ -23,11 +31,15 @@
|
||||
android:id="@+id/resultBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_banner_warning"
|
||||
android:background="@drawable/bg_status_badge"
|
||||
android:contentDescription="@string/status_completed"
|
||||
android:gravity="center"
|
||||
android:minHeight="32dp"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:text="@string/status_completed"
|
||||
android:textStyle="bold" />
|
||||
android:textAppearance="?attr/textAppearanceLabelLarge"
|
||||
android:textColor="?attr/colorOnPrimary" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
@@ -36,16 +48,21 @@
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/copyButton"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:contentDescription="@string/cd_copy_result"
|
||||
android:minWidth="64dp"
|
||||
android:text="@string/copy_text" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/newButton"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/cd_new_job"
|
||||
android:minWidth="64dp"
|
||||
android:text="@string/new_job" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -54,31 +71,46 @@
|
||||
android:id="@+id/resultWebView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_weight="1" />
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/result_preview" />
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="12dp"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pdfButton"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/export_pdf" />
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/docxButton"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/export_docx" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pdfButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/cd_export_pdf"
|
||||
android:text="@string/export_pdf" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/docxButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/cd_export_docx"
|
||||
android:text="@string/export_docx" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="4dp"
|
||||
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:gravity="center_vertical"
|
||||
android:minHeight="64dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
@@ -23,28 +28,35 @@
|
||||
android:id="@+id/userEmail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBody1"
|
||||
android:textStyle="bold" />
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userUsage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/actionButton"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/block_user" />
|
||||
android:contentDescription="@string/cd_admin_action"
|
||||
android:minWidth="72dp"
|
||||
android:text="@string/block_user"
|
||||
android:textColor="?attr/colorError" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/menuMyResumes"
|
||||
android:title="@string/menu_my_resumes" />
|
||||
<item
|
||||
android:id="@+id/menuAdminPanel"
|
||||
android:title="@string/menu_admin_panel"
|
||||
|
||||
@@ -1,16 +1,90 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.ResBuilder" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Primary color roles (dark) -->
|
||||
<item name="colorPrimary">#4DD9DB</item>
|
||||
<item name="colorOnPrimary">#003738</item>
|
||||
<item name="colorPrimaryContainer">#004F51</item>
|
||||
<item name="colorOnPrimaryContainer">#6FF6F8</item>
|
||||
<item name="colorPrimaryInverse">#006A6C</item>
|
||||
|
||||
<!-- Secondary color roles (dark) -->
|
||||
<item name="colorSecondary">#B0CCCB</item>
|
||||
<item name="colorOnSecondary">#1B3535</item>
|
||||
<item name="colorSecondaryContainer">#324C4B</item>
|
||||
<item name="colorOnSecondaryContainer">#CCE8E7</item>
|
||||
|
||||
<!-- Tertiary color roles (dark) -->
|
||||
<item name="colorTertiary">#B1C8E8</item>
|
||||
<item name="colorOnTertiary">#19324C</item>
|
||||
<item name="colorTertiaryContainer">#324963</item>
|
||||
<item name="colorOnTertiaryContainer">#D2E4FF</item>
|
||||
|
||||
<!-- Surface color roles (dark) -->
|
||||
<item name="android:colorBackground">#0E1515</item>
|
||||
<item name="colorSurface">#0E1515</item>
|
||||
<item name="colorOnSurface">#DDE4E4</item>
|
||||
<item name="colorSurfaceVariant">#3F4949</item>
|
||||
<item name="colorOnSurfaceVariant">#BEC9C8</item>
|
||||
<item name="colorSurfaceContainerLowest">#090F0F</item>
|
||||
<item name="colorSurfaceContainerLow">#161D1D</item>
|
||||
<item name="colorSurfaceContainer">#1A2121</item>
|
||||
<item name="colorSurfaceContainerHigh">#242B2B</item>
|
||||
<item name="colorSurfaceContainerHighest">#2F3636</item>
|
||||
<item name="colorSurfaceInverse">#DDE4E4</item>
|
||||
<item name="colorOnSurfaceInverse">#2D3131</item>
|
||||
|
||||
<!-- Outline -->
|
||||
<item name="colorOutline">#889393</item>
|
||||
<item name="colorOutlineVariant">#3F4949</item>
|
||||
|
||||
<!-- Error (dark) -->
|
||||
<item name="colorError">#F2B8B5</item>
|
||||
<item name="colorOnError">#601410</item>
|
||||
<item name="colorErrorContainer">#8C1D18</item>
|
||||
<item name="colorOnErrorContainer">#F9DEDC</item>
|
||||
|
||||
<!-- Status and custom semantic color attrs (dark) -->
|
||||
<item name="colorSuccess">#81C784</item>
|
||||
<item name="colorOnSuccess">#003D00</item>
|
||||
<item name="colorSuccessContainer">#1B5E1F</item>
|
||||
<item name="colorOnSuccessContainer">#D9F6DA</item>
|
||||
|
||||
<item name="colorWarning">#FFB74D</item>
|
||||
<item name="colorOnWarning">#4A2C00</item>
|
||||
<item name="colorWarningContainer">#693B00</item>
|
||||
<item name="colorOnWarningContainer">#FFE3B6</item>
|
||||
|
||||
<item name="colorInfo">#64B5F6</item>
|
||||
<item name="colorOnInfo">#0B4576</item>
|
||||
<item name="colorInfoContainer">#0B4576</item>
|
||||
<item name="colorOnInfoContainer">#D4EAFB</item>
|
||||
|
||||
<!-- System bars - edge-to-edge ready (dark) -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
|
||||
<!-- M3 Typography and shape defaults inherited from Material3 parent -->
|
||||
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
|
||||
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
|
||||
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
|
||||
<item name="textAppearanceHeadlineLarge">@style/TextAppearance.Material3.HeadlineLarge</item>
|
||||
<item name="textAppearanceHeadlineMedium">@style/TextAppearance.Material3.HeadlineMedium</item>
|
||||
<item name="textAppearanceHeadlineSmall">@style/TextAppearance.Material3.HeadlineSmall</item>
|
||||
<item name="textAppearanceTitleLarge">@style/TextAppearance.Material3.TitleLarge</item>
|
||||
<item name="textAppearanceTitleMedium">@style/TextAppearance.Material3.TitleMedium</item>
|
||||
<item name="textAppearanceTitleSmall">@style/TextAppearance.Material3.TitleSmall</item>
|
||||
<item name="textAppearanceBodyLarge">@style/TextAppearance.Material3.BodyLarge</item>
|
||||
<item name="textAppearanceBodyMedium">@style/TextAppearance.Material3.BodyMedium</item>
|
||||
<item name="textAppearanceBodySmall">@style/TextAppearance.Material3.BodySmall</item>
|
||||
<item name="textAppearanceLabelLarge">@style/TextAppearance.Material3.LabelLarge</item>
|
||||
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
|
||||
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
|
||||
|
||||
<!-- Default M3 component styles inherited from Material3 parent; explicit overrides kept to a minimum -->
|
||||
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
|
||||
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
|
||||
|
||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Semantic status color attributes used across ResBuilder UI -->
|
||||
<attr name="colorSuccess" format="color" />
|
||||
<attr name="colorOnSuccess" format="color" />
|
||||
<attr name="colorSuccessContainer" format="color" />
|
||||
<attr name="colorOnSuccessContainer" format="color" />
|
||||
|
||||
<attr name="colorWarning" format="color" />
|
||||
<attr name="colorOnWarning" format="color" />
|
||||
<attr name="colorWarningContainer" format="color" />
|
||||
<attr name="colorOnWarningContainer" format="color" />
|
||||
|
||||
<attr name="colorInfo" format="color" />
|
||||
<attr name="colorOnInfo" format="color" />
|
||||
<attr name="colorInfoContainer" format="color" />
|
||||
<attr name="colorOnInfoContainer" format="color" />
|
||||
</resources>
|
||||
@@ -1,20 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<!-- Material Design 3 semantic color tokens -->
|
||||
|
||||
<!-- Primary palette -->
|
||||
<color name="primary">#006A6C</color>
|
||||
<color name="on_primary">#FFFFFF</color>
|
||||
<color name="primary_container">#6FF6F8</color>
|
||||
<color name="on_primary_container">#002021</color>
|
||||
|
||||
<!-- Secondary palette -->
|
||||
<color name="secondary">#4A6363</color>
|
||||
<color name="on_secondary">#FFFFFF</color>
|
||||
<color name="secondary_container">#CCE8E7</color>
|
||||
<color name="on_secondary_container">#051F1F</color>
|
||||
|
||||
<!-- Tertiary palette -->
|
||||
<color name="tertiary">#4B617C</color>
|
||||
<color name="on_tertiary">#FFFFFF</color>
|
||||
<color name="tertiary_container">#D2E4FF</color>
|
||||
<color name="on_tertiary_container">#051E35</color>
|
||||
|
||||
<!-- Surface colors -->
|
||||
<color name="surface">#F4FBFB</color>
|
||||
<color name="on_surface">#161D1D</color>
|
||||
<color name="surface_variant">#DAE5E4</color>
|
||||
<color name="on_surface_variant">#3F4949</color>
|
||||
<color name="surface_container_lowest">#FFFFFF</color>
|
||||
<color name="surface_container_low">#EEF6F6</color>
|
||||
<color name="surface_container">#E8F0F0</color>
|
||||
<color name="surface_container_high">#E2EAEA</color>
|
||||
<color name="surface_container_highest">#DDE5E5</color>
|
||||
|
||||
<!-- Outline -->
|
||||
<color name="outline">#6F7979</color>
|
||||
<color name="outline_variant">#BEC9C8</color>
|
||||
|
||||
<!-- Status / Semantic colors -->
|
||||
<color name="success">#4CAF50</color>
|
||||
<color name="on_success">#FFFFFF</color>
|
||||
<color name="success_container">#D9F6DA</color>
|
||||
<color name="on_success_container">#1F5E22</color>
|
||||
|
||||
<color name="warning">#FF9800</color>
|
||||
<color name="on_warning">#FFFFFF</color>
|
||||
<color name="warning_container">#FFE3B6</color>
|
||||
<color name="on_warning_container">#693B00</color>
|
||||
|
||||
<color name="error">#F44336</color>
|
||||
<color name="on_error">#FFFFFF</color>
|
||||
<color name="error_container">#F9DEDC</color>
|
||||
<color name="on_error_container">#410E0B</color>
|
||||
|
||||
<color name="info">#2196F3</color>
|
||||
<color name="on_info">#FFFFFF</color>
|
||||
<color name="info_container">#D4EAFB</color>
|
||||
<color name="on_info_container">#0B4576</color>
|
||||
|
||||
<!-- Inverse surface -->
|
||||
<color name="inverse_surface">#2D3131</color>
|
||||
<color name="inverse_on_surface">#DFE4E4</color>
|
||||
<color name="inverse_primary">#4DD9DB</color>
|
||||
|
||||
<!-- Status indicator palettes (kept for programmatic tint compatibility) -->
|
||||
<color name="status_healthy">@color/success</color>
|
||||
<color name="status_degraded">@color/warning</color>
|
||||
<color name="status_unhealthy">@color/error</color>
|
||||
|
||||
<!-- Legacy aliases to avoid breaking any remaining references during migration -->
|
||||
<color name="purple_200">@color/primary_container</color>
|
||||
<color name="purple_500">@color/primary</color>
|
||||
<color name="purple_700">@color/on_primary_container</color>
|
||||
<color name="teal_200">@color/primary_container</color>
|
||||
<color name="teal_700">@color/primary</color>
|
||||
<!-- Legacy neutral aliases kept for programmatic compatibility in unchanged Kotlin code -->
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="status_healthy">#FF4CAF50</color>
|
||||
<color name="status_degraded">#FFFFC107</color>
|
||||
<color name="status_unhealthy">#FFF44336</color>
|
||||
<color name="banner_warning">#FFFFF3E0</color>
|
||||
<color name="banner_warning_text">#FFE65100</color>
|
||||
<color name="status_badge_queued">#FF9E9E9E</color>
|
||||
<color name="status_badge_running">#FF2196F3</color>
|
||||
<color name="status_badge_completed">#FF4CAF50</color>
|
||||
<color name="status_badge_failed">#FFF44336</color>
|
||||
<color name="banner_warning">@color/warning_container</color>
|
||||
<color name="banner_warning_text">@color/on_warning_container</color>
|
||||
|
||||
<color name="status_badge_queued">@color/surface_container_highest</color>
|
||||
<color name="status_badge_running">@color/info</color>
|
||||
<color name="status_badge_completed">@color/success</color>
|
||||
<color name="status_badge_failed">@color/error</color>
|
||||
</resources>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<!-- Login -->
|
||||
<string name="sign_in_with_google">Sign in with Google</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 -->
|
||||
<string name="app_title">resBuilder</string>
|
||||
@@ -45,6 +47,19 @@
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="nothing_to_copy">Nothing to copy</string>
|
||||
|
||||
<!-- Accessibility labels -->
|
||||
<string name="cd_app_logo">resBuilder app logo</string>
|
||||
<string name="cd_google_sign_in">Sign in with Google</string>
|
||||
<string name="cd_loading">Loading</string>
|
||||
<string name="cd_health_indicator">Service health indicator</string>
|
||||
<string name="cd_scrape_job">Scrape job description from SEEK URL</string>
|
||||
<string name="cd_upload_document">Upload existing resume or cover letter</string>
|
||||
<string name="cd_copy_result">Copy generated text to clipboard</string>
|
||||
<string name="cd_new_job">Start a new job</string>
|
||||
<string name="cd_export_pdf">Export as PDF</string>
|
||||
<string name="cd_export_docx">Export as DOCX</string>
|
||||
<string name="cd_admin_action">Toggle user block status</string>
|
||||
|
||||
<!-- Admin -->
|
||||
<string name="admin_title">Admin Panel</string>
|
||||
<string name="block_user">Block</string>
|
||||
@@ -52,4 +67,22 @@
|
||||
<string name="admin_action_success">Action completed</string>
|
||||
<string name="admin_action_failed">Action failed</string>
|
||||
<string name="usage_format">%1$d / %2$d</string>
|
||||
|
||||
<!-- Screen reader / empty / loading labels -->
|
||||
<string name="content_generation_running">Content generation is running</string>
|
||||
<string name="estimated_time">Estimated: %1$s</string>
|
||||
<string name="empty_state_title">Nothing here yet</string>
|
||||
<string name="empty_state_message">Submit a job to see results.</string>
|
||||
<string name="error_state_title">Something went wrong</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="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>
|
||||
|
||||
@@ -1,16 +1,92 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.ResBuilder" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Primary color roles -->
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorOnPrimary">@color/on_primary</item>
|
||||
<item name="colorPrimaryContainer">@color/primary_container</item>
|
||||
<item name="colorOnPrimaryContainer">@color/on_primary_container</item>
|
||||
<item name="colorPrimaryInverse">@color/inverse_primary</item>
|
||||
|
||||
<!-- Secondary color roles -->
|
||||
<item name="colorSecondary">@color/secondary</item>
|
||||
<item name="colorOnSecondary">@color/on_secondary</item>
|
||||
<item name="colorSecondaryContainer">@color/secondary_container</item>
|
||||
<item name="colorOnSecondaryContainer">@color/on_secondary_container</item>
|
||||
|
||||
<!-- Tertiary color roles -->
|
||||
<item name="colorTertiary">@color/tertiary</item>
|
||||
<item name="colorOnTertiary">@color/on_tertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/tertiary_container</item>
|
||||
<item name="colorOnTertiaryContainer">@color/on_tertiary_container</item>
|
||||
|
||||
<!-- Surface color roles -->
|
||||
<item name="android:colorBackground">@color/surface</item>
|
||||
<item name="colorSurface">@color/surface</item>
|
||||
<item name="colorOnSurface">@color/on_surface</item>
|
||||
<item name="colorSurfaceVariant">@color/surface_variant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/on_surface_variant</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/surface_container_lowest</item>
|
||||
<item name="colorSurfaceContainerLow">@color/surface_container_low</item>
|
||||
<item name="colorSurfaceContainer">@color/surface_container</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/surface_container_high</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/surface_container_highest</item>
|
||||
<item name="colorSurfaceInverse">@color/inverse_surface</item>
|
||||
<item name="colorOnSurfaceInverse">@color/inverse_on_surface</item>
|
||||
|
||||
<!-- Outline -->
|
||||
<item name="colorOutline">@color/outline</item>
|
||||
<item name="colorOutlineVariant">@color/outline_variant</item>
|
||||
|
||||
<!-- Error -->
|
||||
<item name="colorError">@color/error</item>
|
||||
<item name="colorOnError">@color/on_error</item>
|
||||
<item name="colorErrorContainer">@color/error_container</item>
|
||||
<item name="colorOnErrorContainer">@color/on_error_container</item>
|
||||
|
||||
<!-- Status and custom semantic color attrs -->
|
||||
<item name="colorSuccess">@color/success</item>
|
||||
<item name="colorOnSuccess">@color/on_success</item>
|
||||
<item name="colorSuccessContainer">@color/success_container</item>
|
||||
<item name="colorOnSuccessContainer">@color/on_success_container</item>
|
||||
|
||||
<item name="colorWarning">@color/warning</item>
|
||||
<item name="colorOnWarning">@color/on_warning</item>
|
||||
<item name="colorWarningContainer">@color/warning_container</item>
|
||||
<item name="colorOnWarningContainer">@color/on_warning_container</item>
|
||||
|
||||
<item name="colorInfo">@color/info</item>
|
||||
<item name="colorOnInfo">@color/on_info</item>
|
||||
<item name="colorInfoContainer">@color/info_container</item>
|
||||
<item name="colorOnInfoContainer">@color/on_info_container</item>
|
||||
|
||||
<!-- System bars - edge-to-edge ready -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
|
||||
<!-- M3 Typography and shape defaults inherited from Material3 parent -->
|
||||
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
|
||||
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
|
||||
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
|
||||
<item name="textAppearanceHeadlineLarge">@style/TextAppearance.Material3.HeadlineLarge</item>
|
||||
<item name="textAppearanceHeadlineMedium">@style/TextAppearance.Material3.HeadlineMedium</item>
|
||||
<item name="textAppearanceHeadlineSmall">@style/TextAppearance.Material3.HeadlineSmall</item>
|
||||
<item name="textAppearanceTitleLarge">@style/TextAppearance.Material3.TitleLarge</item>
|
||||
<item name="textAppearanceTitleMedium">@style/TextAppearance.Material3.TitleMedium</item>
|
||||
<item name="textAppearanceTitleSmall">@style/TextAppearance.Material3.TitleSmall</item>
|
||||
<item name="textAppearanceBodyLarge">@style/TextAppearance.Material3.BodyLarge</item>
|
||||
<item name="textAppearanceBodyMedium">@style/TextAppearance.Material3.BodyMedium</item>
|
||||
<item name="textAppearanceBodySmall">@style/TextAppearance.Material3.BodySmall</item>
|
||||
<item name="textAppearanceLabelLarge">@style/TextAppearance.Material3.LabelLarge</item>
|
||||
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
|
||||
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
|
||||
|
||||
<!-- Default M3 component styles inherited from Material3 parent; explicit overrides kept to a minimum -->
|
||||
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
|
||||
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
|
||||
|
||||
<!-- Window background uses surface so app surfaces are always theme-aware -->
|
||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -10,6 +10,9 @@ okhttp = "4.12.0"
|
||||
gson = "2.10.1"
|
||||
browser = "1.7.0"
|
||||
googlePlayServicesAuth = "20.7.0"
|
||||
credentialManager = "1.5.0-beta01"
|
||||
googleid = "1.1.1"
|
||||
kotlinxCoroutines = "1.9.0"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
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" }
|
||||
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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user