diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ccda1..b50c38d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,5 +47,21 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktor") implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") + implementation("io.coil-kt.coil3:coil-compose-core:$coil") + + implementation("io.ktor:ktor-client-okhttp:$ktor") + implementation("io.ktor:ktor-client-logging:$ktor") + implementation("io.ktor:ktor-client-auth:$ktor") + + implementation("androidx.compose.material:material-icons-extended:1.6.3") + + implementation(platform("io.insert-koin:koin-bom:4.0.0")) + implementation("io.insert-koin:koin-core") + implementation("io.insert-koin:koin-android") + implementation("io.insert-koin:koin-compose") + implementation("io.insert-koin:koin-androidx-compose") + + implementation(platform("io.coil-kt.coil3:coil-bom:3.0.0")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2c02bd..b3798f5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ + android:label="@string/app_name"> diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt deleted file mode 100644 index aa33483..0000000 --- a/app/src/main/java/ru/myitschool/work/App.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ru.myitschool.work - -import android.app.Application -import android.content.Context - -class App: Application() { - override fun onCreate() { - super.onCreate() - context = this - } - - companion object { - lateinit var context: Context - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/Application.kt b/app/src/main/java/ru/myitschool/work/Application.kt new file mode 100644 index 0000000..f1e375d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/Application.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work + +import android.app.Application +import ru.myitschool.work.comp.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class Application : Application(){ + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@Application) + modules(appModule) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/MainActivity.kt b/app/src/main/java/ru/myitschool/work/MainActivity.kt new file mode 100644 index 0000000..b2ff542 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/MainActivity.kt @@ -0,0 +1,19 @@ +package ru.myitschool.work + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import ru.myitschool.work.ui.theme.WorkTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + WorkTheme { + AppNavHost() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/NavigationGraph.kt new file mode 100644 index 0000000..a8a7dd8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/NavigationGraph.kt @@ -0,0 +1,83 @@ +package ru.myitschool.work + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import ru.myitschool.work.comp.presentation.auth.AuthScreenRoot +import ru.myitschool.work.comp.presentation.bookingScreen.BookingScreenRoot +import ru.myitschool.work.comp.presentation.main.MainScreenRoot +import ru.myitschool.work.comp.domain.nav.AuthScreenDestination +import ru.myitschool.work.comp.domain.nav.BookScreenDestination +import ru.myitschool.work.comp.domain.nav.MainScreenDestination +import ru.myitschool.work.comp.domain.nav.SplashScreenDestination +import com.example.result.comp.presentation.splash.SplashScreen + +@Composable +fun AppNavHost( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController() +) { + NavHost( + modifier = modifier, + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None }, + navController = navController, + startDestination = SplashScreenDestination, + ) { + composable { + SplashScreen( + onNavigateToMainScreen = { + navController.navigate(MainScreenDestination){ + popUpTo(SplashScreenDestination){ + inclusive = true + } + } + }, + onNavigateToAuthScreen = { + navController.navigate(AuthScreenDestination){ + popUpTo(SplashScreenDestination){ + inclusive = true + } + } + } + ) + } + composable { + AuthScreenRoot( + onNavigateToMainScreen = { + navController.navigate(MainScreenDestination){ + popUpTo(AuthScreenDestination){ + inclusive = true + } + } + } + ) + } + composable { + MainScreenRoot( + onNavigateToAuthScreen = { + navController.navigate(AuthScreenDestination){ + popUpTo(MainScreenDestination){ + inclusive = true + } + } + }, + onNavigateToBookingScreen = { + navController.navigate(BookScreenDestination) + } + ) + } + composable { + BookingScreenRoot( + onNavigateBack = { + navController.popBackStack() + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/data/dtos/BookDto.kt b/app/src/main/java/ru/myitschool/work/comp/data/dtos/BookDto.kt new file mode 100644 index 0000000..b41c8dd --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/dtos/BookDto.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.comp.data.dtos + +import kotlinx.serialization.Serializable + +@Serializable +data class BookDto( + val date : String, + val placeId : Int +) diff --git a/app/src/main/java/ru/myitschool/work/comp/data/dtos/GetBookingsDto.kt b/app/src/main/java/ru/myitschool/work/comp/data/dtos/GetBookingsDto.kt new file mode 100644 index 0000000..6b92154 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/dtos/GetBookingsDto.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.comp.data.dtos + +import kotlinx.serialization.Serializable + +@Serializable +data class PlaceDto( + val id: Int, + val place: String +) + +typealias BookingsResponse = Map> diff --git a/app/src/main/java/ru/myitschool/work/comp/data/dtos/GetInfoDto.kt b/app/src/main/java/ru/myitschool/work/comp/data/dtos/GetInfoDto.kt new file mode 100644 index 0000000..f792dda --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/dtos/GetInfoDto.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.comp.data.dtos + +import kotlinx.serialization.Serializable + +@Serializable +data class GetInfoDto( + val photoUrl : String, + val booking : Map, + val name : String, +) + +@Serializable +data class BookingDto( + val id: Int, + val place: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/data/mappers/BookMapper.kt b/app/src/main/java/ru/myitschool/work/comp/data/mappers/BookMapper.kt new file mode 100644 index 0000000..445ddb7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/mappers/BookMapper.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.comp.data.mappers + +import ru.myitschool.work.comp.domain.model.BookModel +import ru.myitschool.work.comp.data.dtos.BookDto + + +fun BookModel.toDto(): BookDto { + return BookDto( + date = date, + placeId = placeId + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/data/mappers/GetBookingsMapper.kt b/app/src/main/java/ru/myitschool/work/comp/data/mappers/GetBookingsMapper.kt new file mode 100644 index 0000000..d13a744 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/mappers/GetBookingsMapper.kt @@ -0,0 +1,19 @@ +package ru.myitschool.work.comp.data.mappers + + +import ru.myitschool.work.comp.domain.model.GetBookingsModel +import ru.myitschool.work.comp.domain.model.PlaceModel +import ru.myitschool.work.comp.data.dtos.PlaceDto + +fun Map>.toModel(): GetBookingsModel { + return GetBookingsModel( + bookings = this.mapValues { (_, places) -> + places.map { placeDto -> + PlaceModel( + id = placeDto.id, + place = placeDto.place + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/data/mappers/GetInfoMapper.kt b/app/src/main/java/ru/myitschool/work/comp/data/mappers/GetInfoMapper.kt new file mode 100644 index 0000000..1ebbbb0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/mappers/GetInfoMapper.kt @@ -0,0 +1,24 @@ +package ru.myitschool.work.comp.data.mappers + +import ru.myitschool.work.comp.data.dtos.BookingDto +import ru.myitschool.work.comp.data.dtos.GetInfoDto +import ru.myitschool.work.comp.domain.model.BookingModel +import ru.myitschool.work.comp.domain.model.GetInfoModel + +fun GetInfoDto.toModel(): GetInfoModel { + return GetInfoModel( + photoUrl = photoUrl, + booking = booking.map { (date, bookingItem) -> + bookingItem.toModel(date) + }.toList(), + name = name, + ) +} + +fun BookingDto.toModel(date: String): BookingModel { + return BookingModel( + id = id, + date = date, + place = place + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/data/repository/AppRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/comp/data/repository/AppRepositoryImpl.kt new file mode 100644 index 0000000..a377997 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/data/repository/AppRepositoryImpl.kt @@ -0,0 +1,79 @@ +package ru.myitschool.work.comp.data.repository + +import ru.myitschool.work.comp.data.dtos.BookDto +import ru.myitschool.work.comp.data.dtos.GetInfoDto +import ru.myitschool.work.comp.data.dtos.PlaceDto +import ru.myitschool.work.comp.domain.repository.AppRepository +import ru.myitschool.work.core.auth.CodeProvider +import ru.myitschool.work.core.data.safeCall +import ru.myitschool.work.core.domain.Constants +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.contentType + +class AppRepositoryImpl( + private val httpClient: HttpClient, + private val codeProvider: CodeProvider +) : AppRepository { + private val code: String? + get() = codeProvider.getCode() + + override suspend fun auth(codeToSend: String): Result { + return try { + val response: HttpResponse = httpClient.get( + urlString = "/api/" + codeToSend + Constants.AUTH_URL + ) + + when (response.status.value) { + 200 -> { + Result.Success(Unit) + } + 401 -> { + Result.Error(DataError.Remote.UNAUTHORIZED) + } + 400 -> { + Result.Error(DataError.Remote.BAD_REQUEST) + } + else -> { + Result.Error(DataError.Remote.UNKNOWN) + } + } + } catch (e: Exception) { + e.printStackTrace() + Result.Error(DataError.Remote.NETWORK) + } + } + + override suspend fun getInfo(): Result { + return safeCall { + httpClient.get( + urlString = code + Constants.INFO_URL + ) + } + } + + override suspend fun getBooking(): Result>, DataError.Remote> { + return safeCall>> { + httpClient.get( + urlString = code + Constants.BOOKING_URL + ) + } + } + + override suspend fun book(bookDto: BookDto): Result { + return safeCall { + httpClient.post( + urlString = code + Constants.BOOK_URL + ) { + contentType(ContentType.Application.Json) + setBody(bookDto) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/di/AppModule.kt b/app/src/main/java/ru/myitschool/work/comp/di/AppModule.kt new file mode 100644 index 0000000..92e8ec4 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/di/AppModule.kt @@ -0,0 +1,56 @@ +package ru.myitschool.work.comp.di + +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences +import ru.myitschool.work.comp.data.repository.AppRepositoryImpl +import ru.myitschool.work.comp.domain.repository.AppRepository +import ru.myitschool.work.core.auth.SharedPrefCodeProvider +import ru.myitschool.work.core.data.HttpClientFactory +import ru.myitschool.work.comp.domain.use_case.AppUseCases +import ru.myitschool.work.comp.domain.use_case.AuthUseCase +import ru.myitschool.work.comp.domain.use_case.BookUseCase +import ru.myitschool.work.comp.domain.use_case.GetBookingUseCase +import ru.myitschool.work.comp.domain.use_case.GetInfoUseCase +import ru.myitschool.work.comp.presentation.auth.AuthViewModel +import ru.myitschool.work.comp.presentation.bookingScreen.BookingViewModel +import ru.myitschool.work.comp.presentation.main.MainScreenViewModel +import ru.myitschool.work.comp.presentation.splash.SplashScreenViewModel +import ru.myitschool.work.core.auth.CodeProvider +import ru.myitschool.work.core.domain.validation.ValidateCode +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val appModule = module { + single { + androidContext().getSharedPreferences("code", MODE_PRIVATE) + } + + single{SharedPrefCodeProvider(get())} + + single { HttpClientFactory.create() } + + single { ValidateCode() } + + single { AppRepositoryImpl(get(), get()) } + + single { AuthUseCase(get()) } + single { BookUseCase(get()) } + single { GetBookingUseCase(get()) } + single { GetInfoUseCase(get()) } + + single { + AppUseCases( + authUseCase = AuthUseCase(get()), + bookUseCase = BookUseCase(get()), + getBookingUseCase = GetBookingUseCase(get()), + getInfoUseCase = GetInfoUseCase(get()) + ) + } + + viewModelOf(::SplashScreenViewModel) + viewModelOf(::AuthViewModel) + viewModelOf(::MainScreenViewModel) + viewModelOf(::BookingViewModel) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/model/BookModel.kt b/app/src/main/java/ru/myitschool/work/comp/domain/model/BookModel.kt new file mode 100644 index 0000000..df2c846 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/model/BookModel.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.domain.model + +data class BookModel( + val date : String, + val placeId : Int +) diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/model/GetBookingsModel.kt b/app/src/main/java/ru/myitschool/work/comp/domain/model/GetBookingsModel.kt new file mode 100644 index 0000000..b6e07d6 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/model/GetBookingsModel.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.comp.domain.model + + +data class GetBookingsModel( + val bookings: Map> +) + +data class PlaceModel( + val id: Int, + val place: String +) diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/model/GetInfoModel.kt b/app/src/main/java/ru/myitschool/work/comp/domain/model/GetInfoModel.kt new file mode 100644 index 0000000..9488840 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/model/GetInfoModel.kt @@ -0,0 +1,13 @@ +package ru.myitschool.work.comp.domain.model + +data class GetInfoModel( + val photoUrl: String, + val booking: List, + val name: String, +) + +data class BookingModel( + val id: Int, + val date: String, + val place: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/nav/AppDestination.kt b/app/src/main/java/ru/myitschool/work/comp/domain/nav/AppDestination.kt new file mode 100644 index 0000000..5e585fc --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/nav/AppDestination.kt @@ -0,0 +1,3 @@ +package ru.myitschool.work.comp.domain.nav + +sealed interface AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/nav/AuthScreenDestination.kt b/app/src/main/java/ru/myitschool/work/comp/domain/nav/AuthScreenDestination.kt new file mode 100644 index 0000000..8eff656 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/nav/AuthScreenDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.domain.nav + +import kotlinx.serialization.Serializable + +@Serializable +data object AuthScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/nav/BookScreenDestination.kt b/app/src/main/java/ru/myitschool/work/comp/domain/nav/BookScreenDestination.kt new file mode 100644 index 0000000..7b45901 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/nav/BookScreenDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.domain.nav + +import kotlinx.serialization.Serializable + +@Serializable +data object BookScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/nav/MainScreenDestination.kt b/app/src/main/java/ru/myitschool/work/comp/domain/nav/MainScreenDestination.kt new file mode 100644 index 0000000..f6c2ef7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/nav/MainScreenDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.domain.nav + +import kotlinx.serialization.Serializable + +@Serializable +data object MainScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/nav/SplashScreenDestination.kt b/app/src/main/java/ru/myitschool/work/comp/domain/nav/SplashScreenDestination.kt new file mode 100644 index 0000000..bb71de9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/nav/SplashScreenDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.domain.nav + +import kotlinx.serialization.Serializable + +@Serializable +data object SplashScreenDestination : AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/repository/AppRepository.kt b/app/src/main/java/ru/myitschool/work/comp/domain/repository/AppRepository.kt new file mode 100644 index 0000000..8ff4c35 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/repository/AppRepository.kt @@ -0,0 +1,14 @@ +package ru.myitschool.work.comp.domain.repository + +import ru.myitschool.work.comp.data.dtos.BookDto +import ru.myitschool.work.comp.data.dtos.GetInfoDto +import ru.myitschool.work.comp.data.dtos.PlaceDto +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result + +interface AppRepository { + suspend fun auth(codeToSend: String): Result + suspend fun getInfo(): Result + suspend fun getBooking(): Result>, DataError.Remote> + suspend fun book(bookDto: BookDto): Result +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/use_case/AppUseCases.kt b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/AppUseCases.kt new file mode 100644 index 0000000..adb391b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/AppUseCases.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.comp.domain.use_case + +data class AppUseCases( + val authUseCase: AuthUseCase, + val bookUseCase: BookUseCase, + val getBookingUseCase: GetBookingUseCase, + val getInfoUseCase: GetInfoUseCase +) diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/use_case/AuthUseCase.kt b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/AuthUseCase.kt new file mode 100644 index 0000000..cf34c53 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/AuthUseCase.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.comp.domain.use_case +import ru.myitschool.work.comp.domain.repository.AppRepository +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result + +class AuthUseCase( + private val repository: AppRepository +) { + suspend operator fun invoke(codeToSend: String): Result { + return repository.auth(codeToSend) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/use_case/BookUseCase.kt b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/BookUseCase.kt new file mode 100644 index 0000000..efc8f9c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/BookUseCase.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.comp.domain.use_case + +import ru.myitschool.work.comp.data.mappers.toDto +import ru.myitschool.work.comp.domain.model.BookModel +import ru.myitschool.work.comp.domain.repository.AppRepository +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result + +class BookUseCase( + val repository: AppRepository, +) { + suspend operator fun invoke(bookModel: BookModel) : Result { + return repository + .book(bookModel.toDto()) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/use_case/GetBookingUseCase.kt b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/GetBookingUseCase.kt new file mode 100644 index 0000000..6fa4a32 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/GetBookingUseCase.kt @@ -0,0 +1,34 @@ +package ru.myitschool.work.comp.domain.use_case + +import ru.myitschool.work.comp.data.dtos.PlaceDto +import ru.myitschool.work.comp.domain.model.GetBookingsModel +import ru.myitschool.work.comp.domain.model.PlaceModel +import ru.myitschool.work.comp.domain.repository.AppRepository +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result +import ru.myitschool.work.core.domain.map + +class GetBookingUseCase( + private val repository: AppRepository +) { + suspend operator fun invoke(): Result { + return repository + .getBooking() + .map { bookingsMap: Map> -> + convertToModel(bookingsMap) + } + } + + private fun convertToModel(bookingsMap: Map>): GetBookingsModel { + return GetBookingsModel( + bookings = bookingsMap.mapValues { (_, places) -> + places.map { placeDto -> + PlaceModel( + id = placeDto.id, + place = placeDto.place + ) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/domain/use_case/GetInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/GetInfoUseCase.kt new file mode 100644 index 0000000..0ad390e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/domain/use_case/GetInfoUseCase.kt @@ -0,0 +1,20 @@ +package ru.myitschool.work.comp.domain.use_case + +import ru.myitschool.work.comp.data.mappers.toModel +import ru.myitschool.work.comp.domain.model.GetInfoModel +import ru.myitschool.work.comp.domain.repository.AppRepository +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result +import ru.myitschool.work.core.domain.map + +class GetInfoUseCase( + private val repository: AppRepository +) { + suspend operator fun invoke() : Result { + return repository + .getInfo() + .map { response -> + response.toModel() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthEvent.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthEvent.kt new file mode 100644 index 0000000..87f07f8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthEvent.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.presentation.auth + +interface AuthEvent { + data class CodeChanged(val code : String) : AuthEvent + object Submit : AuthEvent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthScreen.kt new file mode 100644 index 0000000..aae2adc --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthScreen.kt @@ -0,0 +1,226 @@ +package ru.myitschool.work.comp.presentation.auth + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ru.myitschool.work.comp.presentation.auth.component.CustomTextField +import ru.myitschool.work.core.domain.TestIds +import kotlinx.coroutines.flow.collectLatest +import org.koin.androidx.compose.koinViewModel +import ru.myitschool.work.ui.RememberWindowInfo +import ru.myitschool.work.ui.WindowInfo + +@Composable +fun AuthScreenRoot( + viewModel: AuthViewModel = koinViewModel(), + onNavigateToMainScreen : () -> Unit +) { + val windowInfo = RememberWindowInfo() + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + viewModel.uiEvent.collectLatest { event -> + when(event){ + UiEvent.Success -> { + onNavigateToMainScreen() + } + } + } + } + when{ + state.isLoading -> { + LoadingScreen() + } + else -> { + if (windowInfo.screenWidthInfo is WindowInfo.WindowType.Compact){ + AuthScreen( + state = state, + onEvent = { event -> + viewModel.authEvent(event) + } + ) + } else{ + TabletAuthScreen( + state = state, + onEvent = { event -> + viewModel.authEvent(event) + } + ) + } + + } + } +} + +@Composable +fun AuthScreen( + state : AuthState, + onEvent: (AuthEvent) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + text = "Привет! Введи код для авторизации", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + + CustomTextField( + modifier = Modifier + .testTag(TestIds.Auth.CODE_INPUT) + .fillMaxWidth(), + label = "Код", + value = state.code, + onValueChange = { + onEvent(AuthEvent.CodeChanged(it)) + }, + keyboardType = KeyboardType.Text, + error = state.authError + ) + Button( + onClick = { + onEvent(AuthEvent.Submit) + }, + modifier = Modifier + .testTag(TestIds.Auth.SIGN_BUTTON) + .padding(top = 24.dp) + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF007BFF), + disabledContainerColor = Color.Gray + ), + shape = RoundedCornerShape(12.dp), + enabled = state.isCodeValid + ) { + Text( + text = "Войти", + color = Color.White, + modifier = Modifier.padding(vertical = 6.dp) + ) + } + + } +} +@Composable +fun TabletAuthScreen( + state: AuthState, + onEvent: (AuthEvent) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 32.dp, vertical = 16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = "Привет! Введи код для авторизации", + style = MaterialTheme.typography.headlineMedium.copy(fontSize = 28.sp), + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 32.dp) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .sizeIn(maxWidth = 500.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CustomTextField( + modifier = Modifier + .testTag(TestIds.Auth.CODE_INPUT) + .width(500.dp), + label = "Код", + value = state.code, + onValueChange = { + onEvent(AuthEvent.CodeChanged(it)) + }, + keyboardType = KeyboardType.Text, + error = state.authError, + ) + + Button( + onClick = { + onEvent(AuthEvent.Submit) + }, + modifier = Modifier + .testTag(TestIds.Auth.SIGN_BUTTON) + .padding(top = 24.dp) + .width(200.dp) + .heightIn(min = 48.dp, max = 56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF007BFF), + disabledContainerColor = Color.Gray + ), + shape = RoundedCornerShape(12.dp), + enabled = state.isCodeValid + ) { + Text( + text = "Войти", + color = Color.White, + fontSize = 16.sp, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } + + Spacer(modifier = Modifier.height(40.dp)) + } +} + +@Composable +fun LoadingScreen () { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier.size(64.dp) + ) + } +} + + + diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthState.kt new file mode 100644 index 0000000..e060851 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthState.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.comp.presentation.auth + +data class AuthState( + val code : String = "", + val isLoading : Boolean = false, + val authError : String? = null, + val isCodeValid : Boolean = false +) diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthViewModel.kt new file mode 100644 index 0000000..c8e6cef --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/AuthViewModel.kt @@ -0,0 +1,81 @@ +package ru.myitschool.work.comp.presentation.auth + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ru.myitschool.work.comp.domain.use_case.AppUseCases +import ru.myitschool.work.core.auth.CodeProvider +import ru.myitschool.work.core.domain.onError +import ru.myitschool.work.core.domain.onSuccess +import ru.myitschool.work.core.domain.validation.ValidateCode +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class AuthViewModel( + private val codeProvider: CodeProvider, + private val appUseCases: AppUseCases, + private val validateCode: ValidateCode, +) : ViewModel() { + private val _state = MutableStateFlow(AuthState()) + val state = _state.asStateFlow() + + private val _uiEvent = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow() + + fun authEvent(event: AuthEvent){ + when(event){ + is AuthEvent.CodeChanged -> { + codeChanged(code = event.code) + } + is AuthEvent.Submit -> { + submit() + } + } + } + + private fun codeChanged(code : String){ + _state.update { + it.copy(code = code, authError = null) + } + val result = validateCode.execute(code) + if (!result.success){ + _state.update { + it.copy(isCodeValid = false) + } + }else{ + _state.update { + it.copy(isCodeValid = true) + } + } + } + + private fun submit(){ + viewModelScope.launch { + _state.update { + it.copy(isLoading = true) + } + appUseCases.authUseCase + .invoke(codeToSend = _state.value.code) + .onSuccess { + codeProvider.saveCode(code = _state.value.code) + _uiEvent.emit(UiEvent.Success) + _state.update { + it.copy(isLoading = false) + } + } + .onError { + _state.update { + it.copy(isLoading = false, authError = it.toString()) + } + + } + } + } +} + +sealed interface UiEvent{ + object Success : UiEvent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/auth/component/CustomTextField.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/component/CustomTextField.kt new file mode 100644 index 0000000..8d04f55 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/auth/component/CustomTextField.kt @@ -0,0 +1,96 @@ +package ru.myitschool.work.comp.presentation.auth.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +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.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme.shapes +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +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.compose.ui.unit.sp +import ru.myitschool.work.core.domain.TestIds + +@Composable +fun CustomTextField( + modifier: Modifier = Modifier, + label: String, + value: String, + onValueChange: (String) -> Unit, + isPassword: Boolean = false, + keyboardType: KeyboardType, + error: String? + ){ + var isPasswordVisible by remember { mutableStateOf(false) } + Column { + OutlinedTextField( + modifier = modifier + .padding(horizontal = 8.dp, vertical = 12.dp), + shape = shapes.large, + isError = error != null, + value = value, + onValueChange = onValueChange, + colors = OutlinedTextFieldDefaults.colors( + unfocusedLabelColor = Color(0xFF61758A), + unfocusedContainerColor = Color(0xFFF0F2F5), + unfocusedBorderColor = Color(0xFF61758A), + unfocusedTextColor = Color(0xFF61758A), + focusedLabelColor = Color(0xFF61758A), + focusedContainerColor = Color(0xFFF0F2F5), + focusedBorderColor = Color(0xFF61758A), + focusedTextColor = Color(0xFF61758A), + ), + label = { + Text( + text = label + ) + }, + visualTransformation = if(isPassword && !isPasswordVisible) PasswordVisualTransformation() else VisualTransformation.None, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType + ), + trailingIcon = { + if(isPassword) { + IconButton( + onClick = { + isPasswordVisible = !isPasswordVisible + } + ) { + Icon( + imageVector = if(isPasswordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility, + contentDescription = null, + ) + } + } + }, + singleLine = true + ) + error?.let { + Text( + modifier = Modifier + .testTag(TestIds.Auth.ERROR) + .padding(horizontal = 24.dp), + text = it, + fontSize = 15.sp, + color = Color(0xFFb73028) + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreen.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreen.kt new file mode 100644 index 0000000..3556816 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreen.kt @@ -0,0 +1,451 @@ +package ru.myitschool.work.comp.presentation.bookingScreen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ru.myitschool.work.comp.domain.model.PlaceModel +import ru.myitschool.work.comp.presentation.auth.LoadingScreen +import ru.myitschool.work.core.domain.TestIds +import org.koin.androidx.compose.koinViewModel +import ru.myitschool.work.ui.RememberWindowInfo +import ru.myitschool.work.ui.WindowInfo +import java.text.SimpleDateFormat +import java.util.* + +@Composable +fun BookingScreenRoot( + viewModel: BookingViewModel = koinViewModel(), + onNavigateBack: () -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val windowInfo = RememberWindowInfo() + val selectedDate = state.selectedDateKey + + when { + state.isLoading -> { + LoadingScreen() + } + + state.isError -> { + ErrorState( + onRefresh = { + viewModel.onEvent(BookingScreenEvent.RefreshData) + } + ) + } + + state.isEmpty -> { + EmptyState() + } + + else -> { + if (windowInfo.screenWidthInfo is WindowInfo.WindowType.Compact){ + BookingScreen( + state = state, + selectedDateKey = selectedDate, + onDateSelected = { dateKey -> + viewModel.onEvent(BookingScreenEvent.SelectDate(dateKey)) + }, + onPlaceSelected = { place -> + viewModel.onEvent(BookingScreenEvent.SelectPlace(place)) + }, + onNavigateBack = onNavigateBack, + onEvent = { event -> + viewModel.onEvent(event) + } + ) + } else{ + BookingScreen( + state = state, + selectedDateKey = selectedDate, + onDateSelected = { dateKey -> + viewModel.onEvent(BookingScreenEvent.SelectDate(dateKey)) + }, + onPlaceSelected = { place -> + viewModel.onEvent(BookingScreenEvent.SelectPlace(place)) + }, + onNavigateBack = onNavigateBack, + onEvent = { event -> + viewModel.onEvent(event) + } + ) + } + + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun BookingScreen( + state: BookingState, + selectedDateKey: String?, + onDateSelected: (String) -> Unit, + onPlaceSelected: (PlaceModel) -> Unit, + onNavigateBack: () -> Unit, + onEvent: (BookingScreenEvent) -> Unit +) { + val bookingsMap = state.bookings + val selectedPlaces = if (selectedDateKey != null) bookingsMap[selectedDateKey] else emptyList() + + Scaffold( + topBar = { + TopAppBar( + title = { + Text("Бронирование", + color = Color.Black + ) }, + navigationIcon = { + IconButton( + modifier = Modifier + .testTag(TestIds.Book.BACK_BUTTON), + onClick = onNavigateBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + tint = Color.Black + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.White + ) + ) + }, + bottomBar = { + if (!state.isEmpty && !state.isError && bookingsMap.isNotEmpty()) { + BookingBottomBar( + state = state, + onEvent = { onEvent(BookingScreenEvent.Book) }, + onNavigateBack = onNavigateBack + ) + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.White), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (bookingsMap.isNotEmpty()) { + DateTabs( + dateKeys = bookingsMap.keys.toList(), + selectedDateKey = selectedDateKey, + onDateSelected = onDateSelected + ) + + Spacer(modifier = Modifier.height(24.dp)) + if (selectedDateKey != null && selectedPlaces?.isNotEmpty() == true) { + PlacesList( + places = selectedPlaces, + selectedPlace = state.selectedPlace, + onPlaceSelected = onPlaceSelected + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TabletBookingScreen( + state: BookingState, + selectedDateKey: String?, + onDateSelected: (String) -> Unit, + onPlaceSelected: (PlaceModel) -> Unit, + onNavigateBack: () -> Unit, + onEvent: (BookingScreenEvent) -> Unit +) { + val bookingsMap = state.bookings + val selectedPlaces = if (selectedDateKey != null) bookingsMap[selectedDateKey] else emptyList() + + Scaffold( + topBar = { + TopAppBar( + modifier = Modifier + .background(Color.White), + title = { Text("Бронирование") }, + navigationIcon = { + IconButton( + modifier = Modifier + .testTag(TestIds.Book.BACK_BUTTON), + onClick = onNavigateBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + } + ) + }, + bottomBar = { + if (!state.isEmpty && !state.isError && bookingsMap.isNotEmpty()) { + BookingBottomBar( + state = state, + onEvent = { onEvent(BookingScreenEvent.Book) }, + onNavigateBack = onNavigateBack + ) + } + } + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .width(700.dp) + .height(600.dp) + .padding(paddingValues), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (bookingsMap.isNotEmpty()) { + DateTabs( + dateKeys = bookingsMap.keys.toList(), + selectedDateKey = selectedDateKey, + onDateSelected = onDateSelected + ) + + Spacer(modifier = Modifier.height(24.dp)) + if (selectedDateKey != null && selectedPlaces?.isNotEmpty() == true) { + PlacesList( + places = selectedPlaces, + selectedPlace = state.selectedPlace, + onPlaceSelected = onPlaceSelected + ) + } + } + } + } + } +} + +@Composable +private fun DateTabs( + dateKeys: List, + selectedDateKey: String?, + onDateSelected: (String) -> Unit +) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + itemsIndexed(dateKeys.sorted()) { index, dateKey -> + val isSelected = selectedDateKey == dateKey + + ElevatedAssistChip( + modifier = Modifier + .testTag(TestIds.Book.getIdDateItemByPosition(index)), + onClick = { onDateSelected(dateKey) }, + label = { + Text( + modifier = Modifier + .testTag(TestIds.Book.ITEM_DATE), + text = formatDate(dateKey), + color = Color.Black + ) + }, + border = BorderStroke( + width = 2.dp, + color = if (isSelected) { + Color(0xFF007BFF) + } else { + Color.Transparent + } + ), + colors = AssistChipDefaults.assistChipColors( + containerColor = Color.White + ) + ) + } + } +} + +@Composable +private fun PlacesList( + places: List, + selectedPlace: PlaceModel?, + onPlaceSelected: (PlaceModel) -> Unit +) { + Column( + modifier = Modifier.selectableGroup(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + places.forEach { place -> + val isSelected = selectedPlace?.id == place.id + + Surface( + shape = MaterialTheme.shapes.medium, + color = Color.Transparent, + tonalElevation = if (isSelected) 4.dp else 1.dp, + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onPlaceSelected(place) }, + role = Role.RadioButton + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_TEXT), + text = place.place, + style = MaterialTheme.typography.bodyLarge, + color = Color.Black + ) + + RadioButton( + selected = isSelected, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = Color(0xFF007BFF) + ) + ) + } + } + } + } +} + +@Composable +private fun BookingBottomBar( + state: BookingState, + onEvent: (BookingScreenEvent) -> Unit, + onNavigateBack: () -> Unit +) { + Surface( + tonalElevation = 8.dp, + color = Color.White + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = Modifier + .testTag(TestIds.Book.BOOK_BUTTON), + onClick = { + onEvent(BookingScreenEvent.Book) + onNavigateBack() + }, + enabled = state.selectedPlace != null && !state.isBookingLoading, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF007BFF) + ) + ) { + if (state.isBookingLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp + ) + } else { + Text("Забронировать", + color = Color.White) + } + } + } + } +} + +@Composable +private fun ErrorState(onRefresh: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Что-то пошло не так", + color = Color.White, + modifier = Modifier + .testTag(TestIds.Main.ERROR) + .padding(vertical = 6.dp) + ) + Button( + onClick = { + onRefresh() + }, + modifier = Modifier + .testTag(TestIds.Main.REFRESH_BUTTON) + .fillMaxWidth() + .padding(top = 24.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF007BFF)), + shape = RoundedCornerShape(12.dp), + ) { + Text( + text = "Попробовать заново", + color = Color.White, + modifier = Modifier.padding(vertical = 6.dp) + ) + } + } +} + +@Composable +private fun EmptyState() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier + .testTag(TestIds.Book.EMPTY), + text = "Всё забронировано", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + } +} + +private fun formatDate(dateString: String): String { + return try { + val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val outputFormat = SimpleDateFormat("dd.MM", Locale.getDefault()) + val date = inputFormat.parse(dateString) + outputFormat.format(date ?: return dateString) + } catch (e: Exception) { + dateString + } +} diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenEvent.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenEvent.kt new file mode 100644 index 0000000..65c6d43 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenEvent.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.comp.presentation.bookingScreen + +import ru.myitschool.work.comp.domain.model.PlaceModel + +sealed class BookingScreenEvent { + data object GetData : BookingScreenEvent() + data object RefreshData : BookingScreenEvent() + data class SelectDate(val date: String) : BookingScreenEvent() + data class SelectPlace(val place: PlaceModel) : BookingScreenEvent() + data object Book : BookingScreenEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenState.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenState.kt new file mode 100644 index 0000000..f7881dd --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenState.kt @@ -0,0 +1,15 @@ +package ru.myitschool.work.comp.presentation.bookingScreen + +import ru.myitschool.work.comp.domain.model.PlaceModel + +data class BookingState( + val bookings: Map> = emptyMap(), + val selectedDateKey: String? = null, + val selectedPlace: PlaceModel? = null, + val isLoading: Boolean = false, + val isBookingLoading: Boolean = false, + val isEmpty: Boolean = false, + val isBookingSuccess: Boolean = false, + val isError: Boolean = false +) + diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenViewModel.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenViewModel.kt new file mode 100644 index 0000000..14a79b4 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/bookingScreen/BookingScreenViewModel.kt @@ -0,0 +1,175 @@ +package ru.myitschool.work.comp.presentation.bookingScreen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ru.myitschool.work.comp.domain.model.BookModel +import ru.myitschool.work.comp.domain.use_case.BookUseCase +import ru.myitschool.work.comp.domain.use_case.GetBookingUseCase +import ru.myitschool.work.core.domain.Result +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class BookingViewModel( + private val getBookingUseCase: GetBookingUseCase, + private val bookUseCase: BookUseCase +) : ViewModel() { + + private val _state = MutableStateFlow(BookingState()) + val state = _state.asStateFlow() + + init { + loadBookings() + } + + fun onEvent(event: BookingScreenEvent) { + when (event) { + is BookingScreenEvent.GetData -> { + if (!_state.value.isError) { + loadBookings() + } + } + is BookingScreenEvent.RefreshData -> { + loadBookings() + } + is BookingScreenEvent.SelectDate -> { + selectDate(event.date) + } + is BookingScreenEvent.SelectPlace -> { + _state.update { currentState -> + currentState.copy( + selectedPlace = event.place, + isError = false + ) + } + } + is BookingScreenEvent.Book -> { + bookSelected() + } + } + } + + private fun selectDate(dateKey: String) { + _state.update { currentState -> + val placesForDate = currentState.bookings[dateKey] + val updatedSelectedPlace = if (currentState.selectedPlace != null && + placesForDate?.contains(currentState.selectedPlace) != true + ) { + placesForDate?.firstOrNull() + } else { + currentState.selectedPlace ?: placesForDate?.firstOrNull() + } + + currentState.copy( + selectedDateKey = dateKey, + selectedPlace = updatedSelectedPlace, + isError = false + ) + } + } + + private fun loadBookings() { + _state.update { it.copy(isLoading = true, isError = false) } + + viewModelScope.launch { + when (val result = getBookingUseCase()) { + is Result.Success -> { + val bookingsModel = result.data + val bookingsMap = bookingsModel.bookings + val hasAvailableBookings = bookingsMap.any { (_, places) -> + places.isNotEmpty() + } + + if (!hasAvailableBookings) { + _state.update { + it.copy( + bookings = emptyMap(), + selectedDateKey = null, + selectedPlace = null, + isLoading = false, + isEmpty = true, + isError = false + ) + } + return@launch + } + + val sortedDates = bookingsMap.keys.sorted() + + val selectedDateKey = _state.value.selectedDateKey ?: sortedDates.firstOrNull() + val selectedPlace = if (selectedDateKey != null) { + val currentPlace = _state.value.selectedPlace + val placesForDate = bookingsMap[selectedDateKey] + + if (currentPlace != null && placesForDate?.contains(currentPlace) == true) { + currentPlace + } else { + placesForDate?.firstOrNull() + } + } else { + null + } + + _state.update { + it.copy( + bookings = bookingsMap, + selectedDateKey = selectedDateKey, + selectedPlace = selectedPlace, + isLoading = false, + isEmpty = false, + isError = false, + isBookingSuccess = false + ) + } + } + is Result.Error -> { + _state.update { + it.copy( + isLoading = false, + isError = true, + isEmpty = false + ) + } + } + } + } + } + + private fun bookSelected() { + val currentState = _state.value + val selectedDateKey = currentState.selectedDateKey + val selectedPlace = currentState.selectedPlace + + if (selectedDateKey == null || selectedPlace == null) return + + _state.update { it.copy(isBookingLoading = true, isError = false) } + + viewModelScope.launch { + val bookModel = BookModel( + date = selectedDateKey, + placeId = selectedPlace.id + ) + + when (val result = bookUseCase(bookModel)) { + is Result.Success -> { + _state.update { + it.copy( + isBookingLoading = false, + isBookingSuccess = true, + ) + } + loadBookings() + } + is Result.Error -> { + _state.update { + it.copy( + isBookingLoading = false, + isError = true + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreen.kt new file mode 100644 index 0000000..d004890 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreen.kt @@ -0,0 +1,530 @@ +package ru.myitschool.work.comp.presentation.main + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil3.compose.rememberAsyncImagePainter +import ru.myitschool.work.comp.presentation.auth.LoadingScreen +import ru.myitschool.work.comp.presentation.main.component.BookingItem +import ru.myitschool.work.core.domain.TestIds +import org.koin.androidx.compose.koinViewModel +import ru.myitschool.work.ui.RememberWindowInfo +import ru.myitschool.work.ui.WindowInfo + + +@Composable +fun MainScreenRoot( + viewModel : MainScreenViewModel = koinViewModel(), + onNavigateToAuthScreen : () -> Unit, + onNavigateToBookingScreen : () -> Unit +) { + val state by viewModel.state.collectAsState() + val windowInfo = RememberWindowInfo() + when{ + state.isLoading -> { + LoadingScreen() + } + state.isError ->{ + if (windowInfo.screenWidthInfo is WindowInfo.WindowType.Compact){ + ErrorScreen { + viewModel.onEvent(MainScreenEvent.Refresh) + } + } else{ + TabletErrorScreen { + viewModel.onEvent(MainScreenEvent.Refresh) + } + } + + } + state.shouldNavigateToAuth -> { + onNavigateToAuthScreen() + } + else -> { + if (windowInfo.screenWidthInfo is WindowInfo.WindowType.Compact){ + MainScreen( + state = state, + onNavigateToAuthScreen = onNavigateToAuthScreen, + onNavigateToBookingScreen = onNavigateToBookingScreen, + onEvent = {event -> + viewModel.onEvent(event) + }, + onRefresh = {viewModel.onEvent(MainScreenEvent.Refresh)} + ) + } else{ + MainScreen( + state = state, + onNavigateToAuthScreen = onNavigateToAuthScreen, + onNavigateToBookingScreen = onNavigateToBookingScreen, + onEvent = {event -> + viewModel.onEvent(event) + }, + onRefresh = {viewModel.onEvent(MainScreenEvent.Refresh)} + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen( + state: MainScreenState, + onNavigateToAuthScreen : () -> Unit, + onNavigateToBookingScreen : () -> Unit, + onRefresh : () -> Unit, + onEvent : (MainScreenEvent) -> Unit +) { + val userInfo = state.data + + Scaffold( + floatingActionButton = { + FloatingActionButton( + modifier = Modifier + .testTag(TestIds.Main.ADD_BUTTON), + onClick = onNavigateToBookingScreen, + containerColor = Color(0xFF007BFF), + contentColor = Color.White + ) { + Icon( + Icons.Filled.Add, + contentDescription = null + ) + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.White) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .testTag(TestIds.Main.PROFILE_IMAGE) + .size(80.dp) + .clip(CircleShape) + .background(Color(0xFFcfe6ff)), + contentAlignment = Alignment.Center + ) { + if (userInfo?.photoUrl?.isNotEmpty() == true) { + Image(painter = rememberAsyncImagePainter(userInfo.photoUrl), + contentDescription = null, + modifier = Modifier + .size(80.dp) + .clip(CircleShape), contentScale = ContentScale.Crop) + } else { + Icon( + Icons.Filled.Person, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = Color.Black + ) + } + } + + Spacer(modifier = Modifier.size(16.dp)) + + Column { + Text( + text = userInfo?.name ?: "Пользователь", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier + .testTag(TestIds.Main.PROFILE_NAME) + ) + } + + Spacer(modifier = Modifier.size(16.dp)) + + Column{ + IconButton( + onClick = { + onEvent(MainScreenEvent.Logout) + onNavigateToAuthScreen() + }, + modifier = Modifier + .testTag(TestIds.Main.LOGOUT_BUTTON) + .padding(end = 8.dp) + ) { + Icon( + Icons.AutoMirrored.Filled.ExitToApp, + contentDescription = null, + tint = Color.Black, + modifier = Modifier.size(24.dp) + ) + } + } + } + } + Button( + onClick = { + onRefresh() + }, + modifier = Modifier + .testTag(TestIds.Main.REFRESH_BUTTON) + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF007BFF) + ) + ) { + Icon( + Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.White + ) + Spacer(modifier = Modifier.size(8.dp)) + Text("Обновить Данные", + color = Color.White) + } + Text( + text = "Мои Бронирования", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + ) + if (userInfo?.booking?.isEmpty() == true) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = "У вас пока нет бронирований", + style = MaterialTheme.typography.bodyLarge, + color = Color.Gray + ) + } + } else { + val sortedBookings = userInfo?.booking?.sortedBy { it.date } ?: emptyList() + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(sortedBookings) { index, booking -> + BookingItem( + booking = booking, + index = index, + modifier = Modifier + .testTag(TestIds.Main.getIdItemByPosition(index)) + .fillMaxWidth() + ) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TabletMainScreen( + state: MainScreenState, + onNavigateToAuthScreen : () -> Unit, + onNavigateToBookingScreen : () -> Unit, + onRefresh : () -> Unit, + onEvent : (MainScreenEvent) -> Unit +) { + val userInfo = state.data + + Scaffold( + floatingActionButton = { + FloatingActionButton( + modifier = Modifier + .testTag(TestIds.Main.ADD_BUTTON), + onClick = onNavigateToBookingScreen, + containerColor = Color(0xFF007BFF), + contentColor = Color.White + ) { + Icon( + Icons.Filled.Add, + contentDescription = null + ) + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.White) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .testTag(TestIds.Main.PROFILE_IMAGE) + .size(80.dp) + .clip(CircleShape) + .background(Color(0xFFcfe6ff)), + contentAlignment = Alignment.Center + ) { + if (userInfo?.photoUrl?.isNotEmpty() == true) { + Image(painter = rememberAsyncImagePainter(userInfo.photoUrl), + contentDescription = null, + modifier = Modifier + .size(80.dp) + .clip(CircleShape), contentScale = ContentScale.Crop) + } else { + Icon( + Icons.Filled.Person, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = Color.Black + ) + } + } + + Spacer(modifier = Modifier.size(16.dp)) + + Column { + Text( + text = userInfo?.name ?: "Пользователь", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier + .testTag(TestIds.Main.PROFILE_NAME) + ) + } + + Spacer(modifier = Modifier.weight(3f)) + + Column{ + IconButton( + onClick = { + onEvent(MainScreenEvent.Logout) + onNavigateToAuthScreen() + }, + modifier = Modifier + .testTag(TestIds.Main.LOGOUT_BUTTON) + .padding(end = 8.dp) + ) { + Icon( + Icons.AutoMirrored.Filled.ExitToApp, + contentDescription = null, + tint = Color.Black, + modifier = Modifier.size(24.dp) + ) + } + } + } + } + Button( + onClick = { + onRefresh() + }, + modifier = Modifier + .testTag(TestIds.Main.REFRESH_BUTTON) + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF007BFF) + ) + ) { + Icon( + Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text("Обновить данные") + } + Text( + text = "Мои Бронирования", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = Color.Black, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + ) + if (userInfo?.booking?.isEmpty() == true) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = "У вас пока нет бронирований", + style = MaterialTheme.typography.bodyLarge, + color = Color.Gray + ) + } + } else { + val sortedBookings = userInfo?.booking?.sortedBy { it.date } ?: emptyList() + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(sortedBookings) { index, booking -> + BookingItem( + booking = booking, + index = index, + modifier = Modifier + .testTag(TestIds.Main.getIdItemByPosition(index)) + .fillMaxWidth() + ) + } + } + } + } + } +} + +@Composable +fun ErrorScreen( + onRetry : () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Что-то пошло не так", + color = Color.White, + modifier = Modifier + .testTag(TestIds.Main.ERROR) + .padding(vertical = 6.dp) + ) + Button( + onClick = { + onRetry() + }, + modifier = Modifier + .testTag(TestIds.Main.REFRESH_BUTTON) + .fillMaxWidth() + .padding(top = 24.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF007BFF)), + shape = RoundedCornerShape(12.dp), + ) { + Text( + text = "Попробовать заново", + color = Color.White, + modifier = Modifier.padding(vertical = 6.dp) + ) + } + } +} + +@Composable +fun TabletErrorScreen( + onRetry : () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Что-то пошло не так", + color = Color.White, + modifier = Modifier + .testTag(TestIds.Main.ERROR) + .padding(vertical = 6.dp) + ) + Button( + onClick = { + onRetry() + }, + modifier = Modifier + .testTag(TestIds.Main.REFRESH_BUTTON) + .padding(top = 24.dp) + .width(400.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF007BFF)), + shape = RoundedCornerShape(12.dp), + ) { + Text( + text = "Попробовать заново", + color = Color.White, + modifier = Modifier.padding(vertical = 6.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenEvent.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenEvent.kt new file mode 100644 index 0000000..8e5c818 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenEvent.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.comp.presentation.main + +interface MainScreenEvent { + object Refresh : MainScreenEvent + object Logout : MainScreenEvent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenState.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenState.kt new file mode 100644 index 0000000..5dbdfa9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenState.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.comp.presentation.main + +import ru.myitschool.work.comp.domain.model.GetInfoModel + +data class MainScreenState( + val isLoading : Boolean = false, + val isError: Boolean = false, + val data : GetInfoModel? = null, + val shouldNavigateToAuth : Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenViewModel.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenViewModel.kt new file mode 100644 index 0000000..51c1e1d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/main/MainScreenViewModel.kt @@ -0,0 +1,76 @@ +package ru.myitschool.work.comp.presentation.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ru.myitschool.work.comp.domain.use_case.AppUseCases +import ru.myitschool.work.core.auth.CodeProvider +import ru.myitschool.work.core.domain.onError +import ru.myitschool.work.core.domain.onSuccess +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class MainScreenViewModel( + private val appUseCases: AppUseCases, + private val codeProvider: CodeProvider +) : ViewModel() { + private val _state = MutableStateFlow(MainScreenState()) + val state = _state.asStateFlow() + + init { + viewModelScope.launch { + val code = codeProvider.getCode() + if (code != null) { + getInfo() + } else { + _state.update { it.copy(shouldNavigateToAuth = true) } + } + } + } + + fun onEvent(event: MainScreenEvent){ + when(event){ + is MainScreenEvent.Refresh -> { + getInfo() + } + + is MainScreenEvent.Logout -> { + logOut() + } + } + } + + private fun getInfo(){ + viewModelScope.launch { + _state.update { + it.copy( + isLoading = true, + ) + } + appUseCases.getInfoUseCase + .invoke() + .onSuccess { info -> + _state.update { + it.copy( + isLoading = false, + data = info + ) + } + } + .onError { + _state.update { + it.copy( + isLoading = false, + isError = true, + ) + } + } + } + } + + private fun logOut(){ + codeProvider.clearCode() + } +} + diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/main/component/BookingItem.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/main/component/BookingItem.kt new file mode 100644 index 0000000..32200bd --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/main/component/BookingItem.kt @@ -0,0 +1,60 @@ +package ru.myitschool.work.comp.presentation.main.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import ru.myitschool.work.comp.domain.model.BookingModel +import ru.myitschool.work.core.domain.TestIds + +@Composable +fun BookingItem( + booking: BookingModel, + index: Int, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = booking.date, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier + .testTag(TestIds.Main.ITEM_DATE) + .fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = booking.place, + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray, + modifier = Modifier + .testTag(TestIds.Main.ITEM_PLACE) + .fillMaxWidth() + ) + } + } +} + diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreen.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreen.kt new file mode 100644 index 0000000..d1d5a6e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreen.kt @@ -0,0 +1,67 @@ +package com.example.result.comp.presentation.splash + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import ru.myitschool.work.R +import ru.myitschool.work.comp.presentation.splash.SplashScreenViewModel + +@Composable +fun SplashScreen( + onNavigateToAuthScreen : () -> Unit, + onNavigateToMainScreen : () -> Unit, + viewModel: SplashScreenViewModel = koinViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val size = remember { Animatable(initialValue = 75.dp, Dp.VectorConverter) } + + LaunchedEffect(key1 = Unit) { + launch { + while (true){ + size.animateTo( + targetValue = 200.dp, + animationSpec = tween(durationMillis = 800, easing = LinearEasing) + ) + size.animateTo( + targetValue = 150.dp, + animationSpec = tween(durationMillis = 800, easing = LinearEasing) + ) + } + } + delay(1500) + if (state.isAuthenticated == true){ + onNavigateToMainScreen() + }else{ + onNavigateToAuthScreen() + } + } + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ){ + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier.size(size.value) + ) + } +} diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreenState.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreenState.kt new file mode 100644 index 0000000..f56ea20 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreenState.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.comp.presentation.splash + +data class SplashScreenState( + val isAuthenticated : Boolean? = null +) diff --git a/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreenViewModel.kt b/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreenViewModel.kt new file mode 100644 index 0000000..f0a1f56 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/comp/presentation/splash/SplashScreenViewModel.kt @@ -0,0 +1,35 @@ +package ru.myitschool.work.comp.presentation.splash + +import androidx.lifecycle.ViewModel +import ru.myitschool.work.core.auth.CodeProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class SplashScreenViewModel( + private val codeProvider: CodeProvider +) : ViewModel() { + private val _state = MutableStateFlow(SplashScreenState()) + val state = _state.asStateFlow() + + init { + isAuthenticated() + } + + private fun isAuthenticated(){ + val code = codeProvider.getCode() + if(!code.isNullOrBlank()){ + _state.update { + it.copy( + isAuthenticated = true + ) + } + }else{ + _state.update { + it.copy( + isAuthenticated = false + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/auth/CodeProvider.kt b/app/src/main/java/ru/myitschool/work/core/auth/CodeProvider.kt new file mode 100644 index 0000000..434f95f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/auth/CodeProvider.kt @@ -0,0 +1,30 @@ +package ru.myitschool.work.core.auth + +import android.content.SharedPreferences +import androidx.core.content.edit + +interface CodeProvider { + fun getCode() : String? + fun saveCode(code : String) + fun clearCode() +} + +class SharedPrefCodeProvider( + private val sharedPreferences: SharedPreferences, +) : CodeProvider { + override fun getCode(): String? { + return sharedPreferences.getString("code", null) + } + + override fun saveCode(code: String) { + sharedPreferences.edit { + putString("code", "/api/$code") + } + } + + override fun clearCode() { + sharedPreferences.edit { + remove("code",) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/data/HttpClientExtension.kt b/app/src/main/java/ru/myitschool/work/core/data/HttpClientExtension.kt new file mode 100644 index 0000000..2c40a0a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/data/HttpClientExtension.kt @@ -0,0 +1,45 @@ +package ru.myitschool.work.core.data + +import ru.myitschool.work.core.domain.DataError +import ru.myitschool.work.core.domain.Result +import io.ktor.client.call.NoTransformationFoundException +import io.ktor.client.call.body +import io.ktor.client.network.sockets.SocketTimeoutException +import io.ktor.client.statement.HttpResponse +import io.ktor.util.network.UnresolvedAddressException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive + +suspend inline fun safeCall( + execute: () -> HttpResponse +) : Result{ + val response = try{ + execute() + }catch (e: SocketTimeoutException){ + return Result.Error(DataError.Remote.REQUEST_TIMEOUT) + }catch (e: UnresolvedAddressException){ + return Result.Error(DataError.Remote.NO_INTERNET) + }catch (e: Exception){ + currentCoroutineContext().ensureActive() + return Result.Error(DataError.Remote.UNKNOWN) + } + return responseToResult(response) +} + +suspend inline fun responseToResult( + response : HttpResponse +) : Result{ + return when(response.status.value){ + in 200..290 -> { + try { + Result.Success(response.body()) + }catch (e: NoTransformationFoundException){ + Result.Error(DataError.Remote.SERIALIZATION) + } + } + 400 -> Result.Error(DataError.Remote.REQUEST_TIMEOUT) + 429 -> Result.Error(DataError.Remote.TOO_MANY_REQUESTS) + in 500..599 -> Result.Error(DataError.Remote.SERVER_ERROR) + else -> Result.Error(DataError.Remote.UNKNOWN) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/data/HttpClientFactory.kt b/app/src/main/java/ru/myitschool/work/core/data/HttpClientFactory.kt new file mode 100644 index 0000000..bde9760 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/data/HttpClientFactory.kt @@ -0,0 +1,45 @@ +package ru.myitschool.work.core.data + +import ru.myitschool.work.core.domain.Constants +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.accept +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +object HttpClientFactory { + fun create(): HttpClient { + val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = true + isLenient + } + + return HttpClient(OkHttp) { + install(ContentNegotiation) { + json(json) + } + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + println(message) + } + } + level = LogLevel.ALL + } + defaultRequest { + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + url(Constants.HOST) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/domain/Constants.kt similarity index 63% rename from app/src/main/java/ru/myitschool/work/core/Constants.kt rename to app/src/main/java/ru/myitschool/work/core/domain/Constants.kt index a8b7cc5..85bcd8c 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/domain/Constants.kt @@ -1,7 +1,7 @@ -package ru.myitschool.work.core +package ru.myitschool.work.core.domain object Constants { - const val HOST = "http://10.0.2.2:8080" + const val HOST = "http://192.168.1.106:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/core/domain/DataError.kt b/app/src/main/java/ru/myitschool/work/core/domain/DataError.kt new file mode 100644 index 0000000..4a279e1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/domain/DataError.kt @@ -0,0 +1,19 @@ +package ru.myitschool.work.core.domain + +interface DataError : Error { + enum class Remote : DataError{ + REQUEST_TIMEOUT, + TOO_MANY_REQUESTS, + NO_INTERNET, + SERVER_ERROR, + SERIALIZATION, + UNKNOWN, + UNAUTHORIZED, + BAD_REQUEST, + NETWORK + } + enum class Local : DataError{ + DISK_FULL, + UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/domain/Error.kt b/app/src/main/java/ru/myitschool/work/core/domain/Error.kt new file mode 100644 index 0000000..4914933 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/domain/Error.kt @@ -0,0 +1,3 @@ +package ru.myitschool.work.core.domain + +interface Error \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/domain/Result.kt b/app/src/main/java/ru/myitschool/work/core/domain/Result.kt new file mode 100644 index 0000000..0f13f6e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/domain/Result.kt @@ -0,0 +1,31 @@ +package ru.myitschool.work.core.domain + +sealed interface Result{ + data class Success(val data : D) : Result + data class Error(val error : E) : Result +} +inline fun Result.map(map : (T) -> (R)) : Result{ + return when(this){ + is Result.Success -> Result.Success(map(data)) + is Result.Error -> this + } +} + +inline fun Result.onSuccess(action : (T) -> Unit) : Result{ + return when(this){ + is Result.Success -> { + action(data) + this + } + is Result.Error -> this + } +} +inline fun Result.onError(action : (E) -> Unit) : Result{ + return when(this){ + is Result.Success -> this + is Result.Error -> { + action(error) + this + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/TestIds.kt b/app/src/main/java/ru/myitschool/work/core/domain/TestIds.kt similarity index 96% rename from app/src/main/java/ru/myitschool/work/core/TestIds.kt rename to app/src/main/java/ru/myitschool/work/core/domain/TestIds.kt index d67b884..86471c6 100644 --- a/app/src/main/java/ru/myitschool/work/core/TestIds.kt +++ b/app/src/main/java/ru/myitschool/work/core/domain/TestIds.kt @@ -1,4 +1,4 @@ -package ru.myitschool.work.core +package ru.myitschool.work.core.domain object TestIds { object Auth { diff --git a/app/src/main/java/ru/myitschool/work/core/domain/validation/ValidateCode.kt b/app/src/main/java/ru/myitschool/work/core/domain/validation/ValidateCode.kt new file mode 100644 index 0000000..724cb28 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/domain/validation/ValidateCode.kt @@ -0,0 +1,31 @@ +package ru.myitschool.work.core.domain.validation + +import java.util.regex.Pattern + +class ValidateCode { + fun execute(code : String) : ValidationResult { + if(code.isBlank()){ + return ValidationResult( + success = false, + errorMessage = "Поле не может быть пустым" + ) + } + if(code.length != 4){ + return ValidationResult( + success = false, + errorMessage = "Длина кода должна быть рава 4-ем" + ) + } + val pattern = Pattern.compile("^[A-Za-z0-9]+\$") + if(!pattern.matcher(code).matches()){ + return ValidationResult( + success = false, + errorMessage = "Код может содержать только латинские буквы, цифры и специальные символы" + ) + } + return ValidationResult( + success = true + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/domain/validation/ValidationResult.kt b/app/src/main/java/ru/myitschool/work/core/domain/validation/ValidationResult.kt new file mode 100644 index 0000000..4af0f44 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/domain/validation/ValidationResult.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.core.domain.validation + +data class ValidationResult( + val success: Boolean, + val errorMessage : String? = null +) diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt deleted file mode 100644 index 3ef28f1..0000000 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ru.myitschool.work.data.repo - -import ru.myitschool.work.data.source.NetworkDataSource - -object AuthRepository { - - private var codeCache: String? = null - - suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt deleted file mode 100644 index fbdfef5..0000000 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ /dev/null @@ -1,42 +0,0 @@ -package ru.myitschool.work.data.source - -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpStatusCode -import io.ktor.serialization.kotlinx.json.json -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import ru.myitschool.work.core.Constants - -object NetworkDataSource { - private val client by lazy { - HttpClient(CIO) { - install(ContentNegotiation) { - json( - Json { - isLenient = true - ignoreUnknownKeys = true - explicitNulls = true - encodeDefaults = true - } - ) - } - } - } - - suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) - when (response.status) { - HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) - } - } - } - - private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt deleted file mode 100644 index 012fb6f..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ru.myitschool.work.domain.auth - -import ru.myitschool.work.data.repo.AuthRepository - -class CheckAndSaveAuthCodeUseCase( - private val repository: AuthRepository -) { - suspend operator fun invoke( - text: String - ): Result { - return repository.checkAndSave(text).mapCatching { success -> - if (!success) error("Code is incorrect") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/RememberWindowInfo.kt b/app/src/main/java/ru/myitschool/work/ui/RememberWindowInfo.kt new file mode 100644 index 0000000..9feefbe --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/RememberWindowInfo.kt @@ -0,0 +1,38 @@ +package ru.myitschool.work.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun RememberWindowInfo() : WindowInfo { + val configuration = LocalConfiguration.current + return WindowInfo( + screenWidthInfo = when{ + configuration.screenWidthDp < 600 -> WindowInfo.WindowType.Compact + configuration.screenWidthDp < 840 -> WindowInfo.WindowType.Medium + else -> WindowInfo.WindowType.Expanded + }, + screenHeightInfo = when{ + configuration.screenHeightDp < 480 -> WindowInfo.WindowType.Compact + configuration.screenHeightDp < 900 -> WindowInfo.WindowType.Medium + else -> WindowInfo.WindowType.Expanded + }, + screenWidth = configuration.screenWidthDp.dp, + screenHeight = configuration.screenHeightDp.dp + ) +} + +data class WindowInfo( + val screenWidthInfo : WindowType, + val screenHeightInfo : WindowType, + val screenWidth : Dp, + val screenHeight: Dp +){ + sealed class WindowType{ + object Compact : WindowType() + object Medium : WindowType() + object Expanded : WindowType() + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/AppDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/AppDestination.kt deleted file mode 100644 index 557b893..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/nav/AppDestination.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ru.myitschool.work.ui.nav - -sealed interface AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt deleted file mode 100644 index 52660b1..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.ui.nav - -import kotlinx.serialization.Serializable - -@Serializable -data object AuthScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt deleted file mode 100644 index 9a33073..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.ui.nav - -import kotlinx.serialization.Serializable - -@Serializable -data object BookScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt deleted file mode 100644 index deca45f..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.ui.nav - -import kotlinx.serialization.Serializable - -@Serializable -data object MainScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt deleted file mode 100644 index 54b156d..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.myitschool.work.ui.root - -import android.os.Bundle -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.Scaffold -import androidx.compose.ui.Modifier -import ru.myitschool.work.ui.screen.AppNavHost -import ru.myitschool.work.ui.theme.WorkTheme - -class RootActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - WorkTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - AppNavHost( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt deleted file mode 100644 index 01b0f32..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ru.myitschool.work.ui.screen - -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import ru.myitschool.work.ui.nav.AuthScreenDestination -import ru.myitschool.work.ui.nav.BookScreenDestination -import ru.myitschool.work.ui.nav.MainScreenDestination -import ru.myitschool.work.ui.screen.auth.AuthScreen - -@Composable -fun AppNavHost( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController() -) { - NavHost( - modifier = modifier, - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, - navController = navController, - startDestination = AuthScreenDestination, - ) { - composable { - AuthScreen(navController = navController) - } - composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } - } - composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt deleted file mode 100644 index 74f200a..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.ui.screen.auth - -sealed interface AuthIntent { - data class Send(val text: String): AuthIntent - data class TextInput(val text: String): AuthIntent -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt deleted file mode 100644 index f99978e..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ /dev/null @@ -1,97 +0,0 @@ -package ru.myitschool.work.ui.screen.auth - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController -import ru.myitschool.work.R -import ru.myitschool.work.core.TestIds -import ru.myitschool.work.ui.nav.MainScreenDestination - -@Composable -fun AuthScreen( - viewModel: AuthViewModel = viewModel(), - navController: NavController -) { - val state by viewModel.uiState.collectAsState() - - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(all = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(R.string.auth_title), - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - when (val currentState = state) { - is AuthState.Data -> Content(viewModel, currentState) - is AuthState.Loading -> { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) - ) - } - } - } -} - -@Composable -private fun Content( - viewModel: AuthViewModel, - state: AuthState.Data -) { - var inputText by remember { mutableStateOf("") } - Spacer(modifier = Modifier.size(16.dp)) - TextField( - modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), - value = inputText, - onValueChange = { - inputText = it - viewModel.onIntent(AuthIntent.TextInput(it)) - }, - label = { Text(stringResource(R.string.auth_label)) } - ) - Spacer(modifier = Modifier.size(16.dp)) - Button( - modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), - onClick = { - viewModel.onIntent(AuthIntent.Send(inputText)) - }, - enabled = true - ) { - Text(stringResource(R.string.auth_sign_in)) - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt deleted file mode 100644 index a06ba76..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.ui.screen.auth - -sealed interface AuthState { - object Loading: AuthState - object Data: AuthState -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt deleted file mode 100644 index 3153640..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ /dev/null @@ -1,43 +0,0 @@ -package ru.myitschool.work.ui.screen.auth - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ru.myitschool.work.data.repo.AuthRepository -import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase - -class AuthViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(AuthState.Data) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow - - fun onIntent(intent: AuthIntent) { - when (intent) { - is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { - _uiState.update { AuthState.Loading } - checkAndSaveAuthCodeUseCase.invoke("9999").fold( - onSuccess = { - _actionFlow.emit(Unit) - }, - onFailure = { error -> - error.printStackTrace() - _actionFlow.emit(Unit) - } - ) - } - } - is AuthIntent.TextInput -> Unit - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa8bda6..b501947 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,12 @@ - Work - RootActivity + Result + Войти + Код Привет! Введи код для авторизации - Код - Войти + Пользователь + Обновить данные + Мои бронирования + У вас пока нет бронирований + Что-то пошло не так + Попробовать заново \ No newline at end of file