Compare commits
20 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
eb7e19228b | |
|
|
3cd8a005c9 | |
|
|
c5750e6b5e | |
|
|
04d06cd16d | |
|
|
4d7e8e3daf | |
|
|
80cc72bb9d | |
|
|
1277ae09b6 | |
|
|
f8882c1dcc | |
|
|
feca34f892 | |
|
|
d46bc1f461 | |
|
|
88161f7933 | |
|
|
682ed78491 | |
|
|
95c6c598a0 | |
|
|
078bc02c36 | |
|
|
b60f5b9708 | |
|
|
3c8288007a | |
|
|
715b591b8a | |
|
|
6628daed50 | |
|
|
6b634f40b7 | |
|
|
4f8ae88820 |
|
|
@ -64,6 +64,7 @@ dependencies {
|
||||||
|
|
||||||
// AndroidX Security
|
// AndroidX Security
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
|
implementation(libs.androidx.room.runtime.android)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|
@ -72,4 +73,19 @@ dependencies {
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
|
|
||||||
|
//UI
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
androidTestImplementation(platform("androidx.compose:compose-bom:2023.09.00"))
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
|
implementation("androidx.compose.foundation:foundation")
|
||||||
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
|
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||||
|
implementation("androidx.media3:media3-ui:1.3.1")
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.LivingAi_Lg"
|
android:theme="@style/Theme.LivingAi_Lg"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|
@ -25,6 +26,15 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<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>
|
||||||
|
|
@ -4,14 +4,9 @@ import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
|
|
@ -19,39 +14,42 @@ import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.example.livingai_lg.ui.AuthState
|
|
||||||
import com.example.livingai_lg.ui.MainViewModel
|
import com.example.livingai_lg.ui.MainViewModel
|
||||||
import com.example.livingai_lg.ui.MainViewModelFactory
|
import com.example.livingai_lg.ui.MainViewModelFactory
|
||||||
import com.example.livingai_lg.ui.login.*
|
import com.example.livingai_lg.ui.login_legacy.*
|
||||||
import com.example.livingai_lg.ui.theme.LivingAi_LgTheme
|
import com.example.livingai_lg.ui.navigation.AppNavigation
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmMarketplaceTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
//enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
LivingAi_LgTheme {
|
FarmMarketplaceTheme {
|
||||||
val mainViewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current))
|
val mainViewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current))
|
||||||
val authState by mainViewModel.authState.collectAsState()
|
val authState by mainViewModel.authState.collectAsState()
|
||||||
|
|
||||||
when (authState) {
|
AppNavigation(authState = authState, mainViewModel = mainViewModel)
|
||||||
is AuthState.Unknown -> {
|
// when (authState) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
// is AuthState.Unknown -> {
|
||||||
CircularProgressIndicator()
|
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
}
|
// CircularProgressIndicator()
|
||||||
}
|
// }
|
||||||
is AuthState.Authenticated -> {
|
// }
|
||||||
SuccessScreen(mainViewModel)
|
// is AuthState.Authenticated -> {
|
||||||
}
|
// SuccessScreen(mainViewModel)
|
||||||
is AuthState.Unauthenticated -> {
|
// }
|
||||||
AuthNavigation()
|
// is AuthState.Unauthenticated -> {
|
||||||
}
|
// AuthNavigation()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: remove the old code after testing new stuff
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthNavigation() {
|
fun AuthNavigation() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.example.livingai_lg.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import com.example.livingai_lg.BuildConfig
|
import com.example.livingai_lg.BuildConfig
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
|
|
@ -11,14 +12,22 @@ import io.ktor.client.plugins.auth.*
|
||||||
import io.ktor.client.plugins.auth.providers.*
|
import io.ktor.client.plugins.auth.providers.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
// Custom exception for user not found
|
||||||
|
class UserNotFoundException(
|
||||||
|
message: String,
|
||||||
|
val errorCode: String
|
||||||
|
) : Exception(message)
|
||||||
|
|
||||||
class AuthApiClient(private val context: Context) {
|
class AuthApiClient(private val context: Context) {
|
||||||
|
|
||||||
|
private val route = "http://10.0.2.2:3000/"
|
||||||
private val tokenManager = TokenManager(context)
|
private val tokenManager = TokenManager(context)
|
||||||
|
|
||||||
val client = HttpClient(CIO) {
|
val client = HttpClient(CIO) {
|
||||||
|
|
@ -31,41 +40,87 @@ class AuthApiClient(private val context: Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
install(Auth) {
|
install(Auth) {
|
||||||
bearer {
|
bearer {
|
||||||
loadTokens {
|
loadTokens {
|
||||||
val accessToken = tokenManager.getAccessToken()
|
val accessToken = tokenManager.getAccessToken()
|
||||||
val refreshToken = tokenManager.getRefreshToken()
|
val refreshToken = tokenManager.getRefreshToken()
|
||||||
|
android.util.Log.d("AuthApiClient", "loadTokens: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}")
|
||||||
if (accessToken != null && refreshToken != null) {
|
if (accessToken != null && refreshToken != null) {
|
||||||
|
android.util.Log.d("AuthApiClient", "loadTokens: Returning BearerTokens")
|
||||||
BearerTokens(accessToken, refreshToken)
|
BearerTokens(accessToken, refreshToken)
|
||||||
} else {
|
} else {
|
||||||
|
android.util.Log.d("AuthApiClient", "loadTokens: No tokens available, returning null")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTokens {
|
refreshTokens {
|
||||||
val refreshToken = tokenManager.getRefreshToken() ?: return@refreshTokens null
|
android.util.Log.d("AuthApiClient", "refreshTokens: Starting token refresh")
|
||||||
|
val refreshToken = tokenManager.getRefreshToken() ?: run {
|
||||||
|
android.util.Log.e("AuthApiClient", "refreshTokens: No refresh token found!")
|
||||||
|
return@refreshTokens null
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("AuthApiClient", "refreshTokens: Calling /auth/refresh endpoint")
|
||||||
|
try {
|
||||||
|
val response: RefreshResponse = client.post("${route}auth/refresh") {
|
||||||
|
markAsRefreshTokenRequest()
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(RefreshRequest(refreshToken))
|
||||||
|
}.body()
|
||||||
|
|
||||||
val response: RefreshResponse = client.post("http://10.0.2.2:3000/auth/refresh") {
|
android.util.Log.d("AuthApiClient", "refreshTokens: Refresh successful, saving new tokens")
|
||||||
markAsRefreshTokenRequest()
|
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||||
contentType(ContentType.Application.Json)
|
android.util.Log.d("AuthApiClient", "refreshTokens: New tokens saved successfully")
|
||||||
setBody(RefreshRequest(refreshToken))
|
|
||||||
}.body()
|
|
||||||
|
|
||||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
BearerTokens(response.accessToken, response.refreshToken)
|
||||||
|
} catch (e: Exception) {
|
||||||
BearerTokens(response.accessToken, response.refreshToken)
|
android.util.Log.e("AuthApiClient", "refreshTokens: Refresh failed: ${e.message}", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
url("http://10.0.2.2:3000/")
|
url(route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- API Calls ---
|
// --- API Calls ---
|
||||||
|
|
||||||
|
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> = runCatching {
|
||||||
|
val response = client.post("auth/check-user") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(CheckUserRequest(phoneNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
// Success - parse as CheckUserResponse
|
||||||
|
response.body<CheckUserResponse>()
|
||||||
|
} else {
|
||||||
|
// Error - parse as ErrorResponse
|
||||||
|
val errorResponse = try {
|
||||||
|
response.body<ErrorResponse>()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If parsing fails, create default error
|
||||||
|
ErrorResponse(
|
||||||
|
success = false,
|
||||||
|
error = "USER_NOT_FOUND",
|
||||||
|
message = "User is not registered. Please sign up to create a new account.",
|
||||||
|
userExists = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UserNotFoundException(
|
||||||
|
message = errorResponse.message ?: "User is not registered. Please sign up to create a new account.",
|
||||||
|
errorCode = errorResponse.error ?: "USER_NOT_FOUND"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> = runCatching {
|
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> = runCatching {
|
||||||
client.post("auth/request-otp") {
|
client.post("auth/request-otp") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
|
|
@ -74,13 +129,38 @@ class AuthApiClient(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun verifyOtp(phoneNumber: String, code: String, deviceId: String): Result<VerifyOtpResponse> = runCatching {
|
suspend fun verifyOtp(phoneNumber: String, code: String, deviceId: String): Result<VerifyOtpResponse> = runCatching {
|
||||||
val response: VerifyOtpResponse = client.post("auth/verify-otp") {
|
val response = client.post("auth/verify-otp") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo()))
|
setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo()))
|
||||||
}.body()
|
}
|
||||||
|
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
val verifyResponse: VerifyOtpResponse = response.body()
|
||||||
|
tokenManager.saveTokens(verifyResponse.accessToken, verifyResponse.refreshToken)
|
||||||
|
verifyResponse
|
||||||
|
} else {
|
||||||
|
// Parse error response
|
||||||
|
val errorResponse: ErrorResponse = response.body()
|
||||||
|
throw UserNotFoundException(
|
||||||
|
message = errorResponse.message ?: "User not found",
|
||||||
|
errorCode = errorResponse.error ?: "USER_NOT_FOUND"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
suspend fun signup(request: SignupRequest): Result<SignupResponse> = runCatching {
|
||||||
response
|
val response = client.post("auth/signup") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(request.copy(deviceId = getDeviceId(), deviceInfo = getDeviceInfo()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of throwing an exception on non-2xx, we return a result type
|
||||||
|
// that can be handled by the caller.
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
response.body<SignupResponse>()
|
||||||
|
} else {
|
||||||
|
response.body<SignupResponse>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateProfile(name: String, userType: String): Result<User> = runCatching {
|
suspend fun updateProfile(name: String, userType: String): Result<User> = runCatching {
|
||||||
|
|
@ -91,7 +171,40 @@ class AuthApiClient(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUserDetails(): Result<UserDetails> = runCatching {
|
suspend fun getUserDetails(): Result<UserDetails> = runCatching {
|
||||||
client.get("users/me").body()
|
android.util.Log.d("AuthApiClient", "getUserDetails: Calling /users/me endpoint")
|
||||||
|
try {
|
||||||
|
val response = client.get("users/me")
|
||||||
|
android.util.Log.d("AuthApiClient", "getUserDetails: Response status=${response.status}")
|
||||||
|
val userDetails = response.body<UserDetails>()
|
||||||
|
android.util.Log.d("AuthApiClient", "getUserDetails: Success - user id=${userDetails.id}")
|
||||||
|
userDetails
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("AuthApiClient", "getUserDetails: Error - ${e.message}", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshToken(): Result<RefreshResponse> = runCatching {
|
||||||
|
android.util.Log.d("AuthApiClient", "refreshToken: Starting manual token refresh")
|
||||||
|
val refreshToken = tokenManager.getRefreshToken()
|
||||||
|
?: throw IllegalStateException("No refresh token found")
|
||||||
|
|
||||||
|
android.util.Log.d("AuthApiClient", "refreshToken: Calling /auth/refresh endpoint")
|
||||||
|
try {
|
||||||
|
val response: RefreshResponse = client.post("auth/refresh") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(RefreshRequest(refreshToken))
|
||||||
|
}.body()
|
||||||
|
|
||||||
|
android.util.Log.d("AuthApiClient", "refreshToken: Refresh successful, saving new tokens")
|
||||||
|
// Save the new tokens (refresh token rotates)
|
||||||
|
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||||
|
android.util.Log.d("AuthApiClient", "refreshToken: New tokens saved successfully")
|
||||||
|
response
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("AuthApiClient", "refreshToken: Refresh failed: ${e.message}", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun logout(): Result<LogoutResponse> = runCatching {
|
suspend fun logout(): Result<LogoutResponse> = runCatching {
|
||||||
|
|
@ -105,6 +218,55 @@ class AuthApiClient(private val context: Context) {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test API: Get user by ID from BuySellService (port 3200)
|
||||||
|
suspend fun getUserById(userId: String, baseUrl: String = "http://10.0.2.2:3200"): Result<String> = runCatching {
|
||||||
|
android.util.Log.d("AuthApiClient", "getUserById: Calling $baseUrl/users/$userId")
|
||||||
|
|
||||||
|
// Create a separate client for this external service call
|
||||||
|
HttpClient(CIO) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
prettyPrint = true
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
install(Auth) {
|
||||||
|
bearer {
|
||||||
|
loadTokens {
|
||||||
|
val accessToken = tokenManager.getAccessToken()
|
||||||
|
if (accessToken != null) {
|
||||||
|
BearerTokens(accessToken, "")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultRequest {
|
||||||
|
url(baseUrl)
|
||||||
|
}
|
||||||
|
}.use { testClient ->
|
||||||
|
val response = testClient.get("users/$userId")
|
||||||
|
android.util.Log.d("AuthApiClient", "getUserById: Response status=${response.status}")
|
||||||
|
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
// Get raw JSON string
|
||||||
|
val jsonString = response.bodyAsText()
|
||||||
|
android.util.Log.d("AuthApiClient", "getUserById: Success - JSON=${jsonString.take(200)}...")
|
||||||
|
jsonString
|
||||||
|
} else {
|
||||||
|
val errorText = try {
|
||||||
|
response.bodyAsText()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"Error reading response body"
|
||||||
|
}
|
||||||
|
android.util.Log.e("AuthApiClient", "getUserById: Error - status=${response.status}, body=$errorText")
|
||||||
|
throw Exception("API call failed: ${response.status} - $errorText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getDeviceInfo(): DeviceInfo {
|
private fun getDeviceInfo(): DeviceInfo {
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
platform = "android",
|
platform = "android",
|
||||||
|
|
@ -115,4 +277,8 @@ class AuthApiClient(private val context: Context) {
|
||||||
timezone = TimeZone.getDefault().id
|
timezone = TimeZone.getDefault().id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDeviceId(): String {
|
||||||
|
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ class AuthManager(
|
||||||
private val apiClient: AuthApiClient,
|
private val apiClient: AuthApiClient,
|
||||||
private val tokenManager: TokenManager
|
private val tokenManager: TokenManager
|
||||||
) {
|
) {
|
||||||
|
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> {
|
||||||
|
return apiClient.checkUser(phoneNumber)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
|
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
|
||||||
return apiClient.requestOtp(phoneNumber)
|
return apiClient.requestOtp(phoneNumber)
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +20,22 @@ class AuthManager(
|
||||||
val deviceId = getDeviceId()
|
val deviceId = getDeviceId()
|
||||||
return apiClient.verifyOtp(phoneNumber, code, deviceId)
|
return apiClient.verifyOtp(phoneNumber, code, deviceId)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
response.accessToken?.let { accessToken ->
|
||||||
|
response.refreshToken?.let { refreshToken ->
|
||||||
|
tokenManager.saveTokens(accessToken, refreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun signup(signupRequest: SignupRequest): Result<SignupResponse> {
|
||||||
|
return apiClient.signup(signupRequest)
|
||||||
|
.onSuccess { response ->
|
||||||
|
response.accessToken?.let { accessToken ->
|
||||||
|
response.refreshToken?.let { refreshToken ->
|
||||||
|
tokenManager.saveTokens(accessToken, refreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,6 +43,10 @@ class AuthManager(
|
||||||
return apiClient.updateProfile(name, userType)
|
return apiClient.updateProfile(name, userType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun logout(): Result<com.example.livingai_lg.api.LogoutResponse> {
|
||||||
|
return apiClient.logout()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getDeviceId(): String {
|
private fun getDeviceId(): String {
|
||||||
return Settings.Secure.getString(
|
return Settings.Secure.getString(
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.example.livingai_lg.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
|
|
||||||
|
|
@ -20,25 +21,47 @@ class TokenManager(context: Context) {
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "TokenManager"
|
||||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveTokens(accessToken: String, refreshToken: String) {
|
fun saveTokens(accessToken: String, refreshToken: String) {
|
||||||
prefs.edit()
|
Log.d(TAG, "saveTokens: Saving tokens (accessToken length=${accessToken.length}, refreshToken length=${refreshToken.length})")
|
||||||
|
val success = prefs.edit()
|
||||||
.putString(KEY_ACCESS_TOKEN, accessToken)
|
.putString(KEY_ACCESS_TOKEN, accessToken)
|
||||||
.putString(KEY_REFRESH_TOKEN, refreshToken)
|
.putString(KEY_REFRESH_TOKEN, refreshToken)
|
||||||
.apply()
|
.commit() // Use commit() instead of apply() to ensure tokens are saved synchronously
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Log.d(TAG, "saveTokens: Tokens saved successfully")
|
||||||
|
// Verify tokens were saved
|
||||||
|
val savedAccess = prefs.getString(KEY_ACCESS_TOKEN, null)
|
||||||
|
val savedRefresh = prefs.getString(KEY_REFRESH_TOKEN, null)
|
||||||
|
Log.d(TAG, "saveTokens: Verification - accessToken saved=${savedAccess != null}, refreshToken saved=${savedRefresh != null}")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "saveTokens: FAILED to save tokens!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null)
|
fun getAccessToken(): String? {
|
||||||
|
val token = prefs.getString(KEY_ACCESS_TOKEN, null)
|
||||||
|
Log.d(TAG, "getAccessToken: token=${token != null}, length=${token?.length ?: 0}")
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
fun getRefreshToken(): String? = prefs.getString(KEY_REFRESH_TOKEN, null)
|
fun getRefreshToken(): String? {
|
||||||
|
val token = prefs.getString(KEY_REFRESH_TOKEN, null)
|
||||||
|
Log.d(TAG, "getRefreshToken: token=${token != null}, length=${token?.length ?: 0}")
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
fun clearTokens() {
|
fun clearTokens() {
|
||||||
|
Log.d(TAG, "clearTokens: Clearing all tokens")
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove(KEY_ACCESS_TOKEN)
|
.remove(KEY_ACCESS_TOKEN)
|
||||||
.remove(KEY_REFRESH_TOKEN)
|
.remove(KEY_REFRESH_TOKEN)
|
||||||
.apply()
|
.apply()
|
||||||
|
Log.d(TAG, "clearTokens: Tokens cleared")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ data class DeviceInfo(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VerifyOtpRequest(
|
data class VerifyOtpRequest(
|
||||||
@SerialName("phone_number") val phoneNumber: String,
|
@SerialName("phone_number") val phoneNumber: String,
|
||||||
val code: String,
|
val code: Int,
|
||||||
@SerialName("device_id") val deviceId: String,
|
@SerialName("device_id") val deviceId: String,
|
||||||
@SerialName("device_info") val deviceInfo: DeviceInfo? = null
|
@SerialName("device_info") val deviceInfo: DeviceInfo? = null
|
||||||
)
|
)
|
||||||
|
|
@ -37,6 +37,34 @@ data class VerifyOtpResponse(
|
||||||
)
|
)
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region: Signup
|
||||||
|
@Serializable
|
||||||
|
data class SignupRequest(
|
||||||
|
val name: String,
|
||||||
|
@SerialName("phone_number") val phoneNumber: String,
|
||||||
|
val state: String? = null,
|
||||||
|
val district: String? = null,
|
||||||
|
@SerialName("city_village") val cityVillage: String? = null,
|
||||||
|
@SerialName("device_id") val deviceId: String? = null,
|
||||||
|
@SerialName("device_info") val deviceInfo: DeviceInfo? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SignupResponse(
|
||||||
|
val success: Boolean,
|
||||||
|
val user: User? = null,
|
||||||
|
@SerialName("access_token") val accessToken: String? = null,
|
||||||
|
@SerialName("refresh_token") val refreshToken: String? = null,
|
||||||
|
@SerialName("needs_profile") val needsProfile: Boolean? = null,
|
||||||
|
@SerialName("is_new_account") val isNewAccount: Boolean? = null,
|
||||||
|
@SerialName("is_new_device") val isNewDevice: Boolean? = null,
|
||||||
|
@SerialName("active_devices_count") val activeDevicesCount: Int? = null,
|
||||||
|
@SerialName("location_id") val locationId: String? = null,
|
||||||
|
val message: String? = null,
|
||||||
|
@SerialName("user_exists") val userExists: Boolean? = null
|
||||||
|
)
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region: Token Refresh
|
// region: Token Refresh
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RefreshRequest(@SerialName("refresh_token") val refreshToken: String)
|
data class RefreshRequest(@SerialName("refresh_token") val refreshToken: String)
|
||||||
|
|
@ -60,15 +88,17 @@ data class User(
|
||||||
val id: String,
|
val id: String,
|
||||||
@SerialName("phone_number") val phoneNumber: String,
|
@SerialName("phone_number") val phoneNumber: String,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val role: String,
|
val role: String? = null, // Optional field - can be missing from JSON, defaults to null
|
||||||
@SerialName("user_type") val userType: String?
|
@SerialName("user_type") val userType: String? = null, // Optional field - can be missing from JSON, defaults to null
|
||||||
|
@SerialName("created_at") val createdAt: String? = null,
|
||||||
|
@SerialName("country_code") val countryCode: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Location(
|
data class Location(
|
||||||
val city: String?,
|
@SerialName("city_village") val cityVillage: String? = null,
|
||||||
val state: String?,
|
val state: String? = null,
|
||||||
val pincode: String?
|
val pincode: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -88,3 +118,25 @@ data class UserDetails(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LogoutResponse(val ok: Boolean)
|
data class LogoutResponse(val ok: Boolean)
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region: Error Responses
|
||||||
|
@Serializable
|
||||||
|
data class ErrorResponse(
|
||||||
|
val success: Boolean? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
val message: String? = null,
|
||||||
|
@SerialName("user_exists") val userExists: Boolean? = null
|
||||||
|
)
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: User Check
|
||||||
|
@Serializable
|
||||||
|
data class CheckUserRequest(@SerialName("phone_number") val phoneNumber: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CheckUserResponse(
|
||||||
|
val success: Boolean,
|
||||||
|
val message: String? = null,
|
||||||
|
@SerialName("user_exists") val userExists: Boolean
|
||||||
|
)
|
||||||
|
// endregion
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import androidx.lifecycle.viewModelScope
|
||||||
import com.example.livingai_lg.api.AuthApiClient
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
import com.example.livingai_lg.api.TokenManager
|
import com.example.livingai_lg.api.TokenManager
|
||||||
import com.example.livingai_lg.api.UserDetails
|
import com.example.livingai_lg.api.UserDetails
|
||||||
|
import com.example.livingai_lg.ui.navigation.NavEvent
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -37,30 +40,293 @@ class MainViewModel(context: Context) : ViewModel() {
|
||||||
val userState = _userState.asStateFlow()
|
val userState = _userState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
checkAuthStatus()
|
// Immediately check if tokens exist (synchronous check)
|
||||||
|
val accessToken = tokenManager.getAccessToken()
|
||||||
|
val refreshToken = tokenManager.getRefreshToken()
|
||||||
|
val hasTokens = accessToken != null && refreshToken != null
|
||||||
|
|
||||||
|
Log.d(TAG, "MainViewModel.init: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}, hasTokens=$hasTokens")
|
||||||
|
|
||||||
|
if (hasTokens) {
|
||||||
|
// Tokens exist - optimistically set to Authenticated for immediate navigation
|
||||||
|
// Then validate in background (this prevents redirect to landing page on app restart)
|
||||||
|
Log.d(TAG, "MainViewModel.init: Tokens found, setting authState to Authenticated (optimistic)")
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
|
// Validate tokens in background (this will only revert if there's a clear auth failure)
|
||||||
|
checkAuthStatus()
|
||||||
|
} else {
|
||||||
|
// No tokens, immediately set to unauthenticated
|
||||||
|
Log.d(TAG, "MainViewModel.init: No tokens found, setting authState to Unauthenticated")
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public method to refresh auth status after login/signup
|
||||||
|
* Call this after tokens are saved to update the auth state
|
||||||
|
*
|
||||||
|
* This method optimistically sets authState to Authenticated if tokens exist,
|
||||||
|
* then validates in the background. This ensures immediate navigation.
|
||||||
|
*
|
||||||
|
* IMPORTANT: After optimistic authentication, we only revert to Unauthenticated
|
||||||
|
* if there's a clear authentication failure (not network errors).
|
||||||
|
*/
|
||||||
|
fun refreshAuthStatus() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val accessToken = tokenManager.getAccessToken()
|
||||||
|
val refreshToken = tokenManager.getRefreshToken()
|
||||||
|
|
||||||
|
Log.d(TAG, "refreshAuthStatus: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}")
|
||||||
|
|
||||||
|
if (accessToken != null && refreshToken != null) {
|
||||||
|
// Optimistically set to Authenticated for immediate navigation
|
||||||
|
// Then validate in background
|
||||||
|
Log.d(TAG, "Setting authState to Authenticated (optimistic)")
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
|
// Validate tokens in background (this will only revert if there's a clear auth failure)
|
||||||
|
validateTokensOptimistic()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "No tokens found, setting authState to Unauthenticated")
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates tokens after optimistic authentication.
|
||||||
|
* Only reverts authState if there's a clear authentication failure (not network errors).
|
||||||
|
* This prevents users from being logged out due to temporary network issues.
|
||||||
|
*/
|
||||||
|
private fun validateTokensOptimistic() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
Log.d(TAG, "validateTokensOptimistic: Starting token validation")
|
||||||
|
// Try to fetch user details first - Ktor's Auth plugin will auto-refresh if access token is expired
|
||||||
|
authApiClient.getUserDetails()
|
||||||
|
.onSuccess { userDetails ->
|
||||||
|
// Tokens are valid, user is authenticated
|
||||||
|
Log.d(TAG, "validateTokensOptimistic: Token validation successful - user authenticated, userId=${userDetails.id}")
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
|
_userState.value = UserState.Success(userDetails)
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
Log.w(TAG, "validateTokensOptimistic: getUserDetails failed - ${error.message}")
|
||||||
|
// Check if this is a network error or authentication error
|
||||||
|
val isNetworkError = error.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("timeout", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("network", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("connection", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("SocketTimeoutException", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("ConnectException", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("UnknownHostException", ignoreCase = true) == true
|
||||||
|
|
||||||
|
Log.d(TAG, "validateTokensOptimistic: isNetworkError=$isNetworkError")
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
// Network error - keep optimistic authentication state
|
||||||
|
// User might be offline, tokens are still valid
|
||||||
|
Log.w(TAG, "validateTokensOptimistic: Network error detected (keeping optimistic auth): ${error.message}")
|
||||||
|
_userState.value = UserState.Error("Network error. Please check your connection.")
|
||||||
|
// Keep authState as Authenticated - don't revert on network errors
|
||||||
|
Log.d(TAG, "validateTokensOptimistic: Keeping authState as Authenticated despite network error")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a clear authentication error (401, 403, or specific auth error messages)
|
||||||
|
val isAuthError = error.message?.contains("401", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("403", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("Unauthorized", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("Forbidden", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("Invalid token", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("expired token", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("Invalid refresh token", ignoreCase = true) == true
|
||||||
|
|
||||||
|
if (isAuthError) {
|
||||||
|
// Clear authentication error - try refresh as last resort
|
||||||
|
Log.d(TAG, "Authentication error detected, attempting token refresh: ${error.message}")
|
||||||
|
|
||||||
|
authApiClient.refreshToken()
|
||||||
|
.onSuccess { refreshResponse ->
|
||||||
|
// Refresh successful, now try fetching user details again
|
||||||
|
Log.d(TAG, "Token refresh successful, fetching user details")
|
||||||
|
authApiClient.getUserDetails()
|
||||||
|
.onSuccess { userDetails ->
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
|
_userState.value = UserState.Success(userDetails)
|
||||||
|
}
|
||||||
|
.onFailure { fetchError ->
|
||||||
|
// Even after refresh, fetching failed - check if network error
|
||||||
|
val isFetchNetworkError = fetchError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
|
|| fetchError.message?.contains("timeout", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("network", ignoreCase = true) == true
|
||||||
|
|
||||||
|
if (isFetchNetworkError) {
|
||||||
|
// Network error - keep optimistic auth
|
||||||
|
Log.w(TAG, "Network error after refresh (keeping optimistic auth): ${fetchError.message}")
|
||||||
|
_userState.value = UserState.Error("Network error. Please check your connection.")
|
||||||
|
// Keep authState as Authenticated
|
||||||
|
} else {
|
||||||
|
// Clear auth failure - revert to unauthenticated
|
||||||
|
Log.e(TAG, "Authentication failed after refresh: ${fetchError.message}")
|
||||||
|
tokenManager.clearTokens()
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
_userState.value = UserState.Error("Session expired. Please sign in again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { refreshError ->
|
||||||
|
// Check if refresh failed due to network or auth
|
||||||
|
val isRefreshNetworkError = refreshError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
|
|| refreshError.message?.contains("timeout", ignoreCase = true) == true
|
||||||
|
|| refreshError.message?.contains("network", ignoreCase = true) == true
|
||||||
|
|
||||||
|
if (isRefreshNetworkError) {
|
||||||
|
// Network error - keep optimistic auth
|
||||||
|
Log.w(TAG, "Network error during refresh (keeping optimistic auth): ${refreshError.message}")
|
||||||
|
_userState.value = UserState.Error("Network error. Please check your connection.")
|
||||||
|
// Keep authState as Authenticated
|
||||||
|
} else {
|
||||||
|
// Clear auth failure - revert to unauthenticated
|
||||||
|
Log.e(TAG, "Token refresh failed (reverting auth): ${refreshError.message}")
|
||||||
|
tokenManager.clearTokens()
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
_userState.value = UserState.Error("Session expired. Please sign in again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown error - be conservative and keep optimistic auth
|
||||||
|
// Don't log out user for unknown errors
|
||||||
|
Log.w(TAG, "Unknown error during validation (keeping optimistic auth): ${error.message}")
|
||||||
|
_userState.value = UserState.Error("Validation error: ${error.message}")
|
||||||
|
// Keep authState as Authenticated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAuthStatus() {
|
private fun checkAuthStatus() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tokenManager.getAccessToken() != null) {
|
val accessToken = tokenManager.getAccessToken()
|
||||||
_authState.value = AuthState.Authenticated
|
val refreshToken = tokenManager.getRefreshToken()
|
||||||
fetchUserDetails()
|
|
||||||
|
Log.d(TAG, "checkAuthStatus: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}")
|
||||||
|
|
||||||
|
if (accessToken != null && refreshToken != null) {
|
||||||
|
// Tokens exist, validate them using optimistic validation
|
||||||
|
// This keeps authState as Authenticated unless there's a clear auth failure
|
||||||
|
Log.d(TAG, "checkAuthStatus: Validating tokens optimistically")
|
||||||
|
validateTokensOptimistic()
|
||||||
} else {
|
} else {
|
||||||
|
// No tokens, user is not authenticated
|
||||||
|
Log.d(TAG, "checkAuthStatus: No tokens found, setting authState to Unauthenticated")
|
||||||
_authState.value = AuthState.Unauthenticated
|
_authState.value = AuthState.Unauthenticated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateTokens() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Try to fetch user details first - Ktor's Auth plugin will auto-refresh if access token is expired
|
||||||
|
authApiClient.getUserDetails()
|
||||||
|
.onSuccess { userDetails ->
|
||||||
|
// Tokens are valid, user is authenticated
|
||||||
|
Log.d(TAG, "User authenticated successfully")
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
|
_userState.value = UserState.Success(userDetails)
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
// Check if this is a network error or authentication error
|
||||||
|
val isNetworkError = error.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("timeout", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("network", ignoreCase = true) == true
|
||||||
|
|| error.message?.contains("connection", ignoreCase = true) == true
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
// Network error - don't clear tokens, don't change auth state
|
||||||
|
// User might be offline, tokens are still valid
|
||||||
|
// Keep current auth state (Unknown) so navigation doesn't change
|
||||||
|
Log.w(TAG, "Network error during token validation: ${error.message}")
|
||||||
|
_userState.value = UserState.Error("Network error. Please check your connection.")
|
||||||
|
// Don't change auth state - keep it as Unknown so we can retry later
|
||||||
|
// Don't clear tokens - they might still be valid
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// If fetching user details failed, try manual refresh
|
||||||
|
// This handles cases where Ktor's auto-refresh might not have worked
|
||||||
|
Log.d(TAG, "Failed to fetch user details (${error.message}), attempting manual token refresh")
|
||||||
|
|
||||||
|
// Try manual refresh as fallback
|
||||||
|
authApiClient.refreshToken()
|
||||||
|
.onSuccess { refreshResponse ->
|
||||||
|
// Refresh successful, now try fetching user details again
|
||||||
|
Log.d(TAG, "Token refresh successful, fetching user details")
|
||||||
|
authApiClient.getUserDetails()
|
||||||
|
.onSuccess { userDetails ->
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
|
_userState.value = UserState.Success(userDetails)
|
||||||
|
}
|
||||||
|
.onFailure { fetchError ->
|
||||||
|
// Check if this is also a network error
|
||||||
|
val isFetchNetworkError = fetchError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
|
|| fetchError.message?.contains("timeout", ignoreCase = true) == true
|
||||||
|
|| fetchError.message?.contains("network", ignoreCase = true) == true
|
||||||
|
|
||||||
|
if (isFetchNetworkError) {
|
||||||
|
// Network error - don't clear tokens, keep auth state
|
||||||
|
Log.w(TAG, "Network error after refresh: ${fetchError.message}")
|
||||||
|
_userState.value = UserState.Error("Network error. Please check your connection.")
|
||||||
|
// Don't change auth state - keep it as Unknown
|
||||||
|
} else {
|
||||||
|
// Even after refresh, fetching user details failed - likely auth error
|
||||||
|
Log.e(TAG, "Failed to fetch user details after refresh: ${fetchError.message}")
|
||||||
|
tokenManager.clearTokens()
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
_userState.value = UserState.Error("Session expired. Please sign in again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { refreshError ->
|
||||||
|
// Check if refresh failed due to network or auth
|
||||||
|
val isRefreshNetworkError = refreshError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
|
|| refreshError.message?.contains("timeout", ignoreCase = true) == true
|
||||||
|
|| refreshError.message?.contains("network", ignoreCase = true) == true
|
||||||
|
|
||||||
|
if (isRefreshNetworkError) {
|
||||||
|
// Network error - don't clear tokens, keep auth state
|
||||||
|
Log.w(TAG, "Network error during token refresh: ${refreshError.message}")
|
||||||
|
_userState.value = UserState.Error("Network error. Please check your connection.")
|
||||||
|
// Don't change auth state - keep it as Unknown
|
||||||
|
} else {
|
||||||
|
// Refresh failed - tokens are invalid or expired (auth error)
|
||||||
|
Log.d(TAG, "Token refresh failed (auth error): ${refreshError.message}")
|
||||||
|
tokenManager.clearTokens()
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
_userState.value = UserState.Error("Session expired. Please sign in again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is no longer needed as validateTokens() now handles refresh directly
|
||||||
|
// Keeping it for backward compatibility but it's not used
|
||||||
|
@Deprecated("Use validateTokens() instead")
|
||||||
|
private fun attemptTokenRefresh() {
|
||||||
|
validateTokens()
|
||||||
|
}
|
||||||
|
|
||||||
fun fetchUserDetails() {
|
fun fetchUserDetails() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_userState.value = UserState.Loading
|
_userState.value = UserState.Loading
|
||||||
authApiClient.getUserDetails()
|
authApiClient.getUserDetails()
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
_userState.value = UserState.Success(it)
|
_userState.value = UserState.Success(it)
|
||||||
|
_authState.value = AuthState.Authenticated
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
_userState.value = UserState.Error(it.message ?: "Unknown error")
|
_userState.value = UserState.Error(it.message ?: "Unknown error")
|
||||||
_authState.value = AuthState.Unauthenticated
|
// Don't automatically set to Unauthenticated here - let the caller decide
|
||||||
|
// or try refresh if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,4 +346,14 @@ class MainViewModel(context: Context) : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used in navigation logic.
|
||||||
|
private val _navEvents = MutableSharedFlow<NavEvent>()
|
||||||
|
val navEvents = _navEvents.asSharedFlow()
|
||||||
|
|
||||||
|
fun emitNavEvent(event: NavEvent) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_navEvents.emit(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionPopup(
|
||||||
|
visible: Boolean,
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color = Color.Black,
|
||||||
|
contentColor: Color = Color.White,
|
||||||
|
autoDismissMillis: Long = 5000L,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
// Auto dismiss
|
||||||
|
LaunchedEffect(visible) {
|
||||||
|
if (visible) {
|
||||||
|
delay(autoDismissMillis)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn() + slideInVertically { it / 2 },
|
||||||
|
exit = fadeOut() + slideOutVertically { it / 2 },
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.shadow(12.dp, RoundedCornerShape(50))
|
||||||
|
.background(backgroundColor, RoundedCornerShape(50))
|
||||||
|
.clickable(
|
||||||
|
enabled = onClick != null,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onClick?.invoke()
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = contentColor,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = contentColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Close",
|
||||||
|
tint = contentColor.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AdSpaceBanner(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String = "AD SPACE"
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(44.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFFFF0000).copy(alpha = 0.97f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.White, RoundedCornerShape(8.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.LibraryAdd
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.UserAddress
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddressSelectorOverlay(
|
||||||
|
visible: Boolean,
|
||||||
|
addresses: List<UserAddress>,
|
||||||
|
selectedAddressId: String?,
|
||||||
|
onSelect: (String) -> Unit,
|
||||||
|
onClose: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { -it }
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { -it }
|
||||||
|
) + fadeOut()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF4F4F4))
|
||||||
|
) {
|
||||||
|
|
||||||
|
Column {
|
||||||
|
AddressSelectorHeader(onClose)
|
||||||
|
AddressSearchBar()
|
||||||
|
AddressActionsRow()
|
||||||
|
SavedAddressesList(
|
||||||
|
addresses = addresses,
|
||||||
|
selectedAddressId = selectedAddressId,
|
||||||
|
onSelect = onSelect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddressSelectorHeader(onClose: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "Select Your Location",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddressSearchBar() {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = "",
|
||||||
|
onValueChange = {},
|
||||||
|
placeholder = { Text("Search an area or address") },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(Icons.Default.Search, contentDescription = null)
|
||||||
|
},
|
||||||
|
enabled = false // 👈 explicitly disabled for now
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddressActionsRow() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
AddressActionCard(Icons.Default.MyLocation,"Use Current Location")
|
||||||
|
AddressActionCard(Icons.Default.LibraryAdd,"Add New Address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SavedAddressesList(
|
||||||
|
addresses: List<UserAddress>,
|
||||||
|
selectedAddressId: String?,
|
||||||
|
onSelect: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "SAVED ADDRESSES",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
addresses.forEach { address ->
|
||||||
|
AddressItem(
|
||||||
|
address = address,
|
||||||
|
selected = address.id == selectedAddressId,
|
||||||
|
onClick = { onSelect(address.id) }
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddressItem(
|
||||||
|
address: UserAddress,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.White, RoundedCornerShape(12.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Home, contentDescription = null)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(address.name, fontWeight = FontWeight.Medium)
|
||||||
|
if (selected) {
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
"SELECTED",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color.Green
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(address.address, fontSize = 12.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddressActionCard(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.size(110.dp)
|
||||||
|
.background(Color.White, RoundedCornerShape(16.dp))
|
||||||
|
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = label,
|
||||||
|
tint = Color.Black,
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.AnimalType
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimalTypeSelector(
|
||||||
|
animalTypes: List<AnimalType>,
|
||||||
|
selectedAnimalType: String?,
|
||||||
|
onAnimalTypeSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val selectedAnimalType: String = selectedAnimalType ?: ""
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(animalTypes.size) { index ->
|
||||||
|
AnimalTypeButton(
|
||||||
|
animalType = animalTypes[index],
|
||||||
|
isSelected = selectedAnimalType == animalTypes[index].id,
|
||||||
|
onClick = { onAnimalTypeSelected(animalTypes[index].id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AnimalTypeButton(
|
||||||
|
animalType: AnimalType,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(
|
||||||
|
if (isSelected) Color(0xFFEDE9FE) else Color.White,
|
||||||
|
RoundedCornerShape(24.dp)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = animalType.emoji,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = animalType.name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.BottomNavItemData
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomNavigationBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
items: List<BottomNavItemData>,
|
||||||
|
currentItem: String,
|
||||||
|
onItemClick: (route: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.White)
|
||||||
|
.border(1.dp, Color(0xFF000000).copy(alpha = 0.1f))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(66.dp)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
items.forEach { item ->
|
||||||
|
val isSelected = item.label == currentItem
|
||||||
|
BottomNavItem(
|
||||||
|
label = item.label,
|
||||||
|
iconRes = item.iconRes,
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = {
|
||||||
|
if (!isSelected) {
|
||||||
|
onItemClick(item.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomNavItem(
|
||||||
|
label: String,
|
||||||
|
iconRes: Any,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val color = if (selected) Color(0xFF1E88E5) else Color(0xFF0A0A0A)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick)
|
||||||
|
) {
|
||||||
|
if(iconRes is Int) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(iconRes),
|
||||||
|
contentDescription = label,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else if(iconRes is ImageVector) {
|
||||||
|
Icon(
|
||||||
|
imageVector = iconRes,
|
||||||
|
contentDescription = label,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material.icons.outlined.LocationOn
|
||||||
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import com.example.livingai_lg.ui.screens.*
|
||||||
|
import com.example.livingai_lg.ui.models.Animal
|
||||||
|
import com.example.livingai_lg.ui.utils.formatDistance
|
||||||
|
import com.example.livingai_lg.ui.utils.formatPrice
|
||||||
|
import com.example.livingai_lg.ui.utils.formatViews
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BuyAnimalCard(
|
||||||
|
product: Animal,
|
||||||
|
isSaved: Boolean,
|
||||||
|
onSavedChange: (Boolean) -> Unit,
|
||||||
|
onProductClick: () -> Unit,
|
||||||
|
onSellerClick:(sellerId: String)-> Unit,
|
||||||
|
onBookmarkClick: () -> Unit,
|
||||||
|
onInfoClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.shadow(1.078.dp, RoundedCornerShape(14.dp))
|
||||||
|
.background(Color.White, RoundedCornerShape(14.dp))
|
||||||
|
.border(
|
||||||
|
1.078.dp,
|
||||||
|
Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onProductClick
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Image
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(257.dp)
|
||||||
|
) {
|
||||||
|
ImageCarousel(
|
||||||
|
media = product.media ?: emptyList(),
|
||||||
|
enableFullscreenPreview = false,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Views
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(6.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.35f), // 👈 light but effective
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Visibility,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(16.dp).shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = CircleShape,
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text(formatViews(product.views), fontSize = AppTypography.Caption, color = Color.White,style = LocalTextStyle.current.copy(
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(6.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.35f), // 👈 light but effective
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.LocationOn,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(12.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
formatDistance(product.distance),
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
color = Color.White,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
product.breed ?: "",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.padding(horizontal = 4.dp).size(16.dp).align(Alignment.CenterVertically).clickable{onInfoClick()},
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
//InfoIconWithOverlay(infoText = product.breedInfo ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
formatPrice(product.price),
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End, modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onSellerClick(product.sellerId ?: "")
|
||||||
|
}) {
|
||||||
|
Text("Sold By: ${product.sellerName}", fontSize = AppTypography.BodySmall)
|
||||||
|
Text(product.sellerType ?: "???", fontSize = AppTypography.BodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rating
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
RatingStars(product.rating?:0f, starSize = 12.dp)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
"${product.rating} (${product.ratingCount} Ratings)",
|
||||||
|
fontSize = AppTypography.Caption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badges
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Text("AI Score: ", fontSize = AppTypography.Caption)
|
||||||
|
val scoreString = "${product.aiScore ?: 0}"
|
||||||
|
Text(scoreString, fontSize = AppTypography.Caption, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product.milkCapacity != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
"Milk Capacity:",
|
||||||
|
fontSize = AppTypography.Caption
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${product.milkCapacity}L",
|
||||||
|
fontSize = AppTypography.Caption,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
Text(
|
||||||
|
product.description ?: "",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
// Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
// ActionButton(R.drawable.ic_chat, "Chat")
|
||||||
|
// ActionButton(R.drawable.ic_phone, "Call")
|
||||||
|
// ActionButton(R.drawable.ic_location, "Location")
|
||||||
|
// ActionButton(R.drawable.ic_bookmark_plus, "Bookmark")
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FloatingActionBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.zIndex(10f), // 👈 ensure it floats above everything
|
||||||
|
showContainer = false,
|
||||||
|
onChatClick = { /* TODO */ },
|
||||||
|
onCallClick = { /* TODO */ },
|
||||||
|
onLocationClick = { /* TODO */ },
|
||||||
|
onBookmarkClick = onBookmarkClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionButton(icon: Int, label: String) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = label,
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DropdownInput(
|
||||||
|
label: String? = null, // optional label
|
||||||
|
selected: String,
|
||||||
|
options: List<String>,
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
onSelect: (String) -> Unit,
|
||||||
|
placeholder: String = "Select", // NEW - custom placeholder
|
||||||
|
textColor: Color = Color.Black,
|
||||||
|
modifier: Modifier = Modifier // NEW - allows width control
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier, // <-- now caller can control width
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// Optional label
|
||||||
|
// if (label != null) {
|
||||||
|
Text(
|
||||||
|
text = label?:" ",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = FarmTextDark
|
||||||
|
)
|
||||||
|
// } else {
|
||||||
|
// // Reserve label space so layout doesn’t shift
|
||||||
|
// Spacer(modifier = Modifier.height(20.dp)) // ← same height as label text line
|
||||||
|
// }
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { onExpandedChange(!expanded) }
|
||||||
|
) {
|
||||||
|
// Anchor box
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.White, RoundedCornerShape(16.dp))
|
||||||
|
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onExpandedChange(true)
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Custom placeholder support
|
||||||
|
Text(
|
||||||
|
text = selected.ifEmpty { placeholder },
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
color = if (selected.isEmpty()) Color(0xFF99A1AF) else textColor
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = "Dropdown",
|
||||||
|
tint = FarmTextDark
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material3 Dropdown
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { onExpandedChange(false) },
|
||||||
|
modifier = Modifier.background(Color.White)
|
||||||
|
) {
|
||||||
|
options.forEach { item ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(item, fontSize = AppTypography.Body, color = FarmTextDark)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onSelect(item)
|
||||||
|
onExpandedChange(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FarmHeader() {
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = "Farm",
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFFE17100)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Market",
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = FarmTextDark
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Find your perfect livestock",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = FarmTextNormal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilterButton(
|
||||||
|
hasActiveFilters: Boolean = false,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val backgroundColor =
|
||||||
|
if (hasActiveFilters) Color(0xFF0A0A0A) else Color.White
|
||||||
|
|
||||||
|
val contentColor =
|
||||||
|
if (hasActiveFilters) Color.White else Color(0xFF0A0A0A)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(36.dp)
|
||||||
|
.border(
|
||||||
|
1.078.dp,
|
||||||
|
if (hasActiveFilters) Color.Transparent
|
||||||
|
else Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(backgroundColor, RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Icon + dot indicator
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_filter),
|
||||||
|
contentDescription = "Filter",
|
||||||
|
tint = contentColor,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasActiveFilters) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 2.dp)
|
||||||
|
.size(6.dp)
|
||||||
|
.background(Color.Red, RoundedCornerShape(50))
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Filter",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
|
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilterOverlay(
|
||||||
|
visible: Boolean,
|
||||||
|
appliedFilters: FiltersState,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSubmitClick: (filters: FiltersState) -> Unit = {},
|
||||||
|
) {
|
||||||
|
BackHandler(enabled = visible) { onDismiss() }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// Dimmed background
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.35f))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onDismiss() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sliding panel
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInHorizontally(
|
||||||
|
initialOffsetX = { it } // from right
|
||||||
|
),
|
||||||
|
exit = slideOutHorizontally(
|
||||||
|
targetOffsetX = { it } // to right
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
) {
|
||||||
|
FilterScreen(
|
||||||
|
appliedFilters,
|
||||||
|
onBackClick = onDismiss,
|
||||||
|
onCancelClick = onDismiss,
|
||||||
|
onSubmitClick = { filters ->
|
||||||
|
onSubmitClick(filters)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import android.media.Image
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Chat
|
||||||
|
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||||
|
import androidx.compose.material.icons.outlined.Chat
|
||||||
|
import androidx.compose.material.icons.outlined.LocationOn
|
||||||
|
import androidx.compose.material.icons.outlined.Phone
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FloatingActionBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showContainer: Boolean = true,
|
||||||
|
onChatClick: () -> Unit = {},
|
||||||
|
onCallClick: () -> Unit = {},
|
||||||
|
onLocationClick: () -> Unit = {},
|
||||||
|
onBookmarkClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val containerModifier =
|
||||||
|
if (showContainer) {
|
||||||
|
Modifier
|
||||||
|
.shadow(
|
||||||
|
elevation = 12.dp,
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.White,
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
} else {
|
||||||
|
Modifier // no background, no shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
//.padding(horizontal = 24.dp)
|
||||||
|
.then(containerModifier)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
FloatingActionIcon(Icons.AutoMirrored.Outlined.Chat, onChatClick)
|
||||||
|
FloatingActionIcon(Icons.Outlined.Phone, onCallClick)
|
||||||
|
FloatingActionIcon(Icons.Outlined.LocationOn, onLocationClick)
|
||||||
|
FloatingActionIcon(Icons.Outlined.BookmarkAdd, onBookmarkClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FloatingActionIcon(
|
||||||
|
icon: ImageVector,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
clip = false
|
||||||
|
)
|
||||||
|
.background(Color.White, RoundedCornerShape(24.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconCircle(
|
||||||
|
backgroundColor: Color,
|
||||||
|
size: Dp,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.background(backgroundColor, shape = CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.MediaItem
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ImageCarousel(
|
||||||
|
media: List<MediaItem>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enableFullscreenPreview: Boolean = false,
|
||||||
|
onMediaClick: (startIndex: Int) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState { media.size }
|
||||||
|
var startIndex by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
when {
|
||||||
|
media.isEmpty() -> {
|
||||||
|
NoImagePlaceholder(modifier = modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
media.size == 1 -> {
|
||||||
|
when (val item = media.first()) {
|
||||||
|
is MediaItem.Image -> {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier
|
||||||
|
.clickable(enabled = enableFullscreenPreview) {
|
||||||
|
onMediaClick(0)
|
||||||
|
},
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaItem.Video -> {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clickable(enabled = enableFullscreenPreview) {
|
||||||
|
onMediaClick(0)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.thumbnailUrl ?: item.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Play video",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.background(
|
||||||
|
Color.Black.copy(alpha = 0.5f),
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { page ->
|
||||||
|
when (val item = media[page]) {
|
||||||
|
is MediaItem.Image -> {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(enabled = enableFullscreenPreview) {
|
||||||
|
onMediaClick(page)
|
||||||
|
},
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaItem.Video -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(enabled = enableFullscreenPreview) {
|
||||||
|
onMediaClick(page)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.thumbnailUrl ?: item.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
// Play icon overlay
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Play video",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.background(
|
||||||
|
Color.Black.copy(alpha = 0.5f),
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page Indicator (inside image)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
repeat(pagerState.pageCount) { index ->
|
||||||
|
val isSelected = pagerState.currentPage == index
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.width(if (isSelected) 18.dp else 6.dp)
|
||||||
|
.background(
|
||||||
|
Color.White,
|
||||||
|
RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PageIndicator(
|
||||||
|
pageCount: Int,
|
||||||
|
currentPage: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.3f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
repeat(pageCount) { index ->
|
||||||
|
if (index == currentPage) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(18.dp)
|
||||||
|
.height(6.dp)
|
||||||
|
.background(Color.White, RoundedCornerShape(3.dp))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(6.dp)
|
||||||
|
.background(Color.White.copy(alpha = 0.6f), CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NoImagePlaceholder(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.LightGray),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No Images",
|
||||||
|
color = Color.DarkGray,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import com.example.livingai_lg.ui.screens.align
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoOverlay(
|
||||||
|
visible: Boolean,
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
BackHandler(enabled = visible) { onDismiss() }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// Dimmed background
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.35f))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onDismiss() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sliding content
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInVertically { it }, // from bottom
|
||||||
|
exit = slideOutVertically { it },
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
InfoOverlayContent(
|
||||||
|
title = title,
|
||||||
|
text = text,
|
||||||
|
onDismiss = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoOverlayContent(
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
Color.White,
|
||||||
|
RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||||
|
)
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = AppTypography.Title,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Close",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clickable { onDismiss() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
color = Color(0xFF4A5565),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.TooltipBox
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TooltipDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoTooltipOverlay(
|
||||||
|
text: String,
|
||||||
|
visible: Boolean,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
if (!visible) return
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(100f) // ensure it overlays everything
|
||||||
|
) {
|
||||||
|
// Dimmed background
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.35f))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tooltip card
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.background(Color.Black, RoundedCornerShape(12.dp))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
lineHeight = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoIconWithOverlay(
|
||||||
|
infoText: String
|
||||||
|
) {
|
||||||
|
var showInfo by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
contentDescription = "Info",
|
||||||
|
tint = Color.Gray,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.clickable { showInfo = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
InfoTooltipOverlay(
|
||||||
|
text = infoText,
|
||||||
|
visible = showInfo,
|
||||||
|
onDismiss = { showInfo = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InputField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
placeholder: String,
|
||||||
|
onChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = FarmTextDark
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(52.dp)
|
||||||
|
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onChange,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = FarmTextDark
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = placeholder,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color(0xFF99A1AF)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inner()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||||
|
import androidx.compose.foundation.gestures.transformable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.example.livingai_lg.ui.models.MediaItem
|
||||||
|
import androidx.media3.common.MediaItem as ExoMediaItem
|
||||||
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MediaFullscreenOverlay(
|
||||||
|
media: List<MediaItem>,
|
||||||
|
startIndex: Int,
|
||||||
|
onClose: () -> Unit
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = startIndex,
|
||||||
|
pageCount = { media.size }
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(0.85f))
|
||||||
|
.zIndex(100f)
|
||||||
|
) {
|
||||||
|
|
||||||
|
var isZoomed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
userScrollEnabled = !isZoomed
|
||||||
|
) { page ->
|
||||||
|
when (val item = media[page]) {
|
||||||
|
is MediaItem.Image -> {
|
||||||
|
ZoomableImage(
|
||||||
|
url = item.url,
|
||||||
|
onZoomChanged = { isZoomed = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MediaItem.Video -> {
|
||||||
|
VideoPlayer(item.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page Indicator (inside image)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 36.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
repeat(pagerState.pageCount) { index ->
|
||||||
|
val isSelected = pagerState.currentPage == index
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.width(if (isSelected) 18.dp else 6.dp)
|
||||||
|
.background(
|
||||||
|
Color.White,
|
||||||
|
RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Close",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(16.dp)
|
||||||
|
.size(32.dp)
|
||||||
|
.clickable { onClose() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ZoomableImage(
|
||||||
|
url: String,
|
||||||
|
onZoomChanged: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
var scale by remember { mutableStateOf(1f) }
|
||||||
|
var offsetX by remember { mutableStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
val transformState = rememberTransformableState { zoomChange, panChange, _ ->
|
||||||
|
val newScale = (scale * zoomChange).coerceIn(1f, 4f)
|
||||||
|
scale = newScale
|
||||||
|
|
||||||
|
if (newScale > 1f) {
|
||||||
|
offsetX += panChange.x
|
||||||
|
offsetY += panChange.y
|
||||||
|
} else {
|
||||||
|
offsetX = 0f
|
||||||
|
offsetY = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
onZoomChanged(newScale > 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
translationX = offsetX
|
||||||
|
translationY = offsetY
|
||||||
|
}
|
||||||
|
// 👇 double tap handler
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = {
|
||||||
|
if (scale > 1f) {
|
||||||
|
scale = 1f
|
||||||
|
offsetX = 0f
|
||||||
|
offsetY = 0f
|
||||||
|
onZoomChanged(false)
|
||||||
|
} else {
|
||||||
|
scale = 2.5f
|
||||||
|
onZoomChanged(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 👇 only consume gestures when zoomed
|
||||||
|
.then(
|
||||||
|
if (scale > 1f) Modifier.transformable(transformState)
|
||||||
|
else Modifier
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoPlayer(url: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val exoPlayer = remember {
|
||||||
|
ExoPlayer.Builder(context).build().apply {
|
||||||
|
setMediaItem(ExoMediaItem.fromUri(url))
|
||||||
|
prepare()
|
||||||
|
playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
exoPlayer.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
factory = {
|
||||||
|
PlayerView(context).apply {
|
||||||
|
player = exoPlayer
|
||||||
|
useController = true
|
||||||
|
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
|
setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING)
|
||||||
|
|
||||||
|
// 👇 Enable fullscreen button if available
|
||||||
|
setFullscreenButtonClickListener { isFullscreen ->
|
||||||
|
// PlayerView handles system UI automatically
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.outlined.CameraAlt
|
||||||
|
import androidx.compose.material.icons.outlined.Videocam
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.example.livingai_lg.ui.models.MediaType
|
||||||
|
import com.example.livingai_lg.ui.models.MediaUpload
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.example.livingai_lg.ui.utils.createMediaUri
|
||||||
|
import com.example.livingai_lg.ui.utils.getVideoThumbnail
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MediaPickerCard(
|
||||||
|
upload: MediaUpload,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onUriSelected: (Uri?) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var showSheet by remember { mutableStateOf(false) }
|
||||||
|
var cameraUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
var showVideoPlayer by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Gallery
|
||||||
|
val galleryLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) onUriSelected(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera (photo)
|
||||||
|
val imageCameraLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.TakePicture()
|
||||||
|
) { success ->
|
||||||
|
if (success) onUriSelected(cameraUri) else cameraUri = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera (video)
|
||||||
|
val videoCameraLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CaptureVideo()
|
||||||
|
) { success ->
|
||||||
|
if (success) onUriSelected(cameraUri) else cameraUri = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Picker Sheet (NO delete here anymore) ---------- */
|
||||||
|
|
||||||
|
if (showSheet) {
|
||||||
|
ModalBottomSheet(onDismissRequest = { showSheet = false }) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Select Media",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Camera") },
|
||||||
|
leadingContent = { Icon(Icons.Outlined.CameraAlt, null) },
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
showSheet = false
|
||||||
|
cameraUri = createMediaUri(context, upload.type)
|
||||||
|
|
||||||
|
if (upload.type == MediaType.PHOTO) {
|
||||||
|
imageCameraLauncher.launch(cameraUri!!)
|
||||||
|
} else {
|
||||||
|
videoCameraLauncher.launch(cameraUri!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Gallery") },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
if (upload.type == MediaType.PHOTO)
|
||||||
|
Icons.Outlined.CameraAlt
|
||||||
|
else
|
||||||
|
Icons.Outlined.Videocam,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
showSheet = false
|
||||||
|
galleryLauncher.launch(
|
||||||
|
if (upload.type == MediaType.PHOTO) "image/*" else "video/*"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Card ---------- */
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(204.dp)
|
||||||
|
.shadow(0.5.dp, RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.White, RoundedCornerShape(8.dp))
|
||||||
|
.border(1.dp, Color(0x1A000000), RoundedCornerShape(8.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = {
|
||||||
|
if (upload.uri != null && upload.type == MediaType.VIDEO) {
|
||||||
|
showVideoPlayer = true
|
||||||
|
} else {
|
||||||
|
showSheet = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (upload.uri != null) {
|
||||||
|
showSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(8.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
/* ---------- Media Preview ---------- */
|
||||||
|
|
||||||
|
if (upload.uri != null) {
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
||||||
|
if (upload.type == MediaType.VIDEO) {
|
||||||
|
|
||||||
|
val thumbnail = remember(upload.uri) {
|
||||||
|
getVideoThumbnail(context, upload.uri!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnail != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = thumbnail.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ▶ Play overlay
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(56.dp)
|
||||||
|
.background(
|
||||||
|
Color.Black.copy(alpha = 0.6f),
|
||||||
|
RoundedCornerShape(28.dp)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Play Video",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Photo
|
||||||
|
AsyncImage(
|
||||||
|
model = upload.uri,
|
||||||
|
contentDescription = upload.label,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- ❌ Remove Button ---------- */
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Remove media",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(6.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
.background(
|
||||||
|
Color.Black.copy(alpha = 0.6f),
|
||||||
|
RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onUriSelected(null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
/* ---------- Empty State ---------- */
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (upload.type == MediaType.PHOTO)
|
||||||
|
Icons.Outlined.CameraAlt
|
||||||
|
else
|
||||||
|
Icons.Outlined.Videocam,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(0xFF717182),
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (upload.type == MediaType.PHOTO) "Add Photo" else "Add Video",
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (upload.label.isNotEmpty()) {
|
||||||
|
Text(upload.label, color = Color(0xFF717182))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Video Player ---------- */
|
||||||
|
|
||||||
|
if (showVideoPlayer && upload.uri != null) {
|
||||||
|
VideoPlayerDialog(
|
||||||
|
uri = upload.uri!!,
|
||||||
|
onDismiss = { showVideoPlayer = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.example.livingai_lg.ui.models.AppNotification
|
||||||
|
import com.example.livingai_lg.ui.screens.NotificationsScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationsOverlay(
|
||||||
|
visible: Boolean,
|
||||||
|
notifications: List<AppNotification>,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onDismiss: (String) -> Unit,
|
||||||
|
onNotificationClick: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInVertically { -it },
|
||||||
|
exit = slideOutVertically { -it }
|
||||||
|
) {
|
||||||
|
NotificationsScreen(
|
||||||
|
notifications = notifications,
|
||||||
|
onBackClick = onClose,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
onNotificationClick = onNotificationClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OptionCard(
|
||||||
|
label: String,
|
||||||
|
icon: Any,
|
||||||
|
iconBackgroundColor: Color,
|
||||||
|
iconTint: Color = Color.Unspecified,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(90.dp)
|
||||||
|
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.White, RoundedCornerShape(16.dp))
|
||||||
|
.border(1.dp, Color(0xFFF3F4F6), RoundedCornerShape(16.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Icon container
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.background(iconBackgroundColor, RoundedCornerShape(14.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
when (icon) {
|
||||||
|
is ImageVector -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = label,
|
||||||
|
tint = iconTint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Int -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = label,
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dot indicator
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(10.dp)
|
||||||
|
.background(Color(0xFF6B7280), CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Phone
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PhoneNumberInput(
|
||||||
|
phone: String,
|
||||||
|
onChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Enter Phone Number*",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = FarmTextDark
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.height(52.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Country code box
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(65.dp)
|
||||||
|
.height(52.dp)
|
||||||
|
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text("+91", fontSize = 16.sp, color = FarmTextDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone input field
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(52.dp)
|
||||||
|
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Phone,
|
||||||
|
contentDescription = "Phone",
|
||||||
|
tint = Color(0xFF99A1AF),
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicTextField(
|
||||||
|
value = phone,
|
||||||
|
onValueChange = onChange,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number
|
||||||
|
),
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = FarmTextDark
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (phone.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
"Enter your Phone Number",
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = Color(0xFF99A1AF)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inner()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RangeFilter(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
|
||||||
|
label: String,
|
||||||
|
min: Int,
|
||||||
|
max: Int,
|
||||||
|
valueFrom: Int,
|
||||||
|
valueTo: Int,
|
||||||
|
modified: Boolean = false,
|
||||||
|
onValueChange: (from: Int, to: Int) -> Unit,
|
||||||
|
showSlider: Boolean = true,
|
||||||
|
valueFormatter: (Int) -> String = { it.toString() }
|
||||||
|
) {
|
||||||
|
var fromValue by remember(valueFrom) { mutableStateOf(valueFrom) }
|
||||||
|
var toValue by remember(valueTo) { mutableStateOf(valueTo) }
|
||||||
|
|
||||||
|
Column(modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
|
||||||
|
// Label
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color(0xFF364153)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Slider (optional)
|
||||||
|
if (showSlider) {
|
||||||
|
RangeSlider(
|
||||||
|
value = fromValue.toFloat()..toValue.toFloat(),
|
||||||
|
onValueChange = { range ->
|
||||||
|
fromValue = range.start.roundToInt()
|
||||||
|
.coerceIn(min, toValue)
|
||||||
|
toValue = range.endInclusive.roundToInt()
|
||||||
|
.coerceIn(fromValue, max)
|
||||||
|
|
||||||
|
onValueChange(fromValue, toValue)
|
||||||
|
},
|
||||||
|
valueRange = min.toFloat()..max.toFloat(),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFD9D9D9),
|
||||||
|
activeTrackColor = Color(0xFFD9D9D9),
|
||||||
|
inactiveTrackColor = Color(0xFFE5E7EB)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pills
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RangePill(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
value = fromValue,
|
||||||
|
modified = modified,
|
||||||
|
onValueChange = { newFrom ->
|
||||||
|
val safeFrom = newFrom.coerceIn(min, toValue)
|
||||||
|
fromValue = safeFrom
|
||||||
|
onValueChange(safeFrom, toValue)
|
||||||
|
},
|
||||||
|
formatter = valueFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
Text("to", fontSize = 15.sp)
|
||||||
|
|
||||||
|
RangePill(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
value = toValue,
|
||||||
|
modified = modified,
|
||||||
|
onValueChange = { newTo ->
|
||||||
|
val safeTo = newTo.coerceIn(fromValue, max)
|
||||||
|
toValue = safeTo
|
||||||
|
onValueChange(fromValue, safeTo)
|
||||||
|
},
|
||||||
|
formatter = valueFormatter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RangePill(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
value: Int,
|
||||||
|
modified: Boolean = false,
|
||||||
|
onValueChange: (Int) -> Unit,
|
||||||
|
formatter: (Int) -> String
|
||||||
|
) {
|
||||||
|
var text by remember(value) {
|
||||||
|
mutableStateOf(formatter(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(30.dp)
|
||||||
|
.background(Color.White, RoundedCornerShape(16.dp))
|
||||||
|
.border(1.dp, Color(0x12000000), RoundedCornerShape(16.dp))
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
var modified = modified;
|
||||||
|
BasicTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { input ->
|
||||||
|
val digits = input.filter { it.isDigit() }
|
||||||
|
text = digits
|
||||||
|
digits.toIntOrNull()?.let(onValueChange)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = if(modified) Color.Black else Color(0xFF99A1AF)
|
||||||
|
),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RatingStars(
|
||||||
|
rating: Float,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
starSize: Dp = 14.dp,
|
||||||
|
color: Color = Color(0xFFDE9A07)
|
||||||
|
) {
|
||||||
|
val fullStars = rating.toInt()
|
||||||
|
val remainder = rating - fullStars
|
||||||
|
|
||||||
|
val showHalfStar = remainder in 0.25f..0.74f
|
||||||
|
val extraFullStar = remainder >= 0.75f
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Full stars
|
||||||
|
repeat(
|
||||||
|
if (extraFullStar) fullStars + 1 else fullStars
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Star,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.size(starSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Half star (only if not rounded up)
|
||||||
|
if (showHalfStar) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.StarHalf,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.size(starSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortButton(
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(36.dp)
|
||||||
|
.border(
|
||||||
|
1.078.dp,
|
||||||
|
Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.White, RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_sort),
|
||||||
|
contentDescription = "Sort",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Sort by",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.filled.ArrowUpward
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.SortDirection
|
||||||
|
import com.example.livingai_lg.ui.models.SortField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortItem(
|
||||||
|
field: SortField,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val icon = when (field.direction) {
|
||||||
|
SortDirection.ASC -> Icons.Default.ArrowUpward
|
||||||
|
SortDirection.DESC -> Icons.Default.ArrowDownward
|
||||||
|
SortDirection.NONE -> Icons.Default.ArrowDropDown
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
.background(Color.White, RoundedCornerShape(12.dp))
|
||||||
|
.border(1.dp, Color(0x1A000000), RoundedCornerShape(12.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onToggle() }
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = field.label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF364153)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort priority indicator
|
||||||
|
field.order?.let {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.background(Color.Black, RoundedCornerShape(9.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it.toString(),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (field.direction == SortDirection.NONE)
|
||||||
|
Color(0xFF9CA3AF)
|
||||||
|
else
|
||||||
|
Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.livingai_lg.ui.models.SortField
|
||||||
|
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SortScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortOverlay(
|
||||||
|
visible: Boolean,
|
||||||
|
onApplyClick: (selected: List<SortField>) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
BackHandler(enabled = visible) { onDismiss() }
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
||||||
|
// Dim background
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.35f))
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onDismiss() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide-in panel from LEFT
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInHorizontally(
|
||||||
|
initialOffsetX = { -it }
|
||||||
|
),
|
||||||
|
exit = slideOutHorizontally(
|
||||||
|
targetOffsetX = { -it }
|
||||||
|
),
|
||||||
|
modifier = Modifier.align(Alignment.CenterStart)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight(0.85f)
|
||||||
|
.fillMaxWidth(0.75f)
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
topEnd = 24.dp,
|
||||||
|
bottomEnd = 24.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
SortScreen(
|
||||||
|
onApplyClick = { selected ->
|
||||||
|
onApplyClick(selected)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
onCancelClick = onDismiss,
|
||||||
|
onBackClick = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.example.livingai_lg.ui.models.UserAddress
|
||||||
|
import com.example.livingai_lg.ui.models.UserProfile
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserLocationHeader(
|
||||||
|
user: UserProfile,
|
||||||
|
selectedAddressId: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onOpenAddressOverlay: () -> Unit,
|
||||||
|
onProfileClick: () -> Unit = {} // New callback for profile icon click
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.wrapContentWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val selectedAddress = user.addresses.find { it.id == selectedAddressId } ?: user.addresses.first()
|
||||||
|
|
||||||
|
// Profile image - make it clickable
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onProfileClick()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (user.profileImageUrl != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = user.profileImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_profile),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// Address + arrow (click opens overlay)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onOpenAddressOverlay()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = selectedAddress.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.KeyboardArrowDown,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Black,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = selectedAddress.address,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.Black.copy(alpha = 0.7f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoPlayerDialog(
|
||||||
|
uri: Uri,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val player = remember {
|
||||||
|
ExoPlayer.Builder(context).build().apply {
|
||||||
|
setMediaItem(MediaItem.fromUri(uri))
|
||||||
|
prepare()
|
||||||
|
playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
player.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16 / 9f)
|
||||||
|
.background(Color.Black)
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = {
|
||||||
|
PlayerView(it).apply {
|
||||||
|
this.player = player
|
||||||
|
useController = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.example.livingai_lg.ui.components.backgrounds
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import kotlin.math.min
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DecorativeBackground() {
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
val screenW = maxWidth.value
|
||||||
|
val screenH = maxHeight.value
|
||||||
|
|
||||||
|
// Original bg image design size (YOUR asset’s intended size)
|
||||||
|
val designW = 393f
|
||||||
|
val designH = 852f
|
||||||
|
|
||||||
|
// Scale factor that preserves aspect ratio
|
||||||
|
val scale = min(screenW / designW, screenH / designH)
|
||||||
|
|
||||||
|
//------------------------------
|
||||||
|
// Helper to scale dp offsets
|
||||||
|
//------------------------------
|
||||||
|
fun s(value: Float) = (value * scale).dp
|
||||||
|
|
||||||
|
//------------------------------
|
||||||
|
// Background Image
|
||||||
|
//------------------------------
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.bg),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop // ensures full-screen coverage
|
||||||
|
)
|
||||||
|
|
||||||
|
//------------------------------
|
||||||
|
// Decorative Elements (scaled)
|
||||||
|
//------------------------------
|
||||||
|
|
||||||
|
// 🐐 Goat
|
||||||
|
ScaledEmoji(
|
||||||
|
emoji = "🐐",
|
||||||
|
baseFontSize = 48.sp,
|
||||||
|
offsetX = 250f,
|
||||||
|
offsetY = 160f,
|
||||||
|
scale = scale,
|
||||||
|
alpha = 0.10f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 🐄 Cow
|
||||||
|
ScaledEmoji(
|
||||||
|
emoji = "🐄",
|
||||||
|
baseFontSize = 60.sp,
|
||||||
|
offsetX = 64f,
|
||||||
|
offsetY = 569f,
|
||||||
|
scale = scale,
|
||||||
|
alpha = 0.12f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 🌾 Wheat
|
||||||
|
ScaledEmoji(
|
||||||
|
emoji = "🌾🌾🌾",
|
||||||
|
baseFontSize = 32.sp,
|
||||||
|
offsetX = 48f,
|
||||||
|
offsetY = 730f,
|
||||||
|
scale = scale,
|
||||||
|
alpha = 0.15f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScaledEmoji(
|
||||||
|
emoji: String,
|
||||||
|
baseFontSize: TextUnit,
|
||||||
|
offsetX: Float,
|
||||||
|
offsetY: Float,
|
||||||
|
scale: Float,
|
||||||
|
alpha: Float
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = emoji,
|
||||||
|
fontSize = (baseFontSize.value * scale).sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(
|
||||||
|
x = (offsetX * scale).dp,
|
||||||
|
y = (offsetY * scale).dp
|
||||||
|
)
|
||||||
|
.alpha(alpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.example.livingai_lg.ui.components.backgrounds
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.min
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StoreBackground() {
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
val screenW = maxWidth.value
|
||||||
|
val screenH = maxHeight.value
|
||||||
|
|
||||||
|
// Original bg image design size (YOUR asset’s intended size)
|
||||||
|
val designW = 393f
|
||||||
|
val designH = 852f
|
||||||
|
|
||||||
|
// Scale factor that preserves aspect ratio
|
||||||
|
val scale = min(screenW / designW, screenH / designH)
|
||||||
|
|
||||||
|
//------------------------------
|
||||||
|
// Helper to scale dp offsets
|
||||||
|
//------------------------------
|
||||||
|
fun s(value: Float) = (value * scale).dp
|
||||||
|
|
||||||
|
//------------------------------
|
||||||
|
// Background Image
|
||||||
|
//------------------------------
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.bg_shop),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop // ensures full-screen coverage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.example.livingai_lg.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WishlistNameOverlay(
|
||||||
|
onSave: (String) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = true,
|
||||||
|
enter = slideInVertically { -it },
|
||||||
|
exit = slideOutVertically { -it }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = Color.White,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
bottomStart = 20.dp,
|
||||||
|
bottomEnd = 20.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Save Filters",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
placeholder = { Text("Wishlist name") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
onSave(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.example.livingai_lg.ui.layout
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import com.example.livingai_lg.ui.components.BottomNavigationBar
|
||||||
|
import com.example.livingai_lg.ui.models.BottomNavItemData
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomNavScaffold(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
items: List<BottomNavItemData>,
|
||||||
|
currentItem: String,
|
||||||
|
onBottomNavItemClick: (route: String) -> Unit = {},
|
||||||
|
content: @Composable (paddingValues: PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
bottomBar = {
|
||||||
|
BottomNavigationBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(1f)
|
||||||
|
.shadow(8.dp),
|
||||||
|
items,
|
||||||
|
currentItem,
|
||||||
|
onItemClick = onBottomNavItemClick
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
) { paddingValues ->
|
||||||
|
content(paddingValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.example.livingai_lg.ui.login
|
package com.example.livingai_lg.ui.login_legacy
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -14,7 +13,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
@ -52,11 +50,7 @@ fun CreateProfileScreen(navController: NavController, name: String) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -105,7 +99,7 @@ fun ProfileTypeItem(text: String, icon: Int, onClick: () -> Unit) {
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateProfileScreenPreview() {
|
fun CreateProfileScreenPreview() {
|
||||||
LivingAi_LgTheme {
|
FarmMarketplaceTheme {
|
||||||
CreateProfileScreen(rememberNavController(), "John Doe")
|
CreateProfileScreen(rememberNavController(), "John Doe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.livingai_lg.ui.login
|
package com.example.livingai_lg.ui.login_legacy
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -11,7 +11,7 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
@ -29,11 +29,11 @@ fun LoginScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
// .background(
|
||||||
brush = Brush.linearGradient(
|
// brush = Brush.linearGradient(
|
||||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
// colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
) {
|
) {
|
||||||
// Decorative elements...
|
// Decorative elements...
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ fun LoginScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(56.dp)
|
.size(56.dp)
|
||||||
.background(Gold, RoundedCornerShape(28.dp)),
|
.background(Color.Yellow, RoundedCornerShape(28.dp)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(text = "🌾", fontSize = 24.sp)
|
Text(text = "🌾", fontSize = 24.sp)
|
||||||
|
|
@ -64,7 +64,7 @@ fun LoginScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(72.dp)
|
.size(72.dp)
|
||||||
.background(LightOrange.copy(alpha = 0.72f), RoundedCornerShape(36.dp)),
|
.background(Color.Yellow.copy(alpha = 0.72f), RoundedCornerShape(36.dp)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(text = "🌱", fontSize = 32.sp)
|
Text(text = "🌱", fontSize = 32.sp)
|
||||||
|
|
@ -73,7 +73,7 @@ fun LoginScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(56.dp)
|
.size(56.dp)
|
||||||
.background(DarkOrange.copy(alpha = 0.67f), RoundedCornerShape(28.dp)),
|
.background(Color.Yellow.copy(alpha = 0.67f), RoundedCornerShape(28.dp)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(text = "☀️", fontSize = 24.sp)
|
Text(text = "☀️", fontSize = 24.sp)
|
||||||
|
|
@ -86,13 +86,13 @@ fun LoginScreen(navController: NavController) {
|
||||||
text = "Welcome!",
|
text = "Welcome!",
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = DarkBrown
|
color = Color.Yellow
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Join the farm marketplace community",
|
text = "Join the farm marketplace community",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = MidBrown,
|
color = Color.Yellow,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
@ -101,12 +101,12 @@ fun LoginScreen(navController: NavController) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.navigate("signup") },
|
onClick = { navController.navigate("signup") },
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = LightOrange),
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Yellow),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = "New user? Sign up", color = DarkerBrown, fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
Text(text = "New user? Sign up", color = Color.Yellow, fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
@ -115,12 +115,12 @@ fun LoginScreen(navController: NavController) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.navigate("signin") },
|
onClick = { navController.navigate("signin") },
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = TerraCotta),
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Yellow),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = "Already a user? Sign in", color = DarkerBrown, fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
Text(text = "Already a user? Sign in", color = Color.Yellow, fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
@ -128,7 +128,7 @@ fun LoginScreen(navController: NavController) {
|
||||||
// Guest Button
|
// Guest Button
|
||||||
Text(
|
Text(
|
||||||
text = "Continue as Guest",
|
text = "Continue as Guest",
|
||||||
color = MidBrown,
|
color = Color.Yellow,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.clickable { Toast.makeText(context, "Guest mode is not yet available", Toast.LENGTH_SHORT).show() }
|
modifier = Modifier.clickable { Toast.makeText(context, "Guest mode is not yet available", Toast.LENGTH_SHORT).show() }
|
||||||
|
|
@ -142,7 +142,7 @@ fun LoginScreen(navController: NavController) {
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreenPreview() {
|
fun LoginScreenPreview() {
|
||||||
LivingAi_LgTheme {
|
FarmMarketplaceTheme() {
|
||||||
LoginScreen(rememberNavController())
|
LoginScreen(rememberNavController())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package com.example.livingai_lg.ui.login
|
package com.example.livingai_lg.ui.login_legacy
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
|
@ -10,7 +9,6 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -40,11 +38,11 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
// .background(
|
||||||
brush = Brush.linearGradient(
|
// brush = Brush.linearGradient(
|
||||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
// colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -60,7 +58,7 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
value = otp.value,
|
value = otp.value,
|
||||||
onValueChange = { if (it.length <= 6) otp.value = it },
|
onValueChange = { if (it.length <= 6) otp.value = it },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
|
|
@ -117,7 +115,7 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun OtpScreenPreview() {
|
fun OtpScreenPreview() {
|
||||||
LivingAi_LgTheme {
|
FarmMarketplaceTheme() {
|
||||||
OtpScreen(rememberNavController(), "+919876543210", "John Doe")
|
OtpScreen(rememberNavController(), "+919876543210", "John Doe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.livingai_lg.ui.login
|
package com.example.livingai_lg.ui.login_legacy
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -14,7 +14,6 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -42,11 +41,11 @@ fun SignInScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
// .background(
|
||||||
brush = Brush.linearGradient(
|
// brush = Brush.linearGradient(
|
||||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
// colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -161,7 +160,7 @@ fun SignInScreen(navController: NavController) {
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun SignInScreenPreview() {
|
fun SignInScreenPreview() {
|
||||||
LivingAi_LgTheme {
|
FarmMarketplaceTheme() {
|
||||||
SignInScreen(rememberNavController())
|
SignInScreen(rememberNavController())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.livingai_lg.ui.login
|
package com.example.livingai_lg.ui.login_legacy
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -45,11 +44,11 @@ fun SignUpScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
// .background(
|
||||||
brush = Brush.linearGradient(
|
// brush = Brush.linearGradient(
|
||||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
// colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -190,7 +189,7 @@ fun SignUpScreen(navController: NavController) {
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun SignUpScreenPreview() {
|
fun SignUpScreenPreview() {
|
||||||
LivingAi_LgTheme {
|
FarmMarketplaceTheme {
|
||||||
SignUpScreen(rememberNavController())
|
SignUpScreen(rememberNavController())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package com.example.livingai_lg.ui.login
|
package com.example.livingai_lg.ui.login_legacy
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -14,9 +12,6 @@ import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.example.livingai_lg.ui.MainViewModel
|
import com.example.livingai_lg.ui.MainViewModel
|
||||||
import com.example.livingai_lg.ui.UserState
|
import com.example.livingai_lg.ui.UserState
|
||||||
import com.example.livingai_lg.ui.theme.LightCream
|
|
||||||
import com.example.livingai_lg.ui.theme.LighterCream
|
|
||||||
import com.example.livingai_lg.ui.theme.LightestGreen
|
|
||||||
import com.example.livingai_lg.ui.MainViewModelFactory // <-- This was the missing import
|
import com.example.livingai_lg.ui.MainViewModelFactory // <-- This was the missing import
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -28,11 +23,12 @@ fun SuccessScreen(mainViewModel: MainViewModel = viewModel(factory = MainViewMod
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
// .background(
|
||||||
brush = Brush.linearGradient(
|
// brush = Brush.linearGradient(
|
||||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
// colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||||
)
|
// )
|
||||||
),
|
// )
|
||||||
|
,
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (val state = userState) {
|
when (val state = userState) {
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
# Signup API Endpoint
|
||||||
|
|
||||||
|
## POST /auth/signup
|
||||||
|
|
||||||
|
Creates a new user account with name, phone number, and optional location information.
|
||||||
|
|
||||||
|
### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "John Doe", // Required: User's name (string, max 100 chars)
|
||||||
|
"phone_number": "+919876543210", // Required: Phone number in E.164 format
|
||||||
|
"state": "Maharashtra", // Optional: State name (string, max 100 chars)
|
||||||
|
"district": "Mumbai", // Optional: District name (string, max 100 chars)
|
||||||
|
"city_village": "Andheri", // Optional: City/Village name (string, max 150 chars)
|
||||||
|
"device_id": "device-123", // Optional: Device identifier
|
||||||
|
"device_info": { // Optional: Device information
|
||||||
|
"platform": "android",
|
||||||
|
"model": "Samsung Galaxy S21",
|
||||||
|
"os_version": "Android 13",
|
||||||
|
"app_version": "1.0.0",
|
||||||
|
"language_code": "en",
|
||||||
|
"timezone": "Asia/Kolkata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success Response (201 Created)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"user": {
|
||||||
|
"id": "uuid-here",
|
||||||
|
"phone_number": "+919876543210",
|
||||||
|
"name": "John Doe",
|
||||||
|
"country_code": "+91",
|
||||||
|
"created_at": "2024-01-15T10:30:00Z"
|
||||||
|
},
|
||||||
|
"access_token": "jwt-access-token",
|
||||||
|
"refresh_token": "jwt-refresh-token",
|
||||||
|
"needs_profile": true,
|
||||||
|
"is_new_account": true,
|
||||||
|
"is_new_device": true,
|
||||||
|
"active_devices_count": 1,
|
||||||
|
"location_id": "uuid-of-location" // null if no location provided
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
#### 400 Bad Request - Validation Error
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "name is required"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 409 Conflict - User Already Exists
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "User with this phone number already exists. Please sign in instead.",
|
||||||
|
"user_exists": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 403 Forbidden - IP Blocked
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Access denied from this location."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 500 Internal Server Error
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Internal server error"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
1. **User Existence Check**: Automatically checks if a user with the phone number already exists
|
||||||
|
2. **Phone Number Encryption**: Phone numbers are encrypted before storing in database
|
||||||
|
3. **Location Creation**: If state/district/city_village provided, creates a location entry
|
||||||
|
4. **Token Issuance**: Automatically issues access and refresh tokens
|
||||||
|
5. **Device Tracking**: Records device information for security
|
||||||
|
6. **Audit Logging**: Logs signup events for security monitoring
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
#### cURL
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/auth/signup \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "John Doe",
|
||||||
|
"phone_number": "+919876543210",
|
||||||
|
"state": "Maharashtra",
|
||||||
|
"district": "Mumbai",
|
||||||
|
"city_village": "Andheri",
|
||||||
|
"device_id": "android-device-123",
|
||||||
|
"device_info": {
|
||||||
|
"platform": "android",
|
||||||
|
"model": "Samsung Galaxy S21",
|
||||||
|
"os_version": "Android 13"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript/TypeScript
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('/auth/signup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'John Doe',
|
||||||
|
phone_number: '+919876543210',
|
||||||
|
state: 'Maharashtra',
|
||||||
|
district: 'Mumbai',
|
||||||
|
city_village: 'Andheri',
|
||||||
|
device_id: 'android-device-123',
|
||||||
|
device_info: {
|
||||||
|
platform: 'android',
|
||||||
|
model: 'Samsung Galaxy S21',
|
||||||
|
os_version: 'Android 13'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
// Store tokens
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kotlin/Android
|
||||||
|
```kotlin
|
||||||
|
data class SignupRequest(
|
||||||
|
val name: String,
|
||||||
|
val phone_number: String,
|
||||||
|
val state: String? = null,
|
||||||
|
val district: String? = null,
|
||||||
|
val city_village: String? = null,
|
||||||
|
val device_id: String? = null,
|
||||||
|
val device_info: Map<String, String?>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SignupResponse(
|
||||||
|
val success: Boolean,
|
||||||
|
val user: User,
|
||||||
|
val access_token: String,
|
||||||
|
val refresh_token: String,
|
||||||
|
val needs_profile: Boolean,
|
||||||
|
val is_new_account: Boolean,
|
||||||
|
val is_new_device: Boolean,
|
||||||
|
val active_devices_count: Int,
|
||||||
|
val location_id: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
val request = SignupRequest(
|
||||||
|
name = "John Doe",
|
||||||
|
phone_number = "+919876543210",
|
||||||
|
state = "Maharashtra",
|
||||||
|
district = "Mumbai",
|
||||||
|
city_village = "Andheri",
|
||||||
|
device_id = getDeviceId(),
|
||||||
|
device_info = mapOf(
|
||||||
|
"platform" to "android",
|
||||||
|
"model" to Build.MODEL,
|
||||||
|
"os_version" to Build.VERSION.RELEASE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = apiClient.post<SignupResponse>("/auth/signup", request)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Phone number must be in E.164 format (e.g., `+919876543210`)
|
||||||
|
- If phone number is 10 digits without `+`, it will be normalized to `+91` prefix
|
||||||
|
- Location fields are optional - user can be created without location
|
||||||
|
- If user already exists, returns 409 Conflict with `user_exists: true`
|
||||||
|
- All phone numbers are encrypted in the database for security
|
||||||
|
- Country code is automatically extracted from phone number
|
||||||
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
data class Animal(
|
||||||
|
val id: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val age: Int? = null,
|
||||||
|
val breed: String? = null,
|
||||||
|
val breedInfo: String? = null,
|
||||||
|
val price: Long? = null,
|
||||||
|
val isFairPrice: Boolean? = null,
|
||||||
|
val media: List<MediaItem>? = null,
|
||||||
|
val location: String? = null,
|
||||||
|
val displayLocation: String? = null,
|
||||||
|
val distance: Long? = null,
|
||||||
|
val views: Long? = null,
|
||||||
|
val sellerId: String? = null,
|
||||||
|
val sellerName: String? = null,
|
||||||
|
val sellerType: String? = null,
|
||||||
|
val aiScore: Float? = null,
|
||||||
|
val rating: Float? = null,
|
||||||
|
val ratingCount: Int? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val milkCapacity: Float? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val sampleAnimals = listOf(
|
||||||
|
Animal(
|
||||||
|
id = "1",
|
||||||
|
name = "Rita",
|
||||||
|
age = 45,
|
||||||
|
breed = "Gir",
|
||||||
|
breedInfo = "The best in India",
|
||||||
|
location = "Punjab",
|
||||||
|
distance = 12000,
|
||||||
|
media = listOf(MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2F4.bp.blogspot.com%2F_tecSnxaePMo%2FTLLVknW8dOI%2FAAAAAAAAACo%2F_kd1ZNBXU1o%2Fs1600%2FGIR%2CGujrat.jpg&f=1&nofb=1&ipt=da6ba1d040c396b64d3f08cc99998f66200dcd6c001e4a56def143ab3d1a87ea"),MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcpimg.tistatic.com%2F4478702%2Fb%2F4%2Fgir-cow.jpg&f=1&nofb=1&ipt=19bf391461480585c786d01433d863a383c60048ac2ce063ce91f173e215205d"),
|
||||||
|
//MediaItem.Video("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
|
||||||
|
),
|
||||||
|
views = 9001,
|
||||||
|
aiScore = 0.80f,
|
||||||
|
price = 120000,
|
||||||
|
isFairPrice = true,
|
||||||
|
rating = 4.7f,
|
||||||
|
ratingCount = 2076,
|
||||||
|
sellerId = "1",
|
||||||
|
sellerName = "Seller 1",
|
||||||
|
description = "Premium Gir dairy cow in excellent health condition. High-yielding milk producer with consistent output and gentle temperament, making her ideal for commercial dairy operations or family farms.",
|
||||||
|
displayLocation = "Mediatek Pashu Mela, Marathalli, Bangalore (13 km)",
|
||||||
|
milkCapacity = 3.2f
|
||||||
|
),
|
||||||
|
Animal(
|
||||||
|
id = "2",
|
||||||
|
name = "Sahi",
|
||||||
|
price = 95000,
|
||||||
|
age = 35,
|
||||||
|
breed = "Sahiwal",
|
||||||
|
breedInfo = "The 2nd best in India",
|
||||||
|
location = "Punjab",
|
||||||
|
isFairPrice = true,
|
||||||
|
media = listOf(MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdnbbsr.s3waas.gov.in%2Fs3a5a61717dddc3501cfdf7a4e22d7dbaa%2Fuploads%2F2020%2F09%2F2020091812-1024x680.jpg&f=1&nofb=1&ipt=bb426406b3747e54151e4812472e203f33922fa3b4e11c4feef9aa59a5733146")),
|
||||||
|
distance = 0L,
|
||||||
|
views = 100,
|
||||||
|
sellerId = "1",
|
||||||
|
sellerName = "Seller ???",
|
||||||
|
sellerType = "Wholeseller",
|
||||||
|
rating = 5f,
|
||||||
|
ratingCount = 2076,
|
||||||
|
description = "Friendly and energetic companion looking for an active family.",
|
||||||
|
milkCapacity = 2.5f
|
||||||
|
),
|
||||||
|
Animal(
|
||||||
|
id = "3",
|
||||||
|
name = "Bessie",
|
||||||
|
age = 48,
|
||||||
|
breed = "Holstein Friesian",
|
||||||
|
breedInfo = "Not Indian",
|
||||||
|
location = "Punjab",
|
||||||
|
distance = 12000,
|
||||||
|
media = listOf(MediaItem.Image("https://api.builder.io/api/v1/image/assets/TEMP/885e24e34ede6a39f708df13dabc4c1683c3e976?width=786")),
|
||||||
|
views = 94,
|
||||||
|
aiScore = 0.80f,
|
||||||
|
price = 80000,
|
||||||
|
isFairPrice = true,
|
||||||
|
rating = 4.5f,
|
||||||
|
ratingCount = 2076,
|
||||||
|
sellerId = "1",
|
||||||
|
sellerName = "Seller 1",
|
||||||
|
description = "Premium Holstein Friesian dairy cow in excellent health condition. Bessie is a high-yielding milk producer with consistent output and gentle temperament, making her ideal for commercial dairy operations or family farms.",
|
||||||
|
displayLocation = "Mediatek Pashu Mela, Marathalli, Bangalore (13 km)",
|
||||||
|
milkCapacity = 2.3f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
data class AnimalType(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val emoji: String
|
||||||
|
)
|
||||||
|
|
||||||
|
val animalTypes = listOf(
|
||||||
|
AnimalType("cows", "Cows", "🐄"),
|
||||||
|
AnimalType("buffalo", "Buffalo", "🐃"),
|
||||||
|
AnimalType("goat", "Goat", "🐐"),
|
||||||
|
AnimalType("bull", "Bull", "🐂"),
|
||||||
|
AnimalType("baby_cow", "Baby Cow", "🐮"),
|
||||||
|
AnimalType("dog", "Dog", "🐕"),
|
||||||
|
AnimalType("cat", "Cat", "🐱"),
|
||||||
|
AnimalType("others", "Others", "🦜")
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Build
|
||||||
|
import androidx.compose.material.icons.outlined.Home
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
|
||||||
|
data class BottomNavItemData(
|
||||||
|
val label: String,
|
||||||
|
val iconRes: Any,
|
||||||
|
val route: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
val mainBottomNavItems = listOf(
|
||||||
|
BottomNavItemData("Home", R.drawable.ic_home , AppScreen.BUY_ANIMALS),
|
||||||
|
BottomNavItemData("Sell", R.drawable.ic_tag, AppScreen.CREATE_ANIMAL_LISTING),
|
||||||
|
// TODO:
|
||||||
|
BottomNavItemData("Chats", R.drawable.ic_chat, AppScreen.CHATS),
|
||||||
|
BottomNavItemData("Services", R.drawable.ic_config, AppScreen.CREATE_PROFILE),
|
||||||
|
BottomNavItemData("Mandi", R.drawable.ic_shop2, AppScreen.CREATE_PROFILE)
|
||||||
|
)
|
||||||
|
|
||||||
|
val chatBottomNavItems = listOf(
|
||||||
|
BottomNavItemData("Contacts", R.drawable.ic_home ,AppScreen.CONTACTS),
|
||||||
|
BottomNavItemData("Calls", R.drawable.ic_tag, AppScreen.CALLS),
|
||||||
|
BottomNavItemData("Chats", R.drawable.ic_chat, AppScreen.CHATS),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
data class TextFilter(
|
||||||
|
val value: String = "",
|
||||||
|
val filterSet: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RangeFilterState(
|
||||||
|
val min: Int,
|
||||||
|
val max: Int,
|
||||||
|
val filterSet: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FiltersState(
|
||||||
|
val animal: TextFilter = TextFilter(),
|
||||||
|
val breed: TextFilter = TextFilter(),
|
||||||
|
val distance: TextFilter = TextFilter(),
|
||||||
|
val gender: TextFilter = TextFilter(),
|
||||||
|
|
||||||
|
val price: RangeFilterState = RangeFilterState(0, 90_000),
|
||||||
|
val age: RangeFilterState = RangeFilterState(0, 20),
|
||||||
|
val weight: RangeFilterState = RangeFilterState(0, 9_000),
|
||||||
|
val milkYield: RangeFilterState = RangeFilterState(0, 900),
|
||||||
|
|
||||||
|
val pregnancyStatuses: Set<String> = emptySet(),
|
||||||
|
|
||||||
|
val calving: RangeFilterState = RangeFilterState(0, 10)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
fun FiltersState.isDefault(): Boolean {
|
||||||
|
return animal.filterSet.not() &&
|
||||||
|
breed.filterSet.not() &&
|
||||||
|
distance.filterSet.not() &&
|
||||||
|
gender.filterSet.not() &&
|
||||||
|
price.filterSet.not() &&
|
||||||
|
age.filterSet.not() &&
|
||||||
|
weight.filterSet.not() &&
|
||||||
|
milkYield.filterSet.not() &&
|
||||||
|
pregnancyStatuses.isEmpty() &&
|
||||||
|
calving.filterSet.not()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
||||||
|
class NewListingFormState {
|
||||||
|
|
||||||
|
val name = mutableStateOf("")
|
||||||
|
val animal = mutableStateOf("")
|
||||||
|
val breed = mutableStateOf("")
|
||||||
|
val age = mutableStateOf("")
|
||||||
|
val milkYield = mutableStateOf("")
|
||||||
|
val calvingNumber = mutableStateOf("")
|
||||||
|
val reproductiveStatus = mutableStateOf("")
|
||||||
|
val description = mutableStateOf("")
|
||||||
|
|
||||||
|
val animalExpanded = mutableStateOf(false)
|
||||||
|
val breedExpanded = mutableStateOf(false)
|
||||||
|
|
||||||
|
val mediaUploads = mutableStateListOf(
|
||||||
|
MediaUpload("left-view", "Left View", MediaType.PHOTO),
|
||||||
|
MediaUpload("right-view", "Right View", MediaType.PHOTO),
|
||||||
|
MediaUpload("left-angle", "Left Angle View", MediaType.PHOTO),
|
||||||
|
MediaUpload("right-angle", "Right Angle View", MediaType.PHOTO),
|
||||||
|
MediaUpload("angle", "Angle View", MediaType.PHOTO),
|
||||||
|
MediaUpload("video", "", MediaType.VIDEO)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material.icons.filled.Chat
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
|
||||||
|
data class AppNotification(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val icon: ImageVector
|
||||||
|
)
|
||||||
|
|
||||||
|
val sampleNotifications = listOf(
|
||||||
|
AppNotification(
|
||||||
|
id = "1",
|
||||||
|
title = "Animal saved successfully",
|
||||||
|
icon = Icons.Default.Bookmark
|
||||||
|
),
|
||||||
|
AppNotification(
|
||||||
|
id = "2",
|
||||||
|
title = "New message from Seller",
|
||||||
|
icon = Icons.Default.Chat
|
||||||
|
),
|
||||||
|
AppNotification(
|
||||||
|
id = "3",
|
||||||
|
title = "Price dropped on an animal you viewed",
|
||||||
|
icon = Icons.Default.Notifications
|
||||||
|
),
|
||||||
|
AppNotification(
|
||||||
|
id = "4",
|
||||||
|
title = "You received a new rating",
|
||||||
|
icon = Icons.Default.Star
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
data class ProfileType(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val icon: Any,
|
||||||
|
val iconTint: Color = Color.Unspecified,
|
||||||
|
val backgroundColor: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
val profileTypes = listOf(
|
||||||
|
ProfileType(
|
||||||
|
id = "buyer_seller",
|
||||||
|
title = "I'm a Buyer/Seller",
|
||||||
|
icon = R.drawable.ic_shop,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFF9D4EDD)
|
||||||
|
),
|
||||||
|
ProfileType(
|
||||||
|
id = "wholesale_trader",
|
||||||
|
title = "I'm a Wholesale Trader",
|
||||||
|
icon = R.drawable.ic_bag,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFF3A86FF)
|
||||||
|
),
|
||||||
|
ProfileType(
|
||||||
|
id = "service_provider",
|
||||||
|
title = "I'm a Service Provider",
|
||||||
|
icon = R.drawable.ic_spanner,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFFFF5722)
|
||||||
|
),
|
||||||
|
ProfileType(
|
||||||
|
id = "mandi_host",
|
||||||
|
title = "I'm a Mandi Host",
|
||||||
|
icon = R.drawable.ic_shop2,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFF4CAF50)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
class Seller {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
enum class SortDirection {
|
||||||
|
NONE,
|
||||||
|
ASC,
|
||||||
|
DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SortField(
|
||||||
|
val key: String,
|
||||||
|
val label: String,
|
||||||
|
val direction: SortDirection = SortDirection.NONE,
|
||||||
|
val order: Int? = null
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
data class UserAddress(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val isPrimary: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UserProfile(
|
||||||
|
val name: String,
|
||||||
|
val profileImageUrl: String? = null,
|
||||||
|
val addresses: List<UserAddress>
|
||||||
|
)
|
||||||
|
|
||||||
|
val userProfile =
|
||||||
|
UserProfile(
|
||||||
|
name = "John Doe",
|
||||||
|
profileImageUrl = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fstatic.vecteezy.com%2Fsystem%2Fresources%2Fpreviews%2F014%2F016%2F808%2Fnon_2x%2Findian-farmer-face-vector.jpg&f=1&nofb=1&ipt=c352fec591428aebefe6cd263d2958765e85d4da69cce3c46b725ba2ff7d3448",
|
||||||
|
addresses = listOf(
|
||||||
|
UserAddress(
|
||||||
|
id = "1",
|
||||||
|
name = "Home",
|
||||||
|
address = "205 1st floor 7th cross 27th main, PW",
|
||||||
|
isPrimary = true
|
||||||
|
),
|
||||||
|
UserAddress(
|
||||||
|
id = "2",
|
||||||
|
name = "Farm",
|
||||||
|
address = "2nd block, MG Farms"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.example.livingai_lg.ui.models
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
|
||||||
|
class MediaUpload(
|
||||||
|
val id: String,
|
||||||
|
val label: String,
|
||||||
|
val type: MediaType,
|
||||||
|
initialUri: Uri? = null
|
||||||
|
) {
|
||||||
|
var uri by mutableStateOf(initialUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class MediaType {
|
||||||
|
PHOTO, VIDEO
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class MediaItem {
|
||||||
|
data class Image(val url: String) : MediaItem()
|
||||||
|
data class Video(val url: String, val thumbnailUrl: String? = null) : MediaItem()
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,505 @@
|
||||||
|
package com.example.livingai_lg.ui.navigation
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.example.farmmarketplace.ui.screens.CallsScreen
|
||||||
|
import com.example.farmmarketplace.ui.screens.ContactsScreen
|
||||||
|
import com.example.livingai_lg.ui.AuthState
|
||||||
|
import com.example.livingai_lg.ui.MainViewModel
|
||||||
|
import com.example.livingai_lg.ui.screens.AccountsScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.BuyScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.ChooseServiceScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.CreateProfileScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.NewListingScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.PostSaleSurveyScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SaleArchiveScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SavedListingsScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SortScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.WishlistScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.SignUpScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.chat.ChatScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.chat.ChatsScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.testing.ApiTestScreen
|
||||||
|
|
||||||
|
fun NavController.navigateIfAuthenticated(
|
||||||
|
authState: AuthState,
|
||||||
|
targetRoute: String,
|
||||||
|
fallbackRoute: String = AppScreen.LANDING
|
||||||
|
) {
|
||||||
|
if (authState is AuthState.Authenticated) {
|
||||||
|
navigate(targetRoute) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate(fallbackRoute) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppNavigation(
|
||||||
|
authState: AuthState,
|
||||||
|
mainViewModel: MainViewModel
|
||||||
|
) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
/* ---------------------------------------------------
|
||||||
|
* Collect one-time navigation events (OTP, login, logout)
|
||||||
|
* --------------------------------------------------- */
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
mainViewModel.navEvents.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
|
||||||
|
is NavEvent.ToCreateProfile -> {
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.createProfile(event.name)
|
||||||
|
) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is NavEvent.ToChooseService -> {
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.chooseService(event.profileId)
|
||||||
|
) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavEvent.ToLanding -> {
|
||||||
|
navController.navigate(AppScreen.LANDING) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onNavClick: (String) -> Unit = { route ->
|
||||||
|
val currentRoute =
|
||||||
|
navController.currentBackStackEntry?.destination?.route
|
||||||
|
Log.d("Current Route:"," $currentRoute $route")
|
||||||
|
|
||||||
|
if (currentRoute != route) {
|
||||||
|
navController.navigate(route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
//restoreState = true
|
||||||
|
// popUpTo(navController.graph.startDestinationId) {
|
||||||
|
// saveState = true
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------
|
||||||
|
* Single NavHost
|
||||||
|
* --------------------------------------------------- */
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = if(authState is AuthState.Authenticated) AppScreen.BUY_ANIMALS else AppScreen.LANDING
|
||||||
|
) {
|
||||||
|
|
||||||
|
/* ---------------- AUTH ---------------- */
|
||||||
|
|
||||||
|
composable(AppScreen.LANDING) {
|
||||||
|
LandingScreen(
|
||||||
|
onSignUpClick = {
|
||||||
|
navController.navigate(AppScreen.SIGN_UP)
|
||||||
|
},
|
||||||
|
onSignInClick = {
|
||||||
|
navController.navigate(AppScreen.SIGN_IN)
|
||||||
|
},
|
||||||
|
onGuestClick = {
|
||||||
|
mainViewModel.emitNavEvent(
|
||||||
|
NavEvent.ToCreateProfile("guest")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.SIGN_IN) {
|
||||||
|
SignInScreen(
|
||||||
|
onSignUpClick = {
|
||||||
|
navController.navigate(AppScreen.SIGN_UP)
|
||||||
|
},
|
||||||
|
onSignInClick = { phone, name ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.otp(phone, name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.SIGN_UP) {
|
||||||
|
SignUpScreen(
|
||||||
|
onSignUpClick = { phone, name, state, district, village ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.otpWithSignup(
|
||||||
|
phone, name, state, district, village
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSignInClick = {
|
||||||
|
navController.navigate(AppScreen.SIGN_IN)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.OTP}/{phoneNumber}/{name}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("phoneNumber") { type = NavType.StringType },
|
||||||
|
navArgument("name") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
OtpScreen(
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
|
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||||
|
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||||
|
onCreateProfile = { name ->
|
||||||
|
// navController.navigateIfAuthenticated(
|
||||||
|
// authState,
|
||||||
|
// AppScreen.createProfile(name),
|
||||||
|
// AppScreen.LANDING
|
||||||
|
// )
|
||||||
|
mainViewModel.emitNavEvent(
|
||||||
|
NavEvent.ToCreateProfile(name)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
// navController.navigateIfAuthenticated(
|
||||||
|
// authState,
|
||||||
|
// AppScreen.chooseService(""),
|
||||||
|
// AppScreen.LANDING
|
||||||
|
// )
|
||||||
|
mainViewModel.emitNavEvent(
|
||||||
|
NavEvent.ToChooseService()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.OTP}/{phoneNumber}/{name}/{state}/{district}/{village}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("phoneNumber") { type = NavType.StringType },
|
||||||
|
navArgument("name") { type = NavType.StringType },
|
||||||
|
navArgument("state") { type = NavType.StringType },
|
||||||
|
navArgument("district") { type = NavType.StringType },
|
||||||
|
navArgument("village") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
OtpScreen(
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
|
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||||
|
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||||
|
signupState = backStackEntry.arguments?.getString("state"),
|
||||||
|
signupDistrict = backStackEntry.arguments?.getString("district"),
|
||||||
|
signupVillage = backStackEntry.arguments?.getString("village"),
|
||||||
|
onCreateProfile = { name ->
|
||||||
|
mainViewModel.emitNavEvent(
|
||||||
|
NavEvent.ToCreateProfile(name)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
mainViewModel.emitNavEvent(
|
||||||
|
NavEvent.ToChooseService()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- MAIN ---------------- */
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.CREATE_PROFILE}/{name}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("name") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
CreateProfileScreen(
|
||||||
|
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||||
|
onProfileSelected = { profileId ->
|
||||||
|
mainViewModel.emitNavEvent(
|
||||||
|
NavEvent.ToChooseService(profileId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.CHOOSE_SERVICE}/{profileId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("profileId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
ChooseServiceScreen(
|
||||||
|
profileId = backStackEntry.arguments?.getString("profileId") ?: "",
|
||||||
|
onServiceSelected = {
|
||||||
|
navController.navigate(AppScreen.BUY_ANIMALS)
|
||||||
|
// navController.navigateIfAuthenticated(
|
||||||
|
// authState,
|
||||||
|
// AppScreen.BUY_ANIMALS
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.BUY_ANIMALS) {
|
||||||
|
BuyScreen(
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onProductClick = { animalId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.animalProfile(animalId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onNavClick = onNavClick,
|
||||||
|
onSellerClick = { sellerId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.sellerProfile(sellerId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onWishlistClick = {
|
||||||
|
navController.navigate(AppScreen.WISHLIST)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.WISHLIST) {
|
||||||
|
WishlistScreen(
|
||||||
|
onApply = {
|
||||||
|
navController.navigate(AppScreen.BUY_ANIMALS)
|
||||||
|
},
|
||||||
|
onBack = {navController.popBackStack()}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||||
|
// FilterScreen(
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||||
|
// onCancelClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||||
|
// SortScreen(
|
||||||
|
// onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onCancelClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
composable(AppScreen.SAVED_LISTINGS) {
|
||||||
|
SavedListingsScreen(
|
||||||
|
onNavClick = onNavClick,
|
||||||
|
onBackClick = { navController.popBackStack() })
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.ACCOUNTS) {
|
||||||
|
AccountsScreen(
|
||||||
|
onBackClick = { navController.popBackStack() },
|
||||||
|
onLogout = {
|
||||||
|
// Navigate to auth graph after logout
|
||||||
|
navController.navigate(AppScreen.LANDING) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onApiTest = {
|
||||||
|
navController.navigate(AppScreen.API_TEST)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||||
|
NewListingScreen (
|
||||||
|
onSaveClick = {navController.navigate(
|
||||||
|
AppScreen.postSaleSurvey("2")
|
||||||
|
)},
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(AppScreen.BUY_ANIMALS){
|
||||||
|
popUpTo(AppScreen.BUY_ANIMALS){
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("saleId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val saleId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("saleId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
SaleArchiveScreen(
|
||||||
|
saleId = saleId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onSaleSurveyClick = { saleId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.sellerProfile(saleId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("animalId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val animalId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("animalId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
PostSaleSurveyScreen (
|
||||||
|
animalId = animalId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(AppScreen.CREATE_ANIMAL_LISTING)
|
||||||
|
},
|
||||||
|
onSubmit = {navController.navigate(
|
||||||
|
AppScreen.saleArchive("2")
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("animalId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val animalId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("animalId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
AnimalProfileScreen(
|
||||||
|
animalId = animalId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onNavClick = onNavClick,
|
||||||
|
onSellerClick = { sellerId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.sellerProfile(sellerId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("sellerId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val sellerId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("sellerId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
SellerProfileScreen(
|
||||||
|
sellerId = sellerId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.CONTACTS) {
|
||||||
|
ContactsScreen(
|
||||||
|
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||||
|
onTabClick = onNavClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.CALLS) {
|
||||||
|
CallsScreen(
|
||||||
|
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||||
|
onTabClick = onNavClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.CHATS) {
|
||||||
|
ChatsScreen(
|
||||||
|
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||||
|
onTabClick = onNavClick,
|
||||||
|
onChatItemClick = {navController.navigate(AppScreen.chats("2"))}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.CHAT}/{contact}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("contact") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val sellerId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("contact")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
ChatScreen(
|
||||||
|
sellerId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(AppScreen.CHATS)
|
||||||
|
//navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//A route for testing
|
||||||
|
composable(AppScreen.API_TEST) {
|
||||||
|
ApiTestScreen(
|
||||||
|
onBackClick = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.example.livingai_lg.ui.navigation
|
||||||
|
|
||||||
|
object AppScreen {
|
||||||
|
const val LANDING = "landing"
|
||||||
|
const val SIGN_IN = "sign_in"
|
||||||
|
const val SIGN_UP = "sign_up"
|
||||||
|
|
||||||
|
const val OTP = "otp"
|
||||||
|
|
||||||
|
fun otp(phone: String, name: String) =
|
||||||
|
"$OTP/$phone/$name"
|
||||||
|
|
||||||
|
fun otpWithSignup(phone: String, name: String, state: String, district: String, village: String) =
|
||||||
|
"$OTP/$phone/$name/$state/$district/$village"
|
||||||
|
|
||||||
|
const val CHOOSE_SERVICE = "choose_service"
|
||||||
|
fun chooseService(profileId: String?) =
|
||||||
|
"$CHOOSE_SERVICE/$profileId"
|
||||||
|
|
||||||
|
const val CREATE_PROFILE = "create_profile"
|
||||||
|
|
||||||
|
fun createProfile(name: String) =
|
||||||
|
"$CREATE_PROFILE/$name"
|
||||||
|
|
||||||
|
const val BUY_ANIMALS = "buy_animals"
|
||||||
|
const val ANIMAL_PROFILE = "animal_profile"
|
||||||
|
fun animalProfile(animalId: String) =
|
||||||
|
"$ANIMAL_PROFILE/$animalId"
|
||||||
|
|
||||||
|
const val CREATE_ANIMAL_LISTING = "create_animal_listing"
|
||||||
|
|
||||||
|
const val BUY_ANIMALS_FILTERS = "buy_animals_filters"
|
||||||
|
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||||
|
|
||||||
|
const val WISHLIST = "wishlist"
|
||||||
|
|
||||||
|
const val SELLER_PROFILE = "seller_profile"
|
||||||
|
fun sellerProfile(sellerId: String) =
|
||||||
|
"$SELLER_PROFILE/$sellerId"
|
||||||
|
|
||||||
|
const val SALE_ARCHIVE = "sale_archive"
|
||||||
|
fun saleArchive(saleId: String) =
|
||||||
|
"$SALE_ARCHIVE/$saleId"
|
||||||
|
|
||||||
|
const val POST_SALE_SURVEY = "post_sale_survey"
|
||||||
|
fun postSaleSurvey(animalId: String) =
|
||||||
|
"$POST_SALE_SURVEY/$animalId"
|
||||||
|
|
||||||
|
const val SAVED_LISTINGS = "saved_listings"
|
||||||
|
|
||||||
|
const val CONTACTS = "contacts"
|
||||||
|
|
||||||
|
fun chats(contact: String) =
|
||||||
|
"$CHAT/$contact"
|
||||||
|
|
||||||
|
const val CALLS = "calls"
|
||||||
|
|
||||||
|
const val CHAT = "chat"
|
||||||
|
|
||||||
|
const val CHATS = "chats"
|
||||||
|
|
||||||
|
const val ACCOUNTS = "accounts"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Test screens::
|
||||||
|
const val API_TEST = "api_test"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.example.livingai_lg.ui.navigation
|
||||||
|
sealed class NavEvent {
|
||||||
|
data class ToCreateProfile(val name: String) : NavEvent()
|
||||||
|
data class ToChooseService(val profileId: String = "") : NavEvent()
|
||||||
|
object ToLanding : NavEvent()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.navigation
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.example.livingai_lg.ui.MainViewModel
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
// Note: CoroutineScope, Dispatchers, delay, launch still used in onCreateProfile callbacks
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.auth.SignUpScreen
|
||||||
|
|
||||||
|
fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: MainViewModel) {
|
||||||
|
navigation(
|
||||||
|
route = Graph.AUTH,
|
||||||
|
startDestination = AppScreen.LANDING
|
||||||
|
) {
|
||||||
|
composable(AppScreen.LANDING) {
|
||||||
|
LandingScreen(
|
||||||
|
onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||||
|
onSignInClick = { navController.navigate(AppScreen.SIGN_IN) },
|
||||||
|
onGuestClick = { navController.navigate(Graph.main(AppScreen.createProfile("guest"))) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.SIGN_IN) {
|
||||||
|
SignInScreen(
|
||||||
|
onSignUpClick = { navController.navigate(AppScreen.SIGN_UP){
|
||||||
|
popUpTo(AppScreen.SIGN_IN) { inclusive = true }
|
||||||
|
} },
|
||||||
|
onSignInClick = { phone, name ->
|
||||||
|
navController.navigate(AppScreen.otp(phone,name))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.SIGN_UP) {
|
||||||
|
SignUpScreen(
|
||||||
|
onSignUpClick = { phone, name, state, district, village ->
|
||||||
|
navController.navigate(AppScreen.otpWithSignup(phone, name, state, district, village))
|
||||||
|
},
|
||||||
|
onSignInClick = {
|
||||||
|
navController.navigate(AppScreen.SIGN_IN) {
|
||||||
|
popUpTo(AppScreen.SIGN_UP) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.OTP}/{phoneNumber}/{name}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("phoneNumber") { type = NavType.StringType },
|
||||||
|
navArgument("name") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
OtpScreen(
|
||||||
|
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||||
|
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
|
onCreateProfile = {name ->
|
||||||
|
Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
|
||||||
|
// Navigate to main graph first without popping, then navigate to specific route
|
||||||
|
try {
|
||||||
|
// Navigate to main graph (this will use its start destination)
|
||||||
|
navController.navigate(Graph.MAIN) {
|
||||||
|
// Don't pop the AUTH graph yet - keep the graph structure
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
// Then navigate to the specific route after a short delay
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
delay(200)
|
||||||
|
try {
|
||||||
|
navController.navigate(AppScreen.createProfile(name)) {
|
||||||
|
// Now pop the AUTH graph after we're in the MAIN graph
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLanding = {
|
||||||
|
Log.d("AuthNavGraph", "Navigating to landing page for new user")
|
||||||
|
// Navigate to landing page within AUTH graph
|
||||||
|
try {
|
||||||
|
navController.navigate(AppScreen.LANDING) {
|
||||||
|
// Pop all screens up to and including the current OTP screen
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = false }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||||
|
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||||
|
try {
|
||||||
|
navController.navigate(Graph.MAIN) {
|
||||||
|
// Clear back stack to prevent going back to auth screens
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.OTP}/{phoneNumber}/{name}/{state}/{district}/{village}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("phoneNumber") { type = NavType.StringType },
|
||||||
|
navArgument("name") { type = NavType.StringType },
|
||||||
|
navArgument("state") { type = NavType.StringType },
|
||||||
|
navArgument("district") { type = NavType.StringType },
|
||||||
|
navArgument("village") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
OtpScreen(
|
||||||
|
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||||
|
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||||
|
signupState = backStackEntry.arguments?.getString("state"),
|
||||||
|
signupDistrict = backStackEntry.arguments?.getString("district"),
|
||||||
|
signupVillage = backStackEntry.arguments?.getString("village"),
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
|
onCreateProfile = {name ->
|
||||||
|
Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
|
||||||
|
// Navigate to main graph first without popping, then navigate to specific route
|
||||||
|
try {
|
||||||
|
// Navigate to main graph (this will use its start destination)
|
||||||
|
navController.navigate(Graph.MAIN) {
|
||||||
|
// Don't pop the AUTH graph yet - keep the graph structure
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
// Then navigate to the specific route after a short delay
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
delay(200)
|
||||||
|
try {
|
||||||
|
navController.navigate(AppScreen.createProfile(name)) {
|
||||||
|
// Now pop the AUTH graph after we're in the MAIN graph
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLanding = {
|
||||||
|
Log.d("AuthNavGraph", "Navigating to landing page for new user")
|
||||||
|
// Navigate to landing page within AUTH graph
|
||||||
|
try {
|
||||||
|
navController.navigate(AppScreen.LANDING) {
|
||||||
|
// Pop all screens up to and including the current OTP screen
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = false }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||||
|
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||||
|
try {
|
||||||
|
navController.navigate(Graph.MAIN) {
|
||||||
|
// Clear back stack to prevent going back to auth screens
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.navigation
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.example.farmmarketplace.ui.screens.CallsScreen
|
||||||
|
import com.example.farmmarketplace.ui.screens.ContactsScreen
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.AccountsScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.BuyScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.ChooseServiceScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.CreateProfileScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.NewListingScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.PostSaleSurveyScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SaleArchiveScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SavedListingsScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.SortScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.chat.ChatScreen
|
||||||
|
import com.example.livingai_lg.ui.screens.chat.ChatsScreen
|
||||||
|
|
||||||
|
fun NavGraphBuilder.mainNavGraph(navController: NavController) {
|
||||||
|
val onNavClick: (String) -> Unit = { route ->
|
||||||
|
val currentRoute =
|
||||||
|
navController.currentBackStackEntry?.destination?.route
|
||||||
|
Log.d("Current Route:"," $currentRoute $route")
|
||||||
|
|
||||||
|
if (currentRoute != route) {
|
||||||
|
navController.navigate(route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
//restoreState = true
|
||||||
|
// popUpTo(navController.graph.startDestinationId) {
|
||||||
|
// saveState = true
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation(
|
||||||
|
route = Graph.MAIN,
|
||||||
|
startDestination = AppScreen.chooseService("1") // Default to ChooseServiceScreen for authenticated users
|
||||||
|
){
|
||||||
|
|
||||||
|
|
||||||
|
// NavHost(
|
||||||
|
// navController = navController,
|
||||||
|
// startDestination = AppScreen.createProfile("guest")
|
||||||
|
// ) {
|
||||||
|
composable(
|
||||||
|
"${AppScreen.CREATE_PROFILE}/{name}",
|
||||||
|
arguments = listOf(navArgument("name") { type = NavType.StringType })
|
||||||
|
) { backStackEntry ->
|
||||||
|
CreateProfileScreen(
|
||||||
|
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||||
|
onProfileSelected = { profileId ->
|
||||||
|
navController.navigate(AppScreen.chooseService(profileId))
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
"${AppScreen.CHOOSE_SERVICE}/{profileId}",
|
||||||
|
arguments = listOf(navArgument("profileId") { type = NavType.StringType })
|
||||||
|
) { backStackEntry ->
|
||||||
|
ChooseServiceScreen (
|
||||||
|
profileId = backStackEntry.arguments?.getString("profileId") ?: "",
|
||||||
|
onServiceSelected = { navController.navigate(AppScreen.BUY_ANIMALS) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.BUY_ANIMALS) {
|
||||||
|
BuyScreen(
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onProductClick = { animalId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.animalProfile(animalId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onNavClick = onNavClick,
|
||||||
|
onSellerClick = { sellerId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.sellerProfile(sellerId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||||
|
// FilterScreen(
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||||
|
// onCancelClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||||
|
SortScreen(
|
||||||
|
onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onCancelClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.SAVED_LISTINGS) {
|
||||||
|
SavedListingsScreen(
|
||||||
|
onNavClick = onNavClick,
|
||||||
|
onBackClick = { navController.popBackStack() })
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.ACCOUNTS) {
|
||||||
|
AccountsScreen(
|
||||||
|
onBackClick = { navController.popBackStack() },
|
||||||
|
onLogout = {
|
||||||
|
// Navigate to auth graph after logout
|
||||||
|
navController.navigate(Graph.AUTH) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onApiTest = {navController.navigate(AppScreen.API_TEST)}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||||
|
NewListingScreen (
|
||||||
|
onSaveClick = {navController.navigate(
|
||||||
|
AppScreen.postSaleSurvey("2")
|
||||||
|
)},
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(AppScreen.BUY_ANIMALS){
|
||||||
|
popUpTo(AppScreen.BUY_ANIMALS){
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("saleId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val saleId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("saleId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
SaleArchiveScreen(
|
||||||
|
saleId = saleId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onSaleSurveyClick = { saleId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.sellerProfile(saleId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("animalId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val animalId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("animalId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
PostSaleSurveyScreen (
|
||||||
|
animalId = animalId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(AppScreen.CREATE_ANIMAL_LISTING)
|
||||||
|
},
|
||||||
|
onSubmit = {navController.navigate(
|
||||||
|
AppScreen.saleArchive("2")
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("animalId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val animalId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("animalId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
AnimalProfileScreen(
|
||||||
|
animalId = animalId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onNavClick = onNavClick,
|
||||||
|
onSellerClick = { sellerId ->
|
||||||
|
navController.navigate(
|
||||||
|
AppScreen.sellerProfile(sellerId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("sellerId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val sellerId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("sellerId")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
SellerProfileScreen(
|
||||||
|
sellerId = sellerId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.CONTACTS) {
|
||||||
|
ContactsScreen(
|
||||||
|
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||||
|
onTabClick = onNavClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.CALLS) {
|
||||||
|
CallsScreen(
|
||||||
|
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||||
|
onTabClick = onNavClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(AppScreen.CHATS) {
|
||||||
|
ChatsScreen(
|
||||||
|
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||||
|
onTabClick = onNavClick,
|
||||||
|
onChatItemClick = {navController.navigate(AppScreen.chats("2"))}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "${AppScreen.CHAT}/{contact}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("contact") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
|
||||||
|
val sellerId = backStackEntry
|
||||||
|
.arguments
|
||||||
|
?.getString("contact")
|
||||||
|
?: return@composable
|
||||||
|
|
||||||
|
ChatScreen(
|
||||||
|
sellerId,
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(AppScreen.CHATS)
|
||||||
|
//navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import com.example.livingai_lg.ui.AuthState
|
||||||
|
import com.example.livingai_lg.ui.MainViewModel
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
|
||||||
|
object OldAppScreen {
|
||||||
|
const val LANDING = "landing"
|
||||||
|
const val SIGN_IN = "sign_in"
|
||||||
|
const val SIGN_UP = "sign_up"
|
||||||
|
|
||||||
|
const val OTP = "otp"
|
||||||
|
|
||||||
|
const val CHOOSE_SERVICE = "choose_service"
|
||||||
|
|
||||||
|
const val CREATE_PROFILE = "create_profile"
|
||||||
|
|
||||||
|
const val BUY_ANIMALS = "buy_animals"
|
||||||
|
const val ANIMAL_PROFILE = "animal_profile"
|
||||||
|
|
||||||
|
const val CREATE_ANIMAL_LISTING = "create_animal_listing"
|
||||||
|
|
||||||
|
const val BUY_ANIMALS_FILTERS = "buy_animals_filters"
|
||||||
|
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||||
|
|
||||||
|
const val SELLER_PROFILE = "seller_profile"
|
||||||
|
|
||||||
|
const val SALE_ARCHIVE = "sale_archive"
|
||||||
|
|
||||||
|
const val POST_SALE_SURVEY = "post_sale_survey"
|
||||||
|
|
||||||
|
const val SAVED_LISTINGS = "saved_listings"
|
||||||
|
|
||||||
|
const val CONTACTS = "contacts"
|
||||||
|
|
||||||
|
const val CALLS = "calls"
|
||||||
|
|
||||||
|
const val CHAT = "chat"
|
||||||
|
|
||||||
|
const val CHATS = "chats"
|
||||||
|
|
||||||
|
const val ACCOUNTS = "accounts"
|
||||||
|
|
||||||
|
fun chats(contact: String) =
|
||||||
|
"$CHAT/$contact"
|
||||||
|
|
||||||
|
fun otp(phone: String, name: String) =
|
||||||
|
"$OTP/$phone/$name"
|
||||||
|
|
||||||
|
fun otpWithSignup(phone: String, name: String, state: String, district: String, village: String) =
|
||||||
|
"$OTP/$phone/$name/$state/$district/$village"
|
||||||
|
|
||||||
|
fun createProfile(name: String) =
|
||||||
|
"$CREATE_PROFILE/$name"
|
||||||
|
|
||||||
|
fun chooseService(profileId: String) =
|
||||||
|
"$CHOOSE_SERVICE/$profileId"
|
||||||
|
fun postSaleSurvey(animalId: String) =
|
||||||
|
"$POST_SALE_SURVEY/$animalId"
|
||||||
|
|
||||||
|
fun animalProfile(animalId: String) =
|
||||||
|
"$ANIMAL_PROFILE/$animalId"
|
||||||
|
|
||||||
|
fun sellerProfile(sellerId: String) =
|
||||||
|
"$SELLER_PROFILE/$sellerId"
|
||||||
|
|
||||||
|
fun saleArchive(saleId: String) =
|
||||||
|
"$SALE_ARCHIVE/$saleId"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object Graph {
|
||||||
|
const val AUTH = "auth"
|
||||||
|
const val MAIN = "main"
|
||||||
|
|
||||||
|
fun auth(route: String)=
|
||||||
|
"$AUTH/$route"
|
||||||
|
|
||||||
|
fun main(route: String)=
|
||||||
|
"$MAIN/$route"
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
fun OldAppNavigation(
|
||||||
|
authState: AuthState,
|
||||||
|
mainViewModel: MainViewModel
|
||||||
|
) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
// Determine start destination based on initial auth state
|
||||||
|
// This prevents showing landing screen when user is already logged in
|
||||||
|
val startDestination = remember(authState) {
|
||||||
|
when (authState) {
|
||||||
|
is AuthState.Authenticated -> Graph.MAIN
|
||||||
|
is AuthState.Unauthenticated -> Graph.AUTH
|
||||||
|
is AuthState.Unknown -> Graph.AUTH // Show landing while checking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = startDestination
|
||||||
|
) {
|
||||||
|
authNavGraph(navController, mainViewModel)
|
||||||
|
mainNavGraph(navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation based on auth state changes
|
||||||
|
LaunchedEffect(authState) {
|
||||||
|
Log.d("AppNavigation", "LaunchedEffect triggered with authState: $authState")
|
||||||
|
when (authState) {
|
||||||
|
is AuthState.Authenticated -> {
|
||||||
|
// User is authenticated, navigate to ChooseServiceScreen
|
||||||
|
// Add a small delay to ensure NavHost graphs are fully built
|
||||||
|
delay(100)
|
||||||
|
|
||||||
|
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||||
|
Log.d("AppNavigation", "Authenticated - currentRoute: $currentRoute")
|
||||||
|
// Only navigate if we're not already in the MAIN graph or ChooseServiceScreen
|
||||||
|
if (currentRoute?.startsWith(Graph.MAIN) != true &&
|
||||||
|
currentRoute?.startsWith(AppScreen.CHOOSE_SERVICE) != true) {
|
||||||
|
Log.d("AppNavigation", "Navigating to ChooseServiceScreen (default profileId: 1)")
|
||||||
|
try {
|
||||||
|
// Navigate directly to the start destination route of MAIN graph
|
||||||
|
// This avoids the "Sequence is empty" error when navigating to Graph.MAIN
|
||||||
|
navController.navigate(AppScreen.chooseService("1")) {
|
||||||
|
// Clear back stack to prevent going back to auth screens
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
|
// Prevent multiple navigations
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AppNavigation", "Navigation error: ${e.message}", e)
|
||||||
|
// Fallback: try navigating to Graph.MAIN if direct route fails
|
||||||
|
try {
|
||||||
|
navController.navigate(Graph.MAIN) {
|
||||||
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
Log.e("AppNavigation", "Fallback navigation also failed: ${e2.message}", e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d("AppNavigation", "Already in MAIN graph or ChooseServiceScreen, skipping navigation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AuthState.Unauthenticated -> {
|
||||||
|
// User is not authenticated, ensure we're in auth graph (landing screen)
|
||||||
|
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||||
|
if (currentRoute?.startsWith(Graph.MAIN) == true ||
|
||||||
|
currentRoute?.startsWith(Graph.AUTH) != true) {
|
||||||
|
navController.navigate(Graph.AUTH) {
|
||||||
|
// Clear back stack to prevent going back to main screens
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AuthState.Unknown -> {
|
||||||
|
// Still checking auth status
|
||||||
|
// If we're on landing screen, stay there
|
||||||
|
// If we're on main screen and checking, don't navigate yet
|
||||||
|
// This prevents flickering during token validation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// MainNavGraph(navController)
|
||||||
|
// AuthNavGraph(navController)
|
||||||
|
// when (authState) {
|
||||||
|
// is AuthState.Unauthenticated -> {AuthNavGraph()}
|
||||||
|
// is AuthState.Authenticated -> {MainNavGraph()}
|
||||||
|
// is AuthState.Unknown -> {
|
||||||
|
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
// CircularProgressIndicator()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// AuthState.Loading -> SplashScreen()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// val onNavClick: (String) -> Unit = { route ->
|
||||||
|
// val currentRoute =
|
||||||
|
// navController.currentBackStackEntry?.destination?.route
|
||||||
|
//
|
||||||
|
// if (currentRoute != route) {
|
||||||
|
// navController.navigate(route) {
|
||||||
|
// launchSingleTop = true
|
||||||
|
// restoreState = true
|
||||||
|
// popUpTo(navController.graph.startDestinationId) {
|
||||||
|
// saveState = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// NavHost(
|
||||||
|
// navController = navController,
|
||||||
|
// startDestination = AppScreen.LANDING,
|
||||||
|
//
|
||||||
|
// ) {
|
||||||
|
// composable(AppScreen.LANDING) {
|
||||||
|
// LandingScreen(
|
||||||
|
// onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||||
|
// onSignInClick = { navController.navigate(AppScreen.SIGN_IN) },
|
||||||
|
// onGuestClick = { navController.navigate(AppScreen.CREATE_PROFILE) }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.SIGN_IN) {
|
||||||
|
// SignInScreen(
|
||||||
|
// onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||||
|
// onSignInClick = {
|
||||||
|
// navController.navigate(AppScreen.OTP)
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.SIGN_UP) {
|
||||||
|
// SignUpScreen(
|
||||||
|
// onSignUpClick = {
|
||||||
|
// navController.navigate(AppScreen.OTP)
|
||||||
|
// },
|
||||||
|
// onSignInClick = {
|
||||||
|
// navController.navigate(AppScreen.SIGN_IN) {
|
||||||
|
// popUpTo(AppScreen.SIGN_UP) { inclusive = true }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.OTP) {
|
||||||
|
// OtpScreen(
|
||||||
|
// onContinue = { navController.navigate(AppScreen.CREATE_PROFILE) }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// composable(AppScreen.CREATE_PROFILE) {
|
||||||
|
// CreateProfileScreen (
|
||||||
|
// onProfileSelected = { profileId ->
|
||||||
|
// if (profileId == "buyer_seller") {
|
||||||
|
// navController.navigate(AppScreen.BUY_ANIMALS)
|
||||||
|
// } else {
|
||||||
|
// navController.navigate(AppScreen.CHOOSE_SERVICE)
|
||||||
|
// } },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.CHOOSE_SERVICE) {
|
||||||
|
// ChooseServiceScreen (
|
||||||
|
// onServiceSelected = { navController.navigate(AppScreen.LANDING) },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.BUY_ANIMALS) {
|
||||||
|
// BuyScreen(
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onProductClick = { animalId ->
|
||||||
|
// navController.navigate(
|
||||||
|
// AppScreen.animalProfile(animalId)
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// onNavClick = onNavClick,
|
||||||
|
// onFilterClick = {navController.navigate(AppScreen.BUY_ANIMALS_FILTERS)},
|
||||||
|
// onSortClick = {navController.navigate(AppScreen.BUY_ANIMALS_SORT)},
|
||||||
|
// onSellerClick = { sellerId ->
|
||||||
|
// navController.navigate(
|
||||||
|
// AppScreen.sellerProfile(sellerId)
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||||
|
// FilterScreen(
|
||||||
|
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSkipClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onCancelClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||||
|
// SortScreen(
|
||||||
|
// onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSkipClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onCancelClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||||
|
// NewListingScreen (
|
||||||
|
// onSaveClick = {navController.navigate(
|
||||||
|
// AppScreen.postSaleSurvey("2")
|
||||||
|
// )},
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(
|
||||||
|
// route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||||
|
// arguments = listOf(
|
||||||
|
// navArgument("saleId") { type = NavType.StringType }
|
||||||
|
// )
|
||||||
|
// ) { backStackEntry ->
|
||||||
|
//
|
||||||
|
// val saleId = backStackEntry
|
||||||
|
// .arguments
|
||||||
|
// ?.getString("saleId")
|
||||||
|
// ?: return@composable
|
||||||
|
//
|
||||||
|
// SaleArchiveScreen(
|
||||||
|
// saleId = saleId,
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSaleSurveyClick = { saleId ->
|
||||||
|
// navController.navigate(
|
||||||
|
// AppScreen.sellerProfile(saleId)
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(
|
||||||
|
// route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||||
|
// arguments = listOf(
|
||||||
|
// navArgument("animalId") { type = NavType.StringType }
|
||||||
|
// )
|
||||||
|
// ) { backStackEntry ->
|
||||||
|
//
|
||||||
|
// val animalId = backStackEntry
|
||||||
|
// .arguments
|
||||||
|
// ?.getString("animalId")
|
||||||
|
// ?: return@composable
|
||||||
|
//
|
||||||
|
// PostSaleSurveyScreen (
|
||||||
|
// animalId = animalId,
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSubmit = {navController.navigate(
|
||||||
|
// AppScreen.saleArchive("2")
|
||||||
|
// )}
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(
|
||||||
|
// route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||||
|
// arguments = listOf(
|
||||||
|
// navArgument("animalId") { type = NavType.StringType }
|
||||||
|
// )
|
||||||
|
// ) { backStackEntry ->
|
||||||
|
//
|
||||||
|
// val animalId = backStackEntry
|
||||||
|
// .arguments
|
||||||
|
// ?.getString("animalId")
|
||||||
|
// ?: return@composable
|
||||||
|
//
|
||||||
|
// AnimalProfileScreen(
|
||||||
|
// animalId = animalId,
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// },
|
||||||
|
// onSellerClick = { sellerId ->
|
||||||
|
// navController.navigate(
|
||||||
|
// AppScreen.sellerProfile(sellerId)
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// composable(
|
||||||
|
// route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||||
|
// arguments = listOf(
|
||||||
|
// navArgument("sellerId") { type = NavType.StringType }
|
||||||
|
// )
|
||||||
|
// ) { backStackEntry ->
|
||||||
|
//
|
||||||
|
// val sellerId = backStackEntry
|
||||||
|
// .arguments
|
||||||
|
// ?.getString("sellerId")
|
||||||
|
// ?: return@composable
|
||||||
|
//
|
||||||
|
// SellerProfileScreen(
|
||||||
|
// sellerId = sellerId,
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// composable(AppScreen.SELLER_PROFILE) {
|
||||||
|
// SellerProfileScreen (
|
||||||
|
// onBackClick = {
|
||||||
|
// navController.popBackStack()
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
|
import androidx.compose.material.icons.filled.Construction
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
|
import com.example.livingai_lg.api.AuthManager
|
||||||
|
import com.example.livingai_lg.api.TokenManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AccountsScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
onApiTest: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||||
|
val apiClient = remember { AuthApiClient(context) }
|
||||||
|
|
||||||
|
// State for API test dialog
|
||||||
|
var showApiResultDialog by remember { mutableStateOf(false) }
|
||||||
|
var apiResultJson by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
// Top App Bar
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Accounts",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color(0xFFF7F4EE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 24.dp)
|
||||||
|
) {
|
||||||
|
// Logout option
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
scope.launch {
|
||||||
|
authManager.logout()
|
||||||
|
.onSuccess { _: com.example.livingai_lg.api.LogoutResponse ->
|
||||||
|
Toast.makeText(context, "Logged out successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
onLogout()
|
||||||
|
}
|
||||||
|
.onFailure { error: Throwable ->
|
||||||
|
Toast.makeText(context, "Logout failed: ${error.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
// Still call onLogout to navigate away even if API call fails
|
||||||
|
onLogout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = Color.White,
|
||||||
|
shadowElevation = 2.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||||
|
contentDescription = "Logout",
|
||||||
|
tint = Color(0xFFE53935),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Log Out",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFFE53935)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth().padding(vertical = 12.dp)
|
||||||
|
.clickable {
|
||||||
|
// Call API test directly instead of navigating
|
||||||
|
scope.launch {
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
// First get current user's ID
|
||||||
|
val userDetails = apiClient.getUserDetails().getOrNull()
|
||||||
|
if (userDetails != null) {
|
||||||
|
// Call GET /users/:userId on port 3200
|
||||||
|
val result = apiClient.getUserById(userDetails.id, "http://10.0.2.2:3200")
|
||||||
|
result.onSuccess { jsonString ->
|
||||||
|
apiResultJson = jsonString
|
||||||
|
showApiResultDialog = true
|
||||||
|
isLoading = false
|
||||||
|
}.onFailure { error ->
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"API Test Failed: ${error.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Failed to get user details",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Error: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = Color.White,
|
||||||
|
shadowElevation = 2.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Construction,
|
||||||
|
contentDescription = "Api test",
|
||||||
|
tint = Color.Gray,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = if (isLoading) "Testing API..." else "Test API",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Result Dialog
|
||||||
|
if (showApiResultDialog) {
|
||||||
|
Dialog(onDismissRequest = { showApiResultDialog = false }) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.8f)
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = Color.White
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// Dialog Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "API Test Result",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
IconButton(onClick = { showApiResultDialog = false }) {
|
||||||
|
Text(
|
||||||
|
text = "✕",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// JSON Content
|
||||||
|
apiResultJson?.let { json ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = json,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Black,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No result",
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close Button
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Button(
|
||||||
|
onClick = { showApiResultDialog = false },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,480 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import com.example.livingai_lg.ui.components.ImageCarousel
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material.icons.filled.StarHalf
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import com.example.livingai_lg.ui.models.Animal
|
||||||
|
import com.example.livingai_lg.ui.utils.formatPrice
|
||||||
|
import com.example.livingai_lg.ui.utils.formatViews
|
||||||
|
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||||
|
import com.example.livingai_lg.ui.components.FloatingActionBar
|
||||||
|
import com.example.livingai_lg.ui.models.sampleAnimals
|
||||||
|
import com.example.livingai_lg.ui.utils.formatAge
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
import com.example.livingai_lg.ui.components.ActionPopup
|
||||||
|
import com.example.livingai_lg.ui.components.MediaFullscreenOverlay
|
||||||
|
import com.example.livingai_lg.ui.components.RatingStars
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
import com.example.livingai_lg.ui.utils.formatDistance
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimalProfileScreen(
|
||||||
|
animalId: String,
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSellerClick: (sellerId: String) -> Unit = {},
|
||||||
|
onNavClick: (route: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
var showSavedPopup by remember { mutableStateOf(false) }
|
||||||
|
var showMediaOverlay by remember { mutableStateOf(false) }
|
||||||
|
var mediaOverlayStartIndex by remember { mutableStateOf(0) }
|
||||||
|
val animal = sampleAnimals.find { animal -> animal.id == animalId } ?: Animal(id = "null")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// Photo section with overlays
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(350.dp)
|
||||||
|
.shadow(4.dp)
|
||||||
|
) {
|
||||||
|
// Main image
|
||||||
|
val product = null
|
||||||
|
ImageCarousel(
|
||||||
|
media = animal.media ?: emptyList(),
|
||||||
|
enableFullscreenPreview = true,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onMediaClick = { startIndex ->
|
||||||
|
showMediaOverlay = true
|
||||||
|
mediaOverlayStartIndex = startIndex
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gradient overlay at bottom
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBackIos,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(5.dp)
|
||||||
|
.size(36.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = CircleShape,
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.35f),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Views indicator (top left)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(end = 5.dp, top = 5.dp)
|
||||||
|
.shadow(
|
||||||
|
elevation = 6.dp,
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.35f), // 👈 light but effective
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 6.dp, vertical = 3.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_view),
|
||||||
|
contentDescription = "Views",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = formatViews(animal.views),
|
||||||
|
fontSize = AppTypography.BodySmall,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animal info (centered bottom)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 68.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${animal.name}, ${formatAge(animal.age)}",
|
||||||
|
fontSize = AppTypography.Title,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.White,
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black.copy(alpha = 0.75f),
|
||||||
|
offset = Offset(0f, 2f),
|
||||||
|
blurRadius = 6f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${animal.breed} • ${animal.location}, ${formatDistance(animal.distance)}".uppercase(),
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
letterSpacing = 1.2.sp,
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black.copy(alpha = 0.6f),
|
||||||
|
offset = Offset(0f, 1.5f),
|
||||||
|
blurRadius = 4f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI Score badge (bottom left)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(start = 5.dp, bottom = 5.dp)
|
||||||
|
.height(60.dp)
|
||||||
|
.border(2.dp, Color(0xFF717182), CircleShape)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AIScoreCircle(score = animal.aiScore ?: 0f)
|
||||||
|
Text(
|
||||||
|
text = "AI Score",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display location (bottom right)
|
||||||
|
Text(
|
||||||
|
text = buildString {
|
||||||
|
append("At Display: ")
|
||||||
|
append(animal.displayLocation)
|
||||||
|
},
|
||||||
|
fontSize = AppTypography.Caption,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
lineHeight = 13.sp,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
textAlign = TextAlign.Right,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 16.dp, bottom = 8.dp)
|
||||||
|
.width(98.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info card section
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 36.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
// Price and seller info
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatPrice(animal.price),
|
||||||
|
fontSize = AppTypography.Title,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (animal.isFairPrice ?: false) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(start = 0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_thumbs_up),
|
||||||
|
contentDescription = "Fair Price",
|
||||||
|
tint = Color(0xFF00C950),
|
||||||
|
modifier = Modifier.size(15.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Fair Price",
|
||||||
|
fontSize = AppTypography.Caption,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF00C950)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
onSellerClick(animal.sellerId?:"")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = "Sold By: ${animal.sellerName}",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Star rating
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RatingStars(
|
||||||
|
rating = animal.rating ?: 0f,
|
||||||
|
starSize = 12.dp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${animal.rating} (${animal.ratingCount} Ratings)",
|
||||||
|
fontSize = AppTypography.Caption,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// About section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "About",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF09090B).copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = animal.description ?: "",
|
||||||
|
fontSize = AppTypography.BodySmall,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF09090B),
|
||||||
|
lineHeight = 24.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// FloatingActionBar(
|
||||||
|
// modifier = Modifier,
|
||||||
|
// // .offset(y = (-40).dp), // 👈 hover effect
|
||||||
|
// onChatClick = { /* TODO */ },
|
||||||
|
// onCallClick = { /* TODO */ },
|
||||||
|
// onLocationClick = { /* TODO */ },
|
||||||
|
// onBookmarkClick = { /* TODO */ }
|
||||||
|
// )
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Ad space banner
|
||||||
|
AdSpaceBanner()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FloatingActionBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(horizontal = 16.dp,
|
||||||
|
16.dp)
|
||||||
|
.offset(y = (-10).dp)
|
||||||
|
.zIndex(10f), // 👈 ensure it floats above everything
|
||||||
|
onChatClick = { /* TODO */ },
|
||||||
|
onCallClick = { /* TODO */ },
|
||||||
|
onLocationClick = { /* TODO */ },
|
||||||
|
onBookmarkClick = { showSavedPopup = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
ActionPopup(
|
||||||
|
visible = showSavedPopup,
|
||||||
|
text = "Saved",
|
||||||
|
icon = Icons.Default.Bookmark,
|
||||||
|
backgroundColor = Color.Black,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 16.dp, bottom = 96.dp),
|
||||||
|
onClick = {
|
||||||
|
onNavClick(AppScreen.SAVED_LISTINGS)
|
||||||
|
// Navigate to saved items
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showSavedPopup = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMediaOverlay) {
|
||||||
|
MediaFullscreenOverlay(
|
||||||
|
media = animal.media?: emptyList(),
|
||||||
|
startIndex = mediaOverlayStartIndex,
|
||||||
|
onClose = { showMediaOverlay = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.Companion.align(bottomEnd: Alignment) {}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AIScoreCircle(score: Float) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.drawBehind {
|
||||||
|
// Background circle
|
||||||
|
drawCircle(
|
||||||
|
color = Color(0xFFDD88CF),
|
||||||
|
radius = size.minDimension / 2,
|
||||||
|
alpha = 0.3f,
|
||||||
|
style = Stroke(width = 4.dp.toPx())
|
||||||
|
)
|
||||||
|
|
||||||
|
// Progress arc
|
||||||
|
val sweepAngle = 360f * score
|
||||||
|
drawArc(
|
||||||
|
color = Color(0xFF9AFF9A),
|
||||||
|
startAngle = -90f,
|
||||||
|
sweepAngle = sweepAngle,
|
||||||
|
useCenter = false,
|
||||||
|
style = Stroke(width = 4.dp.toPx(), cap = StrokeCap.Round),
|
||||||
|
size = Size(size.width, size.height),
|
||||||
|
topLeft = Offset.Zero
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${(score * 100).toInt()}%",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionButtonLarge(icon: Int, label: String) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.shadow(8.dp, CircleShape)
|
||||||
|
.background(Color.White, CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = label,
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.Indication
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.room.util.copy
|
||||||
|
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||||
|
import com.example.livingai_lg.ui.components.AnimalTypeSelector
|
||||||
|
import com.example.livingai_lg.ui.components.BuyAnimalCard
|
||||||
|
import com.example.livingai_lg.ui.models.animalTypes
|
||||||
|
|
||||||
|
import com.example.livingai_lg.ui.components.FilterButton
|
||||||
|
import com.example.livingai_lg.ui.components.SortButton
|
||||||
|
import com.example.livingai_lg.ui.components.UserLocationHeader
|
||||||
|
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||||
|
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||||
|
import com.example.livingai_lg.ui.models.sampleAnimals
|
||||||
|
import com.example.livingai_lg.ui.models.userProfile
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
import com.example.livingai_lg.ui.components.ActionPopup
|
||||||
|
import com.example.livingai_lg.ui.components.AddressSelectorOverlay
|
||||||
|
import com.example.livingai_lg.ui.components.FilterOverlay
|
||||||
|
import com.example.livingai_lg.ui.components.InfoOverlay
|
||||||
|
import com.example.livingai_lg.ui.components.NotificationsOverlay
|
||||||
|
import com.example.livingai_lg.ui.components.SortOverlay
|
||||||
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
|
import com.example.livingai_lg.ui.models.TextFilter
|
||||||
|
import com.example.livingai_lg.ui.models.isDefault
|
||||||
|
import com.example.livingai_lg.ui.models.sampleNotifications
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
import com.example.livingai_lg.ui.state.FilterStore
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BuyScreen(
|
||||||
|
onProductClick: (productId: String) -> Unit = {},
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onNavClick: (route: String) -> Unit = {},
|
||||||
|
onFilterClick: () -> Unit = {},
|
||||||
|
onSortClick: () -> Unit = {},
|
||||||
|
onSellerClick: (sellerId: String) -> Unit = {},
|
||||||
|
onWishlistClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var activeFilters by remember {
|
||||||
|
mutableStateOf(FilterStore.filters.value)
|
||||||
|
}
|
||||||
|
val isSaved = remember { mutableStateOf(false) }
|
||||||
|
var showAddressSelector by remember { mutableStateOf(false) }
|
||||||
|
var selectedAddressId by remember { mutableStateOf<String?>(userProfile.addresses.find { address -> address.isPrimary }?.id) }
|
||||||
|
val showFilterOverlay = remember { mutableStateOf(false) }
|
||||||
|
val showSortOverlay = remember { mutableStateOf(false) }
|
||||||
|
var showSavedPopup by remember { mutableStateOf(false) }
|
||||||
|
var showNotifications by remember { mutableStateOf(false) }
|
||||||
|
val sampleNotifications = sampleNotifications
|
||||||
|
var showInfoOverlay by remember { mutableStateOf(false) }
|
||||||
|
var selectedItemId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
|
||||||
|
BottomNavScaffold(
|
||||||
|
items = mainBottomNavItems,
|
||||||
|
currentItem = "Home",
|
||||||
|
onBottomNavItemClick = onNavClick,
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
// Header with profile and notification
|
||||||
|
// Top header strip
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
UserLocationHeader(
|
||||||
|
user = userProfile,
|
||||||
|
onOpenAddressOverlay = { showAddressSelector = true },
|
||||||
|
selectedAddressId = selectedAddressId?:"1",
|
||||||
|
onProfileClick = {
|
||||||
|
onNavClick(AppScreen.ACCOUNTS)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Row{
|
||||||
|
// Right-side actions (notifications, etc.)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.FavoriteBorder,
|
||||||
|
contentDescription = "Wishlist",
|
||||||
|
tint = Color.Black,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
){ onWishlistClick() }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Icon(
|
||||||
|
|
||||||
|
painter = painterResource(R.drawable.ic_notification_unread),
|
||||||
|
contentDescription = "Notifications",
|
||||||
|
tint = Color.Black,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
){ showNotifications = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// Row(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxWidth()
|
||||||
|
// .padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
// horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
// verticalAlignment = Alignment.CenterVertically
|
||||||
|
// ) {
|
||||||
|
// UserLocationHeader(
|
||||||
|
// user = userProfile,
|
||||||
|
// modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Icon(
|
||||||
|
// painter = painterResource(R.drawable.ic_notification_unread),
|
||||||
|
// contentDescription = "Notifications",
|
||||||
|
// tint = Color.Black,
|
||||||
|
// modifier = Modifier.size(24.dp)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Animal type filter buttons
|
||||||
|
AnimalTypeSelector(
|
||||||
|
animalTypes = animalTypes,
|
||||||
|
selectedAnimalType = activeFilters.animal.value,
|
||||||
|
onAnimalTypeSelected = { animal->
|
||||||
|
activeFilters = activeFilters.copy(
|
||||||
|
animal = TextFilter(
|
||||||
|
value = animal,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ad space banner
|
||||||
|
AdSpaceBanner(
|
||||||
|
modifier = Modifier.padding(horizontal = 22.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|
||||||
|
// Sort and Filter buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 22.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
SortButton(
|
||||||
|
onClick = { showSortOverlay.value = true }
|
||||||
|
)
|
||||||
|
FilterButton(onClick = { showFilterOverlay.value = true }, hasActiveFilters = !activeFilters.isDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleAnimals.forEach { animal ->
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Product card
|
||||||
|
BuyAnimalCard(
|
||||||
|
product = animal,
|
||||||
|
isSaved = isSaved.value,
|
||||||
|
onSavedChange = { isSaved.value = it },
|
||||||
|
onProductClick = { onProductClick(animal.id)},
|
||||||
|
onSellerClick = onSellerClick,
|
||||||
|
onBookmarkClick = { showSavedPopup = true },
|
||||||
|
onInfoClick = { showInfoOverlay = true
|
||||||
|
selectedItemId=animal.id},
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Ad space banner at bottom
|
||||||
|
AdSpaceBanner(
|
||||||
|
modifier = Modifier.padding(horizontal = 22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddressSelectorOverlay(
|
||||||
|
visible = showAddressSelector,
|
||||||
|
addresses = userProfile.addresses,
|
||||||
|
selectedAddressId = selectedAddressId,
|
||||||
|
onSelect = { addressId ->
|
||||||
|
selectedAddressId = addressId
|
||||||
|
showAddressSelector = false
|
||||||
|
},
|
||||||
|
onClose = { showAddressSelector = false }
|
||||||
|
)
|
||||||
|
|
||||||
|
SortOverlay(
|
||||||
|
visible = showSortOverlay.value,
|
||||||
|
onApplyClick = { selected ->
|
||||||
|
// TODO: apply sort
|
||||||
|
},
|
||||||
|
onDismiss = { showSortOverlay.value = false }
|
||||||
|
)
|
||||||
|
|
||||||
|
FilterOverlay(
|
||||||
|
appliedFilters = activeFilters,
|
||||||
|
visible = showFilterOverlay.value,
|
||||||
|
onDismiss = { showFilterOverlay.value = false },
|
||||||
|
onSubmitClick = { filters ->
|
||||||
|
Log.d("Filters", "$filters")
|
||||||
|
activeFilters = filters
|
||||||
|
// apply filters
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val animal = sampleAnimals.find { it.id == selectedItemId }
|
||||||
|
|
||||||
|
InfoOverlay(
|
||||||
|
visible = showInfoOverlay,
|
||||||
|
title = animal?.breed ?:"",
|
||||||
|
text = animal?.breedInfo ?: "",
|
||||||
|
onDismiss = { showInfoOverlay = false }
|
||||||
|
)
|
||||||
|
|
||||||
|
ActionPopup(
|
||||||
|
visible = showSavedPopup,
|
||||||
|
text = "Saved",
|
||||||
|
icon = Icons.Default.Bookmark,
|
||||||
|
backgroundColor = Color.Black,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 16.dp, bottom = 96.dp),
|
||||||
|
onClick = {
|
||||||
|
onNavClick(AppScreen.SAVED_LISTINGS)
|
||||||
|
// Navigate to saved items
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showSavedPopup = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
NotificationsOverlay(
|
||||||
|
visible = showNotifications,
|
||||||
|
notifications = sampleNotifications,
|
||||||
|
onClose = { showNotifications = false },
|
||||||
|
onDismiss = { id ->
|
||||||
|
// remove notification from list
|
||||||
|
},
|
||||||
|
onNotificationClick = { id ->
|
||||||
|
// optional navigation
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.RadioButtonDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.components.OptionCard
|
||||||
|
import com.example.livingai_lg.ui.components.backgrounds.StoreBackground
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
data class ServiceType(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val icon: Any,
|
||||||
|
val iconTint: Color,
|
||||||
|
val backgroundColor: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChooseServiceScreen(
|
||||||
|
profileId: String? = null,
|
||||||
|
onServiceSelected: (serviceType: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val selectedService = remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val serviceTypes = listOf(
|
||||||
|
ServiceType(
|
||||||
|
id = "transport",
|
||||||
|
title = "Transport",
|
||||||
|
icon = R.drawable.ic_shop,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFF9D4EDD)
|
||||||
|
),
|
||||||
|
ServiceType(
|
||||||
|
id = "vet",
|
||||||
|
title = "Vet",
|
||||||
|
icon = R.drawable.ic_bag,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFF3A86FF)
|
||||||
|
),
|
||||||
|
ServiceType(
|
||||||
|
id = "feed_supplier",
|
||||||
|
title = "Feed Supplier",
|
||||||
|
icon = R.drawable.ic_spanner,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFFFF5722)
|
||||||
|
),
|
||||||
|
ServiceType(
|
||||||
|
id = "medicine_supplier",
|
||||||
|
title = "Medicine Supplier",
|
||||||
|
icon = R.drawable.ic_shop2,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFF4CAF50)
|
||||||
|
),
|
||||||
|
ServiceType(
|
||||||
|
id = "other",
|
||||||
|
title = "Other",
|
||||||
|
icon = R.drawable.ic_other,
|
||||||
|
iconTint = Color.White,
|
||||||
|
backgroundColor = Color(0xFFD4A942)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF5F1E8))
|
||||||
|
) {
|
||||||
|
// Decorative background
|
||||||
|
StoreBackground()
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Choose Service",
|
||||||
|
fontSize = AppTypography.Display,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Choose a Service",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A).copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
// Service selection cards
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
serviceTypes.forEach { service ->
|
||||||
|
OptionCard(
|
||||||
|
label = service.title,
|
||||||
|
icon = service.icon,
|
||||||
|
iconTint = service.iconTint,
|
||||||
|
iconBackgroundColor = service.backgroundColor,
|
||||||
|
onClick = {
|
||||||
|
selectedService.value = service.id
|
||||||
|
onServiceSelected(service.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
|
import com.example.livingai_lg.api.AuthManager
|
||||||
|
import com.example.livingai_lg.api.TokenManager
|
||||||
|
import com.example.livingai_lg.ui.components.OptionCard
|
||||||
|
|
||||||
|
import com.example.livingai_lg.ui.components.backgrounds.StoreBackground
|
||||||
|
import com.example.livingai_lg.ui.models.profileTypes
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CreateProfileScreen(
|
||||||
|
name: String,
|
||||||
|
onProfileSelected: (profileId: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val selectedProfile = remember { mutableStateOf<String>(profileTypes[0].id) }
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||||
|
|
||||||
|
fun updateProfile(profileId: String) {
|
||||||
|
scope.launch {
|
||||||
|
authManager.updateProfile(name, profileId)
|
||||||
|
.onSuccess {
|
||||||
|
onProfileSelected(profileId)
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Toast.makeText(context, "Failed to update profile: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF5F1E8))
|
||||||
|
) {
|
||||||
|
// Decorative background
|
||||||
|
StoreBackground()
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Create Profile",
|
||||||
|
fontSize = AppTypography.Display,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Choose Profile Type",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A).copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
// Profile selection cards
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
profileTypes.forEach { profile ->
|
||||||
|
OptionCard(
|
||||||
|
label = profile.title,
|
||||||
|
icon = profile.icon,
|
||||||
|
iconBackgroundColor = profile.backgroundColor,
|
||||||
|
onClick = {
|
||||||
|
onProfileSelected(profile.id)
|
||||||
|
//updateProfile(profile.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,566 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.components.DropdownInput
|
||||||
|
import com.example.livingai_lg.ui.components.RangeFilter
|
||||||
|
import com.example.livingai_lg.ui.components.WishlistNameOverlay
|
||||||
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
|
import com.example.livingai_lg.ui.models.RangeFilterState
|
||||||
|
import com.example.livingai_lg.ui.models.TextFilter
|
||||||
|
import com.example.livingai_lg.ui.state.WishlistEntry
|
||||||
|
import com.example.livingai_lg.ui.state.WishlistStore
|
||||||
|
import com.example.livingai_lg.ui.models.isDefault
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FilterScreen(
|
||||||
|
appliedFilters: FiltersState,
|
||||||
|
wishlistEditMode: Boolean = false,
|
||||||
|
onSubmitClick: (FiltersState) -> Unit,
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onCancelClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
var filters by remember {
|
||||||
|
mutableStateOf(appliedFilters)
|
||||||
|
}
|
||||||
|
var showWishlistOverlay by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var selectedAnimal =filters.animal
|
||||||
|
var selectedBreed = filters.breed
|
||||||
|
var selectedDistance = filters.distance
|
||||||
|
var selectedGender = filters.gender
|
||||||
|
|
||||||
|
var animalExpanded by remember { mutableStateOf(false) }
|
||||||
|
var breedExpanded by remember { mutableStateOf(false) }
|
||||||
|
var distanceExpanded by remember { mutableStateOf(false) }
|
||||||
|
var genderExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var price =filters.price
|
||||||
|
var age = filters.age
|
||||||
|
var weight = filters.weight
|
||||||
|
var milkYield = filters.milkYield
|
||||||
|
var calving = filters.calving
|
||||||
|
|
||||||
|
var selectedPregnancyStatus = filters.pregnancyStatuses
|
||||||
|
|
||||||
|
|
||||||
|
var calvingFromExpanded by remember { mutableStateOf(false) }
|
||||||
|
var calvingToExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val maxCalving = 10
|
||||||
|
|
||||||
|
val calvingFromOptions = (0..maxCalving).map { it.toString() }
|
||||||
|
val calvingToOptions = (calving.min..maxCalving).map { it.toString() }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Filters",
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
|
||||||
|
if(!wishlistEditMode){
|
||||||
|
IconButton(
|
||||||
|
|
||||||
|
onClick = {
|
||||||
|
if (!filters.isDefault()) {
|
||||||
|
showWishlistOverlay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.FavoriteBorder,
|
||||||
|
contentDescription = "Add to Wishlist",
|
||||||
|
tint = if(!filters.isDefault()) Color.Black else Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollable Content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 32.dp)
|
||||||
|
.padding(bottom = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
// Animal Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
DropdownInput(
|
||||||
|
label = "Animal",
|
||||||
|
selected = if (selectedAnimal.filterSet) selectedAnimal.value else "",
|
||||||
|
options = listOf("Cow", "Buffalo", "Goat", "Sheep"),
|
||||||
|
expanded = animalExpanded,
|
||||||
|
onExpandedChange = { animalExpanded = it },
|
||||||
|
onSelect = { item ->
|
||||||
|
filters = filters.copy(
|
||||||
|
animal = TextFilter(
|
||||||
|
value = item,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
animalExpanded = false
|
||||||
|
},
|
||||||
|
placeholder = "Select Animal", // <--- half width
|
||||||
|
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breed Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
DropdownInput(
|
||||||
|
label = "Breed",
|
||||||
|
selected = if (selectedBreed.filterSet) selectedBreed.value else "",
|
||||||
|
options = listOf("Holstein", "Jersey", "Gir", "Sahiwal"),
|
||||||
|
expanded = breedExpanded,
|
||||||
|
onExpandedChange = { breedExpanded = it },
|
||||||
|
onSelect = { item ->
|
||||||
|
filters = filters.copy(
|
||||||
|
breed = TextFilter(
|
||||||
|
value = item,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
breedExpanded = false
|
||||||
|
},
|
||||||
|
placeholder = "Select Breed", // <--- half width
|
||||||
|
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price and Age Row
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|
||||||
|
) {
|
||||||
|
RangeFilter(
|
||||||
|
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||||
|
|
||||||
|
label = "Price",
|
||||||
|
min = 0,
|
||||||
|
max = 90_000,
|
||||||
|
valueFrom = price.min,
|
||||||
|
valueTo = price.max,
|
||||||
|
modified = price.filterSet,
|
||||||
|
onValueChange = { from, to ->
|
||||||
|
filters = filters.copy(
|
||||||
|
price = RangeFilterState(
|
||||||
|
min = from,
|
||||||
|
max = to,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|
||||||
|
) {
|
||||||
|
RangeFilter(
|
||||||
|
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||||
|
|
||||||
|
label = "Age (years)",
|
||||||
|
min = 0,
|
||||||
|
max = 20,
|
||||||
|
valueFrom = age.min,
|
||||||
|
valueTo = age.max,
|
||||||
|
showSlider = false,
|
||||||
|
modified = age.filterSet,
|
||||||
|
onValueChange = { from, to ->
|
||||||
|
filters = filters.copy(
|
||||||
|
age = RangeFilterState(
|
||||||
|
min = from,
|
||||||
|
max = to,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Distance and Gender Row
|
||||||
|
|
||||||
|
// Distance Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
DropdownInput(
|
||||||
|
label = "Distance",
|
||||||
|
selected = if (selectedDistance.filterSet) selectedDistance.value else "",
|
||||||
|
options = listOf("0-5 km", "5-10 km", "10-20 km", "20+ km"),
|
||||||
|
expanded = distanceExpanded,
|
||||||
|
onExpandedChange = { distanceExpanded = it },
|
||||||
|
onSelect = { item ->
|
||||||
|
filters = filters.copy(
|
||||||
|
distance = TextFilter(
|
||||||
|
value = item,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
distanceExpanded = false
|
||||||
|
},
|
||||||
|
placeholder = "Choose Distance", // <--- half width
|
||||||
|
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gender Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
DropdownInput(
|
||||||
|
label = "Gender",
|
||||||
|
selected = if (selectedGender.filterSet) selectedGender.value else "",
|
||||||
|
options = listOf("Male", "Female"),
|
||||||
|
expanded = genderExpanded,
|
||||||
|
onExpandedChange = { genderExpanded = it },
|
||||||
|
onSelect = { item ->
|
||||||
|
filters = filters.copy(
|
||||||
|
gender = TextFilter(
|
||||||
|
value = item,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
genderExpanded = false
|
||||||
|
},
|
||||||
|
placeholder = "Choose Gender", // <--- half width
|
||||||
|
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Pregnancy Status Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Pregnancy Status",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color(0xFF364153)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(20.dp)
|
||||||
|
) {
|
||||||
|
listOf("Pregnant", "Calved", "Option").forEach { status ->
|
||||||
|
PregnancyStatusChip(
|
||||||
|
label = status,
|
||||||
|
isSelected = selectedPregnancyStatus.contains(status),
|
||||||
|
onClick = {
|
||||||
|
filters = filters.copy(
|
||||||
|
pregnancyStatuses =
|
||||||
|
if (filters.pregnancyStatuses.contains(status))
|
||||||
|
filters.pregnancyStatuses - status
|
||||||
|
else
|
||||||
|
filters.pregnancyStatuses + status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
RangeFilter(
|
||||||
|
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||||
|
|
||||||
|
label = "Weight",
|
||||||
|
min = 0,
|
||||||
|
max = 9000,
|
||||||
|
valueFrom = weight.min,
|
||||||
|
valueTo = weight.max,
|
||||||
|
modified = weight.filterSet,
|
||||||
|
onValueChange = { from, to ->
|
||||||
|
filters = filters.copy(
|
||||||
|
weight = RangeFilterState(
|
||||||
|
min = from,
|
||||||
|
max = to,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
RangeFilter(
|
||||||
|
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||||
|
|
||||||
|
label = "Milk Yield",
|
||||||
|
min = 0,
|
||||||
|
max = 900,
|
||||||
|
valueFrom = milkYield.min,
|
||||||
|
valueTo = milkYield.max,
|
||||||
|
modified = milkYield.filterSet,
|
||||||
|
onValueChange = { from, to ->
|
||||||
|
filters = filters.copy(
|
||||||
|
milkYield = RangeFilterState(
|
||||||
|
min = from,
|
||||||
|
max = to,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Calving Number Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
// FROM
|
||||||
|
DropdownInput(
|
||||||
|
label = "Calving Number",
|
||||||
|
selected = calving.min.toString(),
|
||||||
|
options = calvingFromOptions,
|
||||||
|
expanded = calvingFromExpanded,
|
||||||
|
onExpandedChange = { calvingFromExpanded = it },
|
||||||
|
onSelect = { value ->
|
||||||
|
val newFrom = value.toInt()
|
||||||
|
val newTo = maxOf(calving.max, newFrom)
|
||||||
|
|
||||||
|
filters = filters.copy(
|
||||||
|
calving = RangeFilterState(
|
||||||
|
min = newFrom,
|
||||||
|
max = newTo,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
calvingFromExpanded = false
|
||||||
|
},
|
||||||
|
placeholder = "From",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "to",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
|
||||||
|
// TO
|
||||||
|
DropdownInput(
|
||||||
|
selected = calving.max.toString(),
|
||||||
|
options = calvingToOptions, // 👈 constrained options
|
||||||
|
expanded = calvingToExpanded,
|
||||||
|
onExpandedChange = { calvingToExpanded = it },
|
||||||
|
onSelect = { value ->
|
||||||
|
val newTo = value.toInt()
|
||||||
|
val newFrom = minOf(calving.min, newTo)
|
||||||
|
|
||||||
|
filters = filters.copy(
|
||||||
|
calving = RangeFilterState(
|
||||||
|
min = newFrom,
|
||||||
|
max = newTo,
|
||||||
|
filterSet = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
calvingToExpanded = false
|
||||||
|
},
|
||||||
|
placeholder = "To",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Submit Button
|
||||||
|
Button(
|
||||||
|
onClick = { onSubmitClick(filters) },
|
||||||
|
modifier = Modifier
|
||||||
|
.width(173.dp)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(25.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Black,
|
||||||
|
contentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Submit",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel Button
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onCancelClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(173.dp)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(25.dp),
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
Color(0x1A000000)
|
||||||
|
),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Cancel",
|
||||||
|
fontSize = AppTypography.Body,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showWishlistOverlay) {
|
||||||
|
WishlistNameOverlay(
|
||||||
|
onDismiss = { showWishlistOverlay = false },
|
||||||
|
onSave = { name ->
|
||||||
|
WishlistStore.add(
|
||||||
|
WishlistEntry(
|
||||||
|
name = name,
|
||||||
|
filters = filters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
showWishlistOverlay = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PregnancyStatusChip(
|
||||||
|
label: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(29.dp)
|
||||||
|
.widthIn(min = 76.dp)
|
||||||
|
.background(
|
||||||
|
if (isSelected) Color(0xFF007BFF) else Color.Transparent,
|
||||||
|
RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
Color(0xFFE5E7EB),
|
||||||
|
RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = if (isSelected) Color(0xFFF4F4F4) else Color.Black,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "Selected",
|
||||||
|
tint = Color(0xFFE3E3E3),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,474 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.components.MediaPickerCard
|
||||||
|
import com.example.livingai_lg.ui.models.NewListingFormState
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun NewListingScreen(
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSaveClick: () -> Unit = {},
|
||||||
|
formState: NewListingFormState = remember { NewListingFormState() }
|
||||||
|
) {
|
||||||
|
var name by formState.name
|
||||||
|
var animal by formState.animal
|
||||||
|
var breed by formState.breed
|
||||||
|
var age by formState.age
|
||||||
|
var milkYield by formState.milkYield
|
||||||
|
var calvingNumber by formState.calvingNumber
|
||||||
|
var reproductiveStatus by formState.reproductiveStatus
|
||||||
|
var description by formState.description
|
||||||
|
|
||||||
|
var animalExpanded by formState.animalExpanded
|
||||||
|
var breedExpanded by formState.breedExpanded
|
||||||
|
|
||||||
|
val mediaUploads = formState.mediaUploads
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "New Listing",
|
||||||
|
fontSize = AppTypography.Display,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color(0xFFF7F4EE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Form Content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Name Input
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Name",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Animal and Breed Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Animal Dropdown
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp)
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = animalExpanded,
|
||||||
|
onExpandedChange = { animalExpanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = animal,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Animal",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = "Dropdown",
|
||||||
|
tint = Color(0xFFA5A5A5)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = animalExpanded,
|
||||||
|
onDismissRequest = { animalExpanded = false }
|
||||||
|
) {
|
||||||
|
listOf("Cow", "Buffalo", "Goat", "Sheep").forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(option) },
|
||||||
|
onClick = {
|
||||||
|
animal = option
|
||||||
|
animalExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breed Dropdown
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp)
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = breedExpanded,
|
||||||
|
onExpandedChange = { breedExpanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = breed,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Breed",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = "Dropdown",
|
||||||
|
tint = Color(0xFFA5A5A5)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = breedExpanded,
|
||||||
|
onDismissRequest = { breedExpanded = false }
|
||||||
|
) {
|
||||||
|
listOf("Holstein", "Jersey", "Gir", "Sahiwal").forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(option) },
|
||||||
|
onClick = {
|
||||||
|
breed = option
|
||||||
|
breedExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Age and Milk Yield Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = age,
|
||||||
|
onValueChange = { age = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Age (Years)",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowDropDown,
|
||||||
|
contentDescription = "Sort",
|
||||||
|
tint = Color(0xFF353535)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = milkYield,
|
||||||
|
onValueChange = { milkYield = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Average Milk Yield (L)",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calving Number
|
||||||
|
OutlinedTextField(
|
||||||
|
value = calvingNumber,
|
||||||
|
onValueChange = { calvingNumber = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Calving Number",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reproductive Status
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Reproductive Status",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
listOf("Pregnant", "Calved", "None").forEach { status ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
){
|
||||||
|
reproductiveStatus = status
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = reproductiveStatus == status,
|
||||||
|
onClick = { reproductiveStatus = status },
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = Color(0xFF717182),
|
||||||
|
unselectedColor = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = status,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"Description",
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(74.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x1A000000)
|
||||||
|
),
|
||||||
|
maxLines = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upload Media Section
|
||||||
|
Text(
|
||||||
|
text = "Upload Media",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color(0xFF000000),
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Photo Uploads Grid
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
mediaUploads.chunked(2).forEach { rowItems ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
rowItems.forEach { upload ->
|
||||||
|
MediaPickerCard(
|
||||||
|
upload = upload,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onUriSelected = { uri ->
|
||||||
|
upload.uri = uri
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Fill empty space if odd number of items
|
||||||
|
if (rowItems.size == 1) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Cancel Button
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onBackClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(25.dp),
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
Color(0x1A000000)
|
||||||
|
),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Cancel",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Profile Button
|
||||||
|
Button(
|
||||||
|
onClick = onSaveClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(50.dp),
|
||||||
|
shape = RoundedCornerShape(25.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Black,
|
||||||
|
contentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Save Profile",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.AppNotification
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationsScreen(
|
||||||
|
notifications: List<AppNotification>,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onDismiss: (String) -> Unit,
|
||||||
|
onNotificationClick: (String) -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Notifications",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(color = Color(0xFFE5E7EB))
|
||||||
|
|
||||||
|
if (notifications.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No notifications",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF6B7280)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = notifications,
|
||||||
|
key = { it.id }
|
||||||
|
) { notification ->
|
||||||
|
NotificationItem(
|
||||||
|
notification = notification,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
onClick = onNotificationClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationItem(
|
||||||
|
notification: AppNotification,
|
||||||
|
onDismiss: (String) -> Unit,
|
||||||
|
onClick: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.White, RoundedCornerShape(14.dp))
|
||||||
|
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(14.dp))
|
||||||
|
.clickable { onClick(notification.id) }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = notification.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = notification.title,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF1F2937),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { onDismiss(notification.id) },
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Dismiss",
|
||||||
|
tint = Color(0xFF6B7280),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,511 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.filled.CalendarToday
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
data class SaleSurveyData(
|
||||||
|
val buyerName: String = "",
|
||||||
|
val salePrice: String = "",
|
||||||
|
val saleDate: String = "",
|
||||||
|
val saleLocation: String = "",
|
||||||
|
val notes: String = "",
|
||||||
|
val attachments: List<SurveyAttachment> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SurveyAttachment(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val type: SurveyAttachmentType
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class SurveyAttachmentType {
|
||||||
|
DOCUMENT,
|
||||||
|
PHOTO,
|
||||||
|
ADD_NEW
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PostSaleSurveyScreen(
|
||||||
|
animalId: String? = null,
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSkipClick: () -> Unit = {},
|
||||||
|
onSubmit: (SaleSurveyData) -> Unit = {},
|
||||||
|
onCancel: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var surveyData by remember { mutableStateOf(SaleSurveyData()) }
|
||||||
|
var buyerNameExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val defaultAttachments = listOf(
|
||||||
|
SurveyAttachment("1", "Sale Papers 1", SurveyAttachmentType.DOCUMENT),
|
||||||
|
SurveyAttachment("2", "Photo 2", SurveyAttachmentType.PHOTO),
|
||||||
|
SurveyAttachment("3", "Photo 1", SurveyAttachmentType.PHOTO),
|
||||||
|
SurveyAttachment("4", "Add new", SurveyAttachmentType.ADD_NEW)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onBackClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(26.dp))
|
||||||
|
|
||||||
|
FormField(
|
||||||
|
label = "Sold to*",
|
||||||
|
isRequired = true
|
||||||
|
) {
|
||||||
|
DropdownField(
|
||||||
|
placeholder = "Buyer Name",
|
||||||
|
value = surveyData.buyerName,
|
||||||
|
expanded = buyerNameExpanded,
|
||||||
|
onExpandedChange = { buyerNameExpanded = it },
|
||||||
|
onValueChange = { surveyData = surveyData.copy(buyerName = it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(29.dp))
|
||||||
|
|
||||||
|
FormField(
|
||||||
|
label = "Sale Price*",
|
||||||
|
isRequired = true
|
||||||
|
) {
|
||||||
|
TextInputField(
|
||||||
|
placeholder = "Price",
|
||||||
|
value = surveyData.salePrice,
|
||||||
|
onValueChange = { surveyData = surveyData.copy(salePrice = it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(21.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
FormField(
|
||||||
|
label = "Sale Date*",
|
||||||
|
isRequired = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
DatePickerField(
|
||||||
|
placeholder = "Date",
|
||||||
|
value = surveyData.saleDate,
|
||||||
|
onValueChange = { surveyData = surveyData.copy(saleDate = it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FormField(
|
||||||
|
label = "Sale Location",
|
||||||
|
isRequired = false,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
TextInputField(
|
||||||
|
placeholder = "Location",
|
||||||
|
value = surveyData.saleLocation,
|
||||||
|
onValueChange = { surveyData = surveyData.copy(saleLocation = it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(34.dp))
|
||||||
|
|
||||||
|
MultilineTextInputField(
|
||||||
|
placeholder = "Notes",
|
||||||
|
value = surveyData.notes,
|
||||||
|
onValueChange = { surveyData = surveyData.copy(notes = it) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(104.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(31.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Attachments",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF000000),
|
||||||
|
lineHeight = 29.sp,
|
||||||
|
modifier = Modifier.padding(start = 11.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(31.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(19.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
AttachmentChip(
|
||||||
|
name = defaultAttachments[0].name,
|
||||||
|
type = defaultAttachments[0].type,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AttachmentChip(
|
||||||
|
name = defaultAttachments[1].name,
|
||||||
|
type = defaultAttachments[1].type,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
AttachmentChip(
|
||||||
|
name = defaultAttachments[2].name,
|
||||||
|
type = defaultAttachments[2].type,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AttachmentChip(
|
||||||
|
name = defaultAttachments[3].name,
|
||||||
|
type = defaultAttachments[3].type,
|
||||||
|
isAddNew = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(53.dp))
|
||||||
|
|
||||||
|
PrimaryButton(
|
||||||
|
text = "Submit",
|
||||||
|
onClick = { onSubmit(surveyData) },
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
SecondaryButton(
|
||||||
|
text = "Cancel",
|
||||||
|
onClick = onCancel,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FormField(
|
||||||
|
label: String,
|
||||||
|
isRequired: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF000000),
|
||||||
|
lineHeight = 29.sp
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextInputField(
|
||||||
|
placeholder: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = placeholder,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
letterSpacing = (-0.312).sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MultilineTextInputField(
|
||||||
|
placeholder: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
contentAlignment = Alignment.TopStart
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = placeholder,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = (-0.312).sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DropdownField(
|
||||||
|
placeholder: String,
|
||||||
|
value: String,
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onExpandedChange(!expanded) }
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value.ifEmpty { placeholder },
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
letterSpacing = (-0.312).sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterStart)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = "Dropdown",
|
||||||
|
tint = Color(0xFFA5A5A5),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(15.dp, 10.dp)
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DatePickerField(
|
||||||
|
placeholder: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (value.isEmpty()) placeholder else value,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
letterSpacing = (-0.312).sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterStart)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CalendarToday,
|
||||||
|
contentDescription = "Calendar",
|
||||||
|
tint = Color(0xFF000000),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AttachmentChip(
|
||||||
|
name: String,
|
||||||
|
type: SurveyAttachmentType,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isAddNew: Boolean = false,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(43.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFFE5E7EB),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(24.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onClick() }
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF4A5565),
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isAddNew) Icons.Default.Add else Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = if (isAddNew) "Add" else "Attachment",
|
||||||
|
tint = if (isAddNew) Color(0xFF777777) else Color(0xFFADADAD),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrimaryButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.width(173.dp)
|
||||||
|
.height(50.dp)
|
||||||
|
.background(Color(0xFF0A0A0A), RoundedCornerShape(50.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFFFFFFFF),
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = (-0.312).sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SecondaryButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.width(173.dp)
|
||||||
|
.height(50.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(50.dp)
|
||||||
|
)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(50.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = (-0.312).sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.AttachFile
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
|
||||||
|
data class SaleArchive(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val breed: String,
|
||||||
|
val soldTo: String,
|
||||||
|
val saleDate: String,
|
||||||
|
val salePrice: String,
|
||||||
|
val location: String,
|
||||||
|
val notes: String,
|
||||||
|
val imageUrl: String,
|
||||||
|
val attachments: List<Attachment>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Attachment(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val type: AttachmentType
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class AttachmentType {
|
||||||
|
SALE_PAPERS,
|
||||||
|
PHOTO
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SaleArchiveScreen(
|
||||||
|
saleId: String,
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSaleSurveyClick:(saleId: String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val sale = SaleArchive(
|
||||||
|
id = "1",
|
||||||
|
name = "Gauri",
|
||||||
|
breed = "Gir",
|
||||||
|
soldTo = "Buyer 1",
|
||||||
|
saleDate = "07.12.2025",
|
||||||
|
salePrice = "₹30,000",
|
||||||
|
location = "Ravi Nagar, Nagpur",
|
||||||
|
notes = "Buyer requested a check up after 15 days of sale. Additional support for 20 days.",
|
||||||
|
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/9389ccf57f6d262b500be45f6d9cbfe929302413?width=784",
|
||||||
|
attachments = listOf(
|
||||||
|
Attachment("1", "Sale Papers 1", AttachmentType.SALE_PAPERS),
|
||||||
|
Attachment("2", "Photo 1", AttachmentType.PHOTO),
|
||||||
|
Attachment("3", "Sale Papers 1", AttachmentType.SALE_PAPERS),
|
||||||
|
Attachment("4", "Photo 2", AttachmentType.PHOTO)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(64.dp)
|
||||||
|
.padding(horizontal = 11.dp, vertical = 15.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onBackClick
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = sale.imageUrl,
|
||||||
|
contentDescription = "${sale.name} image",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(273.dp),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(
|
||||||
|
elevation = 25.dp,
|
||||||
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||||
|
spotColor = Color.Black.copy(alpha = 0.25f)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFFE5E7EB).copy(alpha = 0.5f),
|
||||||
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFFCFBF8),
|
||||||
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 21.dp, vertical = 17.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(1.987.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Name, Breed",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF6A7282),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${sale.name}, (${sale.breed})",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF101828),
|
||||||
|
lineHeight = 22.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(11.99.dp)
|
||||||
|
) {
|
||||||
|
InfoField(
|
||||||
|
label = "Sold To",
|
||||||
|
value = sale.soldTo,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
InfoField(
|
||||||
|
label = "Sale Date",
|
||||||
|
value = sale.saleDate,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 17.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(11.99.dp)
|
||||||
|
) {
|
||||||
|
InfoField(
|
||||||
|
label = "Sale Price",
|
||||||
|
value = sale.salePrice,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
InfoField(
|
||||||
|
label = "Location",
|
||||||
|
value = sale.location,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.078.dp)
|
||||||
|
.background(Color(0xFFD1D5DC))
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(5.995.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Notes",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF6A7282),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sale.notes,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF364153),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Attachments",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF000000),
|
||||||
|
lineHeight = 24.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(11.dp)
|
||||||
|
) {
|
||||||
|
AttachmentButton(
|
||||||
|
name = sale.attachments[0].name,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AttachmentButton(
|
||||||
|
name = sale.attachments[1].name,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(11.dp)
|
||||||
|
) {
|
||||||
|
AttachmentButton(
|
||||||
|
name = sale.attachments[2].name,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AttachmentButton(
|
||||||
|
name = sale.attachments[3].name,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(1.987.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF6A7282),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF101828),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AttachmentButton(
|
||||||
|
name: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(38.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFFE5E7EB),
|
||||||
|
shape = RoundedCornerShape(20.dp)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
color = Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(20.dp)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF4A5565),
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(5.995.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.AttachFile,
|
||||||
|
contentDescription = "Attachment",
|
||||||
|
tint = Color(0xFFADADAD),
|
||||||
|
modifier = Modifier.size(19.99.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||||
|
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||||
|
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||||
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
data class SavedListing(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val age: String,
|
||||||
|
val breed: String,
|
||||||
|
val price: String,
|
||||||
|
val views: String,
|
||||||
|
val distance: String,
|
||||||
|
val imageUrl: String
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class BottomNavTab {
|
||||||
|
HOME,
|
||||||
|
SELL,
|
||||||
|
CHATS,
|
||||||
|
SERVICES,
|
||||||
|
MANDI
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SavedListingsScreen(
|
||||||
|
onListingClick: (String) -> Unit = {},
|
||||||
|
onNavClick: (route: String) -> Unit = {},
|
||||||
|
onBackClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val savedListings = listOf(
|
||||||
|
SavedListing(
|
||||||
|
id = "1",
|
||||||
|
name = "Eauri",
|
||||||
|
age = "Age: 3 yrs",
|
||||||
|
breed = "Holstein",
|
||||||
|
price = "₹45,000",
|
||||||
|
views = "124 views",
|
||||||
|
distance = "Distance: 12 km",
|
||||||
|
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/db216f496f6d5fd681db9afc8006eb8da3164c17?width=192"
|
||||||
|
),
|
||||||
|
SavedListing(
|
||||||
|
id = "2",
|
||||||
|
name = "Fauri",
|
||||||
|
age = "Age: 2 yrs",
|
||||||
|
breed = "Jersey",
|
||||||
|
price = "₹38,000",
|
||||||
|
views = "89 views",
|
||||||
|
distance = "Distance: 17 km",
|
||||||
|
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/a49aa5540b0cb6bf76ba3aa99cb8d4f94835e8ee?width=192"
|
||||||
|
),
|
||||||
|
SavedListing(
|
||||||
|
id = "3",
|
||||||
|
name = "Gauri",
|
||||||
|
age = "Age: 4 yrs",
|
||||||
|
breed = "Gir",
|
||||||
|
price = "₹52,000",
|
||||||
|
views = "156 views",
|
||||||
|
distance = "Distance: 20 km",
|
||||||
|
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/26fd38c965180ef066862fb1d0aa6df040a0d389?width=192"
|
||||||
|
),
|
||||||
|
SavedListing(
|
||||||
|
id = "4",
|
||||||
|
name = "Hauri",
|
||||||
|
age = "Age: 1.5 yrs",
|
||||||
|
breed = "Sahiwal",
|
||||||
|
price = "₹28,000",
|
||||||
|
views = "67 views",
|
||||||
|
distance = "Distance: 6 km",
|
||||||
|
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/b8edfd48d908a8e0eeeee55745ab409e454be25f?width=192"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
BottomNavScaffold(
|
||||||
|
items = mainBottomNavItems,
|
||||||
|
currentItem = "Home",
|
||||||
|
onBottomNavItemClick = onNavClick,
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 72.dp)
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Saved Listings",
|
||||||
|
fontSize = AppTypography.Display,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color(0xFFF7F4EE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 15.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
AdSpaceBanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
items(savedListings) { listing ->
|
||||||
|
ListingCard(
|
||||||
|
listing = listing,
|
||||||
|
onClick = { onListingClick(listing.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListingCard(
|
||||||
|
listing: SavedListing,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(138.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFFF3F4F6).copy(alpha = 0.5f),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8), RoundedCornerShape(24.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 21.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = listing.imageUrl,
|
||||||
|
contentDescription = listing.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = listing.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF1E2939),
|
||||||
|
lineHeight = 24.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = listing.age,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF4A5565),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = listing.breed,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF4A5565),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = listing.price,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF1E2939),
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
textAlign = TextAlign.End
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = listing.views,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF6A7282),
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
textAlign = TextAlign.End
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = listing.distance,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF4A5565),
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,560 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Call
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
import androidx.compose.material.icons.filled.Message
|
||||||
|
import androidx.compose.material.icons.filled.Shield
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
data class SellerProfile(
|
||||||
|
val name: String,
|
||||||
|
val initials: String,
|
||||||
|
val location: String,
|
||||||
|
val rating: Double,
|
||||||
|
val reviewCount: Int,
|
||||||
|
val trustScore: Int,
|
||||||
|
val totalSales: Int,
|
||||||
|
val yearsActive: String,
|
||||||
|
val responseRate: String,
|
||||||
|
val about: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PastSale(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val emoji: String,
|
||||||
|
val age: String,
|
||||||
|
val breed: String,
|
||||||
|
val price: String,
|
||||||
|
val rating: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SellerProfileScreen(
|
||||||
|
sellerId: String = "1",
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onCallClick: () -> Unit = {},
|
||||||
|
onMessageClick: () -> Unit = {},
|
||||||
|
onViewAllSalesClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
// Sample data - in real app, fetch based on sellerId
|
||||||
|
val seller = SellerProfile(
|
||||||
|
name = "Ramesh Singh",
|
||||||
|
initials = "RS",
|
||||||
|
location = "Nagpur, Maharashtra",
|
||||||
|
rating = 4.8,
|
||||||
|
reviewCount = 156,
|
||||||
|
trustScore = 95,
|
||||||
|
totalSales = 47,
|
||||||
|
yearsActive = "5+",
|
||||||
|
responseRate = "98%",
|
||||||
|
about = "Experienced cattle farmer and trader with over 5 years in the business. Specializing in dairy cattle breeds including Gir, Holstein, and Jersey. All animals are well-maintained with complete vaccination records and health certificates."
|
||||||
|
)
|
||||||
|
|
||||||
|
val pastSales = listOf(
|
||||||
|
PastSale("1", "Gir Cow", "🐄", "Age: 3 yrs", "Holstein", "₹45,000", 5.0),
|
||||||
|
PastSale("2", "Buffalo", "🐃", "Age: 4 yrs", "Murrah", "₹55,000", 4.9),
|
||||||
|
PastSale("3", "Goat", "🐐", "Age: 2 yrs", "Sirohi", "₹12,000", 4.7)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Seller Profile",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color(0xFFF7F4EE)
|
||||||
|
),
|
||||||
|
modifier = Modifier.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = Color(0x1A000000),
|
||||||
|
shape = RectangleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrollable Content
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 16.dp, bottom = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Profile Card
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Profile Info
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Avatar
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp)
|
||||||
|
.background(Color(0xFFE5E7EB), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = seller.initials,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = seller.name,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.LocationOn,
|
||||||
|
contentDescription = "Location",
|
||||||
|
tint = Color(0xFF717182),
|
||||||
|
modifier = Modifier.size(12.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = seller.location,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Star,
|
||||||
|
contentDescription = "Rating",
|
||||||
|
tint = Color(0xFFFDC700),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = seller.rating.toString(),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "•",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${seller.reviewCount} reviews",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI Trust Score
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
Color(0x4D60A5FA),
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
Color(0x4D60A5FA),
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Shield,
|
||||||
|
contentDescription = "Trust",
|
||||||
|
tint = Color(0xFF155DFC),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "AI Trust Score",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Star,
|
||||||
|
contentDescription = "Score",
|
||||||
|
tint = Color(0xFF155DFC),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${seller.trustScore}/100",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color(0xFF155DFC)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Verified seller with excellent transaction history",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onCallClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(46.dp),
|
||||||
|
shape = RoundedCornerShape(23.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Black,
|
||||||
|
contentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Call,
|
||||||
|
contentDescription = "Call",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Call",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onMessageClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(46.dp),
|
||||||
|
shape = RoundedCornerShape(23.dp),
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
Color(0x1A000000)
|
||||||
|
),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Message,
|
||||||
|
contentDescription = "Message",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Message",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
StatCard(
|
||||||
|
value = seller.totalSales.toString(),
|
||||||
|
label = "Total Sales",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
StatCard(
|
||||||
|
value = seller.yearsActive,
|
||||||
|
label = "Years Active",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
StatCard(
|
||||||
|
value = seller.responseRate,
|
||||||
|
label = "Response\nRate",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Past Sales Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Past Sales",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(onClick = onViewAllSalesClick) {
|
||||||
|
Text(
|
||||||
|
text = "View All",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pastSales.forEach { sale ->
|
||||||
|
PastSaleCard(sale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// About Seller Section
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "About Seller",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = seller.about,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 22.75.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatCard(
|
||||||
|
value: String,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.height(94.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PastSaleCard(sale: PastSale) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Animal Icon
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.background(Color(0xFFF7F4EE), RoundedCornerShape(8.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sale.emoji,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sale.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sale.age,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = sale.breed,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sale.price,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Star,
|
||||||
|
contentDescription = "Rating",
|
||||||
|
tint = Color(0xFFFDC700),
|
||||||
|
modifier = Modifier.size(12.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = sale.rating.toString(),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBackIos
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import com.example.livingai_lg.ui.models.SortDirection
|
||||||
|
import com.example.livingai_lg.ui.models.SortField
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.components.SortItem
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SortScreen(
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onApplyClick: (List<SortField>) -> Unit = {},
|
||||||
|
onCancelClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var fields by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
listOf(
|
||||||
|
SortField("distance", "Distance"),
|
||||||
|
SortField("price", "Price"),
|
||||||
|
SortField("milk", "Milk Capacity"),
|
||||||
|
SortField("ai", "AI Score"),
|
||||||
|
SortField("age", "Age")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleField(index: Int) {
|
||||||
|
val current = fields[index]
|
||||||
|
|
||||||
|
val nextDirection = when (current.direction) {
|
||||||
|
SortDirection.NONE -> SortDirection.ASC
|
||||||
|
SortDirection.ASC -> SortDirection.DESC
|
||||||
|
SortDirection.DESC -> SortDirection.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val updated = fields.toMutableList()
|
||||||
|
|
||||||
|
updated[index] = current.copy(
|
||||||
|
direction = nextDirection,
|
||||||
|
order = if (nextDirection == SortDirection.NONE) null else current.order
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recalculate sort order
|
||||||
|
val active = updated
|
||||||
|
.filter { it.direction != SortDirection.NONE }
|
||||||
|
.sortedBy { it.order ?: Int.MAX_VALUE }
|
||||||
|
.mapIndexed { i, item -> item.copy(order = i + 1) }
|
||||||
|
|
||||||
|
fields = updated.map { field ->
|
||||||
|
active.find { it.key == field.key } ?: field.copy(order = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBackIos,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text("Sort By", fontSize = 32.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
fields.forEachIndexed { index, field ->
|
||||||
|
SortItem(
|
||||||
|
field = field,
|
||||||
|
onToggle = { toggleField(index) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onApplyClick(fields.filter { it.direction != SortDirection.NONE }) },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.width(173.dp)
|
||||||
|
.height(50.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Black,
|
||||||
|
contentColor = Color.White
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(25.dp)
|
||||||
|
) {
|
||||||
|
Text("Apply")
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onCancelClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.width(173.dp)
|
||||||
|
.height(50.dp),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = Color(0xFF0A0A0A)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(25.dp)
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.example.livingai_lg.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
|
import com.example.livingai_lg.ui.state.FilterStore
|
||||||
|
import com.example.livingai_lg.ui.state.WishlistStore
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WishlistScreen(
|
||||||
|
onApply: () -> Unit,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val wishlist by WishlistStore.wishlist.collectAsState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Saved Filters",
|
||||||
|
fontSize = 28.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
if (wishlist.isEmpty()) {
|
||||||
|
Text("No saved filters yet")
|
||||||
|
} else {
|
||||||
|
wishlist.forEach { entry ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.clickable {
|
||||||
|
onApply()
|
||||||
|
FilterStore.set(entry.filters)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = entry.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Tap to apply",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.auth
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||||
|
import com.example.livingai_lg.ui.components.IconCircle
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmButtonPrimary
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmButtonSecondary
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmButtonText
|
||||||
|
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmSproutBg
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmSproutIcon
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmSunBg
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmSunIcon
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmWheatBg
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmWheatIcon
|
||||||
|
import com.example.livingai_lg.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LandingScreen(
|
||||||
|
onSignUpClick: () -> Unit = {},
|
||||||
|
onSignInClick: () -> Unit = {},
|
||||||
|
onGuestClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(FarmTextLight)
|
||||||
|
) {
|
||||||
|
// Decorative background elements
|
||||||
|
DecorativeBackground()
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 32.dp)
|
||||||
|
) {
|
||||||
|
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = horizontalPadding)
|
||||||
|
.padding( top = 56.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
//Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Icon row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
IconCircle(
|
||||||
|
backgroundColor = FarmWheatBg,
|
||||||
|
size = 56.dp
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_wheat),
|
||||||
|
contentDescription = "Wheat Icon",
|
||||||
|
tint = FarmWheatIcon // keeps original vector colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
IconCircle(
|
||||||
|
backgroundColor = FarmSproutBg,
|
||||||
|
size = 64.dp
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_sprout),
|
||||||
|
contentDescription = "Wheat Icon",
|
||||||
|
tint = FarmSproutIcon // keeps original vector colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
IconCircle(
|
||||||
|
backgroundColor = FarmSunBg,
|
||||||
|
size = 56.dp
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_sun),
|
||||||
|
contentDescription = "Wheat Icon",
|
||||||
|
tint = FarmSunIcon // keeps original vector colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
// Welcome text
|
||||||
|
Text(
|
||||||
|
text = "Welcome!",
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = FarmTextDark
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Join the farm marketplace community",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = FarmTextNormal
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
// Sign up button
|
||||||
|
Button(
|
||||||
|
onClick = onSignUpClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = FarmButtonPrimary,
|
||||||
|
contentColor = FarmButtonText
|
||||||
|
),
|
||||||
|
elevation = ButtonDefaults.buttonElevation(
|
||||||
|
defaultElevation = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "New user? Sign up",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Sign in button
|
||||||
|
Button(
|
||||||
|
onClick = onSignInClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = FarmButtonSecondary,
|
||||||
|
contentColor = FarmButtonText
|
||||||
|
),
|
||||||
|
elevation = ButtonDefaults.buttonElevation(
|
||||||
|
defaultElevation = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Already a user? Sign in",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Continue as guest
|
||||||
|
TextButton(
|
||||||
|
onClick = onGuestClick
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Continue as Guest",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = FarmTextNormal,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,544 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
|
import com.example.livingai_lg.api.AuthManager
|
||||||
|
import com.example.livingai_lg.api.SignupRequest
|
||||||
|
import com.example.livingai_lg.api.TokenManager
|
||||||
|
import com.example.livingai_lg.api.UserNotFoundException
|
||||||
|
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||||
|
import kotlin.math.min
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
|
import androidx.compose.ui.input.key.*
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OtpScreen(
|
||||||
|
phoneNumber: String,
|
||||||
|
name: String,
|
||||||
|
mainViewModel: com.example.livingai_lg.ui.MainViewModel,
|
||||||
|
onSuccess: () -> Unit = {},
|
||||||
|
onCreateProfile: (name: String) -> Unit = {},
|
||||||
|
onLanding: () -> Unit = {}, // New callback for navigating to landing page
|
||||||
|
// Optional signup data for signup flow
|
||||||
|
signupState: String? = null,
|
||||||
|
signupDistrict: String? = null,
|
||||||
|
signupVillage: String? = null,
|
||||||
|
) {
|
||||||
|
val otpLength = 6
|
||||||
|
val otp = remember { mutableStateOf(List<String>(otpLength) { "" }) }
|
||||||
|
var focusedIndex by remember { mutableStateOf(0) }
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||||
|
|
||||||
|
fun updateOtpAt(index: Int, value: String) {
|
||||||
|
otp.value = otp.value.mapIndexed { i, old ->
|
||||||
|
if (i == index) value else old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown timer state (1 minutes = 60 seconds)
|
||||||
|
var countdownSeconds by remember { mutableStateOf(10) }
|
||||||
|
var countdownKey by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(countdownKey) {
|
||||||
|
while (countdownSeconds > 0) {
|
||||||
|
delay(1000) // Wait 1 second
|
||||||
|
countdownSeconds--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format countdown as MM:SS
|
||||||
|
val minutes = countdownSeconds / 60
|
||||||
|
val seconds = countdownSeconds % 60
|
||||||
|
val countdownText = String.format("%02d:%02d", minutes, seconds)
|
||||||
|
|
||||||
|
// Flag to determine if this is a sign-in flow for an existing user.
|
||||||
|
val isSignInFlow = name == "existing_user"
|
||||||
|
// Flag to determine if this is a signup flow (has signup data)
|
||||||
|
val isSignupFlow = !isSignInFlow && (signupState != null || signupDistrict != null || signupVillage != null)
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFFFFFBEA),
|
||||||
|
Color(0xFFFDFBE8),
|
||||||
|
Color(0xFFF7FEE7)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
DecorativeBackground()
|
||||||
|
|
||||||
|
val screenW = maxWidth.value
|
||||||
|
val screenH = maxHeight.value
|
||||||
|
|
||||||
|
// Figma design reference size from Flutter widget
|
||||||
|
val designW = 393.39f
|
||||||
|
val designH = 852.53f
|
||||||
|
|
||||||
|
val scale = min(screenW / designW, screenH / designH)
|
||||||
|
|
||||||
|
fun s(v: Float) = (v * scale).dp // dp scaling
|
||||||
|
fun fs(v: Float) = (v * scale).sp // font scaling
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize().padding(horizontal = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// ---------------------------
|
||||||
|
// "Enter OTP" Title
|
||||||
|
// ---------------------------
|
||||||
|
Text(
|
||||||
|
text = "Enter OTP",
|
||||||
|
color = Color(0xFF927B5E),
|
||||||
|
fontSize = fs(20f),
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black.copy(alpha = 0.25f),
|
||||||
|
offset = Offset(0f, s(4f).value),
|
||||||
|
blurRadius = s(4f).value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// OTP 4-Box Input Row
|
||||||
|
// ---------------------------
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(s(17f))
|
||||||
|
) {
|
||||||
|
OTPInputTextFields(
|
||||||
|
otpLength = otpLength,
|
||||||
|
otpValues = otp.value,
|
||||||
|
onUpdateOtpValuesByIndex = { index, value ->
|
||||||
|
updateOtpAt(index,value)
|
||||||
|
},
|
||||||
|
onOtpInputComplete = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Countdown Timer
|
||||||
|
// ---------------------------
|
||||||
|
if (countdownSeconds > 0) {
|
||||||
|
Text(
|
||||||
|
text = countdownText,
|
||||||
|
color = Color(0xFF927B5E),
|
||||||
|
fontSize = fs(16f),
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black.copy(alpha = 0.15f),
|
||||||
|
offset = Offset(0f, s(2f).value),
|
||||||
|
blurRadius = s(2f).value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Resend OTP",
|
||||||
|
color = Color(0xFFE17100),
|
||||||
|
fontSize = fs(16f),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
.clickable {
|
||||||
|
// 🔹 Stub for now
|
||||||
|
scope.launch {
|
||||||
|
|
||||||
|
authManager.requestOtp(phoneNumber)
|
||||||
|
.onSuccess {
|
||||||
|
// OTP sent successfully, navigate to OTP screen with signup data
|
||||||
|
// Pass signup form data through the callback
|
||||||
|
Toast.makeText(context, "Resent OTP",Toast.LENGTH_LONG).show()
|
||||||
|
countdownSeconds = 10
|
||||||
|
countdownKey++
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
Log.e("OtpScreen", "Failed to send OTP ${it.message}", it)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Continue Button
|
||||||
|
// ---------------------------
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth().padding(vertical = 16.dp, horizontal = 48.dp)
|
||||||
|
.size(s(279.25f), s(55.99f))
|
||||||
|
.shadow(
|
||||||
|
elevation = s(6f),
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.10f),
|
||||||
|
shape = RoundedCornerShape(s(16f)),
|
||||||
|
)
|
||||||
|
.shadow(
|
||||||
|
elevation = s(15f),
|
||||||
|
ambientColor = Color.Black.copy(alpha = 0.10f),
|
||||||
|
shape = RoundedCornerShape(s(16f)),
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
listOf(Color(0xFFFD9900), Color(0xFFE17100))
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(s(16f))
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val otpString = otp.value.joinToString("") { it?.toString() ?: "" }
|
||||||
|
|
||||||
|
if (otpString.length < otpLength) {
|
||||||
|
Toast.makeText(context, "Please enter full OTP", Toast.LENGTH_SHORT).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSignupFlow) {
|
||||||
|
// For signup flow: Verify OTP first, then call signup API to update user with name/location
|
||||||
|
android.util.Log.d("OTPScreen", "Signup flow: Verifying OTP...")
|
||||||
|
authManager.login(phoneNumber, otpString)
|
||||||
|
.onSuccess { verifyResponse ->
|
||||||
|
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
|
||||||
|
// OTP verified successfully - user is now logged in
|
||||||
|
mainViewModel.refreshAuthStatus()
|
||||||
|
// Now call signup API to update user with name and location
|
||||||
|
val signupRequest = SignupRequest(
|
||||||
|
name = name,
|
||||||
|
phoneNumber = phoneNumber,
|
||||||
|
state = signupState,
|
||||||
|
district = signupDistrict,
|
||||||
|
cityVillage = signupVillage
|
||||||
|
)
|
||||||
|
authManager.signup(signupRequest)
|
||||||
|
.onSuccess { signupResponse ->
|
||||||
|
android.util.Log.d("OTPScreen", "Signup API response: success=${signupResponse.success}, userExists=${signupResponse.userExists}, needsProfile=${signupResponse.needsProfile}, isNewAccount=${signupResponse.isNewAccount}")
|
||||||
|
// Check if this is a new user account
|
||||||
|
val isNewUser = signupResponse.isNewAccount == true
|
||||||
|
|
||||||
|
if (isNewUser) {
|
||||||
|
// New user - navigate to landing page
|
||||||
|
android.util.Log.d("OTPScreen", "New user detected - navigating to landing page")
|
||||||
|
try {
|
||||||
|
onLanding()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Existing user or signup update - use existing logic
|
||||||
|
if (signupResponse.success || signupResponse.userExists == true) {
|
||||||
|
// Success - user is created/updated and logged in
|
||||||
|
// Check if profile needs completion
|
||||||
|
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
|
||||||
|
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
|
||||||
|
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
||||||
|
// mainViewModel.refreshAuthStatus()
|
||||||
|
try {
|
||||||
|
if (needsProfile) {
|
||||||
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
|
onCreateProfile(name)
|
||||||
|
} else {
|
||||||
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Signup failed but OTP was verified - user is logged in
|
||||||
|
// Navigate to success anyway since verify-otp succeeded
|
||||||
|
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
||||||
|
val needsProfile = verifyResponse.needsProfile
|
||||||
|
// Refresh auth status
|
||||||
|
mainViewModel.refreshAuthStatus()
|
||||||
|
try {
|
||||||
|
if (needsProfile) {
|
||||||
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
|
onCreateProfile(name)
|
||||||
|
} else {
|
||||||
|
// Don't manually navigate - let AppNavigation handle it
|
||||||
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
// Show warning if signup update failed
|
||||||
|
val errorMsg = signupResponse.message
|
||||||
|
if (errorMsg != null) {
|
||||||
|
Toast.makeText(context, "Profile update: $errorMsg", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { signupError ->
|
||||||
|
android.util.Log.e("OTPScreen", "Signup API failed: ${signupError.message}", signupError)
|
||||||
|
// Signup API failed but OTP was verified - user is logged in
|
||||||
|
// For new users, navigate to landing page
|
||||||
|
// For existing users, use existing logic
|
||||||
|
val needsProfile = verifyResponse.needsProfile
|
||||||
|
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
|
||||||
|
// Refresh auth status
|
||||||
|
mainViewModel.refreshAuthStatus()
|
||||||
|
try {
|
||||||
|
// If this is a signup flow and signup failed, treat as new user
|
||||||
|
if (isSignupFlow) {
|
||||||
|
android.util.Log.d("OTPScreen", "Signup flow failed - navigating to landing page")
|
||||||
|
onLanding()
|
||||||
|
} else if (needsProfile) {
|
||||||
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
|
onCreateProfile(name)
|
||||||
|
} else {
|
||||||
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
// Show warning about signup failure
|
||||||
|
val errorMsg = signupError.message ?: "Profile update failed"
|
||||||
|
Toast.makeText(context, "Warning: $errorMsg", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Invalid or expired OTP",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For sign-in flow: Just verify OTP and login
|
||||||
|
authManager.login(phoneNumber, otpString)
|
||||||
|
.onSuccess { response ->
|
||||||
|
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
|
||||||
|
// Tokens are now saved (synchronously via commit())
|
||||||
|
// Refresh auth status - this will optimistically set authState to Authenticated
|
||||||
|
// The LaunchedEffect in AppNavigation will automatically navigate to ChooseServiceScreen
|
||||||
|
//mainViewModel.refreshAuthStatus()
|
||||||
|
if(response.needsProfile) {
|
||||||
|
onCreateProfile(name)
|
||||||
|
} else {
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
android.util.Log.d("OTPScreen", "Auth status refreshed - navigation will happen automatically via LaunchedEffect")
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||||
|
|
||||||
|
// Check if user not found - redirect to signup
|
||||||
|
if (error is UserNotFoundException && error.errorCode == "USER_NOT_FOUND") {
|
||||||
|
android.util.Log.d("OTPScreen", "User not found - redirecting to signup")
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
error.message ?: "Account not found. Please sign up to create a new account.",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
// Navigate back to landing page so user can choose signup
|
||||||
|
try {
|
||||||
|
onLanding()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other errors (invalid OTP, expired, etc.)
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
error.message ?: "Invalid or expired OTP",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Continue",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = fs(16f),
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OTPInputTextFields(
|
||||||
|
otpLength: Int,
|
||||||
|
onUpdateOtpValuesByIndex: (Int, String) -> Unit,
|
||||||
|
onOtpInputComplete: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
otpValues: List<String> = List(otpLength) { "" }, // Pass this as default for future reference
|
||||||
|
isError: Boolean = false,
|
||||||
|
) {
|
||||||
|
val focusRequesters = List(otpLength) { FocusRequester() }
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
// horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
otpValues.forEachIndexed { index, value ->
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
// .width(64.dp)
|
||||||
|
.padding(6.dp)
|
||||||
|
.focusRequester(focusRequesters[index])
|
||||||
|
.background(Color.White)
|
||||||
|
.onKeyEvent { keyEvent ->
|
||||||
|
if (keyEvent.key == Key.Backspace) {
|
||||||
|
if (otpValues[index].isEmpty() && index > 0) {
|
||||||
|
onUpdateOtpValuesByIndex(index, "")
|
||||||
|
focusRequesters[index - 1].requestFocus()
|
||||||
|
} else {
|
||||||
|
onUpdateOtpValuesByIndex(index, "")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value = value,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
// To use OTP code copied from keyboard
|
||||||
|
if (newValue.length == otpLength) {
|
||||||
|
for (i in otpValues.indices) {
|
||||||
|
onUpdateOtpValuesByIndex(
|
||||||
|
i,
|
||||||
|
if (i < newValue.length && newValue[i].isDigit()) newValue[i].toString() else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardController?.hide()
|
||||||
|
onOtpInputComplete() // you should validate the otp values first for, if it is only digits or isNotEmpty
|
||||||
|
} else if (newValue.length <= 1) {
|
||||||
|
onUpdateOtpValuesByIndex(index, newValue)
|
||||||
|
if (newValue.isNotEmpty()) {
|
||||||
|
if (index < otpLength - 1) {
|
||||||
|
focusRequesters[index + 1].requestFocus()
|
||||||
|
} else {
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
onOtpInputComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < otpLength - 1) focusRequesters[index + 1].requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
imeAction = if (index == otpLength - 1) ImeAction.Done else ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = {
|
||||||
|
if (index < otpLength - 1) {
|
||||||
|
focusRequesters[index + 1].requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
onOtpInputComplete()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
isError = isError,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(value) {
|
||||||
|
if (otpValues.all { it.isNotEmpty() }) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
onOtpInputComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequesters.first().requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
|
import com.example.livingai_lg.api.AuthManager
|
||||||
|
import com.example.livingai_lg.api.TokenManager
|
||||||
|
import com.example.livingai_lg.api.UserNotFoundException
|
||||||
|
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||||
|
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||||
|
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextLink
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||||
|
|
||||||
|
import com.example.livingai_lg.ui.components.FarmHeader
|
||||||
|
import com.example.livingai_lg.ui.utils.isKeyboardOpen
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SignInScreen(
|
||||||
|
onSignUpClick: () -> Unit = {},
|
||||||
|
onSignInClick: (phone: String, name: String) -> Unit = {_,_->},
|
||||||
|
) {
|
||||||
|
val phoneNumber = remember { mutableStateOf("") }
|
||||||
|
val isValid = phoneNumber.value.length == 10
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(FarmTextLight)
|
||||||
|
) {
|
||||||
|
DecorativeBackground()
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 32.dp)
|
||||||
|
) {
|
||||||
|
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||||
|
val keyboardOpen = isKeyboardOpen()
|
||||||
|
val topPadding by animateDpAsState(
|
||||||
|
targetValue = if (keyboardOpen) 40.dp else 140.dp,
|
||||||
|
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = horizontalPadding)
|
||||||
|
.padding( top = 40.dp),//topPadding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = if (keyboardOpen) Arrangement.Top else Arrangement.Center
|
||||||
|
) {
|
||||||
|
//Spacer(Modifier.height(if (keyboardOpen) 32.dp else 56.dp))
|
||||||
|
// Header
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
|
||||||
|
FarmHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
PhoneNumberInput(
|
||||||
|
phone = phoneNumber.value,
|
||||||
|
onChange = { phoneNumber.value = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val fullPhoneNumber = "+91${phoneNumber.value}"
|
||||||
|
scope.launch {
|
||||||
|
// First check if user exists before requesting OTP
|
||||||
|
authManager.checkUser(fullPhoneNumber)
|
||||||
|
.onSuccess { checkResponse ->
|
||||||
|
if (checkResponse.userExists) {
|
||||||
|
// User exists, proceed to request OTP
|
||||||
|
authManager.requestOtp(fullPhoneNumber)
|
||||||
|
.onSuccess {
|
||||||
|
onSignInClick(fullPhoneNumber, "existing_user")
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
Log.e("SignInScreen", "Failed to send OTP ${it.message}", it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User doesn't exist (shouldn't happen if API works correctly)
|
||||||
|
Toast.makeText(context, "User is not registered. Please sign up.", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
// User not found or other error
|
||||||
|
if (error is UserNotFoundException) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
error.message ?: "User is not registered. Please sign up to create a new account.",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isValid,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (isValid) Color(0xFFE17100) else Color(0xFFE17100).copy(
|
||||||
|
alpha = 0.4f
|
||||||
|
),
|
||||||
|
contentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Sign In", fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text("Don't have an account? ", fontSize = 16.sp, color = FarmTextNormal)
|
||||||
|
TextButton(onClick = onSignUpClick) {
|
||||||
|
Text("Sign up", fontSize = 16.sp, color = FarmTextLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.auth
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
|
import com.example.livingai_lg.api.AuthManager
|
||||||
|
import com.example.livingai_lg.api.SignupRequest
|
||||||
|
import com.example.livingai_lg.api.TokenManager
|
||||||
|
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||||
|
import com.example.livingai_lg.ui.components.DropdownInput
|
||||||
|
import com.example.livingai_lg.ui.components.InputField
|
||||||
|
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||||
|
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||||
|
import com.example.livingai_lg.ui.components.FarmHeader
|
||||||
|
import com.example.livingai_lg.ui.utils.isKeyboardOpen
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class SignUpFormData(
|
||||||
|
val name: String = "",
|
||||||
|
val state: String = "",
|
||||||
|
val district: String = "",
|
||||||
|
val village: String = "",
|
||||||
|
val phoneNumber: String = ""
|
||||||
|
){
|
||||||
|
val isValid: Boolean
|
||||||
|
get() =
|
||||||
|
name.isNotBlank() &&
|
||||||
|
state.isNotBlank() &&
|
||||||
|
district.isNotBlank() &&
|
||||||
|
village.isNotBlank() &&
|
||||||
|
phoneNumber.isValidPhoneNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.isValidPhoneNumber(): Boolean {
|
||||||
|
return length == 10 && all { it.isDigit() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SignUpScreen(
|
||||||
|
onSignInClick: () -> Unit = {},
|
||||||
|
onSignUpClick: (phone: String, name: String, state: String, district: String, village: String) -> Unit = {_,_,_,_,_->}
|
||||||
|
) {
|
||||||
|
var formData by remember { mutableStateOf(SignUpFormData()) }
|
||||||
|
|
||||||
|
var showStateDropdown by remember { mutableStateOf(false) }
|
||||||
|
var showDistrictDropdown by remember { mutableStateOf(false) }
|
||||||
|
var showVillageDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val states = listOf("Maharashtra", "Gujarat", "Tamil Nadu", "Karnataka", "Rajasthan")
|
||||||
|
val districts = listOf("Mumbai", "Pune", "Nagpur", "Aurangabad", "Nashik")
|
||||||
|
val villages = listOf("Village 1", "Village 2", "Village 3", "Village 4", "Village 5")
|
||||||
|
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
// Use 10.0.2.2 to connect to host machine's localhost from emulator
|
||||||
|
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(FarmTextLight)
|
||||||
|
) {
|
||||||
|
DecorativeBackground()
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 32.dp)
|
||||||
|
) {
|
||||||
|
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||||
|
val keyboardOpen = isKeyboardOpen()
|
||||||
|
// val topPadding by animateDpAsState(
|
||||||
|
// targetValue = if (keyboardOpen) 40.dp else 140.dp,
|
||||||
|
// animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||||
|
// )
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = horizontalPadding)
|
||||||
|
.padding( top = 40.dp),//topPadding),
|
||||||
|
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = if (keyboardOpen) Arrangement.Top else Arrangement.Center
|
||||||
|
) {
|
||||||
|
// Spacer(Modifier.height(if (keyboardOpen) 32.dp else 56.dp))
|
||||||
|
// Title
|
||||||
|
FarmHeader()
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Name Input
|
||||||
|
InputField(
|
||||||
|
label = "Enter Name*",
|
||||||
|
value = formData.name,
|
||||||
|
placeholder = "Enter your name",
|
||||||
|
onChange = { formData = formData.copy(name = it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// State + District Row
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
|
||||||
|
DropdownInput(
|
||||||
|
label = "Enter Location",
|
||||||
|
selected = formData.state,
|
||||||
|
options = states,
|
||||||
|
expanded = showStateDropdown,
|
||||||
|
onExpandedChange = { showStateDropdown = it },
|
||||||
|
onSelect = { formData = formData.copy(state = it) },
|
||||||
|
placeholder = "Select State",
|
||||||
|
modifier = Modifier.weight(0.4f) // <--- half width
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownInput(
|
||||||
|
selected = formData.district,
|
||||||
|
options = districts,
|
||||||
|
expanded = showDistrictDropdown,
|
||||||
|
onExpandedChange = { showDistrictDropdown = it },
|
||||||
|
onSelect = { formData = formData.copy(district = it) },
|
||||||
|
placeholder = "Select District",
|
||||||
|
modifier = Modifier.weight(0.6f) // <--- half width
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Village Dropdown
|
||||||
|
DropdownInput(
|
||||||
|
label = "Enter Village/Place",
|
||||||
|
selected = formData.village,
|
||||||
|
options = villages,
|
||||||
|
expanded = showVillageDropdown,
|
||||||
|
onExpandedChange = { showVillageDropdown = it },
|
||||||
|
onSelect = {
|
||||||
|
formData = formData.copy(village = it); showVillageDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Phone Number
|
||||||
|
PhoneNumberInput(
|
||||||
|
phone = formData.phoneNumber,
|
||||||
|
onChange = { formData = formData.copy(phoneNumber = it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
// Sign Up button
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val fullPhoneNumber = "+91${formData.phoneNumber}"
|
||||||
|
|
||||||
|
// First check if user already exists
|
||||||
|
authManager.checkUser(fullPhoneNumber)
|
||||||
|
.onSuccess { checkResponse ->
|
||||||
|
if (checkResponse.userExists) {
|
||||||
|
// User already registered - show message to sign in instead
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"This phone number is already registered. Please sign in instead.",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
// Optionally navigate to sign in screen
|
||||||
|
onSignInClick()
|
||||||
|
} else {
|
||||||
|
// User doesn't exist - proceed with signup
|
||||||
|
// Request OTP first before allowing signup
|
||||||
|
authManager.requestOtp(fullPhoneNumber)
|
||||||
|
.onSuccess {
|
||||||
|
// OTP sent successfully, navigate to OTP screen with signup data
|
||||||
|
// Pass signup form data through the callback
|
||||||
|
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { checkError ->
|
||||||
|
// If check fails, it might be a network error or user doesn't exist
|
||||||
|
// Try to proceed with signup (user might not exist)
|
||||||
|
if (checkError is com.example.livingai_lg.api.UserNotFoundException) {
|
||||||
|
// User doesn't exist - proceed with signup
|
||||||
|
authManager.requestOtp(fullPhoneNumber)
|
||||||
|
.onSuccess {
|
||||||
|
// OTP sent successfully, navigate to OTP screen with signup data
|
||||||
|
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other error (network, etc.)
|
||||||
|
Toast.makeText(context, "Unable to verify phone number: ${checkError.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
enabled = formData.isValid,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE17100))
|
||||||
|
) {
|
||||||
|
Text("Sign Up", fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Already have account?
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text("Already have an account? ", color = FarmTextNormal)
|
||||||
|
TextButton(onClick = onSignInClick) {
|
||||||
|
Text("Sign In", color = Color(0xFFE17100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,336 @@
|
||||||
|
package com.example.farmmarketplace.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.CallMade
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.CallMissed
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.CallReceived
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||||
|
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||||
|
|
||||||
|
data class CallRecord(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val initials: String,
|
||||||
|
val callType: CallType,
|
||||||
|
val duration: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val isVideoCall: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class CallType {
|
||||||
|
INCOMING,
|
||||||
|
OUTGOING,
|
||||||
|
MISSED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CallsScreen(
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onCallClick: () -> Unit = {},
|
||||||
|
onMenuClick: () -> Unit = {},
|
||||||
|
onCallItemClick: (String) -> Unit = {},
|
||||||
|
onTabClick: (route: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val callHistory = listOf(
|
||||||
|
CallRecord(
|
||||||
|
id = "1",
|
||||||
|
name = "Farmer Kumar",
|
||||||
|
initials = "FK",
|
||||||
|
callType = CallType.INCOMING,
|
||||||
|
duration = "5m 23s",
|
||||||
|
timestamp = "Today, 2:30 PM"
|
||||||
|
),
|
||||||
|
CallRecord(
|
||||||
|
id = "2",
|
||||||
|
name = "Seller Raj",
|
||||||
|
initials = "SR",
|
||||||
|
callType = CallType.OUTGOING,
|
||||||
|
duration = "2m 10s",
|
||||||
|
timestamp = "Today, 11:45 AM"
|
||||||
|
),
|
||||||
|
CallRecord(
|
||||||
|
id = "3",
|
||||||
|
name = "Buyer Priya",
|
||||||
|
initials = "BP",
|
||||||
|
callType = CallType.MISSED,
|
||||||
|
duration = "",
|
||||||
|
timestamp = "Yesterday, 8:15 PM"
|
||||||
|
),
|
||||||
|
CallRecord(
|
||||||
|
id = "4",
|
||||||
|
name = "Seller 1",
|
||||||
|
initials = "S1",
|
||||||
|
callType = CallType.OUTGOING,
|
||||||
|
duration = "8m 45s",
|
||||||
|
timestamp = "Yesterday, 5:30 PM",
|
||||||
|
isVideoCall = true
|
||||||
|
),
|
||||||
|
CallRecord(
|
||||||
|
id = "5",
|
||||||
|
name = "Veterinarian",
|
||||||
|
initials = "V",
|
||||||
|
callType = CallType.INCOMING,
|
||||||
|
duration = "12m 30s",
|
||||||
|
timestamp = "2 days ago"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
) {
|
||||||
|
BottomNavScaffold(
|
||||||
|
items = chatBottomNavItems,
|
||||||
|
currentItem = "Calls",
|
||||||
|
onBottomNavItemClick = onTabClick,
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||||
|
) {
|
||||||
|
CallsHeader(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onCallClick = onCallClick,
|
||||||
|
onMenuClick = onMenuClick
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(callHistory) { call ->
|
||||||
|
CallHistoryItem(
|
||||||
|
call = call,
|
||||||
|
onClick = { onCallItemClick(call.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactsBottomNav(
|
||||||
|
// currentTab = ContactsTab.CALLS,
|
||||||
|
// onTabClick = onTabClick
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CallsHeader(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onCallClick: () -> Unit,
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(65.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(26.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onBackClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Calls",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 42.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Phone,
|
||||||
|
contentDescription = "New Call",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onCallClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onMenuClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CallHistoryItem(
|
||||||
|
call: CallRecord,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(76.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8), RoundedCornerShape(12.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(Color(0xFFE5E7EB), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = call.initials,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 21.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = call.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF1E2939),
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = (-0.312).sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = when (call.callType) {
|
||||||
|
CallType.INCOMING -> Icons.AutoMirrored.Filled.CallReceived
|
||||||
|
CallType.OUTGOING -> Icons.AutoMirrored.Filled.CallMade
|
||||||
|
CallType.MISSED -> Icons.AutoMirrored.Filled.CallMissed
|
||||||
|
},
|
||||||
|
contentDescription = call.callType.name,
|
||||||
|
tint = when (call.callType) {
|
||||||
|
CallType.INCOMING -> Color(0xFF00A63E)
|
||||||
|
CallType.OUTGOING -> Color(0xFF155DFC)
|
||||||
|
CallType.MISSED -> Color(0xFFE7000B)
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = when (call.callType) {
|
||||||
|
CallType.INCOMING -> "Incoming • ${call.duration}"
|
||||||
|
CallType.OUTGOING -> "Outgoing • ${call.duration}"
|
||||||
|
CallType.MISSED -> "Missed"
|
||||||
|
},
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = call.timestamp,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = if (call.isVideoCall) Icons.Default.Videocam else Icons.Default.Phone,
|
||||||
|
contentDescription = if (call.isVideoCall) "Video Call" else "Voice Call",
|
||||||
|
tint = Color(0xFF030213),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Attachment
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardVoice
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Phone
|
||||||
|
import androidx.compose.material.icons.filled.Send
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
data class ChatMessage(
|
||||||
|
val text: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val isSentByCurrentUser: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ChatScreen(
|
||||||
|
sellerName: String = "Seller 1",
|
||||||
|
sellerStatus: String = "Online",
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onPhoneClick: () -> Unit = {},
|
||||||
|
onMenuClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var messageText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val messages = remember {
|
||||||
|
listOf(
|
||||||
|
ChatMessage(
|
||||||
|
text = "Hello! Yes, the cow is still available. Would you like to visit?",
|
||||||
|
timestamp = "10:30 AM",
|
||||||
|
isSentByCurrentUser = false
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text = "Hey is she still available",
|
||||||
|
timestamp = "10:28 AM",
|
||||||
|
isSentByCurrentUser = true
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text = "She's a healthy Gir cow, 2 years old, great milk yield.",
|
||||||
|
timestamp = "10:31 AM",
|
||||||
|
isSentByCurrentUser = false
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text = "What's the price?",
|
||||||
|
timestamp = "10:32 AM",
|
||||||
|
isSentByCurrentUser = true
|
||||||
|
),
|
||||||
|
ChatMessage(
|
||||||
|
text = "₹45,000. Negotiable if you visit today.",
|
||||||
|
timestamp = "10:33 AM",
|
||||||
|
isSentByCurrentUser = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF8F8F8))
|
||||||
|
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
ChatHeader(
|
||||||
|
sellerName = sellerName,
|
||||||
|
sellerStatus = sellerStatus,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onPhoneClick = onPhoneClick,
|
||||||
|
onMenuClick = onMenuClick
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
items(messages) { message ->
|
||||||
|
MessageBubble(message = message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageInputBar(
|
||||||
|
messageText = messageText,
|
||||||
|
onMessageChange = { messageText = it },
|
||||||
|
onSendClick = {
|
||||||
|
messageText = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatHeader(
|
||||||
|
sellerName: String,
|
||||||
|
sellerStatus: String,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onPhoneClick: () -> Unit,
|
||||||
|
onMenuClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(65.dp),
|
||||||
|
color = Color.White,
|
||||||
|
shadowElevation = 1.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.background(Color(0xFFF0F0F0), shape = CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "S1",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = sellerName,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF1E2939)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = sellerStatus,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF4A5565)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onPhoneClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Phone,
|
||||||
|
contentDescription = "Phone",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onMenuClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageBubble(message: ChatMessage) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = if (message.isSentByCurrentUser) Alignment.End else Alignment.Start
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = if (message.isSentByCurrentUser) {
|
||||||
|
RoundedCornerShape(topStart = 16.dp, topEnd = 4.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||||
|
} else {
|
||||||
|
RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||||
|
},
|
||||||
|
color = if (message.isSentByCurrentUser) Color(0xFF0A0A0A) else Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 271.dp)
|
||||||
|
.then(
|
||||||
|
if (!message.isSentByCurrentUser) {
|
||||||
|
Modifier.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = Color(0x1A000000),
|
||||||
|
shape = RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = message.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
color = if (message.isSentByCurrentUser) Color.White else Color(0xFF0A0A0A)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = message.timestamp,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
color = if (message.isSentByCurrentUser) Color(0x99FFFFFF) else Color(0xFF717182)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageInputBar(
|
||||||
|
messageText: String,
|
||||||
|
onMessageChange: (String) -> Unit,
|
||||||
|
onSendClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val isMultiline = messageText.contains("\n") || messageText.length > 40
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().imePadding(),
|
||||||
|
color = Color.White,
|
||||||
|
shadowElevation = 1.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// TEXT INPUT
|
||||||
|
OutlinedTextField(
|
||||||
|
value = messageText,
|
||||||
|
onValueChange = onMessageChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(
|
||||||
|
min = if (isMultiline) 44.dp else 36.dp,
|
||||||
|
max = 120.dp
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "Type a message",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFF717182)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textStyle = LocalTextStyle.current.copy(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.Black // ✅ typed text black
|
||||||
|
),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedBorderColor = Color(0x1A000000),
|
||||||
|
focusedBorderColor = Color(0x33000000),
|
||||||
|
unfocusedContainerColor = Color.White,
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
cursorColor = Color.Black
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// ATTACHMENT ICON
|
||||||
|
IconButton(
|
||||||
|
onClick = { },
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Attachment,
|
||||||
|
contentDescription = "Attachment",
|
||||||
|
tint = Color(0xFF717182),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VOICE BUTTON
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.background(Color(0xFF0A0A0A), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { },
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.KeyboardVoice,
|
||||||
|
contentDescription = "Voice",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEND BUTTON
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.background(Color(0xFF0A0A0A), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onSendClick,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||||
|
contentDescription = "Send",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,341 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.farmmarketplace.ui.screens.ContactsBottomNav
|
||||||
|
import com.example.farmmarketplace.ui.screens.ContactsTab
|
||||||
|
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||||
|
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||||
|
|
||||||
|
data class ChatPreview(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val initials: String,
|
||||||
|
val lastMessage: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val isOnline: Boolean = false,
|
||||||
|
val unreadCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatsScreen(
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onNewChatClick: () -> Unit = {},
|
||||||
|
onMenuClick: () -> Unit = {},
|
||||||
|
onChatItemClick: (String) -> Unit = {},
|
||||||
|
onTabClick: (route: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val chatList = listOf(
|
||||||
|
ChatPreview(
|
||||||
|
id = "1",
|
||||||
|
name = "Farmer Kumar",
|
||||||
|
initials = "FK",
|
||||||
|
lastMessage = "The cows are healthy and ready for viewing",
|
||||||
|
timestamp = "Today, 2:30 PM",
|
||||||
|
isOnline = true,
|
||||||
|
unreadCount = 2
|
||||||
|
),
|
||||||
|
ChatPreview(
|
||||||
|
id = "2",
|
||||||
|
name = "Seller Raj",
|
||||||
|
initials = "SR",
|
||||||
|
lastMessage = "You: Can you send more photos?",
|
||||||
|
timestamp = "Today, 11:45 AM",
|
||||||
|
isOnline = true,
|
||||||
|
unreadCount = 0
|
||||||
|
),
|
||||||
|
ChatPreview(
|
||||||
|
id = "3",
|
||||||
|
name = "Buyer Priya",
|
||||||
|
initials = "BP",
|
||||||
|
lastMessage = "What's the best time to visit?",
|
||||||
|
timestamp = "Yesterday, 8:15 PM",
|
||||||
|
isOnline = false,
|
||||||
|
unreadCount = 1
|
||||||
|
),
|
||||||
|
ChatPreview(
|
||||||
|
id = "4",
|
||||||
|
name = "Seller 1",
|
||||||
|
initials = "S1",
|
||||||
|
lastMessage = "You: Thanks for the information",
|
||||||
|
timestamp = "Yesterday, 5:30 PM",
|
||||||
|
isOnline = true,
|
||||||
|
unreadCount = 0
|
||||||
|
),
|
||||||
|
ChatPreview(
|
||||||
|
id = "5",
|
||||||
|
name = "Veterinarian",
|
||||||
|
initials = "V",
|
||||||
|
lastMessage = "The animal health check is complete",
|
||||||
|
timestamp = "2 days ago",
|
||||||
|
isOnline = false,
|
||||||
|
unreadCount = 0
|
||||||
|
),
|
||||||
|
ChatPreview(
|
||||||
|
id = "6",
|
||||||
|
name = "Market Vendor",
|
||||||
|
initials = "MV",
|
||||||
|
lastMessage = "You: Available this weekend?",
|
||||||
|
timestamp = "3 days ago",
|
||||||
|
isOnline = false,
|
||||||
|
unreadCount = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
) {
|
||||||
|
BottomNavScaffold(
|
||||||
|
items = chatBottomNavItems,
|
||||||
|
currentItem = "Chats",
|
||||||
|
onBottomNavItemClick = onTabClick,
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||||
|
) {
|
||||||
|
ChatsHeader(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onNewChatClick = onNewChatClick,
|
||||||
|
onMenuClick = onMenuClick
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(chatList) { chat ->
|
||||||
|
ChatListItem(
|
||||||
|
chat = chat,
|
||||||
|
onClick = { onChatItemClick(chat.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactsBottomNav(
|
||||||
|
// currentTab = ContactsTab.CHATS,
|
||||||
|
// onTabClick = onTabClick
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatsHeader(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onNewChatClick: () -> Unit,
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(65.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(26.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onBackClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Chats",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 42.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = "New Chat",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onNewChatClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onMenuClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatListItem(
|
||||||
|
chat: ChatPreview,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(76.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8), RoundedCornerShape(12.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(Color(0xFFE5E7EB), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = chat.initials,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 21.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (chat.isOnline) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.size(12.dp)
|
||||||
|
.background(Color(0xFF00A63E), CircleShape)
|
||||||
|
.border(2.dp, Color.White, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = chat.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF1E2939),
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = (-0.312).sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = chat.lastMessage,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = chat.timestamp,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (chat.unreadCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.background(Color(0xFF155DFC), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = chat.unreadCount.toString(),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color.White,
|
||||||
|
lineHeight = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,432 @@
|
||||||
|
package com.example.farmmarketplace.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Message
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Message
|
||||||
|
import androidx.compose.material.icons.filled.Chat
|
||||||
|
import androidx.compose.material.icons.filled.Contacts
|
||||||
|
import androidx.compose.material.icons.filled.Message
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Phone
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.outlined.Phone
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||||
|
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||||
|
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||||
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
|
||||||
|
data class Contact(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val initials: String,
|
||||||
|
val status: String,
|
||||||
|
val isOnline: Boolean = false,
|
||||||
|
val phoneNumber: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ContactsTab {
|
||||||
|
CONTACTS,
|
||||||
|
CALLS,
|
||||||
|
CHATS
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsScreen(
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSearchClick: () -> Unit = {},
|
||||||
|
onMenuClick: () -> Unit = {},
|
||||||
|
onContactClick: (String) -> Unit = {},
|
||||||
|
onCallClick: (String) -> Unit = {},
|
||||||
|
onMessageClick: (String) -> Unit = {},
|
||||||
|
onTabClick: (route: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val contacts = listOf(
|
||||||
|
Contact(
|
||||||
|
id = "1",
|
||||||
|
name = "Farmer Kumar",
|
||||||
|
initials = "FK",
|
||||||
|
status = "Online",
|
||||||
|
isOnline = true
|
||||||
|
),
|
||||||
|
Contact(
|
||||||
|
id = "2",
|
||||||
|
name = "Seller Raj",
|
||||||
|
initials = "SR",
|
||||||
|
status = "+91 98765 43211",
|
||||||
|
isOnline = false,
|
||||||
|
phoneNumber = "+91 98765 43211"
|
||||||
|
),
|
||||||
|
Contact(
|
||||||
|
id = "3",
|
||||||
|
name = "Buyer Priya",
|
||||||
|
initials = "BP",
|
||||||
|
status = "Online",
|
||||||
|
isOnline = true
|
||||||
|
),
|
||||||
|
Contact(
|
||||||
|
id = "4",
|
||||||
|
name = "Seller 1",
|
||||||
|
initials = "S1",
|
||||||
|
status = "+91 98765 43213",
|
||||||
|
isOnline = false,
|
||||||
|
phoneNumber = "+91 98765 43213"
|
||||||
|
),
|
||||||
|
Contact(
|
||||||
|
id = "5",
|
||||||
|
name = "Veterinarian",
|
||||||
|
initials = "V",
|
||||||
|
status = "Online",
|
||||||
|
isOnline = true
|
||||||
|
),
|
||||||
|
Contact(
|
||||||
|
id = "6",
|
||||||
|
name = "Farm Supply",
|
||||||
|
initials = "FS",
|
||||||
|
status = "+91 98765 43215",
|
||||||
|
isOnline = false,
|
||||||
|
phoneNumber = "+91 98765 43215"
|
||||||
|
),
|
||||||
|
Contact(
|
||||||
|
id = "7",
|
||||||
|
name = "Transport Co.",
|
||||||
|
initials = "TC",
|
||||||
|
status = "+91 98765 43216",
|
||||||
|
isOnline = false,
|
||||||
|
phoneNumber = "+91 98765 43216"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
) {
|
||||||
|
BottomNavScaffold(
|
||||||
|
items = chatBottomNavItems,
|
||||||
|
currentItem = "Contacts",
|
||||||
|
onBottomNavItemClick = onTabClick,
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||||
|
) {
|
||||||
|
ContactsHeader(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onSearchClick = onSearchClick,
|
||||||
|
onMenuClick = onMenuClick
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
items(contacts) { contact ->
|
||||||
|
ContactItem(
|
||||||
|
contact = contact,
|
||||||
|
onContactClick = { onContactClick(contact.id) },
|
||||||
|
onCallClick = { onCallClick(contact.id) },
|
||||||
|
onMessageClick = { onMessageClick(contact.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactsBottomNav(
|
||||||
|
// currentTab = ContactsTab.CONTACTS,
|
||||||
|
// onTabClick = onTabClick
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsHeader(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSearchClick: () -> Unit,
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(65.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(26.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onBackClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Contacts",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 42.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = "Search",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onSearchClick() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onMenuClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactItem(
|
||||||
|
contact: Contact,
|
||||||
|
onContactClick: () -> Unit,
|
||||||
|
onCallClick: () -> Unit,
|
||||||
|
onMessageClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(73.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.05f)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onContactClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(Color(0xFFE5E7EB), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = contact.initials,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 21.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = contact.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF1E2939),
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = (-0.312).sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = contact.status,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = Color(0xFF717182),
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onCallClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Phone,
|
||||||
|
contentDescription = "Call",
|
||||||
|
tint = Color(0xFF030213),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onMessageClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.Message,
|
||||||
|
contentDescription = "Message",
|
||||||
|
tint = Color(0xFF030213),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsBottomNav(
|
||||||
|
currentTab: ContactsTab,
|
||||||
|
onTabClick: (route: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(65.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.078.dp,
|
||||||
|
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.background(Color(0xFFFCFBF8))
|
||||||
|
.padding(horizontal = 32.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
ContactsTabItem(
|
||||||
|
icon = Icons.AutoMirrored.Filled.Chat,
|
||||||
|
label = "Chats",
|
||||||
|
isSelected = currentTab == ContactsTab.CHATS,
|
||||||
|
onClick = { onTabClick(AppScreen.CHATS) }
|
||||||
|
)
|
||||||
|
ContactsTabItem(
|
||||||
|
icon = Icons.Default.Phone,
|
||||||
|
label = "Calls",
|
||||||
|
isSelected = currentTab == ContactsTab.CALLS,
|
||||||
|
onClick = { onTabClick(AppScreen.CALLS) }
|
||||||
|
)
|
||||||
|
ContactsTabItem(
|
||||||
|
icon = Icons.Default.Contacts,
|
||||||
|
label = "Contacts",
|
||||||
|
isSelected = currentTab == ContactsTab.CONTACTS,
|
||||||
|
onClick = { onTabClick(AppScreen.CONTACTS) }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsTabItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.clickable(
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) { onClick() }
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = label,
|
||||||
|
tint = Color(0xFF0A0A0A),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = Color(0xFF0A0A0A),
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
package com.example.livingai_lg.ui.screens.testing
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ApiTestScreen(
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Default API endpoint
|
||||||
|
var apiEndpoint by remember {
|
||||||
|
mutableStateOf("https://jsonplaceholder.typicode.com/posts/1")
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var responseText by remember { mutableStateOf<String?>(null) }
|
||||||
|
var errorText by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val client = remember { OkHttpClient() }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF7F4EE))
|
||||||
|
) {
|
||||||
|
// Top App Bar
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "API Test",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
tint = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color(0xFFF7F4EE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// API endpoint input
|
||||||
|
OutlinedTextField(
|
||||||
|
value = apiEndpoint,
|
||||||
|
onValueChange = { apiEndpoint = it },
|
||||||
|
label = { Text("API Endpoint") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call API button
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
isLoading = true
|
||||||
|
responseText = null
|
||||||
|
errorText = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(apiEndpoint)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use { response ->
|
||||||
|
val body = response.body?.string()
|
||||||
|
Pair(response.code, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (code, body) = result
|
||||||
|
responseText = "Status: $code\n\n$body"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
errorText = e.toString()
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isLoading
|
||||||
|
) {
|
||||||
|
Text(if (isLoading) "Calling API..." else "Call API")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result display
|
||||||
|
when {
|
||||||
|
responseText != null -> {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = Color.White,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = responseText ?: "",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorText != null -> {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = Color(0xFFFFEBEE),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = errorText ?: "",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color(0xFFE53935)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.example.livingai_lg.ui.state
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
|
|
||||||
|
object FilterStore {
|
||||||
|
|
||||||
|
private val _filters = MutableStateFlow(FiltersState())
|
||||||
|
val filters: StateFlow<FiltersState> = _filters
|
||||||
|
|
||||||
|
fun update(block: (FiltersState) -> FiltersState) {
|
||||||
|
_filters.update(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun set(newState: FiltersState) {
|
||||||
|
_filters.value = newState
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_filters.value = FiltersState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.example.livingai_lg.ui.state
|
||||||
|
|
||||||
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class WishlistEntry(
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
val name: String,
|
||||||
|
val filters: FiltersState,
|
||||||
|
val createdAt: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
object WishlistStore {
|
||||||
|
|
||||||
|
private val _wishlist =
|
||||||
|
MutableStateFlow<List<WishlistEntry>>(emptyList())
|
||||||
|
|
||||||
|
val wishlist: StateFlow<List<WishlistEntry>> = _wishlist
|
||||||
|
|
||||||
|
fun add(entry: WishlistEntry) {
|
||||||
|
_wishlist.value = _wishlist.value + entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(id: String) {
|
||||||
|
_wishlist.value = _wishlist.value.filterNot { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_wishlist.value = emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.example.livingai_lg.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
object AppTypography {
|
||||||
|
val Display = 32.sp
|
||||||
|
val Title = 20.sp
|
||||||
|
val Body = 16.sp
|
||||||
|
val BodySmall = 14.sp
|
||||||
|
val Caption = 12.sp
|
||||||
|
val Micro = 10.sp
|
||||||
|
}
|
||||||
|
|
@ -2,25 +2,57 @@ package com.example.livingai_lg.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
// Farm Marketplace Color Palette
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
val FarmTextLight = Color(0xFFF5F1E8)
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
val FarmTextDark = Color(0xFF5D4E37) //Title
|
||||||
|
val FarmTextNormal = Color(0xFF8B7355)
|
||||||
|
val FarmTextLink = Color(0xFFE17100)
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
val FarmSproutBg = Color(0xFFFFCB79)
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val FarmSproutIcon = Color(0xFF8B6F47)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
|
||||||
|
|
||||||
// Custom Colors from Figma
|
val FarmWheatBg = Color(0xFFFFDD88)
|
||||||
val LightCream = Color(0xFFFFFBEB)
|
val FarmWheatIcon = Color(0xFFD4A574)
|
||||||
val LighterCream = Color(0xFFFEFCE8)
|
|
||||||
val LightestGreen = Color(0xFFF7FEE7)
|
val FarmSunBg = Color(0xFFFFB179)
|
||||||
val Maize = Color(0xFFF7C35F)
|
val FarmSunIcon = Color(0xFFE07A5F)
|
||||||
val TerraCotta = Color(0xFFD97D5D)
|
|
||||||
val DarkBrown = Color(0xFF5D4E37)
|
val FarmGold = Color(0xFFFFB84D)
|
||||||
val MidBrown = Color(0xFF8B7355)
|
val FarmPink = Color(0xFFEDD5C8)
|
||||||
val DarkerBrown = Color(0xFF322410)
|
val FarmFence = Color(0xFFD4B5A0)
|
||||||
val OrangeBrown = Color(0xFFD4A574)
|
val FarmBox = Color(0xFFE8D9CC)
|
||||||
val DarkOrange = Color(0xFFE07A5F)
|
val FarmButtonPrimary = Color(0xFFFFB84D)
|
||||||
val Gold = Color(0xFFFFDD88)
|
val FarmButtonSecondary = Color(0xFFE07A5F)
|
||||||
val LightOrange = Color(0xFFFFB84D)
|
val FarmButtonText = Color(0xFF322410)
|
||||||
val DarkerOrange = Color(0xFFD96A4F)
|
|
||||||
|
// Material Design 3 Color Scheme
|
||||||
|
val md_theme_light_primary = Color(0xFF5D4E37)
|
||||||
|
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_primaryContainer = Color(0xFFF5F1E8)
|
||||||
|
val md_theme_light_onPrimaryContainer = Color(0xFF3E3220)
|
||||||
|
val md_theme_light_secondary = Color(0xFF8B7355)
|
||||||
|
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_secondaryContainer = Color(0xFFFFDDBD)
|
||||||
|
val md_theme_light_onSecondaryContainer = Color(0xFF2B2415)
|
||||||
|
val md_theme_light_tertiary = Color(0xFFE07A5F)
|
||||||
|
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_tertiaryContainer = Color(0xFFFFDDBD)
|
||||||
|
val md_theme_light_onTertiaryContainer = Color(0xFF3E2415)
|
||||||
|
val md_theme_light_error = Color(0xFFB3261E)
|
||||||
|
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
|
||||||
|
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
|
||||||
|
val md_theme_light_background = Color(0xFFFBF8F3)
|
||||||
|
val md_theme_light_onBackground = Color(0xFF1C1B1F)
|
||||||
|
val md_theme_light_surface = Color(0xFFFBF8F3)
|
||||||
|
val md_theme_light_onSurface = Color(0xFF1C1B1F)
|
||||||
|
val md_theme_light_surfaceVariant = Color(0xFFEFE0D6)
|
||||||
|
val md_theme_light_onSurfaceVariant = Color(0xFF49454E)
|
||||||
|
val md_theme_light_outline = Color(0xFF79747E)
|
||||||
|
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
|
||||||
|
val md_theme_light_inverseSurface = Color(0xFF313033)
|
||||||
|
val md_theme_light_inversePrimary = Color(0xFFFFDDBD)
|
||||||
|
val md_theme_light_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_light_surfaceTint = Color(0xFF5D4E37)
|
||||||
|
val md_theme_light_outlineVariant = Color(0xFFD3C4BC)
|
||||||
|
val md_theme_light_scrim = Color(0xFF000000)
|
||||||
|
|
|
||||||
|
|
@ -3,56 +3,111 @@ package com.example.livingai_lg.ui.theme
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val lightScheme = lightColorScheme(
|
||||||
primary = Purple80,
|
primary = md_theme_light_primary,
|
||||||
secondary = PurpleGrey80,
|
onPrimary = md_theme_light_onPrimary,
|
||||||
tertiary = Pink80
|
primaryContainer = md_theme_light_primaryContainer,
|
||||||
|
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||||
|
secondary = md_theme_light_secondary,
|
||||||
|
onSecondary = md_theme_light_onSecondary,
|
||||||
|
secondaryContainer = md_theme_light_secondaryContainer,
|
||||||
|
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_light_tertiary,
|
||||||
|
onTertiary = md_theme_light_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||||
|
error = md_theme_light_error,
|
||||||
|
onError = md_theme_light_onError,
|
||||||
|
errorContainer = md_theme_light_errorContainer,
|
||||||
|
onErrorContainer = md_theme_light_onErrorContainer,
|
||||||
|
background = md_theme_light_background,
|
||||||
|
onBackground = md_theme_light_onBackground,
|
||||||
|
surface = md_theme_light_surface,
|
||||||
|
onSurface = md_theme_light_onSurface,
|
||||||
|
surfaceVariant = md_theme_light_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||||
|
outline = md_theme_light_outline,
|
||||||
|
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_light_inverseSurface,
|
||||||
|
inversePrimary = md_theme_light_inversePrimary,
|
||||||
|
surfaceTint = md_theme_light_surfaceTint,
|
||||||
|
outlineVariant = md_theme_light_outlineVariant,
|
||||||
|
scrim = md_theme_light_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val darkScheme = darkColorScheme(
|
||||||
primary = Purple40,
|
primary = Color(0xFFFFDDBD),
|
||||||
secondary = PurpleGrey40,
|
onPrimary = Color(0xFF3E3220),
|
||||||
tertiary = Pink40
|
primaryContainer = Color(0xFF57472D),
|
||||||
|
onPrimaryContainer = Color(0xFFFFDDBD),
|
||||||
/* Other default colors to override
|
secondary = Color(0xFFFFDDBD),
|
||||||
background = Color(0xFFFFFBFE),
|
onSecondary = Color(0xFF2B2415),
|
||||||
surface = Color(0xFFFFFBFE),
|
secondaryContainer = Color(0xFF6B5344),
|
||||||
onPrimary = Color.White,
|
onSecondaryContainer = Color(0xFFFFDDBD),
|
||||||
onSecondary = Color.White,
|
tertiary = Color(0xFFFFB893),
|
||||||
onTertiary = Color.White,
|
onTertiary = Color(0xFF3E2415),
|
||||||
onBackground = Color(0xFF1C1B1F),
|
tertiaryContainer = Color(0xFF8B5A3C),
|
||||||
onSurface = Color(0xFF1C1B1F),
|
onTertiaryContainer = Color(0xFFFFDDBD),
|
||||||
*/
|
error = Color(0xFFFFB4AB),
|
||||||
|
onError = Color(0xFF690005),
|
||||||
|
errorContainer = Color(0xFF93000A),
|
||||||
|
onErrorContainer = Color(0xFFFFDAD6),
|
||||||
|
background = Color(0xFF1C1B1F),
|
||||||
|
onBackground = Color(0xFFE7E0E8),
|
||||||
|
surface = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFFE7E0E8),
|
||||||
|
surfaceVariant = Color(0xFF49454E),
|
||||||
|
onSurfaceVariant = Color(0xFFCAC7D0),
|
||||||
|
outline = Color(0xFF94919A),
|
||||||
|
inverseOnSurface = Color(0xFF1C1B1F),
|
||||||
|
inverseSurface = Color(0xFFE7E0E8),
|
||||||
|
inversePrimary = Color(0xFF5D4E37),
|
||||||
|
surfaceTint = Color(0xFFFFDDBD),
|
||||||
|
outlineVariant = Color(0xFF49454E),
|
||||||
|
scrim = Color(0xFF000000),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LivingAi_LgTheme(
|
fun FarmMarketplaceTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
useDarkTheme -> darkScheme
|
||||||
else -> LightColorScheme
|
else -> lightScheme
|
||||||
|
}
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
window.statusBarColor = colorScheme.primary.toArgb()
|
||||||
|
WindowCompat.getInsetsController(window, view)?.isAppearanceLightStatusBars =
|
||||||
|
!useDarkTheme
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography,
|
typography = typography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,29 +6,110 @@ import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
val typography = Typography(
|
||||||
val Typography = Typography(
|
displayLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 57.sp,
|
||||||
|
lineHeight = 64.sp,
|
||||||
|
letterSpacing = (-0.25).sp,
|
||||||
|
),
|
||||||
|
displayMedium = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 45.sp,
|
||||||
|
lineHeight = 52.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
displaySmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 36.sp,
|
||||||
|
lineHeight = 44.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
headlineLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
lineHeight = 40.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
headlineMedium = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
lineHeight = 36.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
lineHeight = 32.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.15.sp,
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp,
|
||||||
|
),
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.15.sp,
|
||||||
)
|
),
|
||||||
/* Other default text styles to override
|
bodyMedium = TextStyle(
|
||||||
titleLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 22.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 28.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.sp
|
letterSpacing = 0.25.sp,
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.4.sp,
|
||||||
|
),
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp,
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
labelSmall = TextStyle(
|
labelSmall = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp,
|
||||||
)
|
),
|
||||||
*/
|
)
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.example.livingai_lg.ui.utils
|
||||||
|
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class FormatUtils {
|
||||||
|
}
|
||||||
|
fun formatter(): NumberFormat{
|
||||||
|
return NumberFormat.getInstance(Locale.forLanguageTag("en-IN"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun formatViews(views: Long?): String {
|
||||||
|
val count = views ?: 0
|
||||||
|
|
||||||
|
val formattedNumber = formatter().format(count)
|
||||||
|
|
||||||
|
return when (count) {
|
||||||
|
1L -> "$formattedNumber View"
|
||||||
|
else -> "$formattedNumber Views"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatPrice(price: Long?): String {
|
||||||
|
val value = price ?: 0
|
||||||
|
|
||||||
|
val formattedNumber = formatter().format(value)
|
||||||
|
|
||||||
|
return "₹$formattedNumber"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatDistance(distanceMeters: Long?): String {
|
||||||
|
val meters = distanceMeters ?: 0L
|
||||||
|
|
||||||
|
return if (meters < 1_000) {
|
||||||
|
val unit = if (meters == 1L) "mt" else "mts"
|
||||||
|
"$meters $unit away"
|
||||||
|
} else {
|
||||||
|
val km = meters / 1_000.0
|
||||||
|
val formatter = DecimalFormat("#.#") // 1 decimal if needed
|
||||||
|
val formattedKm = formatter.format(km)
|
||||||
|
|
||||||
|
val unit = if (formattedKm == "1") "km" else "kms"
|
||||||
|
"$formattedKm $unit away"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatAge(months: Int?): String {
|
||||||
|
val value = months ?: 0
|
||||||
|
|
||||||
|
val years = value / 12f
|
||||||
|
val roundedYears = (years * 10).roundToInt() / 10f
|
||||||
|
|
||||||
|
val unit = if (roundedYears == 1f) "Year" else "Years"
|
||||||
|
|
||||||
|
return "$roundedYears $unit"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.example.livingai_lg.ui.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun isKeyboardOpen(): Boolean {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
return WindowInsets.ime.getBottom(density) > 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.example.livingai_lg.ui.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import com.example.livingai_lg.ui.models.MediaType
|
||||||
|
|
||||||
|
fun createMediaUri(
|
||||||
|
context: Context,
|
||||||
|
type: MediaType
|
||||||
|
): Uri {
|
||||||
|
val directory = when (type) {
|
||||||
|
MediaType.PHOTO -> File(context.cacheDir, "images")
|
||||||
|
MediaType.VIDEO -> File(context.cacheDir, "videos")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileName = when (type) {
|
||||||
|
MediaType.PHOTO -> "${UUID.randomUUID()}.jpg"
|
||||||
|
MediaType.VIDEO -> "${UUID.randomUUID()}.mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
|
||||||
|
return FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getVideoThumbnail(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri
|
||||||
|
): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val retriever = MediaMetadataRetriever()
|
||||||
|
retriever.setDataSource(context, uri)
|
||||||
|
retriever.getFrameAtTime(0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="393dp"
|
||||||
|
android:height="852dp"
|
||||||
|
android:viewportWidth="393"
|
||||||
|
android:viewportHeight="852">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h393v852h-393z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h393v852h-393z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h393v852h-393z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="0"
|
||||||
|
android:startY="0"
|
||||||
|
android:endX="648.1"
|
||||||
|
android:endY="298.95"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFFFBEB"/>
|
||||||
|
<item android:offset="1" android:color="#FFEFF6FF"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h393.39v852.53h-393.39z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="0"
|
||||||
|
android:startY="0"
|
||||||
|
android:endX="648.67"
|
||||||
|
android:endY="299.32"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFFFBEB"/>
|
||||||
|
<item android:offset="0.5" android:color="#FFFEFCE8"/>
|
||||||
|
<item android:offset="1" android:color="#FFF7FEE7"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.11,128.78H111.87V230.77C111.87,236.29 107.39,240.77 101.87,240.77H42.11C36.59,240.77 32.11,236.29 32.11,230.77V128.78Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#C10007"
|
||||||
|
android:fillAlpha="0.09"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M56.11,138.78C56.11,133.26 60.59,128.78 66.11,128.78H77.87C83.4,128.78 87.87,133.26 87.87,138.78V176.77H56.11V138.78Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#7B3306"
|
||||||
|
android:fillAlpha="0.09"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16,690.55C16,688.34 17.79,686.55 20,686.55C22.21,686.55 24,688.34 24,690.55V730.55C24,732.76 22.21,734.55 20,734.55C17.79,734.55 16,732.76 16,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M29.99,690.55C29.99,688.34 31.78,686.55 33.99,686.55C36.2,686.55 37.99,688.34 37.99,690.55V730.55C37.99,732.76 36.2,734.55 33.99,734.55C31.78,734.55 29.99,732.76 29.99,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M61.99,690.55C61.99,688.34 63.78,686.55 65.99,686.55C68.2,686.55 69.99,688.34 69.99,690.55V730.55C69.99,732.76 68.2,734.55 65.99,734.55C63.78,734.55 61.99,732.76 61.99,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75.98,690.55C75.98,688.34 77.77,686.55 79.98,686.55C82.19,686.55 83.98,688.34 83.98,690.55V730.55C83.98,732.76 82.19,734.55 79.98,734.55C77.77,734.55 75.98,732.76 75.98,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M107.98,690.55C107.98,688.34 109.77,686.55 111.98,686.55C114.19,686.55 115.98,688.34 115.98,690.55V730.55C115.98,732.76 114.19,734.55 111.98,734.55C109.77,734.55 107.98,732.76 107.98,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M121.98,690.55C121.98,688.34 123.77,686.55 125.97,686.55C128.18,686.55 129.98,688.34 129.98,690.55V730.55C129.98,732.76 128.18,734.55 125.97,734.55C123.77,734.55 121.98,732.76 121.98,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M153.97,690.55C153.97,688.34 155.76,686.55 157.97,686.55C160.18,686.55 161.97,688.34 161.97,690.55V730.55C161.97,732.76 160.18,734.55 157.97,734.55C155.76,734.55 153.97,732.76 153.97,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M167.97,690.55C167.97,688.34 169.76,686.55 171.97,686.55C174.18,686.55 175.97,688.34 175.97,690.55V730.55C175.97,732.76 174.18,734.55 171.97,734.55C169.76,734.55 167.97,732.76 167.97,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M199.96,690.55C199.96,688.34 201.76,686.55 203.96,686.55C206.17,686.55 207.96,688.34 207.96,690.55V730.55C207.96,732.76 206.17,734.55 203.96,734.55C201.76,734.55 199.96,732.76 199.96,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M213.96,690.55C213.96,688.34 215.75,686.55 217.96,686.55C220.17,686.55 221.96,688.34 221.96,690.55V730.55C221.96,732.76 220.17,734.55 217.96,734.55C215.75,734.55 213.96,732.76 213.96,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M245.96,690.55C245.96,688.34 247.75,686.55 249.96,686.55C252.16,686.55 253.95,688.34 253.95,690.55V730.55C253.95,732.76 252.16,734.55 249.96,734.55C247.75,734.55 245.96,732.76 245.96,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M259.95,690.55C259.95,688.34 261.74,686.55 263.95,686.55C266.16,686.55 267.95,688.34 267.95,690.55V730.55C267.95,732.76 266.16,734.55 263.95,734.55C261.74,734.55 259.95,732.76 259.95,730.55V690.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16,707.55C16,705.9 17.34,704.55 19,704.55H374.4C376.05,704.55 377.39,705.9 377.39,707.55C377.39,709.21 376.05,710.55 374.4,710.55H19C17.34,710.55 16,709.21 16,707.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16,721.55C16,719.89 17.34,718.55 19,718.55H374.4C376.05,718.55 377.39,719.89 377.39,721.55C377.39,723.2 376.05,724.54 374.4,724.54H19C17.34,724.54 16,723.2 16,721.55Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#973C00"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M241.41,618.56C241.41,613.04 245.88,608.56 251.41,608.56H279.4C284.93,608.56 289.4,613.04 289.4,618.56V638.56C289.4,644.08 284.93,648.56 279.4,648.56H251.41C245.88,648.56 241.41,644.08 241.41,638.56V618.56Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#D08700"
|
||||||
|
android:fillAlpha="0.105"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M297.4,618.56C297.4,613.04 301.88,608.56 307.4,608.56H335.4C340.92,608.56 345.4,613.04 345.4,618.56V638.56C345.4,644.08 340.92,648.56 335.4,648.56H307.4C301.88,648.56 297.4,644.08 297.4,638.56V618.56Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#A65F00"
|
||||||
|
android:fillAlpha="0.105"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M241.41,662.55C241.41,657.03 245.88,652.55 251.41,652.55H279.4C284.93,652.55 289.4,657.03 289.4,662.55V682.55C289.4,688.07 284.93,692.55 279.4,692.55H251.41C245.88,692.55 241.41,688.07 241.41,682.55V662.55Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#D08700"
|
||||||
|
android:fillAlpha="0.105"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M63.99,622.07H123.99V562.07H63.99V622.07Z"
|
||||||
|
android:strokeAlpha="0.12"
|
||||||
|
android:fillColor="#33FFFFFF"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M264.9,203.67H312.9V155.67H264.9V203.67Z"
|
||||||
|
android:strokeAlpha="0.1"
|
||||||
|
android:fillColor="#33FFFFFF"
|
||||||
|
android:fillAlpha="0.1"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M79.99,770.99H115.99V734.99H79.99V770.99Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#33FFFFFF"
|
||||||
|
android:fillAlpha="0.15"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M123.56,762.54H153.56V732.54H123.56V762.54Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#33FFFFFF"
|
||||||
|
android:fillAlpha="0.15"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M161.74,770.99H197.74V734.99H161.74V770.99Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#33FFFFFF"
|
||||||
|
android:fillAlpha="0.15"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M281.4,79.99C281.4,62.32 295.73,48 313.4,48C331.07,48 345.4,62.32 345.4,79.99C345.4,97.66 331.07,111.99 313.4,111.99C295.73,111.99 281.4,97.66 281.4,79.99Z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#FDC700"
|
||||||
|
android:fillAlpha="0.12"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M115.94,127.99C115.94,119.15 123.1,111.99 131.94,111.99H147.94C156.77,111.99 163.93,119.15 163.93,127.99C163.93,136.82 156.77,143.99 147.94,143.99H131.94C123.1,143.99 115.94,136.82 115.94,127.99Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M167.93,128C167.93,116.95 176.88,108 187.92,108H203.92C214.97,108 223.92,116.95 223.92,128C223.92,139.04 214.97,147.99 203.92,147.99H187.92C176.88,147.99 167.93,139.04 167.93,128Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M227.91,127.99C227.91,119.15 235.07,111.99 243.91,111.99H251.91C260.74,111.99 267.91,119.15 267.91,127.99C267.91,136.82 260.74,143.99 251.91,143.99H243.91C235.07,143.99 227.91,136.82 227.91,127.99Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="393dp"
|
||||||
|
android:height="852dp"
|
||||||
|
android:viewportWidth="393"
|
||||||
|
android:viewportHeight="852">
|
||||||
|
<path
|
||||||
|
android:pathData="M341.53,596.63V574.64"
|
||||||
|
android:strokeWidth="1.2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#B5A89F"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M339.69,581.64C340.96,581.64 341.99,579.85 341.99,577.64C341.99,575.43 340.96,573.64 339.69,573.64C338.42,573.64 337.39,575.43 337.39,577.64C337.39,579.85 338.42,581.64 339.69,581.64Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M343.37,581.64C344.64,581.64 345.68,579.85 345.68,577.64C345.68,575.43 344.64,573.64 343.37,573.64C342.1,573.64 341.07,575.43 341.07,577.64C341.07,579.85 342.1,581.64 343.37,581.64Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M339.23,586.63C340.5,586.63 341.53,584.84 341.53,582.64C341.53,580.43 340.5,578.64 339.23,578.64C337.96,578.64 336.93,580.43 336.93,582.64C336.93,584.84 337.96,586.63 339.23,586.63Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M343.83,586.63C345.11,586.63 346.14,584.84 346.14,582.64C346.14,580.43 345.11,578.64 343.83,578.64C342.56,578.64 341.53,580.43 341.53,582.64C341.53,584.84 342.56,586.63 343.83,586.63Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M339.69,592.63C340.96,592.63 341.99,590.84 341.99,588.63C341.99,586.42 340.96,584.64 339.69,584.64C338.42,584.64 337.39,586.42 337.39,588.63C337.39,590.84 338.42,592.63 339.69,592.63Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M343.37,592.63C344.64,592.63 345.68,590.84 345.68,588.63C345.68,586.42 344.64,584.64 343.37,584.64C342.1,584.64 341.07,586.42 341.07,588.63C341.07,590.84 342.1,592.63 343.37,592.63Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M297.79,56.96H274.77C274.01,56.96 273.39,57.64 273.39,58.46V77.45C273.39,78.28 274.01,78.95 274.77,78.95H297.79C298.56,78.95 299.17,78.28 299.17,77.45V58.46C299.17,57.64 298.56,56.96 297.79,56.96Z"
|
||||||
|
android:strokeWidth="1.3"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M270.63,56.96L286.28,42.97L301.94,56.96"
|
||||||
|
android:strokeWidth="1.3"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M289.23,64.96H281.5C281.09,64.96 280.76,65.32 280.76,65.76V78.15C280.76,78.59 281.09,78.95 281.5,78.95H289.23C289.64,78.95 289.97,78.59 289.97,78.15V65.76C289.97,65.32 289.64,64.96 289.23,64.96Z"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M41.35,543.66C51.52,543.66 59.77,537.4 59.77,529.67C59.77,521.94 51.52,515.68 41.35,515.68C31.18,515.68 22.94,521.94 22.94,529.67C22.94,537.4 31.18,543.66 41.35,543.66Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.14,535.67C37.74,535.67 42.27,530.3 42.27,523.67C42.27,517.05 37.74,511.68 32.14,511.68C26.55,511.68 22.02,517.05 22.02,523.67C22.02,530.3 26.55,535.67 32.14,535.67Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M33.53,542.41C33.53,541.45 32.8,540.66 31.91,540.66C31.02,540.66 30.3,541.45 30.3,542.41V550.91C30.3,551.87 31.02,552.66 31.91,552.66C32.8,552.66 33.53,551.87 33.53,550.91V542.41Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M39.97,542.41C39.97,541.45 39.25,540.66 38.36,540.66C37.47,540.66 36.75,541.45 36.75,542.41V550.91C36.75,551.87 37.47,552.66 38.36,552.66C39.25,552.66 39.97,551.87 39.97,550.91V542.41Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M46.42,542.41C46.42,541.45 45.7,540.66 44.81,540.66C43.92,540.66 43.19,541.45 43.19,542.41V550.91C43.19,551.87 43.92,552.66 44.81,552.66C45.7,552.66 46.42,551.87 46.42,550.91V542.41Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M52.86,542.41C52.86,541.45 52.14,540.66 51.25,540.66C50.36,540.66 49.64,541.45 49.64,542.41V550.91C49.64,551.87 50.36,552.66 51.25,552.66C52.14,552.66 52.86,551.87 52.86,550.91V542.41Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M13.73,156.4V133.42"
|
||||||
|
android:strokeWidth="1.3"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#A8AFA0"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11.89,140.91C13.16,140.91 14.19,138.9 14.19,136.41C14.19,133.93 13.16,131.92 11.89,131.92C10.62,131.92 9.59,133.93 9.59,136.41C9.59,138.9 10.62,140.91 11.89,140.91Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M15.57,140.91C16.84,140.91 17.87,138.9 17.87,136.41C17.87,133.93 16.84,131.92 15.57,131.92C14.3,131.92 13.27,133.93 13.27,136.41C13.27,138.9 14.3,140.91 15.57,140.91Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11.43,146.91C12.7,146.91 13.73,144.89 13.73,142.41C13.73,139.93 12.7,137.91 11.43,137.91C10.16,137.91 9.13,139.93 9.13,142.41C9.13,144.89 10.16,146.91 11.43,146.91Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16.03,146.91C17.3,146.91 18.33,144.89 18.33,142.41C18.33,139.93 17.3,137.91 16.03,137.91C14.76,137.91 13.73,139.93 13.73,142.41C13.73,144.89 14.76,146.91 16.03,146.91Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11.89,153.9C13.16,153.9 14.19,151.89 14.19,149.41C14.19,146.92 13.16,144.91 11.89,144.91C10.62,144.91 9.59,146.92 9.59,149.41C9.59,151.89 10.62,153.9 11.89,153.9Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M15.57,153.9C16.84,153.9 17.87,151.89 17.87,149.41C17.87,146.92 16.84,144.91 15.57,144.91C14.3,144.91 13.27,146.92 13.27,149.41C13.27,151.89 14.3,153.9 15.57,153.9Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M273.85,741.54H248.99C248.23,741.54 247.61,742.21 247.61,743.04V764.02C247.61,764.85 248.23,765.52 248.99,765.52H273.85C274.62,765.52 275.23,764.85 275.23,764.02V743.04C275.23,742.21 274.62,741.54 273.85,741.54Z"
|
||||||
|
android:strokeWidth="1.4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M244.85,741.54L261.42,726.55L278,741.54"
|
||||||
|
android:strokeWidth="1.4"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M265.29,751.53H256.64C256.23,751.53 255.9,751.89 255.9,752.33V764.72C255.9,765.16 256.23,765.52 256.64,765.52H265.29C265.7,765.52 266.03,765.16 266.03,764.72V752.33C266.03,751.89 265.7,751.53 265.29,751.53Z"
|
||||||
|
android:strokeWidth="1.1"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M270.63,738.54C272.16,738.54 273.39,737.2 273.39,735.54C273.39,733.89 272.16,732.54 270.63,732.54C269.11,732.54 267.87,733.89 267.87,735.54C267.87,737.2 269.11,738.54 270.63,738.54Z"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M351.66,245.85C359.8,245.85 366.39,240.92 366.39,234.85C366.39,228.78 359.8,223.86 351.66,223.86C343.52,223.86 336.93,228.78 336.93,234.85C336.93,240.92 343.52,245.85 351.66,245.85Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M344.29,239.85C348.87,239.85 352.58,235.82 352.58,230.86C352.58,225.89 348.87,221.86 344.29,221.86C339.72,221.86 336.01,225.89 336.01,230.86C336.01,235.82 339.72,239.85 344.29,239.85Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M345.21,245.35C345.21,244.52 344.6,243.85 343.83,243.85C343.07,243.85 342.45,244.52 342.45,245.35V252.34C342.45,253.17 343.07,253.84 343.83,253.84C344.6,253.84 345.21,253.17 345.21,252.34V245.35Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M350.74,245.35C350.74,244.52 350.12,243.85 349.36,243.85C348.6,243.85 347.98,244.52 347.98,245.35V252.34C347.98,253.17 348.6,253.84 349.36,253.84C350.12,253.84 350.74,253.17 350.74,252.34V245.35Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M356.27,245.35C356.27,244.52 355.65,243.85 354.88,243.85C354.12,243.85 353.5,244.52 353.5,245.35V252.34C353.5,253.17 354.12,253.84 354.88,253.84C355.65,253.84 356.27,253.17 356.27,252.34V245.35Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M361.79,245.35C361.79,244.52 361.17,243.85 360.41,243.85C359.65,243.85 359.03,244.52 359.03,245.35V252.34C359.03,253.17 359.65,253.84 360.41,253.84C361.17,253.84 361.79,253.17 361.79,252.34V245.35Z"
|
||||||
|
android:fillColor="#B5A89F"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M32.14,799V780.01"
|
||||||
|
android:strokeWidth="1.1"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#A8AFA0"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M30.76,786.51C31.78,786.51 32.61,784.94 32.61,783.01C32.61,781.08 31.78,779.51 30.76,779.51C29.75,779.51 28.92,781.08 28.92,783.01C28.92,784.94 29.75,786.51 30.76,786.51Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M33.53,786.51C34.54,786.51 35.37,784.94 35.37,783.01C35.37,781.08 34.54,779.51 33.53,779.51C32.51,779.51 31.68,781.08 31.68,783.01C31.68,784.94 32.51,786.51 33.53,786.51Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M30.3,791.51C31.32,791.51 32.14,789.94 32.14,788.01C32.14,786.08 31.32,784.51 30.3,784.51C29.29,784.51 28.46,786.08 28.46,788.01C28.46,789.94 29.29,791.51 30.3,791.51Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M33.99,791.51C35,791.51 35.83,789.94 35.83,788.01C35.83,786.08 35,784.51 33.99,784.51C32.97,784.51 32.14,786.08 32.14,788.01C32.14,789.94 32.97,791.51 33.99,791.51Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M30.76,796.5C31.78,796.5 32.61,794.94 32.61,793.01C32.61,791.07 31.78,789.51 30.76,789.51C29.75,789.51 28.92,791.07 28.92,793.01C28.92,794.94 29.75,796.5 30.76,796.5Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M33.53,796.5C34.54,796.5 35.37,794.94 35.37,793.01C35.37,791.07 34.54,789.51 33.53,789.51C32.51,789.51 31.68,791.07 31.68,793.01C31.68,794.94 32.51,796.5 33.53,796.5Z"
|
||||||
|
android:fillColor="#A8AFA0"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="28dp"
|
||||||
|
android:height="28dp"
|
||||||
|
android:viewportWidth="28"
|
||||||
|
android:viewportHeight="28">
|
||||||
|
<path
|
||||||
|
android:pathData="M18.667,11.667C18.667,12.904 18.175,14.091 17.3,14.967C16.425,15.842 15.238,16.333 14,16.333C12.762,16.333 11.575,15.842 10.7,14.967C9.825,14.091 9.333,12.904 9.333,11.667"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.33333"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3.62,7.04H24.38"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.33333"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3.967,6.378C3.664,6.782 3.5,7.273 3.5,7.778V23.333C3.5,23.952 3.746,24.546 4.183,24.983C4.621,25.421 5.214,25.667 5.833,25.667H22.167C22.785,25.667 23.379,25.421 23.817,24.983C24.254,24.546 24.5,23.952 24.5,23.333V7.778C24.5,7.273 24.336,6.782 24.033,6.378L21.7,3.267C21.483,2.977 21.201,2.742 20.877,2.58C20.553,2.418 20.196,2.333 19.833,2.333H8.167C7.804,2.333 7.447,2.418 7.123,2.58C6.799,2.742 6.517,2.977 6.3,3.267L3.967,6.378Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.33333"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="28dp"
|
||||||
|
android:height="28dp"
|
||||||
|
android:viewportWidth="28"
|
||||||
|
android:viewportHeight="28">
|
||||||
|
<path
|
||||||
|
android:pathData="M22.158,24.49L13.995,19.825L5.831,24.49V5.831C5.831,5.212 6.077,4.619 6.514,4.182C6.952,3.744 7.545,3.499 8.163,3.499H19.826C20.444,3.499 21.037,3.744 21.475,4.182C21.912,4.619 22.158,5.212 22.158,5.831V24.49Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.33241"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue