Compare commits
17 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
04d06cd16d | |
|
|
4d7e8e3daf | |
|
|
80cc72bb9d | |
|
|
1277ae09b6 | |
|
|
f8882c1dcc | |
|
|
feca34f892 | |
|
|
d46bc1f461 | |
|
|
88161f7933 | |
|
|
682ed78491 | |
|
|
95c6c598a0 | |
|
|
078bc02c36 | |
|
|
b60f5b9708 | |
|
|
3c8288007a | |
|
|
715b591b8a | |
|
|
6628daed50 | |
|
|
6b634f40b7 | |
|
|
4f8ae88820 |
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
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: Calling /auth/refresh endpoint")
|
||||
try {
|
||||
val response: RefreshResponse = client.post("${route}auth/refresh") {
|
||||
markAsRefreshTokenRequest()
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(RefreshRequest(refreshToken))
|
||||
}.body()
|
||||
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||
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")
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||
response
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun ActionPopup(
|
||||
visible: Boolean,
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = Color.Black,
|
||||
contentColor: Color = Color.White,
|
||||
autoDismissMillis: Long = 5000L,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
// Auto dismiss
|
||||
LaunchedEffect(visible) {
|
||||
if (visible) {
|
||||
delay(autoDismissMillis)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn() + slideInVertically { it / 2 },
|
||||
exit = fadeOut() + slideOutVertically { it / 2 },
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.shadow(12.dp, RoundedCornerShape(50))
|
||||
.background(backgroundColor, RoundedCornerShape(50))
|
||||
.clickable(
|
||||
enabled = onClick != null,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick?.invoke()
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
color = contentColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = contentColor.copy(alpha = 0.8f),
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun AdSpaceBanner(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String = "AD SPACE"
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(44.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFFF0000).copy(alpha = 0.97f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.White, RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.LibraryAdd
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.MyLocation
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.UserAddress
|
||||
|
||||
@Composable
|
||||
fun AddressSelectorOverlay(
|
||||
visible: Boolean,
|
||||
addresses: List<UserAddress>,
|
||||
selectedAddressId: String?,
|
||||
onSelect: (String) -> Unit,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { -it }
|
||||
) + fadeIn(),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { -it }
|
||||
) + fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF4F4F4))
|
||||
) {
|
||||
|
||||
Column {
|
||||
AddressSelectorHeader(onClose)
|
||||
AddressSearchBar()
|
||||
AddressActionsRow()
|
||||
SavedAddressesList(
|
||||
addresses = addresses,
|
||||
selectedAddressId = selectedAddressId,
|
||||
onSelect = onSelect
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AddressSelectorHeader(onClose: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
Text(
|
||||
text = "Select Your Location",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddressSearchBar() {
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Search an area or address") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
trailingIcon = {
|
||||
Icon(Icons.Default.Search, contentDescription = null)
|
||||
},
|
||||
enabled = false // 👈 explicitly disabled for now
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddressActionsRow() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
AddressActionCard(Icons.Default.MyLocation,"Use Current Location")
|
||||
AddressActionCard(Icons.Default.LibraryAdd,"Add New Address")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SavedAddressesList(
|
||||
addresses: List<UserAddress>,
|
||||
selectedAddressId: String?,
|
||||
onSelect: (String) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "SAVED ADDRESSES",
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
addresses.forEach { address ->
|
||||
AddressItem(
|
||||
address = address,
|
||||
selected = address.id == selectedAddressId,
|
||||
onClick = { onSelect(address.id) }
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddressItem(
|
||||
address: UserAddress,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White, RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Home, contentDescription = null)
|
||||
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(address.name, fontWeight = FontWeight.Medium)
|
||||
if (selected) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"SELECTED",
|
||||
fontSize = 10.sp,
|
||||
color = Color.Green
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(address.address, fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
|
||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddressActionCard(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.size(110.dp)
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.clickable { onClick() }
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.AnimalType
|
||||
|
||||
|
||||
@Composable
|
||||
fun AnimalTypeSelector(
|
||||
animalTypes: List<AnimalType>,
|
||||
selectedAnimalType: String?,
|
||||
onAnimalTypeSelected: (String) -> Unit
|
||||
) {
|
||||
val selectedAnimalType: String = selectedAnimalType ?: ""
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(animalTypes.size) { index ->
|
||||
AnimalTypeButton(
|
||||
animalType = animalTypes[index],
|
||||
isSelected = selectedAnimalType == animalTypes[index].id,
|
||||
onClick = { onAnimalTypeSelected(animalTypes[index].id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimalTypeButton(
|
||||
animalType: AnimalType,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(
|
||||
if (isSelected) Color(0xFFEDE9FE) else Color.White,
|
||||
RoundedCornerShape(24.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = animalType.emoji,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = animalType.name,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.BottomNavItemData
|
||||
|
||||
@Composable
|
||||
fun BottomNavigationBar(
|
||||
modifier: Modifier = Modifier,
|
||||
items: List<BottomNavItemData>,
|
||||
currentItem: String,
|
||||
onItemClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFF000000).copy(alpha = 0.1f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(66.dp)
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
items.forEach { item ->
|
||||
val isSelected = item.label == currentItem
|
||||
BottomNavItem(
|
||||
label = item.label,
|
||||
iconRes = item.iconRes,
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (!isSelected) {
|
||||
onItemClick(item.route)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomNavItem(
|
||||
label: String,
|
||||
iconRes: Any,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
val color = if (selected) Color(0xFF1E88E5) else Color(0xFF0A0A0A)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick)
|
||||
) {
|
||||
if(iconRes is Int) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
contentDescription = label,
|
||||
tint = color,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else if(iconRes is ImageVector) {
|
||||
Icon(
|
||||
imageVector = iconRes,
|
||||
contentDescription = label,
|
||||
tint = color,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DropdownInput(
|
||||
label: String? = null, // optional label
|
||||
selected: String,
|
||||
options: List<String>,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
onSelect: (String) -> Unit,
|
||||
placeholder: String = "Select", // NEW - custom placeholder
|
||||
textColor: Color = Color.Black,
|
||||
modifier: Modifier = Modifier // NEW - allows width control
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier, // <-- now caller can control width
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Optional label
|
||||
// if (label != null) {
|
||||
Text(
|
||||
text = label?:" ",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
// } else {
|
||||
// // Reserve label space so layout doesn’t shift
|
||||
// Spacer(modifier = Modifier.height(20.dp)) // ← same height as label text line
|
||||
// }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { onExpandedChange(!expanded) }
|
||||
) {
|
||||
// Anchor box
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onExpandedChange(true)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
// Custom placeholder support
|
||||
Text(
|
||||
text = selected.ifEmpty { placeholder },
|
||||
fontSize = AppTypography.Body,
|
||||
color = if (selected.isEmpty()) Color(0xFF99A1AF) else textColor
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = FarmTextDark
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Material3 Dropdown
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { onExpandedChange(false) },
|
||||
modifier = Modifier.background(Color.White)
|
||||
) {
|
||||
options.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(item, fontSize = AppTypography.Body, color = FarmTextDark)
|
||||
},
|
||||
onClick = {
|
||||
onSelect(item)
|
||||
onExpandedChange(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
|
||||
@Composable
|
||||
fun FarmHeader() {
|
||||
Row {
|
||||
Text(
|
||||
text = "Farm",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFE17100)
|
||||
)
|
||||
Text(
|
||||
text = "Market",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Find your perfect livestock",
|
||||
fontSize = 16.sp,
|
||||
color = FarmTextNormal
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
|
||||
@Composable
|
||||
fun FilterOverlay(
|
||||
visible: Boolean,
|
||||
appliedFilters: FiltersState,
|
||||
onDismiss: () -> Unit,
|
||||
onSubmitClick: (filters: FiltersState) -> Unit = {},
|
||||
) {
|
||||
BackHandler(enabled = visible) { onDismiss() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Dimmed background
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
// Sliding panel
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInHorizontally(
|
||||
initialOffsetX = { it } // from right
|
||||
),
|
||||
exit = slideOutHorizontally(
|
||||
targetOffsetX = { it } // to right
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.CenterEnd)
|
||||
) {
|
||||
FilterScreen(
|
||||
appliedFilters,
|
||||
onBackClick = onDismiss,
|
||||
onCancelClick = onDismiss,
|
||||
onSubmitClick = { filters ->
|
||||
onSubmitClick(filters)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import android.media.Image
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Chat
|
||||
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||
import androidx.compose.material.icons.outlined.Chat
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun FloatingActionBar(
|
||||
modifier: Modifier = Modifier,
|
||||
showContainer: Boolean = true,
|
||||
onChatClick: () -> Unit = {},
|
||||
onCallClick: () -> Unit = {},
|
||||
onLocationClick: () -> Unit = {},
|
||||
onBookmarkClick: () -> Unit = {}
|
||||
) {
|
||||
val containerModifier =
|
||||
if (showContainer) {
|
||||
Modifier
|
||||
.shadow(
|
||||
elevation = 12.dp,
|
||||
shape = RoundedCornerShape(50),
|
||||
clip = false
|
||||
)
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
} else {
|
||||
Modifier // no background, no shadow
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
//.padding(horizontal = 24.dp)
|
||||
.then(containerModifier)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FloatingActionIcon(Icons.AutoMirrored.Outlined.Chat, onChatClick)
|
||||
FloatingActionIcon(Icons.Outlined.Phone, onCallClick)
|
||||
FloatingActionIcon(Icons.Outlined.LocationOn, onLocationClick)
|
||||
FloatingActionIcon(Icons.Outlined.BookmarkAdd, onBookmarkClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FloatingActionIcon(
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
clip = false
|
||||
)
|
||||
.background(Color.White, RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun IconCircle(
|
||||
backgroundColor: Color,
|
||||
size: Dp,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.shadow(
|
||||
elevation = 4.dp,
|
||||
shape = CircleShape
|
||||
)
|
||||
.background(backgroundColor, shape = CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.example.livingai_lg.ui.screens.align
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
@Composable
|
||||
fun InfoOverlay(
|
||||
visible: Boolean,
|
||||
title: String,
|
||||
text: String,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
BackHandler(enabled = visible) { onDismiss() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Dimmed background
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
// Sliding content
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically { it }, // from bottom
|
||||
exit = slideOutVertically { it },
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
) {
|
||||
InfoOverlayContent(
|
||||
title = title,
|
||||
text = text,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun InfoOverlayContent(
|
||||
title: String,
|
||||
text: String,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Color.White,
|
||||
RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = AppTypography.Title,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = AppTypography.Body,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.zIndex
|
||||
|
||||
@Composable
|
||||
fun InfoTooltipOverlay(
|
||||
text: String,
|
||||
visible: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
if (!visible) return
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(100f) // ensure it overlays everything
|
||||
) {
|
||||
// Dimmed background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
|
||||
// Tooltip card
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(Color.Black, RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.White,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoIconWithOverlay(
|
||||
infoText: String
|
||||
) {
|
||||
var showInfo by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "Info",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clickable { showInfo = true }
|
||||
)
|
||||
|
||||
InfoTooltipOverlay(
|
||||
text = infoText,
|
||||
visible = showInfo,
|
||||
onDismiss = { showInfo = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
|
||||
@Composable
|
||||
fun InputField(
|
||||
label: String,
|
||||
value: String,
|
||||
placeholder: String,
|
||||
onChange: (String) -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
color = FarmTextDark
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { inner ->
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFF99A1AF)
|
||||
)
|
||||
}
|
||||
inner()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.outlined.CameraAlt
|
||||
import androidx.compose.material.icons.outlined.Videocam
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.models.MediaType
|
||||
import com.example.livingai_lg.ui.models.MediaUpload
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.example.livingai_lg.ui.utils.createMediaUri
|
||||
import com.example.livingai_lg.ui.utils.getVideoThumbnail
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MediaPickerCard(
|
||||
upload: MediaUpload,
|
||||
modifier: Modifier = Modifier,
|
||||
onUriSelected: (Uri?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showSheet by remember { mutableStateOf(false) }
|
||||
var cameraUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var showVideoPlayer by remember { mutableStateOf(false) }
|
||||
|
||||
// Gallery
|
||||
val galleryLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri ->
|
||||
if (uri != null) onUriSelected(uri)
|
||||
}
|
||||
|
||||
// Camera (photo)
|
||||
val imageCameraLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.TakePicture()
|
||||
) { success ->
|
||||
if (success) onUriSelected(cameraUri) else cameraUri = null
|
||||
}
|
||||
|
||||
// Camera (video)
|
||||
val videoCameraLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CaptureVideo()
|
||||
) { success ->
|
||||
if (success) onUriSelected(cameraUri) else cameraUri = null
|
||||
}
|
||||
|
||||
/* ---------- Picker Sheet (NO delete here anymore) ---------- */
|
||||
|
||||
if (showSheet) {
|
||||
ModalBottomSheet(onDismissRequest = { showSheet = false }) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Select Media",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text("Camera") },
|
||||
leadingContent = { Icon(Icons.Outlined.CameraAlt, null) },
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
showSheet = false
|
||||
cameraUri = createMediaUri(context, upload.type)
|
||||
|
||||
if (upload.type == MediaType.PHOTO) {
|
||||
imageCameraLauncher.launch(cameraUri!!)
|
||||
} else {
|
||||
videoCameraLauncher.launch(cameraUri!!)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text("Gallery") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
if (upload.type == MediaType.PHOTO)
|
||||
Icons.Outlined.CameraAlt
|
||||
else
|
||||
Icons.Outlined.Videocam,
|
||||
null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
showSheet = false
|
||||
galleryLauncher.launch(
|
||||
if (upload.type == MediaType.PHOTO) "image/*" else "video/*"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Card ---------- */
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(204.dp)
|
||||
.shadow(0.5.dp, RoundedCornerShape(8.dp))
|
||||
.background(Color.White, RoundedCornerShape(8.dp))
|
||||
.border(1.dp, Color(0x1A000000), RoundedCornerShape(8.dp))
|
||||
.combinedClickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
if (upload.uri != null && upload.type == MediaType.VIDEO) {
|
||||
showVideoPlayer = true
|
||||
} else {
|
||||
showSheet = true
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (upload.uri != null) {
|
||||
showSheet = true
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
/* ---------- Media Preview ---------- */
|
||||
|
||||
if (upload.uri != null) {
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
if (upload.type == MediaType.VIDEO) {
|
||||
|
||||
val thumbnail = remember(upload.uri) {
|
||||
getVideoThumbnail(context, upload.uri!!)
|
||||
}
|
||||
|
||||
if (thumbnail != null) {
|
||||
Image(
|
||||
bitmap = thumbnail.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
// ▶ Play overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(56.dp)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
RoundedCornerShape(28.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play Video",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Photo
|
||||
AsyncImage(
|
||||
model = upload.uri,
|
||||
contentDescription = upload.label,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- ❌ Remove Button ---------- */
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove media",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(6.dp)
|
||||
.size(24.dp)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
RoundedCornerShape(12.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onUriSelected(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
/* ---------- Empty State ---------- */
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (upload.type == MediaType.PHOTO)
|
||||
Icons.Outlined.CameraAlt
|
||||
else
|
||||
Icons.Outlined.Videocam,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF717182),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (upload.type == MediaType.PHOTO) "Add Photo" else "Add Video",
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
|
||||
if (upload.label.isNotEmpty()) {
|
||||
Text(upload.label, color = Color(0xFF717182))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Video Player ---------- */
|
||||
|
||||
if (showVideoPlayer && upload.uri != null) {
|
||||
VideoPlayerDialog(
|
||||
uri = upload.uri!!,
|
||||
onDismiss = { showVideoPlayer = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.example.livingai_lg.ui.models.AppNotification
|
||||
import com.example.livingai_lg.ui.screens.NotificationsScreen
|
||||
|
||||
@Composable
|
||||
fun NotificationsOverlay(
|
||||
visible: Boolean,
|
||||
notifications: List<AppNotification>,
|
||||
onClose: () -> Unit,
|
||||
onDismiss: (String) -> Unit,
|
||||
onNotificationClick: (String) -> Unit = {}
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically { -it },
|
||||
exit = slideOutVertically { -it }
|
||||
) {
|
||||
NotificationsScreen(
|
||||
notifications = notifications,
|
||||
onBackClick = onClose,
|
||||
onDismiss = onDismiss,
|
||||
onNotificationClick = onNotificationClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
@Composable
|
||||
fun OptionCard(
|
||||
label: String,
|
||||
icon: Any,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color = Color.Unspecified,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(90.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0xFFF3F4F6), RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Icon container
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.background(iconBackgroundColor, RoundedCornerShape(14.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (icon) {
|
||||
is ImageVector -> {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = iconTint
|
||||
)
|
||||
}
|
||||
is Int -> {
|
||||
Icon(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = label,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Dot indicator
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.background(Color(0xFF6B7280), CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
|
||||
@Composable
|
||||
fun PhoneNumberInput(
|
||||
phone: String,
|
||||
onChange: (String) -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
||||
Text(
|
||||
text = "Enter Phone Number*",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.height(52.dp)
|
||||
) {
|
||||
|
||||
// Country code box
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(65.dp)
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("+91", fontSize = 16.sp, color = FarmTextDark)
|
||||
}
|
||||
|
||||
// Phone input field
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Phone,
|
||||
contentDescription = "Phone",
|
||||
tint = Color(0xFF99A1AF),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
|
||||
BasicTextField(
|
||||
value = phone,
|
||||
onValueChange = onChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
textStyle = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = FarmTextDark
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { inner ->
|
||||
if (phone.isEmpty()) {
|
||||
Text(
|
||||
"Enter your Phone Number",
|
||||
fontSize = 15.sp,
|
||||
color = Color(0xFF99A1AF)
|
||||
)
|
||||
}
|
||||
inner()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun RangeFilter(
|
||||
modifier: Modifier = Modifier,
|
||||
|
||||
label: String,
|
||||
min: Int,
|
||||
max: Int,
|
||||
valueFrom: Int,
|
||||
valueTo: Int,
|
||||
modified: Boolean = false,
|
||||
onValueChange: (from: Int, to: Int) -> Unit,
|
||||
showSlider: Boolean = true,
|
||||
valueFormatter: (Int) -> String = { it.toString() }
|
||||
) {
|
||||
var fromValue by remember(valueFrom) { mutableStateOf(valueFrom) }
|
||||
var toValue by remember(valueTo) { mutableStateOf(valueTo) }
|
||||
|
||||
Column(modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
|
||||
// Label
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF364153)
|
||||
)
|
||||
|
||||
// Slider (optional)
|
||||
if (showSlider) {
|
||||
RangeSlider(
|
||||
value = fromValue.toFloat()..toValue.toFloat(),
|
||||
onValueChange = { range ->
|
||||
fromValue = range.start.roundToInt()
|
||||
.coerceIn(min, toValue)
|
||||
toValue = range.endInclusive.roundToInt()
|
||||
.coerceIn(fromValue, max)
|
||||
|
||||
onValueChange(fromValue, toValue)
|
||||
},
|
||||
valueRange = min.toFloat()..max.toFloat(),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = Color(0xFFD9D9D9),
|
||||
activeTrackColor = Color(0xFFD9D9D9),
|
||||
inactiveTrackColor = Color(0xFFE5E7EB)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Pills
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RangePill(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = fromValue,
|
||||
modified = modified,
|
||||
onValueChange = { newFrom ->
|
||||
val safeFrom = newFrom.coerceIn(min, toValue)
|
||||
fromValue = safeFrom
|
||||
onValueChange(safeFrom, toValue)
|
||||
},
|
||||
formatter = valueFormatter
|
||||
)
|
||||
|
||||
Text("to", fontSize = 15.sp)
|
||||
|
||||
RangePill(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = toValue,
|
||||
modified = modified,
|
||||
onValueChange = { newTo ->
|
||||
val safeTo = newTo.coerceIn(fromValue, max)
|
||||
toValue = safeTo
|
||||
onValueChange(fromValue, safeTo)
|
||||
},
|
||||
formatter = valueFormatter
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RangePill(
|
||||
modifier: Modifier = Modifier,
|
||||
value: Int,
|
||||
modified: Boolean = false,
|
||||
onValueChange: (Int) -> Unit,
|
||||
formatter: (Int) -> String
|
||||
) {
|
||||
var text by remember(value) {
|
||||
mutableStateOf(formatter(value))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(30.dp)
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0x12000000), RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
var modified = modified;
|
||||
BasicTextField(
|
||||
value = text,
|
||||
onValueChange = { input ->
|
||||
val digits = input.filter { it.isDigit() }
|
||||
text = digits
|
||||
digits.toIntOrNull()?.let(onValueChange)
|
||||
},
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = if(modified) Color.Black else Color(0xFF99A1AF)
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun RatingStars(
|
||||
rating: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
starSize: Dp = 14.dp,
|
||||
color: Color = Color(0xFFDE9A07)
|
||||
) {
|
||||
val fullStars = rating.toInt()
|
||||
val remainder = rating - fullStars
|
||||
|
||||
val showHalfStar = remainder in 0.25f..0.74f
|
||||
val extraFullStar = remainder >= 0.75f
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Full stars
|
||||
repeat(
|
||||
if (extraFullStar) fullStars + 1 else fullStars
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(starSize)
|
||||
)
|
||||
}
|
||||
|
||||
// Half star (only if not rounded up)
|
||||
if (showHalfStar) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.StarHalf,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(starSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun SortButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.border(
|
||||
1.078.dp,
|
||||
Color(0xFF000000).copy(alpha = 0.1f),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.White, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_sort),
|
||||
contentDescription = "Sort",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Sort by",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.SortDirection
|
||||
import com.example.livingai_lg.ui.models.SortField
|
||||
|
||||
@Composable
|
||||
fun SortItem(
|
||||
field: SortField,
|
||||
onToggle: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val icon = when (field.direction) {
|
||||
SortDirection.ASC -> Icons.Default.ArrowUpward
|
||||
SortDirection.DESC -> Icons.Default.ArrowDownward
|
||||
SortDirection.NONE -> Icons.Default.ArrowDropDown
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.background(Color.White, RoundedCornerShape(12.dp))
|
||||
.border(1.dp, Color(0x1A000000), RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onToggle() }
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = field.label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF364153)
|
||||
)
|
||||
|
||||
// Sort priority indicator
|
||||
field.order?.let {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(Color.Black, RoundedCornerShape(9.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = it.toString(),
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = if (field.direction == SortDirection.NONE)
|
||||
Color(0xFF9CA3AF)
|
||||
else
|
||||
Color.Black
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai_lg.ui.models.SortField
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
import com.example.livingai_lg.ui.screens.SortScreen
|
||||
|
||||
@Composable
|
||||
fun SortOverlay(
|
||||
visible: Boolean,
|
||||
onApplyClick: (selected: List<SortField>) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
BackHandler(enabled = visible) { onDismiss() }
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// Dim background
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
// Slide-in panel from LEFT
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInHorizontally(
|
||||
initialOffsetX = { -it }
|
||||
),
|
||||
exit = slideOutHorizontally(
|
||||
targetOffsetX = { -it }
|
||||
),
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(0.85f)
|
||||
.fillMaxWidth(0.85f)
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = 24.dp,
|
||||
bottomEnd = 24.dp
|
||||
)
|
||||
)
|
||||
) {
|
||||
SortScreen(
|
||||
onApplyClick = { selected ->
|
||||
onApplyClick(selected)
|
||||
onDismiss()
|
||||
},
|
||||
onCancelClick = onDismiss,
|
||||
onBackClick = onDismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.models.UserAddress
|
||||
import com.example.livingai_lg.ui.models.UserProfile
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun UserLocationHeader(
|
||||
user: UserProfile,
|
||||
selectedAddressId: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenAddressOverlay: () -> Unit,
|
||||
onProfileClick: () -> Unit = {} // New callback for profile icon click
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.wrapContentWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val selectedAddress = user.addresses.find { it.id == selectedAddressId } ?: user.addresses.first()
|
||||
|
||||
// Profile image - make it clickable
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onProfileClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (user.profileImageUrl != null) {
|
||||
AsyncImage(
|
||||
model = user.profileImageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_profile),
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Address + arrow (click opens overlay)
|
||||
Column(
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onOpenAddressOverlay()
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = selectedAddress.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Black
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = selectedAddress.address,
|
||||
fontSize = 13.sp,
|
||||
color = Color.Black.copy(alpha = 0.7f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
|
||||
@Composable
|
||||
fun VideoPlayerDialog(
|
||||
uri: Uri,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val player = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItem(MediaItem.fromUri(uri))
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
player.release()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16 / 9f)
|
||||
.background(Color.Black)
|
||||
) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
PlayerView(it).apply {
|
||||
this.player = player
|
||||
useController = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.example.livingai_lg.ui.components.backgrounds
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import kotlin.math.min
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
|
||||
@Composable
|
||||
fun DecorativeBackground() {
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val screenW = maxWidth.value
|
||||
val screenH = maxHeight.value
|
||||
|
||||
// Original bg image design size (YOUR asset’s intended size)
|
||||
val designW = 393f
|
||||
val designH = 852f
|
||||
|
||||
// Scale factor that preserves aspect ratio
|
||||
val scale = min(screenW / designW, screenH / designH)
|
||||
|
||||
//------------------------------
|
||||
// Helper to scale dp offsets
|
||||
//------------------------------
|
||||
fun s(value: Float) = (value * scale).dp
|
||||
|
||||
//------------------------------
|
||||
// Background Image
|
||||
//------------------------------
|
||||
Image(
|
||||
painter = painterResource(R.drawable.bg),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop // ensures full-screen coverage
|
||||
)
|
||||
|
||||
//------------------------------
|
||||
// Decorative Elements (scaled)
|
||||
//------------------------------
|
||||
|
||||
// 🐐 Goat
|
||||
ScaledEmoji(
|
||||
emoji = "🐐",
|
||||
baseFontSize = 48.sp,
|
||||
offsetX = 250f,
|
||||
offsetY = 160f,
|
||||
scale = scale,
|
||||
alpha = 0.10f
|
||||
)
|
||||
|
||||
// 🐄 Cow
|
||||
ScaledEmoji(
|
||||
emoji = "🐄",
|
||||
baseFontSize = 60.sp,
|
||||
offsetX = 64f,
|
||||
offsetY = 569f,
|
||||
scale = scale,
|
||||
alpha = 0.12f
|
||||
)
|
||||
|
||||
// 🌾 Wheat
|
||||
ScaledEmoji(
|
||||
emoji = "🌾🌾🌾",
|
||||
baseFontSize = 32.sp,
|
||||
offsetX = 48f,
|
||||
offsetY = 730f,
|
||||
scale = scale,
|
||||
alpha = 0.15f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScaledEmoji(
|
||||
emoji: String,
|
||||
baseFontSize: TextUnit,
|
||||
offsetX: Float,
|
||||
offsetY: Float,
|
||||
scale: Float,
|
||||
alpha: Float
|
||||
) {
|
||||
Text(
|
||||
text = emoji,
|
||||
fontSize = (baseFontSize.value * scale).sp,
|
||||
modifier = Modifier
|
||||
.offset(
|
||||
x = (offsetX * scale).dp,
|
||||
y = (offsetY * scale).dp
|
||||
)
|
||||
.alpha(alpha)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.example.livingai_lg.ui.components.backgrounds
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.min
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun StoreBackground() {
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
val screenW = maxWidth.value
|
||||
val screenH = maxHeight.value
|
||||
|
||||
// Original bg image design size (YOUR asset’s intended size)
|
||||
val designW = 393f
|
||||
val designH = 852f
|
||||
|
||||
// Scale factor that preserves aspect ratio
|
||||
val scale = min(screenW / designW, screenH / designH)
|
||||
|
||||
//------------------------------
|
||||
// Helper to scale dp offsets
|
||||
//------------------------------
|
||||
fun s(value: Float) = (value * scale).dp
|
||||
|
||||
//------------------------------
|
||||
// Background Image
|
||||
//------------------------------
|
||||
Image(
|
||||
painter = painterResource(R.drawable.bg_shop),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop // ensures full-screen coverage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.example.livingai_lg.ui.layout
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.example.livingai_lg.ui.components.BottomNavigationBar
|
||||
import com.example.livingai_lg.ui.models.BottomNavItemData
|
||||
|
||||
@Composable
|
||||
fun BottomNavScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
items: List<BottomNavItemData>,
|
||||
currentItem: String,
|
||||
onBottomNavItemClick: (route: String) -> Unit = {},
|
||||
content: @Composable (paddingValues: PaddingValues) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
bottomBar = {
|
||||
BottomNavigationBar(
|
||||
modifier = Modifier
|
||||
.zIndex(1f)
|
||||
.shadow(8.dp),
|
||||
items,
|
||||
currentItem,
|
||||
onItemClick = onBottomNavItemClick
|
||||
)
|
||||
},
|
||||
containerColor = Color.Transparent
|
||||
) { paddingValues ->
|
||||
content(paddingValues)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.example.livingai_lg.ui.login
|
||||
package com.example.livingai_lg.ui.login_legacy
|
||||
|
||||
import android.app.Application
|
||||
import android.widget.Toast
|
||||
import 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
# Signup API Endpoint
|
||||
|
||||
## POST /auth/signup
|
||||
|
||||
Creates a new user account with name, phone number, and optional location information.
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "John Doe", // Required: User's name (string, max 100 chars)
|
||||
"phone_number": "+919876543210", // Required: Phone number in E.164 format
|
||||
"state": "Maharashtra", // Optional: State name (string, max 100 chars)
|
||||
"district": "Mumbai", // Optional: District name (string, max 100 chars)
|
||||
"city_village": "Andheri", // Optional: City/Village name (string, max 150 chars)
|
||||
"device_id": "device-123", // Optional: Device identifier
|
||||
"device_info": { // Optional: Device information
|
||||
"platform": "android",
|
||||
"model": "Samsung Galaxy S21",
|
||||
"os_version": "Android 13",
|
||||
"app_version": "1.0.0",
|
||||
"language_code": "en",
|
||||
"timezone": "Asia/Kolkata"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Success Response (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"user": {
|
||||
"id": "uuid-here",
|
||||
"phone_number": "+919876543210",
|
||||
"name": "John Doe",
|
||||
"country_code": "+91",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
"access_token": "jwt-access-token",
|
||||
"refresh_token": "jwt-refresh-token",
|
||||
"needs_profile": true,
|
||||
"is_new_account": true,
|
||||
"is_new_device": true,
|
||||
"active_devices_count": 1,
|
||||
"location_id": "uuid-of-location" // null if no location provided
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
|
||||
#### 400 Bad Request - Validation Error
|
||||
```json
|
||||
{
|
||||
"error": "name is required"
|
||||
}
|
||||
```
|
||||
|
||||
#### 409 Conflict - User Already Exists
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "User with this phone number already exists. Please sign in instead.",
|
||||
"user_exists": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 403 Forbidden - IP Blocked
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Access denied from this location."
|
||||
}
|
||||
```
|
||||
|
||||
#### 500 Internal Server Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Internal server error"
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
1. **User Existence Check**: Automatically checks if a user with the phone number already exists
|
||||
2. **Phone Number Encryption**: Phone numbers are encrypted before storing in database
|
||||
3. **Location Creation**: If state/district/city_village provided, creates a location entry
|
||||
4. **Token Issuance**: Automatically issues access and refresh tokens
|
||||
5. **Device Tracking**: Records device information for security
|
||||
6. **Audit Logging**: Logs signup events for security monitoring
|
||||
|
||||
### Example Usage
|
||||
|
||||
#### cURL
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/signup \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "John Doe",
|
||||
"phone_number": "+919876543210",
|
||||
"state": "Maharashtra",
|
||||
"district": "Mumbai",
|
||||
"city_village": "Andheri",
|
||||
"device_id": "android-device-123",
|
||||
"device_info": {
|
||||
"platform": "android",
|
||||
"model": "Samsung Galaxy S21",
|
||||
"os_version": "Android 13"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
#### JavaScript/TypeScript
|
||||
```javascript
|
||||
const response = await fetch('/auth/signup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: 'John Doe',
|
||||
phone_number: '+919876543210',
|
||||
state: 'Maharashtra',
|
||||
district: 'Mumbai',
|
||||
city_village: 'Andheri',
|
||||
device_id: 'android-device-123',
|
||||
device_info: {
|
||||
platform: 'android',
|
||||
model: 'Samsung Galaxy S21',
|
||||
os_version: 'Android 13'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Store tokens
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
```
|
||||
|
||||
#### Kotlin/Android
|
||||
```kotlin
|
||||
data class SignupRequest(
|
||||
val name: String,
|
||||
val phone_number: String,
|
||||
val state: String? = null,
|
||||
val district: String? = null,
|
||||
val city_village: String? = null,
|
||||
val device_id: String? = null,
|
||||
val device_info: Map<String, String?>? = null
|
||||
)
|
||||
|
||||
data class SignupResponse(
|
||||
val success: Boolean,
|
||||
val user: User,
|
||||
val access_token: String,
|
||||
val refresh_token: String,
|
||||
val needs_profile: Boolean,
|
||||
val is_new_account: Boolean,
|
||||
val is_new_device: Boolean,
|
||||
val active_devices_count: Int,
|
||||
val location_id: String?
|
||||
)
|
||||
|
||||
// Usage
|
||||
val request = SignupRequest(
|
||||
name = "John Doe",
|
||||
phone_number = "+919876543210",
|
||||
state = "Maharashtra",
|
||||
district = "Mumbai",
|
||||
city_village = "Andheri",
|
||||
device_id = getDeviceId(),
|
||||
device_info = mapOf(
|
||||
"platform" to "android",
|
||||
"model" to Build.MODEL,
|
||||
"os_version" to Build.VERSION.RELEASE
|
||||
)
|
||||
)
|
||||
|
||||
val response = apiClient.post<SignupResponse>("/auth/signup", request)
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Phone number must be in E.164 format (e.g., `+919876543210`)
|
||||
- If phone number is 10 digits without `+`, it will be normalized to `+91` prefix
|
||||
- Location fields are optional - user can be created without location
|
||||
- If user already exists, returns 409 Conflict with `user_exists: true`
|
||||
- All phone numbers are encrypted in the database for security
|
||||
- Country code is automatically extracted from phone number
|
||||
|
||||
|
|
@ -0,0 +1,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
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
data class AnimalType(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val emoji: String
|
||||
)
|
||||
|
||||
val animalTypes = listOf(
|
||||
AnimalType("cows", "Cows", "🐄"),
|
||||
AnimalType("buffalo", "Buffalo", "🐃"),
|
||||
AnimalType("goat", "Goat", "🐐"),
|
||||
AnimalType("bull", "Bull", "🐂"),
|
||||
AnimalType("baby_cow", "Baby Cow", "🐮"),
|
||||
AnimalType("dog", "Dog", "🐕"),
|
||||
AnimalType("cat", "Cat", "🐱"),
|
||||
AnimalType("others", "Others", "🦜")
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Build
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
|
||||
data class BottomNavItemData(
|
||||
val label: String,
|
||||
val iconRes: Any,
|
||||
val route: String,
|
||||
)
|
||||
|
||||
val mainBottomNavItems = listOf(
|
||||
BottomNavItemData("Home", R.drawable.ic_home , AppScreen.BUY_ANIMALS),
|
||||
BottomNavItemData("Sell", R.drawable.ic_tag, AppScreen.CREATE_ANIMAL_LISTING),
|
||||
// TODO:
|
||||
BottomNavItemData("Chats", R.drawable.ic_chat, AppScreen.CHATS),
|
||||
BottomNavItemData("Services", R.drawable.ic_config, AppScreen.CREATE_PROFILE),
|
||||
BottomNavItemData("Mandi", R.drawable.ic_shop2, AppScreen.CREATE_PROFILE)
|
||||
)
|
||||
|
||||
val chatBottomNavItems = listOf(
|
||||
BottomNavItemData("Contacts", R.drawable.ic_home ,AppScreen.CONTACTS),
|
||||
BottomNavItemData("Calls", R.drawable.ic_tag, AppScreen.CALLS),
|
||||
BottomNavItemData("Chats", R.drawable.ic_chat, AppScreen.CHATS),
|
||||
)
|
||||
|
|
@ -0,0 +1,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()
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
class NewListingFormState {
|
||||
|
||||
val name = mutableStateOf("")
|
||||
val animal = mutableStateOf("")
|
||||
val breed = mutableStateOf("")
|
||||
val age = mutableStateOf("")
|
||||
val milkYield = mutableStateOf("")
|
||||
val calvingNumber = mutableStateOf("")
|
||||
val reproductiveStatus = mutableStateOf("")
|
||||
val description = mutableStateOf("")
|
||||
|
||||
val animalExpanded = mutableStateOf(false)
|
||||
val breedExpanded = mutableStateOf(false)
|
||||
|
||||
val mediaUploads = mutableStateListOf(
|
||||
MediaUpload("left-view", "Left View", MediaType.PHOTO),
|
||||
MediaUpload("right-view", "Right View", MediaType.PHOTO),
|
||||
MediaUpload("left-angle", "Left Angle View", MediaType.PHOTO),
|
||||
MediaUpload("right-angle", "Right Angle View", MediaType.PHOTO),
|
||||
MediaUpload("angle", "Angle View", MediaType.PHOTO),
|
||||
MediaUpload("video", "", MediaType.VIDEO)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
|
||||
data class AppNotification(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
val sampleNotifications = listOf(
|
||||
AppNotification(
|
||||
id = "1",
|
||||
title = "Animal saved successfully",
|
||||
icon = Icons.Default.Bookmark
|
||||
),
|
||||
AppNotification(
|
||||
id = "2",
|
||||
title = "New message from Seller",
|
||||
icon = Icons.Default.Chat
|
||||
),
|
||||
AppNotification(
|
||||
id = "3",
|
||||
title = "Price dropped on an animal you viewed",
|
||||
icon = Icons.Default.Notifications
|
||||
),
|
||||
AppNotification(
|
||||
id = "4",
|
||||
title = "You received a new rating",
|
||||
icon = Icons.Default.Star
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
data class ProfileType(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: Any,
|
||||
val iconTint: Color = Color.Unspecified,
|
||||
val backgroundColor: Color,
|
||||
)
|
||||
|
||||
val profileTypes = listOf(
|
||||
ProfileType(
|
||||
id = "buyer_seller",
|
||||
title = "I'm a Buyer/Seller",
|
||||
icon = R.drawable.ic_shop,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF9D4EDD)
|
||||
),
|
||||
ProfileType(
|
||||
id = "wholesale_trader",
|
||||
title = "I'm a Wholesale Trader",
|
||||
icon = R.drawable.ic_bag,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF3A86FF)
|
||||
),
|
||||
ProfileType(
|
||||
id = "service_provider",
|
||||
title = "I'm a Service Provider",
|
||||
icon = R.drawable.ic_spanner,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFFFF5722)
|
||||
),
|
||||
ProfileType(
|
||||
id = "mandi_host",
|
||||
title = "I'm a Mandi Host",
|
||||
icon = R.drawable.ic_shop2,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF4CAF50)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
class Seller {
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
enum class SortDirection {
|
||||
NONE,
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
|
||||
data class SortField(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val direction: SortDirection = SortDirection.NONE,
|
||||
val order: Int? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
data class UserAddress(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val isPrimary: Boolean = false
|
||||
)
|
||||
|
||||
data class UserProfile(
|
||||
val name: String,
|
||||
val profileImageUrl: String? = null,
|
||||
val addresses: List<UserAddress>
|
||||
)
|
||||
|
||||
val userProfile =
|
||||
UserProfile(
|
||||
name = "John Doe",
|
||||
profileImageUrl = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fstatic.vecteezy.com%2Fsystem%2Fresources%2Fpreviews%2F014%2F016%2F808%2Fnon_2x%2Findian-farmer-face-vector.jpg&f=1&nofb=1&ipt=c352fec591428aebefe6cd263d2958765e85d4da69cce3c46b725ba2ff7d3448",
|
||||
addresses = listOf(
|
||||
UserAddress(
|
||||
id = "1",
|
||||
name = "Home",
|
||||
address = "205 1st floor 7th cross 27th main, PW",
|
||||
isPrimary = true
|
||||
),
|
||||
UserAddress(
|
||||
id = "2",
|
||||
name = "Farm",
|
||||
address = "2nd block, MG Farms"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.livingai_lg.ui.navigation
|
||||
sealed class NavEvent {
|
||||
data class ToCreateProfile(val name: String) : NavEvent()
|
||||
data class ToChooseService(val profileId: String = "") : NavEvent()
|
||||
object ToLanding : NavEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.livingai_lg.ui.MainViewModel
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
// Note: CoroutineScope, Dispatchers, delay, launch still used in onCreateProfile callbacks
|
||||
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignUpScreen
|
||||
|
||||
fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: MainViewModel) {
|
||||
navigation(
|
||||
route = Graph.AUTH,
|
||||
startDestination = AppScreen.LANDING
|
||||
) {
|
||||
composable(AppScreen.LANDING) {
|
||||
LandingScreen(
|
||||
onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||
onSignInClick = { navController.navigate(AppScreen.SIGN_IN) },
|
||||
onGuestClick = { navController.navigate(Graph.main(AppScreen.createProfile("guest"))) }
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SIGN_IN) {
|
||||
SignInScreen(
|
||||
onSignUpClick = { navController.navigate(AppScreen.SIGN_UP){
|
||||
popUpTo(AppScreen.SIGN_IN) { inclusive = true }
|
||||
} },
|
||||
onSignInClick = { phone, name ->
|
||||
navController.navigate(AppScreen.otp(phone,name))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SIGN_UP) {
|
||||
SignUpScreen(
|
||||
onSignUpClick = { phone, name, state, district, village ->
|
||||
navController.navigate(AppScreen.otpWithSignup(phone, name, state, district, village))
|
||||
},
|
||||
onSignInClick = {
|
||||
navController.navigate(AppScreen.SIGN_IN) {
|
||||
popUpTo(AppScreen.SIGN_UP) { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.OTP}/{phoneNumber}/{name}",
|
||||
arguments = listOf(
|
||||
navArgument("phoneNumber") { type = NavType.StringType },
|
||||
navArgument("name") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
OtpScreen(
|
||||
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
mainViewModel = mainViewModel,
|
||||
onCreateProfile = {name ->
|
||||
Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
|
||||
// Navigate to main graph first without popping, then navigate to specific route
|
||||
try {
|
||||
// Navigate to main graph (this will use its start destination)
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Don't pop the AUTH graph yet - keep the graph structure
|
||||
launchSingleTop = true
|
||||
}
|
||||
// Then navigate to the specific route after a short delay
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(200)
|
||||
try {
|
||||
navController.navigate(AppScreen.createProfile(name)) {
|
||||
// Now pop the AUTH graph after we're in the MAIN graph
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onLanding = {
|
||||
Log.d("AuthNavGraph", "Navigating to landing page for new user")
|
||||
// Navigate to landing page within AUTH graph
|
||||
try {
|
||||
navController.navigate(AppScreen.LANDING) {
|
||||
// Pop all screens up to and including the current OTP screen
|
||||
popUpTo(Graph.AUTH) { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||
try {
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Clear back stack to prevent going back to auth screens
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.OTP}/{phoneNumber}/{name}/{state}/{district}/{village}",
|
||||
arguments = listOf(
|
||||
navArgument("phoneNumber") { type = NavType.StringType },
|
||||
navArgument("name") { type = NavType.StringType },
|
||||
navArgument("state") { type = NavType.StringType },
|
||||
navArgument("district") { type = NavType.StringType },
|
||||
navArgument("village") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
OtpScreen(
|
||||
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
signupState = backStackEntry.arguments?.getString("state"),
|
||||
signupDistrict = backStackEntry.arguments?.getString("district"),
|
||||
signupVillage = backStackEntry.arguments?.getString("village"),
|
||||
mainViewModel = mainViewModel,
|
||||
onCreateProfile = {name ->
|
||||
Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
|
||||
// Navigate to main graph first without popping, then navigate to specific route
|
||||
try {
|
||||
// Navigate to main graph (this will use its start destination)
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Don't pop the AUTH graph yet - keep the graph structure
|
||||
launchSingleTop = true
|
||||
}
|
||||
// Then navigate to the specific route after a short delay
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(200)
|
||||
try {
|
||||
navController.navigate(AppScreen.createProfile(name)) {
|
||||
// Now pop the AUTH graph after we're in the MAIN graph
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onLanding = {
|
||||
Log.d("AuthNavGraph", "Navigating to landing page for new user")
|
||||
// Navigate to landing page within AUTH graph
|
||||
try {
|
||||
navController.navigate(AppScreen.LANDING) {
|
||||
// Pop all screens up to and including the current OTP screen
|
||||
popUpTo(Graph.AUTH) { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||
try {
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Clear back stack to prevent going back to auth screens
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.farmmarketplace.ui.screens.CallsScreen
|
||||
import com.example.farmmarketplace.ui.screens.ContactsScreen
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import com.example.livingai_lg.ui.screens.AccountsScreen
|
||||
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.BuyScreen
|
||||
import com.example.livingai_lg.ui.screens.ChooseServiceScreen
|
||||
import com.example.livingai_lg.ui.screens.CreateProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
import com.example.livingai_lg.ui.screens.NewListingScreen
|
||||
import com.example.livingai_lg.ui.screens.PostSaleSurveyScreen
|
||||
import com.example.livingai_lg.ui.screens.SaleArchiveScreen
|
||||
import com.example.livingai_lg.ui.screens.SavedListingsScreen
|
||||
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.SortScreen
|
||||
import com.example.livingai_lg.ui.screens.chat.ChatScreen
|
||||
import com.example.livingai_lg.ui.screens.chat.ChatsScreen
|
||||
|
||||
fun NavGraphBuilder.mainNavGraph(navController: NavController) {
|
||||
val onNavClick: (String) -> Unit = { route ->
|
||||
val currentRoute =
|
||||
navController.currentBackStackEntry?.destination?.route
|
||||
Log.d("Current Route:"," $currentRoute $route")
|
||||
|
||||
if (currentRoute != route) {
|
||||
navController.navigate(route) {
|
||||
launchSingleTop = true
|
||||
//restoreState = true
|
||||
// popUpTo(navController.graph.startDestinationId) {
|
||||
// saveState = true
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigation(
|
||||
route = Graph.MAIN,
|
||||
startDestination = AppScreen.chooseService("1") // Default to ChooseServiceScreen for authenticated users
|
||||
){
|
||||
|
||||
|
||||
// NavHost(
|
||||
// navController = navController,
|
||||
// startDestination = AppScreen.createProfile("guest")
|
||||
// ) {
|
||||
composable(
|
||||
"${AppScreen.CREATE_PROFILE}/{name}",
|
||||
arguments = listOf(navArgument("name") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
CreateProfileScreen(
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
onProfileSelected = { profileId ->
|
||||
navController.navigate(AppScreen.chooseService(profileId))
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.CHOOSE_SERVICE}/{profileId}",
|
||||
arguments = listOf(navArgument("profileId") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
ChooseServiceScreen (
|
||||
profileId = backStackEntry.arguments?.getString("profileId") ?: "",
|
||||
onServiceSelected = { navController.navigate(AppScreen.BUY_ANIMALS) },
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.BUY_ANIMALS) {
|
||||
BuyScreen(
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onProductClick = { animalId ->
|
||||
navController.navigate(
|
||||
AppScreen.animalProfile(animalId)
|
||||
)
|
||||
},
|
||||
onNavClick = onNavClick,
|
||||
onSellerClick = { sellerId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||
// FilterScreen(
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
|
||||
composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||
SortScreen(
|
||||
onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onCancelClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SAVED_LISTINGS) {
|
||||
SavedListingsScreen(
|
||||
onNavClick = onNavClick,
|
||||
onBackClick = { navController.popBackStack() })
|
||||
}
|
||||
|
||||
composable(AppScreen.ACCOUNTS) {
|
||||
AccountsScreen(
|
||||
onBackClick = { navController.popBackStack() },
|
||||
onLogout = {
|
||||
// Navigate to auth graph after logout
|
||||
navController.navigate(Graph.AUTH) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onApiTest = {navController.navigate(AppScreen.API_TEST)}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||
NewListingScreen (
|
||||
onSaveClick = {navController.navigate(
|
||||
AppScreen.postSaleSurvey("2")
|
||||
)},
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.BUY_ANIMALS){
|
||||
popUpTo(AppScreen.BUY_ANIMALS){
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||
arguments = listOf(
|
||||
navArgument("saleId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val saleId = backStackEntry
|
||||
.arguments
|
||||
?.getString("saleId")
|
||||
?: return@composable
|
||||
|
||||
SaleArchiveScreen(
|
||||
saleId = saleId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onSaleSurveyClick = { saleId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(saleId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||
arguments = listOf(
|
||||
navArgument("animalId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val animalId = backStackEntry
|
||||
.arguments
|
||||
?.getString("animalId")
|
||||
?: return@composable
|
||||
|
||||
PostSaleSurveyScreen (
|
||||
animalId = animalId,
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.CREATE_ANIMAL_LISTING)
|
||||
},
|
||||
onSubmit = {navController.navigate(
|
||||
AppScreen.saleArchive("2")
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||
arguments = listOf(
|
||||
navArgument("animalId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val animalId = backStackEntry
|
||||
.arguments
|
||||
?.getString("animalId")
|
||||
?: return@composable
|
||||
|
||||
AnimalProfileScreen(
|
||||
animalId = animalId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavClick = onNavClick,
|
||||
onSellerClick = { sellerId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||
arguments = listOf(
|
||||
navArgument("sellerId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val sellerId = backStackEntry
|
||||
.arguments
|
||||
?.getString("sellerId")
|
||||
?: return@composable
|
||||
|
||||
SellerProfileScreen(
|
||||
sellerId = sellerId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CONTACTS) {
|
||||
ContactsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CALLS) {
|
||||
CallsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CHATS) {
|
||||
ChatsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
onChatItemClick = {navController.navigate(AppScreen.chats("2"))}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.CHAT}/{contact}",
|
||||
arguments = listOf(
|
||||
navArgument("contact") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val sellerId = backStackEntry
|
||||
.arguments
|
||||
?.getString("contact")
|
||||
?: return@composable
|
||||
|
||||
ChatScreen(
|
||||
sellerId,
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.CHATS)
|
||||
//navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.delay
|
||||
import com.example.livingai_lg.ui.AuthState
|
||||
import com.example.livingai_lg.ui.MainViewModel
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
|
||||
object OldAppScreen {
|
||||
const val LANDING = "landing"
|
||||
const val SIGN_IN = "sign_in"
|
||||
const val SIGN_UP = "sign_up"
|
||||
|
||||
const val OTP = "otp"
|
||||
|
||||
const val CHOOSE_SERVICE = "choose_service"
|
||||
|
||||
const val CREATE_PROFILE = "create_profile"
|
||||
|
||||
const val BUY_ANIMALS = "buy_animals"
|
||||
const val ANIMAL_PROFILE = "animal_profile"
|
||||
|
||||
const val CREATE_ANIMAL_LISTING = "create_animal_listing"
|
||||
|
||||
const val BUY_ANIMALS_FILTERS = "buy_animals_filters"
|
||||
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||
|
||||
const val SELLER_PROFILE = "seller_profile"
|
||||
|
||||
const val SALE_ARCHIVE = "sale_archive"
|
||||
|
||||
const val POST_SALE_SURVEY = "post_sale_survey"
|
||||
|
||||
const val SAVED_LISTINGS = "saved_listings"
|
||||
|
||||
const val CONTACTS = "contacts"
|
||||
|
||||
const val CALLS = "calls"
|
||||
|
||||
const val CHAT = "chat"
|
||||
|
||||
const val CHATS = "chats"
|
||||
|
||||
const val ACCOUNTS = "accounts"
|
||||
|
||||
fun chats(contact: String) =
|
||||
"$CHAT/$contact"
|
||||
|
||||
fun otp(phone: String, name: String) =
|
||||
"$OTP/$phone/$name"
|
||||
|
||||
fun otpWithSignup(phone: String, name: String, state: String, district: String, village: String) =
|
||||
"$OTP/$phone/$name/$state/$district/$village"
|
||||
|
||||
fun createProfile(name: String) =
|
||||
"$CREATE_PROFILE/$name"
|
||||
|
||||
fun chooseService(profileId: String) =
|
||||
"$CHOOSE_SERVICE/$profileId"
|
||||
fun postSaleSurvey(animalId: String) =
|
||||
"$POST_SALE_SURVEY/$animalId"
|
||||
|
||||
fun animalProfile(animalId: String) =
|
||||
"$ANIMAL_PROFILE/$animalId"
|
||||
|
||||
fun sellerProfile(sellerId: String) =
|
||||
"$SELLER_PROFILE/$sellerId"
|
||||
|
||||
fun saleArchive(saleId: String) =
|
||||
"$SALE_ARCHIVE/$saleId"
|
||||
|
||||
}
|
||||
|
||||
object Graph {
|
||||
const val AUTH = "auth"
|
||||
const val MAIN = "main"
|
||||
|
||||
fun auth(route: String)=
|
||||
"$AUTH/$route"
|
||||
|
||||
fun main(route: String)=
|
||||
"$MAIN/$route"
|
||||
}
|
||||
@Composable
|
||||
fun OldAppNavigation(
|
||||
authState: AuthState,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
// Determine start destination based on initial auth state
|
||||
// This prevents showing landing screen when user is already logged in
|
||||
val startDestination = remember(authState) {
|
||||
when (authState) {
|
||||
is AuthState.Authenticated -> Graph.MAIN
|
||||
is AuthState.Unauthenticated -> Graph.AUTH
|
||||
is AuthState.Unknown -> Graph.AUTH // Show landing while checking
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
authNavGraph(navController, mainViewModel)
|
||||
mainNavGraph(navController)
|
||||
}
|
||||
|
||||
// Handle navigation based on auth state changes
|
||||
LaunchedEffect(authState) {
|
||||
Log.d("AppNavigation", "LaunchedEffect triggered with authState: $authState")
|
||||
when (authState) {
|
||||
is AuthState.Authenticated -> {
|
||||
// User is authenticated, navigate to ChooseServiceScreen
|
||||
// Add a small delay to ensure NavHost graphs are fully built
|
||||
delay(100)
|
||||
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
Log.d("AppNavigation", "Authenticated - currentRoute: $currentRoute")
|
||||
// Only navigate if we're not already in the MAIN graph or ChooseServiceScreen
|
||||
if (currentRoute?.startsWith(Graph.MAIN) != true &&
|
||||
currentRoute?.startsWith(AppScreen.CHOOSE_SERVICE) != true) {
|
||||
Log.d("AppNavigation", "Navigating to ChooseServiceScreen (default profileId: 1)")
|
||||
try {
|
||||
// Navigate directly to the start destination route of MAIN graph
|
||||
// This avoids the "Sequence is empty" error when navigating to Graph.MAIN
|
||||
navController.navigate(AppScreen.chooseService("1")) {
|
||||
// Clear back stack to prevent going back to auth screens
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
// Prevent multiple navigations
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppNavigation", "Navigation error: ${e.message}", e)
|
||||
// Fallback: try navigating to Graph.MAIN if direct route fails
|
||||
try {
|
||||
navController.navigate(Graph.MAIN) {
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
Log.e("AppNavigation", "Fallback navigation also failed: ${e2.message}", e2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d("AppNavigation", "Already in MAIN graph or ChooseServiceScreen, skipping navigation")
|
||||
}
|
||||
}
|
||||
is AuthState.Unauthenticated -> {
|
||||
// User is not authenticated, ensure we're in auth graph (landing screen)
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
if (currentRoute?.startsWith(Graph.MAIN) == true ||
|
||||
currentRoute?.startsWith(Graph.AUTH) != true) {
|
||||
navController.navigate(Graph.AUTH) {
|
||||
// Clear back stack to prevent going back to main screens
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
is AuthState.Unknown -> {
|
||||
// Still checking auth status
|
||||
// If we're on landing screen, stay there
|
||||
// If we're on main screen and checking, don't navigate yet
|
||||
// This prevents flickering during token validation
|
||||
}
|
||||
}
|
||||
}
|
||||
// MainNavGraph(navController)
|
||||
// AuthNavGraph(navController)
|
||||
// when (authState) {
|
||||
// is AuthState.Unauthenticated -> {AuthNavGraph()}
|
||||
// is AuthState.Authenticated -> {MainNavGraph()}
|
||||
// is AuthState.Unknown -> {
|
||||
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
// CircularProgressIndicator()
|
||||
// }
|
||||
// }
|
||||
// AuthState.Loading -> SplashScreen()
|
||||
// }
|
||||
|
||||
// val onNavClick: (String) -> Unit = { route ->
|
||||
// val currentRoute =
|
||||
// navController.currentBackStackEntry?.destination?.route
|
||||
//
|
||||
// if (currentRoute != route) {
|
||||
// navController.navigate(route) {
|
||||
// launchSingleTop = true
|
||||
// restoreState = true
|
||||
// popUpTo(navController.graph.startDestinationId) {
|
||||
// saveState = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// NavHost(
|
||||
// navController = navController,
|
||||
// startDestination = AppScreen.LANDING,
|
||||
//
|
||||
// ) {
|
||||
// composable(AppScreen.LANDING) {
|
||||
// LandingScreen(
|
||||
// onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||
// onSignInClick = { navController.navigate(AppScreen.SIGN_IN) },
|
||||
// onGuestClick = { navController.navigate(AppScreen.CREATE_PROFILE) }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.SIGN_IN) {
|
||||
// SignInScreen(
|
||||
// onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||
// onSignInClick = {
|
||||
// navController.navigate(AppScreen.OTP)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.SIGN_UP) {
|
||||
// SignUpScreen(
|
||||
// onSignUpClick = {
|
||||
// navController.navigate(AppScreen.OTP)
|
||||
// },
|
||||
// onSignInClick = {
|
||||
// navController.navigate(AppScreen.SIGN_IN) {
|
||||
// popUpTo(AppScreen.SIGN_UP) { inclusive = true }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.OTP) {
|
||||
// OtpScreen(
|
||||
// onContinue = { navController.navigate(AppScreen.CREATE_PROFILE) }
|
||||
// )
|
||||
// }
|
||||
|
||||
// composable(AppScreen.CREATE_PROFILE) {
|
||||
// CreateProfileScreen (
|
||||
// onProfileSelected = { profileId ->
|
||||
// if (profileId == "buyer_seller") {
|
||||
// navController.navigate(AppScreen.BUY_ANIMALS)
|
||||
// } else {
|
||||
// navController.navigate(AppScreen.CHOOSE_SERVICE)
|
||||
// } },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.CHOOSE_SERVICE) {
|
||||
// ChooseServiceScreen (
|
||||
// onServiceSelected = { navController.navigate(AppScreen.LANDING) },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.BUY_ANIMALS) {
|
||||
// BuyScreen(
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onProductClick = { animalId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.animalProfile(animalId)
|
||||
// )
|
||||
// },
|
||||
// onNavClick = onNavClick,
|
||||
// onFilterClick = {navController.navigate(AppScreen.BUY_ANIMALS_FILTERS)},
|
||||
// onSortClick = {navController.navigate(AppScreen.BUY_ANIMALS_SORT)},
|
||||
// onSellerClick = { sellerId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.sellerProfile(sellerId)
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||
// FilterScreen(
|
||||
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSkipClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
//
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||
// SortScreen(
|
||||
// onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSkipClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
//
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||
// NewListingScreen (
|
||||
// onSaveClick = {navController.navigate(
|
||||
// AppScreen.postSaleSurvey("2")
|
||||
// )},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("saleId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val saleId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("saleId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// SaleArchiveScreen(
|
||||
// saleId = saleId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSaleSurveyClick = { saleId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.sellerProfile(saleId)
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("animalId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val animalId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("animalId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// PostSaleSurveyScreen (
|
||||
// animalId = animalId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSubmit = {navController.navigate(
|
||||
// AppScreen.saleArchive("2")
|
||||
// )}
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("animalId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val animalId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("animalId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// AnimalProfileScreen(
|
||||
// animalId = animalId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSellerClick = { sellerId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.sellerProfile(sellerId)
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("sellerId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val sellerId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("sellerId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// SellerProfileScreen(
|
||||
// sellerId = sellerId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// composable(AppScreen.SELLER_PROFILE) {
|
||||
// SellerProfileScreen (
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
import androidx.compose.material.icons.filled.Construction
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onApiTest: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
val apiClient = remember { AuthApiClient(context) }
|
||||
|
||||
// State for API test dialog
|
||||
var showApiResultDialog by remember { mutableStateOf(false) }
|
||||
var apiResultJson by remember { mutableStateOf<String?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Top App Bar
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Accounts",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Black
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.Black
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
// Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp)
|
||||
) {
|
||||
// Logout option
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
authManager.logout()
|
||||
.onSuccess { _: com.example.livingai_lg.api.LogoutResponse ->
|
||||
Toast.makeText(context, "Logged out successfully", Toast.LENGTH_SHORT).show()
|
||||
onLogout()
|
||||
}
|
||||
.onFailure { error: Throwable ->
|
||||
Toast.makeText(context, "Logout failed: ${error.message}", Toast.LENGTH_SHORT).show()
|
||||
// Still call onLogout to navigate away even if API call fails
|
||||
onLogout()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||
contentDescription = "Logout",
|
||||
tint = Color(0xFFE53935),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Log Out",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFE53935)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(vertical = 12.dp)
|
||||
.clickable {
|
||||
// Call API test directly instead of navigating
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
try {
|
||||
// First get current user's ID
|
||||
val userDetails = apiClient.getUserDetails().getOrNull()
|
||||
if (userDetails != null) {
|
||||
// Call GET /users/:userId on port 3200
|
||||
val result = apiClient.getUserById(userDetails.id, "http://10.0.2.2:3200")
|
||||
result.onSuccess { jsonString ->
|
||||
apiResultJson = jsonString
|
||||
showApiResultDialog = true
|
||||
isLoading = false
|
||||
}.onFailure { error ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
"API Test Failed: ${error.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Failed to get user details",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
isLoading = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Error: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Construction,
|
||||
contentDescription = "Api test",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = if (isLoading) "Testing API..." else "Test API",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API Result Dialog
|
||||
if (showApiResultDialog) {
|
||||
Dialog(onDismissRequest = { showApiResultDialog = false }) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.8f)
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Dialog Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "API Test Result",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
IconButton(onClick = { showApiResultDialog = false }) {
|
||||
Text(
|
||||
text = "✕",
|
||||
fontSize = 20.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// JSON Content
|
||||
apiResultJson?.let { json ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = json,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No result",
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Close Button
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Button(
|
||||
onClick = { showApiResultDialog = false },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.OptionCard
|
||||
import com.example.livingai_lg.ui.components.backgrounds.StoreBackground
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
data class ServiceType(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: Any,
|
||||
val iconTint: Color,
|
||||
val backgroundColor: Color
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChooseServiceScreen(
|
||||
profileId: String? = null,
|
||||
onServiceSelected: (serviceType: String) -> Unit = {}
|
||||
) {
|
||||
val selectedService = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val serviceTypes = listOf(
|
||||
ServiceType(
|
||||
id = "transport",
|
||||
title = "Transport",
|
||||
icon = R.drawable.ic_shop,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF9D4EDD)
|
||||
),
|
||||
ServiceType(
|
||||
id = "vet",
|
||||
title = "Vet",
|
||||
icon = R.drawable.ic_bag,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF3A86FF)
|
||||
),
|
||||
ServiceType(
|
||||
id = "feed_supplier",
|
||||
title = "Feed Supplier",
|
||||
icon = R.drawable.ic_spanner,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFFFF5722)
|
||||
),
|
||||
ServiceType(
|
||||
id = "medicine_supplier",
|
||||
title = "Medicine Supplier",
|
||||
icon = R.drawable.ic_shop2,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF4CAF50)
|
||||
),
|
||||
ServiceType(
|
||||
id = "other",
|
||||
title = "Other",
|
||||
icon = R.drawable.ic_other,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFFD4A942)
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F1E8))
|
||||
) {
|
||||
// Decorative background
|
||||
StoreBackground()
|
||||
|
||||
// Main content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Choose Service",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Choose a Service",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A).copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Service selection cards
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
serviceTypes.forEach { service ->
|
||||
OptionCard(
|
||||
label = service.title,
|
||||
icon = service.icon,
|
||||
iconTint = service.iconTint,
|
||||
iconBackgroundColor = service.backgroundColor,
|
||||
onClick = {
|
||||
selectedService.value = service.id
|
||||
onServiceSelected(service.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.ui.components.OptionCard
|
||||
|
||||
import com.example.livingai_lg.ui.components.backgrounds.StoreBackground
|
||||
import com.example.livingai_lg.ui.models.profileTypes
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun CreateProfileScreen(
|
||||
name: String,
|
||||
onProfileSelected: (profileId: String) -> Unit = {}
|
||||
) {
|
||||
val selectedProfile = remember { mutableStateOf<String>(profileTypes[0].id) }
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
fun updateProfile(profileId: String) {
|
||||
scope.launch {
|
||||
authManager.updateProfile(name, profileId)
|
||||
.onSuccess {
|
||||
onProfileSelected(profileId)
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Failed to update profile: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F1E8))
|
||||
) {
|
||||
// Decorative background
|
||||
StoreBackground()
|
||||
|
||||
// Main content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Create Profile",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Choose Profile Type",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A).copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Profile selection cards
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
profileTypes.forEach { profile ->
|
||||
OptionCard(
|
||||
label = profile.title,
|
||||
icon = profile.icon,
|
||||
iconBackgroundColor = profile.backgroundColor,
|
||||
onClick = {
|
||||
onProfileSelected(profile.id)
|
||||
//updateProfile(profile.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.MediaPickerCard
|
||||
import com.example.livingai_lg.ui.models.NewListingFormState
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewListingScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onSaveClick: () -> Unit = {},
|
||||
formState: NewListingFormState = remember { NewListingFormState() }
|
||||
) {
|
||||
var name by formState.name
|
||||
var animal by formState.animal
|
||||
var breed by formState.breed
|
||||
var age by formState.age
|
||||
var milkYield by formState.milkYield
|
||||
var calvingNumber by formState.calvingNumber
|
||||
var reproductiveStatus by formState.reproductiveStatus
|
||||
var description by formState.description
|
||||
|
||||
var animalExpanded by formState.animalExpanded
|
||||
var breedExpanded by formState.breedExpanded
|
||||
|
||||
val mediaUploads = formState.mediaUploads
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Header
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "New Listing",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
// Form Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Name Input
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Name",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Animal and Breed Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Animal Dropdown
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = animalExpanded,
|
||||
onExpandedChange = { animalExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = animal,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
placeholder = {
|
||||
Text(
|
||||
"Animal",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = Color(0xFFA5A5A5)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = animalExpanded,
|
||||
onDismissRequest = { animalExpanded = false }
|
||||
) {
|
||||
listOf("Cow", "Buffalo", "Goat", "Sheep").forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option) },
|
||||
onClick = {
|
||||
animal = option
|
||||
animalExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Breed Dropdown
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = breedExpanded,
|
||||
onExpandedChange = { breedExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = breed,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
placeholder = {
|
||||
Text(
|
||||
"Breed",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = Color(0xFFA5A5A5)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = breedExpanded,
|
||||
onDismissRequest = { breedExpanded = false }
|
||||
) {
|
||||
listOf("Holstein", "Jersey", "Gir", "Sahiwal").forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option) },
|
||||
onClick = {
|
||||
breed = option
|
||||
breedExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Age and Milk Yield Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = age,
|
||||
onValueChange = { age = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Age (Years)",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowDropDown,
|
||||
contentDescription = "Sort",
|
||||
tint = Color(0xFF353535)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = milkYield,
|
||||
onValueChange = { milkYield = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Average Milk Yield (L)",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
// Calving Number
|
||||
OutlinedTextField(
|
||||
value = calvingNumber,
|
||||
onValueChange = { calvingNumber = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Calving Number",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Reproductive Status
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Reproductive Status",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
listOf("Pregnant", "Calved", "None").forEach { status ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){
|
||||
reproductiveStatus = status
|
||||
}
|
||||
) {
|
||||
RadioButton(
|
||||
selected = reproductiveStatus == status,
|
||||
onClick = { reproductiveStatus = status },
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = Color(0xFF717182),
|
||||
unselectedColor = Color(0xFF717182)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = status,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Description",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(74.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
maxLines = 3
|
||||
)
|
||||
|
||||
// Upload Media Section
|
||||
Text(
|
||||
text = "Upload Media",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF000000),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
// Photo Uploads Grid
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
mediaUploads.chunked(2).forEach { rowItems ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
rowItems.forEach { upload ->
|
||||
MediaPickerCard(
|
||||
upload = upload,
|
||||
modifier = Modifier.weight(1f),
|
||||
onUriSelected = { uri ->
|
||||
upload.uri = uri
|
||||
}
|
||||
)
|
||||
}
|
||||
// Fill empty space if odd number of items
|
||||
if (rowItems.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Cancel Button
|
||||
OutlinedButton(
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Color(0x1A000000)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color(0xFF0A0A0A)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
// Save Profile Button
|
||||
Button(
|
||||
onClick = onSaveClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Black,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Save Profile",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.AppNotification
|
||||
|
||||
@Composable
|
||||
fun NotificationsScreen(
|
||||
notifications: List<AppNotification>,
|
||||
onBackClick: () -> Unit,
|
||||
onDismiss: (String) -> Unit,
|
||||
onNotificationClick: (String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Notifications",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
Divider(color = Color(0xFFE5E7EB))
|
||||
|
||||
if (notifications.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No notifications",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF6B7280)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(
|
||||
items = notifications,
|
||||
key = { it.id }
|
||||
) { notification ->
|
||||
NotificationItem(
|
||||
notification = notification,
|
||||
onDismiss = onDismiss,
|
||||
onClick = onNotificationClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationItem(
|
||||
notification: AppNotification,
|
||||
onDismiss: (String) -> Unit,
|
||||
onClick: (String) -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White, RoundedCornerShape(14.dp))
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(14.dp))
|
||||
.clickable { onClick(notification.id) }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = notification.icon,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = notification.title,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF1F2937),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { onDismiss(notification.id) },
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Dismiss",
|
||||
tint = Color(0xFF6B7280),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,511 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
data class SaleSurveyData(
|
||||
val buyerName: String = "",
|
||||
val salePrice: String = "",
|
||||
val saleDate: String = "",
|
||||
val saleLocation: String = "",
|
||||
val notes: String = "",
|
||||
val attachments: List<SurveyAttachment> = emptyList()
|
||||
)
|
||||
|
||||
data class SurveyAttachment(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val type: SurveyAttachmentType
|
||||
)
|
||||
|
||||
enum class SurveyAttachmentType {
|
||||
DOCUMENT,
|
||||
PHOTO,
|
||||
ADD_NEW
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostSaleSurveyScreen(
|
||||
animalId: String? = null,
|
||||
onBackClick: () -> Unit = {},
|
||||
onSkipClick: () -> Unit = {},
|
||||
onSubmit: (SaleSurveyData) -> Unit = {},
|
||||
onCancel: () -> Unit = {}
|
||||
) {
|
||||
var surveyData by remember { mutableStateOf(SaleSurveyData()) }
|
||||
var buyerNameExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val defaultAttachments = listOf(
|
||||
SurveyAttachment("1", "Sale Papers 1", SurveyAttachmentType.DOCUMENT),
|
||||
SurveyAttachment("2", "Photo 2", SurveyAttachmentType.PHOTO),
|
||||
SurveyAttachment("3", "Photo 1", SurveyAttachmentType.PHOTO),
|
||||
SurveyAttachment("4", "Add new", SurveyAttachmentType.ADD_NEW)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(26.dp))
|
||||
|
||||
FormField(
|
||||
label = "Sold to*",
|
||||
isRequired = true
|
||||
) {
|
||||
DropdownField(
|
||||
placeholder = "Buyer Name",
|
||||
value = surveyData.buyerName,
|
||||
expanded = buyerNameExpanded,
|
||||
onExpandedChange = { buyerNameExpanded = it },
|
||||
onValueChange = { surveyData = surveyData.copy(buyerName = it) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(29.dp))
|
||||
|
||||
FormField(
|
||||
label = "Sale Price*",
|
||||
isRequired = true
|
||||
) {
|
||||
TextInputField(
|
||||
placeholder = "Price",
|
||||
value = surveyData.salePrice,
|
||||
onValueChange = { surveyData = surveyData.copy(salePrice = it) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(21.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
FormField(
|
||||
label = "Sale Date*",
|
||||
isRequired = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
DatePickerField(
|
||||
placeholder = "Date",
|
||||
value = surveyData.saleDate,
|
||||
onValueChange = { surveyData = surveyData.copy(saleDate = it) }
|
||||
)
|
||||
}
|
||||
|
||||
FormField(
|
||||
label = "Sale Location",
|
||||
isRequired = false,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
TextInputField(
|
||||
placeholder = "Location",
|
||||
value = surveyData.saleLocation,
|
||||
onValueChange = { surveyData = surveyData.copy(saleLocation = it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(34.dp))
|
||||
|
||||
MultilineTextInputField(
|
||||
placeholder = "Notes",
|
||||
value = surveyData.notes,
|
||||
onValueChange = { surveyData = surveyData.copy(notes = it) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(104.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(31.dp))
|
||||
|
||||
Text(
|
||||
text = "Attachments",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 29.sp,
|
||||
modifier = Modifier.padding(start = 11.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(31.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(19.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[0].name,
|
||||
type = defaultAttachments[0].type,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[1].name,
|
||||
type = defaultAttachments[1].type,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[2].name,
|
||||
type = defaultAttachments[2].type,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[3].name,
|
||||
type = defaultAttachments[3].type,
|
||||
isAddNew = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(53.dp))
|
||||
|
||||
PrimaryButton(
|
||||
text = "Submit",
|
||||
onClick = { onSubmit(surveyData) },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
SecondaryButton(
|
||||
text = "Cancel",
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FormField(
|
||||
label: String,
|
||||
isRequired: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 29.sp
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextInputField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MultilineTextInputField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onExpandedChange(!expanded) }
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = value.ifEmpty { placeholder },
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
letterSpacing = (-0.312).sp,
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = Color(0xFFA5A5A5),
|
||||
modifier = Modifier
|
||||
.size(15.dp, 10.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DatePickerField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (value.isEmpty()) placeholder else value,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
letterSpacing = (-0.312).sp,
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarToday,
|
||||
contentDescription = "Calendar",
|
||||
tint = Color(0xFF000000),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AttachmentChip(
|
||||
name: String,
|
||||
type: SurveyAttachmentType,
|
||||
modifier: Modifier = Modifier,
|
||||
isAddNew: Boolean = false,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(43.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFE5E7EB),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 24.sp,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = if (isAddNew) Icons.Default.Add else Icons.Default.ArrowDropDown,
|
||||
contentDescription = if (isAddNew) "Add" else "Attachment",
|
||||
tint = if (isAddNew) Color(0xFF777777) else Color(0xFFADADAD),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrimaryButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(173.dp)
|
||||
.height(50.dp)
|
||||
.background(Color(0xFF0A0A0A), RoundedCornerShape(50.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFFFFFFFF),
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.312).sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SecondaryButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(173.dp)
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(50.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(50.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.312).sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
data class SaleArchive(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val breed: String,
|
||||
val soldTo: String,
|
||||
val saleDate: String,
|
||||
val salePrice: String,
|
||||
val location: String,
|
||||
val notes: String,
|
||||
val imageUrl: String,
|
||||
val attachments: List<Attachment>
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val type: AttachmentType
|
||||
)
|
||||
|
||||
enum class AttachmentType {
|
||||
SALE_PAPERS,
|
||||
PHOTO
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaleArchiveScreen(
|
||||
saleId: String,
|
||||
onBackClick: () -> Unit = {},
|
||||
onSaleSurveyClick:(saleId: String) -> Unit = {},
|
||||
) {
|
||||
val sale = SaleArchive(
|
||||
id = "1",
|
||||
name = "Gauri",
|
||||
breed = "Gir",
|
||||
soldTo = "Buyer 1",
|
||||
saleDate = "07.12.2025",
|
||||
salePrice = "₹30,000",
|
||||
location = "Ravi Nagar, Nagpur",
|
||||
notes = "Buyer requested a check up after 15 days of sale. Additional support for 20 days.",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/9389ccf57f6d262b500be45f6d9cbfe929302413?width=784",
|
||||
attachments = listOf(
|
||||
Attachment("1", "Sale Papers 1", AttachmentType.SALE_PAPERS),
|
||||
Attachment("2", "Photo 1", AttachmentType.PHOTO),
|
||||
Attachment("3", "Sale Papers 1", AttachmentType.SALE_PAPERS),
|
||||
Attachment("4", "Photo 2", AttachmentType.PHOTO)
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 11.dp, vertical = 15.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onBackClick
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = sale.imageUrl,
|
||||
contentDescription = "${sale.name} image",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(273.dp),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(
|
||||
elevation = 25.dp,
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||
spotColor = Color.Black.copy(alpha = 0.25f)
|
||||
)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFE5E7EB).copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||
)
|
||||
.background(
|
||||
color = Color(0xFFFCFBF8),
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||
)
|
||||
.padding(horizontal = 21.dp, vertical = 17.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(1.987.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Name, Breed",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = "${sale.name}, (${sale.breed})",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF101828),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.99.dp)
|
||||
) {
|
||||
InfoField(
|
||||
label = "Sold To",
|
||||
value = sale.soldTo,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
InfoField(
|
||||
label = "Sale Date",
|
||||
value = sale.saleDate,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 17.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.99.dp)
|
||||
) {
|
||||
InfoField(
|
||||
label = "Sale Price",
|
||||
value = sale.salePrice,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
InfoField(
|
||||
label = "Location",
|
||||
value = sale.location,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.078.dp)
|
||||
.background(Color(0xFFD1D5DC))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(5.995.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Notes",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color.Transparent,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = sale.notes,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF364153),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Attachments",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.dp)
|
||||
) {
|
||||
AttachmentButton(
|
||||
name = sale.attachments[0].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentButton(
|
||||
name = sale.attachments[1].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.dp)
|
||||
) {
|
||||
AttachmentButton(
|
||||
name = sale.attachments[2].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentButton(
|
||||
name = sale.attachments[3].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoField(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(1.987.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF101828),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AttachmentButton(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(38.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFE5E7EB),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.background(
|
||||
color = Color.Transparent,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
Spacer(modifier = Modifier.width(5.995.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.AttachFile,
|
||||
contentDescription = "Attachment",
|
||||
tint = Color(0xFFADADAD),
|
||||
modifier = Modifier.size(19.99.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
data class SavedListing(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val age: String,
|
||||
val breed: String,
|
||||
val price: String,
|
||||
val views: String,
|
||||
val distance: String,
|
||||
val imageUrl: String
|
||||
)
|
||||
|
||||
enum class BottomNavTab {
|
||||
HOME,
|
||||
SELL,
|
||||
CHATS,
|
||||
SERVICES,
|
||||
MANDI
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SavedListingsScreen(
|
||||
onListingClick: (String) -> Unit = {},
|
||||
onNavClick: (route: String) -> Unit = {},
|
||||
onBackClick: () -> Unit = {}
|
||||
) {
|
||||
val savedListings = listOf(
|
||||
SavedListing(
|
||||
id = "1",
|
||||
name = "Eauri",
|
||||
age = "Age: 3 yrs",
|
||||
breed = "Holstein",
|
||||
price = "₹45,000",
|
||||
views = "124 views",
|
||||
distance = "Distance: 12 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/db216f496f6d5fd681db9afc8006eb8da3164c17?width=192"
|
||||
),
|
||||
SavedListing(
|
||||
id = "2",
|
||||
name = "Fauri",
|
||||
age = "Age: 2 yrs",
|
||||
breed = "Jersey",
|
||||
price = "₹38,000",
|
||||
views = "89 views",
|
||||
distance = "Distance: 17 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/a49aa5540b0cb6bf76ba3aa99cb8d4f94835e8ee?width=192"
|
||||
),
|
||||
SavedListing(
|
||||
id = "3",
|
||||
name = "Gauri",
|
||||
age = "Age: 4 yrs",
|
||||
breed = "Gir",
|
||||
price = "₹52,000",
|
||||
views = "156 views",
|
||||
distance = "Distance: 20 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/26fd38c965180ef066862fb1d0aa6df040a0d389?width=192"
|
||||
),
|
||||
SavedListing(
|
||||
id = "4",
|
||||
name = "Hauri",
|
||||
age = "Age: 1.5 yrs",
|
||||
breed = "Sahiwal",
|
||||
price = "₹28,000",
|
||||
views = "67 views",
|
||||
distance = "Distance: 6 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/b8edfd48d908a8e0eeeee55745ab409e454be25f?width=192"
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = mainBottomNavItems,
|
||||
currentItem = "Home",
|
||||
onBottomNavItemClick = onNavClick,
|
||||
) { paddingValues ->
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 72.dp)
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Saved Listings",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 15.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
item {
|
||||
AdSpaceBanner()
|
||||
}
|
||||
|
||||
items(savedListings) { listing ->
|
||||
ListingCard(
|
||||
listing = listing,
|
||||
onClick = { onListingClick(listing.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListingCard(
|
||||
listing: SavedListing,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(138.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFF3F4F6).copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8), RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 21.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = listing.imageUrl,
|
||||
contentDescription = listing.name,
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = listing.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = listing.age,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = listing.breed,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = listing.price,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 24.sp,
|
||||
textAlign = TextAlign.End
|
||||
)
|
||||
|
||||
Text(
|
||||
text = listing.views,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.End
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = listing.distance,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Call
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Message
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
data class SellerProfile(
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val location: String,
|
||||
val rating: Double,
|
||||
val reviewCount: Int,
|
||||
val trustScore: Int,
|
||||
val totalSales: Int,
|
||||
val yearsActive: String,
|
||||
val responseRate: String,
|
||||
val about: String
|
||||
)
|
||||
|
||||
data class PastSale(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val emoji: String,
|
||||
val age: String,
|
||||
val breed: String,
|
||||
val price: String,
|
||||
val rating: Double
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SellerProfileScreen(
|
||||
sellerId: String = "1",
|
||||
onBackClick: () -> Unit = {},
|
||||
onCallClick: () -> Unit = {},
|
||||
onMessageClick: () -> Unit = {},
|
||||
onViewAllSalesClick: () -> Unit = {}
|
||||
) {
|
||||
// Sample data - in real app, fetch based on sellerId
|
||||
val seller = SellerProfile(
|
||||
name = "Ramesh Singh",
|
||||
initials = "RS",
|
||||
location = "Nagpur, Maharashtra",
|
||||
rating = 4.8,
|
||||
reviewCount = 156,
|
||||
trustScore = 95,
|
||||
totalSales = 47,
|
||||
yearsActive = "5+",
|
||||
responseRate = "98%",
|
||||
about = "Experienced cattle farmer and trader with over 5 years in the business. Specializing in dairy cattle breeds including Gir, Holstein, and Jersey. All animals are well-maintained with complete vaccination records and health certificates."
|
||||
)
|
||||
|
||||
val pastSales = listOf(
|
||||
PastSale("1", "Gir Cow", "🐄", "Age: 3 yrs", "Holstein", "₹45,000", 5.0),
|
||||
PastSale("2", "Buffalo", "🐃", "Age: 4 yrs", "Murrah", "₹55,000", 4.9),
|
||||
PastSale("3", "Goat", "🐐", "Age: 2 yrs", "Sirohi", "₹12,000", 4.7)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Header
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Seller Profile",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
),
|
||||
modifier = Modifier.border(
|
||||
width = 1.dp,
|
||||
color = Color(0x1A000000),
|
||||
shape = RectangleShape
|
||||
)
|
||||
)
|
||||
|
||||
// Scrollable Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Profile Card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Profile Info
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Avatar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = seller.initials,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
// Info
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = seller.name,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
contentDescription = "Location",
|
||||
tint = Color(0xFF717182),
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
text = seller.location,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Rating",
|
||||
tint = Color(0xFFFDC700),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = seller.rating.toString(),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "•",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${seller.reviewCount} reviews",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI Trust Score
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Color(0x4D60A5FA),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.border(
|
||||
1.dp,
|
||||
Color(0x4D60A5FA),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Shield,
|
||||
contentDescription = "Trust",
|
||||
tint = Color(0xFF155DFC),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "AI Trust Score",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Score",
|
||||
tint = Color(0xFF155DFC),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${seller.trustScore}/100",
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFF155DFC)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Verified seller with excellent transaction history",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onCallClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(46.dp),
|
||||
shape = RoundedCornerShape(23.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Black,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Call,
|
||||
contentDescription = "Call",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Call",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onMessageClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(46.dp),
|
||||
shape = RoundedCornerShape(23.dp),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Color(0x1A000000)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color(0xFF0A0A0A)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Message,
|
||||
contentDescription = "Message",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Message",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stats Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
StatCard(
|
||||
value = seller.totalSales.toString(),
|
||||
label = "Total Sales",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
StatCard(
|
||||
value = seller.yearsActive,
|
||||
label = "Years Active",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
StatCard(
|
||||
value = seller.responseRate,
|
||||
label = "Response\nRate",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// Past Sales Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Past Sales",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
TextButton(onClick = onViewAllSalesClick) {
|
||||
Text(
|
||||
text = "View All",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182),
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pastSales.forEach { sale ->
|
||||
PastSaleCard(sale)
|
||||
}
|
||||
}
|
||||
|
||||
// About Seller Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "About Seller",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = seller.about,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 22.75.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatCard(
|
||||
value: String,
|
||||
label: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.height(94.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PastSaleCard(sale: PastSale) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Animal Icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(Color(0xFFF7F4EE), RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = sale.emoji,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
|
||||
// Details
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = sale.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = sale.age,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
Text(
|
||||
text = sale.breed,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = sale.price,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Rating",
|
||||
tint = Color(0xFFFDC700),
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
text = sale.rating.toString(),
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import com.example.livingai_lg.ui.components.IconCircle
|
||||
import com.example.livingai_lg.ui.theme.FarmButtonPrimary
|
||||
import com.example.livingai_lg.ui.theme.FarmButtonSecondary
|
||||
import com.example.livingai_lg.ui.theme.FarmButtonText
|
||||
|
||||
import com.example.livingai_lg.ui.theme.FarmSproutBg
|
||||
import com.example.livingai_lg.ui.theme.FarmSproutIcon
|
||||
import com.example.livingai_lg.ui.theme.FarmSunBg
|
||||
import com.example.livingai_lg.ui.theme.FarmSunIcon
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
import com.example.livingai_lg.ui.theme.FarmWheatBg
|
||||
import com.example.livingai_lg.ui.theme.FarmWheatIcon
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun LandingScreen(
|
||||
onSignUpClick: () -> Unit = {},
|
||||
onSignInClick: () -> Unit = {},
|
||||
onGuestClick: () -> Unit = {}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FarmTextLight)
|
||||
) {
|
||||
// Decorative background elements
|
||||
DecorativeBackground()
|
||||
|
||||
// Main content
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding( top = 56.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
//Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Icon row
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
IconCircle(
|
||||
backgroundColor = FarmWheatBg,
|
||||
size = 56.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_wheat),
|
||||
contentDescription = "Wheat Icon",
|
||||
tint = FarmWheatIcon // keeps original vector colors
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
IconCircle(
|
||||
backgroundColor = FarmSproutBg,
|
||||
size = 64.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_sprout),
|
||||
contentDescription = "Wheat Icon",
|
||||
tint = FarmSproutIcon // keeps original vector colors
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
IconCircle(
|
||||
backgroundColor = FarmSunBg,
|
||||
size = 56.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_sun),
|
||||
contentDescription = "Wheat Icon",
|
||||
tint = FarmSunIcon // keeps original vector colors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Welcome text
|
||||
Text(
|
||||
text = "Welcome!",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Join the farm marketplace community",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = FarmTextNormal
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Sign up button
|
||||
Button(
|
||||
onClick = onSignUpClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = FarmButtonPrimary,
|
||||
contentColor = FarmButtonText
|
||||
),
|
||||
elevation = ButtonDefaults.buttonElevation(
|
||||
defaultElevation = 8.dp
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "New user? Sign up",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Sign in button
|
||||
Button(
|
||||
onClick = onSignInClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = FarmButtonSecondary,
|
||||
contentColor = FarmButtonText
|
||||
),
|
||||
elevation = ButtonDefaults.buttonElevation(
|
||||
defaultElevation = 8.dp
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Already a user? Sign in",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Continue as guest
|
||||
TextButton(
|
||||
onClick = onGuestClick
|
||||
) {
|
||||
Text(
|
||||
text = "Continue as Guest",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = FarmTextNormal,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,544 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.SignupRequest
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.api.UserNotFoundException
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import kotlin.math.min
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
@Composable
|
||||
fun OtpScreen(
|
||||
phoneNumber: String,
|
||||
name: String,
|
||||
mainViewModel: com.example.livingai_lg.ui.MainViewModel,
|
||||
onSuccess: () -> Unit = {},
|
||||
onCreateProfile: (name: String) -> Unit = {},
|
||||
onLanding: () -> Unit = {}, // New callback for navigating to landing page
|
||||
// Optional signup data for signup flow
|
||||
signupState: String? = null,
|
||||
signupDistrict: String? = null,
|
||||
signupVillage: String? = null,
|
||||
) {
|
||||
val otpLength = 6
|
||||
val otp = remember { mutableStateOf(List<String>(otpLength) { "" }) }
|
||||
var focusedIndex by remember { mutableStateOf(0) }
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
fun updateOtpAt(index: Int, value: String) {
|
||||
otp.value = otp.value.mapIndexed { i, old ->
|
||||
if (i == index) value else old
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown timer state (1 minutes = 60 seconds)
|
||||
var countdownSeconds by remember { mutableStateOf(10) }
|
||||
var countdownKey by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(countdownKey) {
|
||||
while (countdownSeconds > 0) {
|
||||
delay(1000) // Wait 1 second
|
||||
countdownSeconds--
|
||||
}
|
||||
}
|
||||
|
||||
// Format countdown as MM:SS
|
||||
val minutes = countdownSeconds / 60
|
||||
val seconds = countdownSeconds % 60
|
||||
val countdownText = String.format("%02d:%02d", minutes, seconds)
|
||||
|
||||
// Flag to determine if this is a sign-in flow for an existing user.
|
||||
val isSignInFlow = name == "existing_user"
|
||||
// Flag to determine if this is a signup flow (has signup data)
|
||||
val isSignupFlow = !isSignInFlow && (signupState != null || signupDistrict != null || signupVillage != null)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
Color(0xFFFFFBEA),
|
||||
Color(0xFFFDFBE8),
|
||||
Color(0xFFF7FEE7)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
DecorativeBackground()
|
||||
|
||||
val screenW = maxWidth.value
|
||||
val screenH = maxHeight.value
|
||||
|
||||
// Figma design reference size from Flutter widget
|
||||
val designW = 393.39f
|
||||
val designH = 852.53f
|
||||
|
||||
val scale = min(screenW / designW, screenH / designH)
|
||||
|
||||
fun s(v: Float) = (v * scale).dp // dp scaling
|
||||
fun fs(v: Float) = (v * scale).sp // font scaling
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(horizontal = 12.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// ---------------------------
|
||||
// "Enter OTP" Title
|
||||
// ---------------------------
|
||||
Text(
|
||||
text = "Enter OTP",
|
||||
color = Color(0xFF927B5E),
|
||||
fontSize = fs(20f),
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.25f),
|
||||
offset = Offset(0f, s(4f).value),
|
||||
blurRadius = s(4f).value
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// ---------------------------
|
||||
// OTP 4-Box Input Row
|
||||
// ---------------------------
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(s(17f))
|
||||
) {
|
||||
OTPInputTextFields(
|
||||
otpLength = otpLength,
|
||||
otpValues = otp.value,
|
||||
onUpdateOtpValuesByIndex = { index, value ->
|
||||
updateOtpAt(index,value)
|
||||
},
|
||||
onOtpInputComplete = {}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Countdown Timer
|
||||
// ---------------------------
|
||||
if (countdownSeconds > 0) {
|
||||
Text(
|
||||
text = countdownText,
|
||||
color = Color(0xFF927B5E),
|
||||
fontSize = fs(16f),
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.15f),
|
||||
offset = Offset(0f, s(2f).value),
|
||||
blurRadius = s(2f).value
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Resend OTP",
|
||||
color = Color(0xFFE17100),
|
||||
fontSize = fs(16f),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.clickable {
|
||||
// 🔹 Stub for now
|
||||
scope.launch {
|
||||
|
||||
authManager.requestOtp(phoneNumber)
|
||||
.onSuccess {
|
||||
// OTP sent successfully, navigate to OTP screen with signup data
|
||||
// Pass signup form data through the callback
|
||||
Toast.makeText(context, "Resent OTP",Toast.LENGTH_LONG).show()
|
||||
countdownSeconds = 10
|
||||
countdownKey++
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
Log.e("OtpScreen", "Failed to send OTP ${it.message}", it)
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// Continue Button
|
||||
// ---------------------------
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(vertical = 16.dp, horizontal = 48.dp)
|
||||
.size(s(279.25f), s(55.99f))
|
||||
.shadow(
|
||||
elevation = s(6f),
|
||||
ambientColor = Color.Black.copy(alpha = 0.10f),
|
||||
shape = RoundedCornerShape(s(16f)),
|
||||
)
|
||||
.shadow(
|
||||
elevation = s(15f),
|
||||
ambientColor = Color.Black.copy(alpha = 0.10f),
|
||||
shape = RoundedCornerShape(s(16f)),
|
||||
)
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFFFD9900), Color(0xFFE17100))
|
||||
),
|
||||
shape = RoundedCornerShape(s(16f))
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val otpString = otp.value.joinToString("") { it?.toString() ?: "" }
|
||||
|
||||
if (otpString.length < otpLength) {
|
||||
Toast.makeText(context, "Please enter full OTP", Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (isSignupFlow) {
|
||||
// For signup flow: Verify OTP first, then call signup API to update user with name/location
|
||||
android.util.Log.d("OTPScreen", "Signup flow: Verifying OTP...")
|
||||
authManager.login(phoneNumber, otpString)
|
||||
.onSuccess { verifyResponse ->
|
||||
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
|
||||
// OTP verified successfully - user is now logged in
|
||||
mainViewModel.refreshAuthStatus()
|
||||
// Now call signup API to update user with name and location
|
||||
val signupRequest = SignupRequest(
|
||||
name = name,
|
||||
phoneNumber = phoneNumber,
|
||||
state = signupState,
|
||||
district = signupDistrict,
|
||||
cityVillage = signupVillage
|
||||
)
|
||||
authManager.signup(signupRequest)
|
||||
.onSuccess { signupResponse ->
|
||||
android.util.Log.d("OTPScreen", "Signup API response: success=${signupResponse.success}, userExists=${signupResponse.userExists}, needsProfile=${signupResponse.needsProfile}, isNewAccount=${signupResponse.isNewAccount}")
|
||||
// Check if this is a new user account
|
||||
val isNewUser = signupResponse.isNewAccount == true
|
||||
|
||||
if (isNewUser) {
|
||||
// New user - navigate to landing page
|
||||
android.util.Log.d("OTPScreen", "New user detected - navigating to landing page")
|
||||
try {
|
||||
onLanding()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// Existing user or signup update - use existing logic
|
||||
if (signupResponse.success || signupResponse.userExists == true) {
|
||||
// Success - user is created/updated and logged in
|
||||
// Check if profile needs completion
|
||||
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
|
||||
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
|
||||
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
||||
// mainViewModel.refreshAuthStatus()
|
||||
try {
|
||||
if (needsProfile) {
|
||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// Signup failed but OTP was verified - user is logged in
|
||||
// Navigate to success anyway since verify-otp succeeded
|
||||
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
||||
val needsProfile = verifyResponse.needsProfile
|
||||
// Refresh auth status
|
||||
mainViewModel.refreshAuthStatus()
|
||||
try {
|
||||
if (needsProfile) {
|
||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
// Don't manually navigate - let AppNavigation handle it
|
||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// Show warning if signup update failed
|
||||
val errorMsg = signupResponse.message
|
||||
if (errorMsg != null) {
|
||||
Toast.makeText(context, "Profile update: $errorMsg", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure { signupError ->
|
||||
android.util.Log.e("OTPScreen", "Signup API failed: ${signupError.message}", signupError)
|
||||
// Signup API failed but OTP was verified - user is logged in
|
||||
// For new users, navigate to landing page
|
||||
// For existing users, use existing logic
|
||||
val needsProfile = verifyResponse.needsProfile
|
||||
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
|
||||
// Refresh auth status
|
||||
mainViewModel.refreshAuthStatus()
|
||||
try {
|
||||
// If this is a signup flow and signup failed, treat as new user
|
||||
if (isSignupFlow) {
|
||||
android.util.Log.d("OTPScreen", "Signup flow failed - navigating to landing page")
|
||||
onLanding()
|
||||
} else if (needsProfile) {
|
||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// Show warning about signup failure
|
||||
val errorMsg = signupError.message ?: "Profile update failed"
|
||||
Toast.makeText(context, "Warning: $errorMsg", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.onFailure { error ->
|
||||
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Invalid or expired OTP",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
// For sign-in flow: Just verify OTP and login
|
||||
authManager.login(phoneNumber, otpString)
|
||||
.onSuccess { response ->
|
||||
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
|
||||
// Tokens are now saved (synchronously via commit())
|
||||
// Refresh auth status - this will optimistically set authState to Authenticated
|
||||
// The LaunchedEffect in AppNavigation will automatically navigate to ChooseServiceScreen
|
||||
//mainViewModel.refreshAuthStatus()
|
||||
if(response.needsProfile) {
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
onSuccess()
|
||||
}
|
||||
android.util.Log.d("OTPScreen", "Auth status refreshed - navigation will happen automatically via LaunchedEffect")
|
||||
}
|
||||
.onFailure { error ->
|
||||
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||
|
||||
// Check if user not found - redirect to signup
|
||||
if (error is UserNotFoundException && error.errorCode == "USER_NOT_FOUND") {
|
||||
android.util.Log.d("OTPScreen", "User not found - redirecting to signup")
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.message ?: "Account not found. Please sign up to create a new account.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
// Navigate back to landing page so user can choose signup
|
||||
try {
|
||||
onLanding()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
} else {
|
||||
// Other errors (invalid OTP, expired, etc.)
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.message ?: "Invalid or expired OTP",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
"Continue",
|
||||
color = Color.White,
|
||||
fontSize = fs(16f),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OTPInputTextFields(
|
||||
otpLength: Int,
|
||||
onUpdateOtpValuesByIndex: (Int, String) -> Unit,
|
||||
onOtpInputComplete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
otpValues: List<String> = List(otpLength) { "" }, // Pass this as default for future reference
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val focusRequesters = List(otpLength) { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
// horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
otpValues.forEachIndexed { index, value ->
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
// .width(64.dp)
|
||||
.padding(6.dp)
|
||||
.focusRequester(focusRequesters[index])
|
||||
.background(Color.White)
|
||||
.onKeyEvent { keyEvent ->
|
||||
if (keyEvent.key == Key.Backspace) {
|
||||
if (otpValues[index].isEmpty() && index > 0) {
|
||||
onUpdateOtpValuesByIndex(index, "")
|
||||
focusRequesters[index - 1].requestFocus()
|
||||
} else {
|
||||
onUpdateOtpValuesByIndex(index, "")
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
value = value,
|
||||
onValueChange = { newValue ->
|
||||
// To use OTP code copied from keyboard
|
||||
if (newValue.length == otpLength) {
|
||||
for (i in otpValues.indices) {
|
||||
onUpdateOtpValuesByIndex(
|
||||
i,
|
||||
if (i < newValue.length && newValue[i].isDigit()) newValue[i].toString() else ""
|
||||
)
|
||||
}
|
||||
|
||||
keyboardController?.hide()
|
||||
onOtpInputComplete() // you should validate the otp values first for, if it is only digits or isNotEmpty
|
||||
} else if (newValue.length <= 1) {
|
||||
onUpdateOtpValuesByIndex(index, newValue)
|
||||
if (newValue.isNotEmpty()) {
|
||||
if (index < otpLength - 1) {
|
||||
focusRequesters[index + 1].requestFocus()
|
||||
} else {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onOtpInputComplete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (index < otpLength - 1) focusRequesters[index + 1].requestFocus()
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = if (index == otpLength - 1) ImeAction.Done else ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
if (index < otpLength - 1) {
|
||||
focusRequesters[index + 1].requestFocus()
|
||||
}
|
||||
},
|
||||
onDone = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onOtpInputComplete()
|
||||
}
|
||||
),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
isError = isError,
|
||||
textStyle = TextStyle(
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||
)
|
||||
)
|
||||
|
||||
LaunchedEffect(value) {
|
||||
if (otpValues.all { it.isNotEmpty() }) {
|
||||
focusManager.clearFocus()
|
||||
onOtpInputComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequesters.first().requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.api.UserNotFoundException
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLink
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
|
||||
import com.example.livingai_lg.ui.components.FarmHeader
|
||||
import com.example.livingai_lg.ui.utils.isKeyboardOpen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SignInScreen(
|
||||
onSignUpClick: () -> Unit = {},
|
||||
onSignInClick: (phone: String, name: String) -> Unit = {_,_->},
|
||||
) {
|
||||
val phoneNumber = remember { mutableStateOf("") }
|
||||
val isValid = phoneNumber.value.length == 10
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FarmTextLight)
|
||||
) {
|
||||
DecorativeBackground()
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||
val keyboardOpen = isKeyboardOpen()
|
||||
val topPadding by animateDpAsState(
|
||||
targetValue = if (keyboardOpen) 40.dp else 140.dp,
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding( top = 40.dp),//topPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (keyboardOpen) Arrangement.Top else Arrangement.Center
|
||||
) {
|
||||
//Spacer(Modifier.height(if (keyboardOpen) 32.dp else 56.dp))
|
||||
// Header
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
||||
FarmHeader()
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
PhoneNumberInput(
|
||||
phone = phoneNumber.value,
|
||||
onChange = { phoneNumber.value = it }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val fullPhoneNumber = "+91${phoneNumber.value}"
|
||||
scope.launch {
|
||||
// First check if user exists before requesting OTP
|
||||
authManager.checkUser(fullPhoneNumber)
|
||||
.onSuccess { checkResponse ->
|
||||
if (checkResponse.userExists) {
|
||||
// User exists, proceed to request OTP
|
||||
authManager.requestOtp(fullPhoneNumber)
|
||||
.onSuccess {
|
||||
onSignInClick(fullPhoneNumber, "existing_user")
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
Log.e("SignInScreen", "Failed to send OTP ${it.message}", it)
|
||||
}
|
||||
} else {
|
||||
// User doesn't exist (shouldn't happen if API works correctly)
|
||||
Toast.makeText(context, "User is not registered. Please sign up.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
.onFailure { error ->
|
||||
// User not found or other error
|
||||
if (error is UserNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.message ?: "User is not registered. Please sign up to create a new account.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = isValid,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isValid) Color(0xFFE17100) else Color(0xFFE17100).copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Sign In", fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Don't have an account? ", fontSize = 16.sp, color = FarmTextNormal)
|
||||
TextButton(onClick = onSignUpClick) {
|
||||
Text("Sign up", fontSize = 16.sp, color = FarmTextLink)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.SignupRequest
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import com.example.livingai_lg.ui.components.DropdownInput
|
||||
import com.example.livingai_lg.ui.components.InputField
|
||||
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
import com.example.livingai_lg.ui.components.FarmHeader
|
||||
import com.example.livingai_lg.ui.utils.isKeyboardOpen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SignUpFormData(
|
||||
val name: String = "",
|
||||
val state: String = "",
|
||||
val district: String = "",
|
||||
val village: String = "",
|
||||
val phoneNumber: String = ""
|
||||
){
|
||||
val isValid: Boolean
|
||||
get() =
|
||||
name.isNotBlank() &&
|
||||
state.isNotBlank() &&
|
||||
district.isNotBlank() &&
|
||||
village.isNotBlank() &&
|
||||
phoneNumber.isValidPhoneNumber()
|
||||
}
|
||||
|
||||
private fun String.isValidPhoneNumber(): Boolean {
|
||||
return length == 10 && all { it.isDigit() }
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SignUpScreen(
|
||||
onSignInClick: () -> Unit = {},
|
||||
onSignUpClick: (phone: String, name: String, state: String, district: String, village: String) -> Unit = {_,_,_,_,_->}
|
||||
) {
|
||||
var formData by remember { mutableStateOf(SignUpFormData()) }
|
||||
|
||||
var showStateDropdown by remember { mutableStateOf(false) }
|
||||
var showDistrictDropdown by remember { mutableStateOf(false) }
|
||||
var showVillageDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
val states = listOf("Maharashtra", "Gujarat", "Tamil Nadu", "Karnataka", "Rajasthan")
|
||||
val districts = listOf("Mumbai", "Pune", "Nagpur", "Aurangabad", "Nashik")
|
||||
val villages = listOf("Village 1", "Village 2", "Village 3", "Village 4", "Village 5")
|
||||
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
// Use 10.0.2.2 to connect to host machine's localhost from emulator
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FarmTextLight)
|
||||
) {
|
||||
DecorativeBackground()
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||
val keyboardOpen = isKeyboardOpen()
|
||||
// val topPadding by animateDpAsState(
|
||||
// targetValue = if (keyboardOpen) 40.dp else 140.dp,
|
||||
// animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
// )
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding( top = 40.dp),//topPadding),
|
||||
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (keyboardOpen) Arrangement.Top else Arrangement.Center
|
||||
) {
|
||||
// Spacer(Modifier.height(if (keyboardOpen) 32.dp else 56.dp))
|
||||
// Title
|
||||
FarmHeader()
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Name Input
|
||||
InputField(
|
||||
label = "Enter Name*",
|
||||
value = formData.name,
|
||||
placeholder = "Enter your name",
|
||||
onChange = { formData = formData.copy(name = it) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// State + District Row
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
|
||||
DropdownInput(
|
||||
label = "Enter Location",
|
||||
selected = formData.state,
|
||||
options = states,
|
||||
expanded = showStateDropdown,
|
||||
onExpandedChange = { showStateDropdown = it },
|
||||
onSelect = { formData = formData.copy(state = it) },
|
||||
placeholder = "Select State",
|
||||
modifier = Modifier.weight(0.4f) // <--- half width
|
||||
)
|
||||
|
||||
DropdownInput(
|
||||
selected = formData.district,
|
||||
options = districts,
|
||||
expanded = showDistrictDropdown,
|
||||
onExpandedChange = { showDistrictDropdown = it },
|
||||
onSelect = { formData = formData.copy(district = it) },
|
||||
placeholder = "Select District",
|
||||
modifier = Modifier.weight(0.6f) // <--- half width
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Village Dropdown
|
||||
DropdownInput(
|
||||
label = "Enter Village/Place",
|
||||
selected = formData.village,
|
||||
options = villages,
|
||||
expanded = showVillageDropdown,
|
||||
onExpandedChange = { showVillageDropdown = it },
|
||||
onSelect = {
|
||||
formData = formData.copy(village = it); showVillageDropdown = false
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Phone Number
|
||||
PhoneNumberInput(
|
||||
phone = formData.phoneNumber,
|
||||
onChange = { formData = formData.copy(phoneNumber = it) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
// Sign Up button
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val fullPhoneNumber = "+91${formData.phoneNumber}"
|
||||
|
||||
// First check if user already exists
|
||||
authManager.checkUser(fullPhoneNumber)
|
||||
.onSuccess { checkResponse ->
|
||||
if (checkResponse.userExists) {
|
||||
// User already registered - show message to sign in instead
|
||||
Toast.makeText(
|
||||
context,
|
||||
"This phone number is already registered. Please sign in instead.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
// Optionally navigate to sign in screen
|
||||
onSignInClick()
|
||||
} else {
|
||||
// User doesn't exist - proceed with signup
|
||||
// Request OTP first before allowing signup
|
||||
authManager.requestOtp(fullPhoneNumber)
|
||||
.onSuccess {
|
||||
// OTP sent successfully, navigate to OTP screen with signup data
|
||||
// Pass signup form data through the callback
|
||||
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure { checkError ->
|
||||
// If check fails, it might be a network error or user doesn't exist
|
||||
// Try to proceed with signup (user might not exist)
|
||||
if (checkError is com.example.livingai_lg.api.UserNotFoundException) {
|
||||
// User doesn't exist - proceed with signup
|
||||
authManager.requestOtp(fullPhoneNumber)
|
||||
.onSuccess {
|
||||
// OTP sent successfully, navigate to OTP screen with signup data
|
||||
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// Other error (network, etc.)
|
||||
Toast.makeText(context, "Unable to verify phone number: ${checkError.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
enabled = formData.isValid,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE17100))
|
||||
) {
|
||||
Text("Sign Up", fontSize = 16.sp)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Already have account?
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Already have an account? ", color = FarmTextNormal)
|
||||
TextButton(onClick = onSignInClick) {
|
||||
Text("Sign In", color = Color(0xFFE17100))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
package com.example.farmmarketplace.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.CallMade
|
||||
import androidx.compose.material.icons.automirrored.filled.CallMissed
|
||||
import androidx.compose.material.icons.automirrored.filled.CallReceived
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||
|
||||
data class CallRecord(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val callType: CallType,
|
||||
val duration: String,
|
||||
val timestamp: String,
|
||||
val isVideoCall: Boolean = false
|
||||
)
|
||||
|
||||
enum class CallType {
|
||||
INCOMING,
|
||||
OUTGOING,
|
||||
MISSED
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onCallClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {},
|
||||
onCallItemClick: (String) -> Unit = {},
|
||||
onTabClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
val callHistory = listOf(
|
||||
CallRecord(
|
||||
id = "1",
|
||||
name = "Farmer Kumar",
|
||||
initials = "FK",
|
||||
callType = CallType.INCOMING,
|
||||
duration = "5m 23s",
|
||||
timestamp = "Today, 2:30 PM"
|
||||
),
|
||||
CallRecord(
|
||||
id = "2",
|
||||
name = "Seller Raj",
|
||||
initials = "SR",
|
||||
callType = CallType.OUTGOING,
|
||||
duration = "2m 10s",
|
||||
timestamp = "Today, 11:45 AM"
|
||||
),
|
||||
CallRecord(
|
||||
id = "3",
|
||||
name = "Buyer Priya",
|
||||
initials = "BP",
|
||||
callType = CallType.MISSED,
|
||||
duration = "",
|
||||
timestamp = "Yesterday, 8:15 PM"
|
||||
),
|
||||
CallRecord(
|
||||
id = "4",
|
||||
name = "Seller 1",
|
||||
initials = "S1",
|
||||
callType = CallType.OUTGOING,
|
||||
duration = "8m 45s",
|
||||
timestamp = "Yesterday, 5:30 PM",
|
||||
isVideoCall = true
|
||||
),
|
||||
CallRecord(
|
||||
id = "5",
|
||||
name = "Veterinarian",
|
||||
initials = "V",
|
||||
callType = CallType.INCOMING,
|
||||
duration = "12m 30s",
|
||||
timestamp = "2 days ago"
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = chatBottomNavItems,
|
||||
currentItem = "Calls",
|
||||
onBottomNavItemClick = onTabClick,
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||
) {
|
||||
CallsHeader(
|
||||
onBackClick = onBackClick,
|
||||
onCallClick = onCallClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(callHistory) { call ->
|
||||
CallHistoryItem(
|
||||
call = call,
|
||||
onClick = { onCallItemClick(call.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactsBottomNav(
|
||||
// currentTab = ContactsTab.CALLS,
|
||||
// onTabClick = onTabClick
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsHeader(
|
||||
onBackClick: () -> Unit,
|
||||
onCallClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Calls",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 42.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Phone,
|
||||
contentDescription = "New Call",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onCallClick() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMenuClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallHistoryItem(
|
||||
call: CallRecord,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(76.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8), RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = call.initials,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 21.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = call.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (call.callType) {
|
||||
CallType.INCOMING -> Icons.AutoMirrored.Filled.CallReceived
|
||||
CallType.OUTGOING -> Icons.AutoMirrored.Filled.CallMade
|
||||
CallType.MISSED -> Icons.AutoMirrored.Filled.CallMissed
|
||||
},
|
||||
contentDescription = call.callType.name,
|
||||
tint = when (call.callType) {
|
||||
CallType.INCOMING -> Color(0xFF00A63E)
|
||||
CallType.OUTGOING -> Color(0xFF155DFC)
|
||||
CallType.MISSED -> Color(0xFFE7000B)
|
||||
},
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = when (call.callType) {
|
||||
CallType.INCOMING -> "Incoming • ${call.duration}"
|
||||
CallType.OUTGOING -> "Outgoing • ${call.duration}"
|
||||
CallType.MISSED -> "Missed"
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = call.timestamp,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = if (call.isVideoCall) Icons.Default.Videocam else Icons.Default.Phone,
|
||||
contentDescription = if (call.isVideoCall) "Video Call" else "Voice Call",
|
||||
tint = Color(0xFF030213),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
package com.example.livingai_lg.ui.screens.chat
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Attachment
|
||||
import androidx.compose.material.icons.filled.KeyboardVoice
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
data class ChatMessage(
|
||||
val text: String,
|
||||
val timestamp: String,
|
||||
val isSentByCurrentUser: Boolean
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
sellerName: String = "Seller 1",
|
||||
sellerStatus: String = "Online",
|
||||
onBackClick: () -> Unit = {},
|
||||
onPhoneClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {}
|
||||
) {
|
||||
var messageText by remember { mutableStateOf("") }
|
||||
|
||||
val messages = remember {
|
||||
listOf(
|
||||
ChatMessage(
|
||||
text = "Hello! Yes, the cow is still available. Would you like to visit?",
|
||||
timestamp = "10:30 AM",
|
||||
isSentByCurrentUser = false
|
||||
),
|
||||
ChatMessage(
|
||||
text = "Hey is she still available",
|
||||
timestamp = "10:28 AM",
|
||||
isSentByCurrentUser = true
|
||||
),
|
||||
ChatMessage(
|
||||
text = "She's a healthy Gir cow, 2 years old, great milk yield.",
|
||||
timestamp = "10:31 AM",
|
||||
isSentByCurrentUser = false
|
||||
),
|
||||
ChatMessage(
|
||||
text = "What's the price?",
|
||||
timestamp = "10:32 AM",
|
||||
isSentByCurrentUser = true
|
||||
),
|
||||
ChatMessage(
|
||||
text = "₹45,000. Negotiable if you visit today.",
|
||||
timestamp = "10:33 AM",
|
||||
isSentByCurrentUser = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF8F8F8))
|
||||
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
ChatHeader(
|
||||
sellerName = sellerName,
|
||||
sellerStatus = sellerStatus,
|
||||
onBackClick = onBackClick,
|
||||
onPhoneClick = onPhoneClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(vertical = 16.dp)
|
||||
) {
|
||||
items(messages) { message ->
|
||||
MessageBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
MessageInputBar(
|
||||
messageText = messageText,
|
||||
onMessageChange = { messageText = it },
|
||||
onSendClick = {
|
||||
messageText = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatHeader(
|
||||
sellerName: String,
|
||||
sellerStatus: String,
|
||||
onBackClick: () -> Unit,
|
||||
onPhoneClick: () -> Unit,
|
||||
onMenuClick: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 1.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(Color(0xFFF0F0F0), shape = CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "S1",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = sellerName,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1E2939)
|
||||
)
|
||||
Text(
|
||||
text = sellerStatus,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF4A5565)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
IconButton(onClick = onPhoneClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Phone,
|
||||
contentDescription = "Phone",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onMenuClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageBubble(message: ChatMessage) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = if (message.isSentByCurrentUser) Alignment.End else Alignment.Start
|
||||
) {
|
||||
Surface(
|
||||
shape = if (message.isSentByCurrentUser) {
|
||||
RoundedCornerShape(topStart = 16.dp, topEnd = 4.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||
} else {
|
||||
RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||
},
|
||||
color = if (message.isSentByCurrentUser) Color(0xFF0A0A0A) else Color.White,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 271.dp)
|
||||
.then(
|
||||
if (!message.isSentByCurrentUser) {
|
||||
Modifier.border(
|
||||
width = 1.dp,
|
||||
color = Color(0x1A000000),
|
||||
shape = RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message.text,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
color = if (message.isSentByCurrentUser) Color.White else Color(0xFF0A0A0A)
|
||||
)
|
||||
Text(
|
||||
text = message.timestamp,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = if (message.isSentByCurrentUser) Color(0x99FFFFFF) else Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageInputBar(
|
||||
messageText: String,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onSendClick: () -> Unit
|
||||
) {
|
||||
val isMultiline = messageText.contains("\n") || messageText.length > 40
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().imePadding(),
|
||||
color = Color.White,
|
||||
shadowElevation = 1.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
// TEXT INPUT
|
||||
OutlinedTextField(
|
||||
value = messageText,
|
||||
onValueChange = onMessageChange,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(
|
||||
min = if (isMultiline) 44.dp else 36.dp,
|
||||
max = 120.dp
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Type a message",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontSize = 14.sp,
|
||||
color = Color.Black // ✅ typed text black
|
||||
),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x33000000),
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
cursorColor = Color.Black
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
singleLine = false,
|
||||
maxLines = 4
|
||||
)
|
||||
|
||||
// ATTACHMENT ICON
|
||||
IconButton(
|
||||
onClick = { },
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Attachment,
|
||||
contentDescription = "Attachment",
|
||||
tint = Color(0xFF717182),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// VOICE BUTTON
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(Color(0xFF0A0A0A), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { },
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardVoice,
|
||||
contentDescription = "Voice",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SEND BUTTON
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(Color(0xFF0A0A0A), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onSendClick,
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
package com.example.livingai_lg.ui.screens.chat
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.farmmarketplace.ui.screens.ContactsBottomNav
|
||||
import com.example.farmmarketplace.ui.screens.ContactsTab
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||
|
||||
data class ChatPreview(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val lastMessage: String,
|
||||
val timestamp: String,
|
||||
val isOnline: Boolean = false,
|
||||
val unreadCount: Int = 0
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChatsScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onNewChatClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {},
|
||||
onChatItemClick: (String) -> Unit = {},
|
||||
onTabClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
val chatList = listOf(
|
||||
ChatPreview(
|
||||
id = "1",
|
||||
name = "Farmer Kumar",
|
||||
initials = "FK",
|
||||
lastMessage = "The cows are healthy and ready for viewing",
|
||||
timestamp = "Today, 2:30 PM",
|
||||
isOnline = true,
|
||||
unreadCount = 2
|
||||
),
|
||||
ChatPreview(
|
||||
id = "2",
|
||||
name = "Seller Raj",
|
||||
initials = "SR",
|
||||
lastMessage = "You: Can you send more photos?",
|
||||
timestamp = "Today, 11:45 AM",
|
||||
isOnline = true,
|
||||
unreadCount = 0
|
||||
),
|
||||
ChatPreview(
|
||||
id = "3",
|
||||
name = "Buyer Priya",
|
||||
initials = "BP",
|
||||
lastMessage = "What's the best time to visit?",
|
||||
timestamp = "Yesterday, 8:15 PM",
|
||||
isOnline = false,
|
||||
unreadCount = 1
|
||||
),
|
||||
ChatPreview(
|
||||
id = "4",
|
||||
name = "Seller 1",
|
||||
initials = "S1",
|
||||
lastMessage = "You: Thanks for the information",
|
||||
timestamp = "Yesterday, 5:30 PM",
|
||||
isOnline = true,
|
||||
unreadCount = 0
|
||||
),
|
||||
ChatPreview(
|
||||
id = "5",
|
||||
name = "Veterinarian",
|
||||
initials = "V",
|
||||
lastMessage = "The animal health check is complete",
|
||||
timestamp = "2 days ago",
|
||||
isOnline = false,
|
||||
unreadCount = 0
|
||||
),
|
||||
ChatPreview(
|
||||
id = "6",
|
||||
name = "Market Vendor",
|
||||
initials = "MV",
|
||||
lastMessage = "You: Available this weekend?",
|
||||
timestamp = "3 days ago",
|
||||
isOnline = false,
|
||||
unreadCount = 0
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = chatBottomNavItems,
|
||||
currentItem = "Chats",
|
||||
onBottomNavItemClick = onTabClick,
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||
) {
|
||||
ChatsHeader(
|
||||
onBackClick = onBackClick,
|
||||
onNewChatClick = onNewChatClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(chatList) { chat ->
|
||||
ChatListItem(
|
||||
chat = chat,
|
||||
onClick = { onChatItemClick(chat.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactsBottomNav(
|
||||
// currentTab = ContactsTab.CHATS,
|
||||
// onTabClick = onTabClick
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatsHeader(
|
||||
onBackClick: () -> Unit,
|
||||
onNewChatClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Chats",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 42.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "New Chat",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onNewChatClick() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMenuClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListItem(
|
||||
chat: ChatPreview,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(76.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8), RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = chat.initials,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 21.sp
|
||||
)
|
||||
|
||||
if (chat.isOnline) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(12.dp)
|
||||
.background(Color(0xFF00A63E), CircleShape)
|
||||
.border(2.dp, Color.White, CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = chat.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = chat.lastMessage,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = chat.timestamp,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
|
||||
if (chat.unreadCount > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(Color(0xFF155DFC), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = chat.unreadCount.toString(),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
package com.example.farmmarketplace.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.automirrored.filled.Message
|
||||
import androidx.compose.material.icons.automirrored.outlined.Message
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.Contacts
|
||||
import androidx.compose.material.icons.filled.Message
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
|
||||
data class Contact(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val status: String,
|
||||
val isOnline: Boolean = false,
|
||||
val phoneNumber: String? = null
|
||||
)
|
||||
|
||||
enum class ContactsTab {
|
||||
CONTACTS,
|
||||
CALLS,
|
||||
CHATS
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onSearchClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {},
|
||||
onContactClick: (String) -> Unit = {},
|
||||
onCallClick: (String) -> Unit = {},
|
||||
onMessageClick: (String) -> Unit = {},
|
||||
onTabClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
val contacts = listOf(
|
||||
Contact(
|
||||
id = "1",
|
||||
name = "Farmer Kumar",
|
||||
initials = "FK",
|
||||
status = "Online",
|
||||
isOnline = true
|
||||
),
|
||||
Contact(
|
||||
id = "2",
|
||||
name = "Seller Raj",
|
||||
initials = "SR",
|
||||
status = "+91 98765 43211",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43211"
|
||||
),
|
||||
Contact(
|
||||
id = "3",
|
||||
name = "Buyer Priya",
|
||||
initials = "BP",
|
||||
status = "Online",
|
||||
isOnline = true
|
||||
),
|
||||
Contact(
|
||||
id = "4",
|
||||
name = "Seller 1",
|
||||
initials = "S1",
|
||||
status = "+91 98765 43213",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43213"
|
||||
),
|
||||
Contact(
|
||||
id = "5",
|
||||
name = "Veterinarian",
|
||||
initials = "V",
|
||||
status = "Online",
|
||||
isOnline = true
|
||||
),
|
||||
Contact(
|
||||
id = "6",
|
||||
name = "Farm Supply",
|
||||
initials = "FS",
|
||||
status = "+91 98765 43215",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43215"
|
||||
),
|
||||
Contact(
|
||||
id = "7",
|
||||
name = "Transport Co.",
|
||||
initials = "TC",
|
||||
status = "+91 98765 43216",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43216"
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = chatBottomNavItems,
|
||||
currentItem = "Contacts",
|
||||
onBottomNavItemClick = onTabClick,
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||
) {
|
||||
ContactsHeader(
|
||||
onBackClick = onBackClick,
|
||||
onSearchClick = onSearchClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
items(contacts) { contact ->
|
||||
ContactItem(
|
||||
contact = contact,
|
||||
onContactClick = { onContactClick(contact.id) },
|
||||
onCallClick = { onCallClick(contact.id) },
|
||||
onMessageClick = { onMessageClick(contact.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactsBottomNav(
|
||||
// currentTab = ContactsTab.CONTACTS,
|
||||
// onTabClick = onTabClick
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsHeader(
|
||||
onBackClick: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Contacts",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 42.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onSearchClick() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMenuClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactItem(
|
||||
contact: Contact,
|
||||
onContactClick: () -> Unit,
|
||||
onCallClick: () -> Unit,
|
||||
onMessageClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(73.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.05f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onContactClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = contact.initials,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 21.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = contact.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = contact.status,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onCallClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Phone,
|
||||
contentDescription = "Call",
|
||||
tint = Color(0xFF030213),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMessageClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Message,
|
||||
contentDescription = "Message",
|
||||
tint = Color(0xFF030213),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsBottomNav(
|
||||
currentTab: ContactsTab,
|
||||
onTabClick: (route: String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
.padding(horizontal = 32.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
ContactsTabItem(
|
||||
icon = Icons.AutoMirrored.Filled.Chat,
|
||||
label = "Chats",
|
||||
isSelected = currentTab == ContactsTab.CHATS,
|
||||
onClick = { onTabClick(AppScreen.CHATS) }
|
||||
)
|
||||
ContactsTabItem(
|
||||
icon = Icons.Default.Phone,
|
||||
label = "Calls",
|
||||
isSelected = currentTab == ContactsTab.CALLS,
|
||||
onClick = { onTabClick(AppScreen.CALLS) }
|
||||
)
|
||||
ContactsTabItem(
|
||||
icon = Icons.Default.Contacts,
|
||||
label = "Contacts",
|
||||
isSelected = currentTab == ContactsTab.CONTACTS,
|
||||
onClick = { onTabClick(AppScreen.CONTACTS) }
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsTabItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package com.example.livingai_lg.ui.screens.testing
|
||||
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ApiTestScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Default API endpoint
|
||||
var apiEndpoint by remember {
|
||||
mutableStateOf("https://jsonplaceholder.typicode.com/posts/1")
|
||||
}
|
||||
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var responseText by remember { mutableStateOf<String?>(null) }
|
||||
var errorText by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val client = remember { OkHttpClient() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Top App Bar
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "API Test",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Black
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.Black
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
|
||||
// API endpoint input
|
||||
OutlinedTextField(
|
||||
value = apiEndpoint,
|
||||
onValueChange = { apiEndpoint = it },
|
||||
label = { Text("API Endpoint") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Call API button
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
responseText = null
|
||||
errorText = null
|
||||
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url(apiEndpoint)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
val body = response.body?.string()
|
||||
Pair(response.code, body)
|
||||
}
|
||||
}
|
||||
|
||||
val (code, body) = result
|
||||
responseText = "Status: $code\n\n$body"
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
errorText = e.toString()
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading
|
||||
) {
|
||||
Text(if (isLoading) "Calling API..." else "Call API")
|
||||
}
|
||||
|
||||
// Result display
|
||||
when {
|
||||
responseText != null -> {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = responseText ?: "",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
errorText != null -> {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color(0xFFFFEBEE),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = errorText ?: "",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFE53935)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai_lg.ui.theme
|
||||
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
object AppTypography {
|
||||
val Display = 32.sp
|
||||
val Title = 20.sp
|
||||
val Body = 16.sp
|
||||
val BodySmall = 14.sp
|
||||
val Caption = 12.sp
|
||||
val Micro = 10.sp
|
||||
}
|
||||
|
|
@ -2,25 +2,57 @@ package com.example.livingai_lg.ui.theme
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.example.livingai_lg.ui.utils
|
||||
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class FormatUtils {
|
||||
}
|
||||
fun formatter(): NumberFormat{
|
||||
return NumberFormat.getInstance(Locale.forLanguageTag("en-IN"))
|
||||
}
|
||||
|
||||
|
||||
fun formatViews(views: Long?): String {
|
||||
val count = views ?: 0
|
||||
|
||||
val formattedNumber = formatter().format(count)
|
||||
|
||||
return when (count) {
|
||||
1L -> "$formattedNumber View"
|
||||
else -> "$formattedNumber Views"
|
||||
}
|
||||
}
|
||||
|
||||
fun formatPrice(price: Long?): String {
|
||||
val value = price ?: 0
|
||||
|
||||
val formattedNumber = formatter().format(value)
|
||||
|
||||
return "₹$formattedNumber"
|
||||
}
|
||||
|
||||
fun formatDistance(distanceMeters: Long?): String {
|
||||
val meters = distanceMeters ?: 0L
|
||||
|
||||
return if (meters < 1_000) {
|
||||
val unit = if (meters == 1L) "mt" else "mts"
|
||||
"$meters $unit away"
|
||||
} else {
|
||||
val km = meters / 1_000.0
|
||||
val formatter = DecimalFormat("#.#") // 1 decimal if needed
|
||||
val formattedKm = formatter.format(km)
|
||||
|
||||
val unit = if (formattedKm == "1") "km" else "kms"
|
||||
"$formattedKm $unit away"
|
||||
}
|
||||
}
|
||||
|
||||
fun formatAge(months: Int?): String {
|
||||
val value = months ?: 0
|
||||
|
||||
val years = value / 12f
|
||||
val roundedYears = (years * 10).roundToInt() / 10f
|
||||
|
||||
val unit = if (roundedYears == 1f) "Year" else "Years"
|
||||
|
||||
return "$roundedYears $unit"
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai_lg.ui.utils
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
||||
@Composable
|
||||
fun isKeyboardOpen(): Boolean {
|
||||
val density = LocalDensity.current
|
||||
return WindowInsets.ime.getBottom(density) > 0
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.example.livingai_lg.ui.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import com.example.livingai_lg.ui.models.MediaType
|
||||
|
||||
fun createMediaUri(
|
||||
context: Context,
|
||||
type: MediaType
|
||||
): Uri {
|
||||
val directory = when (type) {
|
||||
MediaType.PHOTO -> File(context.cacheDir, "images")
|
||||
MediaType.VIDEO -> File(context.cacheDir, "videos")
|
||||
}
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
val fileName = when (type) {
|
||||
MediaType.PHOTO -> "${UUID.randomUUID()}.jpg"
|
||||
MediaType.VIDEO -> "${UUID.randomUUID()}.mp4"
|
||||
}
|
||||
|
||||
val file = File(directory, fileName)
|
||||
|
||||
return FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun getVideoThumbnail(
|
||||
context: Context,
|
||||
uri: Uri
|
||||
): Bitmap? {
|
||||
return try {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(context, uri)
|
||||
retriever.getFrameAtTime(0)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="393dp"
|
||||
android:height="852dp"
|
||||
android:viewportWidth="393"
|
||||
android:viewportHeight="852">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h393v852h-393z"/>
|
||||
<path
|
||||
android:pathData="M0,0h393v852h-393z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M0,0h393v852h-393z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:endX="648.1"
|
||||
android:endY="298.95"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFFBEB"/>
|
||||
<item android:offset="1" android:color="#FFEFF6FF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M0,0h393.39v852.53h-393.39z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:endX="648.67"
|
||||
android:endY="299.32"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFFFBEB"/>
|
||||
<item android:offset="0.5" android:color="#FFFEFCE8"/>
|
||||
<item android:offset="1" android:color="#FFF7FEE7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M32.11,128.78H111.87V230.77C111.87,236.29 107.39,240.77 101.87,240.77H42.11C36.59,240.77 32.11,236.29 32.11,230.77V128.78Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#C10007"
|
||||
android:fillAlpha="0.09"/>
|
||||
<path
|
||||
android:pathData="M56.11,138.78C56.11,133.26 60.59,128.78 66.11,128.78H77.87C83.4,128.78 87.87,133.26 87.87,138.78V176.77H56.11V138.78Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#7B3306"
|
||||
android:fillAlpha="0.09"/>
|
||||
<path
|
||||
android:pathData="M16,690.55C16,688.34 17.79,686.55 20,686.55C22.21,686.55 24,688.34 24,690.55V730.55C24,732.76 22.21,734.55 20,734.55C17.79,734.55 16,732.76 16,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M29.99,690.55C29.99,688.34 31.78,686.55 33.99,686.55C36.2,686.55 37.99,688.34 37.99,690.55V730.55C37.99,732.76 36.2,734.55 33.99,734.55C31.78,734.55 29.99,732.76 29.99,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M61.99,690.55C61.99,688.34 63.78,686.55 65.99,686.55C68.2,686.55 69.99,688.34 69.99,690.55V730.55C69.99,732.76 68.2,734.55 65.99,734.55C63.78,734.55 61.99,732.76 61.99,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M75.98,690.55C75.98,688.34 77.77,686.55 79.98,686.55C82.19,686.55 83.98,688.34 83.98,690.55V730.55C83.98,732.76 82.19,734.55 79.98,734.55C77.77,734.55 75.98,732.76 75.98,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M107.98,690.55C107.98,688.34 109.77,686.55 111.98,686.55C114.19,686.55 115.98,688.34 115.98,690.55V730.55C115.98,732.76 114.19,734.55 111.98,734.55C109.77,734.55 107.98,732.76 107.98,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M121.98,690.55C121.98,688.34 123.77,686.55 125.97,686.55C128.18,686.55 129.98,688.34 129.98,690.55V730.55C129.98,732.76 128.18,734.55 125.97,734.55C123.77,734.55 121.98,732.76 121.98,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M153.97,690.55C153.97,688.34 155.76,686.55 157.97,686.55C160.18,686.55 161.97,688.34 161.97,690.55V730.55C161.97,732.76 160.18,734.55 157.97,734.55C155.76,734.55 153.97,732.76 153.97,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M167.97,690.55C167.97,688.34 169.76,686.55 171.97,686.55C174.18,686.55 175.97,688.34 175.97,690.55V730.55C175.97,732.76 174.18,734.55 171.97,734.55C169.76,734.55 167.97,732.76 167.97,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M199.96,690.55C199.96,688.34 201.76,686.55 203.96,686.55C206.17,686.55 207.96,688.34 207.96,690.55V730.55C207.96,732.76 206.17,734.55 203.96,734.55C201.76,734.55 199.96,732.76 199.96,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M213.96,690.55C213.96,688.34 215.75,686.55 217.96,686.55C220.17,686.55 221.96,688.34 221.96,690.55V730.55C221.96,732.76 220.17,734.55 217.96,734.55C215.75,734.55 213.96,732.76 213.96,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M245.96,690.55C245.96,688.34 247.75,686.55 249.96,686.55C252.16,686.55 253.95,688.34 253.95,690.55V730.55C253.95,732.76 252.16,734.55 249.96,734.55C247.75,734.55 245.96,732.76 245.96,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M259.95,690.55C259.95,688.34 261.74,686.55 263.95,686.55C266.16,686.55 267.95,688.34 267.95,690.55V730.55C267.95,732.76 266.16,734.55 263.95,734.55C261.74,734.55 259.95,732.76 259.95,730.55V690.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M16,707.55C16,705.9 17.34,704.55 19,704.55H374.4C376.05,704.55 377.39,705.9 377.39,707.55C377.39,709.21 376.05,710.55 374.4,710.55H19C17.34,710.55 16,709.21 16,707.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M16,721.55C16,719.89 17.34,718.55 19,718.55H374.4C376.05,718.55 377.39,719.89 377.39,721.55C377.39,723.2 376.05,724.54 374.4,724.54H19C17.34,724.54 16,723.2 16,721.55Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#973C00"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M241.41,618.56C241.41,613.04 245.88,608.56 251.41,608.56H279.4C284.93,608.56 289.4,613.04 289.4,618.56V638.56C289.4,644.08 284.93,648.56 279.4,648.56H251.41C245.88,648.56 241.41,644.08 241.41,638.56V618.56Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#D08700"
|
||||
android:fillAlpha="0.105"/>
|
||||
<path
|
||||
android:pathData="M297.4,618.56C297.4,613.04 301.88,608.56 307.4,608.56H335.4C340.92,608.56 345.4,613.04 345.4,618.56V638.56C345.4,644.08 340.92,648.56 335.4,648.56H307.4C301.88,648.56 297.4,644.08 297.4,638.56V618.56Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#A65F00"
|
||||
android:fillAlpha="0.105"/>
|
||||
<path
|
||||
android:pathData="M241.41,662.55C241.41,657.03 245.88,652.55 251.41,652.55H279.4C284.93,652.55 289.4,657.03 289.4,662.55V682.55C289.4,688.07 284.93,692.55 279.4,692.55H251.41C245.88,692.55 241.41,688.07 241.41,682.55V662.55Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#D08700"
|
||||
android:fillAlpha="0.105"/>
|
||||
<path
|
||||
android:pathData="M63.99,622.07H123.99V562.07H63.99V622.07Z"
|
||||
android:strokeAlpha="0.12"
|
||||
android:fillColor="#33FFFFFF"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M264.9,203.67H312.9V155.67H264.9V203.67Z"
|
||||
android:strokeAlpha="0.1"
|
||||
android:fillColor="#33FFFFFF"
|
||||
android:fillAlpha="0.1"/>
|
||||
<path
|
||||
android:pathData="M79.99,770.99H115.99V734.99H79.99V770.99Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#33FFFFFF"
|
||||
android:fillAlpha="0.15"/>
|
||||
<path
|
||||
android:pathData="M123.56,762.54H153.56V732.54H123.56V762.54Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#33FFFFFF"
|
||||
android:fillAlpha="0.15"/>
|
||||
<path
|
||||
android:pathData="M161.74,770.99H197.74V734.99H161.74V770.99Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#33FFFFFF"
|
||||
android:fillAlpha="0.15"/>
|
||||
<path
|
||||
android:pathData="M281.4,79.99C281.4,62.32 295.73,48 313.4,48C331.07,48 345.4,62.32 345.4,79.99C345.4,97.66 331.07,111.99 313.4,111.99C295.73,111.99 281.4,97.66 281.4,79.99Z"
|
||||
android:strokeAlpha="0.2"
|
||||
android:fillColor="#FDC700"
|
||||
android:fillAlpha="0.12"/>
|
||||
<path
|
||||
android:pathData="M115.94,127.99C115.94,119.15 123.1,111.99 131.94,111.99H147.94C156.77,111.99 163.93,119.15 163.93,127.99C163.93,136.82 156.77,143.99 147.94,143.99H131.94C123.1,143.99 115.94,136.82 115.94,127.99Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.4"/>
|
||||
<path
|
||||
android:pathData="M167.93,128C167.93,116.95 176.88,108 187.92,108H203.92C214.97,108 223.92,116.95 223.92,128C223.92,139.04 214.97,147.99 203.92,147.99H187.92C176.88,147.99 167.93,139.04 167.93,128Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.4"/>
|
||||
<path
|
||||
android:pathData="M227.91,127.99C227.91,119.15 235.07,111.99 243.91,111.99H251.91C260.74,111.99 267.91,119.15 267.91,127.99C267.91,136.82 260.74,143.99 251.91,143.99H243.91C235.07,143.99 227.91,136.82 227.91,127.99Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.4"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="393dp"
|
||||
android:height="852dp"
|
||||
android:viewportWidth="393"
|
||||
android:viewportHeight="852">
|
||||
<path
|
||||
android:pathData="M341.53,596.63V574.64"
|
||||
android:strokeWidth="1.2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#B5A89F"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M339.69,581.64C340.96,581.64 341.99,579.85 341.99,577.64C341.99,575.43 340.96,573.64 339.69,573.64C338.42,573.64 337.39,575.43 337.39,577.64C337.39,579.85 338.42,581.64 339.69,581.64Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M343.37,581.64C344.64,581.64 345.68,579.85 345.68,577.64C345.68,575.43 344.64,573.64 343.37,573.64C342.1,573.64 341.07,575.43 341.07,577.64C341.07,579.85 342.1,581.64 343.37,581.64Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M339.23,586.63C340.5,586.63 341.53,584.84 341.53,582.64C341.53,580.43 340.5,578.64 339.23,578.64C337.96,578.64 336.93,580.43 336.93,582.64C336.93,584.84 337.96,586.63 339.23,586.63Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M343.83,586.63C345.11,586.63 346.14,584.84 346.14,582.64C346.14,580.43 345.11,578.64 343.83,578.64C342.56,578.64 341.53,580.43 341.53,582.64C341.53,584.84 342.56,586.63 343.83,586.63Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M339.69,592.63C340.96,592.63 341.99,590.84 341.99,588.63C341.99,586.42 340.96,584.64 339.69,584.64C338.42,584.64 337.39,586.42 337.39,588.63C337.39,590.84 338.42,592.63 339.69,592.63Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M343.37,592.63C344.64,592.63 345.68,590.84 345.68,588.63C345.68,586.42 344.64,584.64 343.37,584.64C342.1,584.64 341.07,586.42 341.07,588.63C341.07,590.84 342.1,592.63 343.37,592.63Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M297.79,56.96H274.77C274.01,56.96 273.39,57.64 273.39,58.46V77.45C273.39,78.28 274.01,78.95 274.77,78.95H297.79C298.56,78.95 299.17,78.28 299.17,77.45V58.46C299.17,57.64 298.56,56.96 297.79,56.96Z"
|
||||
android:strokeWidth="1.3"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M270.63,56.96L286.28,42.97L301.94,56.96"
|
||||
android:strokeWidth="1.3"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#B5A89F"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M289.23,64.96H281.5C281.09,64.96 280.76,65.32 280.76,65.76V78.15C280.76,78.59 281.09,78.95 281.5,78.95H289.23C289.64,78.95 289.97,78.59 289.97,78.15V65.76C289.97,65.32 289.64,64.96 289.23,64.96Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M41.35,543.66C51.52,543.66 59.77,537.4 59.77,529.67C59.77,521.94 51.52,515.68 41.35,515.68C31.18,515.68 22.94,521.94 22.94,529.67C22.94,537.4 31.18,543.66 41.35,543.66Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M32.14,535.67C37.74,535.67 42.27,530.3 42.27,523.67C42.27,517.05 37.74,511.68 32.14,511.68C26.55,511.68 22.02,517.05 22.02,523.67C22.02,530.3 26.55,535.67 32.14,535.67Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M33.53,542.41C33.53,541.45 32.8,540.66 31.91,540.66C31.02,540.66 30.3,541.45 30.3,542.41V550.91C30.3,551.87 31.02,552.66 31.91,552.66C32.8,552.66 33.53,551.87 33.53,550.91V542.41Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M39.97,542.41C39.97,541.45 39.25,540.66 38.36,540.66C37.47,540.66 36.75,541.45 36.75,542.41V550.91C36.75,551.87 37.47,552.66 38.36,552.66C39.25,552.66 39.97,551.87 39.97,550.91V542.41Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M46.42,542.41C46.42,541.45 45.7,540.66 44.81,540.66C43.92,540.66 43.19,541.45 43.19,542.41V550.91C43.19,551.87 43.92,552.66 44.81,552.66C45.7,552.66 46.42,551.87 46.42,550.91V542.41Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M52.86,542.41C52.86,541.45 52.14,540.66 51.25,540.66C50.36,540.66 49.64,541.45 49.64,542.41V550.91C49.64,551.87 50.36,552.66 51.25,552.66C52.14,552.66 52.86,551.87 52.86,550.91V542.41Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M13.73,156.4V133.42"
|
||||
android:strokeWidth="1.3"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#A8AFA0"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M11.89,140.91C13.16,140.91 14.19,138.9 14.19,136.41C14.19,133.93 13.16,131.92 11.89,131.92C10.62,131.92 9.59,133.93 9.59,136.41C9.59,138.9 10.62,140.91 11.89,140.91Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M15.57,140.91C16.84,140.91 17.87,138.9 17.87,136.41C17.87,133.93 16.84,131.92 15.57,131.92C14.3,131.92 13.27,133.93 13.27,136.41C13.27,138.9 14.3,140.91 15.57,140.91Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M11.43,146.91C12.7,146.91 13.73,144.89 13.73,142.41C13.73,139.93 12.7,137.91 11.43,137.91C10.16,137.91 9.13,139.93 9.13,142.41C9.13,144.89 10.16,146.91 11.43,146.91Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M16.03,146.91C17.3,146.91 18.33,144.89 18.33,142.41C18.33,139.93 17.3,137.91 16.03,137.91C14.76,137.91 13.73,139.93 13.73,142.41C13.73,144.89 14.76,146.91 16.03,146.91Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M11.89,153.9C13.16,153.9 14.19,151.89 14.19,149.41C14.19,146.92 13.16,144.91 11.89,144.91C10.62,144.91 9.59,146.92 9.59,149.41C9.59,151.89 10.62,153.9 11.89,153.9Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M15.57,153.9C16.84,153.9 17.87,151.89 17.87,149.41C17.87,146.92 16.84,144.91 15.57,144.91C14.3,144.91 13.27,146.92 13.27,149.41C13.27,151.89 14.3,153.9 15.57,153.9Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M273.85,741.54H248.99C248.23,741.54 247.61,742.21 247.61,743.04V764.02C247.61,764.85 248.23,765.52 248.99,765.52H273.85C274.62,765.52 275.23,764.85 275.23,764.02V743.04C275.23,742.21 274.62,741.54 273.85,741.54Z"
|
||||
android:strokeWidth="1.4"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M244.85,741.54L261.42,726.55L278,741.54"
|
||||
android:strokeWidth="1.4"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M265.29,751.53H256.64C256.23,751.53 255.9,751.89 255.9,752.33V764.72C255.9,765.16 256.23,765.52 256.64,765.52H265.29C265.7,765.52 266.03,765.16 266.03,764.72V752.33C266.03,751.89 265.7,751.53 265.29,751.53Z"
|
||||
android:strokeWidth="1.1"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#A8AFA0"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M270.63,738.54C272.16,738.54 273.39,737.2 273.39,735.54C273.39,733.89 272.16,732.54 270.63,732.54C269.11,732.54 267.87,733.89 267.87,735.54C267.87,737.2 269.11,738.54 270.63,738.54Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M351.66,245.85C359.8,245.85 366.39,240.92 366.39,234.85C366.39,228.78 359.8,223.86 351.66,223.86C343.52,223.86 336.93,228.78 336.93,234.85C336.93,240.92 343.52,245.85 351.66,245.85Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M344.29,239.85C348.87,239.85 352.58,235.82 352.58,230.86C352.58,225.89 348.87,221.86 344.29,221.86C339.72,221.86 336.01,225.89 336.01,230.86C336.01,235.82 339.72,239.85 344.29,239.85Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M345.21,245.35C345.21,244.52 344.6,243.85 343.83,243.85C343.07,243.85 342.45,244.52 342.45,245.35V252.34C342.45,253.17 343.07,253.84 343.83,253.84C344.6,253.84 345.21,253.17 345.21,252.34V245.35Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M350.74,245.35C350.74,244.52 350.12,243.85 349.36,243.85C348.6,243.85 347.98,244.52 347.98,245.35V252.34C347.98,253.17 348.6,253.84 349.36,253.84C350.12,253.84 350.74,253.17 350.74,252.34V245.35Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M356.27,245.35C356.27,244.52 355.65,243.85 354.88,243.85C354.12,243.85 353.5,244.52 353.5,245.35V252.34C353.5,253.17 354.12,253.84 354.88,253.84C355.65,253.84 356.27,253.17 356.27,252.34V245.35Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M361.79,245.35C361.79,244.52 361.17,243.85 360.41,243.85C359.65,243.85 359.03,244.52 359.03,245.35V252.34C359.03,253.17 359.65,253.84 360.41,253.84C361.17,253.84 361.79,253.17 361.79,252.34V245.35Z"
|
||||
android:fillColor="#B5A89F"/>
|
||||
<path
|
||||
android:pathData="M32.14,799V780.01"
|
||||
android:strokeWidth="1.1"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#A8AFA0"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M30.76,786.51C31.78,786.51 32.61,784.94 32.61,783.01C32.61,781.08 31.78,779.51 30.76,779.51C29.75,779.51 28.92,781.08 28.92,783.01C28.92,784.94 29.75,786.51 30.76,786.51Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M33.53,786.51C34.54,786.51 35.37,784.94 35.37,783.01C35.37,781.08 34.54,779.51 33.53,779.51C32.51,779.51 31.68,781.08 31.68,783.01C31.68,784.94 32.51,786.51 33.53,786.51Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M30.3,791.51C31.32,791.51 32.14,789.94 32.14,788.01C32.14,786.08 31.32,784.51 30.3,784.51C29.29,784.51 28.46,786.08 28.46,788.01C28.46,789.94 29.29,791.51 30.3,791.51Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M33.99,791.51C35,791.51 35.83,789.94 35.83,788.01C35.83,786.08 35,784.51 33.99,784.51C32.97,784.51 32.14,786.08 32.14,788.01C32.14,789.94 32.97,791.51 33.99,791.51Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M30.76,796.5C31.78,796.5 32.61,794.94 32.61,793.01C32.61,791.07 31.78,789.51 30.76,789.51C29.75,789.51 28.92,791.07 28.92,793.01C28.92,794.94 29.75,796.5 30.76,796.5Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
<path
|
||||
android:pathData="M33.53,796.5C34.54,796.5 35.37,794.94 35.37,793.01C35.37,791.07 34.54,789.51 33.53,789.51C32.51,789.51 31.68,791.07 31.68,793.01C31.68,794.94 32.51,796.5 33.53,796.5Z"
|
||||
android:fillColor="#A8AFA0"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:pathData="M18.667,11.667C18.667,12.904 18.175,14.091 17.3,14.967C16.425,15.842 15.238,16.333 14,16.333C12.762,16.333 11.575,15.842 10.7,14.967C9.825,14.091 9.333,12.904 9.333,11.667"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.33333"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M3.62,7.04H24.38"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.33333"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M3.967,6.378C3.664,6.782 3.5,7.273 3.5,7.778V23.333C3.5,23.952 3.746,24.546 4.183,24.983C4.621,25.421 5.214,25.667 5.833,25.667H22.167C22.785,25.667 23.379,25.421 23.817,24.983C24.254,24.546 24.5,23.952 24.5,23.333V7.778C24.5,7.273 24.336,6.782 24.033,6.378L21.7,3.267C21.483,2.977 21.201,2.742 20.877,2.58C20.553,2.418 20.196,2.333 19.833,2.333H8.167C7.804,2.333 7.447,2.418 7.123,2.58C6.799,2.742 6.517,2.977 6.3,3.267L3.967,6.378Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.33333"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:pathData="M22.158,24.49L13.995,19.825L5.831,24.49V5.831C5.831,5.212 6.077,4.619 6.514,4.182C6.952,3.744 7.545,3.499 8.163,3.499H19.826C20.444,3.499 21.037,3.744 21.475,4.182C21.912,4.619 22.158,5.212 22.158,5.831V24.49Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.33241"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Reference in New Issue