Compare commits

..

No commits in common. "master" and "design/material3-redesign" have entirely different histories.

52 changed files with 684 additions and 1687 deletions

7
.gitignore vendored
View File

@ -1,7 +1,12 @@
*.iml *.iml
.gradle .gradle
/local.properties /local.properties
/.idea/ /.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build /build
/captures /captures

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
resBuilder

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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

11
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

17
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?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">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

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

View File

@ -18,25 +18,15 @@ android {
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE", "\"https://resume.aridgwayweb.com\"")
} }
buildTypes { buildTypes {
release { release {
isMinifyEnabled = true
optimization { optimization {
enable = false enable = false
} }
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
} }
} }
buildFeatures {
buildConfig = true
}
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
@ -46,18 +36,11 @@ android {
dependencies { dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.material) implementation(libs.material)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.gson) implementation(libs.gson)
implementation(libs.browser) implementation(libs.browser)
implementation(libs.google.play.services.auth) implementation(libs.google.play.services.auth)
implementation(libs.androidx.credential.manager)
implementation(libs.androidx.credential.manager.play.services)
implementation(libs.googleid)
implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -1,11 +0,0 @@
# R8/ProGuard rules for resbuilder_app.
# Gson reflects over the API data models. Keep their fields intact.
-keep class com.example.resbuilder.data.model.** { <fields>; }
# Keep the OkHttp client class and its members (OkHttp ships its own consumer rules,
# but ApiClient is referenced reflectively nowhere; keep it to be safe).
-keep class com.example.resbuilder.data.remote.ApiClient { *; }
# Credential Manager / Google Identity libs register via reflection.
-keep class com.google.android.libraries.identity.googleid.** { *; }

View File

@ -11,7 +11,6 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.ResBuilder" android:theme="@style/Theme.ResBuilder"
@ -34,16 +33,6 @@
android:name=".ui.AdminActivity" android:name=".ui.AdminActivity"
android:exported="false" /> android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

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

View File

@ -5,7 +5,6 @@ import com.example.resbuilder.data.model.BuildRequest
import com.example.resbuilder.data.model.BuildResponse import com.example.resbuilder.data.model.BuildResponse
import com.example.resbuilder.data.model.HealthResponse import com.example.resbuilder.data.model.HealthResponse
import com.example.resbuilder.data.model.Job import com.example.resbuilder.data.model.Job
import com.example.resbuilder.data.model.JobListResponse
import com.example.resbuilder.data.model.ScrapeResponse import com.example.resbuilder.data.model.ScrapeResponse
import com.example.resbuilder.data.model.UploadResponse import com.example.resbuilder.data.model.UploadResponse
import com.example.resbuilder.data.model.User import com.example.resbuilder.data.model.User
@ -18,7 +17,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -28,7 +26,7 @@ import java.util.concurrent.TimeUnit
object ApiClient { object ApiClient {
internal const val API_BASE = com.example.resbuilder.BuildConfig.API_BASE internal const val API_BASE = "https://resume.aridgwayweb.com"
private val gson = Gson() private val gson = Gson()
private val cookieStore = mutableListOf<Cookie>() private val cookieStore = mutableListOf<Cookie>()
@ -53,8 +51,6 @@ object ApiClient {
bearerToken?.let { builder.header("Authorization", "Bearer $it") } bearerToken?.let { builder.header("Authorization", "Bearer $it") }
chain.proceed(builder.build()) chain.proceed(builder.build())
} }
// HTTP/1.1 only: backend/proxy resets HTTP/2 streams mid-response.
.protocols(listOf(Protocol.HTTP_1_1))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS)
@ -119,24 +115,6 @@ object ApiClient {
} }
} }
@Throws(IOException::class)
fun getMyJobs(): JobListResponse {
val req = Request.Builder()
.url("$API_BASE/jobs")
.build()
client.newCall(req).execute().use { res ->
if (!res.isSuccessful) {
val errBody = res.body?.string()
val detail = try {
gson.fromJson(errBody, Map::class.java)["detail"] as? String
} catch (_: Exception) { null }
throw IOException(detail ?: "Failed to load your documents")
}
val body = res.body?.string() ?: throw IOException("Empty response")
return gson.fromJson(body, JobListResponse::class.java)
}
}
fun getLoginUrl(): String = "$API_BASE/auth/login" fun getLoginUrl(): String = "$API_BASE/auth/login"
fun getLogoutUrl(): String = "$API_BASE/auth/logout" fun getLogoutUrl(): String = "$API_BASE/auth/logout"
@ -313,21 +291,4 @@ object ApiClient {
fun getExportPdfUrl(jobId: String): String = "$API_BASE/export/$jobId/pdf" fun getExportPdfUrl(jobId: String): String = "$API_BASE/export/$jobId/pdf"
fun getExportDocxUrl(jobId: String): String = "$API_BASE/export/$jobId/docx" fun getExportDocxUrl(jobId: String): String = "$API_BASE/export/$jobId/docx"
@Throws(IOException::class)
fun downloadExport(url: String): ByteArray {
val req = Request.Builder()
.url(url)
.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 ?: "Export failed")
}
return res.body?.bytes() ?: throw IOException("Empty response")
}
}
} }

View File

@ -147,7 +147,7 @@ class JobFormFragment : Fragment() {
private fun scrapeUrl() { private fun scrapeUrl() {
val url = seekUrlInput.text?.toString()?.trim() ?: "" val url = seekUrlInput.text?.toString()?.trim() ?: ""
if (url.isEmpty()) { if (url.isEmpty()) {
seekUrlInput.error = getString(R.string.enter_url) seekUrlInput.error = "Enter a URL"
return return
} }
showLoading(true) showLoading(true)

View File

@ -2,46 +2,34 @@ package com.example.resbuilder.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialException
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import androidx.activity.viewModels
import com.example.resbuilder.R import com.example.resbuilder.R
import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.example.resbuilder.data.remote.ApiClient
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.material.button.MaterialButton import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.gms.common.SignInButton
import kotlinx.coroutines.launch import com.google.android.gms.common.api.ApiException
import java.security.SecureRandom import com.google.android.gms.tasks.Task
import java.util.Base64
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels() private lateinit var googleSignInButton: SignInButton
private lateinit var progress: ProgressBar
private lateinit var googleSignInButton: MaterialButton private val handler = Handler(Looper.getMainLooper())
private lateinit var progress: CircularProgressIndicator
private lateinit var credentialManager: CredentialManager
private lateinit var googleSignInLauncher: ActivityResultLauncher<IntentSenderRequest> private lateinit var googleSignInLauncher: ActivityResultLauncher<Intent>
// Client IDs from Google Cloud Console // Same client ID registered in Google Cloud Console for the backend OAuth
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com" private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com"
private val androidClientId = "416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -49,45 +37,23 @@ class LoginActivity : AppCompatActivity() {
googleSignInButton = findViewById(R.id.googleSignInButton) googleSignInButton = findViewById(R.id.googleSignInButton)
progress = findViewById(R.id.loginProgress) progress = findViewById(R.id.loginProgress)
credentialManager = CredentialManager.create(this)
googleSignInButton.setOnClickListener { launchGoogleSignIn() } googleSignInButton.setOnClickListener { launchGoogleSignIn() }
googleSignInLauncher = registerForActivityResult( googleSignInLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
if (result.resultCode == RESULT_OK && result.data != null) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
handleSignInResult(task)
} else {
showLoading(false) showLoading(false)
Toast.makeText(this, R.string.sign_in_interrupted, Toast.LENGTH_SHORT).show() Toast.makeText(this, "Sign in cancelled", Toast.LENGTH_SHORT).show()
}
} }
observeAuthState()
showLoading(true) showLoading(true)
viewModel.checkExistingAuth() checkExistingAuth()
}
private fun observeAuthState() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
showLoading(state.loading)
state.error?.let {
Toast.makeText(this@LoginActivity, it, Toast.LENGTH_LONG).show()
viewModel.setLoading(false)
}
state.user?.let { user ->
if (state.authenticated) {
Toast.makeText(
this@LoginActivity,
getString(R.string.welcome_user, user.name ?: user.email),
Toast.LENGTH_SHORT
).show()
launchMain()
}
}
}
}
}
} }
private fun showLoading(loading: Boolean) { private fun showLoading(loading: Boolean) {
@ -95,61 +61,86 @@ class LoginActivity : AppCompatActivity() {
googleSignInButton.isEnabled = !loading googleSignInButton.isEnabled = !loading
} }
private fun checkExistingAuth() {
Thread {
try {
val user = ApiClient.getMe()
handler.post {
showLoading(false)
if (user.authenticated) {
launchMain()
}
}
} catch (_: Exception) {
handler.post { showLoading(false) }
}
}.start()
}
private fun launchGoogleSignIn() { private fun launchGoogleSignIn() {
showLoading(true) showLoading(true)
val googleIdOption = GetGoogleIdOption.Builder() val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.setFilterByAuthorizedAccounts(false) .requestIdToken(webClientId)
.setServerClientId(webClientId) .requestEmail()
.setAutoSelectEnabled(false)
.setNonce(generateNonce())
.build() .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)
}
}
val request = GetCredentialRequest.Builder() private fun handleSignInResult(task: Task<GoogleSignInAccount>) {
.addCredentialOption(googleIdOption)
.build()
viewModel.viewModelScope.launch {
try { try {
val result = credentialManager.getCredential( val account = task.getResult(ApiException::class.java)
request = request, val idToken = account?.idToken
context = this@LoginActivity
)
handleCredentialResponse(result)
} catch (e: GetCredentialException) {
showLoading(false)
val errorMsg = when (e) {
is androidx.credentials.exceptions.NoCredentialException -> getString(R.string.no_google_accounts)
is androidx.credentials.exceptions.GetCredentialCancellationException -> getString(R.string.sign_in_cancelled)
else -> getString(R.string.sign_in_failed, e.message)
}
Toast.makeText(this@LoginActivity, errorMsg, Toast.LENGTH_LONG).show()
}
}
}
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()) { if (idToken.isNullOrEmpty()) {
showLoading(false) showLoading(false)
Toast.makeText(this, R.string.no_id_token, Toast.LENGTH_LONG).show() Toast.makeText(this, "No ID token from Google", Toast.LENGTH_LONG).show()
return return
} }
viewModel.exchangeToken(idToken) exchangeToken(idToken)
} catch (e: GoogleIdTokenParsingException) { } catch (e: ApiException) {
showLoading(false) showLoading(false)
Toast.makeText(this, getString(R.string.invalid_google_id_token, e.message), Toast.LENGTH_LONG).show() Toast.makeText(this, "Sign in failed: ${e.message}", Toast.LENGTH_LONG).show()
} }
} }
else -> {
private fun exchangeToken(idToken: String) {
Thread {
try {
val token = ApiClient.exchangeGoogleToken(idToken)
ApiClient.setToken(token)
val user = ApiClient.getMe()
handler.post {
showLoading(false) showLoading(false)
Toast.makeText(this, R.string.unexpected_credential_type, Toast.LENGTH_LONG).show() 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)
Toast.makeText(
this,
e.message ?: "Token exchange failed",
Toast.LENGTH_LONG
).show()
}
}
}.start()
} }
private fun launchMain() { private fun launchMain() {
@ -157,10 +148,8 @@ class LoginActivity : AppCompatActivity() {
finish() finish()
} }
private fun generateNonce(): String { override fun onDestroy() {
val secureRandom = SecureRandom() super.onDestroy()
val bytes = ByteArray(32) handler.removeCallbacksAndMessages(null)
secureRandom.nextBytes(bytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
} }
} }

View File

@ -1,62 +0,0 @@
package com.example.resbuilder.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.resbuilder.data.model.User
import com.example.resbuilder.data.remote.ApiClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class LoginUiState(
val loading: Boolean = false,
val authenticated: Boolean = false,
val user: User? = null,
val error: String? = null
)
class LoginViewModel : ViewModel() {
private val _state = MutableStateFlow(LoginUiState())
val state: StateFlow<LoginUiState> = _state.asStateFlow()
fun checkExistingAuth() {
viewModelScope.launch {
try {
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
_state.value = LoginUiState(loading = false, authenticated = user.authenticated, user = user)
} catch (_: Exception) {
_state.value = LoginUiState(loading = false, error = "check auth failed")
}
}
}
fun exchangeToken(idToken: String) {
viewModelScope.launch {
_state.value = _state.value.copy(loading = true, error = null)
try {
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
ApiClient.setToken(token)
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
_state.value = LoginUiState(
loading = false,
authenticated = user.authenticated,
user = user
)
} catch (e: Exception) {
_state.value = LoginUiState(loading = false, error = e.message ?: "Token exchange failed")
}
}
}
fun setLoading(loading: Boolean) {
_state.value = _state.value.copy(loading = loading)
}
fun consumeAuth() {
_state.value = _state.value.copy(authenticated = false)
}
}

View File

@ -5,6 +5,7 @@ import android.graphics.PorterDuff
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@ -12,21 +13,18 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.example.resbuilder.R import com.example.resbuilder.R
import com.example.resbuilder.data.model.User import com.example.resbuilder.data.model.User
import com.example.resbuilder.data.remote.ApiClient import com.example.resbuilder.data.remote.ApiClient
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.navigation.NavigationView import com.google.android.material.tabs.TabLayout
class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, ResultFragment.OnNewListener { class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, ResultFragment.OnNewListener {
private lateinit var drawerLayout: DrawerLayout
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private lateinit var navView: NavigationView private lateinit var tabLayout: TabLayout
private lateinit var healthIndicator: ImageView private lateinit var healthIndicator: ImageView
private lateinit var userName: TextView private lateinit var userName: TextView
private lateinit var userEmail: TextView
private lateinit var usageBanner: TextView private lateinit var usageBanner: TextView
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@ -34,65 +32,43 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
private var healthRunnable: Runnable? = null private var healthRunnable: Runnable? = null
companion object { companion object {
private const val NAV_BUILD_RESUME = 0 private const val TAB_RESUME = 0
private const val NAV_BUILD_COVER_LETTER = 1 private const val TAB_COVER = 1
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawerLayout)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
navView = findViewById(R.id.navView) tabLayout = findViewById(R.id.tabLayout)
healthIndicator = findViewById(R.id.healthIndicator)
userName = findViewById(R.id.userName)
usageBanner = findViewById(R.id.usageBanner) usageBanner = findViewById(R.id.usageBanner)
val header = navView.getHeaderView(0)
healthIndicator = header.findViewById(R.id.healthIndicator)
userName = header.findViewById(R.id.userName)
userEmail = header.findViewById(R.id.userEmail)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_hamburger)
navView.setNavigationItemSelectedListener { item -> tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_build_resume))
drawerLayout.closeDrawers() tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_build_cover_letter))
handleNavItem(item.itemId)
true tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
switchFormTab(tab?.position ?: TAB_RESUME)
} }
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {
switchFormTab(tab?.position ?: TAB_RESUME)
}
})
loadUser() loadUser()
startHealthPolling() startHealthPolling()
if (savedInstanceState == null) { if (savedInstanceState == null) {
showBuildForm(NAV_BUILD_RESUME) switchFormTab(TAB_RESUME)
} }
} }
private fun handleNavItem(itemId: Int) {
when (itemId) {
R.id.navBuildResume -> showBuildForm(NAV_BUILD_RESUME)
R.id.navBuildCoverLetter -> showBuildForm(NAV_BUILD_COVER_LETTER)
R.id.navMyResumes -> showMyResumes()
R.id.navAdmin -> startActivity(Intent(this, AdminActivity::class.java))
R.id.navSignOut -> signOut()
}
}
private fun showBuildForm(position: Int) {
val type = if (position == NAV_BUILD_COVER_LETTER) JobFormFragment.TYPE_COVER_LETTER else JobFormFragment.TYPE_RESUME
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, JobFormFragment.newInstance(type))
.commitAllowingStateLoss()
}
private fun showMyResumes() {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, MyResumesFragment())
.addToBackStack(null)
.commitAllowingStateLoss()
}
private fun loadUser() { private fun loadUser() {
Thread { Thread {
try { try {
@ -110,13 +86,20 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
private fun updateUserUI() { private fun updateUserUI() {
userName.text = user.name ?: user.email ?: getString(R.string.unknown_user) userName.text = user.name ?: user.email ?: getString(R.string.unknown_user)
userEmail.text = user.email ?: getString(R.string.unknown_email) if (!user.authenticated || (!user.is_admin && !user.can_submit)) {
usageBanner.visibility = if (!user.authenticated || (!user.is_admin && !user.can_submit)) { usageBanner.visibility = View.VISIBLE
View.VISIBLE
} else { } else {
View.GONE usageBanner.visibility = View.GONE
} }
navView.menu.findItem(R.id.navAdmin)?.isVisible = user.is_admin invalidateOptionsMenu()
}
private fun switchFormTab(position: Int) {
val type = if (position == TAB_COVER) JobFormFragment.TYPE_COVER_LETTER else JobFormFragment.TYPE_RESUME
val fragment = JobFormFragment.newInstance(type)
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, fragment)
.commitAllowingStateLoss()
} }
private fun startHealthPolling() { private fun startHealthPolling() {
@ -147,12 +130,27 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
}.start() }.start()
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menuAdminPanel)?.isVisible = user.is_admin
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) { return when (item.itemId) {
drawerLayout.openDrawer(navView) R.id.menuSignOut -> {
signOut()
true true
} else { }
super.onOptionsItemSelected(item) R.id.menuAdminPanel -> {
startActivity(Intent(this, AdminActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
} }
} }
@ -178,8 +176,8 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
fun onResultNewJob() { fun onResultNewJob() {
supportFragmentManager.popBackStackImmediate() supportFragmentManager.popBackStackImmediate()
navView.menu.getItem(NAV_BUILD_RESUME)?.isChecked = true tabLayout.getTabAt(TAB_RESUME)?.select()
showBuildForm(NAV_BUILD_RESUME) switchFormTab(TAB_RESUME)
} }
override fun onNewJob() { override fun onNewJob() {

View File

@ -1,133 +0,0 @@
package com.example.resbuilder.ui
import android.os.Bundle
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 androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.example.resbuilder.R
import com.example.resbuilder.data.model.JobSummary
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.launch
class MyResumesFragment : Fragment() {
private val viewModel: MyResumesViewModel by viewModels()
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
viewModel.openJob(item.job_id)
}
observeState()
viewModel.loadJobs()
}
private fun observeState() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
progress.visibility = if (state.loading) View.VISIBLE else View.GONE
state.error?.let {
emptyView.text = getString(R.string.my_resumes_error)
emptyView.visibility = View.VISIBLE
Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
}
if (!state.loading && state.jobs.isNotEmpty()) {
emptyView.visibility = View.GONE
listView.visibility = View.VISIBLE
listView.adapter = JobListAdapter(requireContext(), state.jobs)
} else if (!state.loading && state.error == null) {
emptyView.text = getString(R.string.my_resumes_empty)
emptyView.visibility = View.VISIBLE
listView.visibility = View.GONE
}
state.openedJob?.let { job ->
viewModel.consumeOpenedJob()
openJobResult(job)
}
}
}
}
}
private fun openJobResult(job: com.example.resbuilder.data.model.Job) {
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(job.job_id ?: "", job.type ?: JobFormFragment.TYPE_RESUME))
}
}
private fun replaceWith(fragment: Fragment) {
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, fragment)
.addToBackStack(null)
.commitAllowingStateLoss()
}
private class JobListAdapter(
context: android.content.Context,
private val jobs: List<JobSummary>
) : ArrayAdapter<JobSummary>(context, R.layout.item_job_summary, jobs) {
private val inflater = android.view.LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = convertView ?: inflater.inflate(R.layout.item_job_summary, parent, false)
val job = jobs[position]
v.findViewById<TextView>(R.id.itemTitle).text =
job.title?.takeIf { it.isNotBlank() } ?: getKindLabel(job.type)
v.findViewById<TextView>(R.id.itemSubtitle).text = buildSubtitle(job)
return v
}
private fun getKindLabel(type: String?): String =
if (type == JobFormFragment.TYPE_COVER_LETTER) context.getString(R.string.type_cover_letter)
else context.getString(R.string.type_resume)
private fun buildSubtitle(job: JobSummary): String {
val kind = getKindLabel(job.type)
val created = job.created_at?.let(::formatDate)?.let { " · $it" }.orEmpty()
return "$kind$created"
}
private fun formatDate(iso: String): String {
return try {
val date = java.time.OffsetDateTime.parse(iso)
val local = date.atZoneSameInstant(java.time.ZoneId.systemDefault())
java.time.format.DateTimeFormatter.ofPattern("d MMM yyyy").format(local)
} catch (_: Exception) {
iso
}
}
}
}

View File

@ -1,54 +0,0 @@
package com.example.resbuilder.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.resbuilder.data.model.Job
import com.example.resbuilder.data.model.JobSummary
import com.example.resbuilder.data.remote.ApiClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class MyResumesUiState(
val loading: Boolean = false,
val jobs: List<JobSummary> = emptyList(),
val error: String? = null,
val openedJob: Job? = null
)
class MyResumesViewModel : ViewModel() {
private val _state = MutableStateFlow(MyResumesUiState())
val state: StateFlow<MyResumesUiState> = _state.asStateFlow()
fun loadJobs() {
_state.value = _state.value.copy(loading = true, error = null)
viewModelScope.launch {
try {
val jobs = withContext(Dispatchers.IO) { ApiClient.getMyJobs().jobs }
_state.value = _state.value.copy(loading = false, jobs = jobs)
} catch (e: Exception) {
_state.value = _state.value.copy(loading = false, error = e.message ?: "load failed")
}
}
}
fun openJob(jobId: String) {
viewModelScope.launch {
_state.value = _state.value.copy(loading = true, error = null)
try {
val job = withContext(Dispatchers.IO) { ApiClient.getJob(jobId) }
_state.value = _state.value.copy(loading = false, openedJob = job)
} catch (e: Exception) {
_state.value = _state.value.copy(loading = false, error = e.message ?: "open failed")
}
}
}
fun consumeOpenedJob() {
_state.value = _state.value.copy(openedJob = null)
}
}

View File

@ -13,14 +13,11 @@ import android.webkit.WebView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.example.resbuilder.R import com.example.resbuilder.R
import com.example.resbuilder.data.model.Job import com.example.resbuilder.data.model.Job
import com.example.resbuilder.data.remote.ApiClient import com.example.resbuilder.data.remote.ApiClient
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import java.io.File
import java.io.FileOutputStream
class ResultFragment : Fragment() { class ResultFragment : Fragment() {
@ -89,58 +86,14 @@ class ResultFragment : Fragment() {
newButton.setOnClickListener { listener?.onNewJob() } newButton.setOnClickListener { listener?.onNewJob() }
jobId?.let { id -> jobId?.let { id ->
pdfButton.setOnClickListener { downloadAndOpen(id, "pdf") } pdfButton.setOnClickListener { openUrl(ApiClient.getExportPdfUrl(id)) }
docxButton.setOnClickListener { downloadAndOpen(id, "docx") } docxButton.setOnClickListener { openUrl(ApiClient.getExportDocxUrl(id)) }
} ?: run { } ?: run {
pdfButton.isEnabled = false pdfButton.isEnabled = false
docxButton.isEnabled = false docxButton.isEnabled = false
} }
} }
private fun downloadAndOpen(jobId: String, format: String) {
val url = if (format == "pdf") ApiClient.getExportPdfUrl(jobId) else ApiClient.getExportDocxUrl(jobId)
val dir = File(requireContext().cacheDir, "exports")
if (!dir.exists()) dir.mkdirs()
val safeId = jobId.replace("/", "_").replace("..", "_").take(8)
val file = File(dir, "$safeId.$format")
pdfButton.isEnabled = false
docxButton.isEnabled = false
Toast.makeText(context, R.string.exporting, Toast.LENGTH_SHORT).show()
Thread {
try {
val bytes = ApiClient.downloadExport(url)
FileOutputStream(file).use { it.write(bytes) }
requireActivity().runOnUiThread {
pdfButton.isEnabled = true
docxButton.isEnabled = true
openFile(file)
}
} catch (e: Exception) {
requireActivity().runOnUiThread {
pdfButton.isEnabled = true
docxButton.isEnabled = true
Toast.makeText(context, e.message ?: getString(R.string.export_failed), Toast.LENGTH_LONG).show()
}
}
}.start()
}
private fun openFile(file: File) {
val uri = FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.fileprovider", file)
val mime = if (file.extension == "pdf") "application/pdf" else "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, mime)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
try {
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(context, R.string.export_no_viewer, Toast.LENGTH_LONG).show()
}
}
private fun copyText() { private fun copyText() {
val raw = rawContent val raw = rawContent
if (raw.isNullOrEmpty()) { if (raw.isNullOrEmpty()) {
@ -152,6 +105,11 @@ class ResultFragment : Fragment() {
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
} }
private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
fun setOnNewListener(l: OnNewListener) { fun setOnNewListener(l: OnNewListener) {
listener = l listener = l
} }

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="?attr/colorWarningContainer" /> <solid android:color="?attr/colorWarningContainer" />
<corners android:radius="12dp" /> <corners android:radius="12dp" />

View File

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

View File

@ -1,9 +0,0 @@
<?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:centerColor="?attr/colorPrimaryInverse"
android:endColor="?attr/colorPrimaryContainer"
android:type="linear" />
</shape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="?attr/colorSurfaceContainerHighest" /> <solid android:color="?attr/colorSurfaceContainerHighest" />
<corners android:radius="8dp" /> <corners android:radius="8dp" />

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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="?attr/colorOnSurfaceVariant"
android:pathData="M14,2H6C4.9,2 4,2.9 4,4V20C4,21.1 4.9,22 6,22H18C19.1,22 20,21.1 20,20V8L14,2M13,9V3.5L18.5,9H13Z" />
</vector>

View File

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

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M3,6h18v2H3V6zM3,11h18v2H3v-2zM3,16h18v2H3v-2z" />
</vector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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="?attr/colorOnPrimary"
android:pathData="M12,2L13.5,9.5L21,12L13.5,14.5L12,22L10.5,14.5L3,12L10.5,9.5L12,2Z" />
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -5,50 +5,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:padding="16dp"
tools:context=".ui.LoginActivity"> tools:context=".ui.LoginActivity">
<!-- Decorative gradient background behind card --> <!-- Skeleton loading placeholder visible while auth is checked -->
<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" />
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginEnd="@dimen/space_lg"
android:layout_marginBottom="@dimen/space_lg"
android:alpha="0.15"
android:contentDescription="@null"
android:src="@drawable/ic_sparkle"
app:layout_constraintBottom_toTopOf="@id/guidelineTop"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.40" />
<!-- Main content card -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/loginCard" android:id="@+id/loginSkeletonCard"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_lg" android:layout_marginHorizontal="16dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow" android:visibility="gone"
app:cardCornerRadius="@dimen/card_radius_large" app:cardCornerRadius="12dp"
app:cardElevation="0dp" app:cardElevation="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -57,103 +29,91 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/space_xl"> android:padding="24dp">
<!-- App icon with tonal surface --> <View
<com.google.android.material.card.MaterialCardView android:layout_width="80dp"
android:layout_width="@dimen/space_xxl" android:layout_height="80dp"
android:layout_height="@dimen/space_xxl" android:background="?attr/colorSurfaceContainerHigh" />
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="@dimen/card_radius_large"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<ImageView <View
android:layout_width="160dp"
android:layout_height="24dp"
android:layout_marginTop="16dp"
android:background="?attr/colorSurfaceContainerHigh" />
<View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="48dp"
android:contentDescription="@string/cd_app_logo" android:layout_marginTop="24dp"
android:padding="@dimen/space_md" android:background="?attr/colorSurfaceContainerHigh" />
android:scaleType="centerInside" </LinearLayout>
android:src="@drawable/ic_launcher_foreground"
app:tint="?attr/colorOnPrimaryContainer" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Title --> <ImageView
android:id="@+id/appIcon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginBottom="24dp"
android:background="?attr/colorPrimaryContainer"
android:contentDescription="@string/cd_app_logo"
android:importantForAccessibility="yes"
android:padding="24dp"
android:src="@drawable/ic_launcher_foreground"
app:layout_constraintBottom_toTopOf="@id/loginTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:tint="?attr/colorOnPrimaryContainer" />
<TextView <TextView
android:id="@+id/loginTitle" android:id="@+id/loginTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_lg" android:layout_marginBottom="8dp"
android:text="@string/app_title" android:text="@string/app_title"
android:textAlignment="center" android:textAppearance="?attr/textAppearanceHeadlineLarge"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface"
android:textStyle="bold" /> app:layout_constraintBottom_toTopOf="@id/loginSubtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<!-- Subtitle -->
<TextView <TextView
android:id="@+id/loginSubtitle" android:id="@+id/loginSubtitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm" android:layout_marginBottom="24dp"
android:text="@string/welcome_subtitle" android:text="@string/checking_auth"
android:textAlignment="center" android:textAppearance="?attr/textAppearanceBodyMedium"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textColor="?attr/colorOnSurfaceVariant"
android:textColor="?attr/colorOnSurfaceVariant" /> app:layout_constraintBottom_toTopOf="@id/googleSignInButton"
<!-- Divider -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="@dimen/space_lg"
android:background="?attr/colorOutlineVariant" />
<!-- Sign in button -->
<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:minHeight="@dimen/button_height"
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="@dimen/space_md"
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="@dimen/space_md"
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="@dimen/space_xxl"
app:layout_constraintBottom_toTopOf="@id/loginCard"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guidelineTop" app:layout_constraintTop_toBottomOf="@id/loginTitle" />
app:trackColor="?attr/colorPrimaryContainer" />
<ProgressBar
android:id="@+id/loginProgress"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginBottom="24dp"
android:contentDescription="@string/cd_loading"
android:indeterminateTint="?attr/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/loginTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.gms.common.SignInButton
android:id="@+id/googleSignInButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:contentDescription="@string/cd_google_sign_in"
android:minWidth="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginSubtitle" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,82 +1,105 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context=".ui.MainActivity"> tools:context=".ui.MainActivity">
<!-- Main content -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Modern colored app bar with gradient surface and centered title -->
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" android:background="?attr/colorSurface"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:elevation="0dp"> app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="@drawable/bg_gradient_primary_bottom" android:background="?attr/colorSurface"
android:elevation="0dp" android:fitsSystemWindows="true"
app:navigationIcon="@drawable/ic_menu_hamburger" app:layout_scrollFlags="scroll|enterAlways|snap"
app:navigationIconTint="?attr/colorOnPrimary"
app:title="@string/app_title" app:title="@string/app_title"
app:titleCentered="true" app:titleCentered="true"
app:titleTextAppearance="?attr/textAppearanceTitleLarge" app:titleTextAppearance="?attr/textAppearanceTitleLarge"
app:titleTextColor="?attr/colorOnPrimary" /> app:titleTextColor="?attr/colorOnSurface">
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/healthContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="16dp"
android:background="?attr/colorSurfaceContainer"
android:gravity="center_vertical"
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/cd_health_indicator"
android:src="@drawable/ic_circle_status" />
<TextView
android:id="@+id/userName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/unknown_user"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurface" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<TextView <TextView
android:id="@+id/usageBanner" android:id="@+id/usageBanner"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_md" android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/space_sm" android:layout_marginVertical="8dp"
android:background="@drawable/bg_banner_warning" android:background="@drawable/bg_banner_warning"
android:drawableStart="@drawable/ic_circle_status" android:drawableStart="@drawable/ic_circle_status"
android:drawableTint="?attr/colorWarning" android:drawableTint="?attr/colorWarning"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="@dimen/touch_target_min" android:minHeight="48dp"
android:padding="@dimen/space_sm" android:padding="12dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/usage_limit_banner" android:text="@string/usage_limit_banner"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> 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>
<FrameLayout <FrameLayout
android:id="@+id/fragmentContainer" android:id="@+id/fragmentContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" /> android:background="?attr/colorSurface"
</LinearLayout> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- Navigation drawer with richer surface and modern item styling --> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="?attr/colorSurfaceContainerLowest"
app:headerLayout="@layout/nav_header"
app:itemHorizontalPadding="@dimen/space_md"
app:itemIconPadding="@dimen/space_md"
app:itemIconTint="?attr/colorOnSurfaceVariant"
app:itemRippleColor="?attr/colorPrimaryContainer"
app:itemTextAppearance="?attr/textAppearanceBodyLarge"
app:itemTextColor="?attr/colorOnSurface"
app:itemVerticalPadding="@dimen/space_md"
app:menu="@menu/menu_nav_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -6,67 +6,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:fillViewport="true" android:fillViewport="true"
android:clipToPadding="false"
android:paddingBottom="@dimen/space_md"
tools:context=".ui.JobFormFragment"> tools:context=".ui.JobFormFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Hero header with gradient surface and section title -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_gradient_primary"
android:minHeight="@dimen/hero_header_min_height"
android:padding="@dimen/space_lg">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="top|end"
android:alpha="0.25"
android:contentDescription="@null"
android:src="@drawable/ic_sparkle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_title"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnPrimary"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_xs"
android:text="@string/hint_job_title"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnPrimary" />
</LinearLayout>
</FrameLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/space_md"> android:padding="16dp">
<!-- Job details card -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow" app:cardCornerRadius="12dp"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp" app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -75,7 +26,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/space_md"> android:padding="16dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -87,26 +38,24 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm" android:layout_marginTop="12dp"
android:hint="@string/hint_job_title" android:hint="@string/hint_job_title"
style="@style/Widget.Material3.TextInputLayout.FilledBox" app:boxBackgroundColor="?attr/colorSurfaceContainer"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest" app:hintTextColor="?attr/colorOnSurfaceVariant"
app:boxStrokeColor="?attr/colorPrimary" style="@style/Widget.Material3.TextInputLayout.FilledBox">
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/jobTitleInput" android:id="@+id/jobTitleInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" android:inputType="text"
android:minHeight="@dimen/touch_target_min" /> android:minHeight="48dp" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm" android:layout_marginTop="12dp"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="horizontal"> android:orientation="horizontal">
@ -115,26 +64,24 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/hint_seek_url" android:hint="@string/hint_seek_url"
style="@style/Widget.Material3.TextInputLayout.FilledBox" app:boxBackgroundColor="?attr/colorSurfaceContainer"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest" app:hintTextColor="?attr/colorOnSurfaceVariant"
app:boxStrokeColor="?attr/colorPrimary" style="@style/Widget.Material3.TextInputLayout.FilledBox">
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/seekUrlInput" android:id="@+id/seekUrlInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textUri" android:inputType="textUri"
android:minHeight="@dimen/touch_target_min" /> android:minHeight="48dp" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/scrapeButton" android:id="@+id/scrapeButton"
style="@style/Widget.Material3.Button.TonalButton" style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:layout_marginStart="@dimen/space_sm" android:layout_marginStart="8dp"
android:contentDescription="@string/cd_scrape_job" android:contentDescription="@string/cd_scrape_job"
android:minWidth="64dp" android:minWidth="64dp"
android:text="@string/button_scrape" /> android:text="@string/button_scrape" />
@ -143,13 +90,11 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_md" android:layout_marginTop="16dp"
android:hint="@string/hint_job_description" android:hint="@string/hint_job_description"
style="@style/Widget.Material3.TextInputLayout.FilledBox" app:boxBackgroundColor="?attr/colorSurfaceContainer"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest" app:hintTextColor="?attr/colorOnSurfaceVariant"
app:boxStrokeColor="?attr/colorPrimary" style="@style/Widget.Material3.TextInputLayout.FilledBox">
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/jobDescriptionInput" android:id="@+id/jobDescriptionInput"
@ -157,19 +102,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="top" android:gravity="top"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:minLines="4" android:minLines="4" />
android:minHeight="@dimen/touch_target_min" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Resume / cover letter input card -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_md" android:layout_marginTop="12dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow" app:cardCornerRadius="12dp"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp" app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -178,7 +120,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/space_md"> android:padding="16dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -199,7 +141,7 @@
android:id="@+id/uploadButton" android:id="@+id/uploadButton"
style="@style/Widget.Material3.Button.TonalButton" style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:contentDescription="@string/cd_upload_document" android:contentDescription="@string/cd_upload_document"
android:text="@string/button_upload" /> android:text="@string/button_upload" />
</LinearLayout> </LinearLayout>
@ -208,13 +150,11 @@
android:id="@+id/docInputLayout" android:id="@+id/docInputLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm" android:layout_marginTop="12dp"
android:hint="@string/hint_your_resume" android:hint="@string/hint_your_resume"
style="@style/Widget.Material3.TextInputLayout.FilledBox" app:boxBackgroundColor="?attr/colorSurfaceContainer"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest" app:hintTextColor="?attr/colorOnSurfaceVariant"
app:boxStrokeColor="?attr/colorPrimary" style="@style/Widget.Material3.TextInputLayout.FilledBox">
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/docInput" android:id="@+id/docInput"
@ -222,8 +162,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="top" android:gravity="top"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:minLines="6" android:minLines="6" />
android:minHeight="@dimen/touch_target_min" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
@ -231,19 +170,18 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/submitButton" android:id="@+id/submitButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:layout_marginTop="@dimen/space_md" android:layout_marginTop="16dp"
android:text="@string/generate_resume" /> android:text="@string/generate_resume" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="@dimen/space_xxl" android:layout_width="48dp"
android:layout_height="@dimen/space_xxl" android:layout_height="48dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/space_md" android:layout_marginTop="16dp"
android:contentDescription="@string/cd_loading" android:contentDescription="@string/cd_loading"
android:indeterminateTint="?attr/colorPrimary" android:indeterminateTint="?attr/colorPrimary"
android:visibility="gone" /> android:visibility="gone" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</ScrollView> </ScrollView>

View File

@ -7,15 +7,14 @@
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/space_md"> android:padding="16dp">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_md" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="@dimen/space_sm" android:layout_marginVertical="8dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow" app:cardCornerRadius="12dp"
app:cardCornerRadius="@dimen/card_radius_large"
app:cardElevation="0dp" app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -25,36 +24,27 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/space_lg"> android:padding="24dp">
<!-- Status badge: now a tonal chip-style pill -->
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<TextView <TextView
android:id="@+id/statusBadge" android:id="@+id/statusBadge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/bg_status_badge"
android:contentDescription="@string/status_queued" android:contentDescription="@string/status_queued"
android:gravity="center" android:gravity="center"
android:minHeight="32dp" android:minHeight="32dp"
android:paddingHorizontal="@dimen/space_md" android:paddingHorizontal="16dp"
android:paddingVertical="4dp" android:paddingVertical="4dp"
android:text="@string/status_queued" android:text="@string/status_queued"
android:textAppearance="?attr/textAppearanceLabelLarge" android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorOnPrimaryContainer" /> android:textColor="?attr/colorOnPrimary" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/statusProgress" android:id="@+id/statusProgress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_lg" android:layout_marginTop="24dp"
android:contentDescription="@string/content_generation_running" android:contentDescription="@string/content_generation_running"
android:indeterminate="true" android:indeterminate="true"
app:indicatorColor="?attr/colorPrimary" app:indicatorColor="?attr/colorPrimary"
@ -64,7 +54,7 @@
android:id="@+id/statusMessage" android:id="@+id/statusMessage"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_md" android:layout_marginTop="16dp"
android:gravity="center" android:gravity="center"
android:text="@string/job_wait_message" android:text="@string/job_wait_message"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceBodyLarge"
@ -74,7 +64,7 @@
android:id="@+id/statusDetail" android:id="@+id/statusDetail"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm" android:layout_marginTop="8dp"
android:gravity="center" android:gravity="center"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" android:textColor="?attr/colorOnSurfaceVariant"
@ -84,8 +74,8 @@
android:id="@+id/tryAgainButton" android:id="@+id/tryAgainButton"
style="@style/Widget.Material3.Button.TonalButton" style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:layout_marginTop="@dimen/space_lg" android:layout_marginTop="24dp"
android:minWidth="120dp" android:minWidth="120dp"
android:text="@string/try_again" android:text="@string/try_again"
android:visibility="gone" /> android:visibility="gone" />

View File

@ -1,88 +0,0 @@
<?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">
<!-- Hero header for list screen -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_gradient_primary"
android:minHeight="@dimen/hero_header_min_height"
android:padding="@dimen/space_lg">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="top|end"
android:alpha="0.25"
android:contentDescription="@null"
android:src="@drawable/ic_sparkle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_title"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnPrimary"
android:textStyle="bold" />
<TextView
android:id="@+id/myResumesTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_xs"
android:text="@string/my_resumes_title"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnPrimary" />
</LinearLayout>
</FrameLayout>
<TextView
android:id="@+id/myResumesEmpty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_md"
android:layout_marginTop="@dimen/space_xl"
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="@dimen/space_md"
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_marginHorizontal="@dimen/space_md"
android:layout_marginTop="@dimen/space_md"
android:clipToPadding="false"
android:divider="@null"
android:listSelector="@android:color/transparent"
android:paddingBottom="@dimen/space_md"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -8,15 +8,13 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".ui.ResultFragment"> tools:context=".ui.ResultFragment">
<!-- Top action bar card with tonal surface -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_md" android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/space_md" android:layout_marginTop="16dp"
android:layout_marginBottom="@dimen/space_sm" android:layout_marginBottom="8dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow" app:cardCornerRadius="12dp"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp" app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -25,31 +23,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="@dimen/touch_target_min" android:minHeight="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/space_sm"> android:padding="12dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSuccessContainer"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<TextView <TextView
android:id="@+id/resultBadge" android:id="@+id/resultBadge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/bg_status_badge"
android:contentDescription="@string/status_completed" android:contentDescription="@string/status_completed"
android:gravity="center" android:gravity="center"
android:minHeight="32dp" android:minHeight="32dp"
android:paddingHorizontal="@dimen/space_md" android:paddingHorizontal="12dp"
android:paddingVertical="4dp" android:paddingVertical="4dp"
android:text="@string/status_completed" android:text="@string/status_completed"
android:textAppearance="?attr/textAppearanceLabelLarge" android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorOnSuccessContainer" /> android:textColor="?attr/colorOnPrimary" />
</com.google.android.material.card.MaterialCardView>
<View <View
android:layout_width="0dp" android:layout_width="0dp"
@ -60,7 +50,7 @@
android:id="@+id/copyButton" android:id="@+id/copyButton"
style="@style/Widget.Material3.Button.TextButton" style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:contentDescription="@string/cd_copy_result" android:contentDescription="@string/cd_copy_result"
android:minWidth="64dp" android:minWidth="64dp"
android:text="@string/copy_text" /> android:text="@string/copy_text" />
@ -69,8 +59,8 @@
android:id="@+id/newButton" android:id="@+id/newButton"
style="@style/Widget.Material3.Button.TextButton" style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:layout_marginStart="@dimen/space_xs" android:layout_marginStart="4dp"
android:contentDescription="@string/cd_new_job" android:contentDescription="@string/cd_new_job"
android:minWidth="64dp" android:minWidth="64dp"
android:text="@string/new_job" /> android:text="@string/new_job" />
@ -81,19 +71,17 @@
android:id="@+id/resultWebView" android:id="@+id/resultWebView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginHorizontal="@dimen/space_md" android:layout_marginHorizontal="16dp"
android:layout_weight="1" android:layout_weight="1"
android:contentDescription="@string/result_preview" /> android:contentDescription="@string/result_preview" />
<!-- Export action bar with tonal surface -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_md" android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/space_sm" android:layout_marginTop="8dp"
android:layout_marginBottom="@dimen/space_md" android:layout_marginBottom="16dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow" app:cardCornerRadius="12dp"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp" app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant" app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -102,14 +90,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/space_sm"> android:padding="12dp">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/pdfButton" android:id="@+id/pdfButton"
style="@style/Widget.Material3.Button.OutlinedButton" style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:layout_marginEnd="@dimen/space_sm" android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:contentDescription="@string/cd_export_pdf" android:contentDescription="@string/cd_export_pdf"
android:text="@string/export_pdf" /> android:text="@string/export_pdf" />
@ -118,8 +106,8 @@
android:id="@+id/docxButton" android:id="@+id/docxButton"
style="@style/Widget.Material3.Button.OutlinedButton" style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/button_height" android:layout_height="48dp"
android:layout_marginStart="@dimen/space_sm" android:layout_marginStart="8dp"
android:layout_weight="1" android:layout_weight="1"
android:contentDescription="@string/cd_export_docx" android:contentDescription="@string/cd_export_docx"
android:text="@string/export_docx" /> android:text="@string/export_docx" />

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -1,76 +0,0 @@
<?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="@dimen/space_sm"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:rippleColor="?attr/colorPrimaryContainer"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="@dimen/touch_target_min"
android:orientation="horizontal"
android:padding="@dimen/space_md">
<com.google.android.material.card.MaterialCardView
android:layout_width="@dimen/space_xxl"
android:layout_height="@dimen/space_xxl"
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:contentDescription="@string/result_preview"
android:src="@drawable/ic_document"
app:tint="?attr/colorOnPrimaryContainer" />
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_md"
android:layout_weight="1"
android:orientation="vertical">
<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="@dimen/space_xs"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
tools:text="Resume · 12 Aug 2026" />
</LinearLayout>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="@dimen/space_sm"
android:contentDescription="@null"
android:src="@drawable/ic_circle_status"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,49 +0,0 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_gradient_primary"
android:gravity="center_vertical"
android:orientation="vertical"
android:padding="@dimen/space_lg">
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurfaceContainerLowest"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<ImageView
android:id="@+id/healthIndicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_margin="@dimen/space_xs"
android:contentDescription="@string/cd_health_indicator"
android:src="@drawable/ic_circle_status" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/userName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_md"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/unknown_user"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnPrimary" />
<TextView
android:id="@+id/userEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_xs"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnPrimary" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menuAdminPanel"
android:title="@string/menu_admin_panel"
android:visible="false" />
<item
android:id="@+id/menuSignOut"
android:title="@string/menu_sign_out" />
</menu>

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:id="@+id/navGroupActions"
android:checkableBehavior="single">
<item
android:id="@+id/navBuildResume"
android:icon="@drawable/ic_sparkle"
android:title="@string/tab_build_resume" />
<item
android:id="@+id/navBuildCoverLetter"
android:icon="@drawable/ic_sparkle"
android:title="@string/tab_build_cover_letter" />
<item
android:id="@+id/navMyResumes"
android:icon="@drawable/ic_circle_status"
android:title="@string/menu_my_resumes" />
</group>
<group
android:id="@+id/navGroupAdmin"
android:checkableBehavior="none">
<item
android:id="@+id/navAdmin"
android:title="@string/menu_admin_panel"
android:visible="false" />
</group>
<group
android:id="@+id/navGroupAccount"
android:checkableBehavior="none">
<item
android:id="@+id/navSignOut"
android:title="@string/menu_sign_out" />
</group>
</menu>

View File

@ -1,43 +1,41 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary color roles (dark): bright teal on deep surfaces --> <!-- Primary color roles (dark) -->
<item name="colorPrimary">#80DADC</item> <item name="colorPrimary">#D0BCFF</item>
<item name="colorOnPrimary">#003738</item> <item name="colorOnPrimary">#381E72</item>
<item name="colorPrimaryContainer">#005A5C</item> <item name="colorPrimaryContainer">#4F378B</item>
<item name="colorOnPrimaryContainer">#B5F2F3</item> <item name="colorOnPrimaryContainer">#EADDFF</item>
<item name="colorPrimaryInverse">#004F4F</item> <item name="colorPrimaryInverse">#6750A4</item>
<item name="colorPrimaryFixed">#80DADC</item>
<item name="colorOnPrimaryFixed">#002020</item>
<!-- Secondary color roles (dark) --> <!-- Secondary color roles (dark) -->
<item name="colorSecondary">#9CBEBD</item> <item name="colorSecondary">#CCC2DC</item>
<item name="colorOnSecondary">#1B3534</item> <item name="colorOnSecondary">#332D41</item>
<item name="colorSecondaryContainer">#2F4A49</item> <item name="colorSecondaryContainer">#4A4458</item>
<item name="colorOnSecondaryContainer">#BDD6D5</item> <item name="colorOnSecondaryContainer">#E8DEF8</item>
<!-- Tertiary color roles (dark) --> <!-- Tertiary color roles (dark) -->
<item name="colorTertiary">#A4C9E8</item> <item name="colorTertiary">#EFB8C8</item>
<item name="colorOnTertiary">#0B2D45</item> <item name="colorOnTertiary">#492532</item>
<item name="colorTertiaryContainer">#27475F</item> <item name="colorTertiaryContainer">#633B48</item>
<item name="colorOnTertiaryContainer">#BFE0FF</item> <item name="colorOnTertiaryContainer">#FFD8E4</item>
<!-- Surface color roles (dark): deep charcoal-teal --> <!-- Surface color roles (dark) -->
<item name="android:colorBackground">#0C1414</item> <item name="android:colorBackground">#1D1B20</item>
<item name="colorSurface">#0C1414</item> <item name="colorSurface">#1D1B20</item>
<item name="colorOnSurface">#E0ECEC</item> <item name="colorOnSurface">#E6E1E5</item>
<item name="colorSurfaceVariant">#3F4A4A</item> <item name="colorSurfaceVariant">#49454F</item>
<item name="colorOnSurfaceVariant">#BEC9C9</item> <item name="colorOnSurfaceVariant">#CAC4D0</item>
<item name="colorSurfaceContainerLowest">#070E0E</item> <item name="colorSurfaceContainerLowest">#16141C</item>
<item name="colorSurfaceContainerLow">#121B1B</item> <item name="colorSurfaceContainerLow">#1D1B20</item>
<item name="colorSurfaceContainer">#182222</item> <item name="colorSurfaceContainer">#211F26</item>
<item name="colorSurfaceContainerHigh">#242D2D</item> <item name="colorSurfaceContainerHigh">#27252D</item>
<item name="colorSurfaceContainerHighest">#303939</item> <item name="colorSurfaceContainerHighest">#36343B</item>
<item name="colorSurfaceInverse">#E0ECEC</item> <item name="colorSurfaceInverse">#E6E0E9</item>
<item name="colorOnSurfaceInverse">#0C1414</item> <item name="colorOnSurfaceInverse">#322F35</item>
<!-- Outline --> <!-- Outline -->
<item name="colorOutline">#899999</item> <item name="colorOutline">#938F99</item>
<item name="colorOutlineVariant">#3F4A4A</item> <item name="colorOutlineVariant">#49454F</item>
<!-- Error (dark) --> <!-- Error (dark) -->
<item name="colorError">#F2B8B5</item> <item name="colorError">#F2B8B5</item>
@ -61,12 +59,12 @@
<item name="colorInfoContainer">#0B4576</item> <item name="colorInfoContainer">#0B4576</item>
<item name="colorOnInfoContainer">#D4EAFB</item> <item name="colorOnInfoContainer">#D4EAFB</item>
<!-- System bars --> <!-- System bars - edge-to-edge ready (dark) -->
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item> <item name="android:windowLightStatusBar">false</item>
<!-- Typography --> <!-- M3 Typography and shape defaults inherited from Material3 parent -->
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item> <item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item> <item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item> <item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
@ -83,7 +81,7 @@
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item> <item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item> <item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
<!-- Component defaults --> <!-- Default M3 component styles inherited from Material3 parent; explicit overrides kept to a minimum -->
<item name="materialButtonStyle">@style/Widget.Material3.Button</item> <item name="materialButtonStyle">@style/Widget.Material3.Button</item>
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item> <item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>

View File

@ -2,78 +2,77 @@
<resources> <resources>
<!-- Material Design 3 semantic color tokens --> <!-- Material Design 3 semantic color tokens -->
<!-- Primary palette: rich teal with stronger contrast --> <!-- Primary palette -->
<color name="primary">#004F4F</color> <color name="primary">#6750A4</color>
<color name="on_primary">#FFFFFF</color> <color name="on_primary">#FFFFFF</color>
<color name="primary_container">#B5F2F3</color> <color name="primary_container">#EADDFF</color>
<color name="on_primary_container">#002020</color> <color name="on_primary_container">#21005D</color>
<color name="primary_fixed">#B5F2F3</color>
<color name="on_primary_fixed">#002020</color>
<!-- Secondary palette: cool slate with more depth --> <!-- Secondary palette -->
<color name="secondary">#3D5252</color> <color name="secondary">#625B71</color>
<color name="on_secondary">#FFFFFF</color> <color name="on_secondary">#FFFFFF</color>
<color name="secondary_container">#BDD6D5</color> <color name="secondary_container">#E8DEF8</color>
<color name="on_secondary_container">#041E1E</color> <color name="on_secondary_container">#1D192B</color>
<!-- Tertiary palette: deep blue-grey accent --> <!-- Tertiary palette -->
<color name="tertiary">#365064</color> <color name="tertiary">#7D5260</color>
<color name="on_tertiary">#FFFFFF</color> <color name="on_tertiary">#FFFFFF</color>
<color name="tertiary_container">#BFE0FF</color> <color name="tertiary_container">#FFD8E4</color>
<color name="on_tertiary_container">#001D31</color> <color name="on_tertiary_container">#31111D</color>
<!-- Surface colors: warmer off-white base with visible tonal elevation --> <!-- Surface colors -->
<color name="surface">#FAFDFD</color> <color name="surface">#FFFBFE</color>
<color name="on_surface">#0A1414</color> <color name="on_surface">#1D1B20</color>
<color name="surface_variant">#D8E5E5</color> <color name="surface_variant">#E7E0EC</color>
<color name="on_surface_variant">#3F4A4A</color> <color name="on_surface_variant">#49454F</color>
<color name="surface_container_lowest">#FFFFFF</color> <color name="surface_container_lowest">#FFFFFF</color>
<color name="surface_container_low">#F0F6F6</color> <color name="surface_container_low">#F5EFF7</color>
<color name="surface_container">#E8F1F1</color> <color name="surface_container">#F3EDF7</color>
<color name="surface_container_high">#DFEAEA</color> <color name="surface_container_high">#ECE6F0</color>
<color name="surface_container_highest">#D6E4E4</color> <color name="surface_container_highest">#E6E0E9</color>
<!-- Outline --> <!-- Outline -->
<color name="outline">#6F7E7E</color> <color name="outline">#79747E</color>
<color name="outline_variant">#BEC9C9</color> <color name="outline_variant">#CAC4D0</color>
<!-- Status / Semantic colors: richer and more saturated --> <!-- Status / Semantic colors -->
<color name="success">#2E7D32</color> <color name="success">#4CAF50</color>
<color name="on_success">#FFFFFF</color> <color name="on_success">#FFFFFF</color>
<color name="success_container">#D9F6DA</color> <color name="success_container">#D9F6DA</color>
<color name="on_success_container">#1F5E22</color> <color name="on_success_container">#1F5E22</color>
<color name="warning">#E65100</color> <color name="warning">#FF9800</color>
<color name="on_warning">#FFFFFF</color> <color name="on_warning">#FFFFFF</color>
<color name="warning_container">#FFE3B6</color> <color name="warning_container">#FFE3B6</color>
<color name="on_warning_container">#693B00</color> <color name="on_warning_container">#693B00</color>
<color name="error">#C62828</color> <color name="error">#F44336</color>
<color name="on_error">#FFFFFF</color> <color name="on_error">#FFFFFF</color>
<color name="error_container">#F9DEDC</color> <color name="error_container">#F9DEDC</color>
<color name="on_error_container">#410E0B</color> <color name="on_error_container">#410E0B</color>
<color name="info">#1565C0</color> <color name="info">#2196F3</color>
<color name="on_info">#FFFFFF</color> <color name="on_info">#FFFFFF</color>
<color name="info_container">#D4EAFB</color> <color name="info_container">#D4EAFB</color>
<color name="on_info_container">#0B4576</color> <color name="on_info_container">#0B4576</color>
<!-- Inverse surface --> <!-- Inverse surface -->
<color name="inverse_surface">#2D3131</color> <color name="inverse_surface">#322F35</color>
<color name="inverse_on_surface">#DFE4E4</color> <color name="inverse_on_surface">#F5EFF7</color>
<color name="inverse_primary">#80DADC</color> <color name="inverse_primary">#D0BCFF</color>
<!-- Status indicator palettes --> <!-- Status indicator palettes (kept for programmatic tint compatibility) -->
<color name="status_healthy">@color/success</color> <color name="status_healthy">@color/success</color>
<color name="status_degraded">@color/warning</color> <color name="status_degraded">@color/warning</color>
<color name="status_unhealthy">@color/error</color> <color name="status_unhealthy">@color/error</color>
<!-- Legacy aliases --> <!-- Legacy aliases to avoid breaking any remaining references during migration -->
<color name="purple_200">@color/primary_container</color> <color name="purple_200">@color/primary_container</color>
<color name="purple_500">@color/primary</color> <color name="purple_500">@color/primary</color>
<color name="purple_700">@color/on_primary_container</color> <color name="purple_700">@color/on_primary_container</color>
<color name="teal_200">@color/primary_container</color> <color name="teal_200">@color/secondary_container</color>
<color name="teal_700">@color/primary</color> <color name="teal_700">@color/secondary</color>
<!-- Legacy neutral aliases kept for programmatic compatibility in unchanged Kotlin code -->
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Spacing system: multiples of 4dp -->
<dimen name="space_xs">4dp</dimen>
<dimen name="space_sm">8dp</dimen>
<dimen name="space_md">16dp</dimen>
<dimen name="space_lg">24dp</dimen>
<dimen name="space_xl">32dp</dimen>
<dimen name="space_xxl">48dp</dimen>
<!-- Component dimensions -->
<dimen name="card_radius">12dp</dimen>
<dimen name="card_radius_large">16dp</dimen>
<dimen name="button_height">48dp</dimen>
<dimen name="touch_target_min">48dp</dimen>
<dimen name="hero_header_min_height">160dp</dimen>
<dimen name="app_logo_size">64dp</dimen>
</resources>

View File

@ -4,16 +4,6 @@
<!-- Login --> <!-- Login -->
<string name="sign_in_with_google">Sign in with Google</string> <string name="sign_in_with_google">Sign in with Google</string>
<string name="checking_auth">Checking authentication…</string> <string name="checking_auth">Checking authentication…</string>
<string name="welcome_subtitle">AI-powered resume and cover letter builder</string>
<string name="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
<string name="sign_in_interrupted">Sign-in interrupted</string>
<string name="no_id_token">No ID token from Google</string>
<string name="invalid_google_id_token">Invalid Google ID token: %1$s</string>
<string name="unexpected_credential_type">Unexpected credential type</string>
<string name="no_google_accounts">No Google accounts found on device</string>
<string name="sign_in_cancelled">Sign-in cancelled</string>
<string name="sign_in_failed">Sign-in failed: %1$s</string>
<string name="welcome_user">Welcome %1$s</string>
<!-- Main --> <!-- Main -->
<string name="app_title">resBuilder</string> <string name="app_title">resBuilder</string>
@ -23,7 +13,6 @@
<string name="menu_admin_panel">Admin Panel</string> <string name="menu_admin_panel">Admin Panel</string>
<string name="usage_limit_banner">You have reached your free limit. Please contact support to upgrade.</string> <string name="usage_limit_banner">You have reached your free limit. Please contact support to upgrade.</string>
<string name="unknown_user">Unknown user</string> <string name="unknown_user">Unknown user</string>
<string name="unknown_email">Not signed in</string>
<!-- Job form --> <!-- Job form -->
<string name="hint_job_title">Job Title</string> <string name="hint_job_title">Job Title</string>
@ -37,7 +26,6 @@
<string name="generate_cover_letter">Generate Cover Letter</string> <string name="generate_cover_letter">Generate Cover Letter</string>
<string name="please_log_in">Please log in to continue</string> <string name="please_log_in">Please log in to continue</string>
<string name="fields_required">Please fill in all fields</string> <string name="fields_required">Please fill in all fields</string>
<string name="enter_url">Enter a URL</string>
<!-- Status --> <!-- Status -->
<string name="status_queued">Queued</string> <string name="status_queued">Queued</string>
@ -56,9 +44,6 @@
<string name="export_docx">DOCX</string> <string name="export_docx">DOCX</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
<string name="nothing_to_copy">Nothing to copy</string> <string name="nothing_to_copy">Nothing to copy</string>
<string name="exporting">Exporting…</string>
<string name="export_failed">Export failed</string>
<string name="export_no_viewer">No app found to open this file</string>
<!-- Accessibility labels --> <!-- Accessibility labels -->
<string name="cd_app_logo">resBuilder app logo</string> <string name="cd_app_logo">resBuilder app logo</string>
@ -90,12 +75,4 @@
<string name="error_state_message">We could not load this screen. Please try again.</string> <string name="error_state_message">We could not load this screen. Please try again.</string>
<string name="usage_limit_title">Free limit reached</string> <string name="usage_limit_title">Free limit reached</string>
<string name="result_preview">Result preview</string> <string name="result_preview">Result preview</string>
<!-- My Resumes -->
<string name="menu_my_resumes">My Resumes</string>
<string name="my_resumes_title">My Documents</string>
<string name="my_resumes_empty">No generated documents yet. Submit a job to see your results here.</string>
<string name="my_resumes_error">Could not load your documents. Please try again.</string>
<string name="type_resume">Resume</string>
<string name="type_cover_letter">Cover letter</string>
</resources> </resources>

View File

@ -6,8 +6,6 @@
<item name="colorPrimaryContainer">@color/primary_container</item> <item name="colorPrimaryContainer">@color/primary_container</item>
<item name="colorOnPrimaryContainer">@color/on_primary_container</item> <item name="colorOnPrimaryContainer">@color/on_primary_container</item>
<item name="colorPrimaryInverse">@color/inverse_primary</item> <item name="colorPrimaryInverse">@color/inverse_primary</item>
<item name="colorPrimaryFixed">@color/primary_fixed</item>
<item name="colorOnPrimaryFixed">@color/on_primary_fixed</item>
<!-- Secondary color roles --> <!-- Secondary color roles -->
<item name="colorSecondary">@color/secondary</item> <item name="colorSecondary">@color/secondary</item>
@ -66,7 +64,7 @@
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<!-- M3 Typography --> <!-- M3 Typography and shape defaults inherited from Material3 parent -->
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item> <item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item> <item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item> <item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
@ -83,10 +81,11 @@
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item> <item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item> <item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
<!-- Default M3 component styles --> <!-- Default M3 component styles inherited from Material3 parent; explicit overrides kept to a minimum -->
<item name="materialButtonStyle">@style/Widget.Material3.Button</item> <item name="materialButtonStyle">@style/Widget.Material3.Button</item>
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</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> <item name="android:windowBackground">?attr/colorSurface</item>
</style> </style>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="exports"
path="exports/" />
</paths>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Enforce HTTPS for the backend API domain. Cleartext is disallowed. -->
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="false">resume.aridgwayweb.com</domain>
</domain-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>

View File

@ -1,81 +0,0 @@
package com.example.resbuilder
import com.example.resbuilder.data.model.JobListResponse
import com.example.resbuilder.data.model.JobSummary
import com.google.gson.Gson
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Tests for the API response models introduced with the "My Resumes" feature.
*
* These verify the exact wire format expected from `GET /jobs` so that a backend
* contract change fails loudly here rather than silently breaking the UI.
*/
class JobListModelsTest {
private val gson = Gson()
@Test
fun `parses a completed resume job from the backend shape`() {
val json = """
{
"jobs": [
{
"job_id": "abc-123",
"type": "resume",
"status": "completed",
"title": "Senior Android Engineer",
"created_at": "2026-08-04T12:00:00Z"
}
]
}
""".trimIndent()
val parsed = gson.fromJson(json, JobListResponse::class.java)
assertEquals(1, parsed.jobs.size)
val job = parsed.jobs[0]
assertEquals("abc-123", job.job_id)
assertEquals("resume", job.type)
assertEquals("completed", job.status)
assertEquals("Senior Android Engineer", job.title)
assertEquals("2026-08-04T12:00:00Z", job.created_at)
}
@Test
fun `title is null when backend omits it`() {
val json = """
{"jobs": [{"job_id": "x", "type": "cover_letter", "status": "queued"}]}
""".trimIndent()
val parsed = gson.fromJson(json, JobListResponse::class.java)
val job = parsed.jobs[0]
assertNull(job.title)
assertNull(job.created_at)
assertEquals("cover_letter", job.type)
}
@Test
fun `empty jobs list parses to empty collection`() {
val json = """{"jobs": []}"""
val parsed = gson.fromJson(json, JobListResponse::class.java)
assertTrue(parsed.jobs.isEmpty())
}
@Test
fun `JobSummary fields are immutable`() {
val job = JobSummary(job_id = "id", type = "resume", status = "completed")
// Reflectively assert all fields are val (final). This guards against
// regressions that would make the model mutable and thread-unsafe.
for (field in JobSummary::class.java.declaredFields) {
assertEquals("Field ${field.name} must be final", java.lang.reflect.Modifier.FINAL, field.modifiers and java.lang.reflect.Modifier.FINAL)
}
}
}

View File

@ -10,18 +10,9 @@ okhttp = "4.12.0"
gson = "2.10.1" gson = "2.10.1"
browser = "1.7.0" browser = "1.7.0"
googlePlayServicesAuth = "20.7.0" googlePlayServicesAuth = "20.7.0"
credentialManager = "1.5.0-beta01"
googleid = "1.1.1"
kotlinxCoroutines = "1.9.0"
lifecycleViewmodel = "2.8.7"
lifecycleRuntime = "2.8.7"
fragmentKtx = "1.8.5"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodel" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@ -31,10 +22,6 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
browser = { group = "androidx.browser", name = "browser", version.ref = "browser" } browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayServicesAuth" } google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayServicesAuth" }
androidx-credential-manager = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" }
androidx-credential-manager-play-services = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialManager" }
googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleid" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }