Compare commits

..

11 Commits

Author SHA1 Message Date
e9ba43e09a Merge pull request 'My Resumes history, modern M3 redesign, authenticated exports' (#2) from feature/my-resumes-modern into master
Reviewed-on: #2
2026-08-04 23:58:12 +10:00
9b0253cb6c
build: inject API base URL via buildConfigField instead of hardcoding
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:56:38 +10:00
e90af27e66
fix: download PDF/DOCX exports through authenticated client instead of browser
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:50:18 +10:00
5754fcf445
design: modernize Material 3 UI with drawer navigation and richer teal theme
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:49:37 +10:00
54b002b72f
test: add Gson parsing tests for job list models
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:14:37 +10:00
1d37371431
build: enable R8 minification and fix release lint namespace typos
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:14:30 +10:00
1333db5d1d
security: enforce HTTPS via network security config
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:14:12 +10:00
43d1a6013b
refactor: move network logic into ViewModels and externalize strings
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:14:06 +10:00
98b15c748a
chore: remove .idea from version control
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-04 23:13:44 +10:00
3c9553ad39
updating colors and api requested for backend 2026-08-04 22:44:19 +10:00
c589e5f9e4
design: Material Design 3 redesign across all layouts and themes
- colors.xml: replaced legacy palette with M3 semantic tokens
  (primary, secondary, tertiary, surface containers, outline, status)
- themes.xml + values-night/themes.xml: switched to
  Theme.Material3.DayNight.NoActionBar, mapped all M3 color roles,
  transparent system bars for edge-to-edge, M3 typography
- attrs.xml: custom semantic attrs (colorSuccess, colorWarning, colorInfo)
- strings.xml: added accessibility labels, loading/empty/error strings
- activity_login.xml: centered branding, M3 headline/body, skeleton card
- activity_main.xml: edge-to-edge CoordinatorLayout, M3 AppBar/toolbar,
  health indicator group, M3 banner, M3 TabLayout
- fragment_job_form.xml: grouped fields in outlined cards (12dp radius),
  FilledBox text inputs, TonalButtons, 16dp margins
- fragment_job_status.xml: chip-style badge, LinearProgressIndicator,
  centered card with error/try-again states
- fragment_result.xml: SuggestionChip badge, WebView fills space,
  bottom action bar in outlined card
- activity_admin.xml + item_admin_user.xml: M3 card list items,
  proper type scale hierarchy
- Updated drawables to use theme attributes

Refs DESIGN.md Material Design 3 + Amazon/Google guidelines.
2026-07-18 23:48:33 +10:00
54 changed files with 2091 additions and 559 deletions

7
.gitignore vendored
View File

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

3
.idea/.gitignore generated vendored
View File

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

1
.idea/.name generated
View File

@ -1 +0,0 @@
resBuilder

View File

@ -1,6 +0,0 @@
<?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
View File

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

View File

@ -1,11 +0,0 @@
<?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>

View File

@ -1,13 +0,0 @@
<?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
View File

@ -1,17 +0,0 @@
<?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
View File

@ -1,10 +0,0 @@
<?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>

View File

@ -1,17 +0,0 @@
<?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>

133
GOOGLE_AUTH_SETUP.md Normal file
View File

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

View File

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

11
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,11 @@
# 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,6 +11,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ResBuilder"
@ -33,6 +34,16 @@
android:name=".ui.AdminActivity"
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>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,133 @@
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

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

View File

@ -13,11 +13,14 @@ import android.webkit.WebView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import com.example.resbuilder.R
import com.example.resbuilder.data.model.Job
import com.example.resbuilder.data.remote.ApiClient
import com.google.android.material.button.MaterialButton
import java.io.File
import java.io.FileOutputStream
class ResultFragment : Fragment() {
@ -86,14 +89,58 @@ class ResultFragment : Fragment() {
newButton.setOnClickListener { listener?.onNewJob() }
jobId?.let { id ->
pdfButton.setOnClickListener { openUrl(ApiClient.getExportPdfUrl(id)) }
docxButton.setOnClickListener { openUrl(ApiClient.getExportDocxUrl(id)) }
pdfButton.setOnClickListener { downloadAndOpen(id, "pdf") }
docxButton.setOnClickListener { downloadAndOpen(id, "docx") }
} ?: run {
pdfButton.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() {
val raw = rawContent
if (raw.isNullOrEmpty()) {
@ -105,11 +152,6 @@ class ResultFragment : Fragment() {
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) {
listener = l
}

View File

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

View File

@ -0,0 +1,9 @@
<?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

@ -0,0 +1,9 @@
<?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

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

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:fillColor="?attr/colorOnSurface"
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" />
</vector>

View File

@ -0,0 +1,10 @@
<?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

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

View File

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,10 @@
<?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,25 +1,50 @@
<?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:orientation="vertical">
android:background="?attr/colorSurface"
android:orientation="vertical"
tools:context=".ui.AdminActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/adminToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
android:background="?attr/colorSurface"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationIconTint="?attr/colorOnSurface"
app:title="@string/admin_title"
xmlns:app="http://schemas.android.com/apk/res-auto" />
app:titleCentered="true"
app:titleTextAppearance="?attr/textAppearanceTitleLarge"
app:titleTextColor="?attr/colorOnSurface" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/adminRecyclerView"
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="0dp"
android:layout_margin="16dp"
android:layout_weight="1"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/adminRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:contentDescription="@string/admin_title"
android:padding="8dp" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -1,42 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
android:background="?attr/colorSurface"
tools:context=".ui.LoginActivity">
<ProgressBar
android:id="@+id/loginProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/loginTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/loginTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/app_title"
android:textAppearance="?attr/textAppearanceHeadline3"
android:textColor="?attr/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/googleSignInButton"
<!-- Decorative gradient background behind card -->
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_gradient_primary"
app:layout_constraintBottom_toTopOf="@id/guidelineTop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.gms.common.SignInButton
android:id="@+id/googleSignInButton"
<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:layout_marginTop="16dp"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.40" />
<!-- Main content card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/loginCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_lg"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius_large"
app:cardElevation="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginTitle" />
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="@dimen/space_xl">
<!-- App icon with tonal surface -->
<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_large"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/cd_app_logo"
android:padding="@dimen/space_md"
android:scaleType="centerInside"
android:src="@drawable/ic_launcher_foreground"
app:tint="?attr/colorOnPrimaryContainer" />
</com.google.android.material.card.MaterialCardView>
<!-- Title -->
<TextView
android:id="@+id/loginTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_lg"
android:text="@string/app_title"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceHeadlineMedium"
android:textColor="?attr/colorOnSurface"
android:textStyle="bold" />
<!-- Subtitle -->
<TextView
android:id="@+id/loginSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm"
android:text="@string/welcome_subtitle"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurfaceVariant" />
<!-- Divider -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="@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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guidelineTop"
app:trackColor="?attr/colorPrimaryContainer" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,77 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?attr/colorSurface"
android:fitsSystemWindows="true"
tools:context=".ui.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
<!-- Main content -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
<!-- Modern colored app bar with gradient surface and centered title -->
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Dark"
app:title="@string/app_title"
app:titleTextColor="@color/white">
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:fitsSystemWindows="true"
app:elevation="0dp">
<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:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/bg_gradient_primary_bottom"
android:elevation="0dp"
app:navigationIcon="@drawable/ic_menu_hamburger"
app:navigationIconTint="?attr/colorOnPrimary"
app:title="@string/app_title"
app:titleCentered="true"
app:titleTextAppearance="?attr/textAppearanceTitleLarge"
app:titleTextColor="?attr/colorOnPrimary" />
</com.google.android.material.appbar.AppBarLayout>
<ImageView
android:id="@+id/healthIndicator"
android:layout_width="12dp"
android:layout_height="12dp"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_circle_status" />
<TextView
android:id="@+id/usageBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/space_md"
android:layout_marginTop="@dimen/space_sm"
android:background="@drawable/bg_banner_warning"
android:drawableStart="@drawable/ic_circle_status"
android:drawableTint="?attr/colorWarning"
android:gravity="center_vertical"
android:minHeight="@dimen/touch_target_min"
android:padding="@dimen/space_sm"
android:text="@string/usage_limit_banner"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurface"
android:visibility="gone"
tools:visibility="visible" />
<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/textAppearanceBody2"
android:textColor="@color/white" />
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<!-- Navigation drawer with richer surface and modern item styling -->
<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" />
<TextView
android:id="@+id/usageBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/bg_banner_warning"
android:padding="12dp"
android:text="@string/usage_limit_banner"
android:textColor="@color/banner_warning_text"
android:visibility="gone" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed" />
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -1,111 +1,249 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:fillViewport="true"
android:padding="16dp">
android:clipToPadding="false"
android:paddingBottom="@dimen/space_md"
tools:context=".ui.JobFormFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
<!-- Hero header with gradient surface and section title -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_job_title"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
android:background="@drawable/bg_gradient_primary"
android:minHeight="@dimen/hero_header_min_height"
android:padding="@dimen/space_lg">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/jobTitleInput"
<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:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
android:orientation="vertical"
android:padding="@dimen/space_md">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
<!-- Job details card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/hint_seek_url"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/seekUrlInput"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
android:orientation="vertical"
android:padding="@dimen/space_md">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hint_job_title"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm"
android:hint="@string/hint_job_title"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
app:boxStrokeColor="?attr/colorPrimary"
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/jobTitleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:minHeight="@dimen/touch_target_min" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm"
android:baselineAligned="false"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/hint_seek_url"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
app:boxStrokeColor="?attr/colorPrimary"
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/seekUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:minHeight="@dimen/touch_target_min" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/scrapeButton"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/button_height"
android:layout_marginStart="@dimen/space_sm"
android:contentDescription="@string/cd_scrape_job"
android:minWidth="64dp"
android:text="@string/button_scrape" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_md"
android:hint="@string/hint_job_description"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
app:boxStrokeColor="?attr/colorPrimary"
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/jobDescriptionInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:minLines="4"
android:minHeight="@dimen/touch_target_min" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Resume / cover letter input card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_md"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/space_md">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/docInputTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hint_your_resume"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/uploadButton"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="@dimen/button_height"
android:contentDescription="@string/cd_upload_document"
android:text="@string/button_upload" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/docInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_sm"
android:hint="@string/hint_your_resume"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
app:boxStrokeColor="?attr/colorPrimary"
app:cursorColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/docInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:minLines="6"
android:minHeight="@dimen/touch_target_min" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/scrapeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/button_scrape" />
android:id="@+id/submitButton"
android:layout_width="match_parent"
android:layout_height="@dimen/button_height"
android:layout_marginTop="@dimen/space_md"
android:text="@string/generate_resume" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="@dimen/space_xxl"
android:layout_height="@dimen/space_xxl"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/space_md"
android:contentDescription="@string/cd_loading"
android:indeterminateTint="?attr/colorPrimary"
android:visibility="gone" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_job_description"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/jobDescriptionInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:minLines="4" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/uploadButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/button_upload" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/docInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_your_resume"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/docInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:minLines="6" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/submitButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/generate_resume" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:visibility="gone" />
</LinearLayout>
</ScrollView>

View File

@ -1,68 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
android:padding="@dimen/space_md">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
android:layout_marginHorizontal="@dimen/space_md"
android:layout_marginVertical="@dimen/space_sm"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius_large"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
android:padding="@dimen/space_lg">
<TextView
android:id="@+id/statusBadge"
<!-- Status badge: now a tonal chip-style pill -->
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_banner_warning"
android:paddingHorizontal="16dp"
android:paddingVertical="4dp"
android:text="@string/status_queued"
android:textAppearance="?attr/textAppearanceBody2"
android:textStyle="bold" />
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<ProgressBar
<TextView
android:id="@+id/statusBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/status_queued"
android:gravity="center"
android:minHeight="32dp"
android:paddingHorizontal="@dimen/space_md"
android:paddingVertical="4dp"
android:text="@string/status_queued"
android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorOnPrimaryContainer" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/statusProgress"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp" />
android:layout_marginTop="@dimen/space_lg"
android:contentDescription="@string/content_generation_running"
android:indeterminate="true"
app:indicatorColor="?attr/colorPrimary"
app:trackColor="?attr/colorSurfaceContainerHighest" />
<TextView
android:id="@+id/statusMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginTop="@dimen/space_md"
android:gravity="center"
android:text="@string/job_wait_message"
android:textAppearance="?attr/textAppearanceBody1" />
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/statusDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceCaption"
android:layout_marginTop="@dimen/space_sm"
android:gravity="center"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/tryAgainButton"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:layout_height="@dimen/button_height"
android:layout_marginTop="@dimen/space_lg"
android:minWidth="120dp"
android:text="@string/try_again"
android:visibility="gone" />
</LinearLayout>

View File

@ -0,0 +1,88 @@
<?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,33 +1,55 @@
<?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:orientation="vertical">
android:background="?attr/colorSurface"
android:orientation="vertical"
tools:context=".ui.ResultFragment">
<!-- Top action bar card with tonal surface -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
android:layout_marginHorizontal="@dimen/space_md"
android:layout_marginTop="@dimen/space_md"
android:layout_marginBottom="@dimen/space_sm"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="@dimen/touch_target_min"
android:orientation="horizontal"
android:padding="12dp">
android:padding="@dimen/space_sm">
<TextView
android:id="@+id/resultBadge"
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_banner_warning"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp"
android:text="@string/status_completed"
android:textStyle="bold" />
app:cardBackgroundColor="?attr/colorSuccessContainer"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<TextView
android:id="@+id/resultBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/status_completed"
android:gravity="center"
android:minHeight="32dp"
android:paddingHorizontal="@dimen/space_md"
android:paddingVertical="4dp"
android:text="@string/status_completed"
android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorOnSuccessContainer" />
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="0dp"
@ -36,16 +58,21 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/copyButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="@dimen/button_height"
android:contentDescription="@string/cd_copy_result"
android:minWidth="64dp"
android:text="@string/copy_text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/newButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="@dimen/button_height"
android:layout_marginStart="@dimen/space_xs"
android:contentDescription="@string/cd_new_job"
android:minWidth="64dp"
android:text="@string/new_job" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@ -54,31 +81,48 @@
android:id="@+id/resultWebView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="12dp"
android:layout_weight="1" />
android:layout_marginHorizontal="@dimen/space_md"
android:layout_weight="1"
android:contentDescription="@string/result_preview" />
<LinearLayout
<!-- Export action bar with tonal surface -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:orientation="horizontal">
android:layout_marginHorizontal="@dimen/space_md"
android:layout_marginTop="@dimen/space_sm"
android:layout_marginBottom="@dimen/space_md"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="@dimen/card_radius"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/pdfButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="@string/export_pdf" />
android:orientation="horizontal"
android:padding="@dimen/space_sm">
<com.google.android.material.button.MaterialButton
android:id="@+id/docxButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/export_docx" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/pdfButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="@dimen/button_height"
android:layout_marginEnd="@dimen/space_sm"
android:layout_weight="1"
android:contentDescription="@string/cd_export_pdf"
android:text="@string/export_pdf" />
<com.google.android.material.button.MaterialButton
android:id="@+id/docxButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="@dimen/button_height"
android:layout_marginStart="@dimen/space_sm"
android:layout_weight="1"
android:contentDescription="@string/cd_export_docx"
android:text="@string/export_docx" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -3,13 +3,18 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="4dp"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutlineVariant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="64dp"
android:orientation="horizontal"
android:padding="16dp">
@ -23,28 +28,35 @@
android:id="@+id/userEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:textStyle="bold" />
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/userName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody2" />
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/userUsage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceCaption" />
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/actionButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:text="@string/block_user" />
android:contentDescription="@string/cd_admin_action"
android:minWidth="72dp"
android:text="@string/block_user"
android:textColor="?attr/colorError" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,76 @@
<?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

@ -0,0 +1,49 @@
<?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

@ -1,10 +0,0 @@
<?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

@ -0,0 +1,39 @@
<?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,16 +1,92 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ResBuilder" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary color roles (dark): bright teal on deep surfaces -->
<item name="colorPrimary">#80DADC</item>
<item name="colorOnPrimary">#003738</item>
<item name="colorPrimaryContainer">#005A5C</item>
<item name="colorOnPrimaryContainer">#B5F2F3</item>
<item name="colorPrimaryInverse">#004F4F</item>
<item name="colorPrimaryFixed">#80DADC</item>
<item name="colorOnPrimaryFixed">#002020</item>
<!-- Secondary color roles (dark) -->
<item name="colorSecondary">#9CBEBD</item>
<item name="colorOnSecondary">#1B3534</item>
<item name="colorSecondaryContainer">#2F4A49</item>
<item name="colorOnSecondaryContainer">#BDD6D5</item>
<!-- Tertiary color roles (dark) -->
<item name="colorTertiary">#A4C9E8</item>
<item name="colorOnTertiary">#0B2D45</item>
<item name="colorTertiaryContainer">#27475F</item>
<item name="colorOnTertiaryContainer">#BFE0FF</item>
<!-- Surface color roles (dark): deep charcoal-teal -->
<item name="android:colorBackground">#0C1414</item>
<item name="colorSurface">#0C1414</item>
<item name="colorOnSurface">#E0ECEC</item>
<item name="colorSurfaceVariant">#3F4A4A</item>
<item name="colorOnSurfaceVariant">#BEC9C9</item>
<item name="colorSurfaceContainerLowest">#070E0E</item>
<item name="colorSurfaceContainerLow">#121B1B</item>
<item name="colorSurfaceContainer">#182222</item>
<item name="colorSurfaceContainerHigh">#242D2D</item>
<item name="colorSurfaceContainerHighest">#303939</item>
<item name="colorSurfaceInverse">#E0ECEC</item>
<item name="colorOnSurfaceInverse">#0C1414</item>
<!-- Outline -->
<item name="colorOutline">#899999</item>
<item name="colorOutlineVariant">#3F4A4A</item>
<!-- Error (dark) -->
<item name="colorError">#F2B8B5</item>
<item name="colorOnError">#601410</item>
<item name="colorErrorContainer">#8C1D18</item>
<item name="colorOnErrorContainer">#F9DEDC</item>
<!-- Status and custom semantic color attrs (dark) -->
<item name="colorSuccess">#81C784</item>
<item name="colorOnSuccess">#003D00</item>
<item name="colorSuccessContainer">#1B5E1F</item>
<item name="colorOnSuccessContainer">#D9F6DA</item>
<item name="colorWarning">#FFB74D</item>
<item name="colorOnWarning">#4A2C00</item>
<item name="colorWarningContainer">#693B00</item>
<item name="colorOnWarningContainer">#FFE3B6</item>
<item name="colorInfo">#64B5F6</item>
<item name="colorOnInfo">#0B4576</item>
<item name="colorInfoContainer">#0B4576</item>
<item name="colorOnInfoContainer">#D4EAFB</item>
<!-- System bars -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
<!-- Typography -->
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
<item name="textAppearanceHeadlineLarge">@style/TextAppearance.Material3.HeadlineLarge</item>
<item name="textAppearanceHeadlineMedium">@style/TextAppearance.Material3.HeadlineMedium</item>
<item name="textAppearanceHeadlineSmall">@style/TextAppearance.Material3.HeadlineSmall</item>
<item name="textAppearanceTitleLarge">@style/TextAppearance.Material3.TitleLarge</item>
<item name="textAppearanceTitleMedium">@style/TextAppearance.Material3.TitleMedium</item>
<item name="textAppearanceTitleSmall">@style/TextAppearance.Material3.TitleSmall</item>
<item name="textAppearanceBodyLarge">@style/TextAppearance.Material3.BodyLarge</item>
<item name="textAppearanceBodyMedium">@style/TextAppearance.Material3.BodyMedium</item>
<item name="textAppearanceBodySmall">@style/TextAppearance.Material3.BodySmall</item>
<item name="textAppearanceLabelLarge">@style/TextAppearance.Material3.LabelLarge</item>
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
<!-- Component defaults -->
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
<item name="android:windowBackground">?attr/colorSurface</item>
</style>
</resources>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Semantic status color attributes used across ResBuilder UI -->
<attr name="colorSuccess" format="color" />
<attr name="colorOnSuccess" format="color" />
<attr name="colorSuccessContainer" format="color" />
<attr name="colorOnSuccessContainer" format="color" />
<attr name="colorWarning" format="color" />
<attr name="colorOnWarning" format="color" />
<attr name="colorWarningContainer" format="color" />
<attr name="colorOnWarningContainer" format="color" />
<attr name="colorInfo" format="color" />
<attr name="colorOnInfo" format="color" />
<attr name="colorInfoContainer" format="color" />
<attr name="colorOnInfoContainer" format="color" />
</resources>

View File

@ -1,20 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<!-- Material Design 3 semantic color tokens -->
<!-- Primary palette: rich teal with stronger contrast -->
<color name="primary">#004F4F</color>
<color name="on_primary">#FFFFFF</color>
<color name="primary_container">#B5F2F3</color>
<color name="on_primary_container">#002020</color>
<color name="primary_fixed">#B5F2F3</color>
<color name="on_primary_fixed">#002020</color>
<!-- Secondary palette: cool slate with more depth -->
<color name="secondary">#3D5252</color>
<color name="on_secondary">#FFFFFF</color>
<color name="secondary_container">#BDD6D5</color>
<color name="on_secondary_container">#041E1E</color>
<!-- Tertiary palette: deep blue-grey accent -->
<color name="tertiary">#365064</color>
<color name="on_tertiary">#FFFFFF</color>
<color name="tertiary_container">#BFE0FF</color>
<color name="on_tertiary_container">#001D31</color>
<!-- Surface colors: warmer off-white base with visible tonal elevation -->
<color name="surface">#FAFDFD</color>
<color name="on_surface">#0A1414</color>
<color name="surface_variant">#D8E5E5</color>
<color name="on_surface_variant">#3F4A4A</color>
<color name="surface_container_lowest">#FFFFFF</color>
<color name="surface_container_low">#F0F6F6</color>
<color name="surface_container">#E8F1F1</color>
<color name="surface_container_high">#DFEAEA</color>
<color name="surface_container_highest">#D6E4E4</color>
<!-- Outline -->
<color name="outline">#6F7E7E</color>
<color name="outline_variant">#BEC9C9</color>
<!-- Status / Semantic colors: richer and more saturated -->
<color name="success">#2E7D32</color>
<color name="on_success">#FFFFFF</color>
<color name="success_container">#D9F6DA</color>
<color name="on_success_container">#1F5E22</color>
<color name="warning">#E65100</color>
<color name="on_warning">#FFFFFF</color>
<color name="warning_container">#FFE3B6</color>
<color name="on_warning_container">#693B00</color>
<color name="error">#C62828</color>
<color name="on_error">#FFFFFF</color>
<color name="error_container">#F9DEDC</color>
<color name="on_error_container">#410E0B</color>
<color name="info">#1565C0</color>
<color name="on_info">#FFFFFF</color>
<color name="info_container">#D4EAFB</color>
<color name="on_info_container">#0B4576</color>
<!-- Inverse surface -->
<color name="inverse_surface">#2D3131</color>
<color name="inverse_on_surface">#DFE4E4</color>
<color name="inverse_primary">#80DADC</color>
<!-- Status indicator palettes -->
<color name="status_healthy">@color/success</color>
<color name="status_degraded">@color/warning</color>
<color name="status_unhealthy">@color/error</color>
<!-- Legacy aliases -->
<color name="purple_200">@color/primary_container</color>
<color name="purple_500">@color/primary</color>
<color name="purple_700">@color/on_primary_container</color>
<color name="teal_200">@color/primary_container</color>
<color name="teal_700">@color/primary</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="status_healthy">#FF4CAF50</color>
<color name="status_degraded">#FFFFC107</color>
<color name="status_unhealthy">#FFF44336</color>
<color name="banner_warning">#FFFFF3E0</color>
<color name="banner_warning_text">#FFE65100</color>
<color name="status_badge_queued">#FF9E9E9E</color>
<color name="status_badge_running">#FF2196F3</color>
<color name="status_badge_completed">#FF4CAF50</color>
<color name="status_badge_failed">#FFF44336</color>
<color name="banner_warning">@color/warning_container</color>
<color name="banner_warning_text">@color/on_warning_container</color>
<color name="status_badge_queued">@color/surface_container_highest</color>
<color name="status_badge_running">@color/info</color>
<color name="status_badge_completed">@color/success</color>
<color name="status_badge_failed">@color/error</color>
</resources>

View File

@ -0,0 +1,18 @@
<?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,6 +4,16 @@
<!-- Login -->
<string name="sign_in_with_google">Sign in with Google</string>
<string name="checking_auth">Checking authentication…</string>
<string name="welcome_subtitle">AI-powered resume and cover letter builder</string>
<string name="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
<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 -->
<string name="app_title">resBuilder</string>
@ -13,6 +23,7 @@
<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="unknown_user">Unknown user</string>
<string name="unknown_email">Not signed in</string>
<!-- Job form -->
<string name="hint_job_title">Job Title</string>
@ -26,6 +37,7 @@
<string name="generate_cover_letter">Generate Cover Letter</string>
<string name="please_log_in">Please log in to continue</string>
<string name="fields_required">Please fill in all fields</string>
<string name="enter_url">Enter a URL</string>
<!-- Status -->
<string name="status_queued">Queued</string>
@ -44,6 +56,22 @@
<string name="export_docx">DOCX</string>
<string name="copied_to_clipboard">Copied to clipboard</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 -->
<string name="cd_app_logo">resBuilder app logo</string>
<string name="cd_google_sign_in">Sign in with Google</string>
<string name="cd_loading">Loading</string>
<string name="cd_health_indicator">Service health indicator</string>
<string name="cd_scrape_job">Scrape job description from SEEK URL</string>
<string name="cd_upload_document">Upload existing resume or cover letter</string>
<string name="cd_copy_result">Copy generated text to clipboard</string>
<string name="cd_new_job">Start a new job</string>
<string name="cd_export_pdf">Export as PDF</string>
<string name="cd_export_docx">Export as DOCX</string>
<string name="cd_admin_action">Toggle user block status</string>
<!-- Admin -->
<string name="admin_title">Admin Panel</string>
@ -52,4 +80,22 @@
<string name="admin_action_success">Action completed</string>
<string name="admin_action_failed">Action failed</string>
<string name="usage_format">%1$d / %2$d</string>
<!-- Screen reader / empty / loading labels -->
<string name="content_generation_running">Content generation is running</string>
<string name="estimated_time">Estimated: %1$s</string>
<string name="empty_state_title">Nothing here yet</string>
<string name="empty_state_message">Submit a job to see results.</string>
<string name="error_state_title">Something went wrong</string>
<string name="error_state_message">We could not load this screen. Please try again.</string>
<string name="usage_limit_title">Free limit reached</string>
<string name="result_preview">Result preview</string>
<!-- My Resumes -->
<string name="menu_my_resumes">My Resumes</string>
<string name="my_resumes_title">My Documents</string>
<string name="my_resumes_empty">No generated documents yet. Submit a job to see your results here.</string>
<string name="my_resumes_error">Could not load your documents. Please try again.</string>
<string name="type_resume">Resume</string>
<string name="type_cover_letter">Cover letter</string>
</resources>

View File

@ -1,16 +1,93 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ResBuilder" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary color roles -->
<item name="colorPrimary">@color/primary</item>
<item name="colorOnPrimary">@color/on_primary</item>
<item name="colorPrimaryContainer">@color/primary_container</item>
<item name="colorOnPrimaryContainer">@color/on_primary_container</item>
<item name="colorPrimaryInverse">@color/inverse_primary</item>
<item name="colorPrimaryFixed">@color/primary_fixed</item>
<item name="colorOnPrimaryFixed">@color/on_primary_fixed</item>
<!-- Secondary color roles -->
<item name="colorSecondary">@color/secondary</item>
<item name="colorOnSecondary">@color/on_secondary</item>
<item name="colorSecondaryContainer">@color/secondary_container</item>
<item name="colorOnSecondaryContainer">@color/on_secondary_container</item>
<!-- Tertiary color roles -->
<item name="colorTertiary">@color/tertiary</item>
<item name="colorOnTertiary">@color/on_tertiary</item>
<item name="colorTertiaryContainer">@color/tertiary_container</item>
<item name="colorOnTertiaryContainer">@color/on_tertiary_container</item>
<!-- Surface color roles -->
<item name="android:colorBackground">@color/surface</item>
<item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/on_surface</item>
<item name="colorSurfaceVariant">@color/surface_variant</item>
<item name="colorOnSurfaceVariant">@color/on_surface_variant</item>
<item name="colorSurfaceContainerLowest">@color/surface_container_lowest</item>
<item name="colorSurfaceContainerLow">@color/surface_container_low</item>
<item name="colorSurfaceContainer">@color/surface_container</item>
<item name="colorSurfaceContainerHigh">@color/surface_container_high</item>
<item name="colorSurfaceContainerHighest">@color/surface_container_highest</item>
<item name="colorSurfaceInverse">@color/inverse_surface</item>
<item name="colorOnSurfaceInverse">@color/inverse_on_surface</item>
<!-- Outline -->
<item name="colorOutline">@color/outline</item>
<item name="colorOutlineVariant">@color/outline_variant</item>
<!-- Error -->
<item name="colorError">@color/error</item>
<item name="colorOnError">@color/on_error</item>
<item name="colorErrorContainer">@color/error_container</item>
<item name="colorOnErrorContainer">@color/on_error_container</item>
<!-- Status and custom semantic color attrs -->
<item name="colorSuccess">@color/success</item>
<item name="colorOnSuccess">@color/on_success</item>
<item name="colorSuccessContainer">@color/success_container</item>
<item name="colorOnSuccessContainer">@color/on_success_container</item>
<item name="colorWarning">@color/warning</item>
<item name="colorOnWarning">@color/on_warning</item>
<item name="colorWarningContainer">@color/warning_container</item>
<item name="colorOnWarningContainer">@color/on_warning_container</item>
<item name="colorInfo">@color/info</item>
<item name="colorOnInfo">@color/on_info</item>
<item name="colorInfoContainer">@color/info_container</item>
<item name="colorOnInfoContainer">@color/on_info_container</item>
<!-- System bars - edge-to-edge ready -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<!-- M3 Typography -->
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
<item name="textAppearanceHeadlineLarge">@style/TextAppearance.Material3.HeadlineLarge</item>
<item name="textAppearanceHeadlineMedium">@style/TextAppearance.Material3.HeadlineMedium</item>
<item name="textAppearanceHeadlineSmall">@style/TextAppearance.Material3.HeadlineSmall</item>
<item name="textAppearanceTitleLarge">@style/TextAppearance.Material3.TitleLarge</item>
<item name="textAppearanceTitleMedium">@style/TextAppearance.Material3.TitleMedium</item>
<item name="textAppearanceTitleSmall">@style/TextAppearance.Material3.TitleSmall</item>
<item name="textAppearanceBodyLarge">@style/TextAppearance.Material3.BodyLarge</item>
<item name="textAppearanceBodyMedium">@style/TextAppearance.Material3.BodyMedium</item>
<item name="textAppearanceBodySmall">@style/TextAppearance.Material3.BodySmall</item>
<item name="textAppearanceLabelLarge">@style/TextAppearance.Material3.LabelLarge</item>
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
<!-- Default M3 component styles -->
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
<item name="android:windowBackground">?attr/colorSurface</item>
</style>
</resources>
</resources>

View File

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

View File

@ -0,0 +1,8 @@
<?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

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