server selection
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt)
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
@@ -71,6 +72,7 @@ android {
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.14"
|
||||
}
|
||||
buildToolsVersion = "33.0.1"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -92,12 +94,21 @@ dependencies {
|
||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.5.4")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
|
||||
implementation(libs.androidx.compose.ui.tooling)
|
||||
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
||||
implementation("androidx.navigation:navigation-compose:2.7.7")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
|
||||
// Network
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
|
||||
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
|
||||
implementation("com.squareup.moshi:moshi:1.15.0")
|
||||
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package kr.smartsoltech.wellshe
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import kr.smartsoltech.wellshe.emergency.domain.repository.EmergencyRepository
|
||||
import kr.smartsoltech.wellshe.data.preferences.ServerPreferences
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
@@ -11,12 +13,22 @@ import dagger.hilt.components.SingletonComponent
|
||||
@HiltAndroidApp
|
||||
class WellSheApplication : Application() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WellSheApplication"
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface EmergencyRepositoryEntryPoint {
|
||||
fun emergencyRepository(): EmergencyRepository
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ServerPreferencesEntryPoint {
|
||||
fun serverPreferences(): ServerPreferences
|
||||
}
|
||||
|
||||
val emergencyRepository: EmergencyRepository by lazy {
|
||||
val hiltEntryPoint = EntryPointAccessors.fromApplication(
|
||||
this,
|
||||
@@ -25,8 +37,24 @@ class WellSheApplication : Application() {
|
||||
hiltEntryPoint.emergencyRepository()
|
||||
}
|
||||
|
||||
private val serverPreferences: ServerPreferences by lazy {
|
||||
val hiltEntryPoint = EntryPointAccessors.fromApplication(
|
||||
this,
|
||||
ServerPreferencesEntryPoint::class.java
|
||||
)
|
||||
hiltEntryPoint.serverPreferences()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Hilt will inject dependencies after onCreate
|
||||
Log.d(TAG, "WellShe Application starting...")
|
||||
|
||||
// Логируем текущие настройки сервера при запуске
|
||||
try {
|
||||
serverPreferences.debugSettings()
|
||||
Log.d(TAG, "Application started successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during app startup", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.smartsoltech.wellshe.data.api
|
||||
|
||||
import kr.smartsoltech.wellshe.data.model.ServerHealthResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface HealthApi {
|
||||
@GET("api/v1/health")
|
||||
suspend fun getHealth(): Response<ServerHealthResponse>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package kr.smartsoltech.wellshe.data.model
|
||||
|
||||
data class ServerHealthResponse(
|
||||
val status: String,
|
||||
val timestamp: String? = null,
|
||||
val version: String? = null
|
||||
)
|
||||
|
||||
data class ServerStatus(
|
||||
val url: String,
|
||||
val isHealthy: Boolean,
|
||||
val pingMs: Long,
|
||||
val status: HealthStatus,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
enum class HealthStatus {
|
||||
EXCELLENT, // < 10ms, зеленый
|
||||
GOOD, // 10-200ms, желтый
|
||||
POOR, // 200-600ms, оранжевый
|
||||
BAD, // 600ms+, красный
|
||||
OFFLINE // недоступен, серый
|
||||
}
|
||||
|
||||
fun Long.toHealthStatus(): HealthStatus {
|
||||
return when {
|
||||
this < 10 -> HealthStatus.EXCELLENT
|
||||
this < 200 -> HealthStatus.GOOD
|
||||
this < 600 -> HealthStatus.POOR
|
||||
else -> HealthStatus.BAD
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import kr.smartsoltech.wellshe.data.preferences.ServerPreferences
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
@@ -11,22 +12,24 @@ import java.util.concurrent.TimeUnit
|
||||
/**
|
||||
* Класс для настройки и создания API-клиентов
|
||||
*/
|
||||
object ApiClient {
|
||||
private const val BASE_URL = "http://192.168.0.112:8000/api/v1/"
|
||||
private const val CONNECT_TIMEOUT = 15L
|
||||
private const val READ_TIMEOUT = 15L
|
||||
private const val WRITE_TIMEOUT = 15L
|
||||
class ApiClient(private val serverPreferences: ServerPreferences) {
|
||||
private val defaultBaseUrl = "http://192.168.0.112:8000/api/v1/"
|
||||
private val connectTimeout = 15L
|
||||
private val readTimeout = 15L
|
||||
private val writeTimeout = 15L
|
||||
|
||||
/**
|
||||
* Создает экземпляр Retrofit с настройками для работы с API
|
||||
*/
|
||||
private fun createRetrofit(baseUrl: String = BASE_URL): Retrofit {
|
||||
private fun createRetrofit(baseUrl: String? = null): Retrofit {
|
||||
val actualBaseUrl = baseUrl ?: serverPreferences.getApiBaseUrl()
|
||||
|
||||
val gson: Gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.baseUrl(actualBaseUrl)
|
||||
.client(createOkHttpClient())
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
@@ -42,9 +45,9 @@ object ApiClient {
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
|
||||
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
|
||||
.readTimeout(readTimeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(writeTimeout, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import com.google.gson.Gson
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RetrofitFactory @Inject constructor(
|
||||
private val gson: Gson,
|
||||
private val authTokenRepository: AuthTokenRepository
|
||||
) {
|
||||
|
||||
fun create(baseUrl: String): Retrofit {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
|
||||
val authInterceptor = AuthInterceptor(authTokenRepository)
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||
import kr.smartsoltech.wellshe.data.preferences.ServerPreferences
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Inject
|
||||
|
||||
@ActivityRetainedScoped
|
||||
class RetrofitProvider @Inject constructor(
|
||||
private val serverPreferences: ServerPreferences,
|
||||
private val retrofitFactory: RetrofitFactory
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "RetrofitProvider"
|
||||
}
|
||||
|
||||
private var currentServerUrl: String? = null
|
||||
private var currentRetrofit: Retrofit? = null
|
||||
|
||||
fun getRetrofit(): Retrofit {
|
||||
val serverUrl = serverPreferences.getApiBaseUrl()
|
||||
Log.d(TAG, "Getting Retrofit for serverUrl: $serverUrl")
|
||||
|
||||
if (currentRetrofit == null || currentServerUrl != serverUrl) {
|
||||
Log.d(TAG, "Creating new Retrofit instance. Old URL: $currentServerUrl, New URL: $serverUrl")
|
||||
currentServerUrl = serverUrl
|
||||
currentRetrofit = retrofitFactory.create(serverUrl)
|
||||
Log.d(TAG, "Retrofit instance created successfully with baseUrl: $serverUrl")
|
||||
|
||||
// Показываем настройки для отладки
|
||||
serverPreferences.debugSettings()
|
||||
} else {
|
||||
Log.d(TAG, "Reusing existing Retrofit instance with baseUrl: $serverUrl")
|
||||
}
|
||||
|
||||
return currentRetrofit!!
|
||||
}
|
||||
|
||||
fun recreateRetrofit() {
|
||||
Log.d(TAG, "Forcing Retrofit recreation. Current URL: $currentServerUrl")
|
||||
currentRetrofit = null
|
||||
currentServerUrl = null
|
||||
Log.d(TAG, "Retrofit instance cleared, will be recreated on next access")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package kr.smartsoltech.wellshe.data.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ServerPreferences @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ServerPreferences"
|
||||
private const val PREF_NAME = "server_preferences"
|
||||
private const val KEY_SERVER_URL = "server_url"
|
||||
// Используем локальный IP для разработки - можно легко изменить через UI
|
||||
private const val DEFAULT_SERVER_URL = "http://10.0.2.2:8000"
|
||||
}
|
||||
|
||||
fun getServerUrl(): String {
|
||||
val url = sharedPreferences.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
|
||||
Log.d(TAG, "Getting server URL: $url")
|
||||
return url
|
||||
}
|
||||
|
||||
fun getApiBaseUrl(): String {
|
||||
val serverUrl = getServerUrl()
|
||||
val apiUrl = if (serverUrl.endsWith("/")) {
|
||||
"${serverUrl}api/v1/"
|
||||
} else {
|
||||
"$serverUrl/api/v1/"
|
||||
}
|
||||
Log.d(TAG, "Getting API base URL: $apiUrl")
|
||||
return apiUrl
|
||||
}
|
||||
|
||||
fun setServerUrl(url: String) {
|
||||
Log.d(TAG, "Setting server URL: $url")
|
||||
val success = sharedPreferences.edit()
|
||||
.putString(KEY_SERVER_URL, url)
|
||||
.commit() // Используем commit() вместо apply() для синхронного сохранения
|
||||
|
||||
if (success) {
|
||||
Log.d(TAG, "Server URL saved successfully: $url")
|
||||
// Проверяем, что значение действительно сохранилось
|
||||
val savedUrl = sharedPreferences.getString(KEY_SERVER_URL, "NOT_FOUND")
|
||||
Log.d(TAG, "Verification - saved URL: $savedUrl")
|
||||
} else {
|
||||
Log.e(TAG, "Failed to save server URL: $url")
|
||||
}
|
||||
}
|
||||
|
||||
// Метод для получения предложенных серверов
|
||||
fun getSuggestedServers(): List<String> {
|
||||
return listOf(
|
||||
"http://10.0.2.2:8000", // Android Emulator localhost
|
||||
"http://192.168.0.112:8000", // Локальная сеть
|
||||
"http://localhost:8000", // Localhost
|
||||
"https://api.wellshe.example.com" // Пример продакшн сервера
|
||||
)
|
||||
}
|
||||
|
||||
// Метод для отладки - показывает все сохраненные настройки
|
||||
fun debugSettings() {
|
||||
Log.d(TAG, "=== Debug Server Settings ===")
|
||||
Log.d(TAG, "Preferences file: $PREF_NAME")
|
||||
Log.d(TAG, "Current server URL: ${getServerUrl()}")
|
||||
Log.d(TAG, "Default server URL: $DEFAULT_SERVER_URL")
|
||||
Log.d(TAG, "All preferences: ${sharedPreferences.all}")
|
||||
Log.d(TAG, "===============================")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package kr.smartsoltech.wellshe.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import kr.smartsoltech.wellshe.data.api.HealthApi
|
||||
import kr.smartsoltech.wellshe.data.model.HealthStatus
|
||||
import kr.smartsoltech.wellshe.data.model.ServerStatus
|
||||
import kr.smartsoltech.wellshe.data.model.toHealthStatus
|
||||
import kr.smartsoltech.wellshe.data.network.RetrofitFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ServerHealthRepository @Inject constructor(
|
||||
private val retrofitFactory: RetrofitFactory
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "ServerHealthRepository"
|
||||
private const val HEALTH_CHECK_TIMEOUT_MS = 5000L
|
||||
}
|
||||
|
||||
suspend fun checkServerHealth(serverUrl: String): ServerStatus = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Checking health for server: $serverUrl")
|
||||
|
||||
try {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
// Создаем отдельный Retrofit для health check'а
|
||||
val baseUrl = if (serverUrl.endsWith("/")) serverUrl else "$serverUrl/"
|
||||
val retrofit = retrofitFactory.create(baseUrl)
|
||||
val healthApi = retrofit.create(HealthApi::class.java)
|
||||
|
||||
// Выполняем запрос с таймаутом
|
||||
val response = withTimeoutOrNull(HEALTH_CHECK_TIMEOUT_MS) {
|
||||
healthApi.getHealth()
|
||||
}
|
||||
|
||||
val endTime = System.currentTimeMillis()
|
||||
val pingMs = endTime - startTime
|
||||
|
||||
Log.d(TAG, "Health check for $serverUrl completed in ${pingMs}ms")
|
||||
|
||||
if (response != null && response.isSuccessful) {
|
||||
val healthResponse = response.body()
|
||||
val isHealthy = healthResponse?.status?.lowercase() == "healthy" ||
|
||||
healthResponse?.status?.lowercase() == "ok"
|
||||
|
||||
Log.d(TAG, "Server $serverUrl is ${if (isHealthy) "healthy" else "unhealthy"}, ping: ${pingMs}ms")
|
||||
|
||||
ServerStatus(
|
||||
url = serverUrl,
|
||||
isHealthy = isHealthy,
|
||||
pingMs = pingMs,
|
||||
status = if (isHealthy) pingMs.toHealthStatus() else HealthStatus.POOR
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Health check failed for $serverUrl: HTTP ${response?.code()}")
|
||||
ServerStatus(
|
||||
url = serverUrl,
|
||||
isHealthy = false,
|
||||
pingMs = pingMs,
|
||||
status = HealthStatus.OFFLINE,
|
||||
error = "HTTP ${response?.code() ?: "timeout"}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking health for $serverUrl", e)
|
||||
ServerStatus(
|
||||
url = serverUrl,
|
||||
isHealthy = false,
|
||||
pingMs = HEALTH_CHECK_TIMEOUT_MS,
|
||||
status = HealthStatus.OFFLINE,
|
||||
error = e.message ?: "Connection failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkMultipleServers(serverUrls: List<String>): List<ServerStatus> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Checking health for ${serverUrls.size} servers")
|
||||
|
||||
val deferredResults = serverUrls.map { url ->
|
||||
async { checkServerHealth(url) }
|
||||
}
|
||||
|
||||
val results = deferredResults.awaitAll()
|
||||
|
||||
Log.d(TAG, "Health check completed for all servers")
|
||||
results.forEach { status ->
|
||||
Log.d(TAG, "Server ${status.url}: ${status.status} (${status.pingMs}ms)")
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import kr.smartsoltech.wellshe.data.network.AuthService
|
||||
import kr.smartsoltech.wellshe.data.network.RetrofitFactory
|
||||
import kr.smartsoltech.wellshe.data.network.RetrofitProvider
|
||||
import kr.smartsoltech.wellshe.data.preferences.ServerPreferences
|
||||
import kr.smartsoltech.wellshe.data.repository.AuthRepository
|
||||
import kr.smartsoltech.wellshe.data.storage.TokenManager
|
||||
import kr.smartsoltech.wellshe.domain.auth.GetUserProfileUseCase
|
||||
@@ -15,7 +18,6 @@ import kr.smartsoltech.wellshe.domain.auth.LoginUseCase
|
||||
import kr.smartsoltech.wellshe.domain.auth.LogoutUseCase
|
||||
import kr.smartsoltech.wellshe.domain.auth.RegisterUseCase
|
||||
import kr.smartsoltech.wellshe.domain.auth.RefreshTokenUseCase
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -36,8 +38,17 @@ object AuthModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthService(retrofit: Retrofit): AuthService {
|
||||
return retrofit.create(AuthService::class.java)
|
||||
fun provideRetrofitProvider(
|
||||
serverPreferences: ServerPreferences,
|
||||
retrofitFactory: RetrofitFactory
|
||||
): RetrofitProvider {
|
||||
return RetrofitProvider(serverPreferences, retrofitFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideAuthService(retrofitProvider: RetrofitProvider): AuthService {
|
||||
// Каждый раз получаем актуальный Retrofit, который может иметь новый baseUrl
|
||||
return retrofitProvider.getRetrofit().create(AuthService::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -6,22 +6,16 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kr.smartsoltech.wellshe.BuildConfig
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import kr.smartsoltech.wellshe.data.network.ApiClient
|
||||
import kr.smartsoltech.wellshe.data.network.AuthInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kr.smartsoltech.wellshe.data.network.RetrofitFactory
|
||||
import kr.smartsoltech.wellshe.data.preferences.ServerPreferences
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
private const val CONNECT_TIMEOUT = 15L
|
||||
private const val READ_TIMEOUT = 15L
|
||||
private const val WRITE_TIMEOUT = 15L
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@@ -39,20 +33,16 @@ object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(gson: Gson, authInterceptor: AuthInterceptor): Retrofit {
|
||||
val client = OkHttpClient.Builder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
})
|
||||
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
|
||||
.build()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.client(client)
|
||||
.build()
|
||||
fun provideRetrofitFactory(
|
||||
gson: Gson,
|
||||
authTokenRepository: AuthTokenRepository
|
||||
): RetrofitFactory {
|
||||
return RetrofitFactory(gson, authTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiClient(serverPreferences: ServerPreferences): ApiClient {
|
||||
return ApiClient(serverPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kr.smartsoltech.wellshe.data.model.ServerStatus
|
||||
import kr.smartsoltech.wellshe.data.network.RetrofitProvider
|
||||
import kr.smartsoltech.wellshe.data.preferences.ServerPreferences
|
||||
import kr.smartsoltech.wellshe.data.repository.ServerHealthRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ServerSettingsViewModel @Inject constructor(
|
||||
private val serverPreferences: ServerPreferences,
|
||||
private val retrofitProvider: RetrofitProvider,
|
||||
private val serverHealthRepository: ServerHealthRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ServerSettingsViewModel"
|
||||
}
|
||||
|
||||
private val _serverUrl = MutableStateFlow("")
|
||||
val serverUrl: StateFlow<String> = _serverUrl
|
||||
|
||||
private val _suggestedServers = MutableStateFlow<List<String>>(emptyList())
|
||||
val suggestedServers: StateFlow<List<String>> = _suggestedServers
|
||||
|
||||
private val _serverStatuses = MutableStateFlow<Map<String, ServerStatus>>(emptyMap())
|
||||
val serverStatuses: StateFlow<Map<String, ServerStatus>> = _serverStatuses
|
||||
|
||||
private val _isCheckingHealth = MutableStateFlow(false)
|
||||
val isCheckingHealth: StateFlow<Boolean> = _isCheckingHealth
|
||||
|
||||
init {
|
||||
Log.d(TAG, "ServerSettingsViewModel initialized")
|
||||
loadServerUrl()
|
||||
loadSuggestedServers()
|
||||
checkServersHealth()
|
||||
}
|
||||
|
||||
private fun loadServerUrl() {
|
||||
viewModelScope.launch {
|
||||
_serverUrl.value = serverPreferences.getServerUrl()
|
||||
Log.d(TAG, "Loaded server URL: ${_serverUrl.value}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSuggestedServers() {
|
||||
viewModelScope.launch {
|
||||
_suggestedServers.value = serverPreferences.getSuggestedServers()
|
||||
Log.d(TAG, "Loaded suggested servers: ${_suggestedServers.value}")
|
||||
}
|
||||
}
|
||||
|
||||
fun saveServerUrl(url: String) {
|
||||
viewModelScope.launch {
|
||||
val trimmedUrl = url.trim()
|
||||
Log.d(TAG, "Saving server URL: $trimmedUrl")
|
||||
serverPreferences.setServerUrl(trimmedUrl)
|
||||
_serverUrl.value = trimmedUrl
|
||||
// Пересоздаем Retrofit с новым URL
|
||||
retrofitProvider.recreateRetrofit()
|
||||
Log.d(TAG, "Server URL saved and Retrofit recreated")
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentApiBaseUrl(): String {
|
||||
return serverPreferences.getApiBaseUrl()
|
||||
}
|
||||
|
||||
fun checkServersHealth() {
|
||||
if (_isCheckingHealth.value) {
|
||||
Log.d(TAG, "Health check already in progress, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting health check for all servers")
|
||||
_isCheckingHealth.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val servers = _suggestedServers.value
|
||||
Log.d(TAG, "Checking health for servers: $servers")
|
||||
|
||||
val healthResults = serverHealthRepository.checkMultipleServers(servers)
|
||||
|
||||
val statusMap = healthResults.associateBy { it.url }
|
||||
_serverStatuses.value = statusMap
|
||||
|
||||
Log.d(TAG, "Health check completed. Results: ${statusMap.values.map { "${it.url}: ${it.status}" }}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during health check", e)
|
||||
} finally {
|
||||
_isCheckingHealth.value = false
|
||||
Log.d(TAG, "Health check finished")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getServerStatus(url: String): ServerStatus? {
|
||||
return _serverStatuses.value[url]
|
||||
}
|
||||
|
||||
fun refreshServerHealth() {
|
||||
Log.d(TAG, "Manual refresh of server health requested")
|
||||
checkServersHealth()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
@@ -22,24 +23,28 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
import kr.smartsoltech.wellshe.ui.auth.ServerSettingsViewModel
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onNavigateToRegister: () -> Unit,
|
||||
onLoginSuccess: () -> Unit,
|
||||
viewModel: AuthViewModel = hiltViewModel()
|
||||
viewModel: AuthViewModel = hiltViewModel(),
|
||||
serverSettingsViewModel: ServerSettingsViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val authState by viewModel.authState.observeAsState()
|
||||
val isLoading by viewModel.isLoading.observeAsState()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val serverUrl by serverSettingsViewModel.serverUrl.collectAsStateWithLifecycle()
|
||||
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var isFormValid by remember { mutableStateOf(false) }
|
||||
var showServerSettings by remember { mutableStateOf(false) }
|
||||
|
||||
// FocusRequester для переключения фокуса между полями
|
||||
val passwordFocusRequester = remember { FocusRequester() }
|
||||
@@ -63,7 +68,23 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { paddingValues ->
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = { showServerSettings = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "Настройки сервера"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -147,6 +168,29 @@ fun LoginScreen(
|
||||
Text("Создать новый аккаунт")
|
||||
}
|
||||
|
||||
// Показываем текущий сервер
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Сервер:",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = serverUrl.ifEmpty { "Загрузка..." },
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (authState is AuthViewModel.AuthState.AuthError) {
|
||||
Text(
|
||||
text = (authState as AuthViewModel.AuthState.AuthError).message,
|
||||
@@ -156,4 +200,16 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Диалог настроек сервера
|
||||
if (showServerSettings) {
|
||||
ServerSettingsDialog(
|
||||
onDismiss = { showServerSettings = false },
|
||||
onSave = { newUrl ->
|
||||
serverSettingsViewModel.saveServerUrl(newUrl)
|
||||
showServerSettings = false
|
||||
},
|
||||
currentServerUrl = serverUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth.compose
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import kr.smartsoltech.wellshe.ui.auth.ServerSettingsViewModel
|
||||
|
||||
@Composable
|
||||
fun ServerSettingsDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String) -> Unit,
|
||||
currentServerUrl: String,
|
||||
viewModel: ServerSettingsViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val suggestedServers by viewModel.suggestedServers.collectAsState()
|
||||
val serverStatuses by viewModel.serverStatuses.collectAsState()
|
||||
val isCheckingHealth by viewModel.isCheckingHealth.collectAsState()
|
||||
|
||||
var serverUrl by remember { mutableStateOf(currentServerUrl) }
|
||||
|
||||
// Валидация URL
|
||||
val isValid = remember(serverUrl) {
|
||||
serverUrl.isNotBlank() &&
|
||||
(serverUrl.startsWith("http://") || serverUrl.startsWith("https://"))
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Заголовок с кнопкой обновления
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Настройки сервера",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { viewModel.refreshServerHealth() },
|
||||
enabled = !isCheckingHealth
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = "Обновить статус серверов"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Текущий сервер:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = currentServerUrl,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Заголовок предустановленных серверов
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Предустановленные серверы:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
if (isCheckingHealth) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(12.dp),
|
||||
strokeWidth = 1.dp
|
||||
)
|
||||
Text(
|
||||
text = "Проверка...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Список предустановленных серверов
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 200.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(suggestedServers) { server ->
|
||||
ServerStatusRow(
|
||||
server = server,
|
||||
serverStatus = serverStatuses[server],
|
||||
isChecking = isCheckingHealth,
|
||||
isSelected = server == serverUrl,
|
||||
onClick = {
|
||||
serverUrl = server
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Поле для ввода пользовательского URL
|
||||
Column {
|
||||
Text(
|
||||
text = "Или введите свой URL:",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = serverUrl,
|
||||
onValueChange = { serverUrl = it },
|
||||
label = { Text("URL сервера") },
|
||||
placeholder = { Text("http://192.168.1.100:8000") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
isError = serverUrl.isNotBlank() && !isValid,
|
||||
supportingText = {
|
||||
if (serverUrl.isNotBlank() && !isValid) {
|
||||
Text(
|
||||
text = "URL должен начинаться с http:// или https://",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Кнопки
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Отмена")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (isValid) {
|
||||
onSave(serverUrl.trim())
|
||||
Toast.makeText(
|
||||
context,
|
||||
"✅ Сервер изменён!\nСтарый: $currentServerUrl\nНовый: ${serverUrl.trim()}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
},
|
||||
enabled = isValid,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Сохранить")
|
||||
}
|
||||
}
|
||||
|
||||
// Легенда статусов
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Статусы серверов:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
val statuses = listOf(
|
||||
"🟢 < 10мс - Отлично",
|
||||
"🟡 10-200мс - Хорошо",
|
||||
"🟠 200-600мс - Медленно",
|
||||
"🔴 600мс+ - Очень медленно",
|
||||
"⚫ Недоступен"
|
||||
)
|
||||
|
||||
statuses.forEach { status ->
|
||||
Text(
|
||||
text = status,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kr.smartsoltech.wellshe.data.model.HealthStatus
|
||||
import kr.smartsoltech.wellshe.data.model.ServerStatus
|
||||
|
||||
@Composable
|
||||
fun ServerStatusIndicator(
|
||||
serverStatus: ServerStatus?,
|
||||
isChecking: Boolean = false
|
||||
) {
|
||||
if (isChecking) {
|
||||
// Показываем индикатор загрузки
|
||||
Box(
|
||||
modifier = Modifier.size(12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(10.dp),
|
||||
strokeWidth = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else if (serverStatus != null) {
|
||||
// Показываем цветной индикатор
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(getStatusColor(serverStatus.status))
|
||||
)
|
||||
} else {
|
||||
// Показываем серый индикатор если статус неизвестен
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Gray)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServerStatusRow(
|
||||
server: String,
|
||||
serverStatus: ServerStatus?,
|
||||
isChecking: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
),
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = server,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
|
||||
if (serverStatus != null && !isChecking) {
|
||||
Text(
|
||||
text = getStatusText(serverStatus),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 10.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ServerStatusIndicator(
|
||||
serverStatus = serverStatus,
|
||||
isChecking = isChecking
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusColor(status: HealthStatus): Color {
|
||||
return when (status) {
|
||||
HealthStatus.EXCELLENT -> Color(0xFF4CAF50) // Зеленый
|
||||
HealthStatus.GOOD -> Color(0xFFFFC107) // Желтый
|
||||
HealthStatus.POOR -> Color(0xFFFF9800) // Оранжевый
|
||||
HealthStatus.BAD -> Color(0xFFF44336) // Красный
|
||||
HealthStatus.OFFLINE -> Color(0xFF9E9E9E) // Серый
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusText(serverStatus: ServerStatus): String {
|
||||
return if (serverStatus.isHealthy) {
|
||||
"${serverStatus.pingMs}ms • ${getStatusLabel(serverStatus.status)}"
|
||||
} else {
|
||||
serverStatus.error ?: "Недоступен"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusLabel(status: HealthStatus): String {
|
||||
return when (status) {
|
||||
HealthStatus.EXCELLENT -> "Отлично"
|
||||
HealthStatus.GOOD -> "Хорошо"
|
||||
HealthStatus.POOR -> "Медленно"
|
||||
HealthStatus.BAD -> "Очень медленно"
|
||||
HealthStatus.OFFLINE -> "Недоступен"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth.compose
|
||||
|
||||
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.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kr.smartsoltech.wellshe.data.model.HealthStatus
|
||||
import kr.smartsoltech.wellshe.data.model.ServerStatus
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ServerStatusIndicatorPreview() {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text("Индикаторы статуса серверов:", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
// Отличный статус
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ServerStatusIndicator(
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://example.com",
|
||||
isHealthy = true,
|
||||
pingMs = 5,
|
||||
status = HealthStatus.EXCELLENT
|
||||
)
|
||||
)
|
||||
Text("Отлично (5мс)")
|
||||
}
|
||||
|
||||
// Хороший статус
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ServerStatusIndicator(
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://example.com",
|
||||
isHealthy = true,
|
||||
pingMs = 100,
|
||||
status = HealthStatus.GOOD
|
||||
)
|
||||
)
|
||||
Text("Хорошо (100мс)")
|
||||
}
|
||||
|
||||
// Медленный статус
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ServerStatusIndicator(
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://example.com",
|
||||
isHealthy = true,
|
||||
pingMs = 400,
|
||||
status = HealthStatus.POOR
|
||||
)
|
||||
)
|
||||
Text("Медленно (400мс)")
|
||||
}
|
||||
|
||||
// Очень медленный статус
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ServerStatusIndicator(
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://example.com",
|
||||
isHealthy = false,
|
||||
pingMs = 800,
|
||||
status = HealthStatus.BAD
|
||||
)
|
||||
)
|
||||
Text("Очень медленно (800мс)")
|
||||
}
|
||||
|
||||
// Недоступен
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ServerStatusIndicator(
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://example.com",
|
||||
isHealthy = false,
|
||||
pingMs = 5000,
|
||||
status = HealthStatus.OFFLINE,
|
||||
error = "Connection failed"
|
||||
)
|
||||
)
|
||||
Text("Недоступен")
|
||||
}
|
||||
|
||||
// Проверяется
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ServerStatusIndicator(
|
||||
serverStatus = null,
|
||||
isChecking = true
|
||||
)
|
||||
Text("Проверяется...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ServerStatusRowPreview() {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text("Строки серверов:", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
ServerStatusRow(
|
||||
server = "http://10.0.2.2:8000",
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://10.0.2.2:8000",
|
||||
isHealthy = true,
|
||||
pingMs = 8,
|
||||
status = HealthStatus.EXCELLENT
|
||||
),
|
||||
isChecking = false,
|
||||
isSelected = true,
|
||||
onClick = {}
|
||||
)
|
||||
|
||||
ServerStatusRow(
|
||||
server = "http://192.168.0.112:8000",
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://192.168.0.112:8000",
|
||||
isHealthy = true,
|
||||
pingMs = 150,
|
||||
status = HealthStatus.GOOD
|
||||
),
|
||||
isChecking = false,
|
||||
isSelected = false,
|
||||
onClick = {}
|
||||
)
|
||||
|
||||
ServerStatusRow(
|
||||
server = "http://slow-server.com:8000",
|
||||
serverStatus = ServerStatus(
|
||||
url = "http://slow-server.com:8000",
|
||||
isHealthy = false,
|
||||
pingMs = 5000,
|
||||
status = HealthStatus.OFFLINE,
|
||||
error = "Connection timeout"
|
||||
),
|
||||
isChecking = false,
|
||||
isSelected = false,
|
||||
onClick = {}
|
||||
)
|
||||
|
||||
ServerStatusRow(
|
||||
server = "http://checking-server.com:8000",
|
||||
serverStatus = null,
|
||||
isChecking = true,
|
||||
isSelected = false,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
12
app/src/main/res/drawable/ic_settings_24.xml
Normal file
12
app/src/main/res/drawable/ic_settings_24.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
|
||||
Reference in New Issue
Block a user