Compare commits
20 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
eb7e19228b | |
|
|
3cd8a005c9 | |
|
|
c5750e6b5e | |
|
|
04d06cd16d | |
|
|
4d7e8e3daf | |
|
|
80cc72bb9d | |
|
|
1277ae09b6 | |
|
|
f8882c1dcc | |
|
|
feca34f892 | |
|
|
d46bc1f461 | |
|
|
88161f7933 | |
|
|
682ed78491 | |
|
|
95c6c598a0 | |
|
|
078bc02c36 | |
|
|
b60f5b9708 | |
|
|
3c8288007a | |
|
|
715b591b8a | |
|
|
6628daed50 | |
|
|
6b634f40b7 | |
|
|
4f8ae88820 |
|
|
@ -64,6 +64,7 @@ dependencies {
|
|||
|
||||
// AndroidX Security
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.androidx.room.runtime.android)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
|
@ -72,4 +73,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
|
||||
}
|
||||
|
||||
android.util.Log.d("AuthApiClient", "refreshTokens: Calling /auth/refresh endpoint")
|
||||
try {
|
||||
val response: RefreshResponse = client.post("${route}auth/refresh") {
|
||||
markAsRefreshTokenRequest()
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(RefreshRequest(refreshToken))
|
||||
}.body()
|
||||
|
||||
val response: RefreshResponse = client.post("http://10.0.2.2:3000/auth/refresh") {
|
||||
markAsRefreshTokenRequest()
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(RefreshRequest(refreshToken))
|
||||
}.body()
|
||||
android.util.Log.d("AuthApiClient", "refreshTokens: Refresh successful, saving new tokens")
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||
android.util.Log.d("AuthApiClient", "refreshTokens: New tokens saved successfully")
|
||||
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||
|
||||
BearerTokens(response.accessToken, response.refreshToken)
|
||||
BearerTokens(response.accessToken, response.refreshToken)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("AuthApiClient", "refreshTokens: Refresh failed: ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultRequest {
|
||||
url("http://10.0.2.2:3000/")
|
||||
url(route)
|
||||
}
|
||||
}
|
||||
|
||||
// --- API Calls ---
|
||||
|
||||
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> = runCatching {
|
||||
val response = client.post("auth/check-user") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CheckUserRequest(phoneNumber))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
// Success - parse as CheckUserResponse
|
||||
response.body<CheckUserResponse>()
|
||||
} else {
|
||||
// Error - parse as ErrorResponse
|
||||
val errorResponse = try {
|
||||
response.body<ErrorResponse>()
|
||||
} catch (e: Exception) {
|
||||
// If parsing fails, create default error
|
||||
ErrorResponse(
|
||||
success = false,
|
||||
error = "USER_NOT_FOUND",
|
||||
message = "User is not registered. Please sign up to create a new account.",
|
||||
userExists = false
|
||||
)
|
||||
}
|
||||
|
||||
throw UserNotFoundException(
|
||||
message = errorResponse.message ?: "User is not registered. Please sign up to create a new account.",
|
||||
errorCode = errorResponse.error ?: "USER_NOT_FOUND"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> = runCatching {
|
||||
client.post("auth/request-otp") {
|
||||
contentType(ContentType.Application.Json)
|
||||
|
|
@ -74,13 +129,38 @@ class AuthApiClient(private val context: Context) {
|
|||
}
|
||||
|
||||
suspend fun verifyOtp(phoneNumber: String, code: String, deviceId: String): Result<VerifyOtpResponse> = runCatching {
|
||||
val response: VerifyOtpResponse = client.post("auth/verify-otp") {
|
||||
val response = client.post("auth/verify-otp") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo()))
|
||||
}.body()
|
||||
setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo()))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val verifyResponse: VerifyOtpResponse = response.body()
|
||||
tokenManager.saveTokens(verifyResponse.accessToken, verifyResponse.refreshToken)
|
||||
verifyResponse
|
||||
} else {
|
||||
// Parse error response
|
||||
val errorResponse: ErrorResponse = response.body()
|
||||
throw UserNotFoundException(
|
||||
message = errorResponse.message ?: "User not found",
|
||||
errorCode = errorResponse.error ?: "USER_NOT_FOUND"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||
response
|
||||
suspend fun signup(request: SignupRequest): Result<SignupResponse> = runCatching {
|
||||
val response = client.post("auth/signup") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request.copy(deviceId = getDeviceId(), deviceInfo = getDeviceInfo()))
|
||||
}
|
||||
|
||||
// Instead of throwing an exception on non-2xx, we return a result type
|
||||
// that can be handled by the caller.
|
||||
if (response.status.isSuccess()) {
|
||||
response.body<SignupResponse>()
|
||||
} else {
|
||||
response.body<SignupResponse>()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProfile(name: String, userType: String): Result<User> = runCatching {
|
||||
|
|
@ -91,7 +171,40 @@ class AuthApiClient(private val context: Context) {
|
|||
}
|
||||
|
||||
suspend fun getUserDetails(): Result<UserDetails> = runCatching {
|
||||
client.get("users/me").body()
|
||||
android.util.Log.d("AuthApiClient", "getUserDetails: Calling /users/me endpoint")
|
||||
try {
|
||||
val response = client.get("users/me")
|
||||
android.util.Log.d("AuthApiClient", "getUserDetails: Response status=${response.status}")
|
||||
val userDetails = response.body<UserDetails>()
|
||||
android.util.Log.d("AuthApiClient", "getUserDetails: Success - user id=${userDetails.id}")
|
||||
userDetails
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("AuthApiClient", "getUserDetails: Error - ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshToken(): Result<RefreshResponse> = runCatching {
|
||||
android.util.Log.d("AuthApiClient", "refreshToken: Starting manual token refresh")
|
||||
val refreshToken = tokenManager.getRefreshToken()
|
||||
?: throw IllegalStateException("No refresh token found")
|
||||
|
||||
android.util.Log.d("AuthApiClient", "refreshToken: Calling /auth/refresh endpoint")
|
||||
try {
|
||||
val response: RefreshResponse = client.post("auth/refresh") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(RefreshRequest(refreshToken))
|
||||
}.body()
|
||||
|
||||
android.util.Log.d("AuthApiClient", "refreshToken: Refresh successful, saving new tokens")
|
||||
// Save the new tokens (refresh token rotates)
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
||||
android.util.Log.d("AuthApiClient", "refreshToken: New tokens saved successfully")
|
||||
response
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("AuthApiClient", "refreshToken: Refresh failed: ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout(): Result<LogoutResponse> = runCatching {
|
||||
|
|
@ -105,6 +218,55 @@ class AuthApiClient(private val context: Context) {
|
|||
response
|
||||
}
|
||||
|
||||
// Test API: Get user by ID from BuySellService (port 3200)
|
||||
suspend fun getUserById(userId: String, baseUrl: String = "http://10.0.2.2:3200"): Result<String> = runCatching {
|
||||
android.util.Log.d("AuthApiClient", "getUserById: Calling $baseUrl/users/$userId")
|
||||
|
||||
// Create a separate client for this external service call
|
||||
HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
val accessToken = tokenManager.getAccessToken()
|
||||
if (accessToken != null) {
|
||||
BearerTokens(accessToken, "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultRequest {
|
||||
url(baseUrl)
|
||||
}
|
||||
}.use { testClient ->
|
||||
val response = testClient.get("users/$userId")
|
||||
android.util.Log.d("AuthApiClient", "getUserById: Response status=${response.status}")
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
// Get raw JSON string
|
||||
val jsonString = response.bodyAsText()
|
||||
android.util.Log.d("AuthApiClient", "getUserById: Success - JSON=${jsonString.take(200)}...")
|
||||
jsonString
|
||||
} else {
|
||||
val errorText = try {
|
||||
response.bodyAsText()
|
||||
} catch (e: Exception) {
|
||||
"Error reading response body"
|
||||
}
|
||||
android.util.Log.e("AuthApiClient", "getUserById: Error - status=${response.status}, body=$errorText")
|
||||
throw Exception("API call failed: ${response.status} - $errorText")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceInfo(): DeviceInfo {
|
||||
return DeviceInfo(
|
||||
platform = "android",
|
||||
|
|
@ -115,4 +277,8 @@ class AuthApiClient(private val context: Context) {
|
|||
timezone = TimeZone.getDefault().id
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDeviceId(): String {
|
||||
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,290 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.example.livingai_lg.ui.screens.*
|
||||
import com.example.livingai_lg.ui.models.Animal
|
||||
import com.example.livingai_lg.ui.utils.formatDistance
|
||||
import com.example.livingai_lg.ui.utils.formatPrice
|
||||
import com.example.livingai_lg.ui.utils.formatViews
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
@Composable
|
||||
fun BuyAnimalCard(
|
||||
product: Animal,
|
||||
isSaved: Boolean,
|
||||
onSavedChange: (Boolean) -> Unit,
|
||||
onProductClick: () -> Unit,
|
||||
onSellerClick:(sellerId: String)-> Unit,
|
||||
onBookmarkClick: () -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.shadow(1.078.dp, RoundedCornerShape(14.dp))
|
||||
.background(Color.White, RoundedCornerShape(14.dp))
|
||||
.border(
|
||||
1.078.dp,
|
||||
Color(0xFF000000).copy(alpha = 0.1f),
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onProductClick
|
||||
)
|
||||
) {
|
||||
|
||||
// Image
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(257.dp)
|
||||
) {
|
||||
ImageCarousel(
|
||||
media = product.media ?: emptyList(),
|
||||
enableFullscreenPreview = false,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
// Views
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(6.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = RoundedCornerShape(50),
|
||||
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||
)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.35f), // 👈 light but effective
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.padding(horizontal = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Visibility,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp).shadow(
|
||||
elevation = 6.dp,
|
||||
shape = CircleShape,
|
||||
clip = false
|
||||
)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(formatViews(product.views), fontSize = AppTypography.Caption, color = Color.White,style = LocalTextStyle.current.copy(
|
||||
))
|
||||
}
|
||||
|
||||
// Distance
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(6.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = RoundedCornerShape(50),
|
||||
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||
)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.35f), // 👈 light but effective
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.padding(horizontal = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.LocationOn,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
formatDistance(product.distance),
|
||||
fontSize = AppTypography.Body,
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Row {
|
||||
Text(
|
||||
product.breed ?: "",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
Modifier.padding(horizontal = 4.dp).size(16.dp).align(Alignment.CenterVertically).clickable{onInfoClick()},
|
||||
|
||||
)
|
||||
|
||||
//InfoIconWithOverlay(infoText = product.breedInfo ?: "")
|
||||
}
|
||||
|
||||
Text(
|
||||
formatPrice(product.price),
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End, modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onSellerClick(product.sellerId ?: "")
|
||||
}) {
|
||||
Text("Sold By: ${product.sellerName}", fontSize = AppTypography.BodySmall)
|
||||
Text(product.sellerType ?: "???", fontSize = AppTypography.BodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
// Rating
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RatingStars(product.rating?:0f, starSize = 12.dp)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
"${product.rating} (${product.ratingCount} Ratings)",
|
||||
fontSize = AppTypography.Caption
|
||||
)
|
||||
}
|
||||
|
||||
// Badges
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
) {
|
||||
Row {
|
||||
Text("AI Score: ", fontSize = AppTypography.Caption)
|
||||
val scoreString = "${product.aiScore ?: 0}"
|
||||
Text(scoreString, fontSize = AppTypography.Caption, fontWeight = FontWeight.Bold)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (product.milkCapacity != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
"Milk Capacity:",
|
||||
fontSize = AppTypography.Caption
|
||||
)
|
||||
Text(
|
||||
"${product.milkCapacity}L",
|
||||
fontSize = AppTypography.Caption,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
Text(
|
||||
product.description ?: "",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
// Actions
|
||||
// Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// ActionButton(R.drawable.ic_chat, "Chat")
|
||||
// ActionButton(R.drawable.ic_phone, "Call")
|
||||
// ActionButton(R.drawable.ic_location, "Location")
|
||||
// ActionButton(R.drawable.ic_bookmark_plus, "Bookmark")
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
FloatingActionBar(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 12.dp)
|
||||
.zIndex(10f), // 👈 ensure it floats above everything
|
||||
showContainer = false,
|
||||
onChatClick = { /* TODO */ },
|
||||
onCallClick = { /* TODO */ },
|
||||
onLocationClick = { /* TODO */ },
|
||||
onBookmarkClick = onBookmarkClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(icon: Int, label: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = label,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DropdownInput(
|
||||
label: String? = null, // optional label
|
||||
selected: String,
|
||||
options: List<String>,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
onSelect: (String) -> Unit,
|
||||
placeholder: String = "Select", // NEW - custom placeholder
|
||||
textColor: Color = Color.Black,
|
||||
modifier: Modifier = Modifier // NEW - allows width control
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier, // <-- now caller can control width
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Optional label
|
||||
// if (label != null) {
|
||||
Text(
|
||||
text = label?:" ",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
// } else {
|
||||
// // Reserve label space so layout doesn’t shift
|
||||
// Spacer(modifier = Modifier.height(20.dp)) // ← same height as label text line
|
||||
// }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { onExpandedChange(!expanded) }
|
||||
) {
|
||||
// Anchor box
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onExpandedChange(true)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
// Custom placeholder support
|
||||
Text(
|
||||
text = selected.ifEmpty { placeholder },
|
||||
fontSize = AppTypography.Body,
|
||||
color = if (selected.isEmpty()) Color(0xFF99A1AF) else textColor
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = FarmTextDark
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Material3 Dropdown
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { onExpandedChange(false) },
|
||||
modifier = Modifier.background(Color.White)
|
||||
) {
|
||||
options.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(item, fontSize = AppTypography.Body, color = FarmTextDark)
|
||||
},
|
||||
onClick = {
|
||||
onSelect(item)
|
||||
onExpandedChange(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
|
||||
@Composable
|
||||
fun FarmHeader() {
|
||||
Row {
|
||||
Text(
|
||||
text = "Farm",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFE17100)
|
||||
)
|
||||
Text(
|
||||
text = "Market",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Find your perfect livestock",
|
||||
fontSize = 16.sp,
|
||||
color = FarmTextNormal
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun FilterButton(
|
||||
hasActiveFilters: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val backgroundColor =
|
||||
if (hasActiveFilters) Color(0xFF0A0A0A) else Color.White
|
||||
|
||||
val contentColor =
|
||||
if (hasActiveFilters) Color.White else Color(0xFF0A0A0A)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.border(
|
||||
1.078.dp,
|
||||
if (hasActiveFilters) Color.Transparent
|
||||
else Color(0xFF000000).copy(alpha = 0.1f),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(backgroundColor, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
// Icon + dot indicator
|
||||
Row {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_filter),
|
||||
contentDescription = "Filter",
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
if (hasActiveFilters) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp)
|
||||
.size(6.dp)
|
||||
.background(Color.Red, RoundedCornerShape(50))
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Filter",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
|
||||
@Composable
|
||||
fun FilterOverlay(
|
||||
visible: Boolean,
|
||||
appliedFilters: FiltersState,
|
||||
onDismiss: () -> Unit,
|
||||
onSubmitClick: (filters: FiltersState) -> Unit = {},
|
||||
) {
|
||||
BackHandler(enabled = visible) { onDismiss() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Dimmed background
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
// Sliding panel
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInHorizontally(
|
||||
initialOffsetX = { it } // from right
|
||||
),
|
||||
exit = slideOutHorizontally(
|
||||
targetOffsetX = { it } // to right
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.CenterEnd)
|
||||
) {
|
||||
FilterScreen(
|
||||
appliedFilters,
|
||||
onBackClick = onDismiss,
|
||||
onCancelClick = onDismiss,
|
||||
onSubmitClick = { filters ->
|
||||
onSubmitClick(filters)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import android.media.Image
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Chat
|
||||
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||
import androidx.compose.material.icons.outlined.Chat
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun FloatingActionBar(
|
||||
modifier: Modifier = Modifier,
|
||||
showContainer: Boolean = true,
|
||||
onChatClick: () -> Unit = {},
|
||||
onCallClick: () -> Unit = {},
|
||||
onLocationClick: () -> Unit = {},
|
||||
onBookmarkClick: () -> Unit = {}
|
||||
) {
|
||||
val containerModifier =
|
||||
if (showContainer) {
|
||||
Modifier
|
||||
.shadow(
|
||||
elevation = 12.dp,
|
||||
shape = RoundedCornerShape(50),
|
||||
clip = false
|
||||
)
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
} else {
|
||||
Modifier // no background, no shadow
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
//.padding(horizontal = 24.dp)
|
||||
.then(containerModifier)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FloatingActionIcon(Icons.AutoMirrored.Outlined.Chat, onChatClick)
|
||||
FloatingActionIcon(Icons.Outlined.Phone, onCallClick)
|
||||
FloatingActionIcon(Icons.Outlined.LocationOn, onLocationClick)
|
||||
FloatingActionIcon(Icons.Outlined.BookmarkAdd, onBookmarkClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FloatingActionIcon(
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
clip = false
|
||||
)
|
||||
.background(Color.White, RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun IconCircle(
|
||||
backgroundColor: Color,
|
||||
size: Dp,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.shadow(
|
||||
elevation = 4.dp,
|
||||
shape = CircleShape
|
||||
)
|
||||
.background(backgroundColor, shape = CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.MediaItem
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ImageCarousel(
|
||||
media: List<MediaItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
enableFullscreenPreview: Boolean = false,
|
||||
onMediaClick: (startIndex: Int) -> Unit = {},
|
||||
) {
|
||||
val pagerState = rememberPagerState { media.size }
|
||||
var startIndex by remember { mutableStateOf(0) }
|
||||
|
||||
when {
|
||||
media.isEmpty() -> {
|
||||
NoImagePlaceholder(modifier = modifier)
|
||||
}
|
||||
|
||||
media.size == 1 -> {
|
||||
when (val item = media.first()) {
|
||||
is MediaItem.Image -> {
|
||||
AsyncImage(
|
||||
model = item.url,
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(0)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
is MediaItem.Video -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(0)
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = item.thumbnailUrl ?: item.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play video",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.align(Alignment.Center)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
CircleShape
|
||||
)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
Box(modifier = modifier) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) { page ->
|
||||
when (val item = media[page]) {
|
||||
is MediaItem.Image -> {
|
||||
AsyncImage(
|
||||
model = item.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(page)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
is MediaItem.Video -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(page)
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = item.thumbnailUrl ?: item.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
// Play icon overlay
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play video",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.align(Alignment.Center)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
CircleShape
|
||||
)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page Indicator (inside image)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
repeat(pagerState.pageCount) { index ->
|
||||
val isSelected = pagerState.currentPage == index
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(6.dp)
|
||||
.width(if (isSelected) 18.dp else 6.dp)
|
||||
.background(
|
||||
Color.White,
|
||||
RoundedCornerShape(50)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun PageIndicator(
|
||||
pageCount: Int,
|
||||
currentPage: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(pageCount) { index ->
|
||||
if (index == currentPage) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(18.dp)
|
||||
.height(6.dp)
|
||||
.background(Color.White, RoundedCornerShape(3.dp))
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(6.dp)
|
||||
.background(Color.White.copy(alpha = 0.6f), CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoImagePlaceholder(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.LightGray),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No Images",
|
||||
color = Color.DarkGray,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.example.livingai_lg.ui.screens.align
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
@Composable
|
||||
fun InfoOverlay(
|
||||
visible: Boolean,
|
||||
title: String,
|
||||
text: String,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
BackHandler(enabled = visible) { onDismiss() }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Dimmed background
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
// Sliding content
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically { it }, // from bottom
|
||||
exit = slideOutVertically { it },
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
) {
|
||||
InfoOverlayContent(
|
||||
title = title,
|
||||
text = text,
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun InfoOverlayContent(
|
||||
title: String,
|
||||
text: String,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Color.White,
|
||||
RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = AppTypography.Title,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = AppTypography.Body,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.zIndex
|
||||
|
||||
@Composable
|
||||
fun InfoTooltipOverlay(
|
||||
text: String,
|
||||
visible: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
if (!visible) return
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(100f) // ensure it overlays everything
|
||||
) {
|
||||
// Dimmed background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
|
||||
// Tooltip card
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(Color.Black, RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.White,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoIconWithOverlay(
|
||||
infoText: String
|
||||
) {
|
||||
var showInfo by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "Info",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clickable { showInfo = true }
|
||||
)
|
||||
|
||||
InfoTooltipOverlay(
|
||||
text = infoText,
|
||||
visible = showInfo,
|
||||
onDismiss = { showInfo = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
|
||||
@Composable
|
||||
fun InputField(
|
||||
label: String,
|
||||
value: String,
|
||||
placeholder: String,
|
||||
onChange: (String) -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
color = FarmTextDark
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { inner ->
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFF99A1AF)
|
||||
)
|
||||
}
|
||||
inner()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.models.MediaItem
|
||||
import androidx.media3.common.MediaItem as ExoMediaItem
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MediaFullscreenOverlay(
|
||||
media: List<MediaItem>,
|
||||
startIndex: Int,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = startIndex,
|
||||
pageCount = { media.size }
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(0.85f))
|
||||
.zIndex(100f)
|
||||
) {
|
||||
|
||||
var isZoomed by remember { mutableStateOf(false) }
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
userScrollEnabled = !isZoomed
|
||||
) { page ->
|
||||
when (val item = media[page]) {
|
||||
is MediaItem.Image -> {
|
||||
ZoomableImage(
|
||||
url = item.url,
|
||||
onZoomChanged = { isZoomed = it }
|
||||
)
|
||||
}
|
||||
is MediaItem.Video -> {
|
||||
VideoPlayer(item.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page Indicator (inside image)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 36.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
repeat(pagerState.pageCount) { index ->
|
||||
val isSelected = pagerState.currentPage == index
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(6.dp)
|
||||
.width(if (isSelected) 18.dp else 6.dp)
|
||||
.background(
|
||||
Color.White,
|
||||
RoundedCornerShape(50)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close button
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(16.dp)
|
||||
.size(32.dp)
|
||||
.clickable { onClose() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZoomableImage(
|
||||
url: String,
|
||||
onZoomChanged: (Boolean) -> Unit
|
||||
) {
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var offsetX by remember { mutableStateOf(0f) }
|
||||
var offsetY by remember { mutableStateOf(0f) }
|
||||
|
||||
val transformState = rememberTransformableState { zoomChange, panChange, _ ->
|
||||
val newScale = (scale * zoomChange).coerceIn(1f, 4f)
|
||||
scale = newScale
|
||||
|
||||
if (newScale > 1f) {
|
||||
offsetX += panChange.x
|
||||
offsetY += panChange.y
|
||||
} else {
|
||||
offsetX = 0f
|
||||
offsetY = 0f
|
||||
}
|
||||
|
||||
onZoomChanged(newScale > 1f)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
translationX = offsetX
|
||||
translationY = offsetY
|
||||
}
|
||||
// 👇 double tap handler
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onDoubleTap = {
|
||||
if (scale > 1f) {
|
||||
scale = 1f
|
||||
offsetX = 0f
|
||||
offsetY = 0f
|
||||
onZoomChanged(false)
|
||||
} else {
|
||||
scale = 2.5f
|
||||
onZoomChanged(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// 👇 only consume gestures when zoomed
|
||||
.then(
|
||||
if (scale > 1f) Modifier.transformable(transformState)
|
||||
else Modifier
|
||||
),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun VideoPlayer(url: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItem(ExoMediaItem.fromUri(url))
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
exoPlayer.release()
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = {
|
||||
PlayerView(context).apply {
|
||||
player = exoPlayer
|
||||
useController = true
|
||||
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING)
|
||||
|
||||
// 👇 Enable fullscreen button if available
|
||||
setFullscreenButtonClickListener { isFullscreen ->
|
||||
// PlayerView handles system UI automatically
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.outlined.CameraAlt
|
||||
import androidx.compose.material.icons.outlined.Videocam
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.models.MediaType
|
||||
import com.example.livingai_lg.ui.models.MediaUpload
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.example.livingai_lg.ui.utils.createMediaUri
|
||||
import com.example.livingai_lg.ui.utils.getVideoThumbnail
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MediaPickerCard(
|
||||
upload: MediaUpload,
|
||||
modifier: Modifier = Modifier,
|
||||
onUriSelected: (Uri?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showSheet by remember { mutableStateOf(false) }
|
||||
var cameraUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var showVideoPlayer by remember { mutableStateOf(false) }
|
||||
|
||||
// Gallery
|
||||
val galleryLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri ->
|
||||
if (uri != null) onUriSelected(uri)
|
||||
}
|
||||
|
||||
// Camera (photo)
|
||||
val imageCameraLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.TakePicture()
|
||||
) { success ->
|
||||
if (success) onUriSelected(cameraUri) else cameraUri = null
|
||||
}
|
||||
|
||||
// Camera (video)
|
||||
val videoCameraLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CaptureVideo()
|
||||
) { success ->
|
||||
if (success) onUriSelected(cameraUri) else cameraUri = null
|
||||
}
|
||||
|
||||
/* ---------- Picker Sheet (NO delete here anymore) ---------- */
|
||||
|
||||
if (showSheet) {
|
||||
ModalBottomSheet(onDismissRequest = { showSheet = false }) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Select Media",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text("Camera") },
|
||||
leadingContent = { Icon(Icons.Outlined.CameraAlt, null) },
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
showSheet = false
|
||||
cameraUri = createMediaUri(context, upload.type)
|
||||
|
||||
if (upload.type == MediaType.PHOTO) {
|
||||
imageCameraLauncher.launch(cameraUri!!)
|
||||
} else {
|
||||
videoCameraLauncher.launch(cameraUri!!)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text("Gallery") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
if (upload.type == MediaType.PHOTO)
|
||||
Icons.Outlined.CameraAlt
|
||||
else
|
||||
Icons.Outlined.Videocam,
|
||||
null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
showSheet = false
|
||||
galleryLauncher.launch(
|
||||
if (upload.type == MediaType.PHOTO) "image/*" else "video/*"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Card ---------- */
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(204.dp)
|
||||
.shadow(0.5.dp, RoundedCornerShape(8.dp))
|
||||
.background(Color.White, RoundedCornerShape(8.dp))
|
||||
.border(1.dp, Color(0x1A000000), RoundedCornerShape(8.dp))
|
||||
.combinedClickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
if (upload.uri != null && upload.type == MediaType.VIDEO) {
|
||||
showVideoPlayer = true
|
||||
} else {
|
||||
showSheet = true
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (upload.uri != null) {
|
||||
showSheet = true
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
/* ---------- Media Preview ---------- */
|
||||
|
||||
if (upload.uri != null) {
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
if (upload.type == MediaType.VIDEO) {
|
||||
|
||||
val thumbnail = remember(upload.uri) {
|
||||
getVideoThumbnail(context, upload.uri!!)
|
||||
}
|
||||
|
||||
if (thumbnail != null) {
|
||||
Image(
|
||||
bitmap = thumbnail.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
// ▶ Play overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(56.dp)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
RoundedCornerShape(28.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play Video",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Photo
|
||||
AsyncImage(
|
||||
model = upload.uri,
|
||||
contentDescription = upload.label,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- ❌ Remove Button ---------- */
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove media",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(6.dp)
|
||||
.size(24.dp)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
RoundedCornerShape(12.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onUriSelected(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
/* ---------- Empty State ---------- */
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (upload.type == MediaType.PHOTO)
|
||||
Icons.Outlined.CameraAlt
|
||||
else
|
||||
Icons.Outlined.Videocam,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF717182),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (upload.type == MediaType.PHOTO) "Add Photo" else "Add Video",
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
|
||||
if (upload.label.isNotEmpty()) {
|
||||
Text(upload.label, color = Color(0xFF717182))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Video Player ---------- */
|
||||
|
||||
if (showVideoPlayer && upload.uri != null) {
|
||||
VideoPlayerDialog(
|
||||
uri = upload.uri!!,
|
||||
onDismiss = { showVideoPlayer = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.example.livingai_lg.ui.models.AppNotification
|
||||
import com.example.livingai_lg.ui.screens.NotificationsScreen
|
||||
|
||||
@Composable
|
||||
fun NotificationsOverlay(
|
||||
visible: Boolean,
|
||||
notifications: List<AppNotification>,
|
||||
onClose: () -> Unit,
|
||||
onDismiss: (String) -> Unit,
|
||||
onNotificationClick: (String) -> Unit = {}
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically { -it },
|
||||
exit = slideOutVertically { -it }
|
||||
) {
|
||||
NotificationsScreen(
|
||||
notifications = notifications,
|
||||
onBackClick = onClose,
|
||||
onDismiss = onDismiss,
|
||||
onNotificationClick = onNotificationClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
@Composable
|
||||
fun OptionCard(
|
||||
label: String,
|
||||
icon: Any,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color = Color.Unspecified,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(90.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0xFFF3F4F6), RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Icon container
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.background(iconBackgroundColor, RoundedCornerShape(14.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (icon) {
|
||||
is ImageVector -> {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = iconTint
|
||||
)
|
||||
}
|
||||
is Int -> {
|
||||
Icon(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = label,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Dot indicator
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.background(Color(0xFF6B7280), CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
|
||||
@Composable
|
||||
fun PhoneNumberInput(
|
||||
phone: String,
|
||||
onChange: (String) -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
||||
Text(
|
||||
text = "Enter Phone Number*",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.height(52.dp)
|
||||
) {
|
||||
|
||||
// Country code box
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(65.dp)
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("+91", fontSize = 16.sp, color = FarmTextDark)
|
||||
}
|
||||
|
||||
// Phone input field
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(52.dp)
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp))
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Phone,
|
||||
contentDescription = "Phone",
|
||||
tint = Color(0xFF99A1AF),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
|
||||
BasicTextField(
|
||||
value = phone,
|
||||
onValueChange = onChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
textStyle = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = FarmTextDark
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { inner ->
|
||||
if (phone.isEmpty()) {
|
||||
Text(
|
||||
"Enter your Phone Number",
|
||||
fontSize = 15.sp,
|
||||
color = Color(0xFF99A1AF)
|
||||
)
|
||||
}
|
||||
inner()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun RangeFilter(
|
||||
modifier: Modifier = Modifier,
|
||||
|
||||
label: String,
|
||||
min: Int,
|
||||
max: Int,
|
||||
valueFrom: Int,
|
||||
valueTo: Int,
|
||||
modified: Boolean = false,
|
||||
onValueChange: (from: Int, to: Int) -> Unit,
|
||||
showSlider: Boolean = true,
|
||||
valueFormatter: (Int) -> String = { it.toString() }
|
||||
) {
|
||||
var fromValue by remember(valueFrom) { mutableStateOf(valueFrom) }
|
||||
var toValue by remember(valueTo) { mutableStateOf(valueTo) }
|
||||
|
||||
Column(modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
|
||||
// Label
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF364153)
|
||||
)
|
||||
|
||||
// Slider (optional)
|
||||
if (showSlider) {
|
||||
RangeSlider(
|
||||
value = fromValue.toFloat()..toValue.toFloat(),
|
||||
onValueChange = { range ->
|
||||
fromValue = range.start.roundToInt()
|
||||
.coerceIn(min, toValue)
|
||||
toValue = range.endInclusive.roundToInt()
|
||||
.coerceIn(fromValue, max)
|
||||
|
||||
onValueChange(fromValue, toValue)
|
||||
},
|
||||
valueRange = min.toFloat()..max.toFloat(),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = Color(0xFFD9D9D9),
|
||||
activeTrackColor = Color(0xFFD9D9D9),
|
||||
inactiveTrackColor = Color(0xFFE5E7EB)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Pills
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RangePill(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = fromValue,
|
||||
modified = modified,
|
||||
onValueChange = { newFrom ->
|
||||
val safeFrom = newFrom.coerceIn(min, toValue)
|
||||
fromValue = safeFrom
|
||||
onValueChange(safeFrom, toValue)
|
||||
},
|
||||
formatter = valueFormatter
|
||||
)
|
||||
|
||||
Text("to", fontSize = 15.sp)
|
||||
|
||||
RangePill(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = toValue,
|
||||
modified = modified,
|
||||
onValueChange = { newTo ->
|
||||
val safeTo = newTo.coerceIn(fromValue, max)
|
||||
toValue = safeTo
|
||||
onValueChange(fromValue, safeTo)
|
||||
},
|
||||
formatter = valueFormatter
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RangePill(
|
||||
modifier: Modifier = Modifier,
|
||||
value: Int,
|
||||
modified: Boolean = false,
|
||||
onValueChange: (Int) -> Unit,
|
||||
formatter: (Int) -> String
|
||||
) {
|
||||
var text by remember(value) {
|
||||
mutableStateOf(formatter(value))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(30.dp)
|
||||
.background(Color.White, RoundedCornerShape(16.dp))
|
||||
.border(1.dp, Color(0x12000000), RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
var modified = modified;
|
||||
BasicTextField(
|
||||
value = text,
|
||||
onValueChange = { input ->
|
||||
val digits = input.filter { it.isDigit() }
|
||||
text = digits
|
||||
digits.toIntOrNull()?.let(onValueChange)
|
||||
},
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = if(modified) Color.Black else Color(0xFF99A1AF)
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun RatingStars(
|
||||
rating: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
starSize: Dp = 14.dp,
|
||||
color: Color = Color(0xFFDE9A07)
|
||||
) {
|
||||
val fullStars = rating.toInt()
|
||||
val remainder = rating - fullStars
|
||||
|
||||
val showHalfStar = remainder in 0.25f..0.74f
|
||||
val extraFullStar = remainder >= 0.75f
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Full stars
|
||||
repeat(
|
||||
if (extraFullStar) fullStars + 1 else fullStars
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(starSize)
|
||||
)
|
||||
}
|
||||
|
||||
// Half star (only if not rounded up)
|
||||
if (showHalfStar) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.StarHalf,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(starSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun SortButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.border(
|
||||
1.078.dp,
|
||||
Color(0xFF000000).copy(alpha = 0.1f),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.White, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_sort),
|
||||
contentDescription = "Sort",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Sort by",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.SortDirection
|
||||
import com.example.livingai_lg.ui.models.SortField
|
||||
|
||||
@Composable
|
||||
fun SortItem(
|
||||
field: SortField,
|
||||
onToggle: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val icon = when (field.direction) {
|
||||
SortDirection.ASC -> Icons.Default.ArrowUpward
|
||||
SortDirection.DESC -> Icons.Default.ArrowDownward
|
||||
SortDirection.NONE -> Icons.Default.ArrowDropDown
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.background(Color.White, RoundedCornerShape(12.dp))
|
||||
.border(1.dp, Color(0x1A000000), RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onToggle() }
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = field.label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF364153)
|
||||
)
|
||||
|
||||
// Sort priority indicator
|
||||
field.order?.let {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(Color.Black, RoundedCornerShape(9.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = it.toString(),
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = if (field.direction == SortDirection.NONE)
|
||||
Color(0xFF9CA3AF)
|
||||
else
|
||||
Color.Black
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai_lg.ui.models.SortField
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
import com.example.livingai_lg.ui.screens.SortScreen
|
||||
|
||||
@Composable
|
||||
fun SortOverlay(
|
||||
visible: Boolean,
|
||||
onApplyClick: (selected: List<SortField>) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
BackHandler(enabled = visible) { onDismiss() }
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// Dim background
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onDismiss() }
|
||||
)
|
||||
}
|
||||
|
||||
// Slide-in panel from LEFT
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInHorizontally(
|
||||
initialOffsetX = { -it }
|
||||
),
|
||||
exit = slideOutHorizontally(
|
||||
targetOffsetX = { -it }
|
||||
),
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(0.85f)
|
||||
.fillMaxWidth(0.75f)
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = 24.dp,
|
||||
bottomEnd = 24.dp
|
||||
)
|
||||
)
|
||||
) {
|
||||
SortScreen(
|
||||
onApplyClick = { selected ->
|
||||
onApplyClick(selected)
|
||||
onDismiss()
|
||||
},
|
||||
onCancelClick = onDismiss,
|
||||
onBackClick = onDismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.models.UserAddress
|
||||
import com.example.livingai_lg.ui.models.UserProfile
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun UserLocationHeader(
|
||||
user: UserProfile,
|
||||
selectedAddressId: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenAddressOverlay: () -> Unit,
|
||||
onProfileClick: () -> Unit = {} // New callback for profile icon click
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.wrapContentWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val selectedAddress = user.addresses.find { it.id == selectedAddressId } ?: user.addresses.first()
|
||||
|
||||
// Profile image - make it clickable
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onProfileClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (user.profileImageUrl != null) {
|
||||
AsyncImage(
|
||||
model = user.profileImageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_profile),
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Address + arrow (click opens overlay)
|
||||
Column(
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onOpenAddressOverlay()
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = selectedAddress.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Black
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = selectedAddress.address,
|
||||
fontSize = 13.sp,
|
||||
color = Color.Black.copy(alpha = 0.7f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
|
||||
@Composable
|
||||
fun VideoPlayerDialog(
|
||||
uri: Uri,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val player = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItem(MediaItem.fromUri(uri))
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
player.release()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16 / 9f)
|
||||
.background(Color.Black)
|
||||
) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
PlayerView(it).apply {
|
||||
this.player = player
|
||||
useController = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.example.livingai_lg.ui.components.backgrounds
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import kotlin.math.min
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
|
||||
@Composable
|
||||
fun DecorativeBackground() {
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val screenW = maxWidth.value
|
||||
val screenH = maxHeight.value
|
||||
|
||||
// Original bg image design size (YOUR asset’s intended size)
|
||||
val designW = 393f
|
||||
val designH = 852f
|
||||
|
||||
// Scale factor that preserves aspect ratio
|
||||
val scale = min(screenW / designW, screenH / designH)
|
||||
|
||||
//------------------------------
|
||||
// Helper to scale dp offsets
|
||||
//------------------------------
|
||||
fun s(value: Float) = (value * scale).dp
|
||||
|
||||
//------------------------------
|
||||
// Background Image
|
||||
//------------------------------
|
||||
Image(
|
||||
painter = painterResource(R.drawable.bg),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop // ensures full-screen coverage
|
||||
)
|
||||
|
||||
//------------------------------
|
||||
// Decorative Elements (scaled)
|
||||
//------------------------------
|
||||
|
||||
// 🐐 Goat
|
||||
ScaledEmoji(
|
||||
emoji = "🐐",
|
||||
baseFontSize = 48.sp,
|
||||
offsetX = 250f,
|
||||
offsetY = 160f,
|
||||
scale = scale,
|
||||
alpha = 0.10f
|
||||
)
|
||||
|
||||
// 🐄 Cow
|
||||
ScaledEmoji(
|
||||
emoji = "🐄",
|
||||
baseFontSize = 60.sp,
|
||||
offsetX = 64f,
|
||||
offsetY = 569f,
|
||||
scale = scale,
|
||||
alpha = 0.12f
|
||||
)
|
||||
|
||||
// 🌾 Wheat
|
||||
ScaledEmoji(
|
||||
emoji = "🌾🌾🌾",
|
||||
baseFontSize = 32.sp,
|
||||
offsetX = 48f,
|
||||
offsetY = 730f,
|
||||
scale = scale,
|
||||
alpha = 0.15f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScaledEmoji(
|
||||
emoji: String,
|
||||
baseFontSize: TextUnit,
|
||||
offsetX: Float,
|
||||
offsetY: Float,
|
||||
scale: Float,
|
||||
alpha: Float
|
||||
) {
|
||||
Text(
|
||||
text = emoji,
|
||||
fontSize = (baseFontSize.value * scale).sp,
|
||||
modifier = Modifier
|
||||
.offset(
|
||||
x = (offsetX * scale).dp,
|
||||
y = (offsetY * scale).dp
|
||||
)
|
||||
.alpha(alpha)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.example.livingai_lg.ui.components.backgrounds
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.min
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun StoreBackground() {
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
val screenW = maxWidth.value
|
||||
val screenH = maxHeight.value
|
||||
|
||||
// Original bg image design size (YOUR asset’s intended size)
|
||||
val designW = 393f
|
||||
val designH = 852f
|
||||
|
||||
// Scale factor that preserves aspect ratio
|
||||
val scale = min(screenW / designW, screenH / designH)
|
||||
|
||||
//------------------------------
|
||||
// Helper to scale dp offsets
|
||||
//------------------------------
|
||||
fun s(value: Float) = (value * scale).dp
|
||||
|
||||
//------------------------------
|
||||
// Background Image
|
||||
//------------------------------
|
||||
Image(
|
||||
painter = painterResource(R.drawable.bg_shop),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop // ensures full-screen coverage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.Alignment
|
||||
|
||||
|
||||
@Composable
|
||||
fun WishlistNameOverlay(
|
||||
onSave: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically { -it },
|
||||
exit = slideOutVertically { -it }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = 20.dp,
|
||||
bottomEnd = 20.dp
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Save Filters",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
placeholder = { Text("Wishlist name") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
onSave(name)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.example.livingai_lg.ui.layout
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.example.livingai_lg.ui.components.BottomNavigationBar
|
||||
import com.example.livingai_lg.ui.models.BottomNavItemData
|
||||
|
||||
@Composable
|
||||
fun BottomNavScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
items: List<BottomNavItemData>,
|
||||
currentItem: String,
|
||||
onBottomNavItemClick: (route: String) -> Unit = {},
|
||||
content: @Composable (paddingValues: PaddingValues) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
bottomBar = {
|
||||
BottomNavigationBar(
|
||||
modifier = Modifier
|
||||
.zIndex(1f)
|
||||
.shadow(8.dp),
|
||||
items,
|
||||
currentItem,
|
||||
onItemClick = onBottomNavItemClick
|
||||
)
|
||||
},
|
||||
containerColor = Color.Transparent
|
||||
) { paddingValues ->
|
||||
content(paddingValues)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.example.livingai_lg.ui.login
|
||||
package com.example.livingai_lg.ui.login_legacy
|
||||
|
||||
import android.app.Application
|
||||
import android.widget.Toast
|
||||
import 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,8 +1,7 @@
|
|||
package com.example.livingai_lg.ui.login
|
||||
package com.example.livingai_lg.ui.login_legacy
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
|
|
@ -10,7 +9,6 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -40,11 +38,11 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||
)
|
||||
)
|
||||
// .background(
|
||||
// brush = Brush.linearGradient(
|
||||
// colors = listOf(LightCream, LighterCream, LightestGreen)
|
||||
// )
|
||||
// )
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -60,7 +58,7 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
|
|||
|
||||
TextField(
|
||||
value = otp.value,
|
||||
onValueChange = { if (it.length <= 6) otp.value = it },
|
||||
onValueChange = { if (it.length <= 6) otp.value = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
|
|
@ -117,7 +115,7 @@ fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
|
|||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun OtpScreenPreview() {
|
||||
LivingAi_LgTheme {
|
||||
FarmMarketplaceTheme() {
|
||||
OtpScreen(rememberNavController(), "+919876543210", "John Doe")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,91 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
data class Animal(
|
||||
val id: String,
|
||||
val name: String? = null,
|
||||
val age: Int? = null,
|
||||
val breed: String? = null,
|
||||
val breedInfo: String? = null,
|
||||
val price: Long? = null,
|
||||
val isFairPrice: Boolean? = null,
|
||||
val media: List<MediaItem>? = null,
|
||||
val location: String? = null,
|
||||
val displayLocation: String? = null,
|
||||
val distance: Long? = null,
|
||||
val views: Long? = null,
|
||||
val sellerId: String? = null,
|
||||
val sellerName: String? = null,
|
||||
val sellerType: String? = null,
|
||||
val aiScore: Float? = null,
|
||||
val rating: Float? = null,
|
||||
val ratingCount: Int? = null,
|
||||
val description: String? = null,
|
||||
val milkCapacity: Float? = null,
|
||||
)
|
||||
|
||||
val sampleAnimals = listOf(
|
||||
Animal(
|
||||
id = "1",
|
||||
name = "Rita",
|
||||
age = 45,
|
||||
breed = "Gir",
|
||||
breedInfo = "The best in India",
|
||||
location = "Punjab",
|
||||
distance = 12000,
|
||||
media = listOf(MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2F4.bp.blogspot.com%2F_tecSnxaePMo%2FTLLVknW8dOI%2FAAAAAAAAACo%2F_kd1ZNBXU1o%2Fs1600%2FGIR%2CGujrat.jpg&f=1&nofb=1&ipt=da6ba1d040c396b64d3f08cc99998f66200dcd6c001e4a56def143ab3d1a87ea"),MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcpimg.tistatic.com%2F4478702%2Fb%2F4%2Fgir-cow.jpg&f=1&nofb=1&ipt=19bf391461480585c786d01433d863a383c60048ac2ce063ce91f173e215205d"),
|
||||
//MediaItem.Video("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
|
||||
),
|
||||
views = 9001,
|
||||
aiScore = 0.80f,
|
||||
price = 120000,
|
||||
isFairPrice = true,
|
||||
rating = 4.7f,
|
||||
ratingCount = 2076,
|
||||
sellerId = "1",
|
||||
sellerName = "Seller 1",
|
||||
description = "Premium Gir dairy cow in excellent health condition. High-yielding milk producer with consistent output and gentle temperament, making her ideal for commercial dairy operations or family farms.",
|
||||
displayLocation = "Mediatek Pashu Mela, Marathalli, Bangalore (13 km)",
|
||||
milkCapacity = 3.2f
|
||||
),
|
||||
Animal(
|
||||
id = "2",
|
||||
name = "Sahi",
|
||||
price = 95000,
|
||||
age = 35,
|
||||
breed = "Sahiwal",
|
||||
breedInfo = "The 2nd best in India",
|
||||
location = "Punjab",
|
||||
isFairPrice = true,
|
||||
media = listOf(MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdnbbsr.s3waas.gov.in%2Fs3a5a61717dddc3501cfdf7a4e22d7dbaa%2Fuploads%2F2020%2F09%2F2020091812-1024x680.jpg&f=1&nofb=1&ipt=bb426406b3747e54151e4812472e203f33922fa3b4e11c4feef9aa59a5733146")),
|
||||
distance = 0L,
|
||||
views = 100,
|
||||
sellerId = "1",
|
||||
sellerName = "Seller ???",
|
||||
sellerType = "Wholeseller",
|
||||
rating = 5f,
|
||||
ratingCount = 2076,
|
||||
description = "Friendly and energetic companion looking for an active family.",
|
||||
milkCapacity = 2.5f
|
||||
),
|
||||
Animal(
|
||||
id = "3",
|
||||
name = "Bessie",
|
||||
age = 48,
|
||||
breed = "Holstein Friesian",
|
||||
breedInfo = "Not Indian",
|
||||
location = "Punjab",
|
||||
distance = 12000,
|
||||
media = listOf(MediaItem.Image("https://api.builder.io/api/v1/image/assets/TEMP/885e24e34ede6a39f708df13dabc4c1683c3e976?width=786")),
|
||||
views = 94,
|
||||
aiScore = 0.80f,
|
||||
price = 80000,
|
||||
isFairPrice = true,
|
||||
rating = 4.5f,
|
||||
ratingCount = 2076,
|
||||
sellerId = "1",
|
||||
sellerName = "Seller 1",
|
||||
description = "Premium Holstein Friesian dairy cow in excellent health condition. Bessie is a high-yielding milk producer with consistent output and gentle temperament, making her ideal for commercial dairy operations or family farms.",
|
||||
displayLocation = "Mediatek Pashu Mela, Marathalli, Bangalore (13 km)",
|
||||
milkCapacity = 2.3f
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
data class AnimalType(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val emoji: String
|
||||
)
|
||||
|
||||
val animalTypes = listOf(
|
||||
AnimalType("cows", "Cows", "🐄"),
|
||||
AnimalType("buffalo", "Buffalo", "🐃"),
|
||||
AnimalType("goat", "Goat", "🐐"),
|
||||
AnimalType("bull", "Bull", "🐂"),
|
||||
AnimalType("baby_cow", "Baby Cow", "🐮"),
|
||||
AnimalType("dog", "Dog", "🐕"),
|
||||
AnimalType("cat", "Cat", "🐱"),
|
||||
AnimalType("others", "Others", "🦜")
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Build
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
|
||||
data class BottomNavItemData(
|
||||
val label: String,
|
||||
val iconRes: Any,
|
||||
val route: String,
|
||||
)
|
||||
|
||||
val mainBottomNavItems = listOf(
|
||||
BottomNavItemData("Home", R.drawable.ic_home , AppScreen.BUY_ANIMALS),
|
||||
BottomNavItemData("Sell", R.drawable.ic_tag, AppScreen.CREATE_ANIMAL_LISTING),
|
||||
// TODO:
|
||||
BottomNavItemData("Chats", R.drawable.ic_chat, AppScreen.CHATS),
|
||||
BottomNavItemData("Services", R.drawable.ic_config, AppScreen.CREATE_PROFILE),
|
||||
BottomNavItemData("Mandi", R.drawable.ic_shop2, AppScreen.CREATE_PROFILE)
|
||||
)
|
||||
|
||||
val chatBottomNavItems = listOf(
|
||||
BottomNavItemData("Contacts", R.drawable.ic_home ,AppScreen.CONTACTS),
|
||||
BottomNavItemData("Calls", R.drawable.ic_tag, AppScreen.CALLS),
|
||||
BottomNavItemData("Chats", R.drawable.ic_chat, AppScreen.CHATS),
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
data class TextFilter(
|
||||
val value: String = "",
|
||||
val filterSet: Boolean = false
|
||||
)
|
||||
|
||||
data class RangeFilterState(
|
||||
val min: Int,
|
||||
val max: Int,
|
||||
val filterSet: Boolean = false
|
||||
)
|
||||
|
||||
data class FiltersState(
|
||||
val animal: TextFilter = TextFilter(),
|
||||
val breed: TextFilter = TextFilter(),
|
||||
val distance: TextFilter = TextFilter(),
|
||||
val gender: TextFilter = TextFilter(),
|
||||
|
||||
val price: RangeFilterState = RangeFilterState(0, 90_000),
|
||||
val age: RangeFilterState = RangeFilterState(0, 20),
|
||||
val weight: RangeFilterState = RangeFilterState(0, 9_000),
|
||||
val milkYield: RangeFilterState = RangeFilterState(0, 900),
|
||||
|
||||
val pregnancyStatuses: Set<String> = emptySet(),
|
||||
|
||||
val calving: RangeFilterState = RangeFilterState(0, 10)
|
||||
)
|
||||
|
||||
|
||||
fun FiltersState.isDefault(): Boolean {
|
||||
return animal.filterSet.not() &&
|
||||
breed.filterSet.not() &&
|
||||
distance.filterSet.not() &&
|
||||
gender.filterSet.not() &&
|
||||
price.filterSet.not() &&
|
||||
age.filterSet.not() &&
|
||||
weight.filterSet.not() &&
|
||||
milkYield.filterSet.not() &&
|
||||
pregnancyStatuses.isEmpty() &&
|
||||
calving.filterSet.not()
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
class NewListingFormState {
|
||||
|
||||
val name = mutableStateOf("")
|
||||
val animal = mutableStateOf("")
|
||||
val breed = mutableStateOf("")
|
||||
val age = mutableStateOf("")
|
||||
val milkYield = mutableStateOf("")
|
||||
val calvingNumber = mutableStateOf("")
|
||||
val reproductiveStatus = mutableStateOf("")
|
||||
val description = mutableStateOf("")
|
||||
|
||||
val animalExpanded = mutableStateOf(false)
|
||||
val breedExpanded = mutableStateOf(false)
|
||||
|
||||
val mediaUploads = mutableStateListOf(
|
||||
MediaUpload("left-view", "Left View", MediaType.PHOTO),
|
||||
MediaUpload("right-view", "Right View", MediaType.PHOTO),
|
||||
MediaUpload("left-angle", "Left Angle View", MediaType.PHOTO),
|
||||
MediaUpload("right-angle", "Right Angle View", MediaType.PHOTO),
|
||||
MediaUpload("angle", "Angle View", MediaType.PHOTO),
|
||||
MediaUpload("video", "", MediaType.VIDEO)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
|
||||
data class AppNotification(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
val sampleNotifications = listOf(
|
||||
AppNotification(
|
||||
id = "1",
|
||||
title = "Animal saved successfully",
|
||||
icon = Icons.Default.Bookmark
|
||||
),
|
||||
AppNotification(
|
||||
id = "2",
|
||||
title = "New message from Seller",
|
||||
icon = Icons.Default.Chat
|
||||
),
|
||||
AppNotification(
|
||||
id = "3",
|
||||
title = "Price dropped on an animal you viewed",
|
||||
icon = Icons.Default.Notifications
|
||||
),
|
||||
AppNotification(
|
||||
id = "4",
|
||||
title = "You received a new rating",
|
||||
icon = Icons.Default.Star
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
data class ProfileType(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: Any,
|
||||
val iconTint: Color = Color.Unspecified,
|
||||
val backgroundColor: Color,
|
||||
)
|
||||
|
||||
val profileTypes = listOf(
|
||||
ProfileType(
|
||||
id = "buyer_seller",
|
||||
title = "I'm a Buyer/Seller",
|
||||
icon = R.drawable.ic_shop,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF9D4EDD)
|
||||
),
|
||||
ProfileType(
|
||||
id = "wholesale_trader",
|
||||
title = "I'm a Wholesale Trader",
|
||||
icon = R.drawable.ic_bag,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF3A86FF)
|
||||
),
|
||||
ProfileType(
|
||||
id = "service_provider",
|
||||
title = "I'm a Service Provider",
|
||||
icon = R.drawable.ic_spanner,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFFFF5722)
|
||||
),
|
||||
ProfileType(
|
||||
id = "mandi_host",
|
||||
title = "I'm a Mandi Host",
|
||||
icon = R.drawable.ic_shop2,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF4CAF50)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
class Seller {
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
enum class SortDirection {
|
||||
NONE,
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
|
||||
data class SortField(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val direction: SortDirection = SortDirection.NONE,
|
||||
val order: Int? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
data class UserAddress(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val isPrimary: Boolean = false
|
||||
)
|
||||
|
||||
data class UserProfile(
|
||||
val name: String,
|
||||
val profileImageUrl: String? = null,
|
||||
val addresses: List<UserAddress>
|
||||
)
|
||||
|
||||
val userProfile =
|
||||
UserProfile(
|
||||
name = "John Doe",
|
||||
profileImageUrl = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fstatic.vecteezy.com%2Fsystem%2Fresources%2Fpreviews%2F014%2F016%2F808%2Fnon_2x%2Findian-farmer-face-vector.jpg&f=1&nofb=1&ipt=c352fec591428aebefe6cd263d2958765e85d4da69cce3c46b725ba2ff7d3448",
|
||||
addresses = listOf(
|
||||
UserAddress(
|
||||
id = "1",
|
||||
name = "Home",
|
||||
address = "205 1st floor 7th cross 27th main, PW",
|
||||
isPrimary = true
|
||||
),
|
||||
UserAddress(
|
||||
id = "2",
|
||||
name = "Farm",
|
||||
address = "2nd block, MG Farms"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
class MediaUpload(
|
||||
val id: String,
|
||||
val label: String,
|
||||
val type: MediaType,
|
||||
initialUri: Uri? = null
|
||||
) {
|
||||
var uri by mutableStateOf(initialUri)
|
||||
}
|
||||
|
||||
enum class MediaType {
|
||||
PHOTO, VIDEO
|
||||
}
|
||||
|
||||
sealed class MediaItem {
|
||||
data class Image(val url: String) : MediaItem()
|
||||
data class Video(val url: String, val thumbnailUrl: String? = null) : MediaItem()
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,505 @@
|
|||
package com.example.livingai_lg.ui.navigation
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.farmmarketplace.ui.screens.CallsScreen
|
||||
import com.example.farmmarketplace.ui.screens.ContactsScreen
|
||||
import com.example.livingai_lg.ui.AuthState
|
||||
import com.example.livingai_lg.ui.MainViewModel
|
||||
import com.example.livingai_lg.ui.screens.AccountsScreen
|
||||
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.BuyScreen
|
||||
import com.example.livingai_lg.ui.screens.ChooseServiceScreen
|
||||
import com.example.livingai_lg.ui.screens.CreateProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
import com.example.livingai_lg.ui.screens.NewListingScreen
|
||||
import com.example.livingai_lg.ui.screens.PostSaleSurveyScreen
|
||||
import com.example.livingai_lg.ui.screens.SaleArchiveScreen
|
||||
import com.example.livingai_lg.ui.screens.SavedListingsScreen
|
||||
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.SortScreen
|
||||
import com.example.livingai_lg.ui.screens.WishlistScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignUpScreen
|
||||
import com.example.livingai_lg.ui.screens.chat.ChatScreen
|
||||
import com.example.livingai_lg.ui.screens.chat.ChatsScreen
|
||||
import com.example.livingai_lg.ui.screens.testing.ApiTestScreen
|
||||
|
||||
fun NavController.navigateIfAuthenticated(
|
||||
authState: AuthState,
|
||||
targetRoute: String,
|
||||
fallbackRoute: String = AppScreen.LANDING
|
||||
) {
|
||||
if (authState is AuthState.Authenticated) {
|
||||
navigate(targetRoute) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
} else {
|
||||
navigate(fallbackRoute) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
authState: AuthState,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
/* ---------------------------------------------------
|
||||
* Collect one-time navigation events (OTP, login, logout)
|
||||
* --------------------------------------------------- */
|
||||
LaunchedEffect(Unit) {
|
||||
mainViewModel.navEvents.collect { event ->
|
||||
when (event) {
|
||||
|
||||
is NavEvent.ToCreateProfile -> {
|
||||
navController.navigate(
|
||||
AppScreen.createProfile(event.name)
|
||||
) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
is NavEvent.ToChooseService -> {
|
||||
navController.navigate(
|
||||
AppScreen.chooseService(event.profileId)
|
||||
) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
NavEvent.ToLanding -> {
|
||||
navController.navigate(AppScreen.LANDING) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onNavClick: (String) -> Unit = { route ->
|
||||
val currentRoute =
|
||||
navController.currentBackStackEntry?.destination?.route
|
||||
Log.d("Current Route:"," $currentRoute $route")
|
||||
|
||||
if (currentRoute != route) {
|
||||
navController.navigate(route) {
|
||||
launchSingleTop = true
|
||||
//restoreState = true
|
||||
// popUpTo(navController.graph.startDestinationId) {
|
||||
// saveState = true
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------
|
||||
* Single NavHost
|
||||
* --------------------------------------------------- */
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = if(authState is AuthState.Authenticated) AppScreen.BUY_ANIMALS else AppScreen.LANDING
|
||||
) {
|
||||
|
||||
/* ---------------- AUTH ---------------- */
|
||||
|
||||
composable(AppScreen.LANDING) {
|
||||
LandingScreen(
|
||||
onSignUpClick = {
|
||||
navController.navigate(AppScreen.SIGN_UP)
|
||||
},
|
||||
onSignInClick = {
|
||||
navController.navigate(AppScreen.SIGN_IN)
|
||||
},
|
||||
onGuestClick = {
|
||||
mainViewModel.emitNavEvent(
|
||||
NavEvent.ToCreateProfile("guest")
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SIGN_IN) {
|
||||
SignInScreen(
|
||||
onSignUpClick = {
|
||||
navController.navigate(AppScreen.SIGN_UP)
|
||||
},
|
||||
onSignInClick = { phone, name ->
|
||||
navController.navigate(
|
||||
AppScreen.otp(phone, name)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SIGN_UP) {
|
||||
SignUpScreen(
|
||||
onSignUpClick = { phone, name, state, district, village ->
|
||||
navController.navigate(
|
||||
AppScreen.otpWithSignup(
|
||||
phone, name, state, district, village
|
||||
)
|
||||
)
|
||||
},
|
||||
onSignInClick = {
|
||||
navController.navigate(AppScreen.SIGN_IN)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.OTP}/{phoneNumber}/{name}",
|
||||
arguments = listOf(
|
||||
navArgument("phoneNumber") { type = NavType.StringType },
|
||||
navArgument("name") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
OtpScreen(
|
||||
mainViewModel = mainViewModel,
|
||||
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
onCreateProfile = { name ->
|
||||
// navController.navigateIfAuthenticated(
|
||||
// authState,
|
||||
// AppScreen.createProfile(name),
|
||||
// AppScreen.LANDING
|
||||
// )
|
||||
mainViewModel.emitNavEvent(
|
||||
NavEvent.ToCreateProfile(name)
|
||||
)
|
||||
},
|
||||
onSuccess = {
|
||||
// navController.navigateIfAuthenticated(
|
||||
// authState,
|
||||
// AppScreen.chooseService(""),
|
||||
// AppScreen.LANDING
|
||||
// )
|
||||
mainViewModel.emitNavEvent(
|
||||
NavEvent.ToChooseService()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.OTP}/{phoneNumber}/{name}/{state}/{district}/{village}",
|
||||
arguments = listOf(
|
||||
navArgument("phoneNumber") { type = NavType.StringType },
|
||||
navArgument("name") { type = NavType.StringType },
|
||||
navArgument("state") { type = NavType.StringType },
|
||||
navArgument("district") { type = NavType.StringType },
|
||||
navArgument("village") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
OtpScreen(
|
||||
mainViewModel = mainViewModel,
|
||||
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
signupState = backStackEntry.arguments?.getString("state"),
|
||||
signupDistrict = backStackEntry.arguments?.getString("district"),
|
||||
signupVillage = backStackEntry.arguments?.getString("village"),
|
||||
onCreateProfile = { name ->
|
||||
mainViewModel.emitNavEvent(
|
||||
NavEvent.ToCreateProfile(name)
|
||||
)
|
||||
},
|
||||
onSuccess = {
|
||||
mainViewModel.emitNavEvent(
|
||||
NavEvent.ToChooseService()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------- MAIN ---------------- */
|
||||
|
||||
composable(
|
||||
"${AppScreen.CREATE_PROFILE}/{name}",
|
||||
arguments = listOf(
|
||||
navArgument("name") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
CreateProfileScreen(
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
onProfileSelected = { profileId ->
|
||||
mainViewModel.emitNavEvent(
|
||||
NavEvent.ToChooseService(profileId)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.CHOOSE_SERVICE}/{profileId}",
|
||||
arguments = listOf(
|
||||
navArgument("profileId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
ChooseServiceScreen(
|
||||
profileId = backStackEntry.arguments?.getString("profileId") ?: "",
|
||||
onServiceSelected = {
|
||||
navController.navigate(AppScreen.BUY_ANIMALS)
|
||||
// navController.navigateIfAuthenticated(
|
||||
// authState,
|
||||
// AppScreen.BUY_ANIMALS
|
||||
// )
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.BUY_ANIMALS) {
|
||||
BuyScreen(
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onProductClick = { animalId ->
|
||||
navController.navigate(
|
||||
AppScreen.animalProfile(animalId)
|
||||
)
|
||||
},
|
||||
onNavClick = onNavClick,
|
||||
onSellerClick = { sellerId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
onWishlistClick = {
|
||||
navController.navigate(AppScreen.WISHLIST)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.WISHLIST) {
|
||||
WishlistScreen(
|
||||
onApply = {
|
||||
navController.navigate(AppScreen.BUY_ANIMALS)
|
||||
},
|
||||
onBack = {navController.popBackStack()}
|
||||
)
|
||||
}
|
||||
|
||||
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||
// FilterScreen(
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
|
||||
// composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||
// SortScreen(
|
||||
// onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
//
|
||||
// )
|
||||
// }
|
||||
|
||||
composable(AppScreen.SAVED_LISTINGS) {
|
||||
SavedListingsScreen(
|
||||
onNavClick = onNavClick,
|
||||
onBackClick = { navController.popBackStack() })
|
||||
}
|
||||
|
||||
composable(AppScreen.ACCOUNTS) {
|
||||
AccountsScreen(
|
||||
onBackClick = { navController.popBackStack() },
|
||||
onLogout = {
|
||||
// Navigate to auth graph after logout
|
||||
navController.navigate(AppScreen.LANDING) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onApiTest = {
|
||||
navController.navigate(AppScreen.API_TEST)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||
NewListingScreen (
|
||||
onSaveClick = {navController.navigate(
|
||||
AppScreen.postSaleSurvey("2")
|
||||
)},
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.BUY_ANIMALS){
|
||||
popUpTo(AppScreen.BUY_ANIMALS){
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||
arguments = listOf(
|
||||
navArgument("saleId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val saleId = backStackEntry
|
||||
.arguments
|
||||
?.getString("saleId")
|
||||
?: return@composable
|
||||
|
||||
SaleArchiveScreen(
|
||||
saleId = saleId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onSaleSurveyClick = { saleId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(saleId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||
arguments = listOf(
|
||||
navArgument("animalId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val animalId = backStackEntry
|
||||
.arguments
|
||||
?.getString("animalId")
|
||||
?: return@composable
|
||||
|
||||
PostSaleSurveyScreen (
|
||||
animalId = animalId,
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.CREATE_ANIMAL_LISTING)
|
||||
},
|
||||
onSubmit = {navController.navigate(
|
||||
AppScreen.saleArchive("2")
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||
arguments = listOf(
|
||||
navArgument("animalId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val animalId = backStackEntry
|
||||
.arguments
|
||||
?.getString("animalId")
|
||||
?: return@composable
|
||||
|
||||
AnimalProfileScreen(
|
||||
animalId = animalId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavClick = onNavClick,
|
||||
onSellerClick = { sellerId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||
arguments = listOf(
|
||||
navArgument("sellerId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val sellerId = backStackEntry
|
||||
.arguments
|
||||
?.getString("sellerId")
|
||||
?: return@composable
|
||||
|
||||
SellerProfileScreen(
|
||||
sellerId = sellerId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CONTACTS) {
|
||||
ContactsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CALLS) {
|
||||
CallsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CHATS) {
|
||||
ChatsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
onChatItemClick = {navController.navigate(AppScreen.chats("2"))}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.CHAT}/{contact}",
|
||||
arguments = listOf(
|
||||
navArgument("contact") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val sellerId = backStackEntry
|
||||
.arguments
|
||||
?.getString("contact")
|
||||
?: return@composable
|
||||
|
||||
ChatScreen(
|
||||
sellerId,
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.CHATS)
|
||||
//navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
//A route for testing
|
||||
composable(AppScreen.API_TEST) {
|
||||
ApiTestScreen(
|
||||
onBackClick = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.example.livingai_lg.ui.navigation
|
||||
|
||||
object AppScreen {
|
||||
const val LANDING = "landing"
|
||||
const val SIGN_IN = "sign_in"
|
||||
const val SIGN_UP = "sign_up"
|
||||
|
||||
const val OTP = "otp"
|
||||
|
||||
fun otp(phone: String, name: String) =
|
||||
"$OTP/$phone/$name"
|
||||
|
||||
fun otpWithSignup(phone: String, name: String, state: String, district: String, village: String) =
|
||||
"$OTP/$phone/$name/$state/$district/$village"
|
||||
|
||||
const val CHOOSE_SERVICE = "choose_service"
|
||||
fun chooseService(profileId: String?) =
|
||||
"$CHOOSE_SERVICE/$profileId"
|
||||
|
||||
const val CREATE_PROFILE = "create_profile"
|
||||
|
||||
fun createProfile(name: String) =
|
||||
"$CREATE_PROFILE/$name"
|
||||
|
||||
const val BUY_ANIMALS = "buy_animals"
|
||||
const val ANIMAL_PROFILE = "animal_profile"
|
||||
fun animalProfile(animalId: String) =
|
||||
"$ANIMAL_PROFILE/$animalId"
|
||||
|
||||
const val CREATE_ANIMAL_LISTING = "create_animal_listing"
|
||||
|
||||
const val BUY_ANIMALS_FILTERS = "buy_animals_filters"
|
||||
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||
|
||||
const val WISHLIST = "wishlist"
|
||||
|
||||
const val SELLER_PROFILE = "seller_profile"
|
||||
fun sellerProfile(sellerId: String) =
|
||||
"$SELLER_PROFILE/$sellerId"
|
||||
|
||||
const val SALE_ARCHIVE = "sale_archive"
|
||||
fun saleArchive(saleId: String) =
|
||||
"$SALE_ARCHIVE/$saleId"
|
||||
|
||||
const val POST_SALE_SURVEY = "post_sale_survey"
|
||||
fun postSaleSurvey(animalId: String) =
|
||||
"$POST_SALE_SURVEY/$animalId"
|
||||
|
||||
const val SAVED_LISTINGS = "saved_listings"
|
||||
|
||||
const val CONTACTS = "contacts"
|
||||
|
||||
fun chats(contact: String) =
|
||||
"$CHAT/$contact"
|
||||
|
||||
const val CALLS = "calls"
|
||||
|
||||
const val CHAT = "chat"
|
||||
|
||||
const val CHATS = "chats"
|
||||
|
||||
const val ACCOUNTS = "accounts"
|
||||
|
||||
|
||||
|
||||
// Test screens::
|
||||
const val API_TEST = "api_test"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.livingai_lg.ui.navigation
|
||||
sealed class NavEvent {
|
||||
data class ToCreateProfile(val name: String) : NavEvent()
|
||||
data class ToChooseService(val profileId: String = "") : NavEvent()
|
||||
object ToLanding : NavEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.livingai_lg.ui.MainViewModel
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
// Note: CoroutineScope, Dispatchers, delay, launch still used in onCreateProfile callbacks
|
||||
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignUpScreen
|
||||
|
||||
fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: MainViewModel) {
|
||||
navigation(
|
||||
route = Graph.AUTH,
|
||||
startDestination = AppScreen.LANDING
|
||||
) {
|
||||
composable(AppScreen.LANDING) {
|
||||
LandingScreen(
|
||||
onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||
onSignInClick = { navController.navigate(AppScreen.SIGN_IN) },
|
||||
onGuestClick = { navController.navigate(Graph.main(AppScreen.createProfile("guest"))) }
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SIGN_IN) {
|
||||
SignInScreen(
|
||||
onSignUpClick = { navController.navigate(AppScreen.SIGN_UP){
|
||||
popUpTo(AppScreen.SIGN_IN) { inclusive = true }
|
||||
} },
|
||||
onSignInClick = { phone, name ->
|
||||
navController.navigate(AppScreen.otp(phone,name))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SIGN_UP) {
|
||||
SignUpScreen(
|
||||
onSignUpClick = { phone, name, state, district, village ->
|
||||
navController.navigate(AppScreen.otpWithSignup(phone, name, state, district, village))
|
||||
},
|
||||
onSignInClick = {
|
||||
navController.navigate(AppScreen.SIGN_IN) {
|
||||
popUpTo(AppScreen.SIGN_UP) { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.OTP}/{phoneNumber}/{name}",
|
||||
arguments = listOf(
|
||||
navArgument("phoneNumber") { type = NavType.StringType },
|
||||
navArgument("name") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
OtpScreen(
|
||||
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
mainViewModel = mainViewModel,
|
||||
onCreateProfile = {name ->
|
||||
Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
|
||||
// Navigate to main graph first without popping, then navigate to specific route
|
||||
try {
|
||||
// Navigate to main graph (this will use its start destination)
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Don't pop the AUTH graph yet - keep the graph structure
|
||||
launchSingleTop = true
|
||||
}
|
||||
// Then navigate to the specific route after a short delay
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(200)
|
||||
try {
|
||||
navController.navigate(AppScreen.createProfile(name)) {
|
||||
// Now pop the AUTH graph after we're in the MAIN graph
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onLanding = {
|
||||
Log.d("AuthNavGraph", "Navigating to landing page for new user")
|
||||
// Navigate to landing page within AUTH graph
|
||||
try {
|
||||
navController.navigate(AppScreen.LANDING) {
|
||||
// Pop all screens up to and including the current OTP screen
|
||||
popUpTo(Graph.AUTH) { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||
try {
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Clear back stack to prevent going back to auth screens
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.OTP}/{phoneNumber}/{name}/{state}/{district}/{village}",
|
||||
arguments = listOf(
|
||||
navArgument("phoneNumber") { type = NavType.StringType },
|
||||
navArgument("name") { type = NavType.StringType },
|
||||
navArgument("state") { type = NavType.StringType },
|
||||
navArgument("district") { type = NavType.StringType },
|
||||
navArgument("village") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
OtpScreen(
|
||||
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
signupState = backStackEntry.arguments?.getString("state"),
|
||||
signupDistrict = backStackEntry.arguments?.getString("district"),
|
||||
signupVillage = backStackEntry.arguments?.getString("village"),
|
||||
mainViewModel = mainViewModel,
|
||||
onCreateProfile = {name ->
|
||||
Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
|
||||
// Navigate to main graph first without popping, then navigate to specific route
|
||||
try {
|
||||
// Navigate to main graph (this will use its start destination)
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Don't pop the AUTH graph yet - keep the graph structure
|
||||
launchSingleTop = true
|
||||
}
|
||||
// Then navigate to the specific route after a short delay
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(200)
|
||||
try {
|
||||
navController.navigate(AppScreen.createProfile(name)) {
|
||||
// Now pop the AUTH graph after we're in the MAIN graph
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onLanding = {
|
||||
Log.d("AuthNavGraph", "Navigating to landing page for new user")
|
||||
// Navigate to landing page within AUTH graph
|
||||
try {
|
||||
navController.navigate(AppScreen.LANDING) {
|
||||
// Pop all screens up to and including the current OTP screen
|
||||
popUpTo(Graph.AUTH) { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
|
||||
}
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||
try {
|
||||
navController.navigate(Graph.MAIN) {
|
||||
// Clear back stack to prevent going back to auth screens
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.farmmarketplace.ui.screens.CallsScreen
|
||||
import com.example.farmmarketplace.ui.screens.ContactsScreen
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import com.example.livingai_lg.ui.screens.AccountsScreen
|
||||
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.BuyScreen
|
||||
import com.example.livingai_lg.ui.screens.ChooseServiceScreen
|
||||
import com.example.livingai_lg.ui.screens.CreateProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.FilterScreen
|
||||
import com.example.livingai_lg.ui.screens.NewListingScreen
|
||||
import com.example.livingai_lg.ui.screens.PostSaleSurveyScreen
|
||||
import com.example.livingai_lg.ui.screens.SaleArchiveScreen
|
||||
import com.example.livingai_lg.ui.screens.SavedListingsScreen
|
||||
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.SortScreen
|
||||
import com.example.livingai_lg.ui.screens.chat.ChatScreen
|
||||
import com.example.livingai_lg.ui.screens.chat.ChatsScreen
|
||||
|
||||
fun NavGraphBuilder.mainNavGraph(navController: NavController) {
|
||||
val onNavClick: (String) -> Unit = { route ->
|
||||
val currentRoute =
|
||||
navController.currentBackStackEntry?.destination?.route
|
||||
Log.d("Current Route:"," $currentRoute $route")
|
||||
|
||||
if (currentRoute != route) {
|
||||
navController.navigate(route) {
|
||||
launchSingleTop = true
|
||||
//restoreState = true
|
||||
// popUpTo(navController.graph.startDestinationId) {
|
||||
// saveState = true
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigation(
|
||||
route = Graph.MAIN,
|
||||
startDestination = AppScreen.chooseService("1") // Default to ChooseServiceScreen for authenticated users
|
||||
){
|
||||
|
||||
|
||||
// NavHost(
|
||||
// navController = navController,
|
||||
// startDestination = AppScreen.createProfile("guest")
|
||||
// ) {
|
||||
composable(
|
||||
"${AppScreen.CREATE_PROFILE}/{name}",
|
||||
arguments = listOf(navArgument("name") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
CreateProfileScreen(
|
||||
name = backStackEntry.arguments?.getString("name") ?: "",
|
||||
onProfileSelected = { profileId ->
|
||||
navController.navigate(AppScreen.chooseService(profileId))
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${AppScreen.CHOOSE_SERVICE}/{profileId}",
|
||||
arguments = listOf(navArgument("profileId") { type = NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
ChooseServiceScreen (
|
||||
profileId = backStackEntry.arguments?.getString("profileId") ?: "",
|
||||
onServiceSelected = { navController.navigate(AppScreen.BUY_ANIMALS) },
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.BUY_ANIMALS) {
|
||||
BuyScreen(
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onProductClick = { animalId ->
|
||||
navController.navigate(
|
||||
AppScreen.animalProfile(animalId)
|
||||
)
|
||||
},
|
||||
onNavClick = onNavClick,
|
||||
onSellerClick = { sellerId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||
// FilterScreen(
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
|
||||
composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||
SortScreen(
|
||||
onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onCancelClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.SAVED_LISTINGS) {
|
||||
SavedListingsScreen(
|
||||
onNavClick = onNavClick,
|
||||
onBackClick = { navController.popBackStack() })
|
||||
}
|
||||
|
||||
composable(AppScreen.ACCOUNTS) {
|
||||
AccountsScreen(
|
||||
onBackClick = { navController.popBackStack() },
|
||||
onLogout = {
|
||||
// Navigate to auth graph after logout
|
||||
navController.navigate(Graph.AUTH) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onApiTest = {navController.navigate(AppScreen.API_TEST)}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||
NewListingScreen (
|
||||
onSaveClick = {navController.navigate(
|
||||
AppScreen.postSaleSurvey("2")
|
||||
)},
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.BUY_ANIMALS){
|
||||
popUpTo(AppScreen.BUY_ANIMALS){
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||
arguments = listOf(
|
||||
navArgument("saleId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val saleId = backStackEntry
|
||||
.arguments
|
||||
?.getString("saleId")
|
||||
?: return@composable
|
||||
|
||||
SaleArchiveScreen(
|
||||
saleId = saleId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onSaleSurveyClick = { saleId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(saleId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||
arguments = listOf(
|
||||
navArgument("animalId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val animalId = backStackEntry
|
||||
.arguments
|
||||
?.getString("animalId")
|
||||
?: return@composable
|
||||
|
||||
PostSaleSurveyScreen (
|
||||
animalId = animalId,
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.CREATE_ANIMAL_LISTING)
|
||||
},
|
||||
onSubmit = {navController.navigate(
|
||||
AppScreen.saleArchive("2")
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||
arguments = listOf(
|
||||
navArgument("animalId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val animalId = backStackEntry
|
||||
.arguments
|
||||
?.getString("animalId")
|
||||
?: return@composable
|
||||
|
||||
AnimalProfileScreen(
|
||||
animalId = animalId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavClick = onNavClick,
|
||||
onSellerClick = { sellerId ->
|
||||
navController.navigate(
|
||||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||
arguments = listOf(
|
||||
navArgument("sellerId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val sellerId = backStackEntry
|
||||
.arguments
|
||||
?.getString("sellerId")
|
||||
?: return@composable
|
||||
|
||||
SellerProfileScreen(
|
||||
sellerId = sellerId,
|
||||
onBackClick = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CONTACTS) {
|
||||
ContactsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CALLS) {
|
||||
CallsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.CHATS) {
|
||||
ChatsScreen(
|
||||
onBackClick = {navController.navigate(AppScreen.BUY_ANIMALS)},//{navController.popBackStack()},
|
||||
onTabClick = onNavClick,
|
||||
onChatItemClick = {navController.navigate(AppScreen.chats("2"))}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${AppScreen.CHAT}/{contact}",
|
||||
arguments = listOf(
|
||||
navArgument("contact") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
|
||||
val sellerId = backStackEntry
|
||||
.arguments
|
||||
?.getString("contact")
|
||||
?: return@composable
|
||||
|
||||
ChatScreen(
|
||||
sellerId,
|
||||
onBackClick = {
|
||||
navController.navigate(AppScreen.CHATS)
|
||||
//navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
package com.example.livingai_lg.ui.navigation.navigation_legacy
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.delay
|
||||
import com.example.livingai_lg.ui.AuthState
|
||||
import com.example.livingai_lg.ui.MainViewModel
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
|
||||
object OldAppScreen {
|
||||
const val LANDING = "landing"
|
||||
const val SIGN_IN = "sign_in"
|
||||
const val SIGN_UP = "sign_up"
|
||||
|
||||
const val OTP = "otp"
|
||||
|
||||
const val CHOOSE_SERVICE = "choose_service"
|
||||
|
||||
const val CREATE_PROFILE = "create_profile"
|
||||
|
||||
const val BUY_ANIMALS = "buy_animals"
|
||||
const val ANIMAL_PROFILE = "animal_profile"
|
||||
|
||||
const val CREATE_ANIMAL_LISTING = "create_animal_listing"
|
||||
|
||||
const val BUY_ANIMALS_FILTERS = "buy_animals_filters"
|
||||
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||
|
||||
const val SELLER_PROFILE = "seller_profile"
|
||||
|
||||
const val SALE_ARCHIVE = "sale_archive"
|
||||
|
||||
const val POST_SALE_SURVEY = "post_sale_survey"
|
||||
|
||||
const val SAVED_LISTINGS = "saved_listings"
|
||||
|
||||
const val CONTACTS = "contacts"
|
||||
|
||||
const val CALLS = "calls"
|
||||
|
||||
const val CHAT = "chat"
|
||||
|
||||
const val CHATS = "chats"
|
||||
|
||||
const val ACCOUNTS = "accounts"
|
||||
|
||||
fun chats(contact: String) =
|
||||
"$CHAT/$contact"
|
||||
|
||||
fun otp(phone: String, name: String) =
|
||||
"$OTP/$phone/$name"
|
||||
|
||||
fun otpWithSignup(phone: String, name: String, state: String, district: String, village: String) =
|
||||
"$OTP/$phone/$name/$state/$district/$village"
|
||||
|
||||
fun createProfile(name: String) =
|
||||
"$CREATE_PROFILE/$name"
|
||||
|
||||
fun chooseService(profileId: String) =
|
||||
"$CHOOSE_SERVICE/$profileId"
|
||||
fun postSaleSurvey(animalId: String) =
|
||||
"$POST_SALE_SURVEY/$animalId"
|
||||
|
||||
fun animalProfile(animalId: String) =
|
||||
"$ANIMAL_PROFILE/$animalId"
|
||||
|
||||
fun sellerProfile(sellerId: String) =
|
||||
"$SELLER_PROFILE/$sellerId"
|
||||
|
||||
fun saleArchive(saleId: String) =
|
||||
"$SALE_ARCHIVE/$saleId"
|
||||
|
||||
}
|
||||
|
||||
object Graph {
|
||||
const val AUTH = "auth"
|
||||
const val MAIN = "main"
|
||||
|
||||
fun auth(route: String)=
|
||||
"$AUTH/$route"
|
||||
|
||||
fun main(route: String)=
|
||||
"$MAIN/$route"
|
||||
}
|
||||
@Composable
|
||||
fun OldAppNavigation(
|
||||
authState: AuthState,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
// Determine start destination based on initial auth state
|
||||
// This prevents showing landing screen when user is already logged in
|
||||
val startDestination = remember(authState) {
|
||||
when (authState) {
|
||||
is AuthState.Authenticated -> Graph.MAIN
|
||||
is AuthState.Unauthenticated -> Graph.AUTH
|
||||
is AuthState.Unknown -> Graph.AUTH // Show landing while checking
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
authNavGraph(navController, mainViewModel)
|
||||
mainNavGraph(navController)
|
||||
}
|
||||
|
||||
// Handle navigation based on auth state changes
|
||||
LaunchedEffect(authState) {
|
||||
Log.d("AppNavigation", "LaunchedEffect triggered with authState: $authState")
|
||||
when (authState) {
|
||||
is AuthState.Authenticated -> {
|
||||
// User is authenticated, navigate to ChooseServiceScreen
|
||||
// Add a small delay to ensure NavHost graphs are fully built
|
||||
delay(100)
|
||||
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
Log.d("AppNavigation", "Authenticated - currentRoute: $currentRoute")
|
||||
// Only navigate if we're not already in the MAIN graph or ChooseServiceScreen
|
||||
if (currentRoute?.startsWith(Graph.MAIN) != true &&
|
||||
currentRoute?.startsWith(AppScreen.CHOOSE_SERVICE) != true) {
|
||||
Log.d("AppNavigation", "Navigating to ChooseServiceScreen (default profileId: 1)")
|
||||
try {
|
||||
// Navigate directly to the start destination route of MAIN graph
|
||||
// This avoids the "Sequence is empty" error when navigating to Graph.MAIN
|
||||
navController.navigate(AppScreen.chooseService("1")) {
|
||||
// Clear back stack to prevent going back to auth screens
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
// Prevent multiple navigations
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppNavigation", "Navigation error: ${e.message}", e)
|
||||
// Fallback: try navigating to Graph.MAIN if direct route fails
|
||||
try {
|
||||
navController.navigate(Graph.MAIN) {
|
||||
popUpTo(Graph.AUTH) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
Log.e("AppNavigation", "Fallback navigation also failed: ${e2.message}", e2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d("AppNavigation", "Already in MAIN graph or ChooseServiceScreen, skipping navigation")
|
||||
}
|
||||
}
|
||||
is AuthState.Unauthenticated -> {
|
||||
// User is not authenticated, ensure we're in auth graph (landing screen)
|
||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
||||
if (currentRoute?.startsWith(Graph.MAIN) == true ||
|
||||
currentRoute?.startsWith(Graph.AUTH) != true) {
|
||||
navController.navigate(Graph.AUTH) {
|
||||
// Clear back stack to prevent going back to main screens
|
||||
popUpTo(0) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
is AuthState.Unknown -> {
|
||||
// Still checking auth status
|
||||
// If we're on landing screen, stay there
|
||||
// If we're on main screen and checking, don't navigate yet
|
||||
// This prevents flickering during token validation
|
||||
}
|
||||
}
|
||||
}
|
||||
// MainNavGraph(navController)
|
||||
// AuthNavGraph(navController)
|
||||
// when (authState) {
|
||||
// is AuthState.Unauthenticated -> {AuthNavGraph()}
|
||||
// is AuthState.Authenticated -> {MainNavGraph()}
|
||||
// is AuthState.Unknown -> {
|
||||
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
// CircularProgressIndicator()
|
||||
// }
|
||||
// }
|
||||
// AuthState.Loading -> SplashScreen()
|
||||
// }
|
||||
|
||||
// val onNavClick: (String) -> Unit = { route ->
|
||||
// val currentRoute =
|
||||
// navController.currentBackStackEntry?.destination?.route
|
||||
//
|
||||
// if (currentRoute != route) {
|
||||
// navController.navigate(route) {
|
||||
// launchSingleTop = true
|
||||
// restoreState = true
|
||||
// popUpTo(navController.graph.startDestinationId) {
|
||||
// saveState = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// NavHost(
|
||||
// navController = navController,
|
||||
// startDestination = AppScreen.LANDING,
|
||||
//
|
||||
// ) {
|
||||
// composable(AppScreen.LANDING) {
|
||||
// LandingScreen(
|
||||
// onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||
// onSignInClick = { navController.navigate(AppScreen.SIGN_IN) },
|
||||
// onGuestClick = { navController.navigate(AppScreen.CREATE_PROFILE) }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.SIGN_IN) {
|
||||
// SignInScreen(
|
||||
// onSignUpClick = { navController.navigate(AppScreen.SIGN_UP) },
|
||||
// onSignInClick = {
|
||||
// navController.navigate(AppScreen.OTP)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.SIGN_UP) {
|
||||
// SignUpScreen(
|
||||
// onSignUpClick = {
|
||||
// navController.navigate(AppScreen.OTP)
|
||||
// },
|
||||
// onSignInClick = {
|
||||
// navController.navigate(AppScreen.SIGN_IN) {
|
||||
// popUpTo(AppScreen.SIGN_UP) { inclusive = true }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.OTP) {
|
||||
// OtpScreen(
|
||||
// onContinue = { navController.navigate(AppScreen.CREATE_PROFILE) }
|
||||
// )
|
||||
// }
|
||||
|
||||
// composable(AppScreen.CREATE_PROFILE) {
|
||||
// CreateProfileScreen (
|
||||
// onProfileSelected = { profileId ->
|
||||
// if (profileId == "buyer_seller") {
|
||||
// navController.navigate(AppScreen.BUY_ANIMALS)
|
||||
// } else {
|
||||
// navController.navigate(AppScreen.CHOOSE_SERVICE)
|
||||
// } },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.CHOOSE_SERVICE) {
|
||||
// ChooseServiceScreen (
|
||||
// onServiceSelected = { navController.navigate(AppScreen.LANDING) },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.BUY_ANIMALS) {
|
||||
// BuyScreen(
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onProductClick = { animalId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.animalProfile(animalId)
|
||||
// )
|
||||
// },
|
||||
// onNavClick = onNavClick,
|
||||
// onFilterClick = {navController.navigate(AppScreen.BUY_ANIMALS_FILTERS)},
|
||||
// onSortClick = {navController.navigate(AppScreen.BUY_ANIMALS_SORT)},
|
||||
// onSellerClick = { sellerId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.sellerProfile(sellerId)
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.BUY_ANIMALS_FILTERS) {
|
||||
// FilterScreen(
|
||||
// onSubmitClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSkipClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
//
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.BUY_ANIMALS_SORT) {
|
||||
// SortScreen(
|
||||
// onApplyClick = {navController.navigate(AppScreen.BUY_ANIMALS)},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSkipClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onCancelClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
//
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(AppScreen.CREATE_ANIMAL_LISTING) {
|
||||
// NewListingScreen (
|
||||
// onSaveClick = {navController.navigate(
|
||||
// AppScreen.postSaleSurvey("2")
|
||||
// )},
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.SALE_ARCHIVE}/{saleId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("saleId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val saleId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("saleId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// SaleArchiveScreen(
|
||||
// saleId = saleId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSaleSurveyClick = { saleId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.sellerProfile(saleId)
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.POST_SALE_SURVEY}/{animalId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("animalId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val animalId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("animalId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// PostSaleSurveyScreen (
|
||||
// animalId = animalId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSubmit = {navController.navigate(
|
||||
// AppScreen.saleArchive("2")
|
||||
// )}
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.ANIMAL_PROFILE}/{animalId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("animalId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val animalId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("animalId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// AnimalProfileScreen(
|
||||
// animalId = animalId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// },
|
||||
// onSellerClick = { sellerId ->
|
||||
// navController.navigate(
|
||||
// AppScreen.sellerProfile(sellerId)
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// composable(
|
||||
// route = "${AppScreen.SELLER_PROFILE}/{sellerId}",
|
||||
// arguments = listOf(
|
||||
// navArgument("sellerId") { type = NavType.StringType }
|
||||
// )
|
||||
// ) { backStackEntry ->
|
||||
//
|
||||
// val sellerId = backStackEntry
|
||||
// .arguments
|
||||
// ?.getString("sellerId")
|
||||
// ?: return@composable
|
||||
//
|
||||
// SellerProfileScreen(
|
||||
// sellerId = sellerId,
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// composable(AppScreen.SELLER_PROFILE) {
|
||||
// SellerProfileScreen (
|
||||
// onBackClick = {
|
||||
// navController.popBackStack()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
import androidx.compose.material.icons.filled.Construction
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onApiTest: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
val apiClient = remember { AuthApiClient(context) }
|
||||
|
||||
// State for API test dialog
|
||||
var showApiResultDialog by remember { mutableStateOf(false) }
|
||||
var apiResultJson by remember { mutableStateOf<String?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Top App Bar
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Accounts",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Black
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.Black
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
// Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp)
|
||||
) {
|
||||
// Logout option
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
authManager.logout()
|
||||
.onSuccess { _: com.example.livingai_lg.api.LogoutResponse ->
|
||||
Toast.makeText(context, "Logged out successfully", Toast.LENGTH_SHORT).show()
|
||||
onLogout()
|
||||
}
|
||||
.onFailure { error: Throwable ->
|
||||
Toast.makeText(context, "Logout failed: ${error.message}", Toast.LENGTH_SHORT).show()
|
||||
// Still call onLogout to navigate away even if API call fails
|
||||
onLogout()
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||
contentDescription = "Logout",
|
||||
tint = Color(0xFFE53935),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Log Out",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFE53935)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(vertical = 12.dp)
|
||||
.clickable {
|
||||
// Call API test directly instead of navigating
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
try {
|
||||
// First get current user's ID
|
||||
val userDetails = apiClient.getUserDetails().getOrNull()
|
||||
if (userDetails != null) {
|
||||
// Call GET /users/:userId on port 3200
|
||||
val result = apiClient.getUserById(userDetails.id, "http://10.0.2.2:3200")
|
||||
result.onSuccess { jsonString ->
|
||||
apiResultJson = jsonString
|
||||
showApiResultDialog = true
|
||||
isLoading = false
|
||||
}.onFailure { error ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
"API Test Failed: ${error.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Failed to get user details",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
isLoading = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Error: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Construction,
|
||||
contentDescription = "Api test",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = if (isLoading) "Testing API..." else "Test API",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API Result Dialog
|
||||
if (showApiResultDialog) {
|
||||
Dialog(onDismissRequest = { showApiResultDialog = false }) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.8f)
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Dialog Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "API Test Result",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
IconButton(onClick = { showApiResultDialog = false }) {
|
||||
Text(
|
||||
text = "✕",
|
||||
fontSize = 20.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// JSON Content
|
||||
apiResultJson?.let { json ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = json,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No result",
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Close Button
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Button(
|
||||
onClick = { showApiResultDialog = false },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import com.example.livingai_lg.ui.components.ImageCarousel
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.StarHalf
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.example.livingai_lg.ui.models.Animal
|
||||
import com.example.livingai_lg.ui.utils.formatPrice
|
||||
import com.example.livingai_lg.ui.utils.formatViews
|
||||
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||
import com.example.livingai_lg.ui.components.FloatingActionBar
|
||||
import com.example.livingai_lg.ui.models.sampleAnimals
|
||||
import com.example.livingai_lg.ui.utils.formatAge
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.components.ActionPopup
|
||||
import com.example.livingai_lg.ui.components.MediaFullscreenOverlay
|
||||
import com.example.livingai_lg.ui.components.RatingStars
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
import com.example.livingai_lg.ui.utils.formatDistance
|
||||
|
||||
|
||||
@Composable
|
||||
fun AnimalProfileScreen(
|
||||
animalId: String,
|
||||
onBackClick: () -> Unit = {},
|
||||
onSellerClick: (sellerId: String) -> Unit = {},
|
||||
onNavClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
var showSavedPopup by remember { mutableStateOf(false) }
|
||||
var showMediaOverlay by remember { mutableStateOf(false) }
|
||||
var mediaOverlayStartIndex by remember { mutableStateOf(0) }
|
||||
val animal = sampleAnimals.find { animal -> animal.id == animalId } ?: Animal(id = "null")
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Photo section with overlays
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(350.dp)
|
||||
.shadow(4.dp)
|
||||
) {
|
||||
// Main image
|
||||
val product = null
|
||||
ImageCarousel(
|
||||
media = animal.media ?: emptyList(),
|
||||
enableFullscreenPreview = true,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onMediaClick = { startIndex ->
|
||||
showMediaOverlay = true
|
||||
mediaOverlayStartIndex = startIndex
|
||||
}
|
||||
)
|
||||
|
||||
// Gradient overlay at bottom
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.6f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBackIos,
|
||||
contentDescription = "Back",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(5.dp)
|
||||
.size(36.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = CircleShape,
|
||||
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||
)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.35f),
|
||||
shape = CircleShape
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onBackClick()
|
||||
}
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
// Views indicator (top left)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 5.dp, top = 5.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = RoundedCornerShape(50),
|
||||
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||
)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.35f), // 👈 light but effective
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.padding(horizontal = 6.dp, vertical = 3.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_view),
|
||||
contentDescription = "Views",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatViews(animal.views),
|
||||
fontSize = AppTypography.BodySmall,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
// Animal info (centered bottom)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 68.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "${animal.name}, ${formatAge(animal.age)}",
|
||||
fontSize = AppTypography.Title,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White,
|
||||
style = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.75f),
|
||||
offset = Offset(0f, 2f),
|
||||
blurRadius = 6f
|
||||
)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "${animal.breed} • ${animal.location}, ${formatDistance(animal.distance)}".uppercase(),
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
letterSpacing = 1.2.sp,
|
||||
style = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.6f),
|
||||
offset = Offset(0f, 1.5f),
|
||||
blurRadius = 4f
|
||||
)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
}
|
||||
|
||||
// AI Score badge (bottom left)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(start = 5.dp, bottom = 5.dp)
|
||||
.height(60.dp)
|
||||
.border(2.dp, Color(0xFF717182), CircleShape)
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AIScoreCircle(score = animal.aiScore ?: 0f)
|
||||
Text(
|
||||
text = "AI Score",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
// Display location (bottom right)
|
||||
Text(
|
||||
text = buildString {
|
||||
append("At Display: ")
|
||||
append(animal.displayLocation)
|
||||
},
|
||||
fontSize = AppTypography.Caption,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
lineHeight = 13.sp,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = 8.dp)
|
||||
.width(98.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Info card section
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 36.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
// Price and seller info
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = formatPrice(animal.price),
|
||||
fontSize = AppTypography.Title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
if (animal.isFairPrice ?: false) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 0.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_thumbs_up),
|
||||
contentDescription = "Fair Price",
|
||||
tint = Color(0xFF00C950),
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Fair Price",
|
||||
fontSize = AppTypography.Caption,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF00C950)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onSellerClick(animal.sellerId?:"")
|
||||
})
|
||||
{
|
||||
Text(
|
||||
text = "Sold By: ${animal.sellerName}",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
// Star rating
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RatingStars(
|
||||
rating = animal.rating ?: 0f,
|
||||
starSize = 12.dp
|
||||
)
|
||||
Text(
|
||||
text = "${animal.rating} (${animal.ratingCount} Ratings)",
|
||||
fontSize = AppTypography.Caption,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// About section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "About",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF09090B).copy(alpha = 0.5f)
|
||||
)
|
||||
Text(
|
||||
text = animal.description ?: "",
|
||||
fontSize = AppTypography.BodySmall,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF09090B),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
}
|
||||
|
||||
// Spacer(modifier = Modifier.height(24.dp))
|
||||
//
|
||||
//
|
||||
// FloatingActionBar(
|
||||
// modifier = Modifier,
|
||||
// // .offset(y = (-40).dp), // 👈 hover effect
|
||||
// onChatClick = { /* TODO */ },
|
||||
// onCallClick = { /* TODO */ },
|
||||
// onLocationClick = { /* TODO */ },
|
||||
// onBookmarkClick = { /* TODO */ }
|
||||
// )
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Ad space banner
|
||||
AdSpaceBanner()
|
||||
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
}
|
||||
FloatingActionBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(horizontal = 16.dp,
|
||||
16.dp)
|
||||
.offset(y = (-10).dp)
|
||||
.zIndex(10f), // 👈 ensure it floats above everything
|
||||
onChatClick = { /* TODO */ },
|
||||
onCallClick = { /* TODO */ },
|
||||
onLocationClick = { /* TODO */ },
|
||||
onBookmarkClick = { showSavedPopup = true }
|
||||
)
|
||||
|
||||
ActionPopup(
|
||||
visible = showSavedPopup,
|
||||
text = "Saved",
|
||||
icon = Icons.Default.Bookmark,
|
||||
backgroundColor = Color.Black,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = 96.dp),
|
||||
onClick = {
|
||||
onNavClick(AppScreen.SAVED_LISTINGS)
|
||||
// Navigate to saved items
|
||||
},
|
||||
onDismiss = {
|
||||
showSavedPopup = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showMediaOverlay) {
|
||||
MediaFullscreenOverlay(
|
||||
media = animal.media?: emptyList(),
|
||||
startIndex = mediaOverlayStartIndex,
|
||||
onClose = { showMediaOverlay = false }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Modifier.Companion.align(bottomEnd: Alignment) {}
|
||||
|
||||
@Composable
|
||||
fun AIScoreCircle(score: Float) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.drawBehind {
|
||||
// Background circle
|
||||
drawCircle(
|
||||
color = Color(0xFFDD88CF),
|
||||
radius = size.minDimension / 2,
|
||||
alpha = 0.3f,
|
||||
style = Stroke(width = 4.dp.toPx())
|
||||
)
|
||||
|
||||
// Progress arc
|
||||
val sweepAngle = 360f * score
|
||||
drawArc(
|
||||
color = Color(0xFF9AFF9A),
|
||||
startAngle = -90f,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = false,
|
||||
style = Stroke(width = 4.dp.toPx(), cap = StrokeCap.Round),
|
||||
size = Size(size.width, size.height),
|
||||
topLeft = Offset.Zero
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "${(score * 100).toInt()}%",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButtonLarge(icon: Int, label: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.shadow(8.dp, CircleShape)
|
||||
.background(Color.White, CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = label,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Indication
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.room.util.copy
|
||||
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||
import com.example.livingai_lg.ui.components.AnimalTypeSelector
|
||||
import com.example.livingai_lg.ui.components.BuyAnimalCard
|
||||
import com.example.livingai_lg.ui.models.animalTypes
|
||||
|
||||
import com.example.livingai_lg.ui.components.FilterButton
|
||||
import com.example.livingai_lg.ui.components.SortButton
|
||||
import com.example.livingai_lg.ui.components.UserLocationHeader
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||
import com.example.livingai_lg.ui.models.sampleAnimals
|
||||
import com.example.livingai_lg.ui.models.userProfile
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.components.ActionPopup
|
||||
import com.example.livingai_lg.ui.components.AddressSelectorOverlay
|
||||
import com.example.livingai_lg.ui.components.FilterOverlay
|
||||
import com.example.livingai_lg.ui.components.InfoOverlay
|
||||
import com.example.livingai_lg.ui.components.NotificationsOverlay
|
||||
import com.example.livingai_lg.ui.components.SortOverlay
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.models.TextFilter
|
||||
import com.example.livingai_lg.ui.models.isDefault
|
||||
import com.example.livingai_lg.ui.models.sampleNotifications
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import com.example.livingai_lg.ui.state.FilterStore
|
||||
|
||||
@Composable
|
||||
fun BuyScreen(
|
||||
onProductClick: (productId: String) -> Unit = {},
|
||||
onBackClick: () -> Unit = {},
|
||||
onNavClick: (route: String) -> Unit = {},
|
||||
onFilterClick: () -> Unit = {},
|
||||
onSortClick: () -> Unit = {},
|
||||
onSellerClick: (sellerId: String) -> Unit = {},
|
||||
onWishlistClick: () -> Unit = {}
|
||||
) {
|
||||
var activeFilters by remember {
|
||||
mutableStateOf(FilterStore.filters.value)
|
||||
}
|
||||
val isSaved = remember { mutableStateOf(false) }
|
||||
var showAddressSelector by remember { mutableStateOf(false) }
|
||||
var selectedAddressId by remember { mutableStateOf<String?>(userProfile.addresses.find { address -> address.isPrimary }?.id) }
|
||||
val showFilterOverlay = remember { mutableStateOf(false) }
|
||||
val showSortOverlay = remember { mutableStateOf(false) }
|
||||
var showSavedPopup by remember { mutableStateOf(false) }
|
||||
var showNotifications by remember { mutableStateOf(false) }
|
||||
val sampleNotifications = sampleNotifications
|
||||
var showInfoOverlay by remember { mutableStateOf(false) }
|
||||
var selectedItemId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
|
||||
BottomNavScaffold(
|
||||
items = mainBottomNavItems,
|
||||
currentItem = "Home",
|
||||
onBottomNavItemClick = onNavClick,
|
||||
) { paddingValues ->
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
// Header with profile and notification
|
||||
// Top header strip
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
UserLocationHeader(
|
||||
user = userProfile,
|
||||
onOpenAddressOverlay = { showAddressSelector = true },
|
||||
selectedAddressId = selectedAddressId?:"1",
|
||||
onProfileClick = {
|
||||
onNavClick(AppScreen.ACCOUNTS)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Row{
|
||||
// Right-side actions (notifications, etc.)
|
||||
Icon(
|
||||
imageVector = Icons.Default.FavoriteBorder,
|
||||
contentDescription = "Wishlist",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){ onWishlistClick() }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Icon(
|
||||
|
||||
painter = painterResource(R.drawable.ic_notification_unread),
|
||||
contentDescription = "Notifications",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){ showNotifications = true }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
// horizontalArrangement = Arrangement.SpaceBetween,
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// UserLocationHeader(
|
||||
// user = userProfile,
|
||||
// modifier = Modifier.padding(horizontal = 16.dp)
|
||||
// )
|
||||
//
|
||||
// Icon(
|
||||
// painter = painterResource(R.drawable.ic_notification_unread),
|
||||
// contentDescription = "Notifications",
|
||||
// tint = Color.Black,
|
||||
// modifier = Modifier.size(24.dp)
|
||||
// )
|
||||
// }
|
||||
|
||||
// Animal type filter buttons
|
||||
AnimalTypeSelector(
|
||||
animalTypes = animalTypes,
|
||||
selectedAnimalType = activeFilters.animal.value,
|
||||
onAnimalTypeSelected = { animal->
|
||||
activeFilters = activeFilters.copy(
|
||||
animal = TextFilter(
|
||||
value = animal,
|
||||
filterSet = true
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
// Ad space banner
|
||||
AdSpaceBanner(
|
||||
modifier = Modifier.padding(horizontal = 22.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
|
||||
// Sort and Filter buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 22.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
SortButton(
|
||||
onClick = { showSortOverlay.value = true }
|
||||
)
|
||||
FilterButton(onClick = { showFilterOverlay.value = true }, hasActiveFilters = !activeFilters.isDefault())
|
||||
}
|
||||
|
||||
sampleAnimals.forEach { animal ->
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Product card
|
||||
BuyAnimalCard(
|
||||
product = animal,
|
||||
isSaved = isSaved.value,
|
||||
onSavedChange = { isSaved.value = it },
|
||||
onProductClick = { onProductClick(animal.id)},
|
||||
onSellerClick = onSellerClick,
|
||||
onBookmarkClick = { showSavedPopup = true },
|
||||
onInfoClick = { showInfoOverlay = true
|
||||
selectedItemId=animal.id},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Ad space banner at bottom
|
||||
AdSpaceBanner(
|
||||
modifier = Modifier.padding(horizontal = 22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddressSelectorOverlay(
|
||||
visible = showAddressSelector,
|
||||
addresses = userProfile.addresses,
|
||||
selectedAddressId = selectedAddressId,
|
||||
onSelect = { addressId ->
|
||||
selectedAddressId = addressId
|
||||
showAddressSelector = false
|
||||
},
|
||||
onClose = { showAddressSelector = false }
|
||||
)
|
||||
|
||||
SortOverlay(
|
||||
visible = showSortOverlay.value,
|
||||
onApplyClick = { selected ->
|
||||
// TODO: apply sort
|
||||
},
|
||||
onDismiss = { showSortOverlay.value = false }
|
||||
)
|
||||
|
||||
FilterOverlay(
|
||||
appliedFilters = activeFilters,
|
||||
visible = showFilterOverlay.value,
|
||||
onDismiss = { showFilterOverlay.value = false },
|
||||
onSubmitClick = { filters ->
|
||||
Log.d("Filters", "$filters")
|
||||
activeFilters = filters
|
||||
// apply filters
|
||||
}
|
||||
)
|
||||
|
||||
val animal = sampleAnimals.find { it.id == selectedItemId }
|
||||
|
||||
InfoOverlay(
|
||||
visible = showInfoOverlay,
|
||||
title = animal?.breed ?:"",
|
||||
text = animal?.breedInfo ?: "",
|
||||
onDismiss = { showInfoOverlay = false }
|
||||
)
|
||||
|
||||
ActionPopup(
|
||||
visible = showSavedPopup,
|
||||
text = "Saved",
|
||||
icon = Icons.Default.Bookmark,
|
||||
backgroundColor = Color.Black,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = 96.dp),
|
||||
onClick = {
|
||||
onNavClick(AppScreen.SAVED_LISTINGS)
|
||||
// Navigate to saved items
|
||||
},
|
||||
onDismiss = {
|
||||
showSavedPopup = false
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
NotificationsOverlay(
|
||||
visible = showNotifications,
|
||||
notifications = sampleNotifications,
|
||||
onClose = { showNotifications = false },
|
||||
onDismiss = { id ->
|
||||
// remove notification from list
|
||||
},
|
||||
onNotificationClick = { id ->
|
||||
// optional navigation
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.OptionCard
|
||||
import com.example.livingai_lg.ui.components.backgrounds.StoreBackground
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
data class ServiceType(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val icon: Any,
|
||||
val iconTint: Color,
|
||||
val backgroundColor: Color
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChooseServiceScreen(
|
||||
profileId: String? = null,
|
||||
onServiceSelected: (serviceType: String) -> Unit = {}
|
||||
) {
|
||||
val selectedService = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val serviceTypes = listOf(
|
||||
ServiceType(
|
||||
id = "transport",
|
||||
title = "Transport",
|
||||
icon = R.drawable.ic_shop,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF9D4EDD)
|
||||
),
|
||||
ServiceType(
|
||||
id = "vet",
|
||||
title = "Vet",
|
||||
icon = R.drawable.ic_bag,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF3A86FF)
|
||||
),
|
||||
ServiceType(
|
||||
id = "feed_supplier",
|
||||
title = "Feed Supplier",
|
||||
icon = R.drawable.ic_spanner,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFFFF5722)
|
||||
),
|
||||
ServiceType(
|
||||
id = "medicine_supplier",
|
||||
title = "Medicine Supplier",
|
||||
icon = R.drawable.ic_shop2,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFF4CAF50)
|
||||
),
|
||||
ServiceType(
|
||||
id = "other",
|
||||
title = "Other",
|
||||
icon = R.drawable.ic_other,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = Color(0xFFD4A942)
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F1E8))
|
||||
) {
|
||||
// Decorative background
|
||||
StoreBackground()
|
||||
|
||||
// Main content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Choose Service",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Choose a Service",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A).copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Service selection cards
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
serviceTypes.forEach { service ->
|
||||
OptionCard(
|
||||
label = service.title,
|
||||
icon = service.icon,
|
||||
iconTint = service.iconTint,
|
||||
iconBackgroundColor = service.backgroundColor,
|
||||
onClick = {
|
||||
selectedService.value = service.id
|
||||
onServiceSelected(service.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.ui.components.OptionCard
|
||||
|
||||
import com.example.livingai_lg.ui.components.backgrounds.StoreBackground
|
||||
import com.example.livingai_lg.ui.models.profileTypes
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun CreateProfileScreen(
|
||||
name: String,
|
||||
onProfileSelected: (profileId: String) -> Unit = {}
|
||||
) {
|
||||
val selectedProfile = remember { mutableStateOf<String>(profileTypes[0].id) }
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
fun updateProfile(profileId: String) {
|
||||
scope.launch {
|
||||
authManager.updateProfile(name, profileId)
|
||||
.onSuccess {
|
||||
onProfileSelected(profileId)
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Failed to update profile: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F1E8))
|
||||
) {
|
||||
// Decorative background
|
||||
StoreBackground()
|
||||
|
||||
// Main content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Create Profile",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Choose Profile Type",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A).copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Profile selection cards
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
profileTypes.forEach { profile ->
|
||||
OptionCard(
|
||||
label = profile.title,
|
||||
icon = profile.icon,
|
||||
iconBackgroundColor = profile.backgroundColor,
|
||||
onClick = {
|
||||
onProfileSelected(profile.id)
|
||||
//updateProfile(profile.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.DropdownInput
|
||||
import com.example.livingai_lg.ui.components.RangeFilter
|
||||
import com.example.livingai_lg.ui.components.WishlistNameOverlay
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.models.RangeFilterState
|
||||
import com.example.livingai_lg.ui.models.TextFilter
|
||||
import com.example.livingai_lg.ui.state.WishlistEntry
|
||||
import com.example.livingai_lg.ui.state.WishlistStore
|
||||
import com.example.livingai_lg.ui.models.isDefault
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FilterScreen(
|
||||
appliedFilters: FiltersState,
|
||||
wishlistEditMode: Boolean = false,
|
||||
onSubmitClick: (FiltersState) -> Unit,
|
||||
onBackClick: () -> Unit = {},
|
||||
onCancelClick: () -> Unit = {},
|
||||
) {
|
||||
var filters by remember {
|
||||
mutableStateOf(appliedFilters)
|
||||
}
|
||||
var showWishlistOverlay by remember { mutableStateOf(false) }
|
||||
|
||||
var selectedAnimal =filters.animal
|
||||
var selectedBreed = filters.breed
|
||||
var selectedDistance = filters.distance
|
||||
var selectedGender = filters.gender
|
||||
|
||||
var animalExpanded by remember { mutableStateOf(false) }
|
||||
var breedExpanded by remember { mutableStateOf(false) }
|
||||
var distanceExpanded by remember { mutableStateOf(false) }
|
||||
var genderExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
var price =filters.price
|
||||
var age = filters.age
|
||||
var weight = filters.weight
|
||||
var milkYield = filters.milkYield
|
||||
var calving = filters.calving
|
||||
|
||||
var selectedPregnancyStatus = filters.pregnancyStatuses
|
||||
|
||||
|
||||
var calvingFromExpanded by remember { mutableStateOf(false) }
|
||||
var calvingToExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val maxCalving = 10
|
||||
|
||||
val calvingFromOptions = (0..maxCalving).map { it.toString() }
|
||||
val calvingToOptions = (calving.min..maxCalving).map { it.toString() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Header
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Filters",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
if(!wishlistEditMode){
|
||||
IconButton(
|
||||
|
||||
onClick = {
|
||||
if (!filters.isDefault()) {
|
||||
showWishlistOverlay = true
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FavoriteBorder,
|
||||
contentDescription = "Add to Wishlist",
|
||||
tint = if(!filters.isDefault()) Color.Black else Color.Gray
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// Animal Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
DropdownInput(
|
||||
label = "Animal",
|
||||
selected = if (selectedAnimal.filterSet) selectedAnimal.value else "",
|
||||
options = listOf("Cow", "Buffalo", "Goat", "Sheep"),
|
||||
expanded = animalExpanded,
|
||||
onExpandedChange = { animalExpanded = it },
|
||||
onSelect = { item ->
|
||||
filters = filters.copy(
|
||||
animal = TextFilter(
|
||||
value = item,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
animalExpanded = false
|
||||
},
|
||||
placeholder = "Select Animal", // <--- half width
|
||||
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
// Breed Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
DropdownInput(
|
||||
label = "Breed",
|
||||
selected = if (selectedBreed.filterSet) selectedBreed.value else "",
|
||||
options = listOf("Holstein", "Jersey", "Gir", "Sahiwal"),
|
||||
expanded = breedExpanded,
|
||||
onExpandedChange = { breedExpanded = it },
|
||||
onSelect = { item ->
|
||||
filters = filters.copy(
|
||||
breed = TextFilter(
|
||||
value = item,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
breedExpanded = false
|
||||
},
|
||||
placeholder = "Select Breed", // <--- half width
|
||||
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
// Price and Age Row
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
|
||||
) {
|
||||
RangeFilter(
|
||||
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||
|
||||
label = "Price",
|
||||
min = 0,
|
||||
max = 90_000,
|
||||
valueFrom = price.min,
|
||||
valueTo = price.max,
|
||||
modified = price.filterSet,
|
||||
onValueChange = { from, to ->
|
||||
filters = filters.copy(
|
||||
price = RangeFilterState(
|
||||
min = from,
|
||||
max = to,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
|
||||
) {
|
||||
RangeFilter(
|
||||
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||
|
||||
label = "Age (years)",
|
||||
min = 0,
|
||||
max = 20,
|
||||
valueFrom = age.min,
|
||||
valueTo = age.max,
|
||||
showSlider = false,
|
||||
modified = age.filterSet,
|
||||
onValueChange = { from, to ->
|
||||
filters = filters.copy(
|
||||
age = RangeFilterState(
|
||||
min = from,
|
||||
max = to,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Distance and Gender Row
|
||||
|
||||
// Distance Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
DropdownInput(
|
||||
label = "Distance",
|
||||
selected = if (selectedDistance.filterSet) selectedDistance.value else "",
|
||||
options = listOf("0-5 km", "5-10 km", "10-20 km", "20+ km"),
|
||||
expanded = distanceExpanded,
|
||||
onExpandedChange = { distanceExpanded = it },
|
||||
onSelect = { item ->
|
||||
filters = filters.copy(
|
||||
distance = TextFilter(
|
||||
value = item,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
distanceExpanded = false
|
||||
},
|
||||
placeholder = "Choose Distance", // <--- half width
|
||||
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
// Gender Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
DropdownInput(
|
||||
label = "Gender",
|
||||
selected = if (selectedGender.filterSet) selectedGender.value else "",
|
||||
options = listOf("Male", "Female"),
|
||||
expanded = genderExpanded,
|
||||
onExpandedChange = { genderExpanded = it },
|
||||
onSelect = { item ->
|
||||
filters = filters.copy(
|
||||
gender = TextFilter(
|
||||
value = item,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
genderExpanded = false
|
||||
},
|
||||
placeholder = "Choose Gender", // <--- half width
|
||||
textColor = if (selectedAnimal.filterSet) Color.Black else Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Pregnancy Status Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Pregnancy Status",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF364153)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
listOf("Pregnant", "Calved", "Option").forEach { status ->
|
||||
PregnancyStatusChip(
|
||||
label = status,
|
||||
isSelected = selectedPregnancyStatus.contains(status),
|
||||
onClick = {
|
||||
filters = filters.copy(
|
||||
pregnancyStatuses =
|
||||
if (filters.pregnancyStatuses.contains(status))
|
||||
filters.pregnancyStatuses - status
|
||||
else
|
||||
filters.pregnancyStatuses + status
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
RangeFilter(
|
||||
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||
|
||||
label = "Weight",
|
||||
min = 0,
|
||||
max = 9000,
|
||||
valueFrom = weight.min,
|
||||
valueTo = weight.max,
|
||||
modified = weight.filterSet,
|
||||
onValueChange = { from, to ->
|
||||
filters = filters.copy(
|
||||
weight = RangeFilterState(
|
||||
min = from,
|
||||
max = to,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
RangeFilter(
|
||||
modifier = Modifier.fillMaxWidth(), // 👈 important
|
||||
|
||||
label = "Milk Yield",
|
||||
min = 0,
|
||||
max = 900,
|
||||
valueFrom = milkYield.min,
|
||||
valueTo = milkYield.max,
|
||||
modified = milkYield.filterSet,
|
||||
onValueChange = { from, to ->
|
||||
filters = filters.copy(
|
||||
milkYield = RangeFilterState(
|
||||
min = from,
|
||||
max = to,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Calving Number Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
// FROM
|
||||
DropdownInput(
|
||||
label = "Calving Number",
|
||||
selected = calving.min.toString(),
|
||||
options = calvingFromOptions,
|
||||
expanded = calvingFromExpanded,
|
||||
onExpandedChange = { calvingFromExpanded = it },
|
||||
onSelect = { value ->
|
||||
val newFrom = value.toInt()
|
||||
val newTo = maxOf(calving.max, newFrom)
|
||||
|
||||
filters = filters.copy(
|
||||
calving = RangeFilterState(
|
||||
min = newFrom,
|
||||
max = newTo,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
|
||||
calvingFromExpanded = false
|
||||
},
|
||||
placeholder = "From",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "to",
|
||||
fontSize = 20.sp,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
// TO
|
||||
DropdownInput(
|
||||
selected = calving.max.toString(),
|
||||
options = calvingToOptions, // 👈 constrained options
|
||||
expanded = calvingToExpanded,
|
||||
onExpandedChange = { calvingToExpanded = it },
|
||||
onSelect = { value ->
|
||||
val newTo = value.toInt()
|
||||
val newFrom = minOf(calving.min, newTo)
|
||||
|
||||
filters = filters.copy(
|
||||
calving = RangeFilterState(
|
||||
min = newFrom,
|
||||
max = newTo,
|
||||
filterSet = true
|
||||
)
|
||||
)
|
||||
calvingToExpanded = false
|
||||
},
|
||||
placeholder = "To",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Action Buttons
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Submit Button
|
||||
Button(
|
||||
onClick = { onSubmitClick(filters) },
|
||||
modifier = Modifier
|
||||
.width(173.dp)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Black,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Submit",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
// Cancel Button
|
||||
OutlinedButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier
|
||||
.width(173.dp)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Color(0x1A000000)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color(0xFF0A0A0A)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
fontSize = AppTypography.Body,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showWishlistOverlay) {
|
||||
WishlistNameOverlay(
|
||||
onDismiss = { showWishlistOverlay = false },
|
||||
onSave = { name ->
|
||||
WishlistStore.add(
|
||||
WishlistEntry(
|
||||
name = name,
|
||||
filters = filters
|
||||
)
|
||||
)
|
||||
showWishlistOverlay = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PregnancyStatusChip(
|
||||
label: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(29.dp)
|
||||
.widthIn(min = 76.dp)
|
||||
.background(
|
||||
if (isSelected) Color(0xFF007BFF) else Color.Transparent,
|
||||
RoundedCornerShape(24.dp)
|
||||
)
|
||||
.border(
|
||||
1.dp,
|
||||
Color(0xFFE5E7EB),
|
||||
RoundedCornerShape(24.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (isSelected) Color(0xFFF4F4F4) else Color.Black,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = Color(0xFFE3E3E3),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.MediaPickerCard
|
||||
import com.example.livingai_lg.ui.models.NewListingFormState
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewListingScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onSaveClick: () -> Unit = {},
|
||||
formState: NewListingFormState = remember { NewListingFormState() }
|
||||
) {
|
||||
var name by formState.name
|
||||
var animal by formState.animal
|
||||
var breed by formState.breed
|
||||
var age by formState.age
|
||||
var milkYield by formState.milkYield
|
||||
var calvingNumber by formState.calvingNumber
|
||||
var reproductiveStatus by formState.reproductiveStatus
|
||||
var description by formState.description
|
||||
|
||||
var animalExpanded by formState.animalExpanded
|
||||
var breedExpanded by formState.breedExpanded
|
||||
|
||||
val mediaUploads = formState.mediaUploads
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Header
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "New Listing",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
// Form Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Name Input
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Name",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Animal and Breed Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Animal Dropdown
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = animalExpanded,
|
||||
onExpandedChange = { animalExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = animal,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
placeholder = {
|
||||
Text(
|
||||
"Animal",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = Color(0xFFA5A5A5)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = animalExpanded,
|
||||
onDismissRequest = { animalExpanded = false }
|
||||
) {
|
||||
listOf("Cow", "Buffalo", "Goat", "Sheep").forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option) },
|
||||
onClick = {
|
||||
animal = option
|
||||
animalExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Breed Dropdown
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = breedExpanded,
|
||||
onExpandedChange = { breedExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = breed,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
placeholder = {
|
||||
Text(
|
||||
"Breed",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = Color(0xFFA5A5A5)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = breedExpanded,
|
||||
onDismissRequest = { breedExpanded = false }
|
||||
) {
|
||||
listOf("Holstein", "Jersey", "Gir", "Sahiwal").forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option) },
|
||||
onClick = {
|
||||
breed = option
|
||||
breedExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Age and Milk Yield Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = age,
|
||||
onValueChange = { age = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Age (Years)",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowDropDown,
|
||||
contentDescription = "Sort",
|
||||
tint = Color(0xFF353535)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = milkYield,
|
||||
onValueChange = { milkYield = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Average Milk Yield (L)",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
// Calving Number
|
||||
OutlinedTextField(
|
||||
value = calvingNumber,
|
||||
onValueChange = { calvingNumber = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Calving Number",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Reproductive Status
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Reproductive Status",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
listOf("Pregnant", "Calved", "None").forEach { status ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){
|
||||
reproductiveStatus = status
|
||||
}
|
||||
) {
|
||||
RadioButton(
|
||||
selected = reproductiveStatus == status,
|
||||
onClick = { reproductiveStatus = status },
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = Color(0xFF717182),
|
||||
unselectedColor = Color(0xFF717182)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = status,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Description",
|
||||
color = Color(0xFF717182),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(74.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x1A000000)
|
||||
),
|
||||
maxLines = 3
|
||||
)
|
||||
|
||||
// Upload Media Section
|
||||
Text(
|
||||
text = "Upload Media",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF000000),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
// Photo Uploads Grid
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
mediaUploads.chunked(2).forEach { rowItems ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
rowItems.forEach { upload ->
|
||||
MediaPickerCard(
|
||||
upload = upload,
|
||||
modifier = Modifier.weight(1f),
|
||||
onUriSelected = { uri ->
|
||||
upload.uri = uri
|
||||
}
|
||||
)
|
||||
}
|
||||
// Fill empty space if odd number of items
|
||||
if (rowItems.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Cancel Button
|
||||
OutlinedButton(
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Color(0x1A000000)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color(0xFF0A0A0A)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
// Save Profile Button
|
||||
Button(
|
||||
onClick = onSaveClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Black,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Save Profile",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.AppNotification
|
||||
|
||||
@Composable
|
||||
fun NotificationsScreen(
|
||||
notifications: List<AppNotification>,
|
||||
onBackClick: () -> Unit,
|
||||
onDismiss: (String) -> Unit,
|
||||
onNotificationClick: (String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Notifications",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
Divider(color = Color(0xFFE5E7EB))
|
||||
|
||||
if (notifications.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No notifications",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF6B7280)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(
|
||||
items = notifications,
|
||||
key = { it.id }
|
||||
) { notification ->
|
||||
NotificationItem(
|
||||
notification = notification,
|
||||
onDismiss = onDismiss,
|
||||
onClick = onNotificationClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationItem(
|
||||
notification: AppNotification,
|
||||
onDismiss: (String) -> Unit,
|
||||
onClick: (String) -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White, RoundedCornerShape(14.dp))
|
||||
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(14.dp))
|
||||
.clickable { onClick(notification.id) }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = notification.icon,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = notification.title,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF1F2937),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { onDismiss(notification.id) },
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Dismiss",
|
||||
tint = Color(0xFF6B7280),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,511 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
data class SaleSurveyData(
|
||||
val buyerName: String = "",
|
||||
val salePrice: String = "",
|
||||
val saleDate: String = "",
|
||||
val saleLocation: String = "",
|
||||
val notes: String = "",
|
||||
val attachments: List<SurveyAttachment> = emptyList()
|
||||
)
|
||||
|
||||
data class SurveyAttachment(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val type: SurveyAttachmentType
|
||||
)
|
||||
|
||||
enum class SurveyAttachmentType {
|
||||
DOCUMENT,
|
||||
PHOTO,
|
||||
ADD_NEW
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostSaleSurveyScreen(
|
||||
animalId: String? = null,
|
||||
onBackClick: () -> Unit = {},
|
||||
onSkipClick: () -> Unit = {},
|
||||
onSubmit: (SaleSurveyData) -> Unit = {},
|
||||
onCancel: () -> Unit = {}
|
||||
) {
|
||||
var surveyData by remember { mutableStateOf(SaleSurveyData()) }
|
||||
var buyerNameExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val defaultAttachments = listOf(
|
||||
SurveyAttachment("1", "Sale Papers 1", SurveyAttachmentType.DOCUMENT),
|
||||
SurveyAttachment("2", "Photo 2", SurveyAttachmentType.PHOTO),
|
||||
SurveyAttachment("3", "Photo 1", SurveyAttachmentType.PHOTO),
|
||||
SurveyAttachment("4", "Add new", SurveyAttachmentType.ADD_NEW)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(26.dp))
|
||||
|
||||
FormField(
|
||||
label = "Sold to*",
|
||||
isRequired = true
|
||||
) {
|
||||
DropdownField(
|
||||
placeholder = "Buyer Name",
|
||||
value = surveyData.buyerName,
|
||||
expanded = buyerNameExpanded,
|
||||
onExpandedChange = { buyerNameExpanded = it },
|
||||
onValueChange = { surveyData = surveyData.copy(buyerName = it) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(29.dp))
|
||||
|
||||
FormField(
|
||||
label = "Sale Price*",
|
||||
isRequired = true
|
||||
) {
|
||||
TextInputField(
|
||||
placeholder = "Price",
|
||||
value = surveyData.salePrice,
|
||||
onValueChange = { surveyData = surveyData.copy(salePrice = it) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(21.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
FormField(
|
||||
label = "Sale Date*",
|
||||
isRequired = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
DatePickerField(
|
||||
placeholder = "Date",
|
||||
value = surveyData.saleDate,
|
||||
onValueChange = { surveyData = surveyData.copy(saleDate = it) }
|
||||
)
|
||||
}
|
||||
|
||||
FormField(
|
||||
label = "Sale Location",
|
||||
isRequired = false,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
TextInputField(
|
||||
placeholder = "Location",
|
||||
value = surveyData.saleLocation,
|
||||
onValueChange = { surveyData = surveyData.copy(saleLocation = it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(34.dp))
|
||||
|
||||
MultilineTextInputField(
|
||||
placeholder = "Notes",
|
||||
value = surveyData.notes,
|
||||
onValueChange = { surveyData = surveyData.copy(notes = it) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(104.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(31.dp))
|
||||
|
||||
Text(
|
||||
text = "Attachments",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 29.sp,
|
||||
modifier = Modifier.padding(start = 11.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(31.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(19.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[0].name,
|
||||
type = defaultAttachments[0].type,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[1].name,
|
||||
type = defaultAttachments[1].type,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[2].name,
|
||||
type = defaultAttachments[2].type,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentChip(
|
||||
name = defaultAttachments[3].name,
|
||||
type = defaultAttachments[3].type,
|
||||
isAddNew = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(53.dp))
|
||||
|
||||
PrimaryButton(
|
||||
text = "Submit",
|
||||
onClick = { onSubmit(surveyData) },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
SecondaryButton(
|
||||
text = "Cancel",
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FormField(
|
||||
label: String,
|
||||
isRequired: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 29.sp
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextInputField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MultilineTextInputField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onExpandedChange(!expanded) }
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = value.ifEmpty { placeholder },
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
letterSpacing = (-0.312).sp,
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Dropdown",
|
||||
tint = Color(0xFFA5A5A5),
|
||||
modifier = Modifier
|
||||
.size(15.dp, 10.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DatePickerField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (value.isEmpty()) placeholder else value,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
letterSpacing = (-0.312).sp,
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarToday,
|
||||
contentDescription = "Calendar",
|
||||
tint = Color(0xFF000000),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AttachmentChip(
|
||||
name: String,
|
||||
type: SurveyAttachmentType,
|
||||
modifier: Modifier = Modifier,
|
||||
isAddNew: Boolean = false,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(43.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFE5E7EB),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 24.sp,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = if (isAddNew) Icons.Default.Add else Icons.Default.ArrowDropDown,
|
||||
contentDescription = if (isAddNew) "Add" else "Attachment",
|
||||
tint = if (isAddNew) Color(0xFF777777) else Color(0xFFADADAD),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrimaryButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(173.dp)
|
||||
.height(50.dp)
|
||||
.background(Color(0xFF0A0A0A), RoundedCornerShape(50.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFFFFFFFF),
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.312).sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SecondaryButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(173.dp)
|
||||
.height(50.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(50.dp)
|
||||
)
|
||||
.background(Color.Transparent, RoundedCornerShape(50.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.312).sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
data class SaleArchive(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val breed: String,
|
||||
val soldTo: String,
|
||||
val saleDate: String,
|
||||
val salePrice: String,
|
||||
val location: String,
|
||||
val notes: String,
|
||||
val imageUrl: String,
|
||||
val attachments: List<Attachment>
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val type: AttachmentType
|
||||
)
|
||||
|
||||
enum class AttachmentType {
|
||||
SALE_PAPERS,
|
||||
PHOTO
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaleArchiveScreen(
|
||||
saleId: String,
|
||||
onBackClick: () -> Unit = {},
|
||||
onSaleSurveyClick:(saleId: String) -> Unit = {},
|
||||
) {
|
||||
val sale = SaleArchive(
|
||||
id = "1",
|
||||
name = "Gauri",
|
||||
breed = "Gir",
|
||||
soldTo = "Buyer 1",
|
||||
saleDate = "07.12.2025",
|
||||
salePrice = "₹30,000",
|
||||
location = "Ravi Nagar, Nagpur",
|
||||
notes = "Buyer requested a check up after 15 days of sale. Additional support for 20 days.",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/9389ccf57f6d262b500be45f6d9cbfe929302413?width=784",
|
||||
attachments = listOf(
|
||||
Attachment("1", "Sale Papers 1", AttachmentType.SALE_PAPERS),
|
||||
Attachment("2", "Photo 1", AttachmentType.PHOTO),
|
||||
Attachment("3", "Sale Papers 1", AttachmentType.SALE_PAPERS),
|
||||
Attachment("4", "Photo 2", AttachmentType.PHOTO)
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 11.dp, vertical = 15.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onBackClick
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = sale.imageUrl,
|
||||
contentDescription = "${sale.name} image",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(273.dp),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(
|
||||
elevation = 25.dp,
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||
spotColor = Color.Black.copy(alpha = 0.25f)
|
||||
)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFE5E7EB).copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||
)
|
||||
.background(
|
||||
color = Color(0xFFFCFBF8),
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||
)
|
||||
.padding(horizontal = 21.dp, vertical = 17.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(1.987.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Name, Breed",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = "${sale.name}, (${sale.breed})",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF101828),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.99.dp)
|
||||
) {
|
||||
InfoField(
|
||||
label = "Sold To",
|
||||
value = sale.soldTo,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
InfoField(
|
||||
label = "Sale Date",
|
||||
value = sale.saleDate,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 17.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.99.dp)
|
||||
) {
|
||||
InfoField(
|
||||
label = "Sale Price",
|
||||
value = sale.salePrice,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
InfoField(
|
||||
label = "Location",
|
||||
value = sale.location,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.078.dp)
|
||||
.background(Color(0xFFD1D5DC))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(5.995.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Notes",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color.Transparent,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = sale.notes,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF364153),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Attachments",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF000000),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.dp)
|
||||
) {
|
||||
AttachmentButton(
|
||||
name = sale.attachments[0].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentButton(
|
||||
name = sale.attachments[1].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(11.dp)
|
||||
) {
|
||||
AttachmentButton(
|
||||
name = sale.attachments[2].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AttachmentButton(
|
||||
name = sale.attachments[3].name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoField(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(1.987.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF101828),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AttachmentButton(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(38.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFE5E7EB),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.background(
|
||||
color = Color.Transparent,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
Spacer(modifier = Modifier.width(5.995.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.AttachFile,
|
||||
contentDescription = "Attachment",
|
||||
tint = Color(0xFFADADAD),
|
||||
modifier = Modifier.size(19.99.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.components.AdSpaceBanner
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
data class SavedListing(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val age: String,
|
||||
val breed: String,
|
||||
val price: String,
|
||||
val views: String,
|
||||
val distance: String,
|
||||
val imageUrl: String
|
||||
)
|
||||
|
||||
enum class BottomNavTab {
|
||||
HOME,
|
||||
SELL,
|
||||
CHATS,
|
||||
SERVICES,
|
||||
MANDI
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SavedListingsScreen(
|
||||
onListingClick: (String) -> Unit = {},
|
||||
onNavClick: (route: String) -> Unit = {},
|
||||
onBackClick: () -> Unit = {}
|
||||
) {
|
||||
val savedListings = listOf(
|
||||
SavedListing(
|
||||
id = "1",
|
||||
name = "Eauri",
|
||||
age = "Age: 3 yrs",
|
||||
breed = "Holstein",
|
||||
price = "₹45,000",
|
||||
views = "124 views",
|
||||
distance = "Distance: 12 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/db216f496f6d5fd681db9afc8006eb8da3164c17?width=192"
|
||||
),
|
||||
SavedListing(
|
||||
id = "2",
|
||||
name = "Fauri",
|
||||
age = "Age: 2 yrs",
|
||||
breed = "Jersey",
|
||||
price = "₹38,000",
|
||||
views = "89 views",
|
||||
distance = "Distance: 17 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/a49aa5540b0cb6bf76ba3aa99cb8d4f94835e8ee?width=192"
|
||||
),
|
||||
SavedListing(
|
||||
id = "3",
|
||||
name = "Gauri",
|
||||
age = "Age: 4 yrs",
|
||||
breed = "Gir",
|
||||
price = "₹52,000",
|
||||
views = "156 views",
|
||||
distance = "Distance: 20 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/26fd38c965180ef066862fb1d0aa6df040a0d389?width=192"
|
||||
),
|
||||
SavedListing(
|
||||
id = "4",
|
||||
name = "Hauri",
|
||||
age = "Age: 1.5 yrs",
|
||||
breed = "Sahiwal",
|
||||
price = "₹28,000",
|
||||
views = "67 views",
|
||||
distance = "Distance: 6 km",
|
||||
imageUrl = "https://api.builder.io/api/v1/image/assets/TEMP/b8edfd48d908a8e0eeeee55745ab409e454be25f?width=192"
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = mainBottomNavItems,
|
||||
currentItem = "Home",
|
||||
onBottomNavItemClick = onNavClick,
|
||||
) { paddingValues ->
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 72.dp)
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Saved Listings",
|
||||
fontSize = AppTypography.Display,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 15.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
item {
|
||||
AdSpaceBanner()
|
||||
}
|
||||
|
||||
items(savedListings) { listing ->
|
||||
ListingCard(
|
||||
listing = listing,
|
||||
onClick = { onListingClick(listing.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListingCard(
|
||||
listing: SavedListing,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(138.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFFF3F4F6).copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8), RoundedCornerShape(24.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 21.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = listing.imageUrl,
|
||||
contentDescription = listing.name,
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = listing.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = listing.age,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = listing.breed,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = listing.price,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 24.sp,
|
||||
textAlign = TextAlign.End
|
||||
)
|
||||
|
||||
Text(
|
||||
text = listing.views,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF6A7282),
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.End
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = listing.distance,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF4A5565),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Call
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Message
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
data class SellerProfile(
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val location: String,
|
||||
val rating: Double,
|
||||
val reviewCount: Int,
|
||||
val trustScore: Int,
|
||||
val totalSales: Int,
|
||||
val yearsActive: String,
|
||||
val responseRate: String,
|
||||
val about: String
|
||||
)
|
||||
|
||||
data class PastSale(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val emoji: String,
|
||||
val age: String,
|
||||
val breed: String,
|
||||
val price: String,
|
||||
val rating: Double
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SellerProfileScreen(
|
||||
sellerId: String = "1",
|
||||
onBackClick: () -> Unit = {},
|
||||
onCallClick: () -> Unit = {},
|
||||
onMessageClick: () -> Unit = {},
|
||||
onViewAllSalesClick: () -> Unit = {}
|
||||
) {
|
||||
// Sample data - in real app, fetch based on sellerId
|
||||
val seller = SellerProfile(
|
||||
name = "Ramesh Singh",
|
||||
initials = "RS",
|
||||
location = "Nagpur, Maharashtra",
|
||||
rating = 4.8,
|
||||
reviewCount = 156,
|
||||
trustScore = 95,
|
||||
totalSales = 47,
|
||||
yearsActive = "5+",
|
||||
responseRate = "98%",
|
||||
about = "Experienced cattle farmer and trader with over 5 years in the business. Specializing in dairy cattle breeds including Gir, Holstein, and Jersey. All animals are well-maintained with complete vaccination records and health certificates."
|
||||
)
|
||||
|
||||
val pastSales = listOf(
|
||||
PastSale("1", "Gir Cow", "🐄", "Age: 3 yrs", "Holstein", "₹45,000", 5.0),
|
||||
PastSale("2", "Buffalo", "🐃", "Age: 4 yrs", "Murrah", "₹55,000", 4.9),
|
||||
PastSale("3", "Goat", "🐐", "Age: 2 yrs", "Sirohi", "₹12,000", 4.7)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Header
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Seller Profile",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
),
|
||||
modifier = Modifier.border(
|
||||
width = 1.dp,
|
||||
color = Color(0x1A000000),
|
||||
shape = RectangleShape
|
||||
)
|
||||
)
|
||||
|
||||
// Scrollable Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Profile Card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Profile Info
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Avatar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = seller.initials,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
// Info
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = seller.name,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
contentDescription = "Location",
|
||||
tint = Color(0xFF717182),
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
text = seller.location,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Rating",
|
||||
tint = Color(0xFFFDC700),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = seller.rating.toString(),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "•",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${seller.reviewCount} reviews",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI Trust Score
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Color(0x4D60A5FA),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.border(
|
||||
1.dp,
|
||||
Color(0x4D60A5FA),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Shield,
|
||||
contentDescription = "Trust",
|
||||
tint = Color(0xFF155DFC),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "AI Trust Score",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Score",
|
||||
tint = Color(0xFF155DFC),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${seller.trustScore}/100",
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFF155DFC)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Verified seller with excellent transaction history",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onCallClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(46.dp),
|
||||
shape = RoundedCornerShape(23.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Black,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Call,
|
||||
contentDescription = "Call",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Call",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onMessageClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(46.dp),
|
||||
shape = RoundedCornerShape(23.dp),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Color(0x1A000000)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color(0xFF0A0A0A)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Message,
|
||||
contentDescription = "Message",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Message",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stats Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
StatCard(
|
||||
value = seller.totalSales.toString(),
|
||||
label = "Total Sales",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
StatCard(
|
||||
value = seller.yearsActive,
|
||||
label = "Years Active",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
StatCard(
|
||||
value = seller.responseRate,
|
||||
label = "Response\nRate",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// Past Sales Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Past Sales",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
TextButton(onClick = onViewAllSalesClick) {
|
||||
Text(
|
||||
text = "View All",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182),
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pastSales.forEach { sale ->
|
||||
PastSaleCard(sale)
|
||||
}
|
||||
}
|
||||
|
||||
// About Seller Section
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "About Seller",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = seller.about,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 22.75.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatCard(
|
||||
value: String,
|
||||
label: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.height(94.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PastSaleCard(sale: PastSale) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0x1A000000))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Animal Icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(Color(0xFFF7F4EE), RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = sale.emoji,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
|
||||
// Details
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = sale.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = sale.age,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
Text(
|
||||
text = sale.breed,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = sale.price,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Rating",
|
||||
tint = Color(0xFFFDC700),
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
text = sale.rating.toString(),
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowBackIos
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import com.example.livingai_lg.ui.models.SortDirection
|
||||
import com.example.livingai_lg.ui.models.SortField
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.SortItem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SortScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onApplyClick: (List<SortField>) -> Unit = {},
|
||||
onCancelClick: () -> Unit = {}
|
||||
) {
|
||||
var fields by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
SortField("distance", "Distance"),
|
||||
SortField("price", "Price"),
|
||||
SortField("milk", "Milk Capacity"),
|
||||
SortField("ai", "AI Score"),
|
||||
SortField("age", "Age")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleField(index: Int) {
|
||||
val current = fields[index]
|
||||
|
||||
val nextDirection = when (current.direction) {
|
||||
SortDirection.NONE -> SortDirection.ASC
|
||||
SortDirection.ASC -> SortDirection.DESC
|
||||
SortDirection.DESC -> SortDirection.NONE
|
||||
}
|
||||
|
||||
val updated = fields.toMutableList()
|
||||
|
||||
updated[index] = current.copy(
|
||||
direction = nextDirection,
|
||||
order = if (nextDirection == SortDirection.NONE) null else current.order
|
||||
)
|
||||
|
||||
// Recalculate sort order
|
||||
val active = updated
|
||||
.filter { it.direction != SortDirection.NONE }
|
||||
.sortedBy { it.order ?: Int.MAX_VALUE }
|
||||
.mapIndexed { i, item -> item.copy(order = i + 1) }
|
||||
|
||||
fields = updated.map { field ->
|
||||
active.find { it.key == field.key } ?: field.copy(order = null)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
|
||||
) {
|
||||
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBackIos,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
Text("Sort By", fontSize = 32.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
fields.forEachIndexed { index, field ->
|
||||
SortItem(
|
||||
field = field,
|
||||
onToggle = { toggleField(index) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = { onApplyClick(fields.filter { it.direction != SortDirection.NONE }) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.width(173.dp)
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Black,
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(25.dp)
|
||||
) {
|
||||
Text("Apply")
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.width(173.dp)
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = Color(0xFF0A0A0A)
|
||||
),
|
||||
shape = RoundedCornerShape(25.dp)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.state.FilterStore
|
||||
import com.example.livingai_lg.ui.state.WishlistStore
|
||||
|
||||
@Composable
|
||||
fun WishlistScreen(
|
||||
onApply: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
|
||||
val wishlist by WishlistStore.wishlist.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Saved Filters",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (wishlist.isEmpty()) {
|
||||
Text("No saved filters yet")
|
||||
} else {
|
||||
wishlist.forEach { entry ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable {
|
||||
onApply()
|
||||
FilterStore.set(entry.filters)
|
||||
}
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = entry.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "Tap to apply",
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import com.example.livingai_lg.ui.components.IconCircle
|
||||
import com.example.livingai_lg.ui.theme.FarmButtonPrimary
|
||||
import com.example.livingai_lg.ui.theme.FarmButtonSecondary
|
||||
import com.example.livingai_lg.ui.theme.FarmButtonText
|
||||
|
||||
import com.example.livingai_lg.ui.theme.FarmSproutBg
|
||||
import com.example.livingai_lg.ui.theme.FarmSproutIcon
|
||||
import com.example.livingai_lg.ui.theme.FarmSunBg
|
||||
import com.example.livingai_lg.ui.theme.FarmSunIcon
|
||||
import com.example.livingai_lg.ui.theme.FarmTextDark
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
import com.example.livingai_lg.ui.theme.FarmWheatBg
|
||||
import com.example.livingai_lg.ui.theme.FarmWheatIcon
|
||||
import com.example.livingai_lg.R
|
||||
|
||||
@Composable
|
||||
fun LandingScreen(
|
||||
onSignUpClick: () -> Unit = {},
|
||||
onSignInClick: () -> Unit = {},
|
||||
onGuestClick: () -> Unit = {}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FarmTextLight)
|
||||
) {
|
||||
// Decorative background elements
|
||||
DecorativeBackground()
|
||||
|
||||
// Main content
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding( top = 56.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
//Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Icon row
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
IconCircle(
|
||||
backgroundColor = FarmWheatBg,
|
||||
size = 56.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_wheat),
|
||||
contentDescription = "Wheat Icon",
|
||||
tint = FarmWheatIcon // keeps original vector colors
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
IconCircle(
|
||||
backgroundColor = FarmSproutBg,
|
||||
size = 64.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_sprout),
|
||||
contentDescription = "Wheat Icon",
|
||||
tint = FarmSproutIcon // keeps original vector colors
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
IconCircle(
|
||||
backgroundColor = FarmSunBg,
|
||||
size = 56.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_sun),
|
||||
contentDescription = "Wheat Icon",
|
||||
tint = FarmSunIcon // keeps original vector colors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Welcome text
|
||||
Text(
|
||||
text = "Welcome!",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = FarmTextDark
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Join the farm marketplace community",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = FarmTextNormal
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Sign up button
|
||||
Button(
|
||||
onClick = onSignUpClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = FarmButtonPrimary,
|
||||
contentColor = FarmButtonText
|
||||
),
|
||||
elevation = ButtonDefaults.buttonElevation(
|
||||
defaultElevation = 8.dp
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "New user? Sign up",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Sign in button
|
||||
Button(
|
||||
onClick = onSignInClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = FarmButtonSecondary,
|
||||
contentColor = FarmButtonText
|
||||
),
|
||||
elevation = ButtonDefaults.buttonElevation(
|
||||
defaultElevation = 8.dp
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Already a user? Sign in",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Continue as guest
|
||||
TextButton(
|
||||
onClick = onGuestClick
|
||||
) {
|
||||
Text(
|
||||
text = "Continue as Guest",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = FarmTextNormal,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,544 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.SignupRequest
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.api.UserNotFoundException
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import kotlin.math.min
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
@Composable
|
||||
fun OtpScreen(
|
||||
phoneNumber: String,
|
||||
name: String,
|
||||
mainViewModel: com.example.livingai_lg.ui.MainViewModel,
|
||||
onSuccess: () -> Unit = {},
|
||||
onCreateProfile: (name: String) -> Unit = {},
|
||||
onLanding: () -> Unit = {}, // New callback for navigating to landing page
|
||||
// Optional signup data for signup flow
|
||||
signupState: String? = null,
|
||||
signupDistrict: String? = null,
|
||||
signupVillage: String? = null,
|
||||
) {
|
||||
val otpLength = 6
|
||||
val otp = remember { mutableStateOf(List<String>(otpLength) { "" }) }
|
||||
var focusedIndex by remember { mutableStateOf(0) }
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
fun updateOtpAt(index: Int, value: String) {
|
||||
otp.value = otp.value.mapIndexed { i, old ->
|
||||
if (i == index) value else old
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown timer state (1 minutes = 60 seconds)
|
||||
var countdownSeconds by remember { mutableStateOf(10) }
|
||||
var countdownKey by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(countdownKey) {
|
||||
while (countdownSeconds > 0) {
|
||||
delay(1000) // Wait 1 second
|
||||
countdownSeconds--
|
||||
}
|
||||
}
|
||||
|
||||
// Format countdown as MM:SS
|
||||
val minutes = countdownSeconds / 60
|
||||
val seconds = countdownSeconds % 60
|
||||
val countdownText = String.format("%02d:%02d", minutes, seconds)
|
||||
|
||||
// Flag to determine if this is a sign-in flow for an existing user.
|
||||
val isSignInFlow = name == "existing_user"
|
||||
// Flag to determine if this is a signup flow (has signup data)
|
||||
val isSignupFlow = !isSignInFlow && (signupState != null || signupDistrict != null || signupVillage != null)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
Color(0xFFFFFBEA),
|
||||
Color(0xFFFDFBE8),
|
||||
Color(0xFFF7FEE7)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
DecorativeBackground()
|
||||
|
||||
val screenW = maxWidth.value
|
||||
val screenH = maxHeight.value
|
||||
|
||||
// Figma design reference size from Flutter widget
|
||||
val designW = 393.39f
|
||||
val designH = 852.53f
|
||||
|
||||
val scale = min(screenW / designW, screenH / designH)
|
||||
|
||||
fun s(v: Float) = (v * scale).dp // dp scaling
|
||||
fun fs(v: Float) = (v * scale).sp // font scaling
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(horizontal = 12.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// ---------------------------
|
||||
// "Enter OTP" Title
|
||||
// ---------------------------
|
||||
Text(
|
||||
text = "Enter OTP",
|
||||
color = Color(0xFF927B5E),
|
||||
fontSize = fs(20f),
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.25f),
|
||||
offset = Offset(0f, s(4f).value),
|
||||
blurRadius = s(4f).value
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// ---------------------------
|
||||
// OTP 4-Box Input Row
|
||||
// ---------------------------
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(s(17f))
|
||||
) {
|
||||
OTPInputTextFields(
|
||||
otpLength = otpLength,
|
||||
otpValues = otp.value,
|
||||
onUpdateOtpValuesByIndex = { index, value ->
|
||||
updateOtpAt(index,value)
|
||||
},
|
||||
onOtpInputComplete = {}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Countdown Timer
|
||||
// ---------------------------
|
||||
if (countdownSeconds > 0) {
|
||||
Text(
|
||||
text = countdownText,
|
||||
color = Color(0xFF927B5E),
|
||||
fontSize = fs(16f),
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.15f),
|
||||
offset = Offset(0f, s(2f).value),
|
||||
blurRadius = s(2f).value
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Resend OTP",
|
||||
color = Color(0xFFE17100),
|
||||
fontSize = fs(16f),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.clickable {
|
||||
// 🔹 Stub for now
|
||||
scope.launch {
|
||||
|
||||
authManager.requestOtp(phoneNumber)
|
||||
.onSuccess {
|
||||
// OTP sent successfully, navigate to OTP screen with signup data
|
||||
// Pass signup form data through the callback
|
||||
Toast.makeText(context, "Resent OTP",Toast.LENGTH_LONG).show()
|
||||
countdownSeconds = 10
|
||||
countdownKey++
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
Log.e("OtpScreen", "Failed to send OTP ${it.message}", it)
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// Continue Button
|
||||
// ---------------------------
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(vertical = 16.dp, horizontal = 48.dp)
|
||||
.size(s(279.25f), s(55.99f))
|
||||
.shadow(
|
||||
elevation = s(6f),
|
||||
ambientColor = Color.Black.copy(alpha = 0.10f),
|
||||
shape = RoundedCornerShape(s(16f)),
|
||||
)
|
||||
.shadow(
|
||||
elevation = s(15f),
|
||||
ambientColor = Color.Black.copy(alpha = 0.10f),
|
||||
shape = RoundedCornerShape(s(16f)),
|
||||
)
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFFFD9900), Color(0xFFE17100))
|
||||
),
|
||||
shape = RoundedCornerShape(s(16f))
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val otpString = otp.value.joinToString("") { it?.toString() ?: "" }
|
||||
|
||||
if (otpString.length < otpLength) {
|
||||
Toast.makeText(context, "Please enter full OTP", Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (isSignupFlow) {
|
||||
// For signup flow: Verify OTP first, then call signup API to update user with name/location
|
||||
android.util.Log.d("OTPScreen", "Signup flow: Verifying OTP...")
|
||||
authManager.login(phoneNumber, otpString)
|
||||
.onSuccess { verifyResponse ->
|
||||
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
|
||||
// OTP verified successfully - user is now logged in
|
||||
mainViewModel.refreshAuthStatus()
|
||||
// Now call signup API to update user with name and location
|
||||
val signupRequest = SignupRequest(
|
||||
name = name,
|
||||
phoneNumber = phoneNumber,
|
||||
state = signupState,
|
||||
district = signupDistrict,
|
||||
cityVillage = signupVillage
|
||||
)
|
||||
authManager.signup(signupRequest)
|
||||
.onSuccess { signupResponse ->
|
||||
android.util.Log.d("OTPScreen", "Signup API response: success=${signupResponse.success}, userExists=${signupResponse.userExists}, needsProfile=${signupResponse.needsProfile}, isNewAccount=${signupResponse.isNewAccount}")
|
||||
// Check if this is a new user account
|
||||
val isNewUser = signupResponse.isNewAccount == true
|
||||
|
||||
if (isNewUser) {
|
||||
// New user - navigate to landing page
|
||||
android.util.Log.d("OTPScreen", "New user detected - navigating to landing page")
|
||||
try {
|
||||
onLanding()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// Existing user or signup update - use existing logic
|
||||
if (signupResponse.success || signupResponse.userExists == true) {
|
||||
// Success - user is created/updated and logged in
|
||||
// Check if profile needs completion
|
||||
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
|
||||
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
|
||||
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
||||
// mainViewModel.refreshAuthStatus()
|
||||
try {
|
||||
if (needsProfile) {
|
||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// Signup failed but OTP was verified - user is logged in
|
||||
// Navigate to success anyway since verify-otp succeeded
|
||||
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
||||
val needsProfile = verifyResponse.needsProfile
|
||||
// Refresh auth status
|
||||
mainViewModel.refreshAuthStatus()
|
||||
try {
|
||||
if (needsProfile) {
|
||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
// Don't manually navigate - let AppNavigation handle it
|
||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// Show warning if signup update failed
|
||||
val errorMsg = signupResponse.message
|
||||
if (errorMsg != null) {
|
||||
Toast.makeText(context, "Profile update: $errorMsg", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure { signupError ->
|
||||
android.util.Log.e("OTPScreen", "Signup API failed: ${signupError.message}", signupError)
|
||||
// Signup API failed but OTP was verified - user is logged in
|
||||
// For new users, navigate to landing page
|
||||
// For existing users, use existing logic
|
||||
val needsProfile = verifyResponse.needsProfile
|
||||
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
|
||||
// Refresh auth status
|
||||
mainViewModel.refreshAuthStatus()
|
||||
try {
|
||||
// If this is a signup flow and signup failed, treat as new user
|
||||
if (isSignupFlow) {
|
||||
android.util.Log.d("OTPScreen", "Signup flow failed - navigating to landing page")
|
||||
onLanding()
|
||||
} else if (needsProfile) {
|
||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// Show warning about signup failure
|
||||
val errorMsg = signupError.message ?: "Profile update failed"
|
||||
Toast.makeText(context, "Warning: $errorMsg", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.onFailure { error ->
|
||||
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Invalid or expired OTP",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
// For sign-in flow: Just verify OTP and login
|
||||
authManager.login(phoneNumber, otpString)
|
||||
.onSuccess { response ->
|
||||
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
|
||||
// Tokens are now saved (synchronously via commit())
|
||||
// Refresh auth status - this will optimistically set authState to Authenticated
|
||||
// The LaunchedEffect in AppNavigation will automatically navigate to ChooseServiceScreen
|
||||
//mainViewModel.refreshAuthStatus()
|
||||
if(response.needsProfile) {
|
||||
onCreateProfile(name)
|
||||
} else {
|
||||
onSuccess()
|
||||
}
|
||||
android.util.Log.d("OTPScreen", "Auth status refreshed - navigation will happen automatically via LaunchedEffect")
|
||||
}
|
||||
.onFailure { error ->
|
||||
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||
|
||||
// Check if user not found - redirect to signup
|
||||
if (error is UserNotFoundException && error.errorCode == "USER_NOT_FOUND") {
|
||||
android.util.Log.d("OTPScreen", "User not found - redirecting to signup")
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.message ?: "Account not found. Please sign up to create a new account.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
// Navigate back to landing page so user can choose signup
|
||||
try {
|
||||
onLanding()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||
}
|
||||
} else {
|
||||
// Other errors (invalid OTP, expired, etc.)
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.message ?: "Invalid or expired OTP",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
"Continue",
|
||||
color = Color.White,
|
||||
fontSize = fs(16f),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OTPInputTextFields(
|
||||
otpLength: Int,
|
||||
onUpdateOtpValuesByIndex: (Int, String) -> Unit,
|
||||
onOtpInputComplete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
otpValues: List<String> = List(otpLength) { "" }, // Pass this as default for future reference
|
||||
isError: Boolean = false,
|
||||
) {
|
||||
val focusRequesters = List(otpLength) { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
// horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
otpValues.forEachIndexed { index, value ->
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
// .width(64.dp)
|
||||
.padding(6.dp)
|
||||
.focusRequester(focusRequesters[index])
|
||||
.background(Color.White)
|
||||
.onKeyEvent { keyEvent ->
|
||||
if (keyEvent.key == Key.Backspace) {
|
||||
if (otpValues[index].isEmpty() && index > 0) {
|
||||
onUpdateOtpValuesByIndex(index, "")
|
||||
focusRequesters[index - 1].requestFocus()
|
||||
} else {
|
||||
onUpdateOtpValuesByIndex(index, "")
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
value = value,
|
||||
onValueChange = { newValue ->
|
||||
// To use OTP code copied from keyboard
|
||||
if (newValue.length == otpLength) {
|
||||
for (i in otpValues.indices) {
|
||||
onUpdateOtpValuesByIndex(
|
||||
i,
|
||||
if (i < newValue.length && newValue[i].isDigit()) newValue[i].toString() else ""
|
||||
)
|
||||
}
|
||||
|
||||
keyboardController?.hide()
|
||||
onOtpInputComplete() // you should validate the otp values first for, if it is only digits or isNotEmpty
|
||||
} else if (newValue.length <= 1) {
|
||||
onUpdateOtpValuesByIndex(index, newValue)
|
||||
if (newValue.isNotEmpty()) {
|
||||
if (index < otpLength - 1) {
|
||||
focusRequesters[index + 1].requestFocus()
|
||||
} else {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onOtpInputComplete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (index < otpLength - 1) focusRequesters[index + 1].requestFocus()
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = if (index == otpLength - 1) ImeAction.Done else ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
if (index < otpLength - 1) {
|
||||
focusRequesters[index + 1].requestFocus()
|
||||
}
|
||||
},
|
||||
onDone = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onOtpInputComplete()
|
||||
}
|
||||
),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
isError = isError,
|
||||
textStyle = TextStyle(
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||
)
|
||||
)
|
||||
|
||||
LaunchedEffect(value) {
|
||||
if (otpValues.all { it.isNotEmpty() }) {
|
||||
focusManager.clearFocus()
|
||||
onOtpInputComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequesters.first().requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.api.UserNotFoundException
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLink
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
|
||||
import com.example.livingai_lg.ui.components.FarmHeader
|
||||
import com.example.livingai_lg.ui.utils.isKeyboardOpen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SignInScreen(
|
||||
onSignUpClick: () -> Unit = {},
|
||||
onSignInClick: (phone: String, name: String) -> Unit = {_,_->},
|
||||
) {
|
||||
val phoneNumber = remember { mutableStateOf("") }
|
||||
val isValid = phoneNumber.value.length == 10
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FarmTextLight)
|
||||
) {
|
||||
DecorativeBackground()
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||
val keyboardOpen = isKeyboardOpen()
|
||||
val topPadding by animateDpAsState(
|
||||
targetValue = if (keyboardOpen) 40.dp else 140.dp,
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding( top = 40.dp),//topPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (keyboardOpen) Arrangement.Top else Arrangement.Center
|
||||
) {
|
||||
//Spacer(Modifier.height(if (keyboardOpen) 32.dp else 56.dp))
|
||||
// Header
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
||||
FarmHeader()
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
PhoneNumberInput(
|
||||
phone = phoneNumber.value,
|
||||
onChange = { phoneNumber.value = it }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val fullPhoneNumber = "+91${phoneNumber.value}"
|
||||
scope.launch {
|
||||
// First check if user exists before requesting OTP
|
||||
authManager.checkUser(fullPhoneNumber)
|
||||
.onSuccess { checkResponse ->
|
||||
if (checkResponse.userExists) {
|
||||
// User exists, proceed to request OTP
|
||||
authManager.requestOtp(fullPhoneNumber)
|
||||
.onSuccess {
|
||||
onSignInClick(fullPhoneNumber, "existing_user")
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
Log.e("SignInScreen", "Failed to send OTP ${it.message}", it)
|
||||
}
|
||||
} else {
|
||||
// User doesn't exist (shouldn't happen if API works correctly)
|
||||
Toast.makeText(context, "User is not registered. Please sign up.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
.onFailure { error ->
|
||||
// User not found or other error
|
||||
if (error is UserNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.message ?: "User is not registered. Please sign up to create a new account.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = isValid,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isValid) Color(0xFFE17100) else Color(0xFFE17100).copy(
|
||||
alpha = 0.4f
|
||||
),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Sign In", fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Don't have an account? ", fontSize = 16.sp, color = FarmTextNormal)
|
||||
TextButton(onClick = onSignUpClick) {
|
||||
Text("Sign up", fontSize = 16.sp, color = FarmTextLink)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.example.livingai_lg.ui.screens.auth
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.api.AuthApiClient
|
||||
import com.example.livingai_lg.api.AuthManager
|
||||
import com.example.livingai_lg.api.SignupRequest
|
||||
import com.example.livingai_lg.api.TokenManager
|
||||
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
|
||||
import com.example.livingai_lg.ui.components.DropdownInput
|
||||
import com.example.livingai_lg.ui.components.InputField
|
||||
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||
import com.example.livingai_lg.ui.theme.FarmTextLight
|
||||
import com.example.livingai_lg.ui.theme.FarmTextNormal
|
||||
import com.example.livingai_lg.ui.components.FarmHeader
|
||||
import com.example.livingai_lg.ui.utils.isKeyboardOpen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SignUpFormData(
|
||||
val name: String = "",
|
||||
val state: String = "",
|
||||
val district: String = "",
|
||||
val village: String = "",
|
||||
val phoneNumber: String = ""
|
||||
){
|
||||
val isValid: Boolean
|
||||
get() =
|
||||
name.isNotBlank() &&
|
||||
state.isNotBlank() &&
|
||||
district.isNotBlank() &&
|
||||
village.isNotBlank() &&
|
||||
phoneNumber.isValidPhoneNumber()
|
||||
}
|
||||
|
||||
private fun String.isValidPhoneNumber(): Boolean {
|
||||
return length == 10 && all { it.isDigit() }
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SignUpScreen(
|
||||
onSignInClick: () -> Unit = {},
|
||||
onSignUpClick: (phone: String, name: String, state: String, district: String, village: String) -> Unit = {_,_,_,_,_->}
|
||||
) {
|
||||
var formData by remember { mutableStateOf(SignUpFormData()) }
|
||||
|
||||
var showStateDropdown by remember { mutableStateOf(false) }
|
||||
var showDistrictDropdown by remember { mutableStateOf(false) }
|
||||
var showVillageDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
val states = listOf("Maharashtra", "Gujarat", "Tamil Nadu", "Karnataka", "Rajasthan")
|
||||
val districts = listOf("Mumbai", "Pune", "Nagpur", "Aurangabad", "Nashik")
|
||||
val villages = listOf("Village 1", "Village 2", "Village 3", "Village 4", "Village 5")
|
||||
|
||||
val context = LocalContext.current.applicationContext
|
||||
val scope = rememberCoroutineScope()
|
||||
// Use 10.0.2.2 to connect to host machine's localhost from emulator
|
||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FarmTextLight)
|
||||
) {
|
||||
DecorativeBackground()
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 32.dp)
|
||||
) {
|
||||
val horizontalPadding = maxWidth * 0.06f // proportional padding
|
||||
val keyboardOpen = isKeyboardOpen()
|
||||
// val topPadding by animateDpAsState(
|
||||
// targetValue = if (keyboardOpen) 40.dp else 140.dp,
|
||||
// animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
// )
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.padding( top = 40.dp),//topPadding),
|
||||
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (keyboardOpen) Arrangement.Top else Arrangement.Center
|
||||
) {
|
||||
// Spacer(Modifier.height(if (keyboardOpen) 32.dp else 56.dp))
|
||||
// Title
|
||||
FarmHeader()
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Name Input
|
||||
InputField(
|
||||
label = "Enter Name*",
|
||||
value = formData.name,
|
||||
placeholder = "Enter your name",
|
||||
onChange = { formData = formData.copy(name = it) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// State + District Row
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
|
||||
DropdownInput(
|
||||
label = "Enter Location",
|
||||
selected = formData.state,
|
||||
options = states,
|
||||
expanded = showStateDropdown,
|
||||
onExpandedChange = { showStateDropdown = it },
|
||||
onSelect = { formData = formData.copy(state = it) },
|
||||
placeholder = "Select State",
|
||||
modifier = Modifier.weight(0.4f) // <--- half width
|
||||
)
|
||||
|
||||
DropdownInput(
|
||||
selected = formData.district,
|
||||
options = districts,
|
||||
expanded = showDistrictDropdown,
|
||||
onExpandedChange = { showDistrictDropdown = it },
|
||||
onSelect = { formData = formData.copy(district = it) },
|
||||
placeholder = "Select District",
|
||||
modifier = Modifier.weight(0.6f) // <--- half width
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Village Dropdown
|
||||
DropdownInput(
|
||||
label = "Enter Village/Place",
|
||||
selected = formData.village,
|
||||
options = villages,
|
||||
expanded = showVillageDropdown,
|
||||
onExpandedChange = { showVillageDropdown = it },
|
||||
onSelect = {
|
||||
formData = formData.copy(village = it); showVillageDropdown = false
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Phone Number
|
||||
PhoneNumberInput(
|
||||
phone = formData.phoneNumber,
|
||||
onChange = { formData = formData.copy(phoneNumber = it) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
// Sign Up button
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val fullPhoneNumber = "+91${formData.phoneNumber}"
|
||||
|
||||
// First check if user already exists
|
||||
authManager.checkUser(fullPhoneNumber)
|
||||
.onSuccess { checkResponse ->
|
||||
if (checkResponse.userExists) {
|
||||
// User already registered - show message to sign in instead
|
||||
Toast.makeText(
|
||||
context,
|
||||
"This phone number is already registered. Please sign in instead.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
// Optionally navigate to sign in screen
|
||||
onSignInClick()
|
||||
} else {
|
||||
// User doesn't exist - proceed with signup
|
||||
// Request OTP first before allowing signup
|
||||
authManager.requestOtp(fullPhoneNumber)
|
||||
.onSuccess {
|
||||
// OTP sent successfully, navigate to OTP screen with signup data
|
||||
// Pass signup form data through the callback
|
||||
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure { checkError ->
|
||||
// If check fails, it might be a network error or user doesn't exist
|
||||
// Try to proceed with signup (user might not exist)
|
||||
if (checkError is com.example.livingai_lg.api.UserNotFoundException) {
|
||||
// User doesn't exist - proceed with signup
|
||||
authManager.requestOtp(fullPhoneNumber)
|
||||
.onSuccess {
|
||||
// OTP sent successfully, navigate to OTP screen with signup data
|
||||
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
|
||||
}
|
||||
.onFailure {
|
||||
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// Other error (network, etc.)
|
||||
Toast.makeText(context, "Unable to verify phone number: ${checkError.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
enabled = formData.isValid,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE17100))
|
||||
) {
|
||||
Text("Sign Up", fontSize = 16.sp)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Already have account?
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Already have an account? ", color = FarmTextNormal)
|
||||
TextButton(onClick = onSignInClick) {
|
||||
Text("Sign In", color = Color(0xFFE17100))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
package com.example.farmmarketplace.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.CallMade
|
||||
import androidx.compose.material.icons.automirrored.filled.CallMissed
|
||||
import androidx.compose.material.icons.automirrored.filled.CallReceived
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||
|
||||
data class CallRecord(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val callType: CallType,
|
||||
val duration: String,
|
||||
val timestamp: String,
|
||||
val isVideoCall: Boolean = false
|
||||
)
|
||||
|
||||
enum class CallType {
|
||||
INCOMING,
|
||||
OUTGOING,
|
||||
MISSED
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onCallClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {},
|
||||
onCallItemClick: (String) -> Unit = {},
|
||||
onTabClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
val callHistory = listOf(
|
||||
CallRecord(
|
||||
id = "1",
|
||||
name = "Farmer Kumar",
|
||||
initials = "FK",
|
||||
callType = CallType.INCOMING,
|
||||
duration = "5m 23s",
|
||||
timestamp = "Today, 2:30 PM"
|
||||
),
|
||||
CallRecord(
|
||||
id = "2",
|
||||
name = "Seller Raj",
|
||||
initials = "SR",
|
||||
callType = CallType.OUTGOING,
|
||||
duration = "2m 10s",
|
||||
timestamp = "Today, 11:45 AM"
|
||||
),
|
||||
CallRecord(
|
||||
id = "3",
|
||||
name = "Buyer Priya",
|
||||
initials = "BP",
|
||||
callType = CallType.MISSED,
|
||||
duration = "",
|
||||
timestamp = "Yesterday, 8:15 PM"
|
||||
),
|
||||
CallRecord(
|
||||
id = "4",
|
||||
name = "Seller 1",
|
||||
initials = "S1",
|
||||
callType = CallType.OUTGOING,
|
||||
duration = "8m 45s",
|
||||
timestamp = "Yesterday, 5:30 PM",
|
||||
isVideoCall = true
|
||||
),
|
||||
CallRecord(
|
||||
id = "5",
|
||||
name = "Veterinarian",
|
||||
initials = "V",
|
||||
callType = CallType.INCOMING,
|
||||
duration = "12m 30s",
|
||||
timestamp = "2 days ago"
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = chatBottomNavItems,
|
||||
currentItem = "Calls",
|
||||
onBottomNavItemClick = onTabClick,
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||
) {
|
||||
CallsHeader(
|
||||
onBackClick = onBackClick,
|
||||
onCallClick = onCallClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(callHistory) { call ->
|
||||
CallHistoryItem(
|
||||
call = call,
|
||||
onClick = { onCallItemClick(call.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactsBottomNav(
|
||||
// currentTab = ContactsTab.CALLS,
|
||||
// onTabClick = onTabClick
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsHeader(
|
||||
onBackClick: () -> Unit,
|
||||
onCallClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Calls",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 42.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Phone,
|
||||
contentDescription = "New Call",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onCallClick() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMenuClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallHistoryItem(
|
||||
call: CallRecord,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(76.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8), RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = call.initials,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 21.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = call.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (call.callType) {
|
||||
CallType.INCOMING -> Icons.AutoMirrored.Filled.CallReceived
|
||||
CallType.OUTGOING -> Icons.AutoMirrored.Filled.CallMade
|
||||
CallType.MISSED -> Icons.AutoMirrored.Filled.CallMissed
|
||||
},
|
||||
contentDescription = call.callType.name,
|
||||
tint = when (call.callType) {
|
||||
CallType.INCOMING -> Color(0xFF00A63E)
|
||||
CallType.OUTGOING -> Color(0xFF155DFC)
|
||||
CallType.MISSED -> Color(0xFFE7000B)
|
||||
},
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = when (call.callType) {
|
||||
CallType.INCOMING -> "Incoming • ${call.duration}"
|
||||
CallType.OUTGOING -> "Outgoing • ${call.duration}"
|
||||
CallType.MISSED -> "Missed"
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = call.timestamp,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = if (call.isVideoCall) Icons.Default.Videocam else Icons.Default.Phone,
|
||||
contentDescription = if (call.isVideoCall) "Video Call" else "Voice Call",
|
||||
tint = Color(0xFF030213),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
package com.example.livingai_lg.ui.screens.chat
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Attachment
|
||||
import androidx.compose.material.icons.filled.KeyboardVoice
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
data class ChatMessage(
|
||||
val text: String,
|
||||
val timestamp: String,
|
||||
val isSentByCurrentUser: Boolean
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
sellerName: String = "Seller 1",
|
||||
sellerStatus: String = "Online",
|
||||
onBackClick: () -> Unit = {},
|
||||
onPhoneClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {}
|
||||
) {
|
||||
var messageText by remember { mutableStateOf("") }
|
||||
|
||||
val messages = remember {
|
||||
listOf(
|
||||
ChatMessage(
|
||||
text = "Hello! Yes, the cow is still available. Would you like to visit?",
|
||||
timestamp = "10:30 AM",
|
||||
isSentByCurrentUser = false
|
||||
),
|
||||
ChatMessage(
|
||||
text = "Hey is she still available",
|
||||
timestamp = "10:28 AM",
|
||||
isSentByCurrentUser = true
|
||||
),
|
||||
ChatMessage(
|
||||
text = "She's a healthy Gir cow, 2 years old, great milk yield.",
|
||||
timestamp = "10:31 AM",
|
||||
isSentByCurrentUser = false
|
||||
),
|
||||
ChatMessage(
|
||||
text = "What's the price?",
|
||||
timestamp = "10:32 AM",
|
||||
isSentByCurrentUser = true
|
||||
),
|
||||
ChatMessage(
|
||||
text = "₹45,000. Negotiable if you visit today.",
|
||||
timestamp = "10:33 AM",
|
||||
isSentByCurrentUser = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF8F8F8))
|
||||
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
ChatHeader(
|
||||
sellerName = sellerName,
|
||||
sellerStatus = sellerStatus,
|
||||
onBackClick = onBackClick,
|
||||
onPhoneClick = onPhoneClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(vertical = 16.dp)
|
||||
) {
|
||||
items(messages) { message ->
|
||||
MessageBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
MessageInputBar(
|
||||
messageText = messageText,
|
||||
onMessageChange = { messageText = it },
|
||||
onSendClick = {
|
||||
messageText = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatHeader(
|
||||
sellerName: String,
|
||||
sellerStatus: String,
|
||||
onBackClick: () -> Unit,
|
||||
onPhoneClick: () -> Unit,
|
||||
onMenuClick: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 1.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(Color(0xFFF0F0F0), shape = CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "S1",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF0A0A0A)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = sellerName,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1E2939)
|
||||
)
|
||||
Text(
|
||||
text = sellerStatus,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF4A5565)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
IconButton(onClick = onPhoneClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Phone,
|
||||
contentDescription = "Phone",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onMenuClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageBubble(message: ChatMessage) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = if (message.isSentByCurrentUser) Alignment.End else Alignment.Start
|
||||
) {
|
||||
Surface(
|
||||
shape = if (message.isSentByCurrentUser) {
|
||||
RoundedCornerShape(topStart = 16.dp, topEnd = 4.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||
} else {
|
||||
RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||
},
|
||||
color = if (message.isSentByCurrentUser) Color(0xFF0A0A0A) else Color.White,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 271.dp)
|
||||
.then(
|
||||
if (!message.isSentByCurrentUser) {
|
||||
Modifier.border(
|
||||
width = 1.dp,
|
||||
color = Color(0x1A000000),
|
||||
shape = RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomStart = 16.dp, bottomEnd = 16.dp)
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message.text,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
color = if (message.isSentByCurrentUser) Color.White else Color(0xFF0A0A0A)
|
||||
)
|
||||
Text(
|
||||
text = message.timestamp,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = if (message.isSentByCurrentUser) Color(0x99FFFFFF) else Color(0xFF717182)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageInputBar(
|
||||
messageText: String,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onSendClick: () -> Unit
|
||||
) {
|
||||
val isMultiline = messageText.contains("\n") || messageText.length > 40
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().imePadding(),
|
||||
color = Color.White,
|
||||
shadowElevation = 1.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
// TEXT INPUT
|
||||
OutlinedTextField(
|
||||
value = messageText,
|
||||
onValueChange = onMessageChange,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(
|
||||
min = if (isMultiline) 44.dp else 36.dp,
|
||||
max = 120.dp
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Type a message",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF717182)
|
||||
)
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontSize = 14.sp,
|
||||
color = Color.Black // ✅ typed text black
|
||||
),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedBorderColor = Color(0x1A000000),
|
||||
focusedBorderColor = Color(0x33000000),
|
||||
unfocusedContainerColor = Color.White,
|
||||
focusedContainerColor = Color.White,
|
||||
cursorColor = Color.Black
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
singleLine = false,
|
||||
maxLines = 4
|
||||
)
|
||||
|
||||
// ATTACHMENT ICON
|
||||
IconButton(
|
||||
onClick = { },
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Attachment,
|
||||
contentDescription = "Attachment",
|
||||
tint = Color(0xFF717182),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// VOICE BUTTON
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(Color(0xFF0A0A0A), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { },
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardVoice,
|
||||
contentDescription = "Voice",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SEND BUTTON
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(Color(0xFF0A0A0A), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onSendClick,
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
package com.example.livingai_lg.ui.screens.chat
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.farmmarketplace.ui.screens.ContactsBottomNav
|
||||
import com.example.farmmarketplace.ui.screens.ContactsTab
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||
|
||||
data class ChatPreview(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val lastMessage: String,
|
||||
val timestamp: String,
|
||||
val isOnline: Boolean = false,
|
||||
val unreadCount: Int = 0
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChatsScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onNewChatClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {},
|
||||
onChatItemClick: (String) -> Unit = {},
|
||||
onTabClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
val chatList = listOf(
|
||||
ChatPreview(
|
||||
id = "1",
|
||||
name = "Farmer Kumar",
|
||||
initials = "FK",
|
||||
lastMessage = "The cows are healthy and ready for viewing",
|
||||
timestamp = "Today, 2:30 PM",
|
||||
isOnline = true,
|
||||
unreadCount = 2
|
||||
),
|
||||
ChatPreview(
|
||||
id = "2",
|
||||
name = "Seller Raj",
|
||||
initials = "SR",
|
||||
lastMessage = "You: Can you send more photos?",
|
||||
timestamp = "Today, 11:45 AM",
|
||||
isOnline = true,
|
||||
unreadCount = 0
|
||||
),
|
||||
ChatPreview(
|
||||
id = "3",
|
||||
name = "Buyer Priya",
|
||||
initials = "BP",
|
||||
lastMessage = "What's the best time to visit?",
|
||||
timestamp = "Yesterday, 8:15 PM",
|
||||
isOnline = false,
|
||||
unreadCount = 1
|
||||
),
|
||||
ChatPreview(
|
||||
id = "4",
|
||||
name = "Seller 1",
|
||||
initials = "S1",
|
||||
lastMessage = "You: Thanks for the information",
|
||||
timestamp = "Yesterday, 5:30 PM",
|
||||
isOnline = true,
|
||||
unreadCount = 0
|
||||
),
|
||||
ChatPreview(
|
||||
id = "5",
|
||||
name = "Veterinarian",
|
||||
initials = "V",
|
||||
lastMessage = "The animal health check is complete",
|
||||
timestamp = "2 days ago",
|
||||
isOnline = false,
|
||||
unreadCount = 0
|
||||
),
|
||||
ChatPreview(
|
||||
id = "6",
|
||||
name = "Market Vendor",
|
||||
initials = "MV",
|
||||
lastMessage = "You: Available this weekend?",
|
||||
timestamp = "3 days ago",
|
||||
isOnline = false,
|
||||
unreadCount = 0
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = chatBottomNavItems,
|
||||
currentItem = "Chats",
|
||||
onBottomNavItemClick = onTabClick,
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||
) {
|
||||
ChatsHeader(
|
||||
onBackClick = onBackClick,
|
||||
onNewChatClick = onNewChatClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(chatList) { chat ->
|
||||
ChatListItem(
|
||||
chat = chat,
|
||||
onClick = { onChatItemClick(chat.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactsBottomNav(
|
||||
// currentTab = ContactsTab.CHATS,
|
||||
// onTabClick = onTabClick
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatsHeader(
|
||||
onBackClick: () -> Unit,
|
||||
onNewChatClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Chats",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 42.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "New Chat",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onNewChatClick() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMenuClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListItem(
|
||||
chat: ChatPreview,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(76.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8), RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = chat.initials,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 21.sp
|
||||
)
|
||||
|
||||
if (chat.isOnline) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(12.dp)
|
||||
.background(Color(0xFF00A63E), CircleShape)
|
||||
.border(2.dp, Color.White, CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = chat.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = chat.lastMessage,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = chat.timestamp,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
|
||||
if (chat.unreadCount > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(Color(0xFF155DFC), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = chat.unreadCount.toString(),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
package com.example.farmmarketplace.ui.screens
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.automirrored.filled.Message
|
||||
import androidx.compose.material.icons.automirrored.outlined.Message
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.Contacts
|
||||
import androidx.compose.material.icons.filled.Message
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.outlined.Phone
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.layout.BottomNavScaffold
|
||||
import com.example.livingai_lg.ui.models.chatBottomNavItems
|
||||
import com.example.livingai_lg.ui.models.mainBottomNavItems
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
|
||||
data class Contact(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val initials: String,
|
||||
val status: String,
|
||||
val isOnline: Boolean = false,
|
||||
val phoneNumber: String? = null
|
||||
)
|
||||
|
||||
enum class ContactsTab {
|
||||
CONTACTS,
|
||||
CALLS,
|
||||
CHATS
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsScreen(
|
||||
onBackClick: () -> Unit = {},
|
||||
onSearchClick: () -> Unit = {},
|
||||
onMenuClick: () -> Unit = {},
|
||||
onContactClick: (String) -> Unit = {},
|
||||
onCallClick: (String) -> Unit = {},
|
||||
onMessageClick: (String) -> Unit = {},
|
||||
onTabClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
val contacts = listOf(
|
||||
Contact(
|
||||
id = "1",
|
||||
name = "Farmer Kumar",
|
||||
initials = "FK",
|
||||
status = "Online",
|
||||
isOnline = true
|
||||
),
|
||||
Contact(
|
||||
id = "2",
|
||||
name = "Seller Raj",
|
||||
initials = "SR",
|
||||
status = "+91 98765 43211",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43211"
|
||||
),
|
||||
Contact(
|
||||
id = "3",
|
||||
name = "Buyer Priya",
|
||||
initials = "BP",
|
||||
status = "Online",
|
||||
isOnline = true
|
||||
),
|
||||
Contact(
|
||||
id = "4",
|
||||
name = "Seller 1",
|
||||
initials = "S1",
|
||||
status = "+91 98765 43213",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43213"
|
||||
),
|
||||
Contact(
|
||||
id = "5",
|
||||
name = "Veterinarian",
|
||||
initials = "V",
|
||||
status = "Online",
|
||||
isOnline = true
|
||||
),
|
||||
Contact(
|
||||
id = "6",
|
||||
name = "Farm Supply",
|
||||
initials = "FS",
|
||||
status = "+91 98765 43215",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43215"
|
||||
),
|
||||
Contact(
|
||||
id = "7",
|
||||
name = "Transport Co.",
|
||||
initials = "TC",
|
||||
status = "+91 98765 43216",
|
||||
isOnline = false,
|
||||
phoneNumber = "+91 98765 43216"
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
BottomNavScaffold(
|
||||
items = chatBottomNavItems,
|
||||
currentItem = "Contacts",
|
||||
onBottomNavItemClick = onTabClick,
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues)
|
||||
) {
|
||||
ContactsHeader(
|
||||
onBackClick = onBackClick,
|
||||
onSearchClick = onSearchClick,
|
||||
onMenuClick = onMenuClick
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
items(contacts) { contact ->
|
||||
ContactItem(
|
||||
contact = contact,
|
||||
onContactClick = { onContactClick(contact.id) },
|
||||
onCallClick = { onCallClick(contact.id) },
|
||||
onMessageClick = { onMessageClick(contact.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactsBottomNav(
|
||||
// currentTab = ContactsTab.CONTACTS,
|
||||
// onTabClick = onTabClick
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsHeader(
|
||||
onBackClick: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onBackClick() }
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Contacts",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 42.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onSearchClick() }
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMenuClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactItem(
|
||||
contact: Contact,
|
||||
onContactClick: () -> Unit,
|
||||
onCallClick: () -> Unit,
|
||||
onMessageClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(73.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.05f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onContactClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(0xFFE5E7EB), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = contact.initials,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 21.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = contact.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1E2939),
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = (-0.312).sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = contact.status,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF717182),
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onCallClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Phone,
|
||||
contentDescription = "Call",
|
||||
tint = Color(0xFF030213),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onMessageClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Message,
|
||||
contentDescription = "Message",
|
||||
tint = Color(0xFF030213),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsBottomNav(
|
||||
currentTab: ContactsTab,
|
||||
onTabClick: (route: String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(65.dp)
|
||||
.border(
|
||||
width = 1.078.dp,
|
||||
color = Color(0xFF000000).copy(alpha = 0.1f)
|
||||
)
|
||||
.background(Color(0xFFFCFBF8))
|
||||
.padding(horizontal = 32.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
ContactsTabItem(
|
||||
icon = Icons.AutoMirrored.Filled.Chat,
|
||||
label = "Chats",
|
||||
isSelected = currentTab == ContactsTab.CHATS,
|
||||
onClick = { onTabClick(AppScreen.CHATS) }
|
||||
)
|
||||
ContactsTabItem(
|
||||
icon = Icons.Default.Phone,
|
||||
label = "Calls",
|
||||
isSelected = currentTab == ContactsTab.CALLS,
|
||||
onClick = { onTabClick(AppScreen.CALLS) }
|
||||
)
|
||||
ContactsTabItem(
|
||||
icon = Icons.Default.Contacts,
|
||||
label = "Contacts",
|
||||
isSelected = currentTab == ContactsTab.CONTACTS,
|
||||
onClick = { onTabClick(AppScreen.CONTACTS) }
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsTabItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
.padding(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A),
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package com.example.livingai_lg.ui.screens.testing
|
||||
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ApiTestScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Default API endpoint
|
||||
var apiEndpoint by remember {
|
||||
mutableStateOf("https://jsonplaceholder.typicode.com/posts/1")
|
||||
}
|
||||
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var responseText by remember { mutableStateOf<String?>(null) }
|
||||
var errorText by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val client = remember { OkHttpClient() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
) {
|
||||
// Top App Bar
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "API Test",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Black
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.Black
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF7F4EE)
|
||||
)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
|
||||
// API endpoint input
|
||||
OutlinedTextField(
|
||||
value = apiEndpoint,
|
||||
onValueChange = { apiEndpoint = it },
|
||||
label = { Text("API Endpoint") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Call API button
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
responseText = null
|
||||
errorText = null
|
||||
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url(apiEndpoint)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
val body = response.body?.string()
|
||||
Pair(response.code, body)
|
||||
}
|
||||
}
|
||||
|
||||
val (code, body) = result
|
||||
responseText = "Status: $code\n\n$body"
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
errorText = e.toString()
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading
|
||||
) {
|
||||
Text(if (isLoading) "Calling API..." else "Call API")
|
||||
}
|
||||
|
||||
// Result display
|
||||
when {
|
||||
responseText != null -> {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = responseText ?: "",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
errorText != null -> {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color(0xFFFFEBEE),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = errorText ?: "",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFE53935)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.example.livingai_lg.ui.state
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
|
||||
object FilterStore {
|
||||
|
||||
private val _filters = MutableStateFlow(FiltersState())
|
||||
val filters: StateFlow<FiltersState> = _filters
|
||||
|
||||
fun update(block: (FiltersState) -> FiltersState) {
|
||||
_filters.update(block)
|
||||
}
|
||||
|
||||
fun set(newState: FiltersState) {
|
||||
_filters.value = newState
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_filters.value = FiltersState()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai_lg.ui.state
|
||||
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
data class WishlistEntry(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val name: String,
|
||||
val filters: FiltersState,
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
object WishlistStore {
|
||||
|
||||
private val _wishlist =
|
||||
MutableStateFlow<List<WishlistEntry>>(emptyList())
|
||||
|
||||
val wishlist: StateFlow<List<WishlistEntry>> = _wishlist
|
||||
|
||||
fun add(entry: WishlistEntry) {
|
||||
_wishlist.value = _wishlist.value + entry
|
||||
}
|
||||
|
||||
fun remove(id: String) {
|
||||
_wishlist.value = _wishlist.value.filterNot { it.id == id }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_wishlist.value = emptyList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai_lg.ui.theme
|
||||
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
object AppTypography {
|
||||
val Display = 32.sp
|
||||
val Title = 20.sp
|
||||
val Body = 16.sp
|
||||
val BodySmall = 14.sp
|
||||
val Caption = 12.sp
|
||||
val Micro = 10.sp
|
||||
}
|
||||
|
|
@ -2,25 +2,57 @@ package com.example.livingai_lg.ui.theme
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue