Compare commits
No commits in common. "master" and "design/material3-redesign" have entirely different histories.
master
...
design/mat
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,7 +1,12 @@
|
|||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
|||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
resBuilder
|
||||||
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
.idea/deploymentTargetSelector.xml
generated
Normal file
11
.idea/deploymentTargetSelector.xml
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
.idea/gradle.xml
generated
Normal file
17
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/misc.xml
generated
Normal file
10
.idea/misc.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
.idea/runConfigurations.xml
generated
Normal file
17
.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -1,133 +0,0 @@
|
|||||||
# Google Authentication Setup
|
|
||||||
|
|
||||||
## OAuth Credentials Summary
|
|
||||||
|
|
||||||
You now have **TWO** OAuth credentials configured in Google Cloud Console:
|
|
||||||
|
|
||||||
### 1. Web Application (for backend)
|
|
||||||
- **Client ID**: `416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com`
|
|
||||||
- **Type**: Web application
|
|
||||||
- **Purpose**: Backend token exchange (`POST /auth/google-token`)
|
|
||||||
- **Authorized redirect URIs**: Configured on your backend
|
|
||||||
|
|
||||||
### 2. Android Application (for mobile app)
|
|
||||||
- **Client ID**: `416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com`
|
|
||||||
- **Type**: Android
|
|
||||||
- **Package name**: `com.example.resbuilder`
|
|
||||||
- **SHA-1 fingerprint**: `38:A9:E7:27:69:40:77:77:A7:3D:A1:AC:21:F2:8E:50:5D:C9:AC:7F`
|
|
||||||
- **Purpose**: Android Credential Manager + Google Sign-In
|
|
||||||
|
|
||||||
## What Was Changed
|
|
||||||
|
|
||||||
### Dependencies Added
|
|
||||||
```kotlin
|
|
||||||
// gradle/libs.versions.toml
|
|
||||||
credentialManager = "1.5.0-beta01"
|
|
||||||
googleid = "1.1.1"
|
|
||||||
kotlinxCoroutines = "1.9.0"
|
|
||||||
|
|
||||||
androidx-credential-manager
|
|
||||||
androidx-credential-manager-play-services
|
|
||||||
googleid
|
|
||||||
kotlinx-coroutines-android
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Migration
|
|
||||||
|
|
||||||
The `LoginActivity.kt` now uses:
|
|
||||||
1. **Credential Manager API** (primary) - Modern, recommended approach
|
|
||||||
2. **Legacy Google Sign-In** (fallback) - For devices that don't support Credential Manager
|
|
||||||
|
|
||||||
Key changes:
|
|
||||||
- Uses `androidClientId` for Credential Manager flow
|
|
||||||
- Uses `webClientId` for backend token exchange
|
|
||||||
- Automatic fallback to legacy API if Credential Manager fails
|
|
||||||
- Coroutines for suspend function support
|
|
||||||
- Nonce generation for enhanced security
|
|
||||||
|
|
||||||
## Verification Steps
|
|
||||||
|
|
||||||
1. **Build the app**:
|
|
||||||
```bash
|
|
||||||
./gradlew assembleDebug
|
|
||||||
```
|
|
||||||
APK: `app/build/outputs/apk/debug/app-debug.apk`
|
|
||||||
|
|
||||||
2. **Install on device/emulator** with your debug keystore SHA-1
|
|
||||||
|
|
||||||
3. **Test sign-in flow**:
|
|
||||||
- Tap "Sign in with Google"
|
|
||||||
- Account picker should appear
|
|
||||||
- Select an account
|
|
||||||
- Should authenticate and navigate to main screen
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Error 10: Developer Error
|
|
||||||
- SHA-1 fingerprint doesn't match Google Cloud Console
|
|
||||||
- Package name doesn't match
|
|
||||||
- **Fix**: Verify SHA-1 in Google Cloud Console matches `38:A9:E7:27:69:40:77:77:A7:3D:A1:AC:21:F2:8E:50:5D:C9:AC:7F`
|
|
||||||
|
|
||||||
### Error 16: Sign In Cancelled
|
|
||||||
- User cancelled the account picker
|
|
||||||
- Normal behavior, not an error
|
|
||||||
|
|
||||||
### Credential Manager not working
|
|
||||||
- Device may not support Credential Manager
|
|
||||||
- **Fix**: App automatically falls back to legacy Google Sign-In
|
|
||||||
|
|
||||||
## API Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐
|
|
||||||
│ User taps │
|
|
||||||
│ Sign In │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Credential │
|
|
||||||
│ Manager │◄─── Uses androidClientId
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Google returns │
|
|
||||||
│ ID Token │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌─────────────────┐
|
|
||||||
│ App sends ID │
|
|
||||||
│ Token to backend│
|
|
||||||
│ POST /auth/ │
|
|
||||||
│ google-token │◄─── Backend validates with webClientId
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Backend returns │
|
|
||||||
│ session token │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
v
|
|
||||||
┌─────────────────┐
|
|
||||||
│ App stores │
|
|
||||||
│ session token │
|
|
||||||
│ (Bearer auth) │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps (Optional)
|
|
||||||
|
|
||||||
For production release:
|
|
||||||
1. Get **release SHA-1** from Play Console or your release keystore
|
|
||||||
2. Add release SHA-1 to Google Cloud Console (same Android OAuth client)
|
|
||||||
3. Build signed release APK/AAB
|
|
||||||
4. Test with release credentials
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Credential Manager Documentation](https://developer.android.com/identity/sign-in/credential-manager)
|
|
||||||
- [Google Sign-In Migration Guide](https://developers.google.com/identity/openid-connect/openid-connect)
|
|
||||||
- [OAuth 2.0 for Android](https://developers.google.com/identity/protocols/oauth2/native-app)
|
|
||||||
@ -18,25 +18,15 @@ android {
|
|||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField("String", "API_BASE", "\"https://resume.aridgwayweb.com\"")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
|
||||||
optimization {
|
optimization {
|
||||||
enable = false
|
enable = false
|
||||||
}
|
}
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
@ -46,18 +36,11 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
||||||
implementation(libs.androidx.fragment.ktx)
|
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
implementation(libs.browser)
|
implementation(libs.browser)
|
||||||
implementation(libs.google.play.services.auth)
|
implementation(libs.google.play.services.auth)
|
||||||
implementation(libs.androidx.credential.manager)
|
|
||||||
implementation(libs.androidx.credential.manager.play.services)
|
|
||||||
implementation(libs.googleid)
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
@ -1,11 +0,0 @@
|
|||||||
# R8/ProGuard rules for resbuilder_app.
|
|
||||||
|
|
||||||
# Gson reflects over the API data models. Keep their fields intact.
|
|
||||||
-keep class com.example.resbuilder.data.model.** { <fields>; }
|
|
||||||
|
|
||||||
# Keep the OkHttp client class and its members (OkHttp ships its own consumer rules,
|
|
||||||
# but ApiClient is referenced reflectively nowhere; keep it to be safe).
|
|
||||||
-keep class com.example.resbuilder.data.remote.ApiClient { *; }
|
|
||||||
|
|
||||||
# Credential Manager / Google Identity libs register via reflection.
|
|
||||||
-keep class com.google.android.libraries.identity.googleid.** { *; }
|
|
||||||
@ -11,7 +11,6 @@
|
|||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ResBuilder"
|
android:theme="@style/Theme.ResBuilder"
|
||||||
@ -34,16 +33,6 @@
|
|||||||
android:name=".ui.AdminActivity"
|
android:name=".ui.AdminActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.fileprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@ -62,15 +62,3 @@ data class AdminUser(
|
|||||||
data class AdminUsersResponse(
|
data class AdminUsersResponse(
|
||||||
val users: List<AdminUser>
|
val users: List<AdminUser>
|
||||||
)
|
)
|
||||||
|
|
||||||
data class JobSummary(
|
|
||||||
val job_id: String,
|
|
||||||
val type: String,
|
|
||||||
val status: String,
|
|
||||||
val title: String? = null,
|
|
||||||
val created_at: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class JobListResponse(
|
|
||||||
val jobs: List<JobSummary>
|
|
||||||
)
|
|
||||||
@ -5,7 +5,6 @@ import com.example.resbuilder.data.model.BuildRequest
|
|||||||
import com.example.resbuilder.data.model.BuildResponse
|
import com.example.resbuilder.data.model.BuildResponse
|
||||||
import com.example.resbuilder.data.model.HealthResponse
|
import com.example.resbuilder.data.model.HealthResponse
|
||||||
import com.example.resbuilder.data.model.Job
|
import com.example.resbuilder.data.model.Job
|
||||||
import com.example.resbuilder.data.model.JobListResponse
|
|
||||||
import com.example.resbuilder.data.model.ScrapeResponse
|
import com.example.resbuilder.data.model.ScrapeResponse
|
||||||
import com.example.resbuilder.data.model.UploadResponse
|
import com.example.resbuilder.data.model.UploadResponse
|
||||||
import com.example.resbuilder.data.model.User
|
import com.example.resbuilder.data.model.User
|
||||||
@ -18,7 +17,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
@ -28,7 +26,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
|
|
||||||
internal const val API_BASE = com.example.resbuilder.BuildConfig.API_BASE
|
internal const val API_BASE = "https://resume.aridgwayweb.com"
|
||||||
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val cookieStore = mutableListOf<Cookie>()
|
private val cookieStore = mutableListOf<Cookie>()
|
||||||
@ -53,8 +51,6 @@ object ApiClient {
|
|||||||
bearerToken?.let { builder.header("Authorization", "Bearer $it") }
|
bearerToken?.let { builder.header("Authorization", "Bearer $it") }
|
||||||
chain.proceed(builder.build())
|
chain.proceed(builder.build())
|
||||||
}
|
}
|
||||||
// HTTP/1.1 only: backend/proxy resets HTTP/2 streams mid-response.
|
|
||||||
.protocols(listOf(Protocol.HTTP_1_1))
|
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(60, TimeUnit.SECONDS)
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
.writeTimeout(60, TimeUnit.SECONDS)
|
.writeTimeout(60, TimeUnit.SECONDS)
|
||||||
@ -119,24 +115,6 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getMyJobs(): JobListResponse {
|
|
||||||
val req = Request.Builder()
|
|
||||||
.url("$API_BASE/jobs")
|
|
||||||
.build()
|
|
||||||
client.newCall(req).execute().use { res ->
|
|
||||||
if (!res.isSuccessful) {
|
|
||||||
val errBody = res.body?.string()
|
|
||||||
val detail = try {
|
|
||||||
gson.fromJson(errBody, Map::class.java)["detail"] as? String
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
throw IOException(detail ?: "Failed to load your documents")
|
|
||||||
}
|
|
||||||
val body = res.body?.string() ?: throw IOException("Empty response")
|
|
||||||
return gson.fromJson(body, JobListResponse::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLoginUrl(): String = "$API_BASE/auth/login"
|
fun getLoginUrl(): String = "$API_BASE/auth/login"
|
||||||
fun getLogoutUrl(): String = "$API_BASE/auth/logout"
|
fun getLogoutUrl(): String = "$API_BASE/auth/logout"
|
||||||
|
|
||||||
@ -313,21 +291,4 @@ object ApiClient {
|
|||||||
|
|
||||||
fun getExportPdfUrl(jobId: String): String = "$API_BASE/export/$jobId/pdf"
|
fun getExportPdfUrl(jobId: String): String = "$API_BASE/export/$jobId/pdf"
|
||||||
fun getExportDocxUrl(jobId: String): String = "$API_BASE/export/$jobId/docx"
|
fun getExportDocxUrl(jobId: String): String = "$API_BASE/export/$jobId/docx"
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun downloadExport(url: String): ByteArray {
|
|
||||||
val req = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.build()
|
|
||||||
client.newCall(req).execute().use { res ->
|
|
||||||
if (!res.isSuccessful) {
|
|
||||||
val errBody = res.body?.string()
|
|
||||||
val detail = try {
|
|
||||||
gson.fromJson(errBody, Map::class.java)["detail"] as? String
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
throw IOException(detail ?: "Export failed")
|
|
||||||
}
|
|
||||||
return res.body?.bytes() ?: throw IOException("Empty response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -147,7 +147,7 @@ class JobFormFragment : Fragment() {
|
|||||||
private fun scrapeUrl() {
|
private fun scrapeUrl() {
|
||||||
val url = seekUrlInput.text?.toString()?.trim() ?: ""
|
val url = seekUrlInput.text?.toString()?.trim() ?: ""
|
||||||
if (url.isEmpty()) {
|
if (url.isEmpty()) {
|
||||||
seekUrlInput.error = getString(R.string.enter_url)
|
seekUrlInput.error = "Enter a URL"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
|
|||||||
@ -2,46 +2,34 @@ package com.example.resbuilder.ui
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.credentials.CredentialManager
|
|
||||||
import androidx.credentials.CustomCredential
|
|
||||||
import androidx.credentials.GetCredentialRequest
|
|
||||||
import androidx.credentials.GetCredentialResponse
|
|
||||||
import androidx.credentials.exceptions.GetCredentialException
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import com.example.resbuilder.R
|
import com.example.resbuilder.R
|
||||||
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
import com.example.resbuilder.data.remote.ApiClient
|
||||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
|
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
import com.google.android.gms.common.SignInButton
|
||||||
import kotlinx.coroutines.launch
|
import com.google.android.gms.common.api.ApiException
|
||||||
import java.security.SecureRandom
|
import com.google.android.gms.tasks.Task
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val viewModel: LoginViewModel by viewModels()
|
private lateinit var googleSignInButton: SignInButton
|
||||||
|
private lateinit var progress: ProgressBar
|
||||||
|
|
||||||
private lateinit var googleSignInButton: MaterialButton
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private lateinit var progress: CircularProgressIndicator
|
|
||||||
private lateinit var credentialManager: CredentialManager
|
|
||||||
|
|
||||||
private lateinit var googleSignInLauncher: ActivityResultLauncher<IntentSenderRequest>
|
private lateinit var googleSignInLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
// Client IDs from Google Cloud Console
|
// Same client ID registered in Google Cloud Console for the backend OAuth
|
||||||
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com"
|
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com"
|
||||||
private val androidClientId = "416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com"
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -49,45 +37,23 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
googleSignInButton = findViewById(R.id.googleSignInButton)
|
googleSignInButton = findViewById(R.id.googleSignInButton)
|
||||||
progress = findViewById(R.id.loginProgress)
|
progress = findViewById(R.id.loginProgress)
|
||||||
credentialManager = CredentialManager.create(this)
|
|
||||||
|
|
||||||
googleSignInButton.setOnClickListener { launchGoogleSignIn() }
|
googleSignInButton.setOnClickListener { launchGoogleSignIn() }
|
||||||
|
|
||||||
googleSignInLauncher = registerForActivityResult(
|
googleSignInLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartIntentSenderForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||||
|
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
|
||||||
|
handleSignInResult(task)
|
||||||
|
} else {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, R.string.sign_in_interrupted, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Sign in cancelled", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observeAuthState()
|
|
||||||
|
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
viewModel.checkExistingAuth()
|
checkExistingAuth()
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeAuthState() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
viewModel.state.collect { state ->
|
|
||||||
showLoading(state.loading)
|
|
||||||
state.error?.let {
|
|
||||||
Toast.makeText(this@LoginActivity, it, Toast.LENGTH_LONG).show()
|
|
||||||
viewModel.setLoading(false)
|
|
||||||
}
|
|
||||||
state.user?.let { user ->
|
|
||||||
if (state.authenticated) {
|
|
||||||
Toast.makeText(
|
|
||||||
this@LoginActivity,
|
|
||||||
getString(R.string.welcome_user, user.name ?: user.email),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
launchMain()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoading(loading: Boolean) {
|
private fun showLoading(loading: Boolean) {
|
||||||
@ -95,61 +61,86 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
googleSignInButton.isEnabled = !loading
|
googleSignInButton.isEnabled = !loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkExistingAuth() {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val user = ApiClient.getMe()
|
||||||
|
handler.post {
|
||||||
|
showLoading(false)
|
||||||
|
if (user.authenticated) {
|
||||||
|
launchMain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
handler.post { showLoading(false) }
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchGoogleSignIn() {
|
private fun launchGoogleSignIn() {
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
val googleIdOption = GetGoogleIdOption.Builder()
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||||
.setFilterByAuthorizedAccounts(false)
|
.requestIdToken(webClientId)
|
||||||
.setServerClientId(webClientId)
|
.requestEmail()
|
||||||
.setAutoSelectEnabled(false)
|
|
||||||
.setNonce(generateNonce())
|
|
||||||
.build()
|
.build()
|
||||||
|
val client = GoogleSignIn.getClient(this, gso)
|
||||||
|
// Clear any previous sign-in to force account picker
|
||||||
|
client.signOut().addOnCompleteListener {
|
||||||
|
val signInIntent = client.signInIntent
|
||||||
|
googleSignInLauncher.launch(signInIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val request = GetCredentialRequest.Builder()
|
private fun handleSignInResult(task: Task<GoogleSignInAccount>) {
|
||||||
.addCredentialOption(googleIdOption)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
viewModel.viewModelScope.launch {
|
|
||||||
try {
|
try {
|
||||||
val result = credentialManager.getCredential(
|
val account = task.getResult(ApiException::class.java)
|
||||||
request = request,
|
val idToken = account?.idToken
|
||||||
context = this@LoginActivity
|
|
||||||
)
|
|
||||||
handleCredentialResponse(result)
|
|
||||||
} catch (e: GetCredentialException) {
|
|
||||||
showLoading(false)
|
|
||||||
val errorMsg = when (e) {
|
|
||||||
is androidx.credentials.exceptions.NoCredentialException -> getString(R.string.no_google_accounts)
|
|
||||||
is androidx.credentials.exceptions.GetCredentialCancellationException -> getString(R.string.sign_in_cancelled)
|
|
||||||
else -> getString(R.string.sign_in_failed, e.message)
|
|
||||||
}
|
|
||||||
Toast.makeText(this@LoginActivity, errorMsg, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCredentialResponse(response: GetCredentialResponse) {
|
|
||||||
val credential = response.credential
|
|
||||||
when {
|
|
||||||
credential is CustomCredential && credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> {
|
|
||||||
try {
|
|
||||||
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
|
|
||||||
val idToken = googleIdTokenCredential.idToken
|
|
||||||
if (idToken.isNullOrEmpty()) {
|
if (idToken.isNullOrEmpty()) {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, R.string.no_id_token, Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "No ID token from Google", Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewModel.exchangeToken(idToken)
|
exchangeToken(idToken)
|
||||||
} catch (e: GoogleIdTokenParsingException) {
|
} catch (e: ApiException) {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, getString(R.string.invalid_google_id_token, e.message), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "Sign in failed: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
|
private fun exchangeToken(idToken: String) {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val token = ApiClient.exchangeGoogleToken(idToken)
|
||||||
|
ApiClient.setToken(token)
|
||||||
|
val user = ApiClient.getMe()
|
||||||
|
handler.post {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, R.string.unexpected_credential_type, Toast.LENGTH_LONG).show()
|
if (user.authenticated) {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"Welcome ${user.name ?: user.email}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
launchMain()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"Server rejected the token",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handler.post {
|
||||||
|
showLoading(false)
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
e.message ?: "Token exchange failed",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchMain() {
|
private fun launchMain() {
|
||||||
@ -157,10 +148,8 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateNonce(): String {
|
override fun onDestroy() {
|
||||||
val secureRandom = SecureRandom()
|
super.onDestroy()
|
||||||
val bytes = ByteArray(32)
|
handler.removeCallbacksAndMessages(null)
|
||||||
secureRandom.nextBytes(bytes)
|
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
package com.example.resbuilder.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.example.resbuilder.data.model.User
|
|
||||||
import com.example.resbuilder.data.remote.ApiClient
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
data class LoginUiState(
|
|
||||||
val loading: Boolean = false,
|
|
||||||
val authenticated: Boolean = false,
|
|
||||||
val user: User? = null,
|
|
||||||
val error: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class LoginViewModel : ViewModel() {
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(LoginUiState())
|
|
||||||
val state: StateFlow<LoginUiState> = _state.asStateFlow()
|
|
||||||
|
|
||||||
fun checkExistingAuth() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
|
||||||
_state.value = LoginUiState(loading = false, authenticated = user.authenticated, user = user)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
_state.value = LoginUiState(loading = false, error = "check auth failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exchangeToken(idToken: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.value = _state.value.copy(loading = true, error = null)
|
|
||||||
try {
|
|
||||||
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
|
|
||||||
ApiClient.setToken(token)
|
|
||||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
|
||||||
_state.value = LoginUiState(
|
|
||||||
loading = false,
|
|
||||||
authenticated = user.authenticated,
|
|
||||||
user = user
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.value = LoginUiState(loading = false, error = e.message ?: "Token exchange failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLoading(loading: Boolean) {
|
|
||||||
_state.value = _state.value.copy(loading = loading)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun consumeAuth() {
|
|
||||||
_state.value = _state.value.copy(authenticated = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,6 +5,7 @@ import android.graphics.PorterDuff
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@ -12,21 +13,18 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import com.example.resbuilder.R
|
import com.example.resbuilder.R
|
||||||
import com.example.resbuilder.data.model.User
|
import com.example.resbuilder.data.model.User
|
||||||
import com.example.resbuilder.data.remote.ApiClient
|
import com.example.resbuilder.data.remote.ApiClient
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, ResultFragment.OnNewListener {
|
class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, ResultFragment.OnNewListener {
|
||||||
|
|
||||||
private lateinit var drawerLayout: DrawerLayout
|
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private lateinit var navView: NavigationView
|
private lateinit var tabLayout: TabLayout
|
||||||
private lateinit var healthIndicator: ImageView
|
private lateinit var healthIndicator: ImageView
|
||||||
private lateinit var userName: TextView
|
private lateinit var userName: TextView
|
||||||
private lateinit var userEmail: TextView
|
|
||||||
private lateinit var usageBanner: TextView
|
private lateinit var usageBanner: TextView
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
@ -34,65 +32,43 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
|
|||||||
private var healthRunnable: Runnable? = null
|
private var healthRunnable: Runnable? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAV_BUILD_RESUME = 0
|
private const val TAB_RESUME = 0
|
||||||
private const val NAV_BUILD_COVER_LETTER = 1
|
private const val TAB_COVER = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
drawerLayout = findViewById(R.id.drawerLayout)
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
navView = findViewById(R.id.navView)
|
tabLayout = findViewById(R.id.tabLayout)
|
||||||
|
healthIndicator = findViewById(R.id.healthIndicator)
|
||||||
|
userName = findViewById(R.id.userName)
|
||||||
usageBanner = findViewById(R.id.usageBanner)
|
usageBanner = findViewById(R.id.usageBanner)
|
||||||
|
|
||||||
val header = navView.getHeaderView(0)
|
|
||||||
healthIndicator = header.findViewById(R.id.healthIndicator)
|
|
||||||
userName = header.findViewById(R.id.userName)
|
|
||||||
userEmail = header.findViewById(R.id.userEmail)
|
|
||||||
|
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_hamburger)
|
|
||||||
|
|
||||||
navView.setNavigationItemSelectedListener { item ->
|
tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_build_resume))
|
||||||
drawerLayout.closeDrawers()
|
tabLayout.addTab(tabLayout.newTab().setText(R.string.tab_build_cover_letter))
|
||||||
handleNavItem(item.itemId)
|
|
||||||
true
|
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
switchFormTab(tab?.position ?: TAB_RESUME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||||
|
switchFormTab(tab?.position ?: TAB_RESUME)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
loadUser()
|
loadUser()
|
||||||
startHealthPolling()
|
startHealthPolling()
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
showBuildForm(NAV_BUILD_RESUME)
|
switchFormTab(TAB_RESUME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavItem(itemId: Int) {
|
|
||||||
when (itemId) {
|
|
||||||
R.id.navBuildResume -> showBuildForm(NAV_BUILD_RESUME)
|
|
||||||
R.id.navBuildCoverLetter -> showBuildForm(NAV_BUILD_COVER_LETTER)
|
|
||||||
R.id.navMyResumes -> showMyResumes()
|
|
||||||
R.id.navAdmin -> startActivity(Intent(this, AdminActivity::class.java))
|
|
||||||
R.id.navSignOut -> signOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBuildForm(position: Int) {
|
|
||||||
val type = if (position == NAV_BUILD_COVER_LETTER) JobFormFragment.TYPE_COVER_LETTER else JobFormFragment.TYPE_RESUME
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragmentContainer, JobFormFragment.newInstance(type))
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMyResumes() {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragmentContainer, MyResumesFragment())
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUser() {
|
private fun loadUser() {
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
@ -110,13 +86,20 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
|
|||||||
|
|
||||||
private fun updateUserUI() {
|
private fun updateUserUI() {
|
||||||
userName.text = user.name ?: user.email ?: getString(R.string.unknown_user)
|
userName.text = user.name ?: user.email ?: getString(R.string.unknown_user)
|
||||||
userEmail.text = user.email ?: getString(R.string.unknown_email)
|
if (!user.authenticated || (!user.is_admin && !user.can_submit)) {
|
||||||
usageBanner.visibility = if (!user.authenticated || (!user.is_admin && !user.can_submit)) {
|
usageBanner.visibility = View.VISIBLE
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
usageBanner.visibility = View.GONE
|
||||||
}
|
}
|
||||||
navView.menu.findItem(R.id.navAdmin)?.isVisible = user.is_admin
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchFormTab(position: Int) {
|
||||||
|
val type = if (position == TAB_COVER) JobFormFragment.TYPE_COVER_LETTER else JobFormFragment.TYPE_RESUME
|
||||||
|
val fragment = JobFormFragment.newInstance(type)
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragmentContainer, fragment)
|
||||||
|
.commitAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startHealthPolling() {
|
private fun startHealthPolling() {
|
||||||
@ -147,12 +130,27 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
|
|||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.menu_main, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.findItem(R.id.menuAdminPanel)?.isVisible = user.is_admin
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return if (item.itemId == android.R.id.home) {
|
return when (item.itemId) {
|
||||||
drawerLayout.openDrawer(navView)
|
R.id.menuSignOut -> {
|
||||||
|
signOut()
|
||||||
true
|
true
|
||||||
} else {
|
}
|
||||||
super.onOptionsItemSelected(item)
|
R.id.menuAdminPanel -> {
|
||||||
|
startActivity(Intent(this, AdminActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,8 +176,8 @@ class MainActivity : AppCompatActivity(), JobFormFragment.JobFormListener, Resul
|
|||||||
|
|
||||||
fun onResultNewJob() {
|
fun onResultNewJob() {
|
||||||
supportFragmentManager.popBackStackImmediate()
|
supportFragmentManager.popBackStackImmediate()
|
||||||
navView.menu.getItem(NAV_BUILD_RESUME)?.isChecked = true
|
tabLayout.getTabAt(TAB_RESUME)?.select()
|
||||||
showBuildForm(NAV_BUILD_RESUME)
|
switchFormTab(TAB_RESUME)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewJob() {
|
override fun onNewJob() {
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
package com.example.resbuilder.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.ListView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import com.example.resbuilder.R
|
|
||||||
import com.example.resbuilder.data.model.JobSummary
|
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MyResumesFragment : Fragment() {
|
|
||||||
|
|
||||||
private val viewModel: MyResumesViewModel by viewModels()
|
|
||||||
|
|
||||||
private lateinit var emptyView: TextView
|
|
||||||
private lateinit var progress: LinearProgressIndicator
|
|
||||||
private lateinit var listView: ListView
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_my_resumes, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
emptyView = view.findViewById(R.id.myResumesEmpty)
|
|
||||||
progress = view.findViewById(R.id.myResumesProgress)
|
|
||||||
listView = view.findViewById(R.id.myResumesList)
|
|
||||||
|
|
||||||
listView.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val item = listView.adapter.getItem(position) as JobSummary
|
|
||||||
viewModel.openJob(item.job_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
observeState()
|
|
||||||
viewModel.loadJobs()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeState() {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
viewModel.state.collect { state ->
|
|
||||||
progress.visibility = if (state.loading) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
state.error?.let {
|
|
||||||
emptyView.text = getString(R.string.my_resumes_error)
|
|
||||||
emptyView.visibility = View.VISIBLE
|
|
||||||
Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.loading && state.jobs.isNotEmpty()) {
|
|
||||||
emptyView.visibility = View.GONE
|
|
||||||
listView.visibility = View.VISIBLE
|
|
||||||
listView.adapter = JobListAdapter(requireContext(), state.jobs)
|
|
||||||
} else if (!state.loading && state.error == null) {
|
|
||||||
emptyView.text = getString(R.string.my_resumes_empty)
|
|
||||||
emptyView.visibility = View.VISIBLE
|
|
||||||
listView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
state.openedJob?.let { job ->
|
|
||||||
viewModel.consumeOpenedJob()
|
|
||||||
openJobResult(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openJobResult(job: com.example.resbuilder.data.model.Job) {
|
|
||||||
when (job.status.uppercase()) {
|
|
||||||
"COMPLETED" -> {
|
|
||||||
val type = job.type ?: JobFormFragment.TYPE_RESUME
|
|
||||||
replaceWith(ResultFragment.newInstance(job, type))
|
|
||||||
}
|
|
||||||
"FAILED" -> Toast.makeText(requireContext(), job.error ?: getString(R.string.status_failed), Toast.LENGTH_LONG).show()
|
|
||||||
else -> replaceWith(JobStatusFragment.newInstance(job.job_id ?: "", job.type ?: JobFormFragment.TYPE_RESUME))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun replaceWith(fragment: Fragment) {
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragmentContainer, fragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class JobListAdapter(
|
|
||||||
context: android.content.Context,
|
|
||||||
private val jobs: List<JobSummary>
|
|
||||||
) : ArrayAdapter<JobSummary>(context, R.layout.item_job_summary, jobs) {
|
|
||||||
|
|
||||||
private val inflater = android.view.LayoutInflater.from(context)
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val v = convertView ?: inflater.inflate(R.layout.item_job_summary, parent, false)
|
|
||||||
val job = jobs[position]
|
|
||||||
v.findViewById<TextView>(R.id.itemTitle).text =
|
|
||||||
job.title?.takeIf { it.isNotBlank() } ?: getKindLabel(job.type)
|
|
||||||
v.findViewById<TextView>(R.id.itemSubtitle).text = buildSubtitle(job)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getKindLabel(type: String?): String =
|
|
||||||
if (type == JobFormFragment.TYPE_COVER_LETTER) context.getString(R.string.type_cover_letter)
|
|
||||||
else context.getString(R.string.type_resume)
|
|
||||||
|
|
||||||
private fun buildSubtitle(job: JobSummary): String {
|
|
||||||
val kind = getKindLabel(job.type)
|
|
||||||
val created = job.created_at?.let(::formatDate)?.let { " · $it" }.orEmpty()
|
|
||||||
return "$kind$created"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatDate(iso: String): String {
|
|
||||||
return try {
|
|
||||||
val date = java.time.OffsetDateTime.parse(iso)
|
|
||||||
val local = date.atZoneSameInstant(java.time.ZoneId.systemDefault())
|
|
||||||
java.time.format.DateTimeFormatter.ofPattern("d MMM yyyy").format(local)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
iso
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
package com.example.resbuilder.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.example.resbuilder.data.model.Job
|
|
||||||
import com.example.resbuilder.data.model.JobSummary
|
|
||||||
import com.example.resbuilder.data.remote.ApiClient
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
data class MyResumesUiState(
|
|
||||||
val loading: Boolean = false,
|
|
||||||
val jobs: List<JobSummary> = emptyList(),
|
|
||||||
val error: String? = null,
|
|
||||||
val openedJob: Job? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class MyResumesViewModel : ViewModel() {
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(MyResumesUiState())
|
|
||||||
val state: StateFlow<MyResumesUiState> = _state.asStateFlow()
|
|
||||||
|
|
||||||
fun loadJobs() {
|
|
||||||
_state.value = _state.value.copy(loading = true, error = null)
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val jobs = withContext(Dispatchers.IO) { ApiClient.getMyJobs().jobs }
|
|
||||||
_state.value = _state.value.copy(loading = false, jobs = jobs)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.value = _state.value.copy(loading = false, error = e.message ?: "load failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openJob(jobId: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_state.value = _state.value.copy(loading = true, error = null)
|
|
||||||
try {
|
|
||||||
val job = withContext(Dispatchers.IO) { ApiClient.getJob(jobId) }
|
|
||||||
_state.value = _state.value.copy(loading = false, openedJob = job)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.value = _state.value.copy(loading = false, error = e.message ?: "open failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun consumeOpenedJob() {
|
|
||||||
_state.value = _state.value.copy(openedJob = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,14 +13,11 @@ import android.webkit.WebView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.example.resbuilder.R
|
import com.example.resbuilder.R
|
||||||
import com.example.resbuilder.data.model.Job
|
import com.example.resbuilder.data.model.Job
|
||||||
import com.example.resbuilder.data.remote.ApiClient
|
import com.example.resbuilder.data.remote.ApiClient
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
class ResultFragment : Fragment() {
|
class ResultFragment : Fragment() {
|
||||||
|
|
||||||
@ -89,58 +86,14 @@ class ResultFragment : Fragment() {
|
|||||||
newButton.setOnClickListener { listener?.onNewJob() }
|
newButton.setOnClickListener { listener?.onNewJob() }
|
||||||
|
|
||||||
jobId?.let { id ->
|
jobId?.let { id ->
|
||||||
pdfButton.setOnClickListener { downloadAndOpen(id, "pdf") }
|
pdfButton.setOnClickListener { openUrl(ApiClient.getExportPdfUrl(id)) }
|
||||||
docxButton.setOnClickListener { downloadAndOpen(id, "docx") }
|
docxButton.setOnClickListener { openUrl(ApiClient.getExportDocxUrl(id)) }
|
||||||
} ?: run {
|
} ?: run {
|
||||||
pdfButton.isEnabled = false
|
pdfButton.isEnabled = false
|
||||||
docxButton.isEnabled = false
|
docxButton.isEnabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadAndOpen(jobId: String, format: String) {
|
|
||||||
val url = if (format == "pdf") ApiClient.getExportPdfUrl(jobId) else ApiClient.getExportDocxUrl(jobId)
|
|
||||||
val dir = File(requireContext().cacheDir, "exports")
|
|
||||||
if (!dir.exists()) dir.mkdirs()
|
|
||||||
val safeId = jobId.replace("/", "_").replace("..", "_").take(8)
|
|
||||||
val file = File(dir, "$safeId.$format")
|
|
||||||
|
|
||||||
pdfButton.isEnabled = false
|
|
||||||
docxButton.isEnabled = false
|
|
||||||
Toast.makeText(context, R.string.exporting, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
val bytes = ApiClient.downloadExport(url)
|
|
||||||
FileOutputStream(file).use { it.write(bytes) }
|
|
||||||
requireActivity().runOnUiThread {
|
|
||||||
pdfButton.isEnabled = true
|
|
||||||
docxButton.isEnabled = true
|
|
||||||
openFile(file)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
requireActivity().runOnUiThread {
|
|
||||||
pdfButton.isEnabled = true
|
|
||||||
docxButton.isEnabled = true
|
|
||||||
Toast.makeText(context, e.message ?: getString(R.string.export_failed), Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile(file: File) {
|
|
||||||
val uri = FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.fileprovider", file)
|
|
||||||
val mime = if (file.extension == "pdf") "application/pdf" else "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(uri, mime)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(context, R.string.export_no_viewer, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyText() {
|
private fun copyText() {
|
||||||
val raw = rawContent
|
val raw = rawContent
|
||||||
if (raw.isNullOrEmpty()) {
|
if (raw.isNullOrEmpty()) {
|
||||||
@ -152,6 +105,11 @@ class ResultFragment : Fragment() {
|
|||||||
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openUrl(url: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
fun setOnNewListener(l: OnNewListener) {
|
fun setOnNewListener(l: OnNewListener) {
|
||||||
listener = l
|
listener = l
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
<solid android:color="?attr/colorWarningContainer" />
|
<solid android:color="?attr/colorWarningContainer" />
|
||||||
<corners android:radius="12dp" />
|
<corners android:radius="12dp" />
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<gradient
|
|
||||||
android:angle="135"
|
|
||||||
android:startColor="?attr/colorPrimary"
|
|
||||||
android:centerColor="?attr/colorPrimaryInverse"
|
|
||||||
android:endColor="?attr/colorPrimaryContainer"
|
|
||||||
android:type="linear" />
|
|
||||||
</shape>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<gradient
|
|
||||||
android:angle="270"
|
|
||||||
android:startColor="?attr/colorPrimary"
|
|
||||||
android:centerColor="?attr/colorPrimaryInverse"
|
|
||||||
android:endColor="?attr/colorPrimaryContainer"
|
|
||||||
android:type="linear" />
|
|
||||||
</shape>
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/android"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
<solid android:color="?attr/colorSurfaceContainerHighest" />
|
<solid android:color="?attr/colorSurfaceContainerHighest" />
|
||||||
<corners android:radius="8dp" />
|
<corners android:radius="8dp" />
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
|
||||||
android:pathData="M14,2H6C4.9,2 4,2.9 4,4V20C4,21.1 4.9,22 6,22H18C19.1,22 20,21.1 20,20V8L14,2M13,9V3.5L18.5,9H13Z" />
|
|
||||||
</vector>
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Google G icon -->
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#4285F4"
|
|
||||||
android:pathData="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#34A853"
|
|
||||||
android:pathData="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FBBC05"
|
|
||||||
android:pathData="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#EA4335"
|
|
||||||
android:pathData="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
||||||
</vector>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorOnSurface">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurface"
|
|
||||||
android:pathData="M3,6h18v2H3V6zM3,11h18v2H3v-2zM3,16h18v2H3v-2z" />
|
|
||||||
</vector>
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnPrimary"
|
|
||||||
android:pathData="M12,2L13.5,9.5L21,12L13.5,14.5L12,22L10.5,14.5L3,12L10.5,9.5L12,2Z" />
|
|
||||||
</vector>
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@ -5,50 +5,22 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
|
android:padding="16dp"
|
||||||
tools:context=".ui.LoginActivity">
|
tools:context=".ui.LoginActivity">
|
||||||
|
|
||||||
<!-- Decorative gradient background behind card -->
|
<!-- Skeleton loading placeholder visible while auth is checked -->
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="@drawable/bg_gradient_primary"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/guidelineTop"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="120dp"
|
|
||||||
android:layout_height="120dp"
|
|
||||||
android:layout_marginEnd="@dimen/space_lg"
|
|
||||||
android:layout_marginBottom="@dimen/space_lg"
|
|
||||||
android:alpha="0.15"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_sparkle"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/guidelineTop"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
|
||||||
android:id="@+id/guidelineTop"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintGuide_percent="0.40" />
|
|
||||||
|
|
||||||
<!-- Main content card -->
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/loginCard"
|
android:id="@+id/loginSkeletonCard"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/space_lg"
|
android:layout_marginHorizontal="16dp"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
android:visibility="gone"
|
||||||
app:cardCornerRadius="@dimen/card_radius_large"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
app:strokeWidth="1dp">
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
@ -57,103 +29,91 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/space_xl">
|
android:padding="24dp">
|
||||||
|
|
||||||
<!-- App icon with tonal surface -->
|
<View
|
||||||
<com.google.android.material.card.MaterialCardView
|
android:layout_width="80dp"
|
||||||
android:layout_width="@dimen/space_xxl"
|
android:layout_height="80dp"
|
||||||
android:layout_height="@dimen/space_xxl"
|
android:background="?attr/colorSurfaceContainerHigh" />
|
||||||
app:cardBackgroundColor="?attr/colorPrimaryContainer"
|
|
||||||
app:cardCornerRadius="@dimen/card_radius_large"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<ImageView
|
<View
|
||||||
|
android:layout_width="160dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?attr/colorSurfaceContainerHigh" />
|
||||||
|
|
||||||
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="48dp"
|
||||||
android:contentDescription="@string/cd_app_logo"
|
android:layout_marginTop="24dp"
|
||||||
android:padding="@dimen/space_md"
|
android:background="?attr/colorSurfaceContainerHigh" />
|
||||||
android:scaleType="centerInside"
|
</LinearLayout>
|
||||||
android:src="@drawable/ic_launcher_foreground"
|
|
||||||
app:tint="?attr/colorOnPrimaryContainer" />
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Title -->
|
<ImageView
|
||||||
|
android:id="@+id/appIcon"
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:background="?attr/colorPrimaryContainer"
|
||||||
|
android:contentDescription="@string/cd_app_logo"
|
||||||
|
android:importantForAccessibility="yes"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:src="@drawable/ic_launcher_foreground"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/loginTitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:tint="?attr/colorOnPrimaryContainer" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/loginTitle"
|
android:id="@+id/loginTitle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_lg"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="@string/app_title"
|
android:text="@string/app_title"
|
||||||
android:textAlignment="center"
|
android:textAppearance="?attr/textAppearanceHeadlineLarge"
|
||||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
|
||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:textStyle="bold" />
|
app:layout_constraintBottom_toTopOf="@id/loginSubtitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
<!-- Subtitle -->
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/loginSubtitle"
|
android:id="@+id/loginSubtitle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginBottom="24dp"
|
||||||
android:text="@string/welcome_subtitle"
|
android:text="@string/checking_auth"
|
||||||
android:textAlignment="center"
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
app:layout_constraintBottom_toTopOf="@id/googleSignInButton"
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginVertical="@dimen/space_lg"
|
|
||||||
android:background="?attr/colorOutlineVariant" />
|
|
||||||
|
|
||||||
<!-- Sign in button -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/googleSignInButton"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
android:minHeight="@dimen/button_height"
|
|
||||||
android:text="@string/sign_in_with_google"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:textColor="?attr/colorOnSurface"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:cornerRadius="14dp"
|
|
||||||
app:icon="@drawable/ic_google"
|
|
||||||
app:iconGravity="textStart"
|
|
||||||
app:iconPadding="@dimen/space_md"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@null"
|
|
||||||
app:strokeColor="?attr/colorOutline"
|
|
||||||
app:strokeWidth="1.5dp" />
|
|
||||||
|
|
||||||
<!-- Helper text -->
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_md"
|
|
||||||
android:text="@string/sign_in_terms"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
|
||||||
</LinearLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<!-- Progress indicator -->
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
android:id="@+id/loginProgress"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:indicatorColor="?attr/colorOnPrimary"
|
|
||||||
app:indicatorSize="@dimen/space_xxl"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/loginCard"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/guidelineTop"
|
app:layout_constraintTop_toBottomOf="@id/loginTitle" />
|
||||||
app:trackColor="?attr/colorPrimaryContainer" />
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loginProgress"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:contentDescription="@string/cd_loading"
|
||||||
|
android:indeterminateTint="?attr/colorPrimary"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/loginTitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.gms.common.SignInButton
|
||||||
|
android:id="@+id/googleSignInButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:contentDescription="@string/cd_google_sign_in"
|
||||||
|
android:minWidth="200dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/loginSubtitle" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -1,82 +1,105 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/drawerLayout"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
tools:context=".ui.MainActivity">
|
tools:context=".ui.MainActivity">
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<!-- Modern colored app bar with gradient surface and centered title -->
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorSurface"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
app:elevation="0dp">
|
app:elevation="0dp"
|
||||||
|
app:liftOnScroll="false">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:background="@drawable/bg_gradient_primary_bottom"
|
android:background="?attr/colorSurface"
|
||||||
android:elevation="0dp"
|
android:fitsSystemWindows="true"
|
||||||
app:navigationIcon="@drawable/ic_menu_hamburger"
|
app:layout_scrollFlags="scroll|enterAlways|snap"
|
||||||
app:navigationIconTint="?attr/colorOnPrimary"
|
|
||||||
app:title="@string/app_title"
|
app:title="@string/app_title"
|
||||||
app:titleCentered="true"
|
app:titleCentered="true"
|
||||||
app:titleTextAppearance="?attr/textAppearanceTitleLarge"
|
app:titleTextAppearance="?attr/textAppearanceTitleLarge"
|
||||||
app:titleTextColor="?attr/colorOnPrimary" />
|
app:titleTextColor="?attr/colorOnSurface">
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/healthContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="?attr/colorSurfaceContainer"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="32dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/healthIndicator"
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:contentDescription="@string/cd_health_indicator"
|
||||||
|
android:src="@drawable/ic_circle_status" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/userName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/unknown_user"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/usageBanner"
|
android:id="@+id/usageBanner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginVertical="8dp"
|
||||||
android:background="@drawable/bg_banner_warning"
|
android:background="@drawable/bg_banner_warning"
|
||||||
android:drawableStart="@drawable/ic_circle_status"
|
android:drawableStart="@drawable/ic_circle_status"
|
||||||
android:drawableTint="?attr/colorWarning"
|
android:drawableTint="?attr/colorWarning"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:minHeight="@dimen/touch_target_min"
|
android:minHeight="48dp"
|
||||||
android:padding="@dimen/space_sm"
|
android:padding="12dp"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
android:text="@string/usage_limit_banner"
|
android:text="@string/usage_limit_banner"
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorSurface"
|
||||||
|
android:contentDescription="@string/app_title"
|
||||||
|
app:tabBackground="?attr/colorSurface"
|
||||||
|
app:tabGravity="fill"
|
||||||
|
app:tabIndicatorColor="?attr/colorPrimary"
|
||||||
|
app:tabMode="fixed"
|
||||||
|
app:tabTextAppearance="?attr/textAppearanceLabelLarge"
|
||||||
|
app:tabTextColor="?attr/colorOnSurfaceVariant"
|
||||||
|
app:tabSelectedTextColor="?attr/colorOnSurface" />
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/fragmentContainer"
|
android:id="@+id/fragmentContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface" />
|
android:background="?attr/colorSurface"
|
||||||
</LinearLayout>
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<!-- Navigation drawer with richer surface and modern item styling -->
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
<com.google.android.material.navigation.NavigationView
|
|
||||||
android:id="@+id/navView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="start"
|
|
||||||
android:background="?attr/colorSurfaceContainerLowest"
|
|
||||||
app:headerLayout="@layout/nav_header"
|
|
||||||
app:itemHorizontalPadding="@dimen/space_md"
|
|
||||||
app:itemIconPadding="@dimen/space_md"
|
|
||||||
app:itemIconTint="?attr/colorOnSurfaceVariant"
|
|
||||||
app:itemRippleColor="?attr/colorPrimaryContainer"
|
|
||||||
app:itemTextAppearance="?attr/textAppearanceBodyLarge"
|
|
||||||
app:itemTextColor="?attr/colorOnSurface"
|
|
||||||
app:itemVerticalPadding="@dimen/space_md"
|
|
||||||
app:menu="@menu/menu_nav_drawer" />
|
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
||||||
|
|||||||
@ -6,67 +6,18 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:fillViewport="true"
|
android:fillViewport="true"
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingBottom="@dimen/space_md"
|
|
||||||
tools:context=".ui.JobFormFragment">
|
tools:context=".ui.JobFormFragment">
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<!-- Hero header with gradient surface and section title -->
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/bg_gradient_primary"
|
|
||||||
android:minHeight="@dimen/hero_header_min_height"
|
|
||||||
android:padding="@dimen/space_lg">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_gravity="top|end"
|
|
||||||
android:alpha="0.25"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_sparkle" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|start"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/app_title"
|
|
||||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
|
||||||
android:textColor="?attr/colorOnPrimary"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_xs"
|
|
||||||
android:text="@string/hint_job_title"
|
|
||||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
|
||||||
android:textColor="?attr/colorOnPrimary" />
|
|
||||||
</LinearLayout>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/space_md">
|
android:padding="16dp">
|
||||||
|
|
||||||
<!-- Job details card -->
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
app:strokeWidth="1dp">
|
app:strokeWidth="1dp">
|
||||||
@ -75,7 +26,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/space_md">
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -87,26 +38,24 @@
|
|||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginTop="12dp"
|
||||||
android:hint="@string/hint_job_title"
|
android:hint="@string/hint_job_title"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||||
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
|
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||||
app:boxStrokeColor="?attr/colorPrimary"
|
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||||
app:cursorColor="?attr/colorPrimary"
|
|
||||||
app:hintTextColor="?attr/colorOnSurfaceVariant">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/jobTitleInput"
|
android:id="@+id/jobTitleInput"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:minHeight="@dimen/touch_target_min" />
|
android:minHeight="48dp" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginTop="12dp"
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@ -115,26 +64,24 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/hint_seek_url"
|
android:hint="@string/hint_seek_url"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||||
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
|
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||||
app:boxStrokeColor="?attr/colorPrimary"
|
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||||
app:cursorColor="?attr/colorPrimary"
|
|
||||||
app:hintTextColor="?attr/colorOnSurfaceVariant">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/seekUrlInput"
|
android:id="@+id/seekUrlInput"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textUri"
|
android:inputType="textUri"
|
||||||
android:minHeight="@dimen/touch_target_min" />
|
android:minHeight="48dp" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/scrapeButton"
|
android:id="@+id/scrapeButton"
|
||||||
style="@style/Widget.Material3.Button.TonalButton"
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:layout_marginStart="@dimen/space_sm"
|
android:layout_marginStart="8dp"
|
||||||
android:contentDescription="@string/cd_scrape_job"
|
android:contentDescription="@string/cd_scrape_job"
|
||||||
android:minWidth="64dp"
|
android:minWidth="64dp"
|
||||||
android:text="@string/button_scrape" />
|
android:text="@string/button_scrape" />
|
||||||
@ -143,13 +90,11 @@
|
|||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_md"
|
android:layout_marginTop="16dp"
|
||||||
android:hint="@string/hint_job_description"
|
android:hint="@string/hint_job_description"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||||
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
|
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||||
app:boxStrokeColor="?attr/colorPrimary"
|
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||||
app:cursorColor="?attr/colorPrimary"
|
|
||||||
app:hintTextColor="?attr/colorOnSurfaceVariant">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/jobDescriptionInput"
|
android:id="@+id/jobDescriptionInput"
|
||||||
@ -157,19 +102,16 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="top"
|
android:gravity="top"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine"
|
||||||
android:minLines="4"
|
android:minLines="4" />
|
||||||
android:minHeight="@dimen/touch_target_min" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Resume / cover letter input card -->
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_md"
|
android:layout_marginTop="12dp"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
app:strokeWidth="1dp">
|
app:strokeWidth="1dp">
|
||||||
@ -178,7 +120,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/space_md">
|
android:padding="16dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -199,7 +141,7 @@
|
|||||||
android:id="@+id/uploadButton"
|
android:id="@+id/uploadButton"
|
||||||
style="@style/Widget.Material3.Button.TonalButton"
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:contentDescription="@string/cd_upload_document"
|
android:contentDescription="@string/cd_upload_document"
|
||||||
android:text="@string/button_upload" />
|
android:text="@string/button_upload" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -208,13 +150,11 @@
|
|||||||
android:id="@+id/docInputLayout"
|
android:id="@+id/docInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginTop="12dp"
|
||||||
android:hint="@string/hint_your_resume"
|
android:hint="@string/hint_your_resume"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
app:boxBackgroundColor="?attr/colorSurfaceContainer"
|
||||||
app:boxBackgroundColor="?attr/colorSurfaceContainerHighest"
|
app:hintTextColor="?attr/colorOnSurfaceVariant"
|
||||||
app:boxStrokeColor="?attr/colorPrimary"
|
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||||
app:cursorColor="?attr/colorPrimary"
|
|
||||||
app:hintTextColor="?attr/colorOnSurfaceVariant">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/docInput"
|
android:id="@+id/docInput"
|
||||||
@ -222,8 +162,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="top"
|
android:gravity="top"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine"
|
||||||
android:minLines="6"
|
android:minLines="6" />
|
||||||
android:minHeight="@dimen/touch_target_min" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@ -231,19 +170,18 @@
|
|||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/submitButton"
|
android:id="@+id/submitButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:layout_marginTop="@dimen/space_md"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/generate_resume" />
|
android:text="@string/generate_resume" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
android:layout_width="@dimen/space_xxl"
|
android:layout_width="48dp"
|
||||||
android:layout_height="@dimen/space_xxl"
|
android:layout_height="48dp"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginTop="@dimen/space_md"
|
android:layout_marginTop="16dp"
|
||||||
android:contentDescription="@string/cd_loading"
|
android:contentDescription="@string/cd_loading"
|
||||||
android:indeterminateTint="?attr/colorPrimary"
|
android:indeterminateTint="?attr/colorPrimary"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@ -7,15 +7,14 @@
|
|||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/space_md">
|
android:padding="16dp">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginVertical="@dimen/space_sm"
|
android:layout_marginVertical="8dp"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardCornerRadius="@dimen/card_radius_large"
|
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
app:strokeWidth="1dp">
|
app:strokeWidth="1dp">
|
||||||
@ -25,36 +24,27 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/space_lg">
|
android:padding="24dp">
|
||||||
|
|
||||||
<!-- Status badge: now a tonal chip-style pill -->
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:cardBackgroundColor="?attr/colorPrimaryContainer"
|
|
||||||
app:cardCornerRadius="16dp"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/statusBadge"
|
android:id="@+id/statusBadge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_status_badge"
|
||||||
android:contentDescription="@string/status_queued"
|
android:contentDescription="@string/status_queued"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:minHeight="32dp"
|
android:minHeight="32dp"
|
||||||
android:paddingHorizontal="@dimen/space_md"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingVertical="4dp"
|
android:paddingVertical="4dp"
|
||||||
android:text="@string/status_queued"
|
android:text="@string/status_queued"
|
||||||
android:textAppearance="?attr/textAppearanceLabelLarge"
|
android:textAppearance="?attr/textAppearanceLabelLarge"
|
||||||
android:textColor="?attr/colorOnPrimaryContainer" />
|
android:textColor="?attr/colorOnPrimary" />
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/statusProgress"
|
android:id="@+id/statusProgress"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_lg"
|
android:layout_marginTop="24dp"
|
||||||
android:contentDescription="@string/content_generation_running"
|
android:contentDescription="@string/content_generation_running"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
app:indicatorColor="?attr/colorPrimary"
|
app:indicatorColor="?attr/colorPrimary"
|
||||||
@ -64,7 +54,7 @@
|
|||||||
android:id="@+id/statusMessage"
|
android:id="@+id/statusMessage"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_md"
|
android:layout_marginTop="16dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/job_wait_message"
|
android:text="@string/job_wait_message"
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||||
@ -74,7 +64,7 @@
|
|||||||
android:id="@+id/statusDetail"
|
android:id="@+id/statusDetail"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
android:textColor="?attr/colorOnSurfaceVariant"
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
@ -84,8 +74,8 @@
|
|||||||
android:id="@+id/tryAgainButton"
|
android:id="@+id/tryAgainButton"
|
||||||
style="@style/Widget.Material3.Button.TonalButton"
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:layout_marginTop="@dimen/space_lg"
|
android:layout_marginTop="24dp"
|
||||||
android:minWidth="120dp"
|
android:minWidth="120dp"
|
||||||
android:text="@string/try_again"
|
android:text="@string/try_again"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/colorSurface"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<!-- Hero header for list screen -->
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/bg_gradient_primary"
|
|
||||||
android:minHeight="@dimen/hero_header_min_height"
|
|
||||||
android:padding="@dimen/space_lg">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_gravity="top|end"
|
|
||||||
android:alpha="0.25"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_sparkle" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|start"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/app_title"
|
|
||||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
|
||||||
android:textColor="?attr/colorOnPrimary"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myResumesTitle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_xs"
|
|
||||||
android:text="@string/my_resumes_title"
|
|
||||||
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
|
||||||
android:textColor="?attr/colorOnPrimary" />
|
|
||||||
</LinearLayout>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myResumesEmpty"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
|
||||||
android:layout_marginTop="@dimen/space_xl"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="@string/my_resumes_empty"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
|
||||||
android:textColor="?attr/colorOnSurfaceVariant"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
android:id="@+id/myResumesProgress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_md"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:indicatorColor="?attr/colorPrimary"
|
|
||||||
app:trackColor="?attr/colorSurfaceContainerHighest"
|
|
||||||
tools:visibility="gone" />
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/myResumesList"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
|
||||||
android:layout_marginTop="@dimen/space_md"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:divider="@null"
|
|
||||||
android:listSelector="@android:color/transparent"
|
|
||||||
android:paddingBottom="@dimen/space_md"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -8,15 +8,13 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
tools:context=".ui.ResultFragment">
|
tools:context=".ui.ResultFragment">
|
||||||
|
|
||||||
<!-- Top action bar card with tonal surface -->
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginTop="@dimen/space_md"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="@dimen/space_sm"
|
android:layout_marginBottom="8dp"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
app:strokeWidth="1dp">
|
app:strokeWidth="1dp">
|
||||||
@ -25,31 +23,23 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:minHeight="@dimen/touch_target_min"
|
android:minHeight="48dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="@dimen/space_sm">
|
android:padding="12dp">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:cardBackgroundColor="?attr/colorSuccessContainer"
|
|
||||||
app:cardCornerRadius="16dp"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/resultBadge"
|
android:id="@+id/resultBadge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_status_badge"
|
||||||
android:contentDescription="@string/status_completed"
|
android:contentDescription="@string/status_completed"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:minHeight="32dp"
|
android:minHeight="32dp"
|
||||||
android:paddingHorizontal="@dimen/space_md"
|
android:paddingHorizontal="12dp"
|
||||||
android:paddingVertical="4dp"
|
android:paddingVertical="4dp"
|
||||||
android:text="@string/status_completed"
|
android:text="@string/status_completed"
|
||||||
android:textAppearance="?attr/textAppearanceLabelLarge"
|
android:textAppearance="?attr/textAppearanceLabelLarge"
|
||||||
android:textColor="?attr/colorOnSuccessContainer" />
|
android:textColor="?attr/colorOnPrimary" />
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -60,7 +50,7 @@
|
|||||||
android:id="@+id/copyButton"
|
android:id="@+id/copyButton"
|
||||||
style="@style/Widget.Material3.Button.TextButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:contentDescription="@string/cd_copy_result"
|
android:contentDescription="@string/cd_copy_result"
|
||||||
android:minWidth="64dp"
|
android:minWidth="64dp"
|
||||||
android:text="@string/copy_text" />
|
android:text="@string/copy_text" />
|
||||||
@ -69,8 +59,8 @@
|
|||||||
android:id="@+id/newButton"
|
android:id="@+id/newButton"
|
||||||
style="@style/Widget.Material3.Button.TextButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:layout_marginStart="@dimen/space_xs"
|
android:layout_marginStart="4dp"
|
||||||
android:contentDescription="@string/cd_new_job"
|
android:contentDescription="@string/cd_new_job"
|
||||||
android:minWidth="64dp"
|
android:minWidth="64dp"
|
||||||
android:text="@string/new_job" />
|
android:text="@string/new_job" />
|
||||||
@ -81,19 +71,17 @@
|
|||||||
android:id="@+id/resultWebView"
|
android:id="@+id/resultWebView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:contentDescription="@string/result_preview" />
|
android:contentDescription="@string/result_preview" />
|
||||||
|
|
||||||
<!-- Export action bar with tonal surface -->
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/space_md"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:layout_marginTop="@dimen/space_sm"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="@dimen/space_md"
|
android:layout_marginBottom="16dp"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
app:strokeWidth="1dp">
|
app:strokeWidth="1dp">
|
||||||
@ -102,14 +90,14 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="@dimen/space_sm">
|
android:padding="12dp">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/pdfButton"
|
android:id="@+id/pdfButton"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="@dimen/space_sm"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:contentDescription="@string/cd_export_pdf"
|
android:contentDescription="@string/cd_export_pdf"
|
||||||
android:text="@string/export_pdf" />
|
android:text="@string/export_pdf" />
|
||||||
@ -118,8 +106,8 @@
|
|||||||
android:id="@+id/docxButton"
|
android:id="@+id/docxButton"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="@dimen/button_height"
|
android:layout_height="48dp"
|
||||||
android:layout_marginStart="@dimen/space_sm"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:contentDescription="@string/cd_export_docx"
|
android:contentDescription="@string/cd_export_docx"
|
||||||
android:text="@string/export_docx" />
|
android:text="@string/export_docx" />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginVertical="@dimen/space_sm"
|
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:rippleColor="?attr/colorPrimaryContainer"
|
|
||||||
app:strokeColor="?attr/colorOutlineVariant"
|
|
||||||
app:strokeWidth="1dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:minHeight="@dimen/touch_target_min"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="@dimen/space_md">
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="@dimen/space_xxl"
|
|
||||||
android:layout_height="@dimen/space_xxl"
|
|
||||||
app:cardBackgroundColor="?attr/colorPrimaryContainer"
|
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/result_preview"
|
|
||||||
android:src="@drawable/ic_document"
|
|
||||||
app:tint="?attr/colorOnPrimaryContainer" />
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/space_md"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/itemTitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
|
||||||
android:textColor="?attr/colorOnSurface"
|
|
||||||
tools:text="Senior Android Engineer" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/itemSubtitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_xs"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnSurfaceVariant"
|
|
||||||
tools:text="Resume · 12 Aug 2026" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_marginStart="@dimen/space_sm"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_circle_status"
|
|
||||||
app:tint="?attr/colorOnSurfaceVariant" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/bg_gradient_primary"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/space_lg">
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLowest"
|
|
||||||
app:cardCornerRadius="@dimen/card_radius"
|
|
||||||
app:cardElevation="0dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/healthIndicator"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_margin="@dimen/space_xs"
|
|
||||||
android:contentDescription="@string/cd_health_indicator"
|
|
||||||
android:src="@drawable/ic_circle_status" />
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/userName"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_md"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:text="@string/unknown_user"
|
|
||||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
|
||||||
android:textColor="?attr/colorOnPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/userEmail"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/space_xs"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?attr/colorOnPrimary" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
10
app/src/main/res/menu/menu_main.xml
Normal file
10
app/src/main/res/menu/menu_main.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/menuAdminPanel"
|
||||||
|
android:title="@string/menu_admin_panel"
|
||||||
|
android:visible="false" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menuSignOut"
|
||||||
|
android:title="@string/menu_sign_out" />
|
||||||
|
</menu>
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<group
|
|
||||||
android:id="@+id/navGroupActions"
|
|
||||||
android:checkableBehavior="single">
|
|
||||||
<item
|
|
||||||
android:id="@+id/navBuildResume"
|
|
||||||
android:icon="@drawable/ic_sparkle"
|
|
||||||
android:title="@string/tab_build_resume" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/navBuildCoverLetter"
|
|
||||||
android:icon="@drawable/ic_sparkle"
|
|
||||||
android:title="@string/tab_build_cover_letter" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/navMyResumes"
|
|
||||||
android:icon="@drawable/ic_circle_status"
|
|
||||||
android:title="@string/menu_my_resumes" />
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group
|
|
||||||
android:id="@+id/navGroupAdmin"
|
|
||||||
android:checkableBehavior="none">
|
|
||||||
<item
|
|
||||||
android:id="@+id/navAdmin"
|
|
||||||
android:title="@string/menu_admin_panel"
|
|
||||||
android:visible="false" />
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group
|
|
||||||
android:id="@+id/navGroupAccount"
|
|
||||||
android:checkableBehavior="none">
|
|
||||||
<item
|
|
||||||
android:id="@+id/navSignOut"
|
|
||||||
android:title="@string/menu_sign_out" />
|
|
||||||
</group>
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@ -1,43 +1,41 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Theme.ResBuilder" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
<!-- Primary color roles (dark): bright teal on deep surfaces -->
|
<!-- Primary color roles (dark) -->
|
||||||
<item name="colorPrimary">#80DADC</item>
|
<item name="colorPrimary">#D0BCFF</item>
|
||||||
<item name="colorOnPrimary">#003738</item>
|
<item name="colorOnPrimary">#381E72</item>
|
||||||
<item name="colorPrimaryContainer">#005A5C</item>
|
<item name="colorPrimaryContainer">#4F378B</item>
|
||||||
<item name="colorOnPrimaryContainer">#B5F2F3</item>
|
<item name="colorOnPrimaryContainer">#EADDFF</item>
|
||||||
<item name="colorPrimaryInverse">#004F4F</item>
|
<item name="colorPrimaryInverse">#6750A4</item>
|
||||||
<item name="colorPrimaryFixed">#80DADC</item>
|
|
||||||
<item name="colorOnPrimaryFixed">#002020</item>
|
|
||||||
|
|
||||||
<!-- Secondary color roles (dark) -->
|
<!-- Secondary color roles (dark) -->
|
||||||
<item name="colorSecondary">#9CBEBD</item>
|
<item name="colorSecondary">#CCC2DC</item>
|
||||||
<item name="colorOnSecondary">#1B3534</item>
|
<item name="colorOnSecondary">#332D41</item>
|
||||||
<item name="colorSecondaryContainer">#2F4A49</item>
|
<item name="colorSecondaryContainer">#4A4458</item>
|
||||||
<item name="colorOnSecondaryContainer">#BDD6D5</item>
|
<item name="colorOnSecondaryContainer">#E8DEF8</item>
|
||||||
|
|
||||||
<!-- Tertiary color roles (dark) -->
|
<!-- Tertiary color roles (dark) -->
|
||||||
<item name="colorTertiary">#A4C9E8</item>
|
<item name="colorTertiary">#EFB8C8</item>
|
||||||
<item name="colorOnTertiary">#0B2D45</item>
|
<item name="colorOnTertiary">#492532</item>
|
||||||
<item name="colorTertiaryContainer">#27475F</item>
|
<item name="colorTertiaryContainer">#633B48</item>
|
||||||
<item name="colorOnTertiaryContainer">#BFE0FF</item>
|
<item name="colorOnTertiaryContainer">#FFD8E4</item>
|
||||||
|
|
||||||
<!-- Surface color roles (dark): deep charcoal-teal -->
|
<!-- Surface color roles (dark) -->
|
||||||
<item name="android:colorBackground">#0C1414</item>
|
<item name="android:colorBackground">#1D1B20</item>
|
||||||
<item name="colorSurface">#0C1414</item>
|
<item name="colorSurface">#1D1B20</item>
|
||||||
<item name="colorOnSurface">#E0ECEC</item>
|
<item name="colorOnSurface">#E6E1E5</item>
|
||||||
<item name="colorSurfaceVariant">#3F4A4A</item>
|
<item name="colorSurfaceVariant">#49454F</item>
|
||||||
<item name="colorOnSurfaceVariant">#BEC9C9</item>
|
<item name="colorOnSurfaceVariant">#CAC4D0</item>
|
||||||
<item name="colorSurfaceContainerLowest">#070E0E</item>
|
<item name="colorSurfaceContainerLowest">#16141C</item>
|
||||||
<item name="colorSurfaceContainerLow">#121B1B</item>
|
<item name="colorSurfaceContainerLow">#1D1B20</item>
|
||||||
<item name="colorSurfaceContainer">#182222</item>
|
<item name="colorSurfaceContainer">#211F26</item>
|
||||||
<item name="colorSurfaceContainerHigh">#242D2D</item>
|
<item name="colorSurfaceContainerHigh">#27252D</item>
|
||||||
<item name="colorSurfaceContainerHighest">#303939</item>
|
<item name="colorSurfaceContainerHighest">#36343B</item>
|
||||||
<item name="colorSurfaceInverse">#E0ECEC</item>
|
<item name="colorSurfaceInverse">#E6E0E9</item>
|
||||||
<item name="colorOnSurfaceInverse">#0C1414</item>
|
<item name="colorOnSurfaceInverse">#322F35</item>
|
||||||
|
|
||||||
<!-- Outline -->
|
<!-- Outline -->
|
||||||
<item name="colorOutline">#899999</item>
|
<item name="colorOutline">#938F99</item>
|
||||||
<item name="colorOutlineVariant">#3F4A4A</item>
|
<item name="colorOutlineVariant">#49454F</item>
|
||||||
|
|
||||||
<!-- Error (dark) -->
|
<!-- Error (dark) -->
|
||||||
<item name="colorError">#F2B8B5</item>
|
<item name="colorError">#F2B8B5</item>
|
||||||
@ -61,12 +59,12 @@
|
|||||||
<item name="colorInfoContainer">#0B4576</item>
|
<item name="colorInfoContainer">#0B4576</item>
|
||||||
<item name="colorOnInfoContainer">#D4EAFB</item>
|
<item name="colorOnInfoContainer">#D4EAFB</item>
|
||||||
|
|
||||||
<!-- System bars -->
|
<!-- System bars - edge-to-edge ready (dark) -->
|
||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:windowLightStatusBar">false</item>
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
|
||||||
<!-- Typography -->
|
<!-- M3 Typography and shape defaults inherited from Material3 parent -->
|
||||||
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
|
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
|
||||||
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
|
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
|
||||||
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
|
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
|
||||||
@ -83,7 +81,7 @@
|
|||||||
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
|
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
|
||||||
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
|
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
|
||||||
|
|
||||||
<!-- Component defaults -->
|
<!-- Default M3 component styles inherited from Material3 parent; explicit overrides kept to a minimum -->
|
||||||
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
|
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
|
||||||
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
|
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
|
||||||
|
|
||||||
|
|||||||
@ -2,78 +2,77 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Material Design 3 semantic color tokens -->
|
<!-- Material Design 3 semantic color tokens -->
|
||||||
|
|
||||||
<!-- Primary palette: rich teal with stronger contrast -->
|
<!-- Primary palette -->
|
||||||
<color name="primary">#004F4F</color>
|
<color name="primary">#6750A4</color>
|
||||||
<color name="on_primary">#FFFFFF</color>
|
<color name="on_primary">#FFFFFF</color>
|
||||||
<color name="primary_container">#B5F2F3</color>
|
<color name="primary_container">#EADDFF</color>
|
||||||
<color name="on_primary_container">#002020</color>
|
<color name="on_primary_container">#21005D</color>
|
||||||
<color name="primary_fixed">#B5F2F3</color>
|
|
||||||
<color name="on_primary_fixed">#002020</color>
|
|
||||||
|
|
||||||
<!-- Secondary palette: cool slate with more depth -->
|
<!-- Secondary palette -->
|
||||||
<color name="secondary">#3D5252</color>
|
<color name="secondary">#625B71</color>
|
||||||
<color name="on_secondary">#FFFFFF</color>
|
<color name="on_secondary">#FFFFFF</color>
|
||||||
<color name="secondary_container">#BDD6D5</color>
|
<color name="secondary_container">#E8DEF8</color>
|
||||||
<color name="on_secondary_container">#041E1E</color>
|
<color name="on_secondary_container">#1D192B</color>
|
||||||
|
|
||||||
<!-- Tertiary palette: deep blue-grey accent -->
|
<!-- Tertiary palette -->
|
||||||
<color name="tertiary">#365064</color>
|
<color name="tertiary">#7D5260</color>
|
||||||
<color name="on_tertiary">#FFFFFF</color>
|
<color name="on_tertiary">#FFFFFF</color>
|
||||||
<color name="tertiary_container">#BFE0FF</color>
|
<color name="tertiary_container">#FFD8E4</color>
|
||||||
<color name="on_tertiary_container">#001D31</color>
|
<color name="on_tertiary_container">#31111D</color>
|
||||||
|
|
||||||
<!-- Surface colors: warmer off-white base with visible tonal elevation -->
|
<!-- Surface colors -->
|
||||||
<color name="surface">#FAFDFD</color>
|
<color name="surface">#FFFBFE</color>
|
||||||
<color name="on_surface">#0A1414</color>
|
<color name="on_surface">#1D1B20</color>
|
||||||
<color name="surface_variant">#D8E5E5</color>
|
<color name="surface_variant">#E7E0EC</color>
|
||||||
<color name="on_surface_variant">#3F4A4A</color>
|
<color name="on_surface_variant">#49454F</color>
|
||||||
<color name="surface_container_lowest">#FFFFFF</color>
|
<color name="surface_container_lowest">#FFFFFF</color>
|
||||||
<color name="surface_container_low">#F0F6F6</color>
|
<color name="surface_container_low">#F5EFF7</color>
|
||||||
<color name="surface_container">#E8F1F1</color>
|
<color name="surface_container">#F3EDF7</color>
|
||||||
<color name="surface_container_high">#DFEAEA</color>
|
<color name="surface_container_high">#ECE6F0</color>
|
||||||
<color name="surface_container_highest">#D6E4E4</color>
|
<color name="surface_container_highest">#E6E0E9</color>
|
||||||
|
|
||||||
<!-- Outline -->
|
<!-- Outline -->
|
||||||
<color name="outline">#6F7E7E</color>
|
<color name="outline">#79747E</color>
|
||||||
<color name="outline_variant">#BEC9C9</color>
|
<color name="outline_variant">#CAC4D0</color>
|
||||||
|
|
||||||
<!-- Status / Semantic colors: richer and more saturated -->
|
<!-- Status / Semantic colors -->
|
||||||
<color name="success">#2E7D32</color>
|
<color name="success">#4CAF50</color>
|
||||||
<color name="on_success">#FFFFFF</color>
|
<color name="on_success">#FFFFFF</color>
|
||||||
<color name="success_container">#D9F6DA</color>
|
<color name="success_container">#D9F6DA</color>
|
||||||
<color name="on_success_container">#1F5E22</color>
|
<color name="on_success_container">#1F5E22</color>
|
||||||
|
|
||||||
<color name="warning">#E65100</color>
|
<color name="warning">#FF9800</color>
|
||||||
<color name="on_warning">#FFFFFF</color>
|
<color name="on_warning">#FFFFFF</color>
|
||||||
<color name="warning_container">#FFE3B6</color>
|
<color name="warning_container">#FFE3B6</color>
|
||||||
<color name="on_warning_container">#693B00</color>
|
<color name="on_warning_container">#693B00</color>
|
||||||
|
|
||||||
<color name="error">#C62828</color>
|
<color name="error">#F44336</color>
|
||||||
<color name="on_error">#FFFFFF</color>
|
<color name="on_error">#FFFFFF</color>
|
||||||
<color name="error_container">#F9DEDC</color>
|
<color name="error_container">#F9DEDC</color>
|
||||||
<color name="on_error_container">#410E0B</color>
|
<color name="on_error_container">#410E0B</color>
|
||||||
|
|
||||||
<color name="info">#1565C0</color>
|
<color name="info">#2196F3</color>
|
||||||
<color name="on_info">#FFFFFF</color>
|
<color name="on_info">#FFFFFF</color>
|
||||||
<color name="info_container">#D4EAFB</color>
|
<color name="info_container">#D4EAFB</color>
|
||||||
<color name="on_info_container">#0B4576</color>
|
<color name="on_info_container">#0B4576</color>
|
||||||
|
|
||||||
<!-- Inverse surface -->
|
<!-- Inverse surface -->
|
||||||
<color name="inverse_surface">#2D3131</color>
|
<color name="inverse_surface">#322F35</color>
|
||||||
<color name="inverse_on_surface">#DFE4E4</color>
|
<color name="inverse_on_surface">#F5EFF7</color>
|
||||||
<color name="inverse_primary">#80DADC</color>
|
<color name="inverse_primary">#D0BCFF</color>
|
||||||
|
|
||||||
<!-- Status indicator palettes -->
|
<!-- Status indicator palettes (kept for programmatic tint compatibility) -->
|
||||||
<color name="status_healthy">@color/success</color>
|
<color name="status_healthy">@color/success</color>
|
||||||
<color name="status_degraded">@color/warning</color>
|
<color name="status_degraded">@color/warning</color>
|
||||||
<color name="status_unhealthy">@color/error</color>
|
<color name="status_unhealthy">@color/error</color>
|
||||||
|
|
||||||
<!-- Legacy aliases -->
|
<!-- Legacy aliases to avoid breaking any remaining references during migration -->
|
||||||
<color name="purple_200">@color/primary_container</color>
|
<color name="purple_200">@color/primary_container</color>
|
||||||
<color name="purple_500">@color/primary</color>
|
<color name="purple_500">@color/primary</color>
|
||||||
<color name="purple_700">@color/on_primary_container</color>
|
<color name="purple_700">@color/on_primary_container</color>
|
||||||
<color name="teal_200">@color/primary_container</color>
|
<color name="teal_200">@color/secondary_container</color>
|
||||||
<color name="teal_700">@color/primary</color>
|
<color name="teal_700">@color/secondary</color>
|
||||||
|
<!-- Legacy neutral aliases kept for programmatic compatibility in unchanged Kotlin code -->
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Spacing system: multiples of 4dp -->
|
|
||||||
<dimen name="space_xs">4dp</dimen>
|
|
||||||
<dimen name="space_sm">8dp</dimen>
|
|
||||||
<dimen name="space_md">16dp</dimen>
|
|
||||||
<dimen name="space_lg">24dp</dimen>
|
|
||||||
<dimen name="space_xl">32dp</dimen>
|
|
||||||
<dimen name="space_xxl">48dp</dimen>
|
|
||||||
|
|
||||||
<!-- Component dimensions -->
|
|
||||||
<dimen name="card_radius">12dp</dimen>
|
|
||||||
<dimen name="card_radius_large">16dp</dimen>
|
|
||||||
<dimen name="button_height">48dp</dimen>
|
|
||||||
<dimen name="touch_target_min">48dp</dimen>
|
|
||||||
<dimen name="hero_header_min_height">160dp</dimen>
|
|
||||||
<dimen name="app_logo_size">64dp</dimen>
|
|
||||||
</resources>
|
|
||||||
@ -4,16 +4,6 @@
|
|||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
<string name="sign_in_with_google">Sign in with Google</string>
|
<string name="sign_in_with_google">Sign in with Google</string>
|
||||||
<string name="checking_auth">Checking authentication…</string>
|
<string name="checking_auth">Checking authentication…</string>
|
||||||
<string name="welcome_subtitle">AI-powered resume and cover letter builder</string>
|
|
||||||
<string name="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
|
|
||||||
<string name="sign_in_interrupted">Sign-in interrupted</string>
|
|
||||||
<string name="no_id_token">No ID token from Google</string>
|
|
||||||
<string name="invalid_google_id_token">Invalid Google ID token: %1$s</string>
|
|
||||||
<string name="unexpected_credential_type">Unexpected credential type</string>
|
|
||||||
<string name="no_google_accounts">No Google accounts found on device</string>
|
|
||||||
<string name="sign_in_cancelled">Sign-in cancelled</string>
|
|
||||||
<string name="sign_in_failed">Sign-in failed: %1$s</string>
|
|
||||||
<string name="welcome_user">Welcome %1$s</string>
|
|
||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<string name="app_title">resBuilder</string>
|
<string name="app_title">resBuilder</string>
|
||||||
@ -23,7 +13,6 @@
|
|||||||
<string name="menu_admin_panel">Admin Panel</string>
|
<string name="menu_admin_panel">Admin Panel</string>
|
||||||
<string name="usage_limit_banner">You have reached your free limit. Please contact support to upgrade.</string>
|
<string name="usage_limit_banner">You have reached your free limit. Please contact support to upgrade.</string>
|
||||||
<string name="unknown_user">Unknown user</string>
|
<string name="unknown_user">Unknown user</string>
|
||||||
<string name="unknown_email">Not signed in</string>
|
|
||||||
|
|
||||||
<!-- Job form -->
|
<!-- Job form -->
|
||||||
<string name="hint_job_title">Job Title</string>
|
<string name="hint_job_title">Job Title</string>
|
||||||
@ -37,7 +26,6 @@
|
|||||||
<string name="generate_cover_letter">Generate Cover Letter</string>
|
<string name="generate_cover_letter">Generate Cover Letter</string>
|
||||||
<string name="please_log_in">Please log in to continue</string>
|
<string name="please_log_in">Please log in to continue</string>
|
||||||
<string name="fields_required">Please fill in all fields</string>
|
<string name="fields_required">Please fill in all fields</string>
|
||||||
<string name="enter_url">Enter a URL</string>
|
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<string name="status_queued">Queued</string>
|
<string name="status_queued">Queued</string>
|
||||||
@ -56,9 +44,6 @@
|
|||||||
<string name="export_docx">DOCX</string>
|
<string name="export_docx">DOCX</string>
|
||||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||||
<string name="nothing_to_copy">Nothing to copy</string>
|
<string name="nothing_to_copy">Nothing to copy</string>
|
||||||
<string name="exporting">Exporting…</string>
|
|
||||||
<string name="export_failed">Export failed</string>
|
|
||||||
<string name="export_no_viewer">No app found to open this file</string>
|
|
||||||
|
|
||||||
<!-- Accessibility labels -->
|
<!-- Accessibility labels -->
|
||||||
<string name="cd_app_logo">resBuilder app logo</string>
|
<string name="cd_app_logo">resBuilder app logo</string>
|
||||||
@ -90,12 +75,4 @@
|
|||||||
<string name="error_state_message">We could not load this screen. Please try again.</string>
|
<string name="error_state_message">We could not load this screen. Please try again.</string>
|
||||||
<string name="usage_limit_title">Free limit reached</string>
|
<string name="usage_limit_title">Free limit reached</string>
|
||||||
<string name="result_preview">Result preview</string>
|
<string name="result_preview">Result preview</string>
|
||||||
|
|
||||||
<!-- My Resumes -->
|
|
||||||
<string name="menu_my_resumes">My Resumes</string>
|
|
||||||
<string name="my_resumes_title">My Documents</string>
|
|
||||||
<string name="my_resumes_empty">No generated documents yet. Submit a job to see your results here.</string>
|
|
||||||
<string name="my_resumes_error">Could not load your documents. Please try again.</string>
|
|
||||||
<string name="type_resume">Resume</string>
|
|
||||||
<string name="type_cover_letter">Cover letter</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -6,8 +6,6 @@
|
|||||||
<item name="colorPrimaryContainer">@color/primary_container</item>
|
<item name="colorPrimaryContainer">@color/primary_container</item>
|
||||||
<item name="colorOnPrimaryContainer">@color/on_primary_container</item>
|
<item name="colorOnPrimaryContainer">@color/on_primary_container</item>
|
||||||
<item name="colorPrimaryInverse">@color/inverse_primary</item>
|
<item name="colorPrimaryInverse">@color/inverse_primary</item>
|
||||||
<item name="colorPrimaryFixed">@color/primary_fixed</item>
|
|
||||||
<item name="colorOnPrimaryFixed">@color/on_primary_fixed</item>
|
|
||||||
|
|
||||||
<!-- Secondary color roles -->
|
<!-- Secondary color roles -->
|
||||||
<item name="colorSecondary">@color/secondary</item>
|
<item name="colorSecondary">@color/secondary</item>
|
||||||
@ -66,7 +64,7 @@
|
|||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
|
|
||||||
<!-- M3 Typography -->
|
<!-- M3 Typography and shape defaults inherited from Material3 parent -->
|
||||||
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
|
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
|
||||||
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
|
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
|
||||||
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
|
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
|
||||||
@ -83,10 +81,11 @@
|
|||||||
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
|
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
|
||||||
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
|
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
|
||||||
|
|
||||||
<!-- Default M3 component styles -->
|
<!-- Default M3 component styles inherited from Material3 parent; explicit overrides kept to a minimum -->
|
||||||
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
|
<item name="materialButtonStyle">@style/Widget.Material3.Button</item>
|
||||||
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
|
<item name="materialButtonOutlinedStyle">@style/Widget.Material3.Button.OutlinedButton</item>
|
||||||
|
|
||||||
|
<!-- Window background uses surface so app surfaces are always theme-aware -->
|
||||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths>
|
|
||||||
<cache-path
|
|
||||||
name="exports"
|
|
||||||
path="exports/" />
|
|
||||||
</paths>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<network-security-config>
|
|
||||||
<!-- Enforce HTTPS for the backend API domain. Cleartext is disallowed. -->
|
|
||||||
<domain-config cleartextTrafficPermitted="false">
|
|
||||||
<domain includeSubdomains="false">resume.aridgwayweb.com</domain>
|
|
||||||
</domain-config>
|
|
||||||
<base-config cleartextTrafficPermitted="false" />
|
|
||||||
</network-security-config>
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
package com.example.resbuilder
|
|
||||||
|
|
||||||
import com.example.resbuilder.data.model.JobListResponse
|
|
||||||
import com.example.resbuilder.data.model.JobSummary
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for the API response models introduced with the "My Resumes" feature.
|
|
||||||
*
|
|
||||||
* These verify the exact wire format expected from `GET /jobs` so that a backend
|
|
||||||
* contract change fails loudly here rather than silently breaking the UI.
|
|
||||||
*/
|
|
||||||
class JobListModelsTest {
|
|
||||||
|
|
||||||
private val gson = Gson()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parses a completed resume job from the backend shape`() {
|
|
||||||
val json = """
|
|
||||||
{
|
|
||||||
"jobs": [
|
|
||||||
{
|
|
||||||
"job_id": "abc-123",
|
|
||||||
"type": "resume",
|
|
||||||
"status": "completed",
|
|
||||||
"title": "Senior Android Engineer",
|
|
||||||
"created_at": "2026-08-04T12:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val parsed = gson.fromJson(json, JobListResponse::class.java)
|
|
||||||
|
|
||||||
assertEquals(1, parsed.jobs.size)
|
|
||||||
val job = parsed.jobs[0]
|
|
||||||
assertEquals("abc-123", job.job_id)
|
|
||||||
assertEquals("resume", job.type)
|
|
||||||
assertEquals("completed", job.status)
|
|
||||||
assertEquals("Senior Android Engineer", job.title)
|
|
||||||
assertEquals("2026-08-04T12:00:00Z", job.created_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `title is null when backend omits it`() {
|
|
||||||
val json = """
|
|
||||||
{"jobs": [{"job_id": "x", "type": "cover_letter", "status": "queued"}]}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val parsed = gson.fromJson(json, JobListResponse::class.java)
|
|
||||||
val job = parsed.jobs[0]
|
|
||||||
|
|
||||||
assertNull(job.title)
|
|
||||||
assertNull(job.created_at)
|
|
||||||
assertEquals("cover_letter", job.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `empty jobs list parses to empty collection`() {
|
|
||||||
val json = """{"jobs": []}"""
|
|
||||||
|
|
||||||
val parsed = gson.fromJson(json, JobListResponse::class.java)
|
|
||||||
|
|
||||||
assertTrue(parsed.jobs.isEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `JobSummary fields are immutable`() {
|
|
||||||
val job = JobSummary(job_id = "id", type = "resume", status = "completed")
|
|
||||||
|
|
||||||
// Reflectively assert all fields are val (final). This guards against
|
|
||||||
// regressions that would make the model mutable and thread-unsafe.
|
|
||||||
for (field in JobSummary::class.java.declaredFields) {
|
|
||||||
assertEquals("Field ${field.name} must be final", java.lang.reflect.Modifier.FINAL, field.modifiers and java.lang.reflect.Modifier.FINAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,18 +10,9 @@ okhttp = "4.12.0"
|
|||||||
gson = "2.10.1"
|
gson = "2.10.1"
|
||||||
browser = "1.7.0"
|
browser = "1.7.0"
|
||||||
googlePlayServicesAuth = "20.7.0"
|
googlePlayServicesAuth = "20.7.0"
|
||||||
credentialManager = "1.5.0-beta01"
|
|
||||||
googleid = "1.1.1"
|
|
||||||
kotlinxCoroutines = "1.9.0"
|
|
||||||
lifecycleViewmodel = "2.8.7"
|
|
||||||
lifecycleRuntime = "2.8.7"
|
|
||||||
fragmentKtx = "1.8.5"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodel" }
|
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" }
|
|
||||||
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
@ -31,10 +22,6 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
|
|||||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||||
browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
|
browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
|
||||||
google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayServicesAuth" }
|
google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayServicesAuth" }
|
||||||
androidx-credential-manager = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" }
|
|
||||||
androidx-credential-manager-play-services = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialManager" }
|
|
||||||
googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleid" }
|
|
||||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user