diff --git a/README.md b/README.md
new file mode 100644
index 00000000..131a8e3c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,148 @@
+# Reed - 문장과 감정을 함께 담는 독서 기록
+
+[](https://kotlinlang.org)
+[](https://gradle.org/)
+[](https://developer.android.com/studio)
+[](https://developer.android.com/distribute/best-practices/develop/target-sdk)
+[](https://developer.android.com/distribute/best-practices/develop/target-sdk)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Features
+| 홈 | 도서 검색 및 등록 | 내서재 |
+|:---:|:---:|:---:|
+|
|
|
|
+
+| OCR | 기록 등록 | 도서 & 기록 상세 |
+|:---:|:---:|:---:|
+|
|
|
|
+
+| 기록 카드 공유 |
+|:---:|
+|
|
+
+## TroubleShooting
+- [[Compose] M3 ModalBottomSheet 드래그(터치 이벤트) 막는 법](https://velog.io/@mraz3068/Compose-M3-ModalBottomSheet-Drag-Disabled)
+- [Circuit 찍먹해보기(부제: Circuit 희망편)](https://speakerdeck.com/easyhooon/circuit-jjigmeoghaebogi-buje-circuit-hyimangpyeon)
+- [Circuit 찍먹해보기(부제: Circuit 절망편)](https://speakerdeck.com/easyhooon/circuit-jjigmeoghaebogi-buje-circuit-jeolmangpyeon)
+- [Jetpack Compose에서 CameraX + MLKit으로 OCR을 구현해보자](https://velog.io/@syoon513/Jetpack-Compose%EC%97%90%EC%84%9C-CameraX-MLKit%EC%9C%BC%EB%A1%9C-OCR%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90)
+- [[Android] 일회성 이벤트를 StateFlow, Compose의 State로 처리할 때 주의해야할 점](https://velog.io/@mraz3068/Handle-One-Time-Event-As-State)
+- [Circuit Navigation 사용 시 feature 모듈간의 참조는 어떻게 해결했을까?](https://velog.io/@syoon513/Circuit-Navigation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-feature-%EB%AA%A8%EB%93%88%EA%B0%84-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%96%88%EC%9D%84%EA%B9%8C)
+- [Coroutine 에러 처리 패턴: 여러 API 호출을 한 번에 성공/실패 판정하기](https://velog.io/@syoon513/Coroutine-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC)
+- [[Circuit] ImpressionEffect](https://velog.io/@mraz3068/Circuit-ImpressionEffect)
+- [Coroutine CancellationException 따로 처리해야하는 케이스](https://velog.io/@mraz3068/Coroutine-CancellationException-UseCase)
+
+## Development
+
+### Required
+
+- IDE : Android Studio 최신 버전
+- JDK : Java 17을 실행할 수 있는 JDK
+ - (권장) Android Studio 설치 시 Embedded 된 JDK (Open JDK)
+ - Java 17을 사용하는 JDK (Open JDK, AdoptOpenJDK, GraalVM)
+- Kotlin Language : 2.2.0
+
+### Language
+
+- Kotlin
+
+### Libraries
+
+- AndroidX
+ - Activity Compose
+ - Core
+ - DataStore
+ - StartUp
+ - Splash
+ - CameraX
+
+- Kotlin Libraries (Coroutine, Serialization, Immutable Collection)
+- Compose
+ - Material3
+
+- [Circuit](https://github.com/slackhq/circuit)
+- ~~Google ML Kit~~ Google Cloud Vision
+- Dagger Hilt
+- Retrofit, OkHttp3
+- Lottie-Compose
+- Firebase(Analytics, Crashlytics, Remote Config)
+- Kakao-Auth
+- [Logger](https://github.com/orhanobut/logger)
+- [Compose-Stable-Marker](https://github.com/skydoves/compose-stable-marker)
+- [Landscapist](https://github.com/skydoves/landscapist), Coil-Compose
+- [ComposeExtensions](https://github.com/taehwandev/ComposeExtensions)
+- [compose-effects](https://github.com/skydoves/compose-effects)
+- [compose-shadow](https://github.com/adamglin0/compose-shadow)
+
+#### Test & Code analysis
+
+- Ktlint
+- Detekt
+
+#### Gradle Dependency
+
+- Gradle Version Catalog
+
+## Architecture
+- Android App Architecture
+- MVI
+
+## Developers
+
+|Android|Android|
+|:---:|:---:|
+|[이지훈](https://github.com/easyhooon)|[이서윤](https://github.com/seoyoon513)|
+|
|
|
+
+## Module
+
+
+## Package Structure
+```
+├── app
+│ └── application
+├── build-logic
+├── core
+│ ├── common
+│ ├── data-api
+│ ├── data-impl
+│ ├── datastore-api
+│ ├── datastore-impl
+│ ├── designsystem
+│ ├── model
+│ ├── network
+│ ├── ocr
+│ └── ui
+├── feature
+│ ├── detail
+│ ├── edit
+│ ├── home
+│ ├── library
+│ ├── login
+│ ├── main
+│ ├── onboarding
+│ ├── record
+│ ├── screens
+│ ├── settings
+│ ├── splash
+│ └── webview
+├── gradle
+ └── libs.versions.toml
+
+```
+
diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt
new file mode 100644
index 00000000..bfe3bbf5
--- /dev/null
+++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/UiText.kt
@@ -0,0 +1,27 @@
+package com.ninecraft.booket.core.common.utils
+
+import android.content.Context
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+
+// https://www.youtube.com/watch?v=mB1Lej0aDus
+sealed class UiText {
+ data class DirectString(val value: String) : UiText()
+
+ class StringResource(
+ @StringRes val resId: Int,
+ vararg val args: Any,
+ ) : UiText()
+
+ @Composable
+ fun asString() = when (this) {
+ is DirectString -> value
+ is StringResource -> stringResource(resId, *args)
+ }
+
+ fun asString(context: Context) = when (this) {
+ is DirectString -> value
+ is StringResource -> context.getString(resId, *args)
+ }
+}
diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt
index 35bfbb11..c8943668 100644
--- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt
+++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt
@@ -1,6 +1,7 @@
package com.ninecraft.booket.core.data.api.repository
import com.ninecraft.booket.core.model.AutoLoginState
+import com.ninecraft.booket.core.model.UserState
import kotlinx.coroutines.flow.Flow
interface AuthRepository {
@@ -11,4 +12,8 @@ interface AuthRepository {
suspend fun withdraw(): Result
val autoLoginState: Flow
+
+ val userState: Flow
+
+ suspend fun getCurrentUserState(): UserState
}
diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt
index b29faf03..b2a853de 100644
--- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt
+++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt
@@ -12,6 +12,11 @@ interface BookRepository {
val bookRecentSearches: Flow>
val libraryRecentSearches: Flow>
+ suspend fun searchBookAsGuest(
+ query: String,
+ start: Int,
+ ): Result
+
suspend fun searchBook(
query: String,
start: Int,
diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt
index 85de7a76..ba0e0e96 100644
--- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt
+++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt
@@ -26,6 +26,8 @@ import com.ninecraft.booket.core.network.response.BookSearchResponse
import com.ninecraft.booket.core.network.response.BookSummary
import com.ninecraft.booket.core.network.response.BookUpsertResponse
import com.ninecraft.booket.core.network.response.Category
+import com.ninecraft.booket.core.network.response.GuestBookSearchResponse
+import com.ninecraft.booket.core.network.response.GuestBookSummary
import com.ninecraft.booket.core.network.response.HomeResponse
import com.ninecraft.booket.core.network.response.LibraryBookSummary
import com.ninecraft.booket.core.network.response.LibraryBooks
@@ -79,6 +81,35 @@ internal fun BookSummary.toModel(): BookSummaryModel {
)
}
+internal fun GuestBookSearchResponse.toModel(): BookSearchModel {
+ return BookSearchModel(
+ version = version,
+ title = title,
+ pubDate = pubDate,
+ totalResults = totalResults,
+ startIndex = startIndex,
+ itemsPerPage = itemsPerPage,
+ query = query,
+ searchCategoryId = searchCategoryId,
+ searchCategoryName = searchCategoryName,
+ lastPage = lastPage,
+ books = books.map { it.toModel() },
+ )
+}
+
+internal fun GuestBookSummary.toModel(): BookSummaryModel {
+ return BookSummaryModel(
+ isbn13 = isbn13,
+ title = title.decodeHtmlEntities(),
+ author = author,
+ publisher = publisher,
+ coverImageUrl = coverImageUrl,
+ link = link,
+ userBookStatus = "BEFORE_REGISTRATION",
+ key = "$title-$isbn13",
+ )
+}
+
internal fun BookDetailResponse.toModel(): BookDetailModel {
return BookDetailModel(
version = version,
diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt
index c165bba7..9e560650 100644
--- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt
+++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt
@@ -4,6 +4,7 @@ import com.ninecraft.booket.core.common.utils.runSuspendCatching
import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource
import com.ninecraft.booket.core.model.AutoLoginState
+import com.ninecraft.booket.core.model.UserState
import com.ninecraft.booket.core.network.request.LoginRequest
import com.ninecraft.booket.core.network.service.ReedService
import kotlinx.coroutines.flow.map
@@ -48,9 +49,16 @@ internal class DefaultAuthRepository @Inject constructor(
override val autoLoginState = tokenDataSource.accessToken
.map { accessToken ->
- when {
- accessToken.isBlank() -> AutoLoginState.NOT_LOGGED_IN
- else -> AutoLoginState.LOGGED_IN
- }
+ if (accessToken.isBlank()) AutoLoginState.NOT_LOGGED_IN else AutoLoginState.LOGGED_IN
}
+
+ override val userState = tokenDataSource.accessToken
+ .map { accessToken ->
+ if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn
+ }
+
+ override suspend fun getCurrentUserState(): UserState {
+ val accessToken = tokenDataSource.getAccessToken()
+ return if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn
+ }
}
diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt
index cf64e2f2..2f4580c5 100644
--- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt
+++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt
@@ -17,6 +17,19 @@ internal class DefaultBookRepository @Inject constructor(
override val bookRecentSearches = bookRecentSearchDataSource.recentSearches
override val libraryRecentSearches = libraryRecentSearchDataSource.recentSearches
+ override suspend fun searchBookAsGuest(
+ query: String,
+ start: Int,
+ ) = runSuspendCatching {
+ val result = service.searchBookAsGuest(
+ query = query,
+ start = start,
+ ).toModel()
+
+ bookRecentSearchDataSource.addRecentSearch(query)
+ result
+ }
+
override suspend fun searchBook(
query: String,
start: Int,
diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt
index 57db2455..b4ddfb90 100644
--- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt
@@ -2,12 +2,13 @@ package com.ninecraft.booket.core.designsystem.component.button
import androidx.compose.foundation.BorderStroke
import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.theme.Kakao
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
enum class ReedButtonColorStyle {
- PRIMARY, SECONDARY, TERTIARY, STROKE, KAKAO;
+ PRIMARY, SECONDARY, TERTIARY, STROKE, TEXT, KAKAO;
@Composable
fun containerColor(isPressed: Boolean) = when (this) {
@@ -15,6 +16,7 @@ enum class ReedButtonColorStyle {
SECONDARY -> if (isPressed) ReedTheme.colors.bgSecondaryPressed else ReedTheme.colors.bgSecondary
TERTIARY -> if (isPressed) ReedTheme.colors.bgTertiaryPressed else ReedTheme.colors.bgTertiary
STROKE -> if (isPressed) ReedTheme.colors.basePrimary else ReedTheme.colors.basePrimary
+ TEXT -> Color.Transparent
KAKAO -> Kakao
}
@@ -24,11 +26,15 @@ enum class ReedButtonColorStyle {
SECONDARY -> ReedTheme.colors.contentPrimary
TERTIARY -> ReedTheme.colors.contentBrand
STROKE -> ReedTheme.colors.contentBrand
+ TEXT -> ReedTheme.colors.borderBrand
KAKAO -> ReedTheme.colors.contentPrimary
}
@Composable
- fun disabledContainerColor() = ReedTheme.colors.bgDisabled
+ fun disabledContainerColor() = when (this) {
+ TEXT -> Color.Transparent
+ else -> ReedTheme.colors.bgDisabled
+ }
@Composable
fun disabledContentColor() = ReedTheme.colors.contentDisabled
diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt
new file mode 100644
index 00000000..c1aa1470
--- /dev/null
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedTextButton.kt
@@ -0,0 +1,103 @@
+package com.ninecraft.booket.core.designsystem.component.button
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.ninecraft.booket.core.common.utils.MultipleEventsCutter
+import com.ninecraft.booket.core.common.utils.get
+import com.ninecraft.booket.core.designsystem.ComponentPreview
+
+@Composable
+fun ReedTextButton(
+ onClick: () -> Unit,
+ text: String,
+ sizeStyle: ButtonSizeStyle,
+ colorStyle: ReedButtonColorStyle,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ multipleEventsCutterEnabled: Boolean = true,
+) {
+ val multipleEventsCutter = remember { MultipleEventsCutter.get() }
+
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+
+ TextButton(
+ onClick = {
+ if (multipleEventsCutterEnabled) {
+ multipleEventsCutter.processEvent { onClick() }
+ } else {
+ onClick()
+ }
+ },
+ modifier = modifier,
+ enabled = enabled,
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = colorStyle.containerColor(isPressed),
+ contentColor = colorStyle.contentColor(),
+ disabledContentColor = colorStyle.disabledContentColor(),
+ disabledContainerColor = colorStyle.disabledContainerColor(),
+ ),
+ contentPadding = sizeStyle.paddingValues,
+ ) {
+ Column(
+ modifier = Modifier.width(IntrinsicSize.Max),
+ verticalArrangement = Arrangement.spacedBy(1.dp),
+ ) {
+ Text(
+ text = text,
+ style = sizeStyle.textStyle.copy(
+ color = if (enabled) colorStyle.contentColor() else colorStyle.disabledContentColor(),
+ ),
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = if (enabled) colorStyle.contentColor() else Color.Transparent,
+ )
+ }
+ }
+}
+
+@ComponentPreview
+@Composable
+private fun ReedTextButtonPreview() {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ ) {
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ ) {
+ ReedTextButton(
+ onClick = {},
+ colorStyle = ReedButtonColorStyle.TEXT,
+ sizeStyle = largeButtonStyle,
+ text = "text button",
+ )
+
+ ReedTextButton(
+ onClick = {},
+ enabled = false,
+ colorStyle = ReedButtonColorStyle.TEXT,
+ sizeStyle = largeButtonStyle,
+ text = "text button",
+ )
+ }
+ }
+}
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 90f1c070..a2793c60 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -3,4 +3,5 @@
이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요.
알 수 없는 오류가 발생하였습니다.
도서 검색 후 내 서재에 담아보세요
+ 로그인이 필요한 기능입니다
diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt
index cc592841..f1c8b05c 100644
--- a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt
+++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/BookSearchModel.kt
@@ -27,4 +27,11 @@ data class BookSummaryModel(
val link: String = "",
val userBookStatus: String = "",
val key: String = "",
-)
+) {
+ val isRegistered: Boolean
+ get() = userBookStatus != BEFORE_REGISTRATION
+
+ companion object {
+ const val BEFORE_REGISTRATION = "BEFORE_REGISTRATION"
+ }
+}
diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt
new file mode 100644
index 00000000..43ec0824
--- /dev/null
+++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserState.kt
@@ -0,0 +1,6 @@
+package com.ninecraft.booket.core.model
+
+sealed interface UserState {
+ data object Guest : UserState
+ data object LoggedIn : UserState
+}
diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt
index a3544d49..a49e6fe8 100644
--- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt
+++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenInterceptor.kt
@@ -13,6 +13,7 @@ internal class TokenInterceptor @Inject constructor(
private val noAuthEndpoints = setOf(
"api/v1/auth/signin",
"api/v1/auth/refresh",
+ "api/v1/books/guest/search",
)
override fun intercept(chain: Interceptor.Chain): Response {
diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt
new file mode 100644
index 00000000..eb38bfe5
--- /dev/null
+++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/GuestBookSearchResponse.kt
@@ -0,0 +1,46 @@
+package com.ninecraft.booket.core.network.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GuestBookSearchResponse(
+ @SerialName("version")
+ val version: String,
+ @SerialName("title")
+ val title: String,
+ @SerialName("pubDate")
+ val pubDate: String,
+ @SerialName("totalResults")
+ val totalResults: Int,
+ @SerialName("startIndex")
+ val startIndex: Int,
+ @SerialName("itemsPerPage")
+ val itemsPerPage: Int,
+ @SerialName("query")
+ val query: String,
+ @SerialName("searchCategoryId")
+ val searchCategoryId: Int,
+ @SerialName("searchCategoryName")
+ val searchCategoryName: String,
+ @SerialName("lastPage")
+ val lastPage: Boolean,
+ @SerialName("books")
+ val books: List,
+)
+
+@Serializable
+data class GuestBookSummary(
+ @SerialName("isbn13")
+ val isbn13: String,
+ @SerialName("title")
+ val title: String,
+ @SerialName("author")
+ val author: String,
+ @SerialName("publisher")
+ val publisher: String,
+ @SerialName("coverImageUrl")
+ val coverImageUrl: String,
+ @SerialName("link")
+ val link: String,
+)
diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt
index f2b0b280..00dc717b 100644
--- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt
+++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt
@@ -8,6 +8,7 @@ import com.ninecraft.booket.core.network.request.TermsAgreementRequest
import com.ninecraft.booket.core.network.response.BookDetailResponse
import com.ninecraft.booket.core.network.response.BookSearchResponse
import com.ninecraft.booket.core.network.response.BookUpsertResponse
+import com.ninecraft.booket.core.network.response.GuestBookSearchResponse
import com.ninecraft.booket.core.network.response.HomeResponse
import com.ninecraft.booket.core.network.response.LibraryResponse
import com.ninecraft.booket.core.network.response.LoginResponse
@@ -50,6 +51,19 @@ interface ReedService {
@GET("api/v1/users/me")
suspend fun getUserProfile(): UserProfileResponse
+ // Book endpoints (no auth required)
+ @GET("api/v1/books/guest/search")
+ suspend fun searchBookAsGuest(
+ @Query("query") query: String,
+ @Query("queryType") queryType: String = "Title",
+ @Query("searchTarget") searchTarget: String = "Book",
+ @Query("maxResults") maxResults: Int = 20,
+ @Query("start") start: Int = 1,
+ @Query("sort") sort: String = "Accuracy",
+ @Query("cover") cover: String? = "Big",
+ @Query("categoryId") categoryId: Int = 0,
+ ): GuestBookSearchResponse
+
// Book endpoints (auth required)
@GET("api/v1/books/search")
suspend fun searchBook(
diff --git a/core/ocr/build.gradle.kts b/core/ocr/build.gradle.kts
index 4d70a87e..8fcb5005 100644
--- a/core/ocr/build.gradle.kts
+++ b/core/ocr/build.gradle.kts
@@ -26,9 +26,6 @@ dependencies {
projects.core.common,
libs.logger,
- libs.androidx.camera.core,
-
- libs.google.mlkit.text.recognition.korean,
)
}
diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt
deleted file mode 100644
index 0b5ab9b0..00000000
--- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.ninecraft.booket.core.ocr.analyzer
-
-import androidx.annotation.OptIn
-import androidx.camera.core.ExperimentalGetImage
-import androidx.camera.core.ImageProxy
-import com.google.mlkit.vision.common.InputImage
-import com.google.mlkit.vision.text.TextRecognizer
-import com.orhanobut.logger.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-/**
- * 실시간 카메라 스트림에서 프레임 단위로 텍스트 분석하는 Analyzer 클래스
- *
- * ML Kit의 TextRecognizer를 사용하여 `ImageProxy` 객체로부터 텍스트를 추출한다
- *
- * @param textRecognizer ML Kit의 TextRecognizer 인스턴스
- * @param onTextDetected 텍스트 인식 성공 시 호출되는 콜백 (인식된 전체 텍스트 전달)
- *
- * 안정적인 연속 프레임 분석을 위해 CoroutineScope에 [SupervisorJob]을 사용하여
- * 한 프레임 분석에서 예외가 발생해도 다음 프레임 분석에 영향을 주지 않도록 설계
- */
-class LiveTextAnalyzer @AssistedInject constructor(
- private val textRecognizer: TextRecognizer,
- @Assisted private val onTextDetected: (String) -> Unit,
-) : TextAnalyzer {
-
- companion object {
- const val THROTTLE_TIMEOUT_MS = 1_000L // 프레임 처리 간 인터벌
- }
-
- private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- @OptIn(ExperimentalGetImage::class)
- override fun analyze(imageProxy: ImageProxy) {
- scope.launch {
- val mediaImage = imageProxy.image ?: run { imageProxy.close(); return@launch }
- val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
-
- suspendCoroutine { continuation ->
- textRecognizer.process(inputImage)
- .addOnSuccessListener { visionText ->
- onTextDetected(visionText.text)
- }
- .addOnFailureListener { exception ->
- Logger.e(exception.message ?: "Unknown error")
- }
- .addOnCompleteListener {
- continuation.resume(Unit)
- }
- }
- delay(THROTTLE_TIMEOUT_MS)
- }.invokeOnCompletion { exception ->
- if (exception != null) {
- Logger.e(exception.message ?: "Unknown error")
- }
- imageProxy.close()
- }
- }
-
- fun cancel() {
- scope.cancel()
- }
-
- @AssistedFactory
- interface Factory {
- fun create(
- onTextDetected: (String) -> Unit,
- ): LiveTextAnalyzer
- }
-}
diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt
deleted file mode 100644
index 39865504..00000000
--- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.ninecraft.booket.core.ocr.analyzer
-
-import androidx.annotation.OptIn
-import androidx.camera.core.ExperimentalGetImage
-import androidx.camera.core.ImageProxy
-import com.google.mlkit.vision.common.InputImage
-import com.google.mlkit.vision.text.TextRecognizer
-import com.orhanobut.logger.Logger
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-/**
- * 정적인 카메라 이미지에서 텍스트를 분석하는 클래스
- *
- * CameraX의 단일 ImageProxy 프레임을 받아 ML Kit을 통해 텍스트를 추출하고 결과를 콜백으로 전달한다.
- *
- * @param textRecognizer ML Kit의 TextRecognizer 인스턴스
- * @param onTextDetected 텍스트 인식 성공 시 호출되는 콜백 (인식된 전체 텍스트 전달)
- * @param onFailure 인식 실패 시 호출되는 콜백
- *
- * 분석이 끝난 후 반드시 imageProxy.close() 호출하여 리소스 해제
- */
-class StillTextAnalyzer @AssistedInject constructor(
- private val textRecognizer: TextRecognizer,
- @Assisted private val onTextDetected: (String) -> Unit,
- @Assisted private val onFailure: () -> Unit,
-) : TextAnalyzer {
-
- private val scope = CoroutineScope(Dispatchers.IO)
-
- @OptIn(ExperimentalGetImage::class)
- override fun analyze(imageProxy: ImageProxy) {
- scope.launch {
- val mediaImage = imageProxy.image ?: run { imageProxy.close(); return@launch }
- val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
-
- suspendCoroutine { continuation ->
- textRecognizer.process(inputImage)
- .addOnSuccessListener { visionText ->
- onTextDetected(visionText.text)
- }
- .addOnFailureListener {
- onFailure()
- }
- .addOnCompleteListener {
- continuation.resume(Unit)
- }
- }
- }.invokeOnCompletion { exception ->
- if (exception != null) {
- Logger.e(exception.message ?: "Unknown error")
- }
- imageProxy.close()
- }
- }
-
- fun cancel() {
- scope.cancel()
- }
-
- @AssistedFactory
- interface Factory {
- fun create(
- onTextDetected: (String) -> Unit,
- onFailure: () -> Unit,
- ): StillTextAnalyzer
- }
-}
diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt
deleted file mode 100644
index c4d30b3c..00000000
--- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/TextAnalyzer.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.ninecraft.booket.core.ocr.analyzer
-
-import androidx.camera.core.ImageProxy
-
-interface TextAnalyzer {
- fun analyze(imageProxy: ImageProxy)
-}
diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt
deleted file mode 100644
index 99ce5740..00000000
--- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/di/OcrModule.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.ninecraft.booket.core.ocr.di
-
-import com.google.mlkit.vision.text.TextRecognition
-import com.google.mlkit.vision.text.TextRecognizer
-import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-object OcrModule {
-
- @Provides
- @Singleton
- fun provideTextRecognizer(): TextRecognizer =
- TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())
-}
diff --git a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/CloudOcrRecognizer.kt b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt
similarity index 97%
rename from core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/CloudOcrRecognizer.kt
rename to core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt
index 538095db..9035c5cf 100644
--- a/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/CloudOcrRecognizer.kt
+++ b/core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/recognizer/CloudOcrRecognizer.kt
@@ -1,4 +1,4 @@
-package com.ninecraft.booket.core.ocr.analyzer
+package com.ninecraft.booket.core.ocr.recognizer
import android.net.Uri
import android.util.Base64
diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt
index 3adbc6b0..cd8f9ade 100644
--- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt
+++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedLoadingIndicator.kt
@@ -4,23 +4,34 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.ninecraft.booket.core.common.extensions.noRippleClickable
import com.ninecraft.booket.core.designsystem.ComponentPreview
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
+import kotlinx.coroutines.delay
@Composable
fun ReedLoadingIndicator(
modifier: Modifier = Modifier,
+ delayMillis: Long = 500L,
) {
- Box(
- modifier = modifier
- .fillMaxSize()
- .noRippleClickable {},
- contentAlignment = Alignment.Center,
- ) {
- CircularProgressIndicator(color = ReedTheme.colors.contentBrand)
+ val showProgressBar by produceState(initialValue = false, key1 = delayMillis) {
+ delay(delayMillis)
+ value = true
+ }
+
+ if (showProgressBar) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .noRippleClickable {},
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator(color = ReedTheme.colors.contentBrand)
+ }
}
}
diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
index 4d00094f..67d3e962 100644
--- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
+++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
@@ -1,7 +1,6 @@
package com.ninecraft.booket.feature.detail.book
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -23,8 +22,8 @@ import com.ninecraft.booket.feature.screens.RecordDetailScreen
import com.ninecraft.booket.feature.screens.RecordEditScreen
import com.ninecraft.booket.feature.screens.RecordScreen
import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs
-import com.ninecraft.booket.feature.screens.extensions.delayedGoTo
import com.orhanobut.logger.Logger
+import com.skydoves.compose.effects.RememberedEffect
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
@@ -135,7 +134,7 @@ class BookDetailPresenter @AssistedInject constructor(
exception = e,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
@@ -160,7 +159,7 @@ class BookDetailPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
@@ -210,7 +209,7 @@ class BookDetailPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
@@ -234,14 +233,14 @@ class BookDetailPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
}
}
- LaunchedEffect(Unit) {
+ RememberedEffect(Unit) {
initialLoad()
}
@@ -304,15 +303,13 @@ class BookDetailPresenter @AssistedInject constructor(
is BookDetailUiEvent.OnShareRecordClick -> {
isRecordMenuBottomSheetVisible = false
- scope.launch {
- navigator.delayedGoTo(
- RecordCardScreen(
- quote = selectedRecordInfo.quote,
- bookTitle = selectedRecordInfo.bookTitle,
- emotionTag = selectedRecordInfo.emotionTags[0],
- ),
- )
- }
+ navigator.goTo(
+ RecordCardScreen(
+ quote = selectedRecordInfo.quote,
+ bookTitle = selectedRecordInfo.bookTitle,
+ emotionTag = selectedRecordInfo.emotionTags[0],
+ ),
+ )
}
is BookDetailUiEvent.OnEditRecordClick -> {
diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
index 0c1a6d7d..cc27a011 100644
--- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
+++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
@@ -136,7 +136,10 @@ internal fun BookDetailUi(
},
sheetState = recordMenuBottomSheetState,
onShareRecordClick = {
- state.eventSink(BookDetailUiEvent.OnShareRecordClick)
+ coroutineScope.launch {
+ recordMenuBottomSheetState.hide()
+ state.eventSink(BookDetailUiEvent.OnShareRecordClick)
+ }
},
onEditRecordClick = {
coroutineScope.launch {
diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
index f5921683..151eeb03 100644
--- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
+++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
@@ -14,7 +14,6 @@ import com.ninecraft.booket.feature.screens.RecordCardScreen
import com.ninecraft.booket.feature.screens.RecordDetailScreen
import com.ninecraft.booket.feature.screens.RecordEditScreen
import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs
-import com.ninecraft.booket.feature.screens.extensions.delayedGoTo
import com.orhanobut.logger.Logger
import com.skydoves.compose.effects.RememberedEffect
import com.slack.circuit.codegen.annotations.CircuitInject
@@ -69,7 +68,7 @@ class RecordDetailPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
@@ -93,7 +92,7 @@ class RecordDetailPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
@@ -124,15 +123,13 @@ class RecordDetailPresenter @AssistedInject constructor(
is RecordDetailUiEvent.OnShareRecordClick -> {
isRecordMenuBottomSheetVisible = false
- scope.launch {
- navigator.delayedGoTo(
- RecordCardScreen(
- quote = recordDetailInfo.quote,
- bookTitle = recordDetailInfo.bookTitle,
- emotionTag = recordDetailInfo.emotionTags[0],
- ),
- )
- }
+ navigator.goTo(
+ RecordCardScreen(
+ quote = recordDetailInfo.quote,
+ bookTitle = recordDetailInfo.bookTitle,
+ emotionTag = recordDetailInfo.emotionTags[0],
+ ),
+ )
}
is RecordDetailUiEvent.OnEditRecordClick -> {
diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
index ebd2bea1..48248dd9 100644
--- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
+++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
@@ -83,7 +83,10 @@ internal fun RecordDetailUi(
},
sheetState = recordMenuBottomSheetState,
onShareRecordClick = {
- state.eventSink(RecordDetailUiEvent.OnShareRecordClick)
+ coroutineScope.launch {
+ recordMenuBottomSheetState.hide()
+ state.eventSink(RecordDetailUiEvent.OnShareRecordClick)
+ }
},
onEditRecordClick = {
coroutineScope.launch {
diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt
index f420ff45..427ce11e 100644
--- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt
+++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt
@@ -106,7 +106,7 @@ class RecordEditPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt
index 0a7972c2..9f1257eb 100644
--- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt
+++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt
@@ -85,114 +85,119 @@ internal fun RecordEditUi(
@Composable
private fun ColumnScope.RecordEditContent(state: RecordEditUiState) {
- BookItem(
- imageUrl = state.recordInfo.bookCoverImageUrl,
- bookTitle = state.recordInfo.bookTitle,
- author = state.recordInfo.author,
- publisher = state.recordInfo.bookPublisher,
- )
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
- HorizontalDivider(
- modifier = Modifier.fillMaxWidth(),
- thickness = ReedTheme.border.border1,
- color = ReedTheme.colors.dividerSm,
- )
Column(
modifier = Modifier
.weight(1f)
- .padding(horizontal = ReedTheme.spacing.spacing5)
.verticalScroll(rememberScrollState()),
) {
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
- Text(
- text = stringResource(R.string.edit_record_page_label),
- color = ReedTheme.colors.contentPrimary,
- style = ReedTheme.typography.body1Medium,
- )
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
- ReedRecordTextField(
- recordState = state.recordPageState,
- recordHintRes = R.string.edit_record_page_hint,
- inputTransformation = digitOnlyInputTransformation,
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- lineLimits = TextFieldLineLimits.SingleLine,
- isError = state.isPageError,
- errorMessage = stringResource(R.string.edit_record_page_input_error),
- onClear = {
- state.eventSink(RecordEditUiEvent.OnClearClick)
- },
- modifier = Modifier
- .fillMaxWidth()
- .height(50.dp),
- )
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8))
- Text(
- text = stringResource(R.string.edit_record_quote_label),
- color = ReedTheme.colors.contentPrimary,
- style = ReedTheme.typography.body1Medium,
+ BookItem(
+ imageUrl = state.recordInfo.bookCoverImageUrl,
+ bookTitle = state.recordInfo.bookTitle,
+ author = state.recordInfo.author,
+ publisher = state.recordInfo.bookPublisher,
)
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
- ReedRecordTextField(
- recordState = state.recordQuoteState,
- recordHintRes = R.string.edit_record_quote_hint,
- modifier = Modifier
- .fillMaxWidth()
- .height(140.dp),
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Text,
- imeAction = ImeAction.Default,
- ),
- )
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8))
- Text(
- text = stringResource(R.string.edit_record_impression_label),
- color = ReedTheme.colors.contentPrimary,
- style = ReedTheme.typography.body1Medium,
+ HorizontalDivider(
+ modifier = Modifier.fillMaxWidth(),
+ thickness = ReedTheme.border.border1,
+ color = ReedTheme.colors.dividerSm,
)
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
- ReedRecordTextField(
- recordState = state.recordImpressionState,
- recordHintRes = R.string.edit_record_impression_hint,
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
+ Column(
modifier = Modifier
.fillMaxWidth()
- .height(140.dp),
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Text,
- imeAction = ImeAction.Default,
- ),
- )
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8))
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
+ .padding(horizontal = ReedTheme.spacing.spacing5),
) {
Text(
- text = stringResource(R.string.edit_record_emotion_label),
+ text = stringResource(R.string.edit_record_page_label),
color = ReedTheme.colors.contentPrimary,
style = ReedTheme.typography.body1Medium,
)
- Spacer(modifier = Modifier.weight(1f))
- Row(
- modifier = Modifier.clickable {
- state.eventSink(RecordEditUiEvent.OnEmotionEditClick)
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
+ ReedRecordTextField(
+ recordState = state.recordPageState,
+ recordHintRes = R.string.edit_record_page_hint,
+ inputTransformation = digitOnlyInputTransformation,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ lineLimits = TextFieldLineLimits.SingleLine,
+ isError = state.isPageError,
+ errorMessage = stringResource(R.string.edit_record_page_input_error),
+ onClear = {
+ state.eventSink(RecordEditUiEvent.OnClearClick)
},
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8))
+ Text(
+ text = stringResource(R.string.edit_record_quote_label),
+ color = ReedTheme.colors.contentPrimary,
+ style = ReedTheme.typography.body1Medium,
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
+ ReedRecordTextField(
+ recordState = state.recordQuoteState,
+ recordHintRes = R.string.edit_record_quote_hint,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(140.dp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Default,
+ ),
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8))
+ Text(
+ text = stringResource(R.string.edit_record_impression_label),
+ color = ReedTheme.colors.contentPrimary,
+ style = ReedTheme.typography.body1Medium,
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
+ ReedRecordTextField(
+ recordState = state.recordImpressionState,
+ recordHintRes = R.string.edit_record_impression_hint,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(140.dp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Default,
+ ),
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
) {
- val emotion = state.recordInfo.emotionTags.firstOrNull() ?: ""
-
Text(
- text = emotion,
- color = ReedTheme.colors.contentSecondary,
+ text = stringResource(R.string.edit_record_emotion_label),
+ color = ReedTheme.colors.contentPrimary,
style = ReedTheme.typography.body1Medium,
)
- Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1))
- Icon(
- imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right),
- contentDescription = "Chevron Right Icon",
- tint = ReedTheme.colors.contentSecondary,
- )
+ Spacer(modifier = Modifier.weight(1f))
+ Row(
+ modifier = Modifier.clickable {
+ state.eventSink(RecordEditUiEvent.OnEmotionEditClick)
+ },
+ ) {
+ val emotion = state.recordInfo.emotionTags.firstOrNull() ?: ""
+
+ Text(
+ text = emotion,
+ color = ReedTheme.colors.contentSecondary,
+ style = ReedTheme.typography.body1Medium,
+ )
+ Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1))
+ Icon(
+ imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right),
+ contentDescription = "Chevron Right Icon",
+ tint = ReedTheme.colors.contentSecondary,
+ )
+ }
}
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing16))
}
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing16))
}
ReedButton(
onClick = {
diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
index dbfc9623..175b1804 100644
--- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
+++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
@@ -6,15 +6,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.ninecraft.booket.core.common.analytics.AnalyticsHelper
+import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.core.data.api.repository.BookRepository
import com.ninecraft.booket.core.model.RecentBookModel
+import com.ninecraft.booket.core.model.UserState
import com.ninecraft.booket.feature.screens.BookDetailScreen
import com.ninecraft.booket.feature.screens.HomeScreen
import com.ninecraft.booket.feature.screens.RecordScreen
-import com.ninecraft.booket.feature.screens.SearchScreen
+import com.ninecraft.booket.feature.screens.BookSearchScreen
import com.ninecraft.booket.feature.screens.SettingsScreen
import com.skydoves.compose.effects.RememberedEffect
import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.retained.collectAsRetainedState
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
@@ -29,14 +32,15 @@ import kotlinx.coroutines.launch
class HomePresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
- private val repository: BookRepository,
+ private val bookRepository: BookRepository,
+ private val authRepository: AuthRepository,
private val analyticsHelper: AnalyticsHelper,
) : Presenter {
@Composable
override fun present(): HomeUiState {
val scope = rememberCoroutineScope()
-
+ val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest)
var uiState by rememberRetained { mutableStateOf(UiState.Idle) }
var recentBooks by rememberRetained { mutableStateOf(persistentListOf()) }
@@ -46,7 +50,7 @@ class HomePresenter @AssistedInject constructor(
uiState = UiState.Loading
}
- repository.getHome()
+ bookRepository.getHome()
.onSuccess { result ->
uiState = UiState.Success
recentBooks = result.recentBooks.toPersistentList()
@@ -63,7 +67,7 @@ class HomePresenter @AssistedInject constructor(
}
is HomeUiEvent.OnBookRegisterClick -> {
- navigator.goTo(SearchScreen)
+ navigator.goTo(BookSearchScreen)
}
is HomeUiEvent.OnRecordButtonClick -> {
@@ -88,8 +92,10 @@ class HomePresenter @AssistedInject constructor(
}
}
- RememberedEffect(true) {
- loadHomeContent()
+ RememberedEffect(userState) {
+ if (userState !is UserState.Guest) {
+ loadHomeContent()
+ }
}
ImpressionEffect {
@@ -99,6 +105,7 @@ class HomePresenter @AssistedInject constructor(
return HomeUiState(
uiState = uiState,
recentBooks = recentBooks,
+ isGuestMode = userState is UserState.Guest,
eventSink = ::handleEvent,
)
}
diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
index 82221807..8307716e 100644
--- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
+++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
@@ -94,85 +94,110 @@ internal fun HomeContent(
.fillMaxSize()
.background(ReedTheme.colors.baseSecondary),
) {
- when (state.uiState) {
- is UiState.Idle -> {}
- is UiState.Loading -> {
- ReedLoadingIndicator()
+ if (state.isGuestMode) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ ) {
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
+ Text(
+ text = stringResource(R.string.home_content_label_reading_now),
+ modifier = Modifier.padding(start = ReedTheme.spacing.spacing5),
+ color = ReedTheme.colors.contentSecondary,
+ style = ReedTheme.typography.headline2Medium,
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3))
+ EmptyBookCard(
+ onBookRegisterClick = {
+ state.eventSink(HomeUiEvent.OnBookRegisterClick)
+ },
+ modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5),
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
}
+ } else {
+ when (state.uiState) {
+ is UiState.Idle -> {}
+ is UiState.Loading -> {
+ ReedLoadingIndicator()
+ }
- is UiState.Success -> {
- Column(
- modifier = modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState()),
- ) {
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
- Text(
- text = stringResource(R.string.home_content_label_reading_now),
- modifier = Modifier.padding(start = ReedTheme.spacing.spacing5),
- color = ReedTheme.colors.contentSecondary,
- style = ReedTheme.typography.headline2Medium,
- )
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3))
-
- if (state.recentBooks.isEmpty()) {
- EmptyBookCard(
- onBookRegisterClick = {
- state.eventSink(HomeUiEvent.OnBookRegisterClick)
- },
- modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5),
+ is UiState.Success -> {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ ) {
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
+ Text(
+ text = stringResource(R.string.home_content_label_reading_now),
+ modifier = Modifier.padding(start = ReedTheme.spacing.spacing5),
+ color = ReedTheme.colors.contentSecondary,
+ style = ReedTheme.typography.headline2Medium,
)
- } else {
- val pagerState = rememberPagerState(pageCount = { state.recentBooks.size })
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3))
- HorizontalPager(
- state = pagerState,
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = ReedTheme.spacing.spacing5),
- pageSpacing = ReedTheme.spacing.spacing5,
- ) { page ->
- BookCard(
- recentBookInfo = state.recentBooks[page],
- onBookDetailClick = {
- state.eventSink(
- HomeUiEvent.OnBookDetailClick(
- state.recentBooks[page].userBookId,
- state.recentBooks[page].isbn13,
- ),
- )
- },
- onRecordButtonClick = {
- state.eventSink(HomeUiEvent.OnRecordButtonClick(state.recentBooks[page].userBookId))
+ if (state.recentBooks.isEmpty()) {
+ EmptyBookCard(
+ onBookRegisterClick = {
+ state.eventSink(HomeUiEvent.OnBookRegisterClick)
},
+ modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing5),
)
- }
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
- ) {
- repeat(pagerState.pageCount) { iteration ->
- val color =
- if (pagerState.currentPage == iteration) ReedTheme.colors.bgPrimary else ReedTheme.colors.bgSecondaryPressed
- Box(
- modifier = Modifier
- .size(12.dp)
- .padding(3.dp)
- .clip(CircleShape)
- .background(color),
+ } else {
+ val pagerState = rememberPagerState(pageCount = { state.recentBooks.size })
+
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(horizontal = ReedTheme.spacing.spacing5),
+ pageSpacing = ReedTheme.spacing.spacing5,
+ ) { page ->
+ BookCard(
+ recentBookInfo = state.recentBooks[page],
+ onBookDetailClick = {
+ state.eventSink(
+ HomeUiEvent.OnBookDetailClick(
+ state.recentBooks[page].userBookId,
+ state.recentBooks[page].isbn13,
+ ),
+ )
+ },
+ onRecordButtonClick = {
+ state.eventSink(HomeUiEvent.OnRecordButtonClick(state.recentBooks[page].userBookId))
+ },
)
}
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ repeat(pagerState.pageCount) { iteration ->
+ val color =
+ if (pagerState.currentPage == iteration) ReedTheme.colors.bgPrimary else ReedTheme.colors.bgSecondaryPressed
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .padding(3.dp)
+ .clip(CircleShape)
+ .background(color),
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing7))
}
- Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing7))
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6))
}
}
- }
- is UiState.Error -> {
- ReedErrorUi(
- exception = state.uiState.exception,
- onRetryClick = { state.eventSink(HomeUiEvent.OnRetryClick) },
- )
+ is UiState.Error -> {
+ ReedErrorUi(
+ exception = state.uiState.exception,
+ onRetryClick = { state.eventSink(HomeUiEvent.OnRetryClick) },
+ )
+ }
}
}
}
diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt
index 9b3115ee..c90d001a 100644
--- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt
+++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt
@@ -20,6 +20,7 @@ sealed interface UiState {
data class HomeUiState(
val uiState: UiState = UiState.Idle,
val recentBooks: ImmutableList = persistentListOf(),
+ val isGuestMode: Boolean = false,
val sideEffect: HomeSideEffect? = null,
val eventSink: (HomeUiEvent) -> Unit,
) : CircuitUiState
diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt
index 1b20a035..84681666 100644
--- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt
+++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/HandleLibrarySideEffects.kt
@@ -15,7 +15,7 @@ internal fun HandleLibrarySideEffects(
RememberedEffect(state.sideEffect) {
when (state.sideEffect) {
is LibrarySideEffect.ShowToast -> {
- Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, state.sideEffect.message.asString(context), Toast.LENGTH_SHORT).show()
}
null -> {}
diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt
index 25b609cc..712d21d0 100644
--- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt
+++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt
@@ -7,16 +7,21 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.ninecraft.booket.core.common.analytics.AnalyticsHelper
+import com.ninecraft.booket.core.common.utils.UiText
+import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.core.data.api.repository.BookRepository
import com.ninecraft.booket.core.model.LibraryBookSummaryModel
+import com.ninecraft.booket.core.model.UserState
import com.ninecraft.booket.core.ui.component.FooterState
import com.ninecraft.booket.feature.screens.BookDetailScreen
import com.ninecraft.booket.feature.screens.LibraryScreen
import com.ninecraft.booket.feature.screens.LibrarySearchScreen
import com.ninecraft.booket.feature.screens.SettingsScreen
+import com.ninecraft.booket.feature.screens.extensions.redirectToLogin
import com.orhanobut.logger.Logger
import com.skydoves.compose.effects.RememberedEffect
import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.retained.collectAsRetainedState
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
@@ -28,10 +33,12 @@ import dagger.hilt.android.components.ActivityRetainedComponent
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
+import com.ninecraft.booket.core.designsystem.R as designR
class LibraryPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
- private val repository: BookRepository,
+ private val bookRepository: BookRepository,
+ private val authRepository: AuthRepository,
private val analyticsHelper: AnalyticsHelper,
) : Presenter {
companion object {
@@ -42,7 +49,7 @@ class LibraryPresenter @AssistedInject constructor(
@Composable
override fun present(): LibraryUiState {
val scope = rememberCoroutineScope()
-
+ val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest)
var uiState by rememberRetained { mutableStateOf(UiState.Idle) }
var footerState by rememberRetained { mutableStateOf(FooterState.Idle) }
var filterChips by rememberRetained {
@@ -63,7 +70,7 @@ class LibraryPresenter @AssistedInject constructor(
footerState = FooterState.Loading
}
- repository.filterLibraryBooks(status = status, page = page, size = size)
+ bookRepository.filterLibraryBooks(status = status, page = page, size = size)
.onSuccess { result ->
filterChips = filterChips.map { chip ->
when (chip.option) {
@@ -109,7 +116,14 @@ class LibraryPresenter @AssistedInject constructor(
}
is LibraryUiEvent.OnLibrarySearchClick -> {
- navigator.goTo(LibrarySearchScreen)
+ if (userState is UserState.Guest) {
+ scope.launch {
+ sideEffect = LibrarySideEffect.ShowToast(UiText.StringResource(designR.string.login_required))
+ navigator.redirectToLogin()
+ }
+ } else {
+ navigator.goTo(LibrarySearchScreen)
+ }
}
is LibraryUiEvent.OnSettingsClick -> {
@@ -151,15 +165,23 @@ class LibraryPresenter @AssistedInject constructor(
restoreState = true,
)
}
+
+ is LibraryUiEvent.OnLoginClick -> {
+ scope.launch {
+ navigator.redirectToLogin()
+ }
+ }
}
}
- RememberedEffect(Unit) {
- filterLibraryBooks(
- status = currentFilter.getApiValue(),
- page = START_INDEX,
- size = PAGE_SIZE,
- )
+ RememberedEffect(userState) {
+ if (userState !is UserState.Guest) {
+ filterLibraryBooks(
+ status = currentFilter.getApiValue(),
+ page = START_INDEX,
+ size = PAGE_SIZE,
+ )
+ }
}
ImpressionEffect {
@@ -172,6 +194,7 @@ class LibraryPresenter @AssistedInject constructor(
filterChips = filterChips,
currentFilter = currentFilter,
books = books,
+ isGuestMode = userState is UserState.Guest,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
index ed1f0512..c69c74bd 100644
--- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
+++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
@@ -1,7 +1,6 @@
package com.ninecraft.booket.feature.library
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.Spacer
@@ -18,6 +17,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.DevicePreview
+import com.ninecraft.booket.core.designsystem.component.button.ReedButton
+import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
+import com.ninecraft.booket.core.designsystem.component.button.mediumButtonStyle
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
import com.ninecraft.booket.core.model.LibraryBookSummaryModel
import com.ninecraft.booket.core.ui.ReedScaffold
@@ -89,7 +91,6 @@ internal fun LibraryContent(
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
) {
FilterChipGroup(
filterList = state.filterChips,
@@ -100,54 +101,86 @@ internal fun LibraryContent(
)
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1))
- when (state.uiState) {
- is UiState.Idle -> {
- EmptyResult()
+ if (state.isGuestMode) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = stringResource(R.string.library_login_required_title),
+ color = ReedTheme.colors.contentPrimary,
+ textAlign = TextAlign.Center,
+ style = ReedTheme.typography.headline1SemiBold,
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
+ Text(
+ text = stringResource(R.string.library_login_required_description),
+ color = ReedTheme.colors.contentSecondary,
+ textAlign = TextAlign.Center,
+ style = ReedTheme.typography.body1Medium,
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
+ ReedButton(
+ onClick = {
+ state.eventSink(LibraryUiEvent.OnLoginClick)
+ },
+ text = stringResource(R.string.login),
+ colorStyle = ReedButtonColorStyle.SECONDARY,
+ sizeStyle = mediumButtonStyle,
+ )
+ }
}
+ } else {
+ when (state.uiState) {
+ is UiState.Idle -> {
+ EmptyResult()
+ }
- is UiState.Loading -> {
- ReedLoadingIndicator()
- }
+ is UiState.Loading -> {
+ ReedLoadingIndicator()
+ }
- is UiState.Success -> {
- if (state.books.isEmpty()) {
- EmptyResult()
- } else {
- InfinityLazyColumn(
- modifier = Modifier.fillMaxSize(),
- loadMore = {
- state.eventSink(LibraryUiEvent.OnLoadMore)
- },
- ) {
- items(state.books) {
- LibraryBookItem(
- book = it,
- onBookClick = {
- state.eventSink(LibraryUiEvent.OnBookClick(it.userBookId, it.isbn13))
- },
- )
- Box(
- modifier = modifier
- .fillMaxWidth()
- .height(1.dp)
- .background(ReedTheme.colors.borderPrimary),
- )
- }
- item {
- LoadStateFooter(
- footerState = state.footerState,
- onRetryClick = { state.eventSink(LibraryUiEvent.OnLoadMore) },
- )
+ is UiState.Success -> {
+ if (state.books.isEmpty()) {
+ EmptyResult()
+ } else {
+ InfinityLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ loadMore = {
+ state.eventSink(LibraryUiEvent.OnLoadMore)
+ },
+ ) {
+ items(state.books) {
+ LibraryBookItem(
+ book = it,
+ onBookClick = {
+ state.eventSink(LibraryUiEvent.OnBookClick(it.userBookId, it.isbn13))
+ },
+ )
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(ReedTheme.colors.borderPrimary),
+ )
+ }
+ item {
+ LoadStateFooter(
+ footerState = state.footerState,
+ onRetryClick = { state.eventSink(LibraryUiEvent.OnLoadMore) },
+ )
+ }
}
}
}
- }
- is UiState.Error -> {
- ReedErrorUi(
- exception = state.uiState.exception,
- onRetryClick = { state.eventSink(LibraryUiEvent.OnRetryClick) },
- )
+ is UiState.Error -> {
+ ReedErrorUi(
+ exception = state.uiState.exception,
+ onRetryClick = { state.eventSink(LibraryUiEvent.OnRetryClick) },
+ )
+ }
}
}
}
diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt
index 83675e2a..cac40f41 100644
--- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt
+++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt
@@ -1,6 +1,7 @@
package com.ninecraft.booket.feature.library
import androidx.compose.runtime.Immutable
+import com.ninecraft.booket.core.common.utils.UiText
import com.ninecraft.booket.core.model.LibraryBookSummaryModel
import com.ninecraft.booket.core.ui.component.FooterState
import com.ninecraft.booket.feature.screens.component.MainTab
@@ -9,6 +10,7 @@ import com.slack.circuit.runtime.CircuitUiState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
+import java.util.UUID
@Immutable
sealed interface UiState {
@@ -25,13 +27,17 @@ data class LibraryUiState(
LibraryFilterOption.entries.map { LibraryFilterChip(option = it, count = 0) }.toPersistentList(),
val currentFilter: LibraryFilterOption = LibraryFilterOption.TOTAL,
val books: ImmutableList = persistentListOf(),
+ val isGuestMode: Boolean = false,
val sideEffect: LibrarySideEffect? = null,
val eventSink: (LibraryUiEvent) -> Unit,
) : CircuitUiState
@Immutable
sealed interface LibrarySideEffect {
- data class ShowToast(val message: String) : LibrarySideEffect
+ data class ShowToast(
+ val message: UiText,
+ private val key: String = UUID.randomUUID().toString(),
+ ) : LibrarySideEffect
}
sealed interface LibraryUiEvent : CircuitUiEvent {
@@ -42,10 +48,12 @@ sealed interface LibraryUiEvent : CircuitUiEvent {
val userBookId: String,
val isbn13: String,
) : LibraryUiEvent
+
data object OnLoadMore : LibraryUiEvent
data object OnRetryClick : LibraryUiEvent
data class OnFilterClick(val filterOption: LibraryFilterOption) : LibraryUiEvent
data class OnTabSelected(val tab: MainTab) : LibraryUiEvent
+ data object OnLoginClick : LibraryUiEvent
}
data class LibraryFilterChip(
diff --git a/feature/library/src/main/res/values/strings.xml b/feature/library/src/main/res/values/strings.xml
index 145b3d4a..5bc04e2a 100644
--- a/feature/library/src/main/res/values/strings.xml
+++ b/feature/library/src/main/res/values/strings.xml
@@ -8,4 +8,7 @@
완독
아직 등록된 책이 없어요
도서 등록 후 나만의 아카이브를 만들어보세요
+ 로그인이 필요해요
+ 로그인 후 나만의 서재를 채워보세요
+ 로그인 하기
diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt
index 1fbc323f..f0f90cc6 100644
--- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt
+++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt
@@ -15,6 +15,7 @@ import com.orhanobut.logger.Logger
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.popUntil
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuitx.effects.ImpressionEffect
import dagger.assisted.Assisted
@@ -24,6 +25,7 @@ import dagger.hilt.android.components.ActivityRetainedComponent
import kotlinx.coroutines.launch
class LoginPresenter @AssistedInject constructor(
+ @Assisted private val screen: LoginScreen,
@Assisted private val navigator: Navigator,
private val authRepository: AuthRepository,
private val userRepository: UserRepository,
@@ -45,9 +47,13 @@ class LoginPresenter @AssistedInject constructor(
userRepository.getUserProfile()
.onSuccess { userProfile ->
if (userProfile.termsAgreed) {
- navigator.resetRoot(HomeScreen)
+ if (screen.returnToScreen == null) {
+ navigator.resetRoot(HomeScreen)
+ } else {
+ navigator.popUntil { it == screen.returnToScreen }
+ }
} else {
- navigator.resetRoot(TermsAgreementScreen)
+ navigator.resetRoot(TermsAgreementScreen(screen.returnToScreen))
}
}.onFailure { exception ->
exception.message?.let { Logger.e(it) }
@@ -90,15 +96,24 @@ class LoginPresenter @AssistedInject constructor(
}
}
}
+
+ is LoginUiEvent.OnGuestLoginButtonClick -> {
+ navigator.resetRoot(HomeScreen)
+ }
+
+ is LoginUiEvent.OnCloseButtonClick -> {
+ navigator.pop()
+ }
}
}
ImpressionEffect {
- analyticsHelper.logScreenView(LoginScreen.name)
+ analyticsHelper.logScreenView(screen.name)
}
return LoginUiState(
isLoading = isLoading,
+ returnToScreen = screen.returnToScreen,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
@@ -107,6 +122,9 @@ class LoginPresenter @AssistedInject constructor(
@CircuitInject(LoginScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
- fun create(navigator: Navigator): LoginPresenter
+ fun create(
+ screen: LoginScreen,
+ navigator: Navigator,
+ ): LoginPresenter
}
}
diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt
index c56d466f..359989bf 100644
--- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt
+++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -25,10 +24,13 @@ import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.DevicePreview
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
+import com.ninecraft.booket.core.designsystem.component.button.ReedTextButton
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
+import com.ninecraft.booket.core.designsystem.component.button.smallButtonStyle
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
import com.ninecraft.booket.core.designsystem.theme.White
import com.ninecraft.booket.core.ui.ReedScaffold
+import com.ninecraft.booket.core.ui.component.ReedCloseTopAppBar
import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator
import com.ninecraft.booket.feature.screens.LoginScreen
import com.slack.circuit.codegen.annotations.CircuitInject
@@ -52,12 +54,19 @@ internal fun LoginUi(
modifier = modifier
.fillMaxSize()
.background(White)
- .systemBarsPadding(),
+ .padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
- Box(modifier = modifier.fillMaxSize()) {
+ Box(modifier = Modifier.fillMaxSize()) {
Column {
+ if (state.returnToScreen != null) {
+ ReedCloseTopAppBar(
+ onClose = {
+ state.eventSink(LoginUiEvent.OnCloseButtonClick)
+ },
+ )
+ }
Column(
modifier = Modifier
.fillMaxWidth()
@@ -77,28 +86,45 @@ internal fun LoginUi(
style = ReedTheme.typography.headline2SemiBold,
)
}
- ReedButton(
- onClick = {
- state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick)
- },
- sizeStyle = largeButtonStyle,
- colorStyle = ReedButtonColorStyle.KAKAO,
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- start = ReedTheme.spacing.spacing5,
- end = ReedTheme.spacing.spacing5,
- bottom = ReedTheme.spacing.spacing8,
- ),
- text = stringResource(id = R.string.kakao_login),
- leadingIcon = {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao),
- contentDescription = "Kakao Icon",
- tint = Color.Unspecified,
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ ReedButton(
+ onClick = {
+ state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick)
+ },
+ sizeStyle = largeButtonStyle,
+ colorStyle = ReedButtonColorStyle.KAKAO,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = ReedTheme.spacing.spacing5,
+ end = ReedTheme.spacing.spacing5,
+ ),
+ text = stringResource(id = R.string.kakao_login),
+ leadingIcon = {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao),
+ contentDescription = "Kakao Icon",
+ tint = Color.Unspecified,
+ )
+ },
+ )
+ Spacer(
+ modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing2 else ReedTheme.spacing.spacing8),
+ )
+ if (state.returnToScreen == null) {
+ ReedTextButton(
+ onClick = {
+ state.eventSink(LoginUiEvent.OnGuestLoginButtonClick)
+ },
+ text = stringResource(R.string.guest_login),
+ sizeStyle = smallButtonStyle,
+ colorStyle = ReedButtonColorStyle.TEXT,
)
- },
- )
+ }
+ }
}
if (state.isLoading) {
diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt
index fea02984..731e2bcb 100644
--- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt
+++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt
@@ -3,10 +3,12 @@ package com.ninecraft.booket.feature.login
import androidx.compose.runtime.Immutable
import com.slack.circuit.runtime.CircuitUiEvent
import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.screen.Screen
import java.util.UUID
data class LoginUiState(
val isLoading: Boolean = false,
+ val returnToScreen: Screen? = null,
val sideEffect: LoginSideEffect? = null,
val eventSink: (LoginUiEvent) -> Unit,
) : CircuitUiState
@@ -24,4 +26,6 @@ sealed interface LoginUiEvent : CircuitUiEvent {
data object OnKakaoLoginButtonClick : LoginUiEvent
data class Login(val accessToken: String) : LoginUiEvent
data class LoginFailure(val message: String) : LoginUiEvent
+ data object OnGuestLoginButtonClick : LoginUiEvent
+ data object OnCloseButtonClick : LoginUiEvent
}
diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt
index cd68ea36..a1f4cf04 100644
--- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt
+++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt
@@ -17,6 +17,7 @@ import com.orhanobut.logger.Logger
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.popUntil
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuitx.effects.ImpressionEffect
import dagger.assisted.Assisted
@@ -28,6 +29,7 @@ import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
class TermsAgreementPresenter @AssistedInject constructor(
+ @Assisted private val screen: TermsAgreementScreen,
@Assisted private val navigator: Navigator,
private val userRepository: UserRepository,
private val analyticsHelper: AnalyticsHelper,
@@ -73,7 +75,11 @@ class TermsAgreementPresenter @AssistedInject constructor(
scope.launch {
userRepository.agreeTerms(true)
.onSuccess {
- navigator.resetRoot(HomeScreen)
+ if (screen.returnToScreen != null) {
+ navigator.popUntil { it == screen.returnToScreen }
+ } else {
+ navigator.resetRoot(HomeScreen)
+ }
}.onFailure { exception ->
exception.message?.let { Logger.e(it) }
sideEffect = exception.message?.let {
@@ -86,7 +92,7 @@ class TermsAgreementPresenter @AssistedInject constructor(
}
ImpressionEffect {
- analyticsHelper.logScreenView(TermsAgreementScreen.name)
+ analyticsHelper.logScreenView(screen.name)
}
return TermsAgreementUiState(
@@ -99,6 +105,9 @@ class TermsAgreementPresenter @AssistedInject constructor(
@CircuitInject(TermsAgreementScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
- fun create(navigator: Navigator): TermsAgreementPresenter
+ fun create(
+ screen: TermsAgreementScreen,
+ navigator: Navigator,
+ ): TermsAgreementPresenter
}
}
diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml
index efbe84fc..51ac38df 100644
--- a/feature/login/src/main/res/values/strings.xml
+++ b/feature/login/src/main/res/values/strings.xml
@@ -5,6 +5,7 @@
약관 동의 후\n독서 기록을 남겨보세요
약관 전체 동의
시작하기
+ 회원가입 없이 둘러보기
- (필수)서비스 이용약관
- (필수)개인정보처리방침
diff --git a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt
index 92efa53b..63304a27 100644
--- a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt
+++ b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingPresenter.kt
@@ -37,7 +37,7 @@ class OnboardingPresenter @AssistedInject constructor(
if (event.currentPage == 2) {
scope.launch {
repository.setOnboardingCompleted(true)
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
}
} else {
pagerState.let { state ->
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt
index 86291fc4..7ddb581d 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt
@@ -7,7 +7,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.ninecraft.booket.core.common.utils.handleException
-import com.ninecraft.booket.core.ocr.analyzer.CloudOcrRecognizer
+import com.ninecraft.booket.core.ocr.recognizer.CloudOcrRecognizer
import com.ninecraft.booket.core.common.analytics.AnalyticsHelper
import com.ninecraft.booket.feature.screens.OcrScreen
import com.orhanobut.logger.Logger
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt
index 4a77fefb..76af3913 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUi.kt
@@ -35,10 +35,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
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
@@ -68,7 +68,6 @@ import com.ninecraft.booket.feature.record.R
import com.ninecraft.booket.feature.record.ocr.component.CameraFrame
import com.ninecraft.booket.feature.record.ocr.component.SentenceBox
import com.ninecraft.booket.feature.screens.OcrScreen
-import com.skydoves.compose.effects.RememberedEffect
import com.slack.circuit.codegen.annotations.CircuitInject
import dagger.hilt.android.components.ActivityRetainedComponent
import tech.thdev.compose.exteions.system.ui.controller.rememberSystemUiController
@@ -101,16 +100,31 @@ private fun CameraPreview(
/**
* Camera Permission Request
*/
- var isGranted by remember {
- mutableStateOf(
- ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED,
- )
+ val isGranted by produceState(
+ initialValue = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED,
+ key1 = lifecycleOwner, // lifecycle 변경 시 재설정
+ ) {
+ // 최초 동기화
+ value = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+
+ // 포그라운드 복귀 시 OS 권한 동기화
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ value = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+ if (value) {
+ state.eventSink(OcrUiEvent.OnHidePermissionDialog)
+ } else {
+ state.eventSink(OcrUiEvent.OnShowPermissionDialog)
+ }
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ awaitDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
+
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
) { granted ->
- isGranted = granted
-
if (!granted) {
state.eventSink(OcrUiEvent.OnShowPermissionDialog)
}
@@ -120,29 +134,13 @@ private fun CameraPreview(
) { _ -> }
// 최초 진입 시 권한 요청
- RememberedEffect(Unit) {
+ LaunchedEffect(Unit) {
if (!isGranted) {
state.eventSink(OcrUiEvent.OnHidePermissionDialog)
permissionLauncher.launch(permission)
}
}
- // 앱이 포그라운드로 북귀할 때 OS 권한 동기화
- DisposableEffect(Unit) {
- val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME) {
- isGranted = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
- if (isGranted) {
- state.eventSink(OcrUiEvent.OnHidePermissionDialog)
- } else {
- state.eventSink(OcrUiEvent.OnShowPermissionDialog)
- }
- }
- }
- lifecycleOwner.lifecycle.addObserver(observer)
- onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
- }
-
/**
* Camera Controller
*/
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
index 63fb8fda..26a58a92 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
@@ -146,7 +146,7 @@ class RecordRegisterPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt
index 3ae920d7..ac3a7278 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt
@@ -85,7 +85,7 @@ internal fun RecordRegisterUi(
}
if (state.isLoading) {
- ReedLoadingIndicator()
+ ReedLoadingIndicator(delayMillis = 0L)
}
if (state.isExitDialogVisible) {
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt
index 58f22329..36b7b33e 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt
@@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.common.extensions.clickableSingle
import com.ninecraft.booket.core.designsystem.ComponentPreview
import com.ninecraft.booket.core.designsystem.EmotionTag
-import com.ninecraft.booket.core.designsystem.RecordStep
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
@@ -113,7 +112,7 @@ fun EmotionStep(
.padding(bottom = ReedTheme.spacing.spacing4),
enabled = state.isNextButtonEnabled,
text = stringResource(R.string.record_next_button),
- multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION,
+ multipleEventsCutterEnabled = false,
)
}
}
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt
index 346e51b8..948ac648 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt
@@ -38,7 +38,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.ComponentPreview
-import com.ninecraft.booket.core.designsystem.RecordStep
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
@@ -163,7 +162,7 @@ fun ImpressionStep(
),
enabled = state.isNextButtonEnabled,
text = stringResource(R.string.record_next_button),
- multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION,
+ multipleEventsCutterEnabled = true,
)
}
@@ -190,6 +189,7 @@ fun ImpressionStep(
coroutineScope.launch {
impressionGuideBottomSheetState.hide()
state.eventSink(RecordRegisterUiEvent.OnImpressionGuideConfirmed)
+ focusRequester.requestFocus()
}
},
)
diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt
index ecdd2a26..b9318765 100644
--- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt
+++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt
@@ -36,7 +36,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.ComponentPreview
-import com.ninecraft.booket.core.designsystem.RecordStep
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
@@ -178,7 +177,7 @@ internal fun QuoteStep(
),
enabled = state.isNextButtonEnabled,
text = stringResource(R.string.record_next_button),
- multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION,
+ multipleEventsCutterEnabled = false,
)
}
}
diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt
index 1c31c461..338d70a1 100644
--- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt
+++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/ScreenNames.kt
@@ -5,7 +5,7 @@ object ScreenNames {
const val HOME = "home_main"
const val LIBRARY = "library_main"
const val LOGIN = "login_select_method"
- const val SEARCH = "search_book_input"
+ const val BOOK_SEARCH = "search_book_start"
const val LIBRARY_SEARCH = "library_search_book"
const val TERMS_AGREEMENT = "login_terms_agreement"
const val SETTINGS = "settings_main"
diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt
index f7f1ae61..05f01d7b 100644
--- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt
+++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt
@@ -16,16 +16,16 @@ data object HomeScreen : ReedScreen(name = ScreenNames.HOME)
data object LibraryScreen : ReedScreen(name = ScreenNames.LIBRARY)
@Parcelize
-data object LoginScreen : ReedScreen(name = ScreenNames.LOGIN)
+data class LoginScreen(val returnToScreen: Screen? = null) : ReedScreen(name = ScreenNames.LOGIN)
@Parcelize
-data object SearchScreen : ReedScreen(name = ScreenNames.SEARCH)
+data object BookSearchScreen : ReedScreen(name = ScreenNames.BOOK_SEARCH)
@Parcelize
data object LibrarySearchScreen : ReedScreen(name = ScreenNames.LIBRARY_SEARCH)
@Parcelize
-data object TermsAgreementScreen : ReedScreen(name = ScreenNames.TERMS_AGREEMENT)
+data class TermsAgreementScreen(val returnToScreen: Screen? = null) : ReedScreen(name = ScreenNames.TERMS_AGREEMENT)
@Parcelize
data object SettingsScreen : ReedScreen(name = ScreenNames.SETTINGS)
diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt
index a23c9bcb..e4195a0a 100644
--- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt
+++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/component/MainBottomBar.kt
@@ -30,10 +30,6 @@ import com.adamglin.composeshadow.dropShadow
import com.ninecraft.booket.core.designsystem.ComponentPreview
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
import com.ninecraft.booket.core.designsystem.theme.White
-import com.slack.circuit.backstack.SaveableBackStack
-import com.slack.circuit.runtime.Navigator
-import com.slack.circuit.runtime.popUntil
-import com.slack.circuit.runtime.screen.Screen
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -132,23 +128,6 @@ private fun RowScope.MainBottomBarItem(
}
}
-@Suppress("unused")
-fun Navigator.popUntilOrGoTo(screen: Screen) {
- if (screen in peekBackStack()) {
- popUntil { it == screen }
- } else {
- goTo(screen)
- }
-}
-
-@Composable
-fun getCurrentTab(backStack: SaveableBackStack): MainTab? {
- val currentScreen = backStack.topRecord?.screen
- return currentScreen?.let { screen ->
- MainTab.entries.find { it.screen::class == currentScreen::class }
- }
-}
-
@ComponentPreview
@Composable
private fun MainBottomBarPreview() {
diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt
index 521365ed..711f4a6e 100644
--- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt
+++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/extensions/Navigator.kt
@@ -1,7 +1,9 @@
package com.ninecraft.booket.feature.screens.extensions
+import com.ninecraft.booket.feature.screens.LoginScreen
import com.ninecraft.booket.feature.screens.ReedScreen
import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.screen.Screen
import kotlinx.coroutines.delay
suspend fun Navigator.delayedGoTo(screen: ReedScreen, delayMillis: Long = 200L) {
@@ -13,3 +15,9 @@ suspend fun Navigator.delayedPop(delayMillis: Long = 200L) {
delay(delayMillis)
pop()
}
+
+suspend fun Navigator.redirectToLogin(): Screen? {
+ val currentScreen = peek()
+ delayedGoTo(LoginScreen(currentScreen))
+ return currentScreen
+}
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt
index 61cc99e5..d6e890ec 100644
--- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt
@@ -10,21 +10,26 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.ninecraft.booket.core.common.analytics.AnalyticsHelper
import com.ninecraft.booket.core.common.constants.BookStatus
+import com.ninecraft.booket.core.common.utils.UiText
import com.ninecraft.booket.core.common.utils.handleException
+import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.core.data.api.repository.BookRepository
import com.ninecraft.booket.core.model.BookSearchModel
import com.ninecraft.booket.core.model.BookSummaryModel
+import com.ninecraft.booket.core.model.UserState
import com.ninecraft.booket.core.ui.component.FooterState
+import com.ninecraft.booket.feature.screens.BookSearchScreen
import com.ninecraft.booket.feature.screens.LoginScreen
import com.ninecraft.booket.feature.screens.RecordScreen
-import com.ninecraft.booket.feature.screens.SearchScreen
-import com.ninecraft.booket.feature.screens.extensions.delayedGoTo
+import com.ninecraft.booket.feature.screens.extensions.redirectToLogin
+import com.ninecraft.booket.feature.search.R
import com.orhanobut.logger.Logger
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.collectAsRetainedState
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuitx.effects.ImpressionEffect
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -33,17 +38,17 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
+import com.ninecraft.booket.core.designsystem.R as designR
class BookSearchPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
private val repository: BookRepository,
+ private val authRepository: AuthRepository,
private val analyticsHelper: AnalyticsHelper,
) : Presenter {
companion object {
private const val START_INDEX = 1
- private const val SEARCH_BOOK_INPUT = "search_book_input"
private const val SEARCH_BOOK_RESULT = "search_book_result"
- private const val SEARCH_BOOK_NO_RESULT = "search_book_noresult"
private const val ERROR_SEARCH_LOADING = "error_search_loading"
private const val REGISTER_BOOK_OPTION = "register_book_option"
private const val REGISTER_BOOK_COMPLETE = "register_book_complete"
@@ -53,6 +58,7 @@ class BookSearchPresenter @AssistedInject constructor(
@Composable
override fun present(): BookSearchUiState {
val scope = rememberCoroutineScope()
+ val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest)
var uiState by rememberRetained { mutableStateOf(UiState.Idle) }
var footerState by rememberRetained { mutableStateOf(FooterState.Idle) }
val queryState = rememberTextFieldState()
@@ -76,7 +82,11 @@ class BookSearchPresenter @AssistedInject constructor(
footerState = FooterState.Loading
}
- repository.searchBook(query = query, start = startIndex)
+ if (userState is UserState.Guest) {
+ repository.searchBookAsGuest(query = query, start = startIndex)
+ } else {
+ repository.searchBook(query = query, start = startIndex)
+ }
.onSuccess { result ->
searchResult = result
books = if (startIndex == START_INDEX) {
@@ -90,14 +100,10 @@ class BookSearchPresenter @AssistedInject constructor(
if (startIndex == START_INDEX) {
uiState = UiState.Success
+ analyticsHelper.logEvent(SEARCH_BOOK_RESULT)
} else {
footerState = if (isLastPage) FooterState.End else FooterState.Idle
}
-
- analyticsHelper.logEvent(SEARCH_BOOK_RESULT)
- if (startIndex == START_INDEX && result.books.isEmpty()) {
- analyticsHelper.logEvent(SEARCH_BOOK_NO_RESULT)
- }
}
.onFailure { exception ->
Logger.d(exception)
@@ -114,6 +120,12 @@ class BookSearchPresenter @AssistedInject constructor(
fun upsertBook(isbn13: String, bookStatus: String) {
scope.launch {
+ if (userState is UserState.Guest) {
+ sideEffect = BookSearchSideEffect.ShowToast(UiText.StringResource(designR.string.login_required))
+ navigator.redirectToLogin()
+ return@launch
+ }
+
repository.upsertBook(isbn13, bookStatus)
.onSuccess {
registeredUserBookId = it.userBookId
@@ -133,14 +145,14 @@ class BookSearchPresenter @AssistedInject constructor(
analyticsHelper.logEvent(ERROR_REGISTER_BOOK)
val handleErrorMessage = { message: String ->
Logger.e(message)
- sideEffect = BookSearchSideEffect.ShowToast(message)
+ sideEffect = BookSearchSideEffect.ShowToast(UiText.DirectString(message))
}
handleException(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
@@ -149,6 +161,10 @@ class BookSearchPresenter @AssistedInject constructor(
fun handleEvent(event: BookSearchUiEvent) {
when (event) {
+ is BookSearchUiEvent.InitSideEffect -> {
+ sideEffect = null
+ }
+
is BookSearchUiEvent.OnBackClick -> {
navigator.pop()
}
@@ -170,7 +186,6 @@ class BookSearchPresenter @AssistedInject constructor(
is BookSearchUiEvent.OnSearchClick -> {
val query = event.query.trim()
if (query.isNotEmpty()) {
- analyticsHelper.logEvent(SEARCH_BOOK_INPUT)
searchBooks(query = query, startIndex = START_INDEX)
}
}
@@ -197,7 +212,7 @@ class BookSearchPresenter @AssistedInject constructor(
selectedBookIsbn = event.isbn13
if (selectedBookIsbn.isEmpty()) {
- sideEffect = BookSearchSideEffect.ShowToast("isbn이 없는 도서는 등록할 수 없습니다")
+ sideEffect = BookSearchSideEffect.ShowToast(UiText.StringResource(R.string.error_book_no_isbn))
} else {
isBookRegisterBottomSheetVisible = true
}
@@ -224,9 +239,7 @@ class BookSearchPresenter @AssistedInject constructor(
is BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick -> {
isBookRegisterSuccessBottomSheetVisible = false
- scope.launch {
- navigator.delayedGoTo(RecordScreen(registeredUserBookId))
- }
+ navigator.goTo(RecordScreen(registeredUserBookId))
}
is BookSearchUiEvent.OnBookRegisterSuccessCancelButtonClick -> {
@@ -235,6 +248,10 @@ class BookSearchPresenter @AssistedInject constructor(
}
}
+ ImpressionEffect {
+ analyticsHelper.logScreenView(BookSearchScreen.name)
+ }
+
return BookSearchUiState(
uiState = uiState,
footerState = footerState,
@@ -246,12 +263,13 @@ class BookSearchPresenter @AssistedInject constructor(
isBookRegisterBottomSheetVisible = isBookRegisterBottomSheetVisible,
selectedBookStatus = selectedBookStatus,
isBookRegisterSuccessBottomSheetVisible = isBookRegisterSuccessBottomSheetVisible,
+ isGuestMode = userState is UserState.Guest,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
}
- @CircuitInject(SearchScreen::class, ActivityRetainedComponent::class)
+ @CircuitInject(BookSearchScreen::class, ActivityRetainedComponent::class)
@AssistedFactory
fun interface Factory {
fun create(navigator: Navigator): BookSearchPresenter
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
index 9e4a9c8f..ad1fd7ec 100644
--- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
@@ -32,7 +32,7 @@ import com.ninecraft.booket.core.ui.component.LoadStateFooter
import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar
import com.ninecraft.booket.core.ui.component.ReedErrorUi
import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator
-import com.ninecraft.booket.feature.screens.SearchScreen
+import com.ninecraft.booket.feature.screens.BookSearchScreen
import com.ninecraft.booket.feature.search.R
import com.ninecraft.booket.feature.search.book.component.BookItem
import com.ninecraft.booket.feature.search.book.component.BookRegisterBottomSheet
@@ -45,13 +45,16 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
import com.ninecraft.booket.core.designsystem.R as designR
-@CircuitInject(SearchScreen::class, ActivityRetainedComponent::class)
+@CircuitInject(BookSearchScreen::class, ActivityRetainedComponent::class)
@Composable
-internal fun SearchUi(
+internal fun BookSearchUi(
state: BookSearchUiState,
modifier: Modifier = Modifier,
) {
- HandleBookSearchSideEffects(state = state)
+ HandleBookSearchSideEffects(
+ state = state,
+ eventSink = state.eventSink,
+ )
ReedScaffold(
modifier = modifier.fillMaxSize(),
@@ -68,7 +71,7 @@ internal fun SearchUi(
state.eventSink(BookSearchUiEvent.OnBackClick)
},
)
- SearchContent(
+ BookSearchContent(
state = state,
modifier = Modifier,
)
@@ -78,7 +81,7 @@ internal fun SearchUi(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-internal fun SearchContent(
+internal fun BookSearchContent(
state: BookSearchUiState,
modifier: Modifier = Modifier,
) {
@@ -219,7 +222,7 @@ internal fun SearchContent(
onBookClick = { book ->
state.eventSink(BookSearchUiEvent.OnBookClick(book.isbn13))
},
- enabled = BookRegisteredState.from(state.books[index].userBookStatus) == BookRegisteredState.BEFORE_REGISTRATION,
+ enabled = !state.books[index].isRegistered,
)
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
@@ -271,7 +274,12 @@ internal fun SearchContent(
state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessBottomSheetDismiss)
}
},
- onOKButtonClick = { state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick) },
+ onOKButtonClick = {
+ coroutineScope.launch {
+ bookRegisterSuccessBottomSheetState.hide()
+ state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick)
+ }
+ },
)
}
}
@@ -279,9 +287,9 @@ internal fun SearchContent(
@DevicePreview
@Composable
-private fun SearchPreview() {
+private fun BookSearchPreview() {
ReedTheme {
- SearchUi(
+ BookSearchUi(
state = BookSearchUiState(
eventSink = {},
),
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt
index 5f023961..09641b05 100644
--- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt
@@ -3,6 +3,7 @@ package com.ninecraft.booket.feature.search.book
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Immutable
import com.ninecraft.booket.core.common.constants.BookStatus
+import com.ninecraft.booket.core.common.utils.UiText
import com.ninecraft.booket.core.model.BookSearchModel
import com.ninecraft.booket.core.model.BookSummaryModel
import com.ninecraft.booket.core.ui.component.FooterState
@@ -31,6 +32,7 @@ data class BookSearchUiState(
val isBookRegisterBottomSheetVisible: Boolean = false,
val selectedBookStatus: BookStatus? = null,
val isBookRegisterSuccessBottomSheetVisible: Boolean = false,
+ val isGuestMode: Boolean = false,
val sideEffect: BookSearchSideEffect? = null,
val eventSink: (BookSearchUiEvent) -> Unit,
) : CircuitUiState {
@@ -40,12 +42,13 @@ data class BookSearchUiState(
@Immutable
sealed interface BookSearchSideEffect {
data class ShowToast(
- val message: String,
+ val message: UiText,
private val key: String = UUID.randomUUID().toString(),
) : BookSearchSideEffect
}
sealed interface BookSearchUiEvent : CircuitUiEvent {
+ data object InitSideEffect : BookSearchUiEvent
data object OnBackClick : BookSearchUiEvent
data class OnRecentSearchClick(val query: String) : BookSearchUiEvent
data class OnRecentSearchDeleteClick(val query: String) : BookSearchUiEvent
@@ -61,14 +64,3 @@ sealed interface BookSearchUiEvent : CircuitUiEvent {
data object OnBookRegisterSuccessOkButtonClick : BookSearchUiEvent
data object OnBookRegisterSuccessCancelButtonClick : BookSearchUiEvent
}
-
-enum class BookRegisteredState(val value: String) {
- BEFORE_REGISTRATION("BEFORE_REGISTRATION"),
- ;
-
- companion object {
- fun from(value: String?): BookRegisteredState? {
- return entries.find { it.value == value }
- }
- }
-}
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandlingBookSearchSideEffect.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandleBookSearchSideEffect.kt
similarity index 75%
rename from feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandlingBookSearchSideEffect.kt
rename to feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandleBookSearchSideEffect.kt
index 7249bf51..27fd56e1 100644
--- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandlingBookSearchSideEffect.kt
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/HandleBookSearchSideEffect.kt
@@ -8,16 +8,21 @@ import com.skydoves.compose.effects.RememberedEffect
@Composable
internal fun HandleBookSearchSideEffects(
state: BookSearchUiState,
+ eventSink: (BookSearchUiEvent) -> Unit,
) {
val context = LocalContext.current
RememberedEffect(state.sideEffect) {
when (state.sideEffect) {
is BookSearchSideEffect.ShowToast -> {
- Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, state.sideEffect.message.asString(context), Toast.LENGTH_SHORT).show()
}
null -> {}
}
+
+ if (state.sideEffect != null) {
+ eventSink(BookSearchUiEvent.InitSideEffect)
+ }
}
}
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt
index 240d20e7..fc4799d0 100644
--- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt
@@ -100,7 +100,7 @@ class LibrarySearchPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml
index fe971294..d8f946d7 100644
--- a/feature/search/src/main/res/values/strings.xml
+++ b/feature/search/src/main/res/values/strings.xml
@@ -18,4 +18,5 @@
등록한 책을 검색해보세요
남긴 기록
내 서재에 해당 도서가 없습니다.
+ isbn이 없는 도서는 등록할 수 없습니다
diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt
index fefe62d2..007b1aa0 100644
--- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt
+++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt
@@ -10,13 +10,16 @@ import com.ninecraft.booket.core.common.constants.WebViewConstants
import com.ninecraft.booket.core.common.utils.handleException
import com.ninecraft.booket.core.data.api.repository.AuthRepository
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
+import com.ninecraft.booket.core.model.UserState
import com.ninecraft.booket.feature.screens.LoginScreen
import com.ninecraft.booket.feature.screens.OssLicensesScreen
import com.ninecraft.booket.feature.screens.SettingsScreen
import com.ninecraft.booket.feature.screens.WebViewScreen
+import com.ninecraft.booket.feature.screens.extensions.redirectToLogin
import com.orhanobut.logger.Logger
import com.skydoves.compose.effects.RememberedEffect
import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.retained.collectAsRetainedState
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
@@ -43,6 +46,7 @@ class SettingsPresenter @AssistedInject constructor(
@Composable
override fun present(): SettingsUiState {
val scope = rememberCoroutineScope()
+ val userState by authRepository.userState.collectAsRetainedState(initial = UserState.Guest)
var isLoading by rememberRetained { mutableStateOf(false) }
var isLogoutDialogVisible by rememberRetained { mutableStateOf(false) }
var isWithdrawBottomSheetVisible by rememberRetained { mutableStateOf(false) }
@@ -58,7 +62,7 @@ class SettingsPresenter @AssistedInject constructor(
authRepository.logout()
.onSuccess {
analyticsHelper.logEvent(SETTINGS_LOGOUT_COMPLETE)
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
}
.onFailure { exception ->
val handleErrorMessage = { message: String ->
@@ -70,13 +74,12 @@ class SettingsPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
} finally {
isLoading = false
- isLogoutDialogVisible = false
}
}
}
@@ -88,7 +91,7 @@ class SettingsPresenter @AssistedInject constructor(
authRepository.withdraw()
.onSuccess {
analyticsHelper.logEvent(SETTINGS_WITHDRAWAL_COMPLETE)
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
}
.onFailure { exception ->
val handleErrorMessage = { message: String ->
@@ -100,13 +103,12 @@ class SettingsPresenter @AssistedInject constructor(
exception = exception,
onError = handleErrorMessage,
onLoginRequired = {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
},
)
}
} finally {
isLoading = false
- isWithdrawBottomSheetVisible = false
}
}
}
@@ -180,16 +182,24 @@ class SettingsPresenter @AssistedInject constructor(
}
is SettingsUiEvent.Logout -> {
+ isLogoutDialogVisible = false
logout()
}
is SettingsUiEvent.Withdraw -> {
+ isWithdrawBottomSheetVisible = false
withdraw()
}
is SettingsUiEvent.OnVersionClick -> {
sideEffect = SettingsSideEffect.NavigateToPlayStore
}
+
+ is SettingsUiEvent.OnLoginClick -> {
+ scope.launch {
+ navigator.redirectToLogin()
+ }
+ }
}
}
@@ -208,6 +218,7 @@ class SettingsPresenter @AssistedInject constructor(
isWithdrawConfirmed = isWithdrawConfirmed,
latestVersion = latestVersion,
isOptionalUpdateDialogVisible = isOptionalUpdateDialogVisible,
+ isGuestMode = userState is UserState.Guest,
sideEffect = sideEffect,
eventSink = ::handleEvent,
)
diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt
index 37a60a3a..b5989fdd 100644
--- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt
+++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt
@@ -150,18 +150,28 @@ internal fun SettingsUi(
},
)
ReedDivider(modifier = Modifier.padding(vertical = ReedTheme.spacing.spacing4))
- SettingItem(
- title = stringResource(R.string.settings_logout),
- onItemClick = {
- state.eventSink(SettingsUiEvent.OnLogoutClick)
- },
- )
- SettingItem(
- title = stringResource(R.string.settings_withdraw),
- onItemClick = {
- state.eventSink(SettingsUiEvent.OnWithdrawClick)
- },
- )
+ if (state.isGuestMode) {
+ SettingItem(
+ title = stringResource(R.string.settings_login),
+ onItemClick = {
+ state.eventSink(SettingsUiEvent.OnLoginClick)
+ },
+ )
+ Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
+ } else {
+ SettingItem(
+ title = stringResource(R.string.settings_logout),
+ onItemClick = {
+ state.eventSink(SettingsUiEvent.OnLogoutClick)
+ },
+ )
+ SettingItem(
+ title = stringResource(R.string.settings_withdraw),
+ onItemClick = {
+ state.eventSink(SettingsUiEvent.OnWithdrawClick)
+ },
+ )
+ }
}
if (state.isLoading) {
diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt
index fb7961e5..43b50cb2 100644
--- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt
+++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt
@@ -13,6 +13,7 @@ data class SettingsUiState(
val latestVersion: String = "",
val isUpdateAvailable: Boolean = false,
val isOptionalUpdateDialogVisible: Boolean = false,
+ val isGuestMode: Boolean = false,
val sideEffect: SettingsSideEffect? = null,
val eventSink: (SettingsUiEvent) -> Unit,
) : CircuitUiState
@@ -40,4 +41,5 @@ sealed interface SettingsUiEvent : CircuitUiEvent {
data object Logout : SettingsUiEvent
data object Withdraw : SettingsUiEvent
data object OnVersionClick : SettingsUiEvent
+ data object OnLoginClick : SettingsUiEvent
}
diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt
index 106d9f2f..e9f23d27 100644
--- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt
+++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesPresenter.kt
@@ -17,7 +17,7 @@ class OssLicensesPresenter @AssistedInject constructor(
override fun present(): OssLicensesUiState {
fun handleEvent(event: OssLicensesUiEvent) {
when (event) {
- is OssLicensesUiEvent.OnBackClicked -> {
+ is OssLicensesUiEvent.OnBackClick -> {
navigator.pop()
}
}
diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt
index 3c31ff3e..dc804a52 100644
--- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt
+++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUi.kt
@@ -71,7 +71,7 @@ internal fun OssLicenses(
ReedBackTopAppBar(
title = stringResource(R.string.oss_licenses_title),
onBackClick = {
- state.eventSink(OssLicensesUiEvent.OnBackClicked)
+ state.eventSink(OssLicensesUiEvent.OnBackClick)
},
)
LazyColumn {
diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt
index 5f829072..30a82745 100644
--- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt
+++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/osslicenses/OssLicensesUiState.kt
@@ -8,5 +8,5 @@ data class OssLicensesUiState(
) : CircuitUiState
sealed interface OssLicensesUiEvent : CircuitUiEvent {
- data object OnBackClicked : OssLicensesUiEvent
+ data object OnBackClick : OssLicensesUiEvent
}
diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml
index fa897943..7051c68a 100644
--- a/feature/settings/src/main/res/values/strings.xml
+++ b/feature/settings/src/main/res/values/strings.xml
@@ -18,4 +18,5 @@
최신 버전이 출시되었습니다.
최적의 사용 환경을 위해 업데이트해주세요.
업데이트하기
+ 로그인
diff --git a/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt b/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt
index a039dc62..6af98e78 100644
--- a/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt
+++ b/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashPresenter.kt
@@ -56,7 +56,7 @@ class SplashPresenter @AssistedInject constructor(
if (userProfile.termsAgreed) {
navigator.resetRoot(HomeScreen)
} else {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
}
}
.onFailure { exception ->
@@ -83,7 +83,7 @@ class SplashPresenter @AssistedInject constructor(
}
AutoLoginState.NOT_LOGGED_IN -> {
- navigator.resetRoot(LoginScreen)
+ navigator.resetRoot(LoginScreen())
}
AutoLoginState.IDLE -> {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 92be2019..bf731c1e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,8 +3,8 @@
minSdk = "28"
targetSdk = "35"
compileSdk = "35"
-versionName = "1.1.2"
-versionCode = "6"
+versionName = "1.2.0"
+versionCode = "7"
packageName = "com.ninecraft.booket"
## Android gradle plugin
@@ -12,7 +12,6 @@ android-gradle-plugin = "8.9.3"
## AndroidX
androidx-core = "1.16.0"
-androidx-lifecycle = "2.9.2"
androidx-activity-compose = "1.10.1"
androidx-startup = "1.2.0"
androidx-splash = "1.0.1"
@@ -33,7 +32,6 @@ ksp = "2.2.0-2.0.2"
kotlin = "2.2.0"
kotlinx-coroutines = "1.10.2"
kotlinx-serialization-json = "1.9.0"
-kotlinx-datetime = "0.7.1"
kotlinx-collections-immutable = "0.4.0"
## Hilt
@@ -60,9 +58,6 @@ landscapist = "2.5.1"
## Lottie
lottie = "6.6.6"
-## OCR
-mlkit-text = "16.0.1"
-
## Extension
# https://github.com/jisungbin/dependency-handler-extensions
gradle-dependency-handler-extensions = "1.1.0"
@@ -89,7 +84,6 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p
compose-compiler-gradle-plugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
-androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
androidx-splash = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-splash" }
androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" }
@@ -149,9 +143,6 @@ kakao-auth = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-c
lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" }
-google-mlkit-text-recognition = { group = "com.google.mlkit", name = "text-recognition", version.ref = "mlkit-text" }
-google-mlkit-text-recognition-korean = { group = "com.google.mlkit", name = "text-recognition-korean", version.ref = "mlkit-text" }
-
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" }