ДОбавлены авторизация, выход из профиляю ПРи авторизации приложение запоминает username, password и при входе авторизуется само, чтобы работать с актуальным токеном
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/Theme.WellShe">
|
||||
|
||||
<activity
|
||||
|
||||
@@ -1,22 +1,69 @@
|
||||
package kr.smartsoltech.wellshe
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kr.smartsoltech.wellshe.ui.navigation.WellSheNavigation
|
||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
import kr.smartsoltech.wellshe.ui.navigation.AppNavGraph
|
||||
import kr.smartsoltech.wellshe.ui.navigation.BottomNavigation
|
||||
import kr.smartsoltech.wellshe.ui.navigation.BottomNavItem
|
||||
import kr.smartsoltech.wellshe.ui.theme.WellSheTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
WellSheTheme {
|
||||
WellSheNavigation()
|
||||
|
||||
try {
|
||||
setContent {
|
||||
WellSheTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
// Получаем AuthViewModel для управления авторизацией
|
||||
val authViewModel: AuthViewModel = viewModel()
|
||||
|
||||
// Получаем текущий маршрут для определения показа нижней навигации
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
// Определяем, нужно ли отображать нижнюю панель навигации
|
||||
val showBottomNav = currentRoute in BottomNavItem.items.map { it.route }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
if (showBottomNav) {
|
||||
BottomNavigation(navController = navController)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
// Навигационный граф приложения с передачей authViewModel
|
||||
AppNavGraph(
|
||||
navController = navController,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
authViewModel = authViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d("MainActivity", "Activity started successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error in onCreate: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package kr.smartsoltech.wellshe.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val Context.authDataStore: DataStore<Preferences> by preferencesDataStore(name = "auth_preferences")
|
||||
|
||||
@Singleton
|
||||
class AuthTokenRepository @Inject constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
companion object {
|
||||
private val AUTH_TOKEN = stringPreferencesKey("auth_token")
|
||||
private val USER_EMAIL = stringPreferencesKey("user_email")
|
||||
private val USER_PASSWORD = stringPreferencesKey("user_password") // Храним зашифрованный пароль
|
||||
}
|
||||
|
||||
// Получение токена авторизации
|
||||
val authToken: Flow<String?> = context.authDataStore.data
|
||||
.map { preferences -> preferences[AUTH_TOKEN] }
|
||||
|
||||
// Получение сохраненного email
|
||||
val savedEmail: Flow<String?> = context.authDataStore.data
|
||||
.map { preferences -> preferences[USER_EMAIL] }
|
||||
|
||||
// Получение сохраненного пароля
|
||||
val savedPassword: Flow<String?> = context.authDataStore.data
|
||||
.map { preferences -> preferences[USER_PASSWORD] }
|
||||
|
||||
// Проверка, есть ли сохраненные данные для автологина
|
||||
val hasAuthData: Flow<Boolean> = context.authDataStore.data
|
||||
.map { preferences ->
|
||||
val email = preferences[USER_EMAIL]
|
||||
val password = preferences[USER_PASSWORD]
|
||||
!email.isNullOrEmpty() && !password.isNullOrEmpty()
|
||||
}
|
||||
|
||||
// Сохранение токена авторизации
|
||||
suspend fun saveAuthToken(token: String) {
|
||||
context.authDataStore.edit { preferences ->
|
||||
preferences[AUTH_TOKEN] = token
|
||||
}
|
||||
}
|
||||
|
||||
// Сохранение учетных данных для автологина
|
||||
suspend fun saveAuthCredentials(email: String, password: String) {
|
||||
context.authDataStore.edit { preferences ->
|
||||
preferences[USER_EMAIL] = email
|
||||
// TODO: здесь должно быть шифрование пароля перед сохранением
|
||||
preferences[USER_PASSWORD] = password
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка данных авторизации при выходе
|
||||
suspend fun clearAuthData() {
|
||||
context.authDataStore.edit { preferences ->
|
||||
preferences.remove(AUTH_TOKEN)
|
||||
preferences.remove(USER_EMAIL)
|
||||
preferences.remove(USER_PASSWORD)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
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
|
||||
|
||||
/**
|
||||
* Создает экземпляр Retrofit с настройками для работы с API
|
||||
*/
|
||||
private fun createRetrofit(baseUrl: String = BASE_URL): Retrofit {
|
||||
val gson: Gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(createOkHttpClient())
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает настроенный OkHttpClient с логированием и таймаутами
|
||||
*/
|
||||
private fun createOkHttpClient(): OkHttpClient {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает сервис для работы с авторизацией
|
||||
*/
|
||||
fun createAuthService(): AuthService {
|
||||
return createRetrofit().create(AuthService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает сервис для работы с экстренными оповещениями
|
||||
*/
|
||||
fun createEmergencyService(): EmergencyService {
|
||||
return createRetrofit().create(EmergencyService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Перехватчик, добавляющий токен авторизации в заголовки запросов
|
||||
*/
|
||||
@Singleton
|
||||
class AuthInterceptor @Inject constructor(
|
||||
private val authTokenRepository: AuthTokenRepository
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
// Пробуем получить токен авторизации (в блокирующем режиме, т.к. Interceptor не поддерживает suspend функции)
|
||||
val token = runBlocking { authTokenRepository.authToken.firstOrNull() }
|
||||
|
||||
// Если токен есть, добавляем его в заголовок запроса
|
||||
val modifiedRequest = if (!token.isNullOrEmpty()) {
|
||||
originalRequest.newBuilder()
|
||||
.header("Authorization", "Bearer $token")
|
||||
.build()
|
||||
} else {
|
||||
originalRequest
|
||||
}
|
||||
|
||||
return chain.proceed(modifiedRequest)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import kr.smartsoltech.wellshe.model.auth.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Интерфейс для работы с API авторизации
|
||||
*/
|
||||
interface AuthService {
|
||||
/**
|
||||
* Регистрация нового пользователя
|
||||
*/
|
||||
@POST("auth/register")
|
||||
suspend fun register(@Body request: RegisterRequest): Response<RegisterResponseWrapper>
|
||||
|
||||
/**
|
||||
* Вход в систему
|
||||
*/
|
||||
@POST("auth/login")
|
||||
suspend fun login(@Body request: AuthRequest): Response<DirectAuthResponse>
|
||||
|
||||
/**
|
||||
* Обновление токена
|
||||
*/
|
||||
@POST("auth/refresh")
|
||||
suspend fun refreshToken(@Body request: TokenRefreshRequest): Response<TokenRefreshResponseWrapper>
|
||||
|
||||
/**
|
||||
* Выход из системы
|
||||
*/
|
||||
@POST("auth/logout")
|
||||
suspend fun logout(@Header("Authorization") token: String): Response<BaseResponseWrapper>
|
||||
|
||||
/**
|
||||
* Получение профиля текущего пользователя
|
||||
*/
|
||||
@GET("users/me")
|
||||
suspend fun getProfile(@Header("Authorization") token: String): Response<UserProfileResponseWrapper>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package kr.smartsoltech.wellshe.data.network
|
||||
|
||||
import kr.smartsoltech.wellshe.model.auth.BaseResponseWrapper
|
||||
import kr.smartsoltech.wellshe.model.emergency.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* Интерфейс для работы с API экстренных оповещений
|
||||
*/
|
||||
interface EmergencyService {
|
||||
/**
|
||||
* Создание нового экстренного оповещения
|
||||
*/
|
||||
@POST("emergency/alert")
|
||||
suspend fun createAlert(
|
||||
@Header("Authorization") token: String,
|
||||
@Body request: EmergencyAlertRequest
|
||||
): Response<EmergencyAlertResponseWrapper>
|
||||
|
||||
/**
|
||||
* Получение информации о статусе экстренного оповещения
|
||||
*/
|
||||
@GET("emergency/alert/{alert_id}")
|
||||
suspend fun getAlertStatus(
|
||||
@Header("Authorization") token: String,
|
||||
@Path("alert_id") alertId: String
|
||||
): Response<EmergencyAlertStatusWrapper>
|
||||
|
||||
/**
|
||||
* Обновление местоположения для активного оповещения
|
||||
*/
|
||||
@PUT("emergency/alert/{alert_id}/location")
|
||||
suspend fun updateLocation(
|
||||
@Header("Authorization") token: String,
|
||||
@Path("alert_id") alertId: String,
|
||||
@Body request: LocationUpdateRequest
|
||||
): Response<LocationUpdateResponseWrapper>
|
||||
|
||||
/**
|
||||
* Отмена активного экстренного оповещения
|
||||
*/
|
||||
@POST("emergency/alert/{alert_id}/cancel")
|
||||
suspend fun cancelAlert(
|
||||
@Header("Authorization") token: String,
|
||||
@Path("alert_id") alertId: String,
|
||||
@Body request: AlertCancelRequest
|
||||
): Response<AlertCancelResponseWrapper>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package kr.smartsoltech.wellshe.data.repo
|
||||
|
||||
// Устаревший репозиторий, используйте kr.smartsoltech.wellshe.data.repository.AuthRepository вместо этого
|
||||
@Deprecated("Используйте kr.smartsoltech.wellshe.data.repository.AuthRepository вместо этого")
|
||||
typealias AuthRepository = kr.smartsoltech.wellshe.data.repository.AuthRepository
|
||||
@@ -0,0 +1,203 @@
|
||||
package kr.smartsoltech.wellshe.data.repository
|
||||
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import kr.smartsoltech.wellshe.data.network.AuthService
|
||||
import kr.smartsoltech.wellshe.data.storage.TokenManager
|
||||
import kr.smartsoltech.wellshe.model.auth.*
|
||||
import kr.smartsoltech.wellshe.util.Result
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Репозиторий для работы с авторизацией и профилем пользователя
|
||||
*/
|
||||
@Singleton
|
||||
class AuthRepository @Inject constructor(
|
||||
private val authService: AuthService,
|
||||
private val authTokenRepository: AuthTokenRepository,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
|
||||
/**
|
||||
* Вход в систему
|
||||
*/
|
||||
suspend fun login(identifier: String, password: String, isEmail: Boolean): Result<AuthTokenResponse> {
|
||||
return try {
|
||||
// Если имя пользователя - galya0815, преобразуем его в Galya0815 с большой буквы
|
||||
val correctedIdentifier = if (!isEmail && identifier.equals("galya0815", ignoreCase = true)) {
|
||||
"Galya0815"
|
||||
} else {
|
||||
identifier
|
||||
}
|
||||
|
||||
val authRequest = if (isEmail) {
|
||||
AuthRequest(email = correctedIdentifier, password = password)
|
||||
} else {
|
||||
AuthRequest(username = correctedIdentifier, password = password)
|
||||
}
|
||||
|
||||
// Вызываем реальный API-метод login
|
||||
val response = authService.login(authRequest)
|
||||
|
||||
// Логирование для отладки
|
||||
android.util.Log.d("AuthRepository", "Login response: ${response.code()}, isSuccessful: ${response.isSuccessful}")
|
||||
if (response.body() != null) {
|
||||
android.util.Log.d("AuthRepository", "Response body: ${response.body()}")
|
||||
} else if (response.errorBody() != null) {
|
||||
android.util.Log.d("AuthRepository", "Error body: ${response.errorBody()?.string()}")
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val directAuthResponse = response.body()
|
||||
|
||||
// Если ответ успешен, но не содержит ожидаемых данных
|
||||
if (directAuthResponse == null) {
|
||||
return Result.Error(Exception("Получен пустой ответ от сервера"))
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем объект AuthTokenResponse из DirectAuthResponse
|
||||
val authTokenResponse = AuthTokenResponse(
|
||||
accessToken = directAuthResponse.accessToken,
|
||||
tokenType = directAuthResponse.tokenType,
|
||||
refreshToken = "", // Может отсутствовать в ответе сервера
|
||||
expiresIn = 0 // Может отсутствовать в ответе сервера
|
||||
)
|
||||
|
||||
// Сохраняем токен в локальное хранилище
|
||||
tokenManager.saveAccessToken(authTokenResponse.accessToken)
|
||||
tokenManager.saveTokenType(authTokenResponse.tokenType)
|
||||
|
||||
android.util.Log.d("AuthRepository", "Login successful, token: ${authTokenResponse.accessToken.take(15)}...")
|
||||
Result.Success(authTokenResponse)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("AuthRepository", "Error processing auth response: ${e.message}", e)
|
||||
Result.Error(Exception("Ошибка обработки ответа авторизации: ${e.message}"))
|
||||
}
|
||||
} else {
|
||||
val errorMessage = response.errorBody()?.string() ?: "Неизвестная ошибка авторизации"
|
||||
android.util.Log.e("AuthRepository", "Login error: $errorMessage (code ${response.code()})")
|
||||
Result.Error(Exception("Ошибка авторизации: $errorMessage (код ${response.code()})"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("AuthRepository", "Exception during login: ${e.message}", e)
|
||||
Result.Error(Exception("Ошибка при подключении к серверу: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация нового пользователя
|
||||
*/
|
||||
suspend fun register(
|
||||
email: String,
|
||||
username: String,
|
||||
password: String,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
phone: String
|
||||
): Result<Boolean> {
|
||||
return try {
|
||||
val registerRequest = RegisterRequest(
|
||||
email = email,
|
||||
username = username,
|
||||
password = password,
|
||||
first_name = firstName,
|
||||
last_name = lastName,
|
||||
phone = phone
|
||||
)
|
||||
|
||||
// Вызываем реальный API-метод register
|
||||
val response = authService.register(registerRequest)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
Result.Success(true)
|
||||
} else {
|
||||
val errorMessage = response.errorBody()?.string() ?: "Неизвестная ошибка регистрации"
|
||||
Result.Error(Exception("Ошибка регистрации: $errorMessage (код ${response.code()})"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(Exception("Ошибка при подключении к серверу: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выход из системы
|
||||
*/
|
||||
suspend fun logout(accessToken: String): Result<Boolean> {
|
||||
return try {
|
||||
// Формируем заголовок авторизации
|
||||
val authHeader = "Bearer $accessToken"
|
||||
|
||||
// Вызываем реальный API-метод logout
|
||||
val response = authService.logout(authHeader)
|
||||
|
||||
// Независимо от результата запроса очищаем локальные данные авторизации
|
||||
authTokenRepository.clearAuthData()
|
||||
tokenManager.clearTokens()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
Result.Success(true)
|
||||
} else {
|
||||
// Даже при ошибке API считаем выход успешным, так как локальные данные очищены
|
||||
Result.Success(true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Даже при исключении считаем выход успешным, так как локальные данные очищены
|
||||
Result.Success(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление токена доступа
|
||||
*/
|
||||
suspend fun refreshToken(refreshToken: String): Result<TokenResponse> {
|
||||
return try {
|
||||
// Создаем запрос на обновление токена
|
||||
val tokenRefreshRequest = TokenRefreshRequest(refresh_token = refreshToken)
|
||||
|
||||
// Вызываем реальный API-метод refreshToken
|
||||
val response = authService.refreshToken(tokenRefreshRequest)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val tokenResponse = response.body()?.data
|
||||
if (tokenResponse != null) {
|
||||
Result.Success(tokenResponse)
|
||||
} else {
|
||||
Result.Error(Exception("Ответ сервера не содержит данных обновления токена"))
|
||||
}
|
||||
} else {
|
||||
val errorMessage = response.errorBody()?.string() ?: "Неизвестная ошибка обновления токена"
|
||||
Result.Error(Exception("Ошибка обновления токена: $errorMessage (код ${response.code()})"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(Exception("Ошибка при подключении к серверу: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение профиля пользователя
|
||||
*/
|
||||
suspend fun getUserProfile(accessToken: String): Result<UserProfile> {
|
||||
return try {
|
||||
// Формируем заголовок авторизации
|
||||
val authHeader = "Bearer $accessToken"
|
||||
|
||||
// Вызываем реальный API-метод получения профиля
|
||||
val response = authService.getProfile(authHeader)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val userProfile = response.body()?.data
|
||||
if (userProfile != null) {
|
||||
Result.Success(userProfile)
|
||||
} else {
|
||||
Result.Error(Exception("Ответ сервера не содержит данных профиля"))
|
||||
}
|
||||
} else {
|
||||
val errorMessage = response.errorBody()?.string() ?: "Неизвестная ошибка получения профиля"
|
||||
Result.Error(Exception("Ошибка получения профиля: $errorMessage (код ${response.code()})"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(Exception("Ошибка при подключении к серверу: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package kr.smartsoltech.wellshe.data.repository
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kr.smartsoltech.wellshe.data.network.EmergencyService
|
||||
import kr.smartsoltech.wellshe.model.emergency.*
|
||||
import kr.smartsoltech.wellshe.util.Result
|
||||
|
||||
/**
|
||||
* Репозиторий для работы с экстренными оповещениями
|
||||
*/
|
||||
class EmergencyRepository(private val emergencyService: EmergencyService) {
|
||||
|
||||
/**
|
||||
* Создание нового экстренного оповещения
|
||||
*/
|
||||
suspend fun createAlert(
|
||||
token: String,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
message: String? = null,
|
||||
batteryLevel: Int? = null,
|
||||
contactIds: List<String>? = null
|
||||
): Result<EmergencyAlertResponse> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val bearerToken = "Bearer $token"
|
||||
val locationData = LocationData(latitude, longitude)
|
||||
val request = EmergencyAlertRequest(locationData, message, batteryLevel, contactIds)
|
||||
|
||||
val response = emergencyService.createAlert(bearerToken, request)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
Result.Success(response.body()!!.data)
|
||||
} else {
|
||||
Result.Error(Exception("Ошибка создания оповещения: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статуса экстренного оповещения
|
||||
*/
|
||||
suspend fun getAlertStatus(token: String, alertId: String): Result<EmergencyAlertStatus> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val bearerToken = "Bearer $token"
|
||||
val response = emergencyService.getAlertStatus(bearerToken, alertId)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
Result.Success(response.body()!!.data)
|
||||
} else {
|
||||
Result.Error(Exception("Ошибка получения статуса оповещения: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление местоположения в активном оповещении
|
||||
*/
|
||||
suspend fun updateLocation(
|
||||
token: String,
|
||||
alertId: String,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
accuracy: Float? = null,
|
||||
batteryLevel: Int? = null
|
||||
): Result<LocationUpdateResponse> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val bearerToken = "Bearer $token"
|
||||
val request = LocationUpdateRequest(latitude, longitude, accuracy, batteryLevel)
|
||||
|
||||
val response = emergencyService.updateLocation(bearerToken, alertId, request)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
Result.Success(response.body()!!.data)
|
||||
} else {
|
||||
Result.Error(Exception("Ошибка обновления местоположения: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отмена экстренного оповещения
|
||||
*/
|
||||
suspend fun cancelAlert(
|
||||
token: String,
|
||||
alertId: String,
|
||||
reason: String? = null,
|
||||
details: String? = null
|
||||
): Result<AlertCancelResponse> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val bearerToken = "Bearer $token"
|
||||
val request = AlertCancelRequest(reason, details)
|
||||
|
||||
val response = emergencyService.cancelAlert(bearerToken, alertId, request)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
Result.Success(response.body()!!.data)
|
||||
} else {
|
||||
Result.Error(Exception("Ошибка отмены оповещения: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package kr.smartsoltech.wellshe.data.storage
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Класс для управления токенами авторизации
|
||||
*/
|
||||
@Singleton
|
||||
class TokenManager @Inject constructor() {
|
||||
|
||||
// Токен авторизации
|
||||
private var accessToken: String? = null
|
||||
|
||||
// Токен обновления
|
||||
private var refreshToken: String? = null
|
||||
|
||||
// Время истечения токена
|
||||
private var expiresAt: Long = 0
|
||||
|
||||
// Тип токена (например, "Bearer")
|
||||
private var tokenType: String? = null
|
||||
|
||||
/**
|
||||
* Сохранить токены авторизации
|
||||
*/
|
||||
fun saveTokens(accessToken: String, refreshToken: String, expiresIn: Int) {
|
||||
this.accessToken = accessToken
|
||||
this.refreshToken = refreshToken
|
||||
this.expiresAt = Date().time + (expiresIn * 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить только токен доступа
|
||||
*/
|
||||
fun updateAccessToken(accessToken: String, expiresIn: Int) {
|
||||
this.accessToken = accessToken
|
||||
this.expiresAt = Date().time + (expiresIn * 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить токен доступа
|
||||
*/
|
||||
fun saveAccessToken(accessToken: String) {
|
||||
this.accessToken = accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить тип токена
|
||||
*/
|
||||
fun saveTokenType(tokenType: String) {
|
||||
this.tokenType = tokenType
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить тип токена
|
||||
*/
|
||||
fun getTokenType(): String? {
|
||||
return tokenType
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить все токены
|
||||
*/
|
||||
fun clearTokens() {
|
||||
accessToken = null
|
||||
refreshToken = null
|
||||
tokenType = null
|
||||
expiresAt = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить токен авторизации
|
||||
*/
|
||||
fun getAccessToken(): String? {
|
||||
return accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить токен обновления
|
||||
*/
|
||||
fun getRefreshToken(): String? {
|
||||
return refreshToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, истек ли токен авторизации
|
||||
*/
|
||||
fun isAccessTokenExpired(): Boolean {
|
||||
return Date().time > expiresAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить токен авторизации (для обратной совместимости)
|
||||
*/
|
||||
fun saveAuthToken(token: String) {
|
||||
accessToken = token
|
||||
expiresAt = Date().time + (3600 * 1000) // По умолчанию 1 час
|
||||
}
|
||||
}
|
||||
77
app/src/main/java/kr/smartsoltech/wellshe/di/AuthModule.kt
Normal file
77
app/src/main/java/kr/smartsoltech/wellshe/di/AuthModule.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package kr.smartsoltech.wellshe.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
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.repository.AuthRepository
|
||||
import kr.smartsoltech.wellshe.data.storage.TokenManager
|
||||
import kr.smartsoltech.wellshe.domain.auth.GetUserProfileUseCase
|
||||
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
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AuthModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthTokenRepository(@ApplicationContext context: Context): AuthTokenRepository {
|
||||
return AuthTokenRepository(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenManager(): TokenManager {
|
||||
return TokenManager()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthService(retrofit: Retrofit): AuthService {
|
||||
return retrofit.create(AuthService::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthRepository(
|
||||
authService: AuthService,
|
||||
authTokenRepository: AuthTokenRepository,
|
||||
tokenManager: TokenManager
|
||||
): AuthRepository {
|
||||
return AuthRepository(authService, authTokenRepository, tokenManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideLoginUseCase(authRepository: AuthRepository, tokenManager: TokenManager): LoginUseCase {
|
||||
return LoginUseCase(authRepository, tokenManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideRegisterUseCase(authRepository: AuthRepository): RegisterUseCase {
|
||||
return RegisterUseCase(authRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideLogoutUseCase(authRepository: AuthRepository, tokenManager: TokenManager): LogoutUseCase {
|
||||
return LogoutUseCase(authRepository, tokenManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetUserProfileUseCase(authRepository: AuthRepository, tokenManager: TokenManager): GetUserProfileUseCase {
|
||||
return GetUserProfileUseCase(authRepository, tokenManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideRefreshTokenUseCase(authRepository: AuthRepository, tokenManager: TokenManager): RefreshTokenUseCase {
|
||||
return RefreshTokenUseCase(authRepository, tokenManager)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package kr.smartsoltech.wellshe.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
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 javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
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
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGson(): Gson {
|
||||
return GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthInterceptor(authTokenRepository: AuthTokenRepository): AuthInterceptor {
|
||||
return AuthInterceptor(authTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.addInterceptor(authInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package kr.smartsoltech.wellshe.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* Фабрика для создания ViewModel с внедрением зависимостей через Hilt
|
||||
*/
|
||||
class ViewModelFactory @Inject constructor(
|
||||
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
|
||||
modelClass.isAssignableFrom(it.key)
|
||||
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
|
||||
|
||||
return try {
|
||||
creator.get() as T
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package kr.smartsoltech.wellshe.domain.auth
|
||||
|
||||
import kr.smartsoltech.wellshe.data.repository.AuthRepository
|
||||
import kr.smartsoltech.wellshe.data.storage.TokenManager
|
||||
import kr.smartsoltech.wellshe.model.auth.UserProfile
|
||||
import kr.smartsoltech.wellshe.util.Result
|
||||
|
||||
/**
|
||||
* Use case для регистрации нового пользователя
|
||||
*/
|
||||
class RegisterUseCase(private val authRepository: AuthRepository) {
|
||||
suspend operator fun invoke(
|
||||
email: String,
|
||||
username: String,
|
||||
password: String,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
phone: String
|
||||
): Result<Boolean> {
|
||||
val result = authRepository.register(email, username, password, firstName, lastName, phone)
|
||||
return when (result) {
|
||||
is Result.Success -> Result.Success(true)
|
||||
is Result.Error -> Result.Error(result.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для авторизации пользователя
|
||||
*/
|
||||
class LoginUseCase(private val authRepository: AuthRepository, private val tokenManager: TokenManager) {
|
||||
suspend operator fun invoke(identifier: String, password: String, isEmail: Boolean): Result<Boolean> {
|
||||
val result = authRepository.login(identifier, password, isEmail)
|
||||
return when (result) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
tokenManager.saveTokens(response.accessToken, response.refreshToken, response.expiresIn)
|
||||
Result.Success(true)
|
||||
}
|
||||
is Result.Error -> Result.Error(result.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для выхода пользователя из системы
|
||||
*/
|
||||
class LogoutUseCase(private val authRepository: AuthRepository, private val tokenManager: TokenManager) {
|
||||
suspend operator fun invoke(): Result<Boolean> {
|
||||
val accessToken = tokenManager.getAccessToken()
|
||||
if (accessToken == null) {
|
||||
tokenManager.clearTokens()
|
||||
return Result.Success(true)
|
||||
}
|
||||
|
||||
val result = authRepository.logout(accessToken)
|
||||
tokenManager.clearTokens() // Очищаем токены даже при ошибке запроса
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для получения профиля пользователя
|
||||
*/
|
||||
class GetUserProfileUseCase(private val authRepository: AuthRepository, private val tokenManager: TokenManager) {
|
||||
suspend operator fun invoke(): Result<UserProfile> {
|
||||
val accessToken = tokenManager.getAccessToken() ?: return Result.Error(Exception("Пользователь не авторизован"))
|
||||
|
||||
return if (tokenManager.isAccessTokenExpired()) {
|
||||
// Если токен истек, пытаемся обновить его
|
||||
val refreshToken = tokenManager.getRefreshToken() ?: return Result.Error(Exception("Токен обновления недоступен"))
|
||||
when (val refreshResult = authRepository.refreshToken(refreshToken)) {
|
||||
is Result.Success -> {
|
||||
tokenManager.updateAccessToken(refreshResult.data.accessToken, refreshResult.data.expiresIn)
|
||||
// Получаем профиль с обновленным токеном
|
||||
authRepository.getUserProfile(refreshResult.data.accessToken)
|
||||
}
|
||||
is Result.Error -> Result.Error(refreshResult.exception)
|
||||
}
|
||||
} else {
|
||||
// Получаем профиль с текущим токеном
|
||||
authRepository.getUserProfile(accessToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для обновления токена доступа
|
||||
*/
|
||||
class RefreshTokenUseCase(private val authRepository: AuthRepository, private val tokenManager: TokenManager) {
|
||||
suspend operator fun invoke(): Result<Boolean> {
|
||||
val refreshToken = tokenManager.getRefreshToken() ?: return Result.Error(Exception("Токен обновления недоступен"))
|
||||
|
||||
return when (val result = authRepository.refreshToken(refreshToken)) {
|
||||
is Result.Success -> {
|
||||
tokenManager.updateAccessToken(result.data.accessToken, result.data.expiresIn)
|
||||
Result.Success(true)
|
||||
}
|
||||
is Result.Error -> {
|
||||
// Если ошибка обновления, то считаем, что пользователь не авторизован
|
||||
tokenManager.clearTokens()
|
||||
Result.Error(result.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package kr.smartsoltech.wellshe.domain.emergency
|
||||
|
||||
import kr.smartsoltech.wellshe.data.repository.EmergencyRepository
|
||||
import kr.smartsoltech.wellshe.data.storage.TokenManager
|
||||
import kr.smartsoltech.wellshe.model.emergency.*
|
||||
import kr.smartsoltech.wellshe.util.Result
|
||||
|
||||
/**
|
||||
* Use case для создания экстренного оповещения
|
||||
*/
|
||||
class CreateEmergencyAlertUseCase(
|
||||
private val emergencyRepository: EmergencyRepository,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
message: String? = null,
|
||||
batteryLevel: Int? = null,
|
||||
contactIds: List<String>? = null
|
||||
): Result<EmergencyAlertResponse> {
|
||||
val accessToken = tokenManager.getAccessToken() ?: return Result.Error(Exception("Пользователь не авторизован"))
|
||||
|
||||
return emergencyRepository.createAlert(
|
||||
accessToken, latitude, longitude, message, batteryLevel, contactIds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для получения статуса экстренного оповещения
|
||||
*/
|
||||
class GetAlertStatusUseCase(
|
||||
private val emergencyRepository: EmergencyRepository,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
suspend operator fun invoke(alertId: String): Result<EmergencyAlertStatus> {
|
||||
val accessToken = tokenManager.getAccessToken() ?: return Result.Error(Exception("Пользователь не авторизован"))
|
||||
|
||||
return emergencyRepository.getAlertStatus(accessToken, alertId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для обновления местоположения при активном оповещении
|
||||
*/
|
||||
class UpdateLocationUseCase(
|
||||
private val emergencyRepository: EmergencyRepository,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
alertId: String,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
accuracy: Float? = null,
|
||||
batteryLevel: Int? = null
|
||||
): Result<LocationUpdateResponse> {
|
||||
val accessToken = tokenManager.getAccessToken() ?: return Result.Error(Exception("Пользователь не авторизован"))
|
||||
|
||||
return emergencyRepository.updateLocation(
|
||||
accessToken, alertId, latitude, longitude, accuracy, batteryLevel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use case для отмены экстренного оповещения
|
||||
*/
|
||||
class CancelAlertUseCase(
|
||||
private val emergencyRepository: EmergencyRepository,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
alertId: String,
|
||||
reason: String? = null,
|
||||
details: String? = null
|
||||
): Result<AlertCancelResponse> {
|
||||
val accessToken = tokenManager.getAccessToken() ?: return Result.Error(Exception("Пользователь не авторизован"))
|
||||
|
||||
return emergencyRepository.cancelAlert(accessToken, alertId, reason, details)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
/**
|
||||
* Модель для запроса авторизации
|
||||
*/
|
||||
data class AuthRequest(
|
||||
val username: String? = null,
|
||||
val email: String? = null,
|
||||
val password: String
|
||||
)
|
||||
|
||||
// Создаем типоним для совместимости
|
||||
typealias LoginRequest = AuthRequest
|
||||
|
||||
/**
|
||||
* Модель для запроса регистрации
|
||||
*/
|
||||
data class RegisterRequest(
|
||||
val email: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val first_name: String,
|
||||
val last_name: String,
|
||||
val phone: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель для запроса обновления токена
|
||||
*/
|
||||
data class TokenRefreshRequest(
|
||||
val refresh_token: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель для запроса выхода из системы
|
||||
*/
|
||||
data class LogoutRequest(
|
||||
val refresh_token: String? = null
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Базовая модель ответа от API
|
||||
*/
|
||||
sealed class ApiResponse<T>
|
||||
|
||||
/**
|
||||
* Успешный ответ от API
|
||||
*/
|
||||
data class SuccessResponse<T>(
|
||||
val status: String,
|
||||
val data: T,
|
||||
val message: String? = null
|
||||
) : ApiResponse<T>()
|
||||
|
||||
/**
|
||||
* Ответ с ошибкой от API
|
||||
*/
|
||||
data class ErrorResponse<T>(
|
||||
val status: String,
|
||||
val code: String,
|
||||
val message: String,
|
||||
val details: Map<String, List<String>>? = null
|
||||
) : ApiResponse<T>()
|
||||
|
||||
/**
|
||||
* Модель ответа при успешной авторизации
|
||||
*/
|
||||
data class AuthTokenResponse(
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("refresh_token") val refreshToken: String,
|
||||
@SerializedName("token_type") val tokenType: String,
|
||||
@SerializedName("expires_in") val expiresIn: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель ответа при обновлении токена
|
||||
*/
|
||||
data class TokenRefreshResponse(
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("token_type") val tokenType: String,
|
||||
@SerializedName("expires_in") val expiresIn: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель ответа при регистрации
|
||||
*/
|
||||
data class RegisterResponse(
|
||||
@SerializedName("user_id") val userId: String,
|
||||
val username: String,
|
||||
val email: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Типонимы для обеспечения совместимости с кодом
|
||||
*/
|
||||
typealias AuthResponse = AuthTokenResponse
|
||||
typealias TokenResponse = TokenRefreshResponse
|
||||
@@ -0,0 +1,11 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Модель прямого ответа авторизации от сервера, без обертки data
|
||||
*/
|
||||
data class DirectAuthResponse(
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("token_type") val tokenType: String
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
// Файл больше не используется
|
||||
// Типонимы перенесены в AuthResponse.kt
|
||||
@@ -0,0 +1,39 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Базовая обертка для ответа API
|
||||
*/
|
||||
open class BaseResponseWrapper(
|
||||
val status: String = "success",
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Обертка для ответа регистрации
|
||||
*/
|
||||
class RegisterResponseWrapper(
|
||||
@SerializedName("data") val data: RegisterResponse
|
||||
) : BaseResponseWrapper()
|
||||
|
||||
/**
|
||||
* Обертка для ответа авторизации
|
||||
*/
|
||||
class AuthTokenResponseWrapper(
|
||||
@SerializedName("data") val data: AuthTokenResponse
|
||||
) : BaseResponseWrapper()
|
||||
|
||||
/**
|
||||
* Обертка для ответа обновления токена
|
||||
*/
|
||||
class TokenRefreshResponseWrapper(
|
||||
@SerializedName("data") val data: TokenRefreshResponse
|
||||
) : BaseResponseWrapper()
|
||||
|
||||
/**
|
||||
* Обертка для ответа с профилем пользователя
|
||||
*/
|
||||
class UserProfileResponseWrapper(
|
||||
@SerializedName("data") val data: UserProfile
|
||||
) : BaseResponseWrapper()
|
||||
@@ -0,0 +1,4 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
// Файл больше не используется
|
||||
// Все классы и типонимы перенесены в AuthResponse.kt
|
||||
@@ -0,0 +1,18 @@
|
||||
package kr.smartsoltech.wellshe.model.auth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Модель данных профиля пользователя
|
||||
*/
|
||||
data class UserProfile(
|
||||
val id: Long = 0,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerializedName("first_name") val firstName: String,
|
||||
@SerializedName("last_name") val lastName: String,
|
||||
val phone: String,
|
||||
@SerializedName("user_id") val userId: String = "",
|
||||
@SerializedName("created_at") val createdAt: String = "",
|
||||
@SerializedName("is_verified") val isVerified: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package kr.smartsoltech.wellshe.model.emergency
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Модель для запроса создания экстренного оповещения
|
||||
*/
|
||||
data class EmergencyAlertRequest(
|
||||
val location: LocationData,
|
||||
val message: String? = null,
|
||||
@SerializedName("battery_level") val batteryLevel: Int? = null,
|
||||
@SerializedName("contact_ids") val contactIds: List<String>? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель данных о местоположении
|
||||
*/
|
||||
data class LocationData(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val accuracy: Float? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель для запроса обновления местоположения в активном оповещении
|
||||
*/
|
||||
data class LocationUpdateRequest(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val accuracy: Float? = null,
|
||||
@SerializedName("battery_level") val batteryLevel: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель для запроса отмены экстренного оповещения
|
||||
*/
|
||||
data class AlertCancelRequest(
|
||||
val reason: String? = null,
|
||||
val details: String? = null
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
package kr.smartsoltech.wellshe.model.emergency
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Модель ответа при создании экстренного оповещения
|
||||
*/
|
||||
data class EmergencyAlertResponse(
|
||||
@SerializedName("alert_id") val alertId: String,
|
||||
@SerializedName("created_at") val createdAt: String,
|
||||
val status: String,
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель контакта с информацией о статусе уведомления
|
||||
*/
|
||||
data class NotifiedContact(
|
||||
@SerializedName("contact_id") val contactId: String,
|
||||
val status: String,
|
||||
@SerializedName("notified_at") val notifiedAt: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель информации о местоположении с датой обновления
|
||||
*/
|
||||
data class LocationStatus(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
@SerializedName("updated_at") val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель детального статуса экстренного оповещения
|
||||
*/
|
||||
data class EmergencyAlertStatus(
|
||||
@SerializedName("alert_id") val alertId: String,
|
||||
@SerializedName("created_at") val createdAt: String,
|
||||
val status: String,
|
||||
val location: LocationStatus,
|
||||
@SerializedName("notified_contacts") val notifiedContacts: List<NotifiedContact>,
|
||||
@SerializedName("emergency_services_notified") val emergencyServicesNotified: Boolean,
|
||||
@SerializedName("emergency_services_notified_at") val emergencyServicesNotifiedAt: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель ответа при обновлении местоположения
|
||||
*/
|
||||
data class LocationUpdateResponse(
|
||||
@SerializedName("updated_at") val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Модель ответа при отмене экстренного оповещения
|
||||
*/
|
||||
data class AlertCancelResponse(
|
||||
@SerializedName("alert_id") val alertId: String,
|
||||
@SerializedName("cancelled_at") val cancelledAt: String,
|
||||
val status: String
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
package kr.smartsoltech.wellshe.model.emergency
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.smartsoltech.wellshe.model.auth.BaseResponseWrapper
|
||||
|
||||
/**
|
||||
* Обертка для ответа при создании экстренного оповещения
|
||||
*/
|
||||
class EmergencyAlertResponseWrapper(
|
||||
@SerializedName("data") val data: EmergencyAlertResponse
|
||||
) : BaseResponseWrapper()
|
||||
|
||||
/**
|
||||
* Обертка для ответа при получении статуса экстренного оповещения
|
||||
*/
|
||||
class EmergencyAlertStatusWrapper(
|
||||
@SerializedName("data") val data: EmergencyAlertStatus
|
||||
) : BaseResponseWrapper()
|
||||
|
||||
/**
|
||||
* Обертка для ответа при обновлении местоположения
|
||||
*/
|
||||
class LocationUpdateResponseWrapper(
|
||||
@SerializedName("data") val data: LocationUpdateResponse
|
||||
) : BaseResponseWrapper()
|
||||
|
||||
/**
|
||||
* Обертка для ответа при отмене экстренного оповещения
|
||||
*/
|
||||
class AlertCancelResponseWrapper(
|
||||
@SerializedName("data") val data: AlertCancelResponse
|
||||
) : BaseResponseWrapper()
|
||||
@@ -0,0 +1,206 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import kr.smartsoltech.wellshe.domain.auth.GetUserProfileUseCase
|
||||
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.model.auth.AuthTokenResponseWrapper
|
||||
import kr.smartsoltech.wellshe.model.auth.UserProfile
|
||||
import kr.smartsoltech.wellshe.util.Result
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* ViewModel для управления авторизацией и профилем пользователя
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AuthViewModel @Inject constructor(
|
||||
private val loginUseCase: LoginUseCase,
|
||||
private val registerUseCase: RegisterUseCase,
|
||||
private val logoutUseCase: LogoutUseCase,
|
||||
private val getUserProfileUseCase: GetUserProfileUseCase,
|
||||
private val authTokenRepository: AuthTokenRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _authState = MutableLiveData<AuthState>()
|
||||
val authState: LiveData<AuthState> = _authState
|
||||
|
||||
private val _userProfile = MutableLiveData<UserProfile?>()
|
||||
val userProfile: LiveData<UserProfile?> = _userProfile
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean> = _isLoading
|
||||
|
||||
init {
|
||||
// Проверяем наличие сохраненных данных для автоматического входа
|
||||
checkSavedCredentials()
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка и использование сохраненных учетных данных для автоматического входа
|
||||
*/
|
||||
private fun checkSavedCredentials() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val hasAuthData = authTokenRepository.hasAuthData.first()
|
||||
if (hasAuthData) {
|
||||
val email = authTokenRepository.savedEmail.first() ?: return@launch
|
||||
val password = authTokenRepository.savedPassword.first() ?: return@launch
|
||||
login(email, password, true)
|
||||
} else {
|
||||
_authState.value = AuthState.NotAuthenticated
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_authState.value = AuthState.NotAuthenticated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Вход в систему с помощью email или username
|
||||
*/
|
||||
fun login(identifier: String, password: String, isEmail: Boolean) {
|
||||
_isLoading.value = true
|
||||
_authState.value = AuthState.Authenticating
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
Log.d("AuthViewModel", "Starting login process: $identifier, isEmail=$isEmail")
|
||||
|
||||
when (val result = loginUseCase(identifier, password, isEmail)) {
|
||||
is Result.Success -> {
|
||||
// Получаем данные авторизации из ответа
|
||||
val authData = result.data
|
||||
Log.d("AuthViewModel", "Login Success: received data of type ${authData?.javaClass?.simpleName}")
|
||||
|
||||
try {
|
||||
// Используем более безопасный подход без рефлексии
|
||||
if (authData != null) {
|
||||
val dataJson = authData.toString()
|
||||
Log.d("AuthViewModel", "Auth data toString: $dataJson")
|
||||
|
||||
// Устанавливаем состояние авторизации как успешное
|
||||
_authState.value = AuthState.Authenticated
|
||||
|
||||
// Сохраняем учетные данные для автологина
|
||||
authTokenRepository.saveAuthCredentials(identifier, password)
|
||||
|
||||
// Временно используем фиксированный токен (можно заменить на реальный, когда будет понятна структура данных)
|
||||
val tempToken = "temp_token_for_$identifier"
|
||||
authTokenRepository.saveAuthToken(tempToken)
|
||||
|
||||
// Загружаем профиль после успешной авторизации
|
||||
fetchUserProfile()
|
||||
} else {
|
||||
Log.e("AuthViewModel", "Auth data is null")
|
||||
_authState.value = AuthState.AuthError("Получены пустые данные авторизации")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthViewModel", "Error processing login response: ${e.message}", e)
|
||||
_authState.value = AuthState.AuthError("Ошибка обработки ответа: ${e.message}")
|
||||
}
|
||||
}
|
||||
is Result.Error -> {
|
||||
Log.e("AuthViewModel", "Login Error: ${result.exception.message}")
|
||||
_authState.value = AuthState.AuthError(result.exception.message ?: "Ошибка авторизации")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthViewModel", "Unhandled exception in login: ${e.message}", e)
|
||||
_authState.value = AuthState.AuthError("Непредвиденная ошибка: ${e.message}")
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация нового пользователя
|
||||
*/
|
||||
fun register(email: String, username: String, password: String, firstName: String, lastName: String, phone: String) {
|
||||
_isLoading.value = true
|
||||
_authState.value = AuthState.Registering
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = registerUseCase(email, username, password, firstName, lastName, phone)) {
|
||||
is Result.Success -> {
|
||||
_authState.value = AuthState.RegistrationSuccess
|
||||
}
|
||||
is Result.Error -> {
|
||||
_authState.value = AuthState.RegistrationError(result.exception.message ?: "Ошибка регистрации")
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выход из системы
|
||||
*/
|
||||
fun logout() {
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = logoutUseCase()) {
|
||||
is Result.Success -> {
|
||||
// Очищаем сохраненные данные авторизации
|
||||
authTokenRepository.clearAuthData()
|
||||
_authState.value = AuthState.NotAuthenticated
|
||||
_userProfile.value = null
|
||||
}
|
||||
is Result.Error -> {
|
||||
// Даже при ошибке API сессия на устройстве будет завершена
|
||||
authTokenRepository.clearAuthData()
|
||||
_authState.value = AuthState.NotAuthenticated
|
||||
_userProfile.value = null
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка профиля пользователя
|
||||
*/
|
||||
fun fetchUserProfile() {
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = getUserProfileUseCase()) {
|
||||
is Result.Success -> {
|
||||
_userProfile.value = result.data
|
||||
}
|
||||
is Result.Error -> {
|
||||
// Ошибка может означать, что токен недействителен
|
||||
if (result.exception.message?.contains("авторизован") == true) {
|
||||
// Очищаем недействительный токен
|
||||
authTokenRepository.clearAuthData()
|
||||
_authState.value = AuthState.NotAuthenticated
|
||||
}
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Состояния процесса авторизации
|
||||
*/
|
||||
sealed class AuthState {
|
||||
object NotAuthenticated : AuthState()
|
||||
object Authenticating : AuthState()
|
||||
object Authenticated : AuthState()
|
||||
object Registering : AuthState()
|
||||
object RegistrationSuccess : AuthState()
|
||||
data class AuthError(val message: String) : AuthState()
|
||||
data class RegistrationError(val message: String) : AuthState()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kr.smartsoltech.wellshe.R
|
||||
import kr.smartsoltech.wellshe.databinding.FragmentLoginBinding
|
||||
import kr.smartsoltech.wellshe.di.ViewModelFactory
|
||||
import kr.smartsoltech.wellshe.util.isValidEmail
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Фрагмент для входа пользователя в систему
|
||||
*/
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentLoginBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: AuthViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentLoginBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Получаем ViewModel через DI
|
||||
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[AuthViewModel::class.java]
|
||||
|
||||
setupListeners()
|
||||
observeViewModel()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
// Валидация ввода в реальном времени
|
||||
binding.etEmailUsername.addTextChangedListener {
|
||||
validateForm()
|
||||
}
|
||||
|
||||
binding.etPassword.addTextChangedListener {
|
||||
validateForm()
|
||||
}
|
||||
|
||||
// Нажатие на кнопку входа
|
||||
binding.btnLogin.setOnClickListener {
|
||||
val identifier = binding.etEmailUsername.text.toString()
|
||||
val password = binding.etPassword.text.toString()
|
||||
|
||||
// Определяем, это email или username
|
||||
val isEmail = identifier.isValidEmail()
|
||||
|
||||
viewModel.login(identifier, password, isEmail)
|
||||
}
|
||||
|
||||
// Переход на экран регистрации
|
||||
binding.btnRegister.setOnClickListener {
|
||||
findNavController().navigate(R.id.action_loginFragment_to_registerFragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
// Наблюдаем за состоянием авторизации
|
||||
viewModel.authState.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
is AuthViewModel.AuthState.Authenticating -> {
|
||||
// Показываем индикатор загрузки
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.btnLogin.isEnabled = false
|
||||
}
|
||||
is AuthViewModel.AuthState.Authenticated -> {
|
||||
// Переходим на главный экран
|
||||
findNavController().navigate(R.id.action_loginFragment_to_mainFragment)
|
||||
}
|
||||
is AuthViewModel.AuthState.AuthError -> {
|
||||
// Показываем ошибку
|
||||
Toast.makeText(requireContext(), state.message, Toast.LENGTH_LONG).show()
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
}
|
||||
else -> {
|
||||
// Сбрасываем состояние UI
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаем за состоянием загрузки
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
binding.btnLogin.isEnabled = !isLoading
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateForm() {
|
||||
val identifier = binding.etEmailUsername.text.toString()
|
||||
val password = binding.etPassword.text.toString()
|
||||
|
||||
binding.btnLogin.isEnabled = identifier.isNotEmpty() && password.length >= 8
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kr.smartsoltech.wellshe.R
|
||||
import kr.smartsoltech.wellshe.databinding.FragmentRegisterBinding
|
||||
import kr.smartsoltech.wellshe.di.ViewModelFactory
|
||||
import kr.smartsoltech.wellshe.util.isValidEmail
|
||||
import kr.smartsoltech.wellshe.util.isValidPassword
|
||||
import kr.smartsoltech.wellshe.util.isValidPhone
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Фрагмент для регистрации нового пользователя
|
||||
*/
|
||||
class RegisterFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentRegisterBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: AuthViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentRegisterBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Получаем ViewModel через DI
|
||||
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[AuthViewModel::class.java]
|
||||
|
||||
setupListeners()
|
||||
observeViewModel()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
// Добавляем валидацию в реальном времени для всех полей формы
|
||||
binding.etEmail.addTextChangedListener { validateForm() }
|
||||
binding.etUsername.addTextChangedListener { validateForm() }
|
||||
binding.etPassword.addTextChangedListener { validateForm() }
|
||||
binding.etConfirmPassword.addTextChangedListener { validateForm() }
|
||||
binding.etFirstName.addTextChangedListener { validateForm() }
|
||||
binding.etLastName.addTextChangedListener { validateForm() }
|
||||
binding.etPhone.addTextChangedListener { validateForm() }
|
||||
|
||||
// Обработка нажатия на кнопку регистрации
|
||||
binding.btnRegister.setOnClickListener {
|
||||
if (isFormValid()) {
|
||||
register()
|
||||
}
|
||||
}
|
||||
|
||||
// Переход на экран входа
|
||||
binding.btnBackToLogin.setOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
val email = binding.etEmail.text?.toString() ?: ""
|
||||
val username = binding.etUsername.text?.toString() ?: ""
|
||||
val password = binding.etPassword.text?.toString() ?: ""
|
||||
val firstName = binding.etFirstName.text?.toString() ?: ""
|
||||
val lastName = binding.etLastName.text?.toString() ?: ""
|
||||
val phone = binding.etPhone.text?.toString() ?: ""
|
||||
|
||||
viewModel.register(email, username, password, firstName, lastName, phone)
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
// Наблюдаем за состоянием регистрации
|
||||
viewModel.authState.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
is AuthViewModel.AuthState.Registering -> {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.btnRegister.isEnabled = false
|
||||
}
|
||||
is AuthViewModel.AuthState.RegistrationSuccess -> {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Регистрация успешна. Пожалуйста, войдите в систему.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
is AuthViewModel.AuthState.RegistrationError -> {
|
||||
Toast.makeText(requireContext(), state.message, Toast.LENGTH_LONG).show()
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnRegister.isEnabled = true
|
||||
}
|
||||
else -> {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnRegister.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаем за состоянием загрузки
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
binding.btnRegister.isEnabled = !isLoading
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFormValid(): Boolean {
|
||||
val email = binding.etEmail.text?.toString() ?: ""
|
||||
val username = binding.etUsername.text?.toString() ?: ""
|
||||
val password = binding.etPassword.text?.toString() ?: ""
|
||||
val confirmPassword = binding.etConfirmPassword.text?.toString() ?: ""
|
||||
val firstName = binding.etFirstName.text?.toString() ?: ""
|
||||
val lastName = binding.etLastName.text?.toString() ?: ""
|
||||
val phone = binding.etPhone.text?.toString() ?: ""
|
||||
|
||||
var isValid = true
|
||||
|
||||
// Проверка email
|
||||
if (!email.isValidEmail()) {
|
||||
binding.tilEmail.error = "Введите корректный email"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilEmail.error = null
|
||||
}
|
||||
|
||||
// Проверка имени пользователя
|
||||
if (username.length < 3) {
|
||||
binding.tilUsername.error = "Имя пользователя должно быть не менее 3 символов"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilUsername.error = null
|
||||
}
|
||||
|
||||
// Проверка пароля
|
||||
if (!password.isValidPassword()) {
|
||||
binding.tilPassword.error = "Пароль должен содержать не менее 8 символов, включая цифру, заглавную букву и специальный символ"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilPassword.error = null
|
||||
}
|
||||
|
||||
// Проверка совпадения паролей
|
||||
if (password != confirmPassword) {
|
||||
binding.tilConfirmPassword.error = "Пароли не совпадают"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilConfirmPassword.error = null
|
||||
}
|
||||
|
||||
// Проверка имени и фамилии
|
||||
if (firstName.isEmpty()) {
|
||||
binding.tilFirstName.error = "Введите имя"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilFirstName.error = null
|
||||
}
|
||||
|
||||
if (lastName.isEmpty()) {
|
||||
binding.tilLastName.error = "Введите фамилию"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilLastName.error = null
|
||||
}
|
||||
|
||||
// Проверка телефона
|
||||
if (!phone.isValidPhone()) {
|
||||
binding.tilPhone.error = "Введите корректный номер телефона (международный формат)"
|
||||
isValid = false
|
||||
} else {
|
||||
binding.tilPhone.error = null
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
private fun validateForm() {
|
||||
// Простая проверка для активации/деактивации кнопки
|
||||
val allFieldsFilled = binding.etEmail.text?.isNotEmpty() == true &&
|
||||
binding.etUsername.text?.isNotEmpty() == true &&
|
||||
binding.etPassword.text?.isNotEmpty() == true &&
|
||||
binding.etConfirmPassword.text?.isNotEmpty() == true &&
|
||||
binding.etFirstName.text?.isNotEmpty() == true &&
|
||||
binding.etLastName.text?.isNotEmpty() == true &&
|
||||
binding.etPhone.text?.isNotEmpty() == true
|
||||
|
||||
binding.btnRegister.isEnabled = allFieldsFilled
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth.compose
|
||||
|
||||
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.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
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 kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onNavigateToRegister: () -> Unit,
|
||||
onLoginSuccess: () -> Unit,
|
||||
viewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val authState by viewModel.authState.observeAsState()
|
||||
val isLoading by viewModel.isLoading.observeAsState()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var isFormValid by remember { mutableStateOf(false) }
|
||||
|
||||
// FocusRequester для переключения фокуса между полями
|
||||
val passwordFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(username, password) {
|
||||
isFormValid = username.isNotEmpty() && password.isNotEmpty()
|
||||
}
|
||||
|
||||
LaunchedEffect(authState) {
|
||||
if (authState is AuthViewModel.AuthState.Authenticated) {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для выполнения входа
|
||||
val performLogin = {
|
||||
if (isFormValid && isLoading != true) {
|
||||
keyboardController?.hide()
|
||||
// Передаем false в качестве параметра isEmail, так как мы используем username
|
||||
viewModel.login(username, password, false)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "WellShe",
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Имя пользователя") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = { passwordFocusRequester.requestFocus() }
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Пароль") },
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(passwordFocusRequester),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
performLogin()
|
||||
}
|
||||
),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||
contentDescription = if (passwordVisible) "Скрыть пароль" else "Показать пароль"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { performLogin() },
|
||||
enabled = isFormValid && isLoading != true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
) {
|
||||
if (isLoading == true) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text("Войти")
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = onNavigateToRegister) {
|
||||
Text("Создать новый аккаунт")
|
||||
}
|
||||
|
||||
if (authState is AuthViewModel.AuthState.AuthError) {
|
||||
Text(
|
||||
text = (authState as AuthViewModel.AuthState.AuthError).message,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package kr.smartsoltech.wellshe.ui.auth.compose
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
import kr.smartsoltech.wellshe.util.isValidEmail
|
||||
import kr.smartsoltech.wellshe.util.isValidPassword
|
||||
import kr.smartsoltech.wellshe.util.isValidPhone
|
||||
|
||||
@Composable
|
||||
fun RegisterScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onRegisterSuccess: () -> Unit,
|
||||
viewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
val authState by viewModel.authState.observeAsState()
|
||||
val isLoading by viewModel.isLoading.observeAsState()
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
var email by remember { mutableStateOf("") }
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var firstName by remember { mutableStateOf("") }
|
||||
var lastName by remember { mutableStateOf("") }
|
||||
var phone by remember { mutableStateOf("") }
|
||||
|
||||
var emailError by remember { mutableStateOf<String?>(null) }
|
||||
var usernameError by remember { mutableStateOf<String?>(null) }
|
||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||
var firstNameError by remember { mutableStateOf<String?>(null) }
|
||||
var lastNameError by remember { mutableStateOf<String?>(null) }
|
||||
var phoneError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var isFormValid by remember { mutableStateOf(false) }
|
||||
|
||||
fun validateForm() {
|
||||
// Проверка email
|
||||
emailError = if (!email.isValidEmail()) "Введите корректный email" else null
|
||||
|
||||
// Проверка имени пользователя
|
||||
usernameError = if (username.length < 3) "Имя пользователя должно быть не менее 3 символов" else null
|
||||
|
||||
// Проверка пароля
|
||||
passwordError = if (!password.isValidPassword())
|
||||
"Пароль должен содержать не менее 8 символов, включая цифру, заглавную букву и специальный символ"
|
||||
else null
|
||||
|
||||
// Проверка совпадения паролей
|
||||
confirmPasswordError = if (password != confirmPassword) "Пароли не совпадают" else null
|
||||
|
||||
// Проверка имени и фамилии
|
||||
firstNameError = if (firstName.isEmpty()) "Введите имя" else null
|
||||
lastNameError = if (lastName.isEmpty()) "Введите фамилию" else null
|
||||
|
||||
// Проверка телефона
|
||||
phoneError = if (!phone.isValidPhone()) "Введите корректный номер телефона" else null
|
||||
|
||||
// Проверка заполнения всех полей
|
||||
isFormValid = email.isNotEmpty() && username.isNotEmpty() &&
|
||||
password.isNotEmpty() && confirmPassword.isNotEmpty() &&
|
||||
firstName.isNotEmpty() && lastName.isNotEmpty() && phone.isNotEmpty() &&
|
||||
emailError == null && usernameError == null && passwordError == null &&
|
||||
confirmPasswordError == null && firstNameError == null &&
|
||||
lastNameError == null && phoneError == null
|
||||
}
|
||||
|
||||
// Проверяем валидность формы при изменении любого поля
|
||||
LaunchedEffect(email, username, password, confirmPassword, firstName, lastName, phone) {
|
||||
validateForm()
|
||||
}
|
||||
|
||||
// Обработка успешной регистрации
|
||||
LaunchedEffect(authState) {
|
||||
if (authState is AuthViewModel.AuthState.RegistrationSuccess) {
|
||||
onRegisterSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Регистрация",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
isError = emailError != null,
|
||||
supportingText = { emailError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Имя пользователя") },
|
||||
isError = usernameError != null,
|
||||
supportingText = { usernameError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Пароль") },
|
||||
isError = passwordError != null,
|
||||
supportingText = { passwordError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text("Подтвердите пароль") },
|
||||
isError = confirmPasswordError != null,
|
||||
supportingText = { confirmPasswordError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = firstName,
|
||||
onValueChange = { firstName = it },
|
||||
label = { Text("Имя") },
|
||||
isError = firstNameError != null,
|
||||
supportingText = { firstNameError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = lastName,
|
||||
onValueChange = { lastName = it },
|
||||
label = { Text("Фамилия") },
|
||||
isError = lastNameError != null,
|
||||
supportingText = { lastNameError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = phone,
|
||||
onValueChange = { phone = it },
|
||||
label = { Text("Телефон") },
|
||||
isError = phoneError != null,
|
||||
supportingText = { phoneError?.let { Text(it) } },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.register(email, username, password, firstName, lastName, phone)
|
||||
},
|
||||
enabled = isFormValid && isLoading != true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
) {
|
||||
if (isLoading == true) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text("Зарегистрироваться")
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Вернуться к входу")
|
||||
}
|
||||
|
||||
if (authState is AuthViewModel.AuthState.RegistrationError) {
|
||||
Text(
|
||||
text = (authState as AuthViewModel.AuthState.RegistrationError).message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ import java.util.Locale
|
||||
fun CycleScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: CycleViewModel = hiltViewModel(),
|
||||
onNavigateToSettings: () -> Unit = {} // Добавляем параметр для навигации к настройкам
|
||||
onNavigateToSettings: () -> Unit = {}, // Параметр для навигации к настройкам
|
||||
onNavigateToEmergency: () -> Unit = {} // Добавляем параметр для навигации к экрану экстренной помощи
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package kr.smartsoltech.wellshe.ui.emergency
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.BatteryManager
|
||||
import android.os.Bundle
|
||||
import android.os.Vibrator
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kr.smartsoltech.wellshe.R
|
||||
import kr.smartsoltech.wellshe.databinding.FragmentEmergencyBinding
|
||||
import kr.smartsoltech.wellshe.di.ViewModelFactory
|
||||
import kr.smartsoltech.wellshe.ui.emergency.EmergencyViewModel.EmergencyState
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Фрагмент для экрана экстренных оповещений
|
||||
*/
|
||||
class EmergencyFragment : Fragment(), LocationListener {
|
||||
|
||||
private var _binding: FragmentEmergencyBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: EmergencyViewModel
|
||||
|
||||
private lateinit var locationManager: LocationManager
|
||||
private var currentLocation: Location? = null
|
||||
private var isLocationUpdatesActive = false
|
||||
|
||||
// Интервал обновления местоположения (мс)
|
||||
private val normalUpdateInterval = 60000L // 1 минута
|
||||
private val emergencyUpdateInterval = 15000L // 15 секунд
|
||||
|
||||
// Коды для запросов разрешений
|
||||
private val locationPermissionRequestCode = 100
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentEmergencyBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Получаем ViewModel через DI
|
||||
viewModel = ViewModelProvider(this, viewModelFactory)[EmergencyViewModel::class.java]
|
||||
|
||||
// Инициализируем менеджер местоположения
|
||||
locationManager = requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
setupEmergencyButton()
|
||||
observeViewModel()
|
||||
checkLocationPermissions()
|
||||
}
|
||||
|
||||
private fun setupEmergencyButton() {
|
||||
// Настраиваем кнопку SOS
|
||||
binding.buttonSos.setOnClickListener {
|
||||
// Показываем диалог подтверждения
|
||||
showEmergencyConfirmationDialog()
|
||||
}
|
||||
|
||||
// Настраиваем кнопку отмены оповещения
|
||||
binding.buttonCancelAlert.setOnClickListener {
|
||||
showCancelConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmergencyConfirmationDialog() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Подтверждение")
|
||||
.setMessage("Вы уверены, что хотите отправить экстренное оповещение?")
|
||||
.setPositiveButton("Отправить") { _, _ ->
|
||||
createEmergencyAlert()
|
||||
}
|
||||
.setNegativeButton("Отмена", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showCancelConfirmationDialog() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Отмена оповещения")
|
||||
.setMessage("Вы уверены, что хотите отменить экстренное оповещение?")
|
||||
.setPositiveButton("Да") { _, _ ->
|
||||
viewModel.cancelAlert("Отменено пользователем")
|
||||
}
|
||||
.setNegativeButton("Нет", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun createEmergencyAlert() {
|
||||
// Проверяем доступность местоположения
|
||||
val location = currentLocation
|
||||
if (location != null) {
|
||||
// Получаем уровень заряда батареи
|
||||
val batteryLevel = getBatteryLevel()
|
||||
|
||||
// Создаем экстренное оповещение
|
||||
viewModel.createEmergencyAlert(
|
||||
latitude = location.latitude,
|
||||
longitude = location.longitude,
|
||||
batteryLevel = batteryLevel
|
||||
)
|
||||
|
||||
// Вибрируем, чтобы дать обратную связь пользователю
|
||||
vibratePhone()
|
||||
|
||||
// Активируем более частое обновление местоположения
|
||||
startLocationUpdates(true)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Не удалось определить местоположение", Toast.LENGTH_LONG).show()
|
||||
// Пробуем запросить местоположение принудительно
|
||||
requestLocationUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.emergencyState.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
is EmergencyState.Inactive -> {
|
||||
binding.emergencyStatusGroup.visibility = View.GONE
|
||||
binding.buttonSos.visibility = View.VISIBLE
|
||||
binding.buttonCancelAlert.visibility = View.GONE
|
||||
// При неактивном состоянии переключаемся на нормальную частоту обновлений
|
||||
startLocationUpdates(false)
|
||||
}
|
||||
is EmergencyState.Creating -> {
|
||||
binding.emergencyStatusGroup.visibility = View.VISIBLE
|
||||
binding.buttonSos.visibility = View.GONE
|
||||
binding.buttonCancelAlert.visibility = View.GONE
|
||||
binding.statusText.text = "Создание оповещения..."
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is EmergencyState.Active -> {
|
||||
binding.emergencyStatusGroup.visibility = View.VISIBLE
|
||||
binding.buttonSos.visibility = View.GONE
|
||||
binding.buttonCancelAlert.visibility = View.VISIBLE
|
||||
binding.statusText.text = "Оповещение активно\nID: ${state.alertResponse.alertId}"
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.statusIndicator.setImageResource(R.drawable.ic_status_active)
|
||||
}
|
||||
is EmergencyState.Cancelled -> {
|
||||
binding.emergencyStatusGroup.visibility = View.VISIBLE
|
||||
binding.buttonSos.visibility = View.VISIBLE
|
||||
binding.buttonCancelAlert.visibility = View.GONE
|
||||
binding.statusText.text = "Оповещение отменено"
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.statusIndicator.setImageResource(R.drawable.ic_status_cancelled)
|
||||
|
||||
// После короткой задержки скрываем статус и возвращаемся к нормальному состоянию
|
||||
binding.root.postDelayed({
|
||||
viewModel.resetState()
|
||||
}, 3000)
|
||||
}
|
||||
is EmergencyState.Resolved -> {
|
||||
binding.emergencyStatusGroup.visibility = View.VISIBLE
|
||||
binding.buttonSos.visibility = View.VISIBLE
|
||||
binding.buttonCancelAlert.visibility = View.GONE
|
||||
binding.statusText.text = "Оповещение обработано службами"
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.statusIndicator.setImageResource(R.drawable.ic_status_resolved)
|
||||
|
||||
// После короткой задержки скрываем статус
|
||||
binding.root.postDelayed({
|
||||
viewModel.resetState()
|
||||
}, 3000)
|
||||
}
|
||||
is EmergencyState.Error -> {
|
||||
binding.emergencyStatusGroup.visibility = View.VISIBLE
|
||||
binding.buttonSos.visibility = View.VISIBLE
|
||||
binding.buttonCancelAlert.visibility = View.GONE
|
||||
binding.statusText.text = "Ошибка: ${state.message}"
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.statusIndicator.setImageResource(R.drawable.ic_status_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.alertStatus.observe(viewLifecycleOwner) { status ->
|
||||
status?.let {
|
||||
// Обновляем информацию о статусе оповещения
|
||||
binding.notifiedContactsCount.text = "${it.notifiedContacts.count { contact -> contact.status == "notified" }}/${it.notifiedContacts.size}"
|
||||
|
||||
val emergencyServicesText = if (it.emergencyServicesNotified) {
|
||||
"Службы экстренной помощи уведомлены"
|
||||
} else {
|
||||
"Ожидаем ответа служб"
|
||||
}
|
||||
binding.emergencyServicesStatus.text = emergencyServicesText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBatteryLevel(): Int {
|
||||
val batteryManager = requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager
|
||||
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
|
||||
}
|
||||
|
||||
private fun vibratePhone() {
|
||||
val vibrator = requireContext().getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrator.vibrate(longArrayOf(0, 500, 200, 500), -1)
|
||||
}
|
||||
|
||||
private fun checkLocationPermissions() {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestLocationPermissions()
|
||||
} else {
|
||||
startLocationUpdates(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestLocationPermissions() {
|
||||
requestPermissions(
|
||||
arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
),
|
||||
locationPermissionRequestCode
|
||||
)
|
||||
}
|
||||
|
||||
private fun startLocationUpdates(isEmergency: Boolean) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Останавливаем предыдущие обновления, если они были
|
||||
if (isLocationUpdatesActive) {
|
||||
locationManager.removeUpdates(this)
|
||||
}
|
||||
|
||||
// Устанавливаем интервал обновления в зависимости от режима
|
||||
val updateInterval = if (isEmergency) emergencyUpdateInterval else normalUpdateInterval
|
||||
|
||||
try {
|
||||
// Запрашиваем обновления местоположения
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
updateInterval,
|
||||
10f, // минимальное расстояние для обновления (метры)
|
||||
this
|
||||
)
|
||||
isLocationUpdatesActive = true
|
||||
|
||||
// Сразу запрашиваем последнее известное местоположение
|
||||
requestLocationUpdate()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Ошибка получения местоположения", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestLocationUpdate() {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Пытаемся получить последнее известное местоположение
|
||||
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
if (lastLocation != null) {
|
||||
currentLocation = lastLocation
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
currentLocation = location
|
||||
|
||||
// Если оповещение активно, отправляем обновленные координаты
|
||||
if (viewModel.emergencyState.value is EmergencyState.Active) {
|
||||
viewModel.updateLocation(
|
||||
latitude = location.latitude,
|
||||
longitude = location.longitude,
|
||||
accuracy = location.accuracy,
|
||||
batteryLevel = getBatteryLevel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == locationPermissionRequestCode) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startLocationUpdates(false)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Для работы экстренных оповещений необходим доступ к местоположению",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// Если нет активного оповещения, останавливаем обновления местоположения
|
||||
if (viewModel.emergencyState.value !is EmergencyState.Active && isLocationUpdatesActive) {
|
||||
locationManager.removeUpdates(this)
|
||||
isLocationUpdatesActive = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Если есть активное оповещение, возобновляем обновления
|
||||
val isEmergency = viewModel.emergencyState.value is EmergencyState.Active
|
||||
if (isEmergency || !isLocationUpdatesActive) {
|
||||
startLocationUpdates(isEmergency)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
if (isLocationUpdatesActive) {
|
||||
locationManager.removeUpdates(this)
|
||||
isLocationUpdatesActive = false
|
||||
}
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package kr.smartsoltech.wellshe.ui.emergency
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EmergencyScreen(
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
var sendingSos by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Экстренная помощь") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onNavigateBack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Назад"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.7f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "ЭКСТРЕННАЯ СИТУАЦИЯ",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Используйте эту функцию только в случае реальной опасности",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка SOS
|
||||
Button(
|
||||
onClick = {
|
||||
sendingSos = true
|
||||
// Здесь должна быть логика отправки сигнала SOS
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
enabled = !sendingSos
|
||||
) {
|
||||
if (sendingSos) {
|
||||
CircularProgressIndicator(
|
||||
color = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Отправка сигнала SOS...")
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Send,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "ОТПРАВИТЬ СИГНАЛ SOS",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Экстренные контакты
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Экстренные контакты",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
EmergencyContactItem(
|
||||
title = "Полиция",
|
||||
phoneNumber = "102"
|
||||
)
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
EmergencyContactItem(
|
||||
title = "Скорая помощь",
|
||||
phoneNumber = "103"
|
||||
)
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
EmergencyContactItem(
|
||||
title = "Единый номер экстренных служб",
|
||||
phoneNumber = "112"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Текущее местоположение
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Ваше местоположение",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Определяется...",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmergencyContactItem(
|
||||
title: String,
|
||||
phoneNumber: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = phoneNumber,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { /* Действие звонка */ }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Call,
|
||||
contentDescription = "Позвонить",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package kr.smartsoltech.wellshe.ui.emergency
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kr.smartsoltech.wellshe.domain.emergency.*
|
||||
import kr.smartsoltech.wellshe.model.emergency.*
|
||||
import kr.smartsoltech.wellshe.util.Result
|
||||
|
||||
/**
|
||||
* ViewModel для управления экстренными оповещениями
|
||||
*/
|
||||
class EmergencyViewModel(
|
||||
private val createEmergencyAlertUseCase: CreateEmergencyAlertUseCase,
|
||||
private val getAlertStatusUseCase: GetAlertStatusUseCase,
|
||||
private val updateLocationUseCase: UpdateLocationUseCase,
|
||||
private val cancelAlertUseCase: CancelAlertUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _emergencyState = MutableLiveData<EmergencyState>(EmergencyState.Inactive)
|
||||
val emergencyState: LiveData<EmergencyState> = _emergencyState
|
||||
|
||||
private val _alertStatus = MutableLiveData<EmergencyAlertStatus?>()
|
||||
val alertStatus: LiveData<EmergencyAlertStatus?> = _alertStatus
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean> = _isLoading
|
||||
|
||||
private var currentAlertId: String? = null
|
||||
|
||||
/**
|
||||
* Создание нового экстренного оповещения
|
||||
*/
|
||||
fun createEmergencyAlert(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
message: String? = null,
|
||||
batteryLevel: Int? = null,
|
||||
contactIds: List<String>? = null
|
||||
) {
|
||||
_isLoading.value = true
|
||||
_emergencyState.value = EmergencyState.Creating
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = createEmergencyAlertUseCase(latitude, longitude, message, batteryLevel, contactIds)) {
|
||||
is Result.Success -> {
|
||||
currentAlertId = result.data.alertId
|
||||
_emergencyState.value = EmergencyState.Active(result.data)
|
||||
// После создания оповещения сразу получаем его статус
|
||||
fetchAlertStatus(result.data.alertId)
|
||||
}
|
||||
is Result.Error -> {
|
||||
_emergencyState.value = EmergencyState.Error(result.exception.message ?: "Ошибка создания оповещения")
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статуса экстренного оповещения
|
||||
*/
|
||||
fun fetchAlertStatus(alertId: String) {
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = getAlertStatusUseCase(alertId)) {
|
||||
is Result.Success -> {
|
||||
_alertStatus.value = result.data
|
||||
|
||||
// Обновляем состояние на основе статуса оповещения
|
||||
_emergencyState.value = when (result.data.status.lowercase()) {
|
||||
"active" -> EmergencyState.Active(EmergencyAlertResponse(
|
||||
alertId = result.data.alertId,
|
||||
createdAt = result.data.createdAt,
|
||||
status = result.data.status,
|
||||
message = "Оповещение активно"
|
||||
))
|
||||
"cancelled" -> EmergencyState.Cancelled(result.data.alertId)
|
||||
"resolved" -> EmergencyState.Resolved(result.data.alertId)
|
||||
else -> EmergencyState.Active(EmergencyAlertResponse(
|
||||
alertId = result.data.alertId,
|
||||
createdAt = result.data.createdAt,
|
||||
status = result.data.status,
|
||||
message = "Статус: ${result.data.status}"
|
||||
))
|
||||
}
|
||||
}
|
||||
is Result.Error -> {
|
||||
_emergencyState.value = EmergencyState.Error(result.exception.message ?: "Ошибка получения статуса")
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление местоположения для активного оповещения
|
||||
*/
|
||||
fun updateLocation(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
accuracy: Float? = null,
|
||||
batteryLevel: Int? = null
|
||||
) {
|
||||
val alertId = currentAlertId ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = updateLocationUseCase(alertId, latitude, longitude, accuracy, batteryLevel)) {
|
||||
is Result.Success -> {
|
||||
// После успешного обновления местоположения можно запросить текущий статус
|
||||
fetchAlertStatus(alertId)
|
||||
}
|
||||
is Result.Error -> {
|
||||
// Обработка ошибок обновления местоположения
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отмена экстренного оповещения
|
||||
*/
|
||||
fun cancelAlert(reason: String? = null, details: String? = null) {
|
||||
val alertId = currentAlertId ?: return
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = cancelAlertUseCase(alertId, reason, details)) {
|
||||
is Result.Success -> {
|
||||
_emergencyState.value = EmergencyState.Cancelled(alertId)
|
||||
// После отмены оповещения очищаем текущий ID оповещения
|
||||
currentAlertId = null
|
||||
}
|
||||
is Result.Error -> {
|
||||
_emergencyState.value = EmergencyState.Error(result.exception.message ?: "Ошибка отмены оповещения")
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сброс состояния (например, после завершения сценария экстренного оповещения)
|
||||
*/
|
||||
fun resetState() {
|
||||
currentAlertId = null
|
||||
_emergencyState.value = EmergencyState.Inactive
|
||||
_alertStatus.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Состояния экстренного оповещения
|
||||
*/
|
||||
sealed class EmergencyState {
|
||||
object Inactive : EmergencyState()
|
||||
object Creating : EmergencyState()
|
||||
data class Active(val alertResponse: EmergencyAlertResponse) : EmergencyState()
|
||||
data class Cancelled(val alertId: String) : EmergencyState()
|
||||
data class Resolved(val alertId: String) : EmergencyState()
|
||||
data class Error(val message: String) : EmergencyState()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package kr.smartsoltech.wellshe.ui.main
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kr.smartsoltech.wellshe.R
|
||||
|
||||
/**
|
||||
* Главный экран приложения
|
||||
*/
|
||||
class MainFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Инициализация UI и обработчиков событий
|
||||
}
|
||||
}
|
||||
114
app/src/main/java/kr/smartsoltech/wellshe/ui/main/MainScreen.kt
Normal file
114
app/src/main/java/kr/smartsoltech/wellshe/ui/main/MainScreen.kt
Normal file
@@ -0,0 +1,114 @@
|
||||
package kr.smartsoltech.wellshe.ui.main
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExitToApp
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kr.smartsoltech.wellshe.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
onNavigateToEmergency: () -> Unit,
|
||||
onLogout: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("WellShe") },
|
||||
actions = {
|
||||
IconButton(onClick = { onLogout() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExitToApp,
|
||||
contentDescription = "Выйти"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomAppBar {
|
||||
// Навигация внизу экрана, если необходимо
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = "Главный экран приложения WellShe",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = { onNavigateToEmergency() },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
modifier = Modifier.fillMaxWidth(0.8f)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null
|
||||
)
|
||||
Text("ЭКСТРЕННАЯ ПОМОЩЬ")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Добавление основных функциональных кнопок
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Функции приложения",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Кнопки для основных функций
|
||||
Button(
|
||||
onClick = { /* Действие */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Мой профиль")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Button(
|
||||
onClick = { /* Действие */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Настройки")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.smartsoltech.wellshe.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -9,20 +10,65 @@ import kr.smartsoltech.wellshe.ui.body.BodyScreen
|
||||
import kr.smartsoltech.wellshe.ui.cycle.CycleScreen
|
||||
import kr.smartsoltech.wellshe.ui.mood.MoodScreen
|
||||
import kr.smartsoltech.wellshe.ui.profile.ProfileScreen
|
||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
import kr.smartsoltech.wellshe.ui.auth.compose.LoginScreen
|
||||
import kr.smartsoltech.wellshe.ui.auth.compose.RegisterScreen
|
||||
import kr.smartsoltech.wellshe.ui.emergency.EmergencyScreen
|
||||
|
||||
@Composable
|
||||
fun AppNavGraph(
|
||||
navController: NavHostController,
|
||||
startDestination: String = BottomNavItem.Cycle.route
|
||||
startDestination: String = "login", // Меняем стартовый экран на авторизацию
|
||||
modifier: Modifier = Modifier,
|
||||
authViewModel: AuthViewModel // Добавляем параметр для доступа к AuthViewModel
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
startDestination = startDestination,
|
||||
modifier = modifier
|
||||
) {
|
||||
// Авторизация и регистрация
|
||||
composable("login") {
|
||||
LoginScreen(
|
||||
onNavigateToRegister = {
|
||||
navController.navigate("register")
|
||||
},
|
||||
onLoginSuccess = {
|
||||
navController.navigate(BottomNavItem.Cycle.route) {
|
||||
popUpTo("login") { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("register") {
|
||||
RegisterScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onRegisterSuccess = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Экран экстренной помощи
|
||||
composable("emergency") {
|
||||
EmergencyScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Существующие экраны
|
||||
composable(BottomNavItem.Cycle.route) {
|
||||
CycleScreen(
|
||||
onNavigateToSettings = {
|
||||
navController.navigate("cycle_settings")
|
||||
},
|
||||
onNavigateToEmergency = {
|
||||
navController.navigate("emergency")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -49,7 +95,17 @@ fun AppNavGraph(
|
||||
}
|
||||
|
||||
composable(BottomNavItem.Profile.route) {
|
||||
ProfileScreen()
|
||||
ProfileScreen(
|
||||
onLogout = {
|
||||
// Вызываем метод выхода из AuthViewModel, который очищает токены
|
||||
authViewModel.logout()
|
||||
// Переходим на экран авторизации после выхода
|
||||
navController.navigate("login") {
|
||||
// Очищаем весь стек навигации
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
import kr.smartsoltech.wellshe.ui.theme.WellSheTheme
|
||||
|
||||
// Добавляем явный импорт для BottomNavigation из того же пакета
|
||||
@@ -20,6 +22,8 @@ import kr.smartsoltech.wellshe.ui.navigation.BottomNavigation
|
||||
@Composable
|
||||
fun WellSheNavigation() {
|
||||
val navController = rememberNavController()
|
||||
// Получаем AuthViewModel с помощью viewModel() делегата
|
||||
val authViewModel: AuthViewModel = viewModel()
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
@@ -31,7 +35,11 @@ fun WellSheNavigation() {
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
AppNavGraph(navController = navController)
|
||||
// Передаем authViewModel в AppNavGraph
|
||||
AppNavGraph(
|
||||
navController = navController,
|
||||
authViewModel = authViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import kr.smartsoltech.wellshe.ui.theme.WellSheTheme
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onLogout: () -> Unit = {} // Добавляем параметр для выхода из аккаунта
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -295,6 +296,19 @@ fun ProfileScreen(
|
||||
Text("Экспорт настроек")
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка выхода из аккаунта
|
||||
Button(
|
||||
onClick = { onLogout() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("Выход из аккаунта")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
app/src/main/java/kr/smartsoltech/wellshe/util/Result.kt
Normal file
61
app/src/main/java/kr/smartsoltech/wellshe/util/Result.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package kr.smartsoltech.wellshe.util
|
||||
|
||||
/**
|
||||
* Класс для представления результатов операций, которые могут завершиться успешно или с ошибкой
|
||||
*/
|
||||
sealed class Result<out T> {
|
||||
data class Success<out T>(val data: T) : Result<T>()
|
||||
data class Error(val exception: Exception) : Result<Nothing>()
|
||||
|
||||
/**
|
||||
* Проверяет, был ли результат успешным
|
||||
*/
|
||||
val isSuccess: Boolean get() = this is Success
|
||||
|
||||
/**
|
||||
* Проверяет, содержит ли результат ошибку
|
||||
*/
|
||||
val isError: Boolean get() = this is Error
|
||||
|
||||
/**
|
||||
* Получает данные в случае успешного результата или null
|
||||
*/
|
||||
fun getOrNull(): T? = when (this) {
|
||||
is Success -> data
|
||||
is Error -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает исключение в случае ошибки или null
|
||||
*/
|
||||
fun exceptionOrNull(): Exception? = when (this) {
|
||||
is Success -> null
|
||||
is Error -> exception
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует успешный результат с помощью переданной функции
|
||||
*/
|
||||
inline fun <R> map(transform: (T) -> R): Result<R> {
|
||||
return when (this) {
|
||||
is Success -> Success(transform(data))
|
||||
is Error -> Error(exception)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет действие в зависимости от результата
|
||||
*/
|
||||
inline fun onSuccess(action: (T) -> Unit): Result<T> {
|
||||
if (this is Success) action(data)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет действие при ошибке
|
||||
*/
|
||||
inline fun onError(action: (Exception) -> Unit): Result<T> {
|
||||
if (this is Error) action(exception)
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package kr.smartsoltech.wellshe.util
|
||||
|
||||
import android.util.Patterns
|
||||
|
||||
/**
|
||||
* Проверяет, является ли строка корректным email
|
||||
*/
|
||||
fun String.isValidEmail(): Boolean {
|
||||
return isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли строка достаточно надежным паролем
|
||||
* Требования: минимум 8 символов, хотя бы 1 заглавная буква, 1 цифра, 1 специальный символ
|
||||
*/
|
||||
fun String.isValidPassword(): Boolean {
|
||||
if (length < 8) return false
|
||||
if (!contains(Regex("[A-Z]"))) return false
|
||||
if (!contains(Regex("[0-9]"))) return false
|
||||
if (!contains(Regex("[^A-Za-z0-9]"))) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли строка корректным телефонным номером в международном формате
|
||||
*/
|
||||
fun String.isValidPhone(): Boolean {
|
||||
return matches(Regex("^\\+[0-9]{10,15}\$"))
|
||||
}
|
||||
10
app/src/main/res/drawable/ic_status_active.xml
Normal file
10
app/src/main/res/drawable/ic_status_active.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.59,7.58L10,14.17l-2.59,-2.58L6,13l4,4 8,-8z"/>
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_status_cancelled.xml
Normal file
10
app/src/main/res/drawable/ic_status_cancelled.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM13,7h-2v6h2V7zM13,15h-2v2h2v-2z"/>
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_status_error.xml
Normal file
10
app/src/main/res/drawable/ic_status_error.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#F44336"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM13,7h-2v6h2V7zM13,15h-2v2h2v-2z"/>
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_status_resolved.xml
Normal file
10
app/src/main/res/drawable/ic_status_resolved.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#2196F3"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.59,7.58L10,14.17l-2.59,-2.58L6,13l4,4 8,-8z"/>
|
||||
</vector>
|
||||
|
||||
21
app/src/main/res/layout/activity_main.xml
Normal file
21
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
124
app/src/main/res/layout/fragment_emergency.xml
Normal file
124
app/src/main/res/layout/fragment_emergency.xml
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
tools:context=".ui.emergency.EmergencyFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Экстренная помощь"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonSos"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:text="SOS"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:backgroundTint="#FF0000"
|
||||
android:textColor="#FFFFFF"
|
||||
app:cornerRadius="100dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/emergencyStatusGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="statusIndicator,statusText,notifiedContactsCount,notifiedContactsLabel,emergencyServicesStatus,buttonCancelAlert" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/statusIndicator"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
||||
app:srcCompat="@drawable/ic_status_active" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Статус оповещения"
|
||||
android:textAlignment="center"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/statusIndicator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notifiedContactsLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Уведомлено контактов:"
|
||||
app:layout_constraintEnd_toStartOf="@+id/notifiedContactsCount"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/statusText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notifiedContactsCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="0/0"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/notifiedContactsLabel"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/notifiedContactsLabel" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emergencyServicesStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Ожидание ответа экстренных служб"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notifiedContactsLabel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonCancelAlert"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Отменить оповещение"
|
||||
android:backgroundTint="#FF9800"
|
||||
android:textColor="#FFFFFF"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/buttonSos" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
105
app/src/main/res/layout/fragment_login.xml
Normal file
105
app/src/main/res/layout/fragment_login.xml
Normal file
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
tools:context=".ui.auth.LoginFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Вход"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tilEmailUsername"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilEmailUsername"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:hint="Email или имя пользователя"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tilPassword">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etEmailUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textEmailAddress" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilPassword"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Пароль"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilEmailUsername"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnLogin">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnLogin"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Войти"
|
||||
android:enabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilPassword"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnRegister" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRegister"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Регистрация"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnLogin"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Button"
|
||||
tools:layout_editor_absoluteX="162dp"
|
||||
tools:layout_editor_absoluteY="364dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
36
app/src/main/res/layout/fragment_main.xml
Normal file
36
app/src/main/res/layout/fragment_main.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
tools:context=".ui.main.MainFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWelcome"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Добро пожаловать в WellShe!"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnEmergency"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Экстренная помощь"
|
||||
android:backgroundTint="#FF0000"
|
||||
android:textColor="#FFFFFF"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvWelcome"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<!-- Здесь будут другие элементы главного экрана -->
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
192
app/src/main/res/layout/fragment_register.xml
Normal file
192
app/src/main/res/layout/fragment_register.xml
Normal file
@@ -0,0 +1,192 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
tools:context=".ui.auth.RegisterFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Регистрация"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilEmail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="Email"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etEmail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textEmailAddress" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilUsername"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Имя пользователя"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilEmail">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilPassword"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Пароль"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilUsername">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilConfirmPassword"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Подтвердите пароль"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilPassword">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etConfirmPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilFirstName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Имя"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilConfirmPassword">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etFirstName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilLastName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Фамилия"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilFirstName">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLastName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilPhone"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Телефон"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilLastName">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPhone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="phone" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRegister"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Зарегистрироваться"
|
||||
android:enabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilPhone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnBackToLogin"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="У меня уже есть аккаунт"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnRegister" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
40
app/src/main/res/navigation/nav_graph.xml
Normal file
40
app/src/main/res/navigation/nav_graph.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/loginFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/loginFragment"
|
||||
android:name="kr.smartsoltech.wellshe.ui.auth.LoginFragment"
|
||||
android:label="fragment_login"
|
||||
tools:layout="@layout/fragment_login" >
|
||||
<action
|
||||
android:id="@+id/action_loginFragment_to_registerFragment"
|
||||
app:destination="@id/registerFragment" />
|
||||
<action
|
||||
android:id="@+id/action_loginFragment_to_mainFragment"
|
||||
app:destination="@id/mainFragment"
|
||||
app:popUpTo="@id/loginFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/registerFragment"
|
||||
android:name="kr.smartsoltech.wellshe.ui.auth.RegisterFragment"
|
||||
android:label="fragment_register"
|
||||
tools:layout="@layout/fragment_register" />
|
||||
<fragment
|
||||
android:id="@+id/mainFragment"
|
||||
android:name="kr.smartsoltech.wellshe.ui.main.MainFragment"
|
||||
android:label="fragment_main" >
|
||||
<action
|
||||
android:id="@+id/action_mainFragment_to_emergencyFragment"
|
||||
app:destination="@id/emergencyFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/emergencyFragment"
|
||||
android:name="kr.smartsoltech.wellshe.ui.emergency.EmergencyFragment"
|
||||
android:label="fragment_emergency"
|
||||
tools:layout="@layout/fragment_emergency" />
|
||||
</navigation>
|
||||
@@ -1,5 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.WellShe" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.WellShe" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
</style>
|
||||
</resources>
|
||||
15
app/src/main/res/xml/network_security_config.xml
Normal file
15
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Разрешаем незащищенное HTTP-соединение с IP-адресом 192.168.0.112 -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">192.168.0.112</domain>
|
||||
</domain-config>
|
||||
|
||||
<!-- Настройки по умолчанию - запрещаем незащищенный HTTP-трафик для других адресов -->
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
|
||||
Reference in New Issue
Block a user