Compare commits

...

17 Commits
main ... temp

Author SHA1 Message Date
ankitsaraf 04d06cd16d Fixes 2025-12-21 15:19:24 +05:30
ankitsaraf 4d7e8e3daf temporary changes. Will merge with the other branch. 2025-12-21 14:44:36 +05:30
Chandresh Kerkar 80cc72bb9d Updated Proper User Api 2025-12-21 02:18:00 +05:30
ankitsaraf 1277ae09b6 Navigate to BuyScreen if the user is already authenticated.
OTP input improvements.
2025-12-20 22:52:36 +05:30
ankitsaraf f8882c1dcc New simplified navigation.
Info overlay
api testing screen
2025-12-20 17:25:57 +05:30
Chandresh Kerkar feca34f892 Updated code 2025-12-20 14:47:14 +05:30
Chandresh Kerkar d46bc1f461 FIXED token issues but added logs 2025-12-20 02:20:19 +05:30
Chandresh Kerkar 88161f7933 Correctly working with correct routes 2025-12-20 02:08:12 +05:30
Chandresh Kerkar 682ed78491 Updated FIxed routes and Logout 2025-12-20 00:39:58 +05:30
Chandresh Kerkar 95c6c598a0 Resolve merge conflicts: keep local changes for AuthApiClient, models, and SignUpScreen 2025-12-19 23:38:30 +05:30
Chandresh Kerkar 078bc02c36 Upadated COde with authentication working 2025-12-19 23:24:09 +05:30
Chandresh Kerkar b60f5b9708 Merge branch 'UI_merge' of https://github.com/true1ck/Living_Ai_App into UI_merge
a
Please enter a commit message to explain why this merge is necessary,
2025-12-19 21:45:28 +05:30
Chandresh Kerkar 3c8288007a Working Signup 2025-12-19 21:43:38 +05:30
ankitsaraf 715b591b8a Minor fixes 2025-12-19 21:20:13 +05:30
ankitsaraf 6628daed50 otp page fix. 2025-12-19 20:24:17 +05:30
ankitsaraf 6b634f40b7 New screens and improvements 2025-12-19 18:59:01 +05:30
ankitsaraf 4f8ae88820 moved the UI repo
integrated existing logic with new screens
2025-12-16 20:12:51 +05:30
124 changed files with 14221 additions and 181 deletions

View File

@ -72,4 +72,19 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
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")
}

View File

@ -14,6 +14,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.LivingAi_Lg"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@ -25,6 +26,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</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>
</manifest>

View File

@ -4,14 +4,9 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
@ -19,39 +14,42 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.livingai_lg.ui.AuthState
import com.example.livingai_lg.ui.MainViewModel
import com.example.livingai_lg.ui.MainViewModelFactory
import com.example.livingai_lg.ui.login.*
import com.example.livingai_lg.ui.theme.LivingAi_LgTheme
import com.example.livingai_lg.ui.login_legacy.*
import com.example.livingai_lg.ui.navigation.AppNavigation
import com.example.livingai_lg.ui.theme.FarmMarketplaceTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
//enableEdgeToEdge()
setContent {
LivingAi_LgTheme {
FarmMarketplaceTheme {
val mainViewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current))
val authState by mainViewModel.authState.collectAsState()
when (authState) {
is AuthState.Unknown -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is AuthState.Authenticated -> {
SuccessScreen(mainViewModel)
}
is AuthState.Unauthenticated -> {
AuthNavigation()
}
}
AppNavigation(authState = authState, mainViewModel = mainViewModel)
// when (authState) {
// is AuthState.Unknown -> {
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// CircularProgressIndicator()
// }
// }
// is AuthState.Authenticated -> {
// SuccessScreen(mainViewModel)
// }
// is AuthState.Unauthenticated -> {
// AuthNavigation()
// }
// }
}
}
}
}
// TODO: remove the old code after testing new stuff
@Composable
fun AuthNavigation() {
val navController = rememberNavController()

View File

@ -2,6 +2,7 @@ package com.example.livingai_lg.api
import android.content.Context
import android.os.Build
import android.provider.Settings
import com.example.livingai_lg.BuildConfig
import io.ktor.client.*
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.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.util.Locale
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) {
private val route = "http://10.0.2.2:3000/"
private val tokenManager = TokenManager(context)
val client = HttpClient(CIO) {
@ -31,41 +40,87 @@ class AuthApiClient(private val context: Context) {
})
}
install(Auth) {
bearer {
loadTokens {
val accessToken = tokenManager.getAccessToken()
val refreshToken = tokenManager.getRefreshToken()
android.util.Log.d("AuthApiClient", "loadTokens: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}")
if (accessToken != null && refreshToken != null) {
android.util.Log.d("AuthApiClient", "loadTokens: Returning BearerTokens")
BearerTokens(accessToken, refreshToken)
} else {
android.util.Log.d("AuthApiClient", "loadTokens: No tokens available, returning null")
null
}
}
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") {
markAsRefreshTokenRequest()
contentType(ContentType.Application.Json)
setBody(RefreshRequest(refreshToken))
}.body()
android.util.Log.d("AuthApiClient", "refreshTokens: Refresh successful, saving new tokens")
tokenManager.saveTokens(response.accessToken, response.refreshToken)
android.util.Log.d("AuthApiClient", "refreshTokens: New tokens saved successfully")
tokenManager.saveTokens(response.accessToken, response.refreshToken)
BearerTokens(response.accessToken, response.refreshToken)
BearerTokens(response.accessToken, response.refreshToken)
} catch (e: Exception) {
android.util.Log.e("AuthApiClient", "refreshTokens: Refresh failed: ${e.message}", e)
throw e
}
}
}
}
defaultRequest {
url("http://10.0.2.2:3000/")
url(route)
}
}
// --- 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 {
client.post("auth/request-otp") {
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 {
val response: VerifyOtpResponse = client.post("auth/verify-otp") {
val response = client.post("auth/verify-otp") {
contentType(ContentType.Application.Json)
setBody(VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo()))
}.body()
setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo()))
}
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)
response
suspend fun signup(request: SignupRequest): Result<SignupResponse> = runCatching {
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 {
@ -91,7 +171,40 @@ class AuthApiClient(private val context: Context) {
}
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 {
@ -105,6 +218,55 @@ class AuthApiClient(private val context: Context) {
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 {
return DeviceInfo(
platform = "android",
@ -115,4 +277,8 @@ class AuthApiClient(private val context: Context) {
timezone = TimeZone.getDefault().id
)
}
private fun getDeviceId(): String {
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
}
}

View File

@ -8,6 +8,10 @@ class AuthManager(
private val apiClient: AuthApiClient,
private val tokenManager: TokenManager
) {
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> {
return apiClient.checkUser(phoneNumber)
}
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
return apiClient.requestOtp(phoneNumber)
}
@ -16,7 +20,22 @@ class AuthManager(
val deviceId = getDeviceId()
return apiClient.verifyOtp(phoneNumber, code, deviceId)
.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)
}
suspend fun logout(): Result<com.example.livingai_lg.api.LogoutResponse> {
return apiClient.logout()
}
private fun getDeviceId(): String {
return Settings.Secure.getString(
context.contentResolver,

View File

@ -2,6 +2,7 @@ package com.example.livingai_lg.api
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
@ -20,25 +21,47 @@ class TokenManager(context: Context) {
)
companion object {
private const val TAG = "TokenManager"
private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_REFRESH_TOKEN = "refresh_token"
}
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_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() {
Log.d(TAG, "clearTokens: Clearing all tokens")
prefs.edit()
.remove(KEY_ACCESS_TOKEN)
.remove(KEY_REFRESH_TOKEN)
.apply()
Log.d(TAG, "clearTokens: Tokens cleared")
}
}

View File

@ -23,7 +23,7 @@ data class DeviceInfo(
@Serializable
data class VerifyOtpRequest(
@SerialName("phone_number") val phoneNumber: String,
val code: String,
val code: Int,
@SerialName("device_id") val deviceId: String,
@SerialName("device_info") val deviceInfo: DeviceInfo? = null
)
@ -37,6 +37,34 @@ data class VerifyOtpResponse(
)
// 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
@Serializable
data class RefreshRequest(@SerialName("refresh_token") val refreshToken: String)
@ -60,15 +88,17 @@ data class User(
val id: String,
@SerialName("phone_number") val phoneNumber: String,
val name: String?,
val role: String,
@SerialName("user_type") val userType: String?
val role: String? = null, // Optional field - can be missing from JSON, defaults to null
@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
data class Location(
val city: String?,
val state: String?,
val pincode: String?
@SerialName("city_village") val cityVillage: String? = null,
val state: String? = null,
val pincode: String? = null
)
@Serializable
@ -88,3 +118,25 @@ data class UserDetails(
@Serializable
data class LogoutResponse(val ok: Boolean)
// 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

View File

@ -7,7 +7,10 @@ import androidx.lifecycle.viewModelScope
import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.TokenManager
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.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@ -37,30 +40,293 @@ class MainViewModel(context: Context) : ViewModel() {
val userState = _userState.asStateFlow()
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() {
viewModelScope.launch {
if (tokenManager.getAccessToken() != null) {
_authState.value = AuthState.Authenticated
fetchUserDetails()
val accessToken = tokenManager.getAccessToken()
val refreshToken = tokenManager.getRefreshToken()
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 {
// No tokens, user is not authenticated
Log.d(TAG, "checkAuthStatus: No tokens found, setting authState to 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() {
viewModelScope.launch {
_userState.value = UserState.Loading
authApiClient.getUserDetails()
.onSuccess {
_userState.value = UserState.Success(it)
_authState.value = AuthState.Authenticated
}
.onFailure {
_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)
}
}
}

View File

@ -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()
}
)
}
}
}

View File

@ -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)
)
}
}

View File

@ -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
)
}
}

View File

@ -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
)
}
}

View File

@ -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
)
}
}

View File

@ -0,0 +1,289 @@
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(
imageUrls = product.imageUrl ?: emptyList(),
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)
)
}
}

View File

@ -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 doesnt 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)
}
)
}
}
}
}
}

View File

@ -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
)
}

View File

@ -0,0 +1,58 @@
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(
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_filter),
contentDescription = "Filter",
tint = Color(0xFF0A0A0A),
modifier = Modifier.size(16.dp)
)
Text(
text = "Filter",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF0A0A0A)
)
}
}

View File

@ -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()
}
)
}
}
}

View File

@ -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)
)
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,146 @@
package com.example.livingai_lg.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ImageCarousel(
imageUrls: List<String>,
modifier: Modifier = Modifier
) {
when {
imageUrls.isEmpty() -> {
Box(
modifier = modifier
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text("No images", color = Color.White)
}
}
imageUrls.size == 1 -> {
AsyncImage(
model = imageUrls.first(),
contentDescription = null,
modifier = modifier,
contentScale = ContentScale.Crop
)
}
else -> {
val pagerState = rememberPagerState { imageUrls.size }
Box(modifier = modifier) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
AsyncImage(
model = imageUrls[page],
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
// Page Indicator (inside image)
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
repeat(imageUrls.size) { 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
)
}
}

View File

@ -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))
}
}
}

View File

@ -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 }
)
}
}

View File

@ -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()
}
)
}
}
}

View File

@ -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 }
)
}
}
}

View File

@ -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
)
}
}

View File

@ -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)
)
}
}
}

View File

@ -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()
}
)
}
}
}
}
}

View File

@ -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)
)
}
}

View File

@ -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)
)
}
}
}

View File

@ -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)
)
}
}

View File

@ -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
)
}
}
}

View File

@ -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.85f)
.background(Color(0xFFF7F4EE))
.clip(
RoundedCornerShape(
topEnd = 24.dp,
bottomEnd = 24.dp
)
)
) {
SortScreen(
onApplyClick = { selected ->
onApplyClick(selected)
onDismiss()
},
onCancelClick = onDismiss,
onBackClick = onDismiss
)
}
}
}
}

View File

@ -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
)
}
}
}

View File

@ -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()
)
}
}
}

View File

@ -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 assets 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)
)
}

View File

@ -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 assets 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
)
}
}

View File

@ -0,0 +1,80 @@
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
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
@Composable
fun WishlistNameOverlay(
onSave: (String) -> Unit,
onDismiss: () -> Unit
) {
var name by remember { mutableStateOf("") }
AnimatedVisibility(
visible = true,
enter = slideInVertically { -it },
exit = slideOutVertically { -it }
) {
Box(
Modifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp)
) {
Column(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
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
Button(
onClick = {
if (name.isNotBlank()) {
onSave(name)
}
}
) {
Text("Save")
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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 androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -14,7 +13,6 @@ import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
@ -52,11 +50,7 @@ fun CreateProfileScreen(navController: NavController, name: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
)
) {
Column(
modifier = Modifier
@ -105,7 +99,7 @@ fun ProfileTypeItem(text: String, icon: Int, onClick: () -> Unit) {
@Preview(showBackground = true)
@Composable
fun CreateProfileScreenPreview() {
LivingAi_LgTheme {
FarmMarketplaceTheme {
CreateProfileScreen(rememberNavController(), "John Doe")
}
}

View File

@ -1,4 +1,4 @@
package com.example.livingai_lg.ui.login
package com.example.livingai_lg.ui.login_legacy
import android.widget.Toast
import androidx.compose.foundation.background
@ -11,7 +11,7 @@ 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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -29,11 +29,11 @@ fun LoginScreen(navController: NavController) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
)
// .background(
// brush = Brush.linearGradient(
// colors = listOf(LightCream, LighterCream, LightestGreen)
// )
// )
) {
// Decorative elements...
@ -55,7 +55,7 @@ fun LoginScreen(navController: NavController) {
Box(
modifier = Modifier
.size(56.dp)
.background(Gold, RoundedCornerShape(28.dp)),
.background(Color.Yellow, RoundedCornerShape(28.dp)),
contentAlignment = Alignment.Center
) {
Text(text = "🌾", fontSize = 24.sp)
@ -64,7 +64,7 @@ fun LoginScreen(navController: NavController) {
Box(
modifier = Modifier
.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
) {
Text(text = "🌱", fontSize = 32.sp)
@ -73,7 +73,7 @@ fun LoginScreen(navController: NavController) {
Box(
modifier = Modifier
.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
) {
Text(text = "☀️", fontSize = 24.sp)
@ -86,13 +86,13 @@ fun LoginScreen(navController: NavController) {
text = "Welcome!",
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
color = DarkBrown
color = Color.Yellow
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Join the farm marketplace community",
fontSize = 16.sp,
color = MidBrown,
color = Color.Yellow,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(48.dp))
@ -101,12 +101,12 @@ fun LoginScreen(navController: NavController) {
Button(
onClick = { navController.navigate("signup") },
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = LightOrange),
colors = ButtonDefaults.buttonColors(containerColor = Color.Yellow),
modifier = Modifier
.fillMaxWidth()
.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))
@ -115,12 +115,12 @@ fun LoginScreen(navController: NavController) {
Button(
onClick = { navController.navigate("signin") },
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = TerraCotta),
colors = ButtonDefaults.buttonColors(containerColor = Color.Yellow),
modifier = Modifier
.fillMaxWidth()
.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))
@ -128,7 +128,7 @@ fun LoginScreen(navController: NavController) {
// Guest Button
Text(
text = "Continue as Guest",
color = MidBrown,
color = Color.Yellow,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
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)
@Composable
fun LoginScreenPreview() {
LivingAi_LgTheme {
FarmMarketplaceTheme() {
LoginScreen(rememberNavController())
}
}

View File

@ -1,8 +1,7 @@
package com.example.livingai_lg.ui.login
package com.example.livingai_lg.ui.login_legacy
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.text.KeyboardOptions
import androidx.compose.material3.*
@ -10,7 +9,6 @@ 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.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
@ -40,11 +38,11 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
)
// .background(
// brush = Brush.linearGradient(
// colors = listOf(LightCream, LighterCream, LightestGreen)
// )
// )
) {
Column(
modifier = Modifier
@ -60,7 +58,7 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
TextField(
value = otp.value,
onValueChange = { if (it.length <= 6) otp.value = it },
onValueChange = { if (it.length <= 6) otp.value = it },
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
@ -117,7 +115,7 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
@Preview(showBackground = true)
@Composable
fun OtpScreenPreview() {
LivingAi_LgTheme {
FarmMarketplaceTheme() {
OtpScreen(rememberNavController(), "+919876543210", "John Doe")
}
}

View File

@ -1,4 +1,4 @@
package com.example.livingai_lg.ui.login
package com.example.livingai_lg.ui.login_legacy
import android.widget.Toast
import androidx.compose.foundation.background
@ -14,7 +14,6 @@ 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.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
@ -42,11 +41,11 @@ fun SignInScreen(navController: NavController) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
)
// .background(
// brush = Brush.linearGradient(
// colors = listOf(LightCream, LighterCream, LightestGreen)
// )
// )
) {
Column(
modifier = Modifier
@ -161,7 +160,7 @@ fun SignInScreen(navController: NavController) {
@Preview(showBackground = true)
@Composable
fun SignInScreenPreview() {
LivingAi_LgTheme {
FarmMarketplaceTheme() {
SignInScreen(rememberNavController())
}
}

View File

@ -1,4 +1,4 @@
package com.example.livingai_lg.ui.login
package com.example.livingai_lg.ui.login_legacy
import android.widget.Toast
import androidx.compose.foundation.background
@ -15,7 +15,6 @@ 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.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
@ -45,11 +44,11 @@ fun SignUpScreen(navController: NavController) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
)
// .background(
// brush = Brush.linearGradient(
// colors = listOf(LightCream, LighterCream, LightestGreen)
// )
// )
) {
Column(
modifier = Modifier
@ -190,7 +189,7 @@ fun SignUpScreen(navController: NavController) {
@Preview(showBackground = true)
@Composable
fun SignUpScreenPreview() {
LivingAi_LgTheme {
FarmMarketplaceTheme {
SignUpScreen(rememberNavController())
}
}

View File

@ -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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -14,9 +12,6 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.livingai_lg.ui.MainViewModel
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
@Composable
@ -28,11 +23,12 @@ fun SuccessScreen(mainViewModel: MainViewModel = viewModel(factory = MainViewMod
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
),
// .background(
// brush = Brush.linearGradient(
// colors = listOf(LightCream, LighterCream, LightestGreen)
// )
// )
,
contentAlignment = Alignment.Center
) {
when (val state = userState) {

View File

@ -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

View File

@ -0,0 +1,89 @@
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 imageUrl: List<String>? = 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,
imageUrl = listOf("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","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"),
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,
imageUrl = listOf("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,
imageUrl = listOf("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
)
)

View File

@ -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", "🦜")
)

View File

@ -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),
)

View File

@ -0,0 +1,42 @@
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()
}

View File

@ -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)
)
}

View File

@ -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
)
)

View File

@ -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)
)
)

View File

@ -0,0 +1,4 @@
package com.example.livingai_lg.ui.models
class Seller {
}

View File

@ -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
)

View File

@ -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"
)
)
)

View File

@ -0,0 +1,32 @@
package com.example.livingai_lg.ui.models
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()
}
}

View File

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

View File

@ -0,0 +1,492 @@
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.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)
)
},
)
}
// 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() }
)
}
}
}

View File

@ -0,0 +1,79 @@
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 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"
}

View File

@ -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()
}

View File

@ -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)
}
}
)
}
}
}

View File

@ -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()
}
)
}
}
}

View File

@ -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()
// }
// )
// }
// }
}

View File

@ -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")
}
}
}
}
}
}

View File

@ -0,0 +1,435 @@
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.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.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) }
val animal = sampleAnimals.find { animal -> animal.id == animalId } ?: Animal(id = "null")
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF7F4EE))
.padding(12.dp)
) {
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(
imageUrls = animal.imageUrl ?: emptyList(),
modifier = Modifier.fillMaxSize()
)
// 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)
)
)
)
)
// Views indicator (top left)
Row(
modifier = Modifier
.align(Alignment.TopStart)
.padding(start = 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 = 24.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(bottom = 10.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
}
)
}
}
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)
)
}
}

View File

@ -0,0 +1,267 @@
package com.example.livingai_lg.ui.screens
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.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 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.sampleNotifications
import com.example.livingai_lg.ui.navigation.AppScreen
@Composable
fun BuyScreen(
onProductClick: (productId: String) -> Unit = {},
onBackClick: () -> Unit = {},
onNavClick: (route: String) -> Unit = {},
onFilterClick: () -> Unit = {},
onSortClick: () -> Unit = {},
onSellerClick: (sellerId: String) -> Unit = {},
) {
val selectedAnimalType = remember { mutableStateOf<String?>(null) }
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)
}
)
// Right-side actions (notifications, etc.)
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 =
) { }
// 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 })
}
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(
visible = showFilterOverlay.value,
onDismiss = { showFilterOverlay.value = false },
onSubmitClick = {
// 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
}
)
}
}

View File

@ -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))
}
}
}

View File

@ -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))
}
}
}

View File

@ -0,0 +1,569 @@
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.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.ArrowBack
import androidx.compose.material.icons.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.models.WishlistEntry
import com.example.livingai_lg.ui.models.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"
)
}
}
}
}
}
// 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)
)
}
}
}
}

View File

@ -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))
}
}
}

View File

@ -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)
)
}
}
}

View File

@ -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
)
}
}

View File

@ -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)
)
}
}

View File

@ -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
)
}
}
}

View File

@ -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)
)
}
}
}
}
}
}

View File

@ -0,0 +1,174 @@
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.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))
}
}
}

View File

@ -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))
}
}
}
}

View File

@ -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()
}
}

View File

@ -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))
}
}
}
}

View File

@ -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))
}
}
}
}

View File

@ -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)
)
}
}
}

View File

@ -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)
)
}
}
}
}
}

View File

@ -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
)
}
}
}
}
}

View File

@ -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
)
}
}

View File

@ -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)
)
}
}
}
}
}
}

View File

@ -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
}

View File

@ -2,25 +2,57 @@ package com.example.livingai_lg.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
// Farm Marketplace Color Palette
val FarmTextLight = Color(0xFFF5F1E8)
val FarmTextDark = Color(0xFF5D4E37) //Title
val FarmTextNormal = Color(0xFF8B7355)
val FarmTextLink = Color(0xFFE17100)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val FarmSproutBg = Color(0xFFFFCB79)
val FarmSproutIcon = Color(0xFF8B6F47)
// Custom Colors from Figma
val LightCream = Color(0xFFFFFBEB)
val LighterCream = Color(0xFFFEFCE8)
val LightestGreen = Color(0xFFF7FEE7)
val Maize = Color(0xFFF7C35F)
val TerraCotta = Color(0xFFD97D5D)
val DarkBrown = Color(0xFF5D4E37)
val MidBrown = Color(0xFF8B7355)
val DarkerBrown = Color(0xFF322410)
val OrangeBrown = Color(0xFFD4A574)
val DarkOrange = Color(0xFFE07A5F)
val Gold = Color(0xFFFFDD88)
val LightOrange = Color(0xFFFFB84D)
val DarkerOrange = Color(0xFFD96A4F)
val FarmWheatBg = Color(0xFFFFDD88)
val FarmWheatIcon = Color(0xFFD4A574)
val FarmSunBg = Color(0xFFFFB179)
val FarmSunIcon = Color(0xFFE07A5F)
val FarmGold = Color(0xFFFFB84D)
val FarmPink = Color(0xFFEDD5C8)
val FarmFence = Color(0xFFD4B5A0)
val FarmBox = Color(0xFFE8D9CC)
val FarmButtonPrimary = Color(0xFFFFB84D)
val FarmButtonSecondary = Color(0xFFE07A5F)
val FarmButtonText = Color(0xFF322410)
// 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)

View File

@ -3,56 +3,111 @@ package com.example.livingai_lg.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
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.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
private val lightScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
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(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
private val darkScheme = darkColorScheme(
primary = Color(0xFFFFDDBD),
onPrimary = Color(0xFF3E3220),
primaryContainer = Color(0xFF57472D),
onPrimaryContainer = Color(0xFFFFDDBD),
secondary = Color(0xFFFFDDBD),
onSecondary = Color(0xFF2B2415),
secondaryContainer = Color(0xFF6B5344),
onSecondaryContainer = Color(0xFFFFDDBD),
tertiary = Color(0xFFFFB893),
onTertiary = Color(0xFF3E2415),
tertiaryContainer = Color(0xFF8B5A3C),
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
fun LivingAi_LgTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
fun FarmMarketplaceTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
useDarkTheme -> darkScheme
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(
colorScheme = colorScheme,
typography = Typography,
typography = typography,
content = content
)
}
}

View File

@ -6,29 +6,110 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
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(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
letterSpacing = 0.15.sp,
),
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
fontSize = 14.sp,
lineHeight = 20.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(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
letterSpacing = 0.5.sp,
),
)

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,21V5C5,4.45 5.196,3.979 5.588,3.588C5.979,3.196 6.45,3 7,3H13V5H7V17.95L12,15.8L17,17.95V11H19V21L12,18L5,21ZM17,9V7H15V5H17V3H19V5H21V7H19V9H17Z"
android:fillColor="#ffffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M4,12H12V10H4V12ZM4,9H16V7H4V9ZM4,6H16V4H4V6ZM0,20V2C0,1.45 0.196,0.979 0.587,0.587C0.979,0.196 1.45,0 2,0H18C18.55,0 19.021,0.196 19.413,0.587C19.804,0.979 20,1.45 20,2V14C20,14.55 19.804,15.021 19.413,15.413C19.021,15.804 18.55,16 18,16H4L0,20ZM3.15,14H18V2H2V15.125L3.15,14Z"
android:fillColor="#ffffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="21dp"
android:viewportWidth="20"
android:viewportHeight="21">
<path
android:pathData="M16.85,20.975C16.717,20.975 16.592,20.954 16.475,20.913C16.358,20.871 16.25,20.8 16.15,20.7L11.05,15.6C10.95,15.5 10.879,15.392 10.837,15.275C10.796,15.158 10.775,15.033 10.775,14.9C10.775,14.767 10.796,14.642 10.837,14.525C10.879,14.408 10.95,14.3 11.05,14.2L13.175,12.075C13.275,11.975 13.383,11.904 13.5,11.863C13.617,11.821 13.742,11.8 13.875,11.8C14.008,11.8 14.133,11.821 14.25,11.863C14.367,11.904 14.475,11.975 14.575,12.075L19.675,17.175C19.775,17.275 19.846,17.383 19.888,17.5C19.929,17.617 19.95,17.742 19.95,17.875C19.95,18.008 19.929,18.133 19.888,18.25C19.846,18.367 19.775,18.475 19.675,18.575L17.55,20.7C17.45,20.8 17.342,20.871 17.225,20.913C17.108,20.954 16.983,20.975 16.85,20.975ZM16.85,18.6L17.575,17.875L13.9,14.2L13.175,14.925L16.85,18.6ZM3.125,21C2.992,21 2.862,20.975 2.737,20.925C2.612,20.875 2.5,20.8 2.4,20.7L0.3,18.6C0.2,18.5 0.125,18.388 0.075,18.263C0.025,18.138 0,18.008 0,17.875C0,17.742 0.025,17.617 0.075,17.5C0.125,17.383 0.2,17.275 0.3,17.175L5.6,11.875H7.725L8.575,11.025L4.45,6.9H3.025L0,3.875L2.825,1.05L5.85,4.075V5.5L9.975,9.625L12.875,6.725L11.8,5.65L13.2,4.25H10.375L9.675,3.55L13.225,0L13.925,0.7V3.525L15.325,2.125L18.875,5.675C19.158,5.958 19.375,6.279 19.525,6.637C19.675,6.996 19.75,7.375 19.75,7.775C19.75,8.175 19.675,8.558 19.525,8.925C19.375,9.292 19.158,9.617 18.875,9.9L16.75,7.775L15.35,9.175L14.3,8.125L9.125,13.3V15.4L3.825,20.7C3.725,20.8 3.617,20.875 3.5,20.925C3.383,20.975 3.258,21 3.125,21ZM3.125,18.6L7.375,14.35V13.625H6.65L2.4,17.875L3.125,18.6ZM3.125,18.6L2.4,17.875L2.775,18.225L3.125,18.6Z"
android:fillColor="#ffffffff"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More