diff --git a/.github/ISSUE_TEMPLATE/qa-task.yml b/.github/ISSUE_TEMPLATE/qa-task.yml new file mode 100644 index 00000000..be5c5bda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/qa-task.yml @@ -0,0 +1,42 @@ +name: QA Task +description: QA용 이슈 템플릿입니다.(기존 JIRA의 이슈와 연동됩니다) +title: "fix] " +labels: ["🐞 fix", "qa"] +body: + - type: input + id: parentKey + attributes: + label: '🎟️ 작업 (Ticket Number)' + description: '연동할 작업의 Ticket Number를 기입해주세요' + placeholder: 'BOOK-00' + validations: + required: true + + - type: input + id: description + attributes: + label: "🐞 버그 설명" + description: "어떤 버그인지 명확히 작성해주세요" + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: "🔧 수정할 작업 목록" + description: "수정해야 할 항목들을 체크리스트로 작성해주세요" + value: | + - [ ] 버그 재현 + - [ ] 원인 분석 + - [ ] 수정 및 테스트 + validations: + required: true + + - type: input + id: links + attributes: + label: "🔗 참고 링크" + description: "관련 문서, 스크린샷, 로그 등이 있다면 첨부해주세요 (선택)" + placeholder: "https://..." + validations: + required: false diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 5a4e56c9..f3e8862f 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -21,11 +21,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup JDK 17 + - name: Setup JDK 21 uses: actions/setup-java@v4 with: distribution: 'corretto' - java-version: 17 + java-version: 21 - name: Setup Android SDK uses: android-actions/setup-android@v2 @@ -50,3 +50,39 @@ jobs: - name: Run build run: ./gradlew buildDebug --stacktrace + + stability_check: + name: Compose Stability Check + runs-on: ubuntu-latest + + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-ci') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: 21 + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-home-cache-cleanup: true + + - name: Generate local.properties + run: echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 -d > ./local.properties + + - name: Generate keystore.properties + run: echo '${{ secrets.KEYSTORE_PROPERTIES }}' | base64 -d > ./keystore.properties + + - name: Generate google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 -d > ./app/google-services.json + + - name: Compose Stability Check + run: ./gradlew stabilityCheck diff --git a/.github/workflows/close-jira-issue.yml b/.github/workflows/close-jira-issue.yml new file mode 100644 index 00000000..a72b0cb1 --- /dev/null +++ b/.github/workflows/close-jira-issue.yml @@ -0,0 +1,41 @@ +name: Close Jira issue + +on: + issues: + types: + - closed + +jobs: + close-issue: + runs-on: ubuntu-latest + + steps: + - name: Login to Jira + uses: atlassian/gajira-login@v3 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + + - name: Extract Jira issue key from GitHub issue title + id: extract-key + run: | + ISSUE_TITLE="${{ github.event.issue.title }}" + JIRA_KEY=$(echo "$ISSUE_TITLE" | grep -oE '[A-Z]+-[0-9]+' || true) + echo "JIRA_KEY=$JIRA_KEY" >> $GITHUB_ENV + + - name: Get available transitions + if: ${{ env.JIRA_KEY != '' }} + run: | + curl -u ${{ secrets.JIRA_USER_EMAIL }}:${{ secrets.JIRA_API_TOKEN }} \ + -X GET \ + -H "Content-Type: application/json" \ + "${{ secrets.JIRA_BASE_URL }}/rest/api/3/issue/${{ env.JIRA_KEY }}/transitions" \ + | jq '.transitions[] | {id, name, to: .to.name}' + + - name: Close Jira issue + if: ${{ env.JIRA_KEY != '' }} + uses: atlassian/gajira-transition@v3 + with: + issue: ${{ env.JIRA_KEY }} + transition: 개발 완료 diff --git a/.github/workflows/create-jira-issue.yml b/.github/workflows/create-jira-issue.yml index c878dcff..dd646175 100644 --- a/.github/workflows/create-jira-issue.yml +++ b/.github/workflows/create-jira-issue.yml @@ -1,154 +1,156 @@ name: Create Jira Issue on: - issues: - types: - - opened + issues: + types: + - opened jobs: - create-issue: - name: Create Jira issue - runs-on: ubuntu-latest - - steps: - - name: Determine Issue Type - id: type - run: | - if echo "${{ toJson(github.event.issue.labels) }}" | grep -q '✨ feat'; then - echo "type=feature" >> $GITHUB_OUTPUT - echo "template=feature-task.yml" >> $GITHUB_OUTPUT - elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '🐞 fix'; then - echo "type=fix" >> $GITHUB_OUTPUT - echo "template=fix-task.yml" >> $GITHUB_OUTPUT - elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '🔨 refactor'; then - echo "type=refactor" >> $GITHUB_OUTPUT - echo "template=refactor-task.yml" >> $GITHUB_OUTPUT - elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '📃 docs'; then - echo "type=docs" >> $GITHUB_OUTPUT - echo "template=docs-task.yml" >> $GITHUB_OUTPUT - elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '⚙️ chore'; then - echo "type=chore" >> $GITHUB_OUTPUT - echo "template=chore-task.yml" >> $GITHUB_OUTPUT - elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '✅ test'; then - echo "type=test" >> $GITHUB_OUTPUT - echo "template=test-task.yml" >> $GITHUB_OUTPUT - else - echo "type=feature" >> $GITHUB_OUTPUT - echo "template=feature-task.yml" >> $GITHUB_OUTPUT - fi - - - name: Clean Issue Title (for Jira Summary) - id: clean - run: | - raw="${{ github.event.issue.title }}" - # Remove prefix like 'feat]', 'fix]', 'chore]', etc. - clean_title=$(echo "$raw" | sed -E 's/^[a-z]+\]\s*//I') - echo "title=$clean_title" >> $GITHUB_OUTPUT - - - name: Jira Login - uses: atlassian/gajira-login@v3 - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - - - name: Checkout main code - uses: actions/checkout@v4 - with: - ref: develop - - - name: Parse Issue - uses: stefanbuck/github-issue-parser@v3 - id: issue-parser - with: - template-path: .github/ISSUE_TEMPLATE/${{ steps.type.outputs.template }} - - - name: Convert markdown to Jira Syntax - uses: peter-evans/jira2md@v1 - id: md2jira - with: - input-text: | - ### Github Issue Link - - ${{ github.event.issue.html_url }} - - ### 기능 설명 - ${{ steps.issue-parser.outputs.issueparser_description }} - - ### 작업 목록 - ${{ steps.issue-parser.outputs.issueparser_tasks }} - - ### 참고 링크 - ${{ steps.issue-parser.outputs.issueparser_links }} - mode: md2jira - - - name: Create Issue - id: create - uses: atlassian/gajira-create@v3 - with: - project: BOOK - issuetype: Task - summary: '${{ steps.clean.outputs.title }}' - description: '${{ steps.md2jira.outputs.output-text }}' - fields: | - { - "parent": { - "key": "${{ steps.issue-parser.outputs.issueparser_parentKey }}" - } - } - - - name: Checkout both branches - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Switch to develop - run: | - git fetch origin develop - git checkout develop - - - name: Generate Branch Name - id: branch - run: | - issue_number=${{ github.event.issue.number }} - issue_title="${{ github.event.issue.title }}" - slug=$(echo "$issue_title" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g' | sed 's/[^a-z0-9\-]//g') - ticket_key="${{ steps.create.outputs.issue }}" - branch_name="${ticket_key}-${{ steps.type.outputs.type }}/#${issue_number}" - echo "branch=${branch_name}" >> $GITHUB_OUTPUT - - - name: Create and push branch - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "${{ steps.branch.outputs.branch }}" - git push origin "${{ steps.branch.outputs.branch }}" - - - name: Update issue title - uses: actions-cool/issues-helper@v3 - with: - actions: 'update-issue' - token: ${{ secrets.PAT_TOKEN }} - title: '[${{ steps.create.outputs.issue }}/${{ github.event.issue.title }}' - - - name: Add comment with Jira issue link - uses: actions-cool/issues-helper@v3 - with: - actions: 'create-comment' - token: ${{ secrets.PAT_TOKEN }} - issue-number: ${{ github.event.issue.number }} - body: 'Jira Issue Created: [${{ steps.create.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create.outputs.issue }})' - - - name: Add comment with Branch Name - uses: actions-cool/issues-helper@v3 - with: - actions: 'create-comment' - token: ${{ secrets.PAT_TOKEN }} - issue-number: ${{ github.event.issue.number }} - body: '🔀 Branch Created: `${{ steps.branch.outputs.branch }}`' - - - name: Assign issue author - uses: actions-cool/issues-helper@v3 - with: - actions: 'add-assignees' - token: ${{ secrets.PAT_TOKEN }} - issue-number: ${{ github.event.issue.number }} - assignees: ${{ github.event.issue.user.login }} + create-issue: + # qa 라벨이 없을 때만 Jira issue 생성 + if: contains(github.event.issue.labels.*.name, 'qa') == false + name: Create Jira issue + runs-on: ubuntu-latest + + steps: + - name: Determine Issue Type + id: type + run: | + if echo "${{ toJson(github.event.issue.labels) }}" | grep -q '✨ feat'; then + echo "type=feature" >> $GITHUB_OUTPUT + echo "template=feature-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '🐞 fix'; then + echo "type=fix" >> $GITHUB_OUTPUT + echo "template=fix-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '🔨 refactor'; then + echo "type=refactor" >> $GITHUB_OUTPUT + echo "template=refactor-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '📃 docs'; then + echo "type=docs" >> $GITHUB_OUTPUT + echo "template=docs-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '⚙️ chore'; then + echo "type=chore" >> $GITHUB_OUTPUT + echo "template=chore-task.yml" >> $GITHUB_OUTPUT + elif echo "${{ toJson(github.event.issue.labels) }}" | grep -q '✅ test'; then + echo "type=test" >> $GITHUB_OUTPUT + echo "template=test-task.yml" >> $GITHUB_OUTPUT + else + echo "type=feature" >> $GITHUB_OUTPUT + echo "template=feature-task.yml" >> $GITHUB_OUTPUT + fi + + - name: Clean Issue Title (for Jira Summary) + id: clean + run: | + raw="${{ github.event.issue.title }}" + # Remove prefix like 'feat]', 'fix]', 'chore]', etc. + clean_title=$(echo "$raw" | sed -E 's/^[a-z]+\]\s*//I') + echo "title=$clean_title" >> $GITHUB_OUTPUT + + - name: Jira Login + uses: atlassian/gajira-login@v3 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + + - name: Checkout main code + uses: actions/checkout@v4 + with: + ref: develop + + - name: Parse Issue + uses: stefanbuck/github-issue-parser@v3 + id: issue-parser + with: + template-path: .github/ISSUE_TEMPLATE/${{ steps.type.outputs.template }} + + - name: Convert markdown to Jira Syntax + uses: peter-evans/jira2md@v1 + id: md2jira + with: + input-text: | + ### Github Issue Link + - ${{ github.event.issue.html_url }} + + ### 기능 설명 + ${{ steps.issue-parser.outputs.issueparser_description }} + + ### 작업 목록 + ${{ steps.issue-parser.outputs.issueparser_tasks }} + + ### 참고 링크 + ${{ steps.issue-parser.outputs.issueparser_links }} + mode: md2jira + + - name: Create Issue + id: create + uses: atlassian/gajira-create@v3 + with: + project: BOOK + issuetype: 하위 작업 + summary: '${{ steps.clean.outputs.title }}' + description: '${{ steps.md2jira.outputs.output-text }}' + fields: | + { + "parent": { + "key": "${{ steps.issue-parser.outputs.issueparser_parentKey }}" + } + } + + - name: Checkout both branches + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Switch to develop + run: | + git fetch origin develop + git checkout develop + + - name: Generate Branch Name + id: branch + run: | + issue_number=${{ github.event.issue.number }} + issue_title="${{ github.event.issue.title }}" + slug=$(echo "$issue_title" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g' | sed 's/[^a-z0-9\-]//g') + ticket_key="${{ steps.create.outputs.issue }}" + branch_name="${ticket_key}-${{ steps.type.outputs.type }}/#${issue_number}" + echo "branch=${branch_name}" >> $GITHUB_OUTPUT + + - name: Create and push branch + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "${{ steps.branch.outputs.branch }}" + git push origin "${{ steps.branch.outputs.branch }}" + + - name: Update issue title + uses: actions-cool/issues-helper@v3 + with: + actions: 'update-issue' + token: ${{ secrets.PAT_TOKEN }} + title: '[${{ steps.create.outputs.issue }}/${{ github.event.issue.title }}' + + - name: Add comment with Jira issue link + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: 'Jira Issue Created: [${{ steps.create.outputs.issue }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.create.outputs.issue }})' + + - name: Add comment with Branch Name + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: '🔀 Branch Created: `${{ steps.branch.outputs.branch }}`' + + - name: Assign issue author + uses: actions-cool/issues-helper@v3 + with: + actions: 'add-assignees' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + assignees: ${{ github.event.issue.user.login }} diff --git a/.github/workflows/link-existing-jira-issue.yml b/.github/workflows/link-existing-jira-issue.yml new file mode 100644 index 00000000..54a3b847 --- /dev/null +++ b/.github/workflows/link-existing-jira-issue.yml @@ -0,0 +1,98 @@ +name: Link Existing Jira Issue +on: + issues: + types: + - opened + +jobs: + link-jira: + # qa 라벨이 있을 때만 Jira 연동 로직 실행 + if: contains(github.event.issue.labels.*.name, 'qa') + name: Link Existing Jira Issue + runs-on: ubuntu-latest + + steps: + - name: Extract Jira Key + id: extract + run: | + title="${{ github.event.issue.title }}" + body="${{ github.event.issue.body }}" + jira_key=$(echo "$title" "$body" | grep -oE '([A-Z]+-[0-9]+)' | head -1) + + if [ -z "$jira_key" ]; then + echo "❌ Jira key not found in issue." + echo "jira_key=" >> $GITHUB_OUTPUT + else + echo "✅ Found Jira key: $jira_key" + echo "jira_key=$jira_key" >> $GITHUB_OUTPUT + fi + + - name: Stop if no Jira key + if: steps.extract.outputs.jira_key == '' + run: | + echo "No Jira key found. Exiting workflow." + exit 0 + + - name: Jira Login + uses: atlassian/gajira-login@v3 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + + - name: Checkout both branches + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Switch to develop + run: | + git fetch origin develop + git checkout develop + + - name: Generate Branch Name + id: branch + run: | + issue_number=${{ github.event.issue.number }} + jira_key="${{ steps.extract.outputs.jira_key }}" + branch_name="${jira_key}-fix/#${issue_number}" + echo "branch=${branch_name}" >> $GITHUB_OUTPUT + + - name: Create and push branch + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "${{ steps.branch.outputs.branch }}" + git push origin "${{ steps.branch.outputs.branch }}" + + - name: Update GitHub issue title + uses: actions-cool/issues-helper@v3 + with: + actions: 'update-issue' + token: ${{ secrets.PAT_TOKEN }} + title: '[${{ steps.extract.outputs.jira_key }}/${{ github.event.issue.title }}' + + - name: Add Jira link comment to GitHub issue + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + 🧩 Linked to Jira Issue: [${{ steps.extract.outputs.jira_key }}](${{ secrets.JIRA_BASE_URL }}/browse/${{ steps.extract.outputs.jira_key }}) + + - name: Add comment with Branch Name + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: '🔀 Branch Created: `${{ steps.branch.outputs.branch }}`' + + - name: Assign issue author + uses: actions-cool/issues-helper@v3 + with: + actions: 'add-assignees' + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + assignees: ${{ github.event.issue.user.login }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 49baa4fb..3790c539 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,6 +59,10 @@ android { } } +composeStabilityAnalyzer { + enabled.set(true) +} + ksp { arg("circuit.codegen.mode", "hilt") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a93c3d6..4cc12899 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + + + + + + + + + + + + + - diff --git a/app/src/main/kotlin/com/ninecraft/booket/ReedFirebaseMessagingService.kt b/app/src/main/kotlin/com/ninecraft/booket/ReedFirebaseMessagingService.kt new file mode 100644 index 00000000..5efc4928 --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/ReedFirebaseMessagingService.kt @@ -0,0 +1,93 @@ +package com.ninecraft.booket + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.core.designsystem.R +import com.ninecraft.booket.feature.main.MainActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class ReedFirebaseMessagingService : FirebaseMessagingService() { + + @Inject + lateinit var userRepository: UserRepository + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override fun onNewToken(token: String) { + super.onNewToken(token) + + scope.launch { + userRepository.syncFcmToken(token) + } + } + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + + val title = message.notification?.title ?: "Reed" + val body = message.notification?.body ?: "" + + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + val builder = NotificationCompat.Builder(this, REED_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setColor(ContextCompat.getColor(this, R.color.green_500)) + .setContentTitle(title) + .setContentText(body) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + manager.notify(System.currentTimeMillis().toInt(), builder.build()) + } + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } + + companion object { + private const val REED_CHANNEL_ID = "REED_PUSH_CHANNEL" + private const val REED_CHANNEL_NAME = "리드 푸시 알림" + private const val REED_CHANNEL_DESC = "리드 앱에서 보내는 푸시 알림을 관리합니다." + + // Android 8.0 이상 필수 채널 생성 + fun createNotificationChannel(context: Context) { + val channel = NotificationChannel( + REED_CHANNEL_ID, + REED_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT, + ).apply { + description = REED_CHANNEL_DESC + } + + val manager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.createNotificationChannel(channel) + } + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt b/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt index 87dc4e6f..23df4723 100644 --- a/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt +++ b/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt @@ -28,7 +28,7 @@ abstract class CircuitModule { ): Circuit = Circuit.Builder() .addPresenterFactories(presenterFactories) .addUiFactories(uiFactories) - // .setAnimatedNavDecoratorFactory(CrossFadeNavDecoratorFactory()) + .setAnimatedNavDecoratorFactory(CrossFadeNavDecoratorFactory()) .build() } } diff --git a/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt b/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt index a79f90f6..a782cd7c 100644 --- a/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt +++ b/app/src/main/kotlin/com/ninecraft/booket/di/CrossFadeNavDecorator.kt @@ -6,13 +6,16 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import com.ninecraft.booket.feature.screens.LoginScreen +import com.ninecraft.booket.feature.screens.SplashScreen +import com.ninecraft.booket.feature.screens.TermsAgreementScreen import com.slack.circuit.backstack.NavArgument import com.slack.circuit.foundation.NavigatorDefaults import com.slack.circuit.foundation.animation.AnimatedNavDecorator import com.slack.circuit.foundation.animation.AnimatedNavEvent import com.slack.circuit.foundation.animation.AnimatedNavState -data class CrossFadeNavDecoratorFactory(val durationMillis: Int = 300) : +data class CrossFadeNavDecoratorFactory(val durationMillis: Int = 500) : AnimatedNavDecorator.Factory { override fun create(): AnimatedNavDecorator = CrossFadeNavDecorator(durationMillis) @@ -24,6 +27,34 @@ class CrossFadeNavDecorator(private val durationMillis: Int) : override fun AnimatedContentTransitionScope.transitionSpec( animatedNavEvent: AnimatedNavEvent, ): ContentTransform { - return fadeIn(tween(durationMillis)) togetherWith fadeOut(tween(durationMillis)) + val shouldUseFade = shouldUseFadeAnimation(initialState, targetState) + + return if (shouldUseFade) { + fadeIn(tween(durationMillis)) togetherWith fadeOut(tween(durationMillis)) + } else { + // Circuit 기본 애니메이션 사용 + with(NavigatorDefaults.DefaultDecorator()) { + transitionSpec(animatedNavEvent) + } + } + } + + private fun shouldUseFadeAnimation( + initialState: AnimatedNavState, + targetState: AnimatedNavState, + ): Boolean { + val fadeScreens = setOf( + SplashScreen::class, + LoginScreen::class, + TermsAgreementScreen::class, + ) + + val initialScreenClass = initialState.top.screen::class + val targetScreenClass = targetState.top.screen::class + + // 앱 시작시 SplashScreen이 첫 화면인 경우 처리 + val isAppLaunchToSplash = targetScreenClass == SplashScreen::class + + return isAppLaunchToSplash || initialScreenClass in fadeScreens || targetScreenClass in fadeScreens } } diff --git a/app/src/main/kotlin/com/ninecraft/booket/initializer/ComposeStabilityAnalyzerInitializer.kt b/app/src/main/kotlin/com/ninecraft/booket/initializer/ComposeStabilityAnalyzerInitializer.kt new file mode 100644 index 00000000..527905f8 --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/initializer/ComposeStabilityAnalyzerInitializer.kt @@ -0,0 +1,16 @@ +package com.ninecraft.booket.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.ninecraft.booket.BuildConfig +import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer + +class ComposeStabilityAnalyzerInitializer : Initializer { + override fun create(context: Context) { + ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG) + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/kotlin/com/ninecraft/booket/initializer/NotificationChannelInitializer.kt b/app/src/main/kotlin/com/ninecraft/booket/initializer/NotificationChannelInitializer.kt new file mode 100644 index 00000000..60aee8a8 --- /dev/null +++ b/app/src/main/kotlin/com/ninecraft/booket/initializer/NotificationChannelInitializer.kt @@ -0,0 +1,16 @@ +package com.ninecraft.booket.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.ninecraft.booket.ReedFirebaseMessagingService.Companion.createNotificationChannel + +class NotificationChannelInitializer : Initializer { + + override fun create(context: Context) { + createNotificationChannel(context) + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/stability/app.stability b/app/stability/app.stability new file mode 100644 index 00000000..dc1f99ee --- /dev/null +++ b/app/stability/app.stability @@ -0,0 +1,21 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :app:stabilityDump + +@Composable +public fun com.ninecraft.booket.di.CrossFadeNavDecorator.Decoration(targetState: com.slack.circuit.foundation.NavigatorDefaults.DefaultDecorator.DefaultAnimatedState, innerContent: @[Composable] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit + skippable: false + restartable: true + params: + - targetState: RUNTIME (requires runtime check) + - innerContent: STABLE (composable function type) + +@Composable +public fun com.ninecraft.booket.di.CrossFadeNavDecorator.updateTransition(args: kotlinx.collections.immutable.ImmutableList): androidx.compose.animation.core.Transition> + skippable: true + restartable: true + params: + - args: STABLE (known stable type) + diff --git a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index 310243b4..7c90de29 100644 --- a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -12,6 +12,7 @@ internal class AndroidApplicationComposeConventionPlugin : Plugin { applyPlugins( Plugins.ANDROID_APPLICATION, Plugins.KOTLIN_COMPOSE, + Plugins.COMPOSE_STABILITY_ANALYZER, ) extensions.configure { diff --git a/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt index fa0b0972..dc9423db 100644 --- a/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt @@ -19,6 +19,7 @@ internal class AndroidFirebaseConventionPlugin : Plugin { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) + implementation(libs.firebase.messaging) } } } diff --git a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 8096e831..2a912fcf 100644 --- a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -12,6 +12,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { applyPlugins( Plugins.ANDROID_LIBRARY, Plugins.KOTLIN_COMPOSE, + Plugins.COMPOSE_STABILITY_ANALYZER, ) extensions.configure { diff --git a/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Plugins.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Plugins.kt index e525f7b6..e8dc9812 100644 --- a/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Plugins.kt +++ b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Plugins.kt @@ -11,6 +11,8 @@ object Plugins { const val ANDROID_APPLICATION = "com.android.application" const val ANDROID_LIBRARY = "com.android.library" + const val COMPOSE_STABILITY_ANALYZER = "com.github.skydoves.compose.stability.analyzer" + const val HILT = "dagger.hilt.android.plugin" const val KSP = "com.google.devtools.ksp" const val GOOGLE_SERVICES = "com.google.gms.google-services" diff --git a/build.gradle.kts b/build.gradle.kts index 63b471a7..6d346c9a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.gradle.dependency.handler.extensions) alias(libs.plugins.kotlin.detekt) alias(libs.plugins.kotlin.ktlint) + alias(libs.plugins.compose.stability.analyzer) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorDialogSpec.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorDialogSpec.kt deleted file mode 100644 index 270ea48e..00000000 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorDialogSpec.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.ninecraft.booket.core.common.constants - -import androidx.annotation.StringRes - -data class ErrorDialogSpec( - val message: String, - @StringRes val buttonLabelResId: Int, - val action: () -> Unit, -) diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt index 9fa2c258..bae8788c 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt @@ -1,5 +1,5 @@ package com.ninecraft.booket.core.common.constants enum class ErrorScope { - GLOBAL, LOGIN, BOOK_REGISTER, RECORD_REGISTER + GLOBAL, LOGIN, AUTH_SESSION_EXPIRED, } diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/DialogEvents.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/DialogEvents.kt new file mode 100644 index 00000000..b19347be --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/DialogEvents.kt @@ -0,0 +1,58 @@ +package com.ninecraft.booket.core.common.event + +import com.ninecraft.booket.core.common.constants.ErrorScope +import com.ninecraft.booket.core.common.utils.isNetworkError +import retrofit2.HttpException + +fun postErrorDialog( + errorScope: ErrorScope, + exception: Throwable, + confirmLabel: String = "확인", + onConfirm: () -> Unit = {}, +) { + val (title, message) = when { + exception.isNetworkError() -> { + null to "네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요" + } + + exception is HttpException -> { + when (errorScope) { + ErrorScope.GLOBAL -> { + null to "알 수 없는 문제가 발생했어요.\n다시 시도해주세요" + } + + ErrorScope.LOGIN -> { + "로그인 오류" to "예기치 않은 오류가 발생했습니다.\n다시 로그인 해주세요." + } + + ErrorScope.AUTH_SESSION_EXPIRED -> { + null to "세션이 만료되었어요.\n다시 로그인 해주세요" + } + } + } + + else -> { + null to "알 수 없는 문제가 발생했어요.\n다시 시도해주세요" + } + } + + val spec = DialogSpec( + title = title, + message = message, + confirmLabel = confirmLabel, + onConfirm = onConfirm, + ) + + EventHelper.sendEvent(event = ReedEvent.ShowDialog(spec)) +} + +fun postLoginRequiredDialog(onConfirm: () -> Unit) { + val spec = DialogSpec( + message = "로그인이 필요한 기능입니다.\n로그인 해주세요.", + confirmLabel = "로그인 하기", + onConfirm = onConfirm, + dismissLabel = "닫기", + ) + + EventHelper.sendEvent(event = ReedEvent.ShowDialog(spec)) +} diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/DialogSpec.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/DialogSpec.kt new file mode 100644 index 00000000..db1866f1 --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/DialogSpec.kt @@ -0,0 +1,10 @@ +package com.ninecraft.booket.core.common.event + +data class DialogSpec( + val message: String, + val confirmLabel: String, + val onConfirm: () -> Unit, + val title: String? = null, + val dismissLabel: String? = null, + val onDismissRequest: () -> Unit = {}, +) diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt deleted file mode 100644 index 2daee82e..00000000 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.ninecraft.booket.core.common.event - -import com.ninecraft.booket.core.common.constants.ErrorDialogSpec -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow -import java.util.UUID - -object ErrorEventHelper { - private val _errorEvent = Channel(Channel.BUFFERED) - val errorEvent = _errorEvent.receiveAsFlow() - - fun sendError(event: ErrorEvent) { - _errorEvent.trySend(event) - } -} - -sealed interface ErrorEvent { - data class ShowDialog( - val spec: ErrorDialogSpec, - val key: String = UUID.randomUUID().toString(), - ) : ErrorEvent -} diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/EventHelper.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/EventHelper.kt new file mode 100644 index 00000000..3ba55163 --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/EventHelper.kt @@ -0,0 +1,17 @@ +package com.ninecraft.booket.core.common.event + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +object EventHelper { + private val _eventFlow = Channel(Channel.BUFFERED) + val eventFlow = _eventFlow.receiveAsFlow() + + fun sendEvent(event: ReedEvent) { + _eventFlow.trySend(event) + } +} + +sealed interface ReedEvent { + data class ShowDialog(val spec: DialogSpec) : ReedEvent +} diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Context.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Context.kt index d1fa560b..17b29ffa 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Context.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Context.kt @@ -16,7 +16,6 @@ import androidx.core.content.FileProvider import com.orhanobut.logger.Logger import java.io.File -@Suppress("TooGenericExceptionCaught") fun Context.externalShareForBitmap(bitmap: ImageBitmap) { try { val file = File(bitmap.saveToDisk(this)) @@ -31,7 +30,6 @@ fun Context.externalShareForBitmap(bitmap: ImageBitmap) { } } -@Suppress("TooGenericExceptionCaught") fun Context.saveImageToGallery(bitmap: ImageBitmap) { try { val fileName = "reed_record_${System.currentTimeMillis()}.png" diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt deleted file mode 100644 index 653a2bf8..00000000 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Emotion.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.ninecraft.booket.core.common.extensions - -import androidx.compose.ui.graphics.Color -import com.ninecraft.booket.core.model.Emotion - -fun Emotion.toTextColor(): Color { - return when (this) { - Emotion.WARM -> Color(0xFFE3931B) - Emotion.JOY -> Color(0xFFEE6B33) - Emotion.SAD -> Color(0xFF9A55E4) - Emotion.INSIGHT -> Color(0xFF2872E9) - } -} - -fun Emotion.toBackgroundColor(): Color { - return when (this) { - Emotion.WARM -> Color(0xFFFFF5D3) - Emotion.JOY -> Color(0xFFFFEBE3) - Emotion.SAD -> Color(0xFFF3E8FF) - Emotion.INSIGHT -> Color(0xFFE1ECFF) - } -} diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Throwable.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Throwable.kt new file mode 100644 index 00000000..2b0014a8 --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Throwable.kt @@ -0,0 +1,12 @@ +package com.ninecraft.booket.core.common.extensions + +import com.ninecraft.booket.core.common.utils.ErrorType +import com.ninecraft.booket.core.common.utils.isNetworkError + +fun Throwable.toErrorType(): ErrorType { + return if (this.isNetworkError()) { + ErrorType.NetworkError + } else { + ErrorType.ServerError + } +} diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/EmotionAnalyzer.kt similarity index 95% rename from core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt rename to core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/EmotionAnalyzer.kt index 5844fd66..06be275c 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/EmotionAnalyzer.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/EmotionAnalyzer.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.core.common.util +package com.ninecraft.booket.core.common.utils import com.ninecraft.booket.core.model.EmotionModel diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/ErrorType.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/ErrorType.kt new file mode 100644 index 00000000..7775656d --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/ErrorType.kt @@ -0,0 +1,9 @@ +package com.ninecraft.booket.core.common.utils + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface ErrorType { + data object NetworkError : ErrorType + data object ServerError : ErrorType +} diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt index ed84d6e8..9e43d3b4 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt @@ -1,11 +1,7 @@ package com.ninecraft.booket.core.common.utils -import androidx.annotation.StringRes -import com.ninecraft.booket.core.common.R -import com.ninecraft.booket.core.common.constants.ErrorDialogSpec import com.ninecraft.booket.core.common.constants.ErrorScope -import com.ninecraft.booket.core.common.event.ErrorEvent -import com.ninecraft.booket.core.common.event.ErrorEventHelper +import com.ninecraft.booket.core.common.event.postErrorDialog import com.ninecraft.booket.core.network.response.ErrorResponse import com.orhanobut.logger.Logger import kotlinx.serialization.SerializationException @@ -23,7 +19,13 @@ fun handleException( ) { when { exception is HttpException && exception.code() == 401 -> { - onLoginRequired() + postErrorDialog( + errorScope = ErrorScope.AUTH_SESSION_EXPIRED, + exception = exception, + onConfirm = { + onLoginRequired() + }, + ) } exception is HttpException -> { @@ -45,62 +47,6 @@ fun handleException( } } -fun postErrorDialog( - errorScope: ErrorScope, - exception: Throwable, - @StringRes buttonLabelResId: Int = R.string.confirm, - action: () -> Unit = {}, -) { - val spec = buildDialog( - scope = errorScope, - exception = exception, - buttonLabelResId = buttonLabelResId, - action = action, - ) - - ErrorEventHelper.sendError(event = ErrorEvent.ShowDialog(spec)) -} - -private fun buildDialog( - scope: ErrorScope, - exception: Throwable, - @StringRes buttonLabelResId: Int, - action: () -> Unit, -): ErrorDialogSpec { - val message = when { - exception.isNetworkError() -> { - "네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요" - } - - exception is HttpException -> { - when (scope) { - ErrorScope.GLOBAL -> { - "알 수 없는 문제가 발생했어요.\n다시 시도해주세요" - } - - ErrorScope.LOGIN -> { - "예기치 않은 오류가 발생했습니다.\n다시 로그인 해주세요." - } - - ErrorScope.BOOK_REGISTER -> { - "도서 등록 중 오류가 발생했어요.\n다시 시도해주세요" - } - - ErrorScope.RECORD_REGISTER -> { - "기록 저장에 실패했어요.\n다시 시도해주세요" - } - } - } - - else -> { - "알 수 없는 문제가 발생했어요.\n다시 시도해주세요" - } - } - - return ErrorDialogSpec(message = message, buttonLabelResId = buttonLabelResId, action = action) -} - -@Suppress("TooGenericExceptionCaught") private fun HttpException.parseErrorMessage(): String? { return try { val errorBody = response()?.errorBody()?.string() diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/NotificationSyncUtils.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/NotificationSyncUtils.kt new file mode 100644 index 00000000..213be15e --- /dev/null +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/NotificationSyncUtils.kt @@ -0,0 +1,4 @@ +package com.ninecraft.booket.core.common.utils + +fun shouldSyncNotification(effectiveEnabled: Boolean, lastSynced: Boolean?): Boolean = + lastSynced == null || lastSynced != effectiveEnabled diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt index b0152b27..59853c23 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/RunSuspendCatching.kt @@ -14,7 +14,7 @@ import kotlin.contracts.ExperimentalContracts */ @OptIn(ExperimentalContracts::class) -@Suppress("WRONG_INVOCATION_KIND", "TooGenericExceptionCaught") +@Suppress("WRONG_INVOCATION_KIND") inline fun runSuspendCatching(block: () -> T): Result { // 계약(contract): 컴파일러에게 'block' 람다의 실행 시점과 횟수를 명시적으로 알림 // 'callsInPlace'와 'EXACTLY_ONCE'는 'block'이 이 함수 내에서 즉시, 그리고 정확히 한 번만 실행됨을 보장 diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/VersionUtils.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/VersionUtils.kt similarity index 96% rename from core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/VersionUtils.kt rename to core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/VersionUtils.kt index c91a6811..5a9a9329 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/util/VersionUtils.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/VersionUtils.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.core.common.util +package com.ninecraft.booket.core.common.utils import com.orhanobut.logger.Logger diff --git a/core/common/stability/common.stability b/core/common/stability/common.stability new file mode 100644 index 00000000..6de75f09 --- /dev/null +++ b/core/common/stability/common.stability @@ -0,0 +1,33 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :common:stabilityDump + +@Composable +public fun com.ninecraft.booket.core.common.utils.HighlightedText(fullText: kotlin.String, highlightText: kotlin.String, highlightColor: androidx.compose.ui.graphics.Color): androidx.compose.ui.text.AnnotatedString + skippable: true + restartable: true + params: + - fullText: STABLE (String is immutable) + - highlightText: STABLE (String is immutable) + - highlightColor: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.core.common.utils.UiText.DirectString.asString(): kotlin.String + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.common.utils.UiText.StringResource.asString(): kotlin.String + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.common.utils.UiText.asString(): kotlin.String + skippable: true + restartable: true + params: + diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt index 049c19b3..20618cc1 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/UserRepository.kt @@ -13,4 +13,22 @@ interface UserRepository { val onboardingState: Flow suspend fun setOnboardingCompleted(isCompleted: Boolean) + + suspend fun syncFcmToken(): Result + + suspend fun syncFcmToken(fcmToken: String): Result + + val isUserNotificationEnabled: Flow + + suspend fun getUserNotificationEnabled(): Boolean + + suspend fun setUserNotificationEnabled(isEnabled: Boolean) + + suspend fun getLastSyncedNotificationEnabled(): Boolean? + + suspend fun setLastNotificationSyncedEnabled(isEnabled: Boolean) + + suspend fun updateNotificationSettings(notificationEnabled: Boolean): Result + + suspend fun resetNotificationData() } diff --git a/core/data/impl/build.gradle.kts b/core/data/impl/build.gradle.kts index f894b8ed..c24e3f2e 100644 --- a/core/data/impl/build.gradle.kts +++ b/core/data/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { platform(libs.firebase.bom), libs.firebase.remote.config, + libs.firebase.messaging, libs.logger, ) } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt index 2a6f28a5..9c5f5f65 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/FirebaseModule.kt @@ -1,6 +1,10 @@ package com.ninecraft.booket.core.data.impl.di import com.google.firebase.Firebase +import com.google.firebase.installations.FirebaseInstallations +import com.google.firebase.installations.installations +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.messaging import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.remoteConfig import com.google.firebase.remoteconfig.remoteConfigSettings @@ -26,4 +30,12 @@ internal object FirebaseModule { setConfigSettingsAsync(configSettings) } } + + @Singleton + @Provides + fun provideFirebaseMessaging(): FirebaseMessaging = Firebase.messaging + + @Singleton + @Provides + fun provideFirebaseInstallation(): FirebaseInstallations = Firebase.installations } 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 ba0e0e96..66a1c893 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 @@ -49,6 +49,7 @@ internal fun UserProfileResponse.toModel(): UserProfileModel { nickname = nickname, provider = provider, termsAgreed = termsAgreed, + notificationEnabled = notificationEnabled, ) } @@ -193,7 +194,7 @@ internal fun RecordRegisterResponse.toModel(): RecordRegisterModel { pageNumber = pageNumber, quote = quote, emotionTags = emotionTags, - review = review, + review = review ?: "", createdAt = createdAt, updatedAt = updatedAt, ) @@ -215,7 +216,7 @@ internal fun ReadingRecord.toModel(): ReadingRecordModel { userBookId = userBookId, pageNumber = pageNumber, quote = quote, - review = review, + review = review ?: "", emotionTags = emotionTags, createdAt = createdAt, updatedAt = updatedAt, @@ -232,7 +233,7 @@ internal fun RecordDetailResponse.toModel(): RecordDetailModel { userBookId = userBookId, pageNumber = pageNumber, quote = quote, - review = review, + review = review ?: "", emotionTags = emotionTags, createdAt = createdAt.toFormattedDate(), updatedAt = updatedAt.toFormattedDate(), diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt index ea9733f6..7f47a519 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRemoteConfigRepository.kt @@ -2,41 +2,33 @@ package com.ninecraft.booket.core.data.impl.repository import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.get -import com.ninecraft.booket.core.common.util.isUpdateRequired +import com.ninecraft.booket.core.common.utils.isUpdateRequired +import com.ninecraft.booket.core.common.utils.runSuspendCatching import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository import com.ninecraft.booket.core.data.impl.BuildConfig import com.orhanobut.logger.Logger -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.tasks.await import javax.inject.Inject -import kotlin.coroutines.resume class DefaultRemoteConfigRepository @Inject constructor( private val remoteConfig: FirebaseRemoteConfig, ) : RemoteConfigRepository { - override suspend fun getLatestVersion(): Result = suspendCancellableCoroutine { continuation -> - remoteConfig.fetchAndActivate().addOnCompleteListener { task -> - if (task.isSuccessful) { - val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString() - Logger.d("LatestVersion: $latestVersion") - continuation.resume(Result.success(latestVersion)) - } else { - Logger.e(task.exception, "getLatestVersion failed") - continuation.resume(Result.failure(task.exception ?: Exception("Unknown error"))) - } - } + override suspend fun getLatestVersion(): Result = runSuspendCatching { + remoteConfig.fetchAndActivate().await() + val latestVersion = remoteConfig[KEY_LATEST_VERSION].asString() + Logger.d("LatestVersion: $latestVersion") + latestVersion + }.onFailure { exception -> + Logger.e(exception, "getLatestVersion failed") } - override suspend fun shouldUpdate(): Result = suspendCancellableCoroutine { continuation -> - remoteConfig.fetchAndActivate().addOnCompleteListener { task -> - if (task.isSuccessful) { - val minVersion = remoteConfig[KEY_MIN_VERSION].asString() - val currentVersion = BuildConfig.APP_VERSION - continuation.resume(Result.success(isUpdateRequired(currentVersion, minVersion))) - } else { - Logger.e(task.exception, "shouldUpdate: getMinVersion failed") - continuation.resume(Result.failure(task.exception ?: Exception("Unknown error"))) - } - } + override suspend fun shouldUpdate(): Result = runSuspendCatching { + remoteConfig.fetchAndActivate().await() + val minVersion = remoteConfig[KEY_MIN_VERSION].asString() + val currentVersion = BuildConfig.APP_VERSION + isUpdateRequired(currentVersion, minVersion) + }.onFailure { exception -> + Logger.e(exception, "shouldUpdate: getMinVersion failed") } companion object { diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt index f9cf7741..22954832 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultUserRepository.kt @@ -1,16 +1,28 @@ package com.ninecraft.booket.core.data.impl.repository +import com.google.firebase.installations.FirebaseInstallations +import com.google.firebase.messaging.FirebaseMessaging import com.ninecraft.booket.core.common.utils.runSuspendCatching import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.core.data.impl.mapper.toModel +import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource +import com.ninecraft.booket.core.network.request.DeviceRegistrationRequest +import com.ninecraft.booket.core.network.request.NotificationSettingsRequest import com.ninecraft.booket.core.network.request.TermsAgreementRequest import com.ninecraft.booket.core.network.service.ReedService +import com.orhanobut.logger.Logger +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.tasks.await import javax.inject.Inject internal class DefaultUserRepository @Inject constructor( private val service: ReedService, private val onboardingDataSource: OnboardingDataSource, + private val notificationDataSource: NotificationDataSource, + private val firebaseMessaging: FirebaseMessaging, + private val firebaseInstallations: FirebaseInstallations, ) : UserRepository { override suspend fun agreeTerms(termsAgreed: Boolean) = runSuspendCatching { service.agreeTerms(TermsAgreementRequest(termsAgreed)).toModel() @@ -25,4 +37,72 @@ internal class DefaultUserRepository @Inject constructor( override suspend fun setOnboardingCompleted(isCompleted: Boolean) { onboardingDataSource.setOnboardingCompleted(isCompleted) } + + override suspend fun syncFcmToken() = runSuspendCatching { + val newToken = getRemoteFcmToken() + registerDevice(newToken) + } + + override suspend fun syncFcmToken(fcmToken: String): Result = runSuspendCatching { + registerDevice(fcmToken) + } + + override val isUserNotificationEnabled = notificationDataSource.isUserNotificationEnabled + + override suspend fun getUserNotificationEnabled(): Boolean = isUserNotificationEnabled.first() + + override suspend fun setUserNotificationEnabled(isEnabled: Boolean) { + notificationDataSource.setUserNotificationEnabled(isEnabled) + } + + override suspend fun getLastSyncedNotificationEnabled(): Boolean? = + notificationDataSource.lastSyncedNotificationEnabled.firstOrNull() + + override suspend fun setLastNotificationSyncedEnabled(isEnabled: Boolean) { + notificationDataSource.setLastSyncedNotificationEnabled(isEnabled) + } + + override suspend fun updateNotificationSettings(notificationEnabled: Boolean) = runSuspendCatching { + service.updateNotificationSettings(NotificationSettingsRequest(notificationEnabled)).toModel() + } + + override suspend fun resetNotificationData() { + try { + deleteRemoteFcmToken() + clearNotificationDataStore() + } catch (e: Exception) { + Logger.e("Failed to reset notification data: ${e.message}") + } + } + + private suspend fun getRemoteFcmToken(): String { + return try { + firebaseMessaging.token.await() + } catch (e: Exception) { + Logger.e("Failed to fetch FCM token: ${e.message}") + throw e + } + } + + private suspend fun getDeviceId(): String { + return try { + firebaseInstallations.id.await() + } catch (e: Exception) { + Logger.e("Failed to fetch device ID: ${e.message}") + throw e + } + } + + private suspend fun registerDevice(fcmToken: String) { + val deviceId = getDeviceId() + service.upsertDevice(DeviceRegistrationRequest(deviceId, fcmToken)) + } + + private suspend fun deleteRemoteFcmToken() { + firebaseMessaging.deleteToken().await() + } + + private suspend fun clearNotificationDataStore() { + notificationDataSource.clearNotificationDataStore() + } } diff --git a/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt b/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt new file mode 100644 index 00000000..1298ec07 --- /dev/null +++ b/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/NotificationDataSource.kt @@ -0,0 +1,13 @@ +package com.ninecraft.booket.core.datastore.api.datasource + +import kotlinx.coroutines.flow.Flow + +interface NotificationDataSource { + val isUserNotificationEnabled: Flow + suspend fun setUserNotificationEnabled(isEnabled: Boolean) + + val lastSyncedNotificationEnabled: Flow + suspend fun setLastSyncedNotificationEnabled(isEnabled: Boolean) + + suspend fun clearNotificationDataStore() +} diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultBookRecentSearchDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultBookRecentSearchDataSource.kt index 2694b2a9..457ea401 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultBookRecentSearchDataSource.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultBookRecentSearchDataSource.kt @@ -17,7 +17,6 @@ import javax.inject.Inject class DefaultBookRecentSearchDataSource @Inject constructor( @BookRecentSearchDataStore private val dataStore: DataStore, ) : BookRecentSearchDataSource { - @Suppress("TooGenericExceptionCaught") override val recentSearches: Flow> = dataStore.data .handleIOException() .map { prefs -> @@ -34,7 +33,6 @@ class DefaultBookRecentSearchDataSource @Inject constructor( } ?: emptyList() } - @Suppress("TooGenericExceptionCaught") override suspend fun addRecentSearch(query: String) { if (query.isBlank()) return @@ -66,7 +64,6 @@ class DefaultBookRecentSearchDataSource @Inject constructor( } } - @Suppress("TooGenericExceptionCaught") override suspend fun deleteRecentSearch(query: String) { dataStore.edit { prefs -> val currentSearches = prefs[BOOK_RECENT_SEARCHES]?.let { jsonString -> diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLibraryRecentSearchDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLibraryRecentSearchDataSource.kt index b666c888..97cd15d7 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLibraryRecentSearchDataSource.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLibraryRecentSearchDataSource.kt @@ -17,7 +17,6 @@ import javax.inject.Inject class DefaultLibraryRecentSearchDataSource @Inject constructor( @LibraryRecentSearchDataStore private val dataStore: DataStore, ) : LibraryRecentSearchDataSource { - @Suppress("TooGenericExceptionCaught") override val recentSearches: Flow> = dataStore.data .handleIOException() .map { prefs -> @@ -34,7 +33,6 @@ class DefaultLibraryRecentSearchDataSource @Inject constructor( } ?: emptyList() } - @Suppress("TooGenericExceptionCaught") override suspend fun addRecentSearch(query: String) { if (query.isBlank()) return @@ -66,7 +64,6 @@ class DefaultLibraryRecentSearchDataSource @Inject constructor( } } - @Suppress("TooGenericExceptionCaught") override suspend fun deleteRecentSearch(query: String) { dataStore.edit { prefs -> val currentSearches = prefs[LIBRARY_RECENT_SEARCHES]?.let { jsonString -> diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt new file mode 100644 index 00000000..22c6d3b4 --- /dev/null +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt @@ -0,0 +1,51 @@ +package com.ninecraft.booket.core.datastore.impl.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource +import com.ninecraft.booket.core.datastore.impl.di.NotificationDataStore +import com.ninecraft.booket.core.datastore.impl.util.handleIOException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DefaultNotificationDataSource @Inject constructor( + @NotificationDataStore private val dataStore: DataStore, +) : NotificationDataSource { + override val isUserNotificationEnabled: Flow = dataStore.data + .handleIOException() + .map { prefs -> + prefs[USER_NOTIFICATION_ENABLED] ?: true + } + + override suspend fun setUserNotificationEnabled(isEnabled: Boolean) { + dataStore.edit { prefs -> + prefs[USER_NOTIFICATION_ENABLED] = isEnabled + } + } + + override val lastSyncedNotificationEnabled: Flow = dataStore.data + .handleIOException() + .map { prefs -> + prefs[LAST_SYNCED_NOTIFICATION_ENABLED] + } + + override suspend fun setLastSyncedNotificationEnabled(isEnabled: Boolean) { + dataStore.edit { prefs -> + prefs[LAST_SYNCED_NOTIFICATION_ENABLED] = isEnabled + } + } + + override suspend fun clearNotificationDataStore() { + dataStore.edit { prefs -> + prefs.clear() + } + } + + companion object Companion { + private val USER_NOTIFICATION_ENABLED = booleanPreferencesKey("USER_NOTIFICATION_ENABLED") + private val LAST_SYNCED_NOTIFICATION_ENABLED = booleanPreferencesKey("LAST_SYNCED_NOTIFICATION_ENABLED") + } +} diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt index 08e11837..88a2c3cc 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreModule.kt @@ -6,11 +6,13 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource +import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultLibraryRecentSearchDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultOnboardingDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultBookRecentSearchDataSource +import com.ninecraft.booket.core.datastore.impl.datasource.DefaultNotificationDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultTokenDataSource import dagger.Binds import dagger.Module @@ -27,11 +29,13 @@ object DataStoreModule { private const val BOOK_RECENT_SEARCH_DATASTORE_NAME = "BOOK_RECENT_SEARCH_DATASTORE" private const val LIBRARY_RECENT_SEARCH_DATASTORE_NAME = "LIBRARY_RECENT_SEARCH_DATASTORE" private const val ONBOARDING_DATASTORE_NAME = "ONBOARDING_DATASTORE" + private const val NOTIFICATION_DATASTORE_NAME = "NOTIFICATION_DATASTORE" private val Context.tokenDataStore by preferencesDataStore(name = TOKEN_DATASTORE_NAME) private val Context.bookRecentSearchDataStore by preferencesDataStore(name = BOOK_RECENT_SEARCH_DATASTORE_NAME) private val Context.libraryRecentSearchDataStore by preferencesDataStore(name = LIBRARY_RECENT_SEARCH_DATASTORE_NAME) private val Context.onboardingDataStore by preferencesDataStore(name = ONBOARDING_DATASTORE_NAME) + private val Context.notificationDataStore by preferencesDataStore(name = NOTIFICATION_DATASTORE_NAME) @TokenDataStore @Provides @@ -60,6 +64,13 @@ object DataStoreModule { fun provideOnboardingDataStore( @ApplicationContext context: Context, ): DataStore = context.onboardingDataStore + + @NotificationDataStore + @Provides + @Singleton + fun provideNotificationDataStore( + @ApplicationContext context: Context, + ): DataStore = context.notificationDataStore } @Module @@ -89,4 +100,10 @@ abstract class DataStoreBindModule { abstract fun bindOnboardingDataSource( defaultOnboardingDataSource: DefaultOnboardingDataSource, ): OnboardingDataSource + + @Binds + @Singleton + abstract fun bindNotificationDataSource( + defaultNotificationDataSource: DefaultNotificationDataSource, + ): NotificationDataSource } diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt index 49262242..8a65ab8c 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt @@ -17,3 +17,7 @@ annotation class LibraryRecentSearchDataStore @Qualifier @Retention(AnnotationRetention.BINARY) annotation class OnboardingDataStore + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class NotificationDataStore diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 70c840be..ba6696b4 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -12,6 +12,7 @@ android { dependencies { implementations( projects.core.common, + projects.core.model, libs.androidx.splash, diff --git a/core/designsystem/src/main/ic_launcher-playstore.png b/core/designsystem/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..afe08328 Binary files /dev/null and b/core/designsystem/src/main/ic_launcher-playstore.png differ diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt new file mode 100644 index 00000000..5a4f72a3 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt @@ -0,0 +1,36 @@ +package com.ninecraft.booket.core.designsystem + +import androidx.compose.ui.graphics.Color +import com.ninecraft.booket.core.designsystem.theme.InsightBgColor +import com.ninecraft.booket.core.designsystem.theme.InsightTextColor +import com.ninecraft.booket.core.designsystem.theme.JoyBgColor +import com.ninecraft.booket.core.designsystem.theme.JoyTextColor +import com.ninecraft.booket.core.designsystem.theme.SadnessBgColor +import com.ninecraft.booket.core.designsystem.theme.SadnessTextColor +import com.ninecraft.booket.core.designsystem.theme.WarmthBgColor +import com.ninecraft.booket.core.designsystem.theme.WarmthTextColor +import com.ninecraft.booket.core.model.Emotion + +val Emotion.bgColor: Color + get() = when (this) { + Emotion.WARM -> WarmthBgColor + Emotion.JOY -> JoyBgColor + Emotion.SAD -> SadnessBgColor + Emotion.INSIGHT -> InsightBgColor + } + +val Emotion.textColor: Color + get() = when (this) { + Emotion.WARM -> WarmthTextColor + Emotion.JOY -> JoyTextColor + Emotion.SAD -> SadnessTextColor + Emotion.INSIGHT -> InsightTextColor + } + +val Emotion.graphicRes: Int + get() = when (this) { + Emotion.WARM -> R.drawable.img_emotion_warmth + Emotion.JOY -> R.drawable.img_emotion_joy + Emotion.SAD -> R.drawable.img_emotion_sadness + Emotion.INSIGHT -> R.drawable.img_emotion_insight + } diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/EmotionTag.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/EmotionTag.kt deleted file mode 100644 index 9f306a2b..00000000 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/EmotionTag.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ninecraft.booket.core.designsystem - -import androidx.compose.ui.graphics.Color -import com.ninecraft.booket.core.designsystem.theme.JoyBgColor -import com.ninecraft.booket.core.designsystem.theme.JoyTextColor -import com.ninecraft.booket.core.designsystem.theme.SadnessBgColor -import com.ninecraft.booket.core.designsystem.theme.SadnessTextColor -import com.ninecraft.booket.core.designsystem.theme.InsightBgColor -import com.ninecraft.booket.core.designsystem.theme.InsightTextColor -import com.ninecraft.booket.core.designsystem.theme.WarmthBgColor -import com.ninecraft.booket.core.designsystem.theme.WarmthTextColor - -enum class EmotionTag(val label: String, val bgColor: Color, val textColor: Color, val graphic: Int) { - WARMTH("따뜻함", WarmthBgColor, WarmthTextColor, R.drawable.img_emotion_warmth), - JOY("즐거움", JoyBgColor, JoyTextColor, R.drawable.img_emotion_joy), - SADNESS("슬픔", SadnessBgColor, SadnessTextColor, R.drawable.img_emotion_sadness), - INSIGHT("깨달음", InsightBgColor, InsightTextColor, R.drawable.img_emotion_insight), -} diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt index 1ee3847d..61997cc9 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt @@ -21,14 +21,14 @@ val Black = Color(0xFF000000) val Green50 = Color(0xFFF2FFF6) val Green100 = Color(0xFFE3F8E9) -val Green200 = Color(0xFFC1E8CA) -val Green300 = Color(0xFF82C090) -val Green400 = Color(0xFF40BF5D) -val Green500 = Color(0xFF2F9647) -val Green600 = Color(0xFF257838) -val Green700 = Color(0xFF1C5A2A) -val Green800 = Color(0xFF123C1C) -val Green900 = Color(0xFF091D0E) +val Green200 = Color(0xFFC4ECCD) +val Green300 = Color(0xFF9CE0AD) +val Green400 = Color(0xFF6BD184) +val Green500 = Color(0xFF3BC25B) +val Green600 = Color(0xFF33A94F) +val Green700 = Color(0xFF247938) +val Green800 = Color(0xFF174822) +val Green900 = Color(0xFF07180B) val Red50 = Color(0xFFFFECEF) val Red100 = Color(0xFFFFCED4) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Typography.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Typography.kt index 9882936e..70347c5b 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Typography.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Typography.kt @@ -75,6 +75,7 @@ data class ReedTypography( val label2Regular: TextStyle = style(13, 18, -0.13f, FontWeight.Normal), // Caption + val caption1Medium: TextStyle = style(12, 16, -0.12f, FontWeight.Medium), val caption1Regular: TextStyle = style(12, 16, -0.12f, FontWeight.Normal), val caption2Regular: TextStyle = style(11, 14, -0.11f, FontWeight.Normal), diff --git a/core/designsystem/src/main/res/drawable/ic_notification.xml b/core/designsystem/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..01204e1b --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp index 66649c8c..5e2980e0 100644 Binary files a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp and b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index 2025215b..0dc42b7e 100644 Binary files a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 75d495ff..b09ca0d7 100644 Binary files a/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp index e2217fd0..51938070 100644 Binary files a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp and b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index 4271d28f..b3b6b7b2 100644 Binary files a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 24696838..22b904c7 100644 Binary files a/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp index 900e618b..6e12b7cd 100644 Binary files a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index 009475e4..d99fe7eb 100644 Binary files a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index f334bce1..038fd48d 100644 Binary files a/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp index d9ae22ce..5c363142 100644 Binary files a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index e6ecea6e..688a3402 100644 Binary files a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 2f9e4e34..559f62ea 100644 Binary files a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index cc933aeb..d566a303 100644 Binary files a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index ad071e6a..694e55a6 100644 Binary files a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index b1f9be81..83c4f486 100644 Binary files a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/core/designsystem/src/main/res/values/colors.xml b/core/designsystem/src/main/res/values/colors.xml index 2fe46cd5..94292d9f 100644 --- a/core/designsystem/src/main/res/values/colors.xml +++ b/core/designsystem/src/main/res/values/colors.xml @@ -7,5 +7,5 @@ #FF018786 #FF000000 #FFFFFFFF - #FF2F9647 + #FF3BC25B diff --git a/core/designsystem/src/main/res/values/splash.xml b/core/designsystem/src/main/res/values/splash.xml index 83ffcd93..4b492d5d 100644 --- a/core/designsystem/src/main/res/values/splash.xml +++ b/core/designsystem/src/main/res/values/splash.xml @@ -2,7 +2,7 @@ diff --git a/core/designsystem/stability/designsystem.stability b/core/designsystem/stability/designsystem.stability new file mode 100644 index 00000000..319a7836 --- /dev/null +++ b/core/designsystem/stability/designsystem.stability @@ -0,0 +1,316 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :designsystem:stabilityDump + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.NetworkImage(imageUrl: kotlin.String, contentDescription: kotlin.String, modifier: androidx.compose.ui.Modifier, placeholder: androidx.compose.ui.graphics.painter.Painter?, contentScale: androidx.compose.ui.layout.ContentScale): kotlin.Unit + skippable: false + restartable: true + params: + - imageUrl: STABLE (String is immutable) + - contentDescription: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - placeholder: RUNTIME (requires runtime check) + - contentScale: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.NetworkImagePreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.RecordProgressBar(currentStep: com.ninecraft.booket.core.designsystem.RecordStep, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - currentStep: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.ReedDivider(modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.ReedDividerPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.ResourceImage(imageRes: kotlin.Int, contentDescription: kotlin.String, modifier: androidx.compose.ui.Modifier, placeholder: androidx.compose.ui.graphics.painter.Painter?, contentScale: androidx.compose.ui.layout.ContentScale): kotlin.Unit + skippable: false + restartable: true + params: + - imageRes: STABLE (primitive type) + - contentDescription: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - placeholder: RUNTIME (requires runtime check) + - contentScale: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.ResourceImagePreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.(): com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.(): com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.(): com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.(): com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.(): com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.(): com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedButton(onClick: kotlin.Function0, text: kotlin.String, sizeStyle: com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle, colorStyle: com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle, modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean, leadingIcon: @[Composable] androidx.compose.runtime.internal.ComposableFunction0?, trailingIcon: @[Composable] androidx.compose.runtime.internal.ComposableFunction0?, multipleEventsCutterEnabled: kotlin.Boolean): kotlin.Unit + skippable: true + restartable: true + params: + - onClick: STABLE (function type) + - text: STABLE (String is immutable) + - sizeStyle: STABLE + - colorStyle: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + - enabled: STABLE (primitive type) + - leadingIcon: STABLE (composable function type) + - trailingIcon: STABLE (composable function type) + - multipleEventsCutterEnabled: STABLE (primitive type) + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle.borderStroke(): androidx.compose.foundation.BorderStroke? + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle.containerColor(isPressed: kotlin.Boolean): androidx.compose.ui.graphics.Color + skippable: true + restartable: true + params: + - isPressed: STABLE (primitive type) + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle.contentColor(): androidx.compose.ui.graphics.Color + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle.disabledContainerColor(): androidx.compose.ui.graphics.Color + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle.disabledContentColor(): androidx.compose.ui.graphics.Color + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.button.ReedButtonDisabledPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.button.ReedLargeButtonPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.button.ReedMediumButtonPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.button.ReedSmallButtonPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.button.ReedTextButton(onClick: kotlin.Function0, text: kotlin.String, sizeStyle: com.ninecraft.booket.core.designsystem.component.button.ButtonSizeStyle, colorStyle: com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle, modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean, multipleEventsCutterEnabled: kotlin.Boolean): kotlin.Unit + skippable: true + restartable: true + params: + - onClick: STABLE (function type) + - text: STABLE (String is immutable) + - sizeStyle: STABLE + - colorStyle: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + - enabled: STABLE (primitive type) + - multipleEventsCutterEnabled: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.button.ReedTextButtonPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.checkbox.CircleCheckBox(checked: kotlin.Boolean, onCheckedChange: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - checked: STABLE (primitive type) + - onCheckedChange: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.checkbox.CircleCheckboxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.checkbox.SquareCheckBox(checked: kotlin.Boolean, onCheckedChange: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - checked: STABLE (primitive type) + - onCheckedChange: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.checkbox.SquareCheckboxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyCheckBox(checked: kotlin.Boolean, onCheckedChange: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - checked: STABLE (primitive type) + - onCheckedChange: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyCheckBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField(recordState: androidx.compose.foundation.text.input.TextFieldState, recordHintRes: kotlin.Int, modifier: androidx.compose.ui.Modifier, inputTransformation: androidx.compose.foundation.text.input.InputTransformation?, keyboardOptions: androidx.compose.foundation.text.KeyboardOptions, lineLimits: androidx.compose.foundation.text.input.TextFieldLineLimits, isError: kotlin.Boolean, errorMessage: kotlin.String, onClear: kotlin.Function0?, onNext: kotlin.Function0, backgroundColor: androidx.compose.ui.graphics.Color, textColor: androidx.compose.ui.graphics.Color, cornerShape: androidx.compose.foundation.shape.RoundedCornerShape, borderStroke: androidx.compose.foundation.BorderStroke): kotlin.Unit + skippable: true + restartable: true + params: + - recordState: STABLE (marked @Stable or @Immutable) + - recordHintRes: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) + - inputTransformation: STABLE (marked @Stable or @Immutable) + - keyboardOptions: STABLE (marked @Stable or @Immutable) + - lineLimits: STABLE (marked @Stable or @Immutable) + - isError: STABLE (primitive type) + - errorMessage: STABLE (String is immutable) + - onClear: STABLE (function type) + - onNext: STABLE (function type) + - backgroundColor: STABLE (marked @Stable or @Immutable) + - textColor: STABLE (marked @Stable or @Immutable) + - cornerShape: STABLE (known stable type) + - borderStroke: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextFieldPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.textfield.ReedTextField(queryState: androidx.compose.foundation.text.input.TextFieldState, queryHintRes: kotlin.Int, onSearch: kotlin.Function1, onClear: kotlin.Function0, modifier: androidx.compose.ui.Modifier, backgroundColor: androidx.compose.ui.graphics.Color, textColor: androidx.compose.ui.graphics.Color, cornerShape: androidx.compose.foundation.shape.RoundedCornerShape, borderStroke: androidx.compose.foundation.BorderStroke?, searchIconTint: androidx.compose.ui.graphics.Color): kotlin.Unit + skippable: true + restartable: true + params: + - queryState: STABLE (marked @Stable or @Immutable) + - queryHintRes: STABLE (primitive type) + - onSearch: STABLE (function type) + - onClear: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + - backgroundColor: STABLE (marked @Stable or @Immutable) + - textColor: STABLE (marked @Stable or @Immutable) + - cornerShape: STABLE (known stable type) + - borderStroke: STABLE (marked @Stable or @Immutable) + - searchIconTint: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.core.designsystem.component.textfield.ReedTextFieldPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme(content: @[Composable] androidx.compose.runtime.internal.ComposableFunction0): kotlin.Unit + skippable: true + restartable: true + params: + - content: STABLE (composable function type) + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.(): com.ninecraft.booket.core.designsystem.theme.ReedBorder + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.(): com.ninecraft.booket.core.designsystem.theme.ReedColorScheme + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.(): com.ninecraft.booket.core.designsystem.theme.ReedRadius + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.(): com.ninecraft.booket.core.designsystem.theme.ReedSpacing + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.(): com.ninecraft.booket.core.designsystem.theme.ReedTypography + skippable: true + restartable: true + params: + diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt index b7e712d0..f972caf8 100644 --- a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/UserProfileModel.kt @@ -9,4 +9,5 @@ data class UserProfileModel( val nickname: String, val provider: String, val termsAgreed: Boolean, + val notificationEnabled: Boolean, ) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt index 1300b8b3..011ab824 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt @@ -12,7 +12,6 @@ import okhttp3.Route import javax.inject.Inject import javax.inject.Provider -@Suppress("TooGenericExceptionCaught") class TokenAuthenticator @Inject constructor( private val tokenDataSource: TokenDataSource, private val serviceProvider: Provider, diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/DeviceRegistrationRequest.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/DeviceRegistrationRequest.kt new file mode 100644 index 00000000..be82c4cd --- /dev/null +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/DeviceRegistrationRequest.kt @@ -0,0 +1,12 @@ +package com.ninecraft.booket.core.network.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DeviceRegistrationRequest( + @SerialName("deviceId") + val deviceId: String, + @SerialName("fcmToken") + val fcmToken: String, +) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/NotificationSettingsRequest.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/NotificationSettingsRequest.kt new file mode 100644 index 00000000..b2dc87cc --- /dev/null +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/NotificationSettingsRequest.kt @@ -0,0 +1,10 @@ +package com.ninecraft.booket.core.network.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class NotificationSettingsRequest( + @SerialName("notificationEnabled") + val notificationEnabled: Boolean, +) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt index 324c5784..f1148819 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt @@ -28,7 +28,7 @@ data class ReadingRecord( @SerialName("quote") val quote: String, @SerialName("review") - val review: String = "", + val review: String?, @SerialName("emotionTags") val emotionTags: List = emptyList(), @SerialName("createdAt") diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordDetailResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordDetailResponse.kt index 1c5441f7..c78a219b 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordDetailResponse.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordDetailResponse.kt @@ -14,7 +14,7 @@ data class RecordDetailResponse( @SerialName("quote") val quote: String, @SerialName("review") - val review: String, + val review: String?, @SerialName("emotionTags") val emotionTags: List, @SerialName("createdAt") diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordRegisterResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordRegisterResponse.kt index b83b33d9..a6a4c2b5 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordRegisterResponse.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/RecordRegisterResponse.kt @@ -16,7 +16,7 @@ data class RecordRegisterResponse( @SerialName("emotionTags") val emotionTags: List, @SerialName("review") - val review: String, + val review: String?, @SerialName("createdAt") val createdAt: String, @SerialName("updatedAt") diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt index a60374d3..3b871550 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/UserProfileResponse.kt @@ -15,4 +15,6 @@ data class UserProfileResponse( val provider: String, @SerialName("termsAgreed") val termsAgreed: Boolean, + @SerialName("notificationEnabled") + val notificationEnabled: Boolean, ) 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 00dc717b..f3926e64 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 @@ -1,7 +1,9 @@ package com.ninecraft.booket.core.network.service import com.ninecraft.booket.core.network.request.BookUpsertRequest +import com.ninecraft.booket.core.network.request.DeviceRegistrationRequest import com.ninecraft.booket.core.network.request.LoginRequest +import com.ninecraft.booket.core.network.request.NotificationSettingsRequest import com.ninecraft.booket.core.network.request.RecordRegisterRequest import com.ninecraft.booket.core.network.request.RefreshTokenRequest import com.ninecraft.booket.core.network.request.TermsAgreementRequest @@ -51,6 +53,12 @@ interface ReedService { @GET("api/v1/users/me") suspend fun getUserProfile(): UserProfileResponse + @PUT("api/v1/users/me/devices") + suspend fun upsertDevice(@Body deviceRegistrationRequest: DeviceRegistrationRequest): UserProfileResponse + + @PUT("api/v1/users/me/notification-settings") + suspend fun updateNotificationSettings(@Body notificationSettingsRequest: NotificationSettingsRequest): UserProfileResponse + // Book endpoints (no auth required) @GET("api/v1/books/guest/search") suspend fun searchBookAsGuest( @@ -91,7 +99,7 @@ interface ReedService { @Query("title") title: String? = null, @Query("page") page: Int, @Query("size") size: Int, - @Query("sort") sort: String = "CREATED_DATE_DESC", + @Query("sort") sort: String = "UPDATED_DATE_DESC", ): LibraryResponse @DELETE("api/v1/books/my-library/{userBookId}") diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/InfinityLazyColumn.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/InfinityLazyColumn.kt index 6805f4fc..b0798ffd 100644 --- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/InfinityLazyColumn.kt +++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/InfinityLazyColumn.kt @@ -35,10 +35,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.skydoves.compose.effects.RememberedEffect +import com.skydoves.compose.stability.runtime.TraceRecomposition // 기기에서 평균적으로 한 화면에 보이는 아이템 개수 private const val LIMIT_COUNT = 6 +@TraceRecomposition @Composable fun InfinityLazyColumn( modifier: Modifier = Modifier, diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt index 083b39db..366b3e69 100644 --- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt +++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import com.ninecraft.booket.core.common.utils.isNetworkError +import com.ninecraft.booket.core.common.utils.ErrorType import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle @@ -21,10 +21,14 @@ import com.ninecraft.booket.core.ui.R @Composable fun ReedErrorUi( - exception: Throwable, + errorType: ErrorType, onRetryClick: () -> Unit, ) { - val message = if (exception.isNetworkError()) stringResource(R.string.network_error_message) else stringResource(R.string.server_error_message) + val message = when (errorType) { + ErrorType.NetworkError -> stringResource(R.string.network_error_message) + ErrorType.ServerError -> stringResource(R.string.server_error_message) + } + Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, @@ -52,7 +56,7 @@ fun ReedErrorUi( private fun ReedNetworkErrorUiPreview() { ReedTheme { ReedErrorUi( - exception = java.io.IOException("네트워크 오류"), + errorType = ErrorType.NetworkError, onRetryClick = {}, ) } @@ -63,7 +67,7 @@ private fun ReedNetworkErrorUiPreview() { private fun ReedServerErrorUiPreview() { ReedTheme { ReedErrorUi( - exception = Exception("알 수 없는 문제"), + errorType = ErrorType.ServerError, onRetryClick = {}, ) } diff --git a/core/ui/stability/ui.stability b/core/ui/stability/ui.stability new file mode 100644 index 00000000..44d5e658 --- /dev/null +++ b/core/ui/stability/ui.stability @@ -0,0 +1,199 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :ui:stabilityDump + +@Composable +public fun com.ninecraft.booket.core.ui.ReedScaffold(modifier: androidx.compose.ui.Modifier, topBar: @[Composable] androidx.compose.runtime.internal.ComposableFunction0, bottomBar: @[Composable] androidx.compose.runtime.internal.ComposableFunction0, snackbarHost: @[Composable] androidx.compose.runtime.internal.ComposableFunction0, floatingActionButton: @[Composable] androidx.compose.runtime.internal.ComposableFunction0, containerColor: androidx.compose.ui.graphics.Color, contentWindowInsets: androidx.compose.foundation.layout.WindowInsets, content: @[Composable] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - topBar: STABLE (composable function type) + - bottomBar: STABLE (composable function type) + - snackbarHost: STABLE (composable function type) + - floatingActionButton: STABLE (composable function type) + - containerColor: STABLE (marked @Stable or @Immutable) + - contentWindowInsets: STABLE (marked @Stable or @Immutable) + - content: STABLE (composable function type) + +@Composable +public fun com.ninecraft.booket.core.ui.component.InfinityLazyColumn(modifier: androidx.compose.ui.Modifier, state: androidx.compose.foundation.lazy.LazyListState, contentPadding: androidx.compose.foundation.layout.PaddingValues, reverseLayout: kotlin.Boolean, verticalArrangement: androidx.compose.foundation.layout.Arrangement.Vertical, horizontalAlignment: androidx.compose.ui.Alignment.Horizontal, flingBehavior: androidx.compose.foundation.gestures.FlingBehavior, userScrollEnabled: kotlin.Boolean, loadMoreLimitCount: kotlin.Int, loadMore: kotlin.Function0, content: @[ExtensionFunctionType] kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - state: STABLE (marked @Stable or @Immutable) + - contentPadding: STABLE (marked @Stable or @Immutable) + - reverseLayout: STABLE (primitive type) + - verticalArrangement: STABLE (marked @Stable or @Immutable) + - horizontalAlignment: STABLE (marked @Stable or @Immutable) + - flingBehavior: STABLE (marked @Stable or @Immutable) + - userScrollEnabled: STABLE (primitive type) + - loadMoreLimitCount: STABLE (primitive type) + - loadMore: STABLE (function type) + - content: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.core.ui.component.InfinityLazyColumnPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.ui.component.LoadStateFooter(footerState: com.ninecraft.booket.core.ui.component.FooterState, onRetryClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - footerState: STABLE (marked @Stable or @Immutable) + - onRetryClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedBackTopAppBar(modifier: androidx.compose.ui.Modifier, isDark: kotlin.Boolean, title: kotlin.String, onBackClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - isDark: STABLE (primitive type) + - title: STABLE (String is immutable) + - onBackClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedBackTopAppBarPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedBottomSheet(onDismissRequest: kotlin.Function0, modifier: androidx.compose.ui.Modifier, sheetState: androidx.compose.material3.SheetState, content: @[Composable] androidx.compose.runtime.internal.ComposableFunction0): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + - sheetState: STABLE (marked @Stable or @Immutable) + - content: STABLE (composable function type) + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedChoiceDialogPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedCloseTopAppBar(modifier: androidx.compose.ui.Modifier, isDark: kotlin.Boolean, title: kotlin.String, onClose: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - isDark: STABLE (primitive type) + - title: STABLE (String is immutable) + - onClose: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedCloseTopAppBarPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedConfirmDialogPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedDialog(confirmButtonText: kotlin.String, onConfirmRequest: kotlin.Function0, modifier: androidx.compose.ui.Modifier, title: kotlin.String?, description: kotlin.String?, dismissButtonText: kotlin.String?, onDismissRequest: kotlin.Function0, headerContent: @[Composable] androidx.compose.runtime.internal.ComposableFunction0?): kotlin.Unit + skippable: true + restartable: true + params: + - confirmButtonText: STABLE (String is immutable) + - onConfirmRequest: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + - title: STABLE + - description: STABLE + - dismissButtonText: STABLE + - onDismissRequest: STABLE (function type) + - headerContent: STABLE (composable function type) + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedErrorUi(errorType: com.ninecraft.booket.core.common.utils.ErrorType, onRetryClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - errorType: STABLE (marked @Stable or @Immutable) + - onRetryClick: STABLE (function type) + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedFullScreen(modifier: androidx.compose.ui.Modifier, backgroundColor: androidx.compose.ui.graphics.Color, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - backgroundColor: STABLE (marked @Stable or @Immutable) + - content: STABLE (composable function type) + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedLoadingIndicator(modifier: androidx.compose.ui.Modifier, delayMillis: kotlin.Long): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - delayMillis: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedLoadingIndicatorPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedNetworkErrorUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedServerErrorUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.ui.component.ReedTopAppBar(modifier: androidx.compose.ui.Modifier, isDark: kotlin.Boolean, title: kotlin.String, startIconRes: kotlin.Int?, startIconDescription: kotlin.String, startIconOnClick: kotlin.Function0, endIconRes: kotlin.Int?, endIconDescription: kotlin.String, endIconOnClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + - isDark: STABLE (primitive type) + - title: STABLE (String is immutable) + - startIconRes: STABLE + - startIconDescription: STABLE (String is immutable) + - startIconOnClick: STABLE (function type) + - endIconRes: STABLE + - endIconDescription: STABLE (String is immutable) + - endIconOnClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.core.ui.component.ReedTopAppBarPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.core.ui.component.onLoadMore(limitCount: kotlin.Int, loadOnBottom: kotlin.Boolean, action: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - limitCount: STABLE (primitive type) + - loadOnBottom: STABLE (primitive type) + - action: STABLE (function type) + diff --git a/detekt-config.yml b/detekt-config.yml index 36951a0c..e37e154c 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -68,6 +68,10 @@ style: MaxLineLength: active: false +exceptions: + TooGenericExceptionCaught: + active: false + naming: TopLevelPropertyNaming: constantPattern: '[A-Z][A-Za-z0-9_]*' 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 67d3e962..07833fe8 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 @@ -87,7 +87,6 @@ class BookDetailPresenter @AssistedInject constructor( var isBookDeleteDialogVisible by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } - @Suppress("TooGenericExceptionCaught") fun initialLoad() { uiState = UiState.Loading @@ -307,7 +306,7 @@ class BookDetailPresenter @AssistedInject constructor( RecordCardScreen( quote = selectedRecordInfo.quote, bookTitle = selectedRecordInfo.bookTitle, - emotionTag = selectedRecordInfo.emotionTags[0], + emotion = selectedRecordInfo.emotionTags[0], ), ) } 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 cc27a011..6ce7d6f7 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 @@ -29,6 +29,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.common.constants.BookStatus +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.ReedDivider import com.ninecraft.booket.core.designsystem.component.button.ReedButton @@ -53,12 +54,14 @@ import com.ninecraft.booket.feature.detail.book.component.RecordItem import com.ninecraft.booket.feature.detail.book.component.RecordSortBottomSheet import com.ninecraft.booket.feature.detail.record.component.RecordMenuBottomSheet import com.ninecraft.booket.feature.screens.BookDetailScreen +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @CircuitInject(BookDetailScreen::class, ActivityRetainedComponent::class) @Composable @@ -194,6 +197,7 @@ internal fun BookDetailUi( } } +@TraceRecomposition @Composable internal fun BookDetailContent( state: BookDetailUiState, @@ -352,7 +356,7 @@ internal fun BookDetailContent( is UiState.Error -> { ReedErrorUi( - exception = state.uiState.exception, + errorType = state.uiState.exception.toErrorType(), onRetryClick = { state.eventSink(BookDetailUiEvent.OnRetryClick) }, ) } diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt index 35b6554f..7d7789ee 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/BookUpdateBottomSheet.kt @@ -37,10 +37,12 @@ import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.ui.component.ReedBottomSheet import com.ninecraft.booket.feature.detail.R +import com.skydoves.compose.stability.runtime.TraceRecomposition import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun BookUpdateBottomSheet( diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt index 98cd894b..a0cd832b 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/EmotionAnalysisResultText.kt @@ -15,8 +15,8 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.util.EmotionDisplayType -import com.ninecraft.booket.core.common.util.analyzeEmotions +import com.ninecraft.booket.core.common.utils.EmotionDisplayType +import com.ninecraft.booket.core.common.utils.analyzeEmotions import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.model.Emotion diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/ReadingRecordsHeader.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/ReadingRecordsHeader.kt index 57c25d97..7dfdefb2 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/ReadingRecordsHeader.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/ReadingRecordsHeader.kt @@ -14,11 +14,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.feature.detail.R import com.ninecraft.booket.feature.detail.book.RecordSort +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @Composable internal fun ReadingRecordsHeader( totalCount: Int, @@ -62,3 +65,15 @@ internal fun ReadingRecordsHeader( } } } + +@ComponentPreview +@Composable +private fun ReadingRecordsHeaderPreview() { + ReedTheme { + ReadingRecordsHeader( + totalCount = 4, + currentRecordSort = RecordSort.PAGE_NUMBER_ASC, + onReadingRecordClick = {}, + ) + } +} diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt index ac9d84d1..ebeaf2a4 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/SeedItem.kt @@ -16,9 +16,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.extensions.toBackgroundColor -import com.ninecraft.booket.core.common.extensions.toTextColor import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.bgColor +import com.ninecraft.booket.core.designsystem.textColor import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.core.model.EmotionModel @@ -42,7 +42,7 @@ internal fun SeedItem( Box( modifier = Modifier .clip(RoundedCornerShape(ReedTheme.radius.full)) - .background(emotion.name.toBackgroundColor()) + .background(emotion.name.bgColor) .padding( horizontal = ReedTheme.spacing.spacing2, vertical = ReedTheme.spacing.spacing1, @@ -51,7 +51,7 @@ internal fun SeedItem( ) { Text( text = emotion.name.displayName, - color = emotion.name.toTextColor(), + color = emotion.name.textColor, style = ReedTheme.typography.label2SemiBold, ) } diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt index 19958164..1ab1de37 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt @@ -74,7 +74,7 @@ class RecordCardPresenter @AssistedInject constructor( isLoading = isLoading, quote = screen.quote, bookTitle = screen.bookTitle, - emotionTag = screen.emotionTag, + emotion = screen.emotion, isCapturing = isCapturing, isSharing = isSharing, sideEffect = sideEffect, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt index 97e39ca7..3e3813be 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt @@ -36,10 +36,12 @@ import com.ninecraft.booket.core.ui.component.ReedTopAppBar import com.ninecraft.booket.feature.detail.R import com.ninecraft.booket.feature.detail.card.component.RecordCard import com.ninecraft.booket.feature.screens.RecordCardScreen +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @CircuitInject(RecordCardScreen::class, ActivityRetainedComponent::class) @Composable internal fun RecordCardUi( @@ -81,7 +83,7 @@ internal fun RecordCardUi( RecordCard( quote = state.quote, bookTitle = state.bookTitle, - emotionTag = state.emotionTag, + emotion = state.emotion, modifier = Modifier .padding(top = ReedTheme.spacing.spacing5) .clip(RoundedCornerShape(ReedTheme.radius.md)) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt index c9880356..43c2c71a 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt @@ -11,7 +11,7 @@ data class RecordCardUiState( val quote: String = "", val bookTitle: String = "", val author: String = "", - val emotionTag: String = "", + val emotion: String = "", val isCapturing: Boolean = false, val isSharing: Boolean = false, val sideEffect: RecordCardSideEffect? = null, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt index 2f9b7bcc..4c055930 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt @@ -19,20 +19,20 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.EmotionTag import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.feature.detail.R @Composable internal fun RecordCard( quote: String, bookTitle: String, - emotionTag: String, + emotion: String, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxWidth()) { Image( - painter = painterResource(getEmotionCardImage(emotionTag)), + painter = painterResource(getEmotionCardImage(emotion)), contentDescription = "Record Card Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, @@ -74,12 +74,12 @@ internal fun RecordCard( } } -private fun getEmotionCardImage(emotionTag: String): Int { - return when (emotionTag) { - EmotionTag.WARMTH.label -> R.drawable.img_record_card_warm - EmotionTag.JOY.label -> R.drawable.img_record_card_joy - EmotionTag.SADNESS.label -> R.drawable.img_record_card_sad - EmotionTag.INSIGHT.label -> R.drawable.img_record_card_insight +private fun getEmotionCardImage(emotion: String): Int { + return when (emotion) { + Emotion.WARM.displayName -> R.drawable.img_record_card_warm + Emotion.JOY.displayName -> R.drawable.img_record_card_joy + Emotion.SAD.displayName -> R.drawable.img_record_card_sad + Emotion.INSIGHT.displayName -> R.drawable.img_record_card_insight else -> R.drawable.img_record_card_warm } } @@ -91,7 +91,7 @@ private fun RecordCardPreview() { RecordCard( quote = "이 세상에 집이라 이름 붙일 수 없는 것이 있다면 그건 바로 여기, 내가 앉아 있는 이곳일 것이다.", bookTitle = "샤이닝", - emotionTag = EmotionTag.WARMTH.label, + emotion = Emotion.WARM.displayName, ) } } 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 151eeb03..fbc0723d 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 @@ -127,7 +127,7 @@ class RecordDetailPresenter @AssistedInject constructor( RecordCardScreen( quote = recordDetailInfo.quote, bookTitle = recordDetailInfo.bookTitle, - emotionTag = recordDetailInfo.emotionTags[0], + emotion = recordDetailInfo.emotionTags[0], ), ) } 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 48248dd9..8bedd731 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 @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.ReedDivider import com.ninecraft.booket.core.designsystem.theme.ReedTheme @@ -30,11 +31,13 @@ import com.ninecraft.booket.feature.detail.record.component.QuoteItem import com.ninecraft.booket.feature.detail.record.component.RecordMenuBottomSheet import com.ninecraft.booket.feature.detail.record.component.ReviewItem import com.ninecraft.booket.feature.screens.RecordDetailScreen +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @CircuitInject(RecordDetailScreen::class, ActivityRetainedComponent::class) @Composable @@ -115,6 +118,7 @@ internal fun RecordDetailUi( } } +@TraceRecomposition @Composable private fun RecordDetailContent( state: RecordDetailUiState, @@ -169,7 +173,7 @@ private fun RecordDetailContent( is UiState.Error -> { ReedErrorUi( - exception = state.uiState.exception, + errorType = state.uiState.exception.toErrorType(), onRetryClick = { }, ) } @@ -202,3 +206,30 @@ private fun ReviewDetailPreview() { ) } } + +@ComponentPreview +@Composable +private fun ReviewDetailEmptyPreview() { + ReedTheme { + RecordDetailUi( + state = RecordDetailUiState( + uiState = UiState.Success, + recordDetailInfo = RecordDetailModel( + id = "", + userBookId = "", + pageNumber = 90, + quote = "소설가들은 늘 소재를 찾아 떠도는 존재 같지만, 실은 그 반대인 경우가 더 잦다.", + review = "", + emotionTags = listOf("따뜻함"), + createdAt = "2023.10.10", + updatedAt = "", + bookTitle = "여름은 오래 그곳에 남아", + bookPublisher = "비채 비채 비채 비채", + bookCoverImageUrl = "", + author = "미쓰이에 마사시, 미쓰이에 마사시, 미쓰이에 마사시, 미쓰이에 마사시", + ), + eventSink = {}, + ), + ) + } +} diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/BookItem.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/BookItem.kt index 98e6d98d..033283d4 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/BookItem.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/BookItem.kt @@ -1,5 +1,6 @@ package com.ninecraft.booket.feature.detail.record.component +import androidx.compose.foundation.border import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -48,7 +49,12 @@ internal fun BookItem( .padding(end = ReedTheme.spacing.spacing4) .width(46.dp) .height(68.dp) - .clip(RoundedCornerShape(size = ReedTheme.radius.xs)), + .clip(RoundedCornerShape(size = ReedTheme.radius.xs)) + .border( + width = 1.dp, + color = ReedTheme.colors.borderPrimary, + shape = RoundedCornerShape(ReedTheme.radius.xs), + ), placeholder = painterResource(R.drawable.ic_placeholder), ) Column(modifier = Modifier.weight(1f)) { diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt index 8c2fa6e5..6a1c14db 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt @@ -39,7 +39,7 @@ internal fun ReviewItem( ) .padding( horizontal = ReedTheme.spacing.spacing4, - vertical = ReedTheme.spacing.spacing3, + vertical = ReedTheme.spacing.spacing4, ), ) { Column { @@ -68,12 +68,14 @@ internal fun ReviewItem( style = ReedTheme.typography.label2Regular, ) } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - Text( - text = review, - color = ReedTheme.colors.contentSecondary, - style = ReedTheme.typography.label1Medium, - ) + if (review.isNotBlank()) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + Text( + text = review, + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.label1Medium, + ) + } } } } @@ -89,3 +91,15 @@ private fun ReviewBoxPreview() { ) } } + +@ComponentPreview +@Composable +private fun ReviewBoxEmptyPreview() { + ReedTheme { + ReviewItem( + emotion = "따뜻함", + review = "", + createdAt = "2025.06.25", + ) + } +} diff --git a/feature/detail/src/main/res/drawable/img_insight.webp b/feature/detail/src/main/res/drawable/img_insight.webp index 235bad8e..77d816fd 100644 Binary files a/feature/detail/src/main/res/drawable/img_insight.webp and b/feature/detail/src/main/res/drawable/img_insight.webp differ diff --git a/feature/detail/src/main/res/drawable/img_joy.webp b/feature/detail/src/main/res/drawable/img_joy.webp index afa54b1e..00414f16 100644 Binary files a/feature/detail/src/main/res/drawable/img_joy.webp and b/feature/detail/src/main/res/drawable/img_joy.webp differ diff --git a/feature/detail/src/main/res/drawable/img_sad.webp b/feature/detail/src/main/res/drawable/img_sad.webp index dcbf98e3..439e3224 100644 Binary files a/feature/detail/src/main/res/drawable/img_sad.webp and b/feature/detail/src/main/res/drawable/img_sad.webp differ diff --git a/feature/detail/src/main/res/drawable/img_warm.webp b/feature/detail/src/main/res/drawable/img_warm.webp index a94574e6..a432f9e6 100644 Binary files a/feature/detail/src/main/res/drawable/img_warm.webp and b/feature/detail/src/main/res/drawable/img_warm.webp differ diff --git a/feature/detail/stability/detail.stability b/feature/detail/stability/detail.stability new file mode 100644 index 00000000..7b658259 --- /dev/null +++ b/feature/detail/stability/detail.stability @@ -0,0 +1,392 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :detail:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.BookDetailContent(state: com.ninecraft.booket.feature.detail.book.BookDetailUiState, innerPadding: androidx.compose.foundation.layout.PaddingValues, modifier: androidx.compose.ui.Modifier, lazyListState: androidx.compose.foundation.lazy.LazyListState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - innerPadding: STABLE (marked @Stable or @Immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - lazyListState: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.detail.book.BookDetailPresenter.present(): com.ninecraft.booket.feature.detail.book.BookDetailUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.book.BookDetailPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.BookDetailUi(state: com.ninecraft.booket.feature.detail.book.BookDetailUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.HandleBookDetailSideEffects(state: com.ninecraft.booket.feature.detail.book.BookDetailUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.BookItem(bookDetail: com.ninecraft.booket.core.model.BookDetailModel, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - bookDetail: STABLE (marked @Stable or @Immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.BookItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.detail.book.component.BookStatusItem(item: com.ninecraft.booket.core.common.constants.BookStatus, selected: kotlin.Boolean, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - item: STABLE + - selected: STABLE (primitive type) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.BookUpdateBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, bookStatuses: kotlinx.collections.immutable.ImmutableList, currentBookStatus: com.ninecraft.booket.core.common.constants.BookStatus?, selectedBookStatus: com.ninecraft.booket.core.common.constants.BookStatus, onItemSelected: kotlin.Function1, onBookUpdateButtonClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onCloseButtonClick: STABLE (function type) + - bookStatuses: STABLE (known stable type) + - currentBookStatus: STABLE + - selectedBookStatus: STABLE + - onItemSelected: STABLE (function type) + - onBookUpdateButtonClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.BookUpdateBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.ChoiceBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.CollectedSeedPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.CollectedSeeds(seedsStats: kotlinx.collections.immutable.ImmutableList, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - seedsStats: STABLE (known stable type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.DetailMenuBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onDeleteBookClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onDeleteBookClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.DetailMenuItem(iconResId: kotlin.Int, iconDescription: kotlin.String, label: kotlin.String, color: androidx.compose.ui.graphics.Color, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - iconResId: STABLE (primitive type) + - iconDescription: STABLE (String is immutable) + - label: STABLE (String is immutable) + - color: STABLE (marked @Stable or @Immutable) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.EmotionAnalysisResultText(emotions: kotlinx.collections.immutable.ImmutableList, brandColor: androidx.compose.ui.graphics.Color, secondaryColor: androidx.compose.ui.graphics.Color, emotionTextStyle: androidx.compose.ui.text.TextStyle, regularTextStyle: androidx.compose.ui.text.TextStyle): androidx.compose.ui.text.AnnotatedString? + skippable: true + restartable: true + params: + - emotions: STABLE (known stable type) + - brandColor: STABLE (marked @Stable or @Immutable) + - secondaryColor: STABLE (marked @Stable or @Immutable) + - emotionTextStyle: STABLE (marked @Stable or @Immutable) + - regularTextStyle: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.EmotionTextAllCasesPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.ReadingRecordsHeader(totalCount: kotlin.Int, currentRecordSort: com.ninecraft.booket.feature.detail.book.RecordSort, onReadingRecordClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - totalCount: STABLE (primitive type) + - currentRecordSort: STABLE + - onReadingRecordClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.ReadingRecordsHeaderPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.RecordItem(recordInfo: com.ninecraft.booket.core.model.ReadingRecordModel, onRecordMenuClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - recordInfo: STABLE (marked @Stable or @Immutable) + - onRecordMenuClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.RecordItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.RecordSortBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, recordSortItems: kotlinx.collections.immutable.ImmutableList, currentRecordSort: com.ninecraft.booket.feature.detail.book.RecordSort, onItemSelected: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onCloseButtonClick: STABLE (function type) + - recordSortItems: STABLE (known stable type) + - currentRecordSort: STABLE + - onItemSelected: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.RecordSortBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.detail.book.component.RecordSortItem(item: com.ninecraft.booket.feature.detail.book.RecordSort, selected: kotlin.Boolean, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - item: STABLE + - selected: STABLE (primitive type) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.book.component.SeedItem(emotion: com.ninecraft.booket.core.model.EmotionModel, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - emotion: STABLE (marked @Stable or @Immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.book.component.SeedItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.card.HandleRecordCardSideEffects(state: com.ninecraft.booket.feature.detail.card.RecordCardUiState, recordCardGraphicsLayer: androidx.compose.ui.graphics.layer.GraphicsLayer, eventSink: kotlin.Function1): kotlin.Unit + skippable: false + restartable: true + params: + - state: STABLE + - recordCardGraphicsLayer: UNSTABLE (has mutable properties or unstable members) + - eventSink: STABLE (function type) + +@Composable +public fun com.ninecraft.booket.feature.detail.card.RecordCardPresenter.present(): com.ninecraft.booket.feature.detail.card.RecordCardUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.card.RecordCardUi(state: com.ninecraft.booket.feature.detail.card.RecordCardUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.card.RecordCardUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.card.component.RecordCard(quote: kotlin.String, bookTitle: kotlin.String, emotionTag: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - quote: STABLE (String is immutable) + - bookTitle: STABLE (String is immutable) + - emotionTag: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.card.component.RecordCardPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.HandleRecordDetailSideEffects(state: com.ninecraft.booket.feature.detail.record.RecordDetailUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +private fun com.ninecraft.booket.feature.detail.record.RecordDetailContent(state: com.ninecraft.booket.feature.detail.record.RecordDetailUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.detail.record.RecordDetailPresenter.present(): com.ninecraft.booket.feature.detail.record.RecordDetailUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.RecordDetailUi(state: com.ninecraft.booket.feature.detail.record.RecordDetailUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.record.ReviewDetailEmptyPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.record.ReviewDetailPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.component.BookItem(imageUrl: kotlin.String, bookTitle: kotlin.String, author: kotlin.String, publisher: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - imageUrl: STABLE (String is immutable) + - bookTitle: STABLE (String is immutable) + - author: STABLE (String is immutable) + - publisher: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.record.component.BookItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.record.component.ChoiceBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.record.component.QuoteBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.component.QuoteItem(quote: kotlin.String, page: kotlin.Int, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - quote: STABLE (String is immutable) + - page: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.component.RecordMenuBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onShareRecordClick: kotlin.Function0, onEditRecordClick: kotlin.Function0, onDeleteRecordClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onShareRecordClick: STABLE (function type) + - onEditRecordClick: STABLE (function type) + - onDeleteRecordClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.record.component.RecordMenuItem(iconResId: kotlin.Int, iconDescription: kotlin.String, label: kotlin.String, color: androidx.compose.ui.graphics.Color, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - iconResId: STABLE (primitive type) + - iconDescription: STABLE (String is immutable) + - label: STABLE (String is immutable) + - color: STABLE (marked @Stable or @Immutable) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.detail.record.component.ReviewBoxEmptyPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.detail.record.component.ReviewBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.component.ReviewItem(emotion: kotlin.String, createdAt: kotlin.String, review: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - emotion: STABLE (String is immutable) + - createdAt: STABLE (String is immutable) + - review: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt index d97243a5..cf132671 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.ninecraft.booket.core.designsystem.EmotionTag +import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.feature.screens.EmotionEditScreen import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained @@ -25,7 +25,7 @@ class EmotionEditPresenter @AssistedInject constructor( @Composable override fun present(): EmotionEditUiState { var selectedEmotion by rememberRetained { mutableStateOf(screen.emotion) } - val emotionTags by rememberRetained { mutableStateOf(EmotionTag.entries.toPersistentList()) } + val emotions by rememberRetained { mutableStateOf(Emotion.entries.toPersistentList()) } val isEditButtonEnabled by remember { derivedStateOf { selectedEmotion != screen.emotion @@ -50,7 +50,7 @@ class EmotionEditPresenter @AssistedInject constructor( return EmotionEditUiState( selectedEmotion = selectedEmotion, - emotionTags = emotionTags, + emotions = emotions, isEditButtonEnabled = isEditButtonEnabled, eventSink = ::handleEvent, ) diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt index 0ddc68c2..d425dba8 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt @@ -26,20 +26,23 @@ import androidx.compose.ui.res.stringResource 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.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.graphicRes import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar import com.ninecraft.booket.feature.edit.R import com.ninecraft.booket.feature.screens.EmotionEditScreen +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.toPersistentList +@TraceRecomposition @CircuitInject(EmotionEditScreen::class, ActivityRetainedComponent::class) @Composable internal fun EmotionEditUi( @@ -65,6 +68,7 @@ internal fun EmotionEditUi( } } +@TraceRecomposition @Composable private fun EmotionEditContent( state: EmotionEditUiState, @@ -97,13 +101,13 @@ private fun EmotionEditContent( verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing3), horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing3), content = { - items(state.emotionTags) { tag -> + items(state.emotions) { tag -> EmotionItem( - emotionTag = tag, + emotion = tag, onClick = { - state.eventSink(EmotionEditUiEvent.OnSelectEmotion(tag.label)) + state.eventSink(EmotionEditUiEvent.OnSelectEmotion(tag.displayName)) }, - isSelected = state.selectedEmotion == tag.label, + isSelected = state.selectedEmotion == tag.displayName, modifier = Modifier.fillMaxWidth(), ) } @@ -126,7 +130,7 @@ private fun EmotionEditContent( @Composable private fun EmotionItem( - emotionTag: EmotionTag, + emotion: Emotion, onClick: () -> Unit, isSelected: Boolean, modifier: Modifier = Modifier, @@ -153,7 +157,7 @@ private fun EmotionItem( contentAlignment = Alignment.Center, ) { Image( - painter = painterResource(emotionTag.graphic), + painter = painterResource(emotion.graphicRes), contentDescription = "Emotion Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, @@ -165,11 +169,11 @@ private fun EmotionItem( @Composable private fun EmotionEditUiPreview() { ReedTheme { - val emotionTags = EmotionTag.entries.toPersistentList() + val emotions = Emotion.entries.toPersistentList() EmotionEditUi( state = EmotionEditUiState( - emotionTags = emotionTags, + emotions = emotions, eventSink = {}, ), ) diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt index 9849988e..4a4bb399 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt @@ -1,6 +1,6 @@ package com.ninecraft.booket.feature.edit.emotion -import com.ninecraft.booket.core.designsystem.EmotionTag +import com.ninecraft.booket.core.model.Emotion import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import kotlinx.collections.immutable.ImmutableList @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentListOf data class EmotionEditUiState( val selectedEmotion: String = "", val isEditButtonEnabled: Boolean = false, - val emotionTags: ImmutableList = persistentListOf(), + val emotions: ImmutableList = persistentListOf(), val eventSink: (EmotionEditUiEvent) -> Unit, ) : CircuitUiState 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 427ce11e..0c1b7220 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 @@ -67,7 +67,6 @@ class RecordEditPresenter @AssistedInject constructor( derivedStateOf { recordPageState.text.isNotEmpty() && recordQuoteState.text.isNotEmpty() && - recordImpressionState.text.isNotEmpty() && !isPageError && hasChanges } 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 9f1257eb..84ee03c6 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 @@ -5,14 +5,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding -import androidx.compose.material3.ScaffoldDefaults import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -21,6 +20,7 @@ import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -45,10 +45,12 @@ import com.ninecraft.booket.feature.edit.R import com.ninecraft.booket.feature.edit.record.component.BookItem import com.ninecraft.booket.feature.screens.RecordEditScreen import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @CircuitInject(RecordEditScreen::class, ActivityRetainedComponent::class) @Composable internal fun RecordEditUi( @@ -83,6 +85,7 @@ internal fun RecordEditUi( } } +@TraceRecomposition @Composable private fun ColumnScope.RecordEditContent(state: RecordEditUiState) { Column( diff --git a/feature/edit/stability/edit.stability b/feature/edit/stability/edit.stability new file mode 100644 index 00000000..c3591cda --- /dev/null +++ b/feature/edit/stability/edit.stability @@ -0,0 +1,95 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :edit:stabilityDump + +@Composable +private fun com.ninecraft.booket.feature.edit.emotion.EmotionEditContent(state: com.ninecraft.booket.feature.edit.emotion.EmotionEditUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.edit.emotion.EmotionEditPresenter.present(): com.ninecraft.booket.feature.edit.emotion.EmotionEditUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.edit.emotion.EmotionEditUi(state: com.ninecraft.booket.feature.edit.emotion.EmotionEditUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.edit.emotion.EmotionEditUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.edit.emotion.EmotionItem(emotionTag: com.ninecraft.booket.core.designsystem.EmotionTag, onClick: kotlin.Function0, isSelected: kotlin.Boolean, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - emotionTag: STABLE + - onClick: STABLE (function type) + - isSelected: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.edit.record.HandleRecordEditSideEffects(state: com.ninecraft.booket.feature.edit.record.RecordEditUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +private fun com.ninecraft.booket.feature.edit.record.RecordEditContent(state: com.ninecraft.booket.feature.edit.record.RecordEditUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +public fun com.ninecraft.booket.feature.edit.record.RecordEditPresenter.present(): com.ninecraft.booket.feature.edit.record.RecordEditUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.edit.record.RecordEditUi(state: com.ninecraft.booket.feature.edit.record.RecordEditUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.edit.record.RecordEditUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.edit.record.component.BookItem(imageUrl: kotlin.String, bookTitle: kotlin.String, author: kotlin.String, publisher: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - imageUrl: STABLE (String is immutable) + - bookTitle: STABLE (String is immutable) + - author: STABLE (String is immutable) + - publisher: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.edit.record.component.BookItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 8c09a08c..840963c2 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementations( libs.logger, + libs.androidx.activity.compose, libs.lottie.compose, ) } 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 175b1804..3ad31023 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,20 @@ 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.handleException +import com.ninecraft.booket.core.common.utils.shouldSyncNotification import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.BookRepository +import com.ninecraft.booket.core.data.api.repository.UserRepository 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.BookSearchScreen import com.ninecraft.booket.feature.screens.HomeScreen +import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordScreen -import com.ninecraft.booket.feature.screens.BookSearchScreen import com.ninecraft.booket.feature.screens.SettingsScreen +import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.collectAsRetainedState @@ -34,6 +39,7 @@ class HomePresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, private val bookRepository: BookRepository, private val authRepository: AuthRepository, + private val userRepository: UserRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { @@ -56,10 +62,27 @@ class HomePresenter @AssistedInject constructor( recentBooks = result.recentBooks.toPersistentList() }.onFailure { exception -> uiState = UiState.Error(exception) + + handleException( + exception = exception, + onError = {}, + onLoginRequired = { + navigator.resetRoot(LoginScreen()) + }, + ) } } } + suspend fun syncNotificationSettings(isGranted: Boolean) { + userRepository.updateNotificationSettings(isGranted) + .onSuccess { + userRepository.setLastNotificationSyncedEnabled(isGranted) + }.onFailure { exception -> + Logger.e("Failed to update notification settings: $exception") + } + } + fun handleEvent(event: HomeUiEvent) { when (event) { is HomeUiEvent.OnSettingsClick -> { @@ -89,6 +112,22 @@ class HomePresenter @AssistedInject constructor( restoreState = true, ) } + + is HomeUiEvent.OnNotificationPermissionResult -> { + scope.launch { + val isPermissionGranted = event.granted + val userSettingEnabled = userRepository.getUserNotificationEnabled() + val lastSyncedServerEnabled = userRepository.getLastSyncedNotificationEnabled() + + val effectiveNotificationEnabled = userSettingEnabled && isPermissionGranted + + val shouldSync = shouldSyncNotification(effectiveNotificationEnabled, lastSyncedServerEnabled) + + if (shouldSync) { + syncNotificationSettings(effectiveNotificationEnabled) + } + } + } } } 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 8307716e..f1858302 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 @@ -1,5 +1,11 @@ package com.ninecraft.booket.feature.home +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -19,10 +25,15 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.theme.HomeBg import com.ninecraft.booket.core.designsystem.theme.ReedTheme @@ -36,10 +47,12 @@ import com.ninecraft.booket.feature.home.component.HomeHeader import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.component.MainBottomBar import com.ninecraft.booket.feature.screens.component.MainTab +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.toImmutableList +@TraceRecomposition @CircuitInject(HomeScreen::class, ActivityRetainedComponent::class) @Composable internal fun HomeUi( @@ -48,6 +61,30 @@ internal fun HomeUi( ) { HandleHomeSideEffects(state = state) + val context = LocalContext.current + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + state.eventSink(HomeUiEvent.OnNotificationPermissionResult(granted)) + } + + if (!state.isGuestMode) { + LaunchedEffect(Unit) { + val isGranted = checkSystemNotificationEnabled(context) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (isGranted) { + state.eventSink(HomeUiEvent.OnNotificationPermissionResult(isGranted)) + } else { + val permission = Manifest.permission.POST_NOTIFICATIONS + permissionLauncher.launch(permission) + } + } else { + state.eventSink(HomeUiEvent.OnNotificationPermissionResult(isGranted)) + } + } + } + ReedScaffold( modifier = modifier.fillMaxSize(), bottomBar = { @@ -194,7 +231,7 @@ internal fun HomeContent( is UiState.Error -> { ReedErrorUi( - exception = state.uiState.exception, + errorType = state.uiState.exception.toErrorType(), onRetryClick = { state.eventSink(HomeUiEvent.OnRetryClick) }, ) } @@ -203,6 +240,14 @@ internal fun HomeContent( } } +private fun checkSystemNotificationEnabled(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + NotificationManagerCompat.from(context).areNotificationsEnabled() + } +} + @DevicePreview @Composable private fun HomePreview() { 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 c90d001a..2f7b61a3 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 @@ -43,4 +43,5 @@ sealed interface HomeUiEvent : CircuitUiEvent { ) : HomeUiEvent data object OnRetryClick : HomeUiEvent data class OnTabSelected(val tab: MainTab) : HomeUiEvent + data class OnNotificationPermissionResult(val granted: Boolean) : HomeUiEvent } diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeBanner.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeBanner.kt index 3b140709..678c2052 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeBanner.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeBanner.kt @@ -29,8 +29,10 @@ import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.HomeBg import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.feature.home.R +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @Composable fun HomeBanner( onBookRegisterClick: () -> Unit, diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeHeader.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeHeader.kt index 72835dab..a193a689 100644 --- a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeHeader.kt +++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/component/HomeHeader.kt @@ -17,10 +17,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.R as designR import com.ninecraft.booket.core.designsystem.theme.HomeBg import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.feature.home.R +import com.ninecraft.booket.core.designsystem.R as designR @Composable fun HomeHeader( diff --git a/feature/home/src/main/res/drawable/img_reed_logo.webp b/feature/home/src/main/res/drawable/img_reed_logo.webp index 00a0355f..5a03058f 100644 Binary files a/feature/home/src/main/res/drawable/img_reed_logo.webp and b/feature/home/src/main/res/drawable/img_reed_logo.webp differ diff --git a/feature/home/stability/home.stability b/feature/home/stability/home.stability new file mode 100644 index 00000000..c66b0bb9 --- /dev/null +++ b/feature/home/stability/home.stability @@ -0,0 +1,99 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :home:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.home.HandleHomeSideEffects(state: com.ninecraft.booket.feature.home.HomeUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +internal fun com.ninecraft.booket.feature.home.HomeContent(state: com.ninecraft.booket.feature.home.HomeUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.home.HomePresenter.present(): com.ninecraft.booket.feature.home.HomeUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.home.HomePreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.home.HomeUi(state: com.ninecraft.booket.feature.home.HomeUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.home.component.BookCard(recentBookInfo: com.ninecraft.booket.core.model.RecentBookModel, onBookDetailClick: kotlin.Function0, onRecordButtonClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - recentBookInfo: STABLE (marked @Stable or @Immutable) + - onBookDetailClick: STABLE (function type) + - onRecordButtonClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.home.component.BookCardPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.home.component.EmptyBookCard(onBookRegisterClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onBookRegisterClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.home.component.EmptyBookCardPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.home.component.HomeBanner(onBookRegisterClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onBookRegisterClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.home.component.HomeBannerPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.home.component.HomeHeader(onSettingsClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onSettingsClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.home.component.HomeHeaderPreview(): kotlin.Unit + skippable: true + restartable: true + params: + 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 712d21d0..e557f31c 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,7 +7,8 @@ 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.common.event.postLoginRequiredDialog +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.LibraryBookSummaryModel @@ -16,6 +17,7 @@ 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.LoginScreen import com.ninecraft.booket.feature.screens.SettingsScreen import com.ninecraft.booket.feature.screens.extensions.redirectToLogin import com.orhanobut.logger.Logger @@ -33,7 +35,6 @@ 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, @@ -105,6 +106,14 @@ class LibraryPresenter @AssistedInject constructor( } else { footerState = FooterState.Error(errorMessage) } + + handleException( + exception = exception, + onError = {}, + onLoginRequired = { + navigator.resetRoot(LoginScreen()) + }, + ) } } } @@ -117,10 +126,13 @@ class LibraryPresenter @AssistedInject constructor( is LibraryUiEvent.OnLibrarySearchClick -> { if (userState is UserState.Guest) { - scope.launch { - sideEffect = LibrarySideEffect.ShowToast(UiText.StringResource(designR.string.login_required)) - navigator.redirectToLogin() - } + postLoginRequiredDialog( + onConfirm = { + scope.launch { + navigator.redirectToLogin() + } + }, + ) } else { navigator.goTo(LibrarySearchScreen) } @@ -136,7 +148,10 @@ class LibraryPresenter @AssistedInject constructor( } currentFilter = event.filterOption - filterLibraryBooks(status = currentFilter.getApiValue(), page = START_INDEX, size = PAGE_SIZE) + + if (userState !is UserState.Guest) { + filterLibraryBooks(status = currentFilter.getApiValue(), page = START_INDEX, size = PAGE_SIZE) + } } is LibraryUiEvent.OnBookClick -> { 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 c69c74bd..aac44e11 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.extensions.toErrorType 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 @@ -33,11 +34,13 @@ import com.ninecraft.booket.feature.library.component.LibraryHeader import com.ninecraft.booket.feature.screens.LibraryScreen import com.ninecraft.booket.feature.screens.component.MainBottomBar import com.ninecraft.booket.feature.screens.component.MainTab +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +@TraceRecomposition @CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class) @Composable internal fun LibraryUi( @@ -83,6 +86,7 @@ internal fun LibraryUi( } } +@TraceRecomposition @Composable internal fun LibraryContent( state: LibraryUiState, @@ -177,7 +181,7 @@ internal fun LibraryContent( is UiState.Error -> { ReedErrorUi( - exception = state.uiState.exception, + errorType = state.uiState.exception.toErrorType(), onRetryClick = { state.eventSink(LibraryUiEvent.OnRetryClick) }, ) } @@ -186,6 +190,7 @@ internal fun LibraryContent( } } +@TraceRecomposition @Composable private fun EmptyResult() { Box( diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChip.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChip.kt index 0e9f1a65..08236f8d 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChip.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChip.kt @@ -20,7 +20,9 @@ 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.ninecraft.booket.feature.library.LibraryFilterOption +import com.skydoves.compose.stability.runtime.TraceRecomposition +@TraceRecomposition @Composable fun FilterChip( option: LibraryFilterOption, diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt index 6f7c2213..c0b0a975 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt @@ -13,9 +13,11 @@ import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.feature.library.LibraryFilterChip import com.ninecraft.booket.feature.library.LibraryFilterOption +import com.skydoves.compose.stability.runtime.TraceRecomposition import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList +@TraceRecomposition @Composable fun FilterChipGroup( filterList: ImmutableList, diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt index 12c6cff7..fc3afd3f 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/LibraryBookItem.kt @@ -1,5 +1,6 @@ package com.ninecraft.booket.feature.library.component +import androidx.compose.foundation.border import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -52,7 +53,12 @@ fun LibraryBookItem( ) .width(68.dp) .height(100.dp) - .clip(RoundedCornerShape(size = ReedTheme.radius.sm)), + .clip(RoundedCornerShape(size = ReedTheme.radius.sm)) + .border( + width = 1.dp, + color = ReedTheme.colors.borderPrimary, + shape = RoundedCornerShape(ReedTheme.radius.sm), + ), placeholder = painterResource(designR.drawable.ic_placeholder), ) Column(modifier = Modifier.weight(1f)) { diff --git a/feature/library/stability/library.stability b/feature/library/stability/library.stability new file mode 100644 index 00000000..edab8a55 --- /dev/null +++ b/feature/library/stability/library.stability @@ -0,0 +1,111 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :library:stabilityDump + +@Composable +private fun com.ninecraft.booket.feature.library.EmptyResult(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.library.HandleLibrarySideEffects(state: com.ninecraft.booket.feature.library.LibraryUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +internal fun com.ninecraft.booket.feature.library.LibraryContent(state: com.ninecraft.booket.feature.library.LibraryUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.library.LibraryPresenter.present(): com.ninecraft.booket.feature.library.LibraryUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.library.LibraryPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.library.LibraryUi(state: com.ninecraft.booket.feature.library.LibraryUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.library.component.ChipPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.library.component.FilterChip(option: com.ninecraft.booket.feature.library.LibraryFilterOption, count: kotlin.Int, isSelected: kotlin.Boolean, onChipClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - option: STABLE + - count: STABLE (primitive type) + - isSelected: STABLE (primitive type) + - onChipClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.library.component.FilterChipGroup(filterList: kotlinx.collections.immutable.ImmutableList, selectedChipOption: com.ninecraft.booket.feature.library.LibraryFilterOption, onChipClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - filterList: STABLE (known stable type) + - selectedChipOption: STABLE + - onChipClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.library.component.FilterChipGroupPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.library.component.LibraryBookItem(book: com.ninecraft.booket.core.model.LibraryBookSummaryModel, onBookClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - book: STABLE (marked @Stable or @Immutable) + - onBookClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.library.component.LibraryBookItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.library.component.LibraryHeader(onSearchClick: kotlin.Function0, onSettingClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onSearchClick: STABLE (function type) + - onSettingClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.library.component.LibraryHeaderPreview(): kotlin.Unit + skippable: true + restartable: true + params: + 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 f0f90cc6..0bb0de32 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 @@ -6,6 +6,8 @@ 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.constants.ErrorScope +import com.ninecraft.booket.core.common.event.postErrorDialog import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.feature.screens.HomeScreen @@ -53,13 +55,18 @@ class LoginPresenter @AssistedInject constructor( navigator.popUntil { it == screen.returnToScreen } } } else { - navigator.resetRoot(TermsAgreementScreen(screen.returnToScreen)) + if (screen.returnToScreen == null) { + navigator.resetRoot(TermsAgreementScreen()) + } else { + navigator.goTo(TermsAgreementScreen(screen.returnToScreen)) + } } }.onFailure { exception -> - exception.message?.let { Logger.e(it) } - sideEffect = exception.message?.let { - LoginSideEffect.ShowToast(it) - } + Logger.e(exception.message ?: "Failed to get user profile") + postErrorDialog( + errorScope = ErrorScope.LOGIN, + exception = exception, + ) } } } @@ -68,7 +75,7 @@ class LoginPresenter @AssistedInject constructor( when (event) { is LoginUiEvent.OnKakaoLoginButtonClick -> { isLoading = true - sideEffect = LoginSideEffect.KakaoLogin + sideEffect = LoginSideEffect.KakaoLogin() } is LoginUiEvent.LoginFailure -> { @@ -83,13 +90,15 @@ class LoginPresenter @AssistedInject constructor( isLoading = true authRepository.login(event.accessToken) .onSuccess { + userRepository.syncFcmToken() navigateAfterLogin() }.onFailure { exception -> - exception.message?.let { Logger.e(it) } + Logger.e(exception.message ?: "Login failed") analyticsHelper.logEvent(EVENT_ERROR_LOGIN) - sideEffect = exception.message?.let { - LoginSideEffect.ShowToast(it) - } + postErrorDialog( + errorScope = ErrorScope.LOGIN, + exception = exception, + ) } } finally { isLoading = false 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 359989bf..4e6bcd93 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 @@ -33,9 +33,11 @@ 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.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent +@TraceRecomposition @CircuitInject(LoginScreen::class, ActivityRetainedComponent::class) @Composable internal fun LoginUi( 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 731e2bcb..1f58e248 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 @@ -15,7 +15,7 @@ data class LoginUiState( @Immutable sealed interface LoginSideEffect { - data object KakaoLogin : LoginSideEffect + data class KakaoLogin(private val key: String = UUID.randomUUID().toString()) : LoginSideEffect data class ShowToast( val message: String, private val key: String = UUID.randomUUID().toString(), diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUi.kt index f017165f..2443ce0b 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementUi.kt @@ -29,10 +29,12 @@ import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.feature.login.R import com.ninecraft.booket.feature.screens.TermsAgreementScreen import com.ninecraft.booket.feature.termsagreement.component.TermItem +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.persistentListOf +@TraceRecomposition @CircuitInject(TermsAgreementScreen::class, ActivityRetainedComponent::class) @Composable internal fun TermsAgreementUi( diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/component/TermsItem.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/component/TermsItem.kt index f8f18caa..f163c417 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/component/TermsItem.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/component/TermsItem.kt @@ -17,8 +17,10 @@ import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyCheckBox import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @Composable internal fun TermItem( title: String, diff --git a/feature/login/src/main/res/drawable/img_reed_logo_big.webp b/feature/login/src/main/res/drawable/img_reed_logo_big.webp index 58cc510f..0618d818 100644 Binary files a/feature/login/src/main/res/drawable/img_reed_logo_big.webp and b/feature/login/src/main/res/drawable/img_reed_logo_big.webp differ diff --git a/feature/login/stability/login.stability b/feature/login/stability/login.stability new file mode 100644 index 00000000..0d5e7076 --- /dev/null +++ b/feature/login/stability/login.stability @@ -0,0 +1,79 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :login:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.login.HandleLoginSideEffects(state: com.ninecraft.booket.feature.login.LoginUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +public fun com.ninecraft.booket.feature.login.LoginPresenter.present(): com.ninecraft.booket.feature.login.LoginUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.login.LoginPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.login.LoginUi(state: com.ninecraft.booket.feature.login.LoginUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.termsagreement.HandleTermsAgreementSideEffects(state: com.ninecraft.booket.feature.termsagreement.TermsAgreementUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +public fun com.ninecraft.booket.feature.termsagreement.TermsAgreementPresenter.present(): com.ninecraft.booket.feature.termsagreement.TermsAgreementUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.termsagreement.TermsAgreementPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.termsagreement.TermsAgreementUi(state: com.ninecraft.booket.feature.termsagreement.TermsAgreementUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.termsagreement.component.TermItem(title: kotlin.String, onCheckClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier, checked: kotlin.Boolean, hasDetailAction: kotlin.Boolean, onDetailClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - title: STABLE (String is immutable) + - onCheckClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + - checked: STABLE (primitive type) + - hasDetailAction: STABLE (primitive type) + - onDetailClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.feature.termsagreement.component.TermItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt index 93ad9fcc..cdc440cb 100644 --- a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt +++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt @@ -11,11 +11,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import com.ninecraft.booket.core.common.constants.ErrorDialogSpec -import com.ninecraft.booket.core.common.event.ErrorEvent -import com.ninecraft.booket.core.common.event.ErrorEventHelper +import com.ninecraft.booket.core.common.event.DialogSpec +import com.ninecraft.booket.core.common.event.EventHelper +import com.ninecraft.booket.core.common.event.ReedEvent import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.ui.component.ReedDialog import com.ninecraft.booket.feature.screens.SplashScreen @@ -55,13 +54,13 @@ class MainActivity : ComponentActivity() { val backStack = rememberSaveableBackStack(root = SplashScreen) val navigator = rememberCircuitNavigator(backStack) - val dialogSpec = remember { mutableStateOf(null) } + val dialogSpec = remember { mutableStateOf(null) } - // 전역 에러 수신 + // 전역 이벤트 수신 LaunchedEffect(Unit) { - ErrorEventHelper.errorEvent.collect { event -> + EventHelper.eventFlow.collect { event -> when (event) { - is ErrorEvent.ShowDialog -> { + is ReedEvent.ShowDialog -> { dialogSpec.value = event.spec } } @@ -70,11 +69,12 @@ class MainActivity : ComponentActivity() { dialogSpec.value?.let { spec -> ReedDialog( + title = spec.title, description = spec.message, - confirmButtonText = stringResource(spec.buttonLabelResId), - + confirmButtonText = spec.confirmLabel, + dismissButtonText = spec.dismissLabel, onConfirmRequest = { - spec.action() + spec.onConfirm() dialogSpec.value = null }, onDismissRequest = { diff --git a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt index c32f2b80..444d9c4b 100644 --- a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt +++ b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt @@ -23,9 +23,11 @@ import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.feature.onboarding.component.OnboardingPage import com.ninecraft.booket.feature.onboarding.component.PagerIndicator import com.ninecraft.booket.feature.screens.OnboardingScreen +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent +@TraceRecomposition @CircuitInject(OnboardingScreen::class, ActivityRetainedComponent::class) @Composable internal fun OnboardingUi( diff --git a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/OnboardingPage.kt b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/OnboardingPage.kt index b596f463..0d451bd0 100644 --- a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/OnboardingPage.kt +++ b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/OnboardingPage.kt @@ -4,11 +4,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -25,16 +27,20 @@ internal fun OnboardingPage( titleRes: Int, highlightTextRes: Int, descriptionRes: Int, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.weight(1f)) Image( painter = painterResource(imageRes), contentDescription = "Onboarding Graphic", - modifier = Modifier.height(274.dp), + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .height(274.dp), ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) Text( diff --git a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/PagerIndicator.kt b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/PagerIndicator.kt index 20ef8632..19d70951 100644 --- a/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/PagerIndicator.kt +++ b/feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/component/PagerIndicator.kt @@ -18,7 +18,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.skydoves.compose.stability.runtime.TraceRecomposition +@TraceRecomposition @Composable internal fun PagerIndicator( pageCount: Int, diff --git a/feature/onboarding/src/main/res/drawable/img_onboarding_first.webp b/feature/onboarding/src/main/res/drawable/img_onboarding_first.webp index 58bcd004..08000efb 100644 Binary files a/feature/onboarding/src/main/res/drawable/img_onboarding_first.webp and b/feature/onboarding/src/main/res/drawable/img_onboarding_first.webp differ diff --git a/feature/onboarding/src/main/res/drawable/img_onboarding_second.webp b/feature/onboarding/src/main/res/drawable/img_onboarding_second.webp index a51b19be..2e88d60d 100644 Binary files a/feature/onboarding/src/main/res/drawable/img_onboarding_second.webp and b/feature/onboarding/src/main/res/drawable/img_onboarding_second.webp differ diff --git a/feature/onboarding/src/main/res/drawable/img_onboarding_third.webp b/feature/onboarding/src/main/res/drawable/img_onboarding_third.webp index 2cbbd72c..d401228d 100644 Binary files a/feature/onboarding/src/main/res/drawable/img_onboarding_third.webp and b/feature/onboarding/src/main/res/drawable/img_onboarding_third.webp differ diff --git a/feature/onboarding/stability/onboarding.stability b/feature/onboarding/stability/onboarding.stability new file mode 100644 index 00000000..f01b11fc --- /dev/null +++ b/feature/onboarding/stability/onboarding.stability @@ -0,0 +1,58 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :onboarding:stabilityDump + +@Composable +public fun com.ninecraft.booket.feature.onboarding.OnboardingPresenter.present(): com.ninecraft.booket.feature.onboarding.OnboardingUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.onboarding.OnboardingScreenPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.onboarding.OnboardingUi(state: com.ninecraft.booket.feature.onboarding.OnboardingUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.onboarding.component.OnboardingPage(imageRes: kotlin.Int, titleRes: kotlin.Int, highlightTextRes: kotlin.Int, descriptionRes: kotlin.Int, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - imageRes: STABLE (primitive type) + - titleRes: STABLE (primitive type) + - highlightTextRes: STABLE (primitive type) + - descriptionRes: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.onboarding.component.OnboardingPagePreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.onboarding.component.PagerIndicator(pageCount: kotlin.Int, pagerState: androidx.compose.foundation.pager.PagerState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - pageCount: STABLE (primitive type) + - pagerState: STABLE (marked @Stable or @Immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.onboarding.component.PagerIndicatorPreview(): kotlin.Unit + skippable: true + restartable: true + params: + diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt index a97890b6..599b6dbd 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt @@ -28,10 +28,12 @@ import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.ui.component.ReedBottomSheet import com.ninecraft.booket.feature.record.R +import com.skydoves.compose.stability.runtime.TraceRecomposition import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @Composable fun ImpressionGuideBottomSheet( diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt index bc0f85b0..641d2cde 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt @@ -20,7 +20,9 @@ import com.ninecraft.booket.core.designsystem.theme.Blank import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.feature.record.R +import com.skydoves.compose.stability.runtime.TraceRecomposition +@TraceRecomposition @Composable fun ImpressionGuideBox( onClick: () -> Unit, 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 7ddb581d..84389369 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 @@ -6,9 +6,9 @@ import androidx.compose.runtime.getValue 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.handleException 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 import com.slack.circuit.codegen.annotations.CircuitInject @@ -21,7 +21,9 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.launch class OcrPresenter @AssistedInject constructor( @@ -40,8 +42,7 @@ class OcrPresenter @AssistedInject constructor( var currentUi by rememberRetained { mutableStateOf(OcrUi.CAMERA) } var isPermissionDialogVisible by rememberRetained { mutableStateOf(false) } var sentenceList by rememberRetained { mutableStateOf(persistentListOf()) } - var recognizedText by rememberRetained { mutableStateOf("") } - var selectedIndices by rememberRetained { mutableStateOf(setOf()) } + var selectedIndices by rememberRetained { mutableStateOf(persistentSetOf()) } var mergedSentence by rememberRetained { mutableStateOf("") } var isTextDetectionFailed by rememberRetained { mutableStateOf(false) } var isRecaptureDialogVisible by rememberRetained { mutableStateOf(false) } @@ -55,7 +56,6 @@ class OcrPresenter @AssistedInject constructor( recognizer.recognizeText(imageUri) .onSuccess { val text = it.responses.firstOrNull()?.fullTextAnnotation?.text.orEmpty() - recognizedText = text if (text.isNotBlank()) { isTextDetectionFailed = false @@ -136,11 +136,11 @@ class OcrPresenter @AssistedInject constructor( selectedIndices - event.index } else { selectedIndices + event.index - } + }.toPersistentSet() } is OcrUiEvent.OnRecaptureDialogConfirmed -> { - selectedIndices = emptySet() + selectedIndices = persistentSetOf() isRecaptureDialogVisible = false currentUi = OcrUi.CAMERA } 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 76af3913..82b9c56c 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 @@ -68,12 +68,14 @@ 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.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import tech.thdev.compose.exteions.system.ui.controller.rememberSystemUiController import java.io.File import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @CircuitInject(OcrScreen::class, ActivityRetainedComponent::class) @Composable internal fun OcrUi( @@ -88,6 +90,7 @@ internal fun OcrUi( } } +@TraceRecomposition @Composable private fun CameraPreview( state: OcrUiState, @@ -312,6 +315,7 @@ private fun CameraPreview( } } +@TraceRecomposition @Composable private fun TextScanResult( state: OcrUiState, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUiState.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUiState.kt index 812fc57e..7b932e0b 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUiState.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrUiState.kt @@ -5,14 +5,16 @@ import androidx.compose.runtime.Immutable import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf import java.util.UUID data class OcrUiState( val currentUi: OcrUi = OcrUi.CAMERA, val isPermissionDialogVisible: Boolean = false, val sentenceList: ImmutableList = persistentListOf(), - val selectedIndices: Set = emptySet(), + val selectedIndices: ImmutableSet = persistentSetOf(), val isTextDetectionFailed: Boolean = false, val isRecaptureDialogVisible: Boolean = false, val isLoading: Boolean = false, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt index 07b39d8f..ddc188bf 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt @@ -15,7 +15,9 @@ import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.skydoves.compose.stability.runtime.TraceRecomposition +@TraceRecomposition @Composable fun SentenceBox( onClick: () -> Unit, @@ -25,7 +27,6 @@ fun SentenceBox( ) { val bgColor = if (isSelected) ReedTheme.colors.bgTertiary else ReedTheme.colors.bgSecondary val borderColor = if (isSelected) ReedTheme.colors.borderBrand else Color.Transparent - val textColor = if (isSelected) ReedTheme.colors.contentBrand else ReedTheme.colors.contentPrimary val textStyle = if (isSelected) ReedTheme.typography.body1Medium else ReedTheme.typography.body1Regular Box( @@ -51,7 +52,7 @@ fun SentenceBox( ) { Text( text = sentence, - color = textColor, + color = ReedTheme.colors.contentPrimary, style = textStyle, ) } 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 26a58a92..16abd5ca 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 @@ -13,8 +13,8 @@ import androidx.compose.ui.text.TextRange import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.RecordRepository -import com.ninecraft.booket.core.designsystem.EmotionTag import com.ninecraft.booket.core.designsystem.RecordStep +import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.OcrScreen import com.ninecraft.booket.feature.screens.RecordDetailScreen @@ -73,8 +73,8 @@ class RecordRegisterPresenter @AssistedInject constructor( ).toPersistentList(), ) } - val emotionTags by rememberRetained { mutableStateOf(EmotionTag.entries.toPersistentList()) } - var selectedEmotion by rememberRetained { mutableStateOf(null) } + val emotions by rememberRetained { mutableStateOf(Emotion.entries.toPersistentList()) } + var selectedEmotion by rememberRetained { mutableStateOf(null) } var selectedImpressionGuide by rememberRetained { mutableStateOf("") } var beforeSelectedImpressionGuide by rememberRetained { mutableStateOf(selectedImpressionGuide) } val impressionState = rememberTextFieldState() @@ -99,9 +99,7 @@ class RecordRegisterPresenter @AssistedInject constructor( selectedEmotion != null } - RecordStep.IMPRESSION -> { - impressionState.text.isNotEmpty() - } + RecordStep.IMPRESSION -> true } } } @@ -256,7 +254,7 @@ class RecordRegisterPresenter @AssistedInject constructor( userBookId = screen.userBookId, pageNumber = recordPageState.text.toString().toIntOrNull() ?: 0, quote = recordSentenceState.text.toString(), - emotionTags = selectedEmotion?.let { listOf(it.label) } ?: emptyList(), + emotionTags = selectedEmotion?.let { listOf(it.displayName) } ?: emptyList(), impression = impressionState.text.toString(), ) } @@ -294,7 +292,7 @@ class RecordRegisterPresenter @AssistedInject constructor( recordPageState = recordPageState, recordSentenceState = recordSentenceState, isPageError = isPageError, - emotionTags = emotionTags, + emotions = emotions, selectedEmotion = selectedEmotion, impressionState = impressionState, impressionGuideList = impressionGuideList, 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 ac3a7278..255338ce 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 @@ -31,9 +31,11 @@ import com.ninecraft.booket.feature.record.step.EmotionStep import com.ninecraft.booket.feature.record.step.ImpressionStep import com.ninecraft.booket.feature.record.step.QuoteStep import com.ninecraft.booket.feature.screens.RecordScreen +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent +@TraceRecomposition @CircuitInject(RecordScreen::class, ActivityRetainedComponent::class) @Composable internal fun RecordRegisterUi( diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt index 9641f987..16732566 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt @@ -2,8 +2,8 @@ package com.ninecraft.booket.feature.record.register import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable -import com.ninecraft.booket.core.designsystem.EmotionTag import com.ninecraft.booket.core.designsystem.RecordStep +import com.ninecraft.booket.core.model.Emotion import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import kotlinx.collections.immutable.ImmutableList @@ -16,8 +16,8 @@ data class RecordRegisterUiState( val recordPageState: TextFieldState = TextFieldState(), val recordSentenceState: TextFieldState = TextFieldState(), val isPageError: Boolean = false, - val emotionTags: ImmutableList = persistentListOf(), - val selectedEmotion: EmotionTag? = null, + val emotions: ImmutableList = persistentListOf(), + val selectedEmotion: Emotion? = null, val impressionState: TextFieldState = TextFieldState(), val impressionGuideList: ImmutableList = persistentListOf(), val selectedImpressionGuide: String = "", @@ -46,7 +46,7 @@ sealed interface RecordRegisterUiEvent : CircuitUiEvent { data object OnClearClick : RecordRegisterUiEvent data object OnNextButtonClick : RecordRegisterUiEvent data object OnSentenceScanButtonClick : RecordRegisterUiEvent - data class OnSelectEmotion(val emotion: EmotionTag) : RecordRegisterUiEvent + data class OnSelectEmotion(val emotion: Emotion) : RecordRegisterUiEvent data object OnImpressionGuideButtonClick : RecordRegisterUiEvent data object OnImpressionGuideBottomSheetDismiss : RecordRegisterUiEvent data class OnSelectImpressionGuide(val index: Int) : RecordRegisterUiEvent 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 36b7b33e..c6b97c24 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 @@ -26,23 +26,26 @@ import androidx.compose.ui.res.stringResource 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.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.graphicRes import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.feature.record.R import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent import com.ninecraft.booket.feature.record.register.RecordRegisterUiState +import com.skydoves.compose.stability.runtime.TraceRecomposition import kotlinx.collections.immutable.toPersistentList +@TraceRecomposition @Composable fun EmotionStep( state: RecordRegisterUiState, modifier: Modifier = Modifier, ) { - val emotionPairs = remember(state.emotionTags) { state.emotionTags.chunked(2) } + val emotionPairs = remember(state.emotions) { state.emotions.chunked(2) } Box( modifier = modifier @@ -83,7 +86,7 @@ fun EmotionStep( ) { pair.forEach { tag -> EmotionItem( - emotionTag = tag, + emotion = tag, onClick = { state.eventSink(RecordRegisterUiEvent.OnSelectEmotion(tag)) }, @@ -111,7 +114,7 @@ fun EmotionStep( .padding(horizontal = ReedTheme.spacing.spacing5) .padding(bottom = ReedTheme.spacing.spacing4), enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_next_button), + text = stringResource(R.string.record_next_button_text), multipleEventsCutterEnabled = false, ) } @@ -119,7 +122,7 @@ fun EmotionStep( @Composable private fun EmotionItem( - emotionTag: EmotionTag, + emotion: Emotion, onClick: () -> Unit, isSelected: Boolean, modifier: Modifier = Modifier, @@ -146,7 +149,7 @@ private fun EmotionItem( contentAlignment = Alignment.Center, ) { Image( - painter = painterResource(emotionTag.graphic), + painter = painterResource(emotion.graphicRes), contentDescription = "Emotion Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, @@ -157,12 +160,12 @@ private fun EmotionItem( @ComponentPreview @Composable private fun RecordRegisterPreview() { - val emotionTags = EmotionTag.entries.toPersistentList() + val emotions = Emotion.entries.toPersistentList() ReedTheme { EmotionStep( state = RecordRegisterUiState( - emotionTags = emotionTags, + emotions = emotions, eventSink = {}, ), ) 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 948ac648..7d219f97 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 @@ -2,7 +2,9 @@ package com.ninecraft.booket.feature.record.step 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.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -10,9 +12,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -28,6 +32,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged @@ -50,11 +55,13 @@ import com.ninecraft.booket.feature.record.component.CustomTooltipBox import com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheet import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent import com.ninecraft.booket.feature.record.register.RecordRegisterUiState +import com.skydoves.compose.stability.runtime.TraceRecomposition import kotlinx.coroutines.delay import kotlinx.coroutines.launch import tech.thdev.compose.extensions.keyboard.state.foundation.rememberKeyboardVisible import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @Composable fun ImpressionStep( @@ -90,11 +97,34 @@ fun ImpressionStep( .padding(bottom = 16.dp) .verticalScroll(scrollState), ) { - Text( - text = stringResource(R.string.impression_step_title), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.heading1Bold, - ) + FlowRow( + itemVerticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.impression_step_title), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.heading1Bold, + ) + Spacer(modifier = Modifier.width(10.dp)) + Box( + modifier = Modifier + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(ReedTheme.colors.bgTertiary), + ) { + Text( + text = stringResource(R.string.select), + modifier = Modifier.padding( + start = ReedTheme.spacing.spacing2, + top = ReedTheme.spacing.spacing05, + end = ReedTheme.spacing.spacing2, + bottom = ReedTheme.spacing.spacing05, + ), + color = ReedTheme.colors.contentBrand, + style = ReedTheme.typography.caption1Medium, + ) + } + } Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) Text( text = stringResource(R.string.impression_step_description), @@ -161,7 +191,7 @@ fun ImpressionStep( vertical = ReedTheme.spacing.spacing4, ), enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_next_button), + text = stringResource(R.string.record_finish_button_text), multipleEventsCutterEnabled = true, ) } 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 b9318765..13f85de0 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 @@ -48,10 +48,12 @@ import com.ninecraft.booket.feature.record.R import com.ninecraft.booket.feature.record.component.CustomTooltipBox import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent import com.ninecraft.booket.feature.record.register.RecordRegisterUiState +import com.skydoves.compose.stability.runtime.TraceRecomposition import kotlinx.coroutines.delay import tech.thdev.compose.extensions.keyboard.state.foundation.rememberKeyboardVisible import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @Composable internal fun QuoteStep( state: RecordRegisterUiState, @@ -176,7 +178,7 @@ internal fun QuoteStep( vertical = ReedTheme.spacing.spacing4, ), enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_next_button), + text = stringResource(R.string.record_next_button_text), multipleEventsCutterEnabled = false, ) } diff --git a/feature/record/src/main/res/values/strings.xml b/feature/record/src/main/res/values/strings.xml index 093c7da1..c24a72f7 100644 --- a/feature/record/src/main/res/values/strings.xml +++ b/feature/record/src/main/res/values/strings.xml @@ -16,7 +16,8 @@ 카메라 권한이 필요해요 문장 인식을 위해 설정에서 권한을 허용해주세요. 설정으로 이동하기 - 다음 + 다음 + 기록 완료 기록하고 싶은 페이지와\n문장을 등록해보세요 책 페이지 문장 기록 @@ -26,7 +27,7 @@ 문장에 대해 어떤 감정이 드셨나요? 대표 감정을 한 가지 선택해주세요 문장에 대한 감상을 남겨주세요 - 감상평 가이드로 쉽게 남길 수 있어요 + 떠오르는 생각이 있다면 자유롭게 작성해 주세요. 내용을 입력해주세요. 감상평 가이드 아래 문장 중 하나를 선택해 이어서 감상을 적어보세요 @@ -40,4 +41,5 @@ 해당 책의 마지막 페이지 수를 초과했습니다 예시 문장을 알려드려요 스캔으로 빠르게 입력해요 + 선택 diff --git a/feature/record/stability/record.stability b/feature/record/stability/record.stability new file mode 100644 index 00000000..21fb836f --- /dev/null +++ b/feature/record/stability/record.stability @@ -0,0 +1,213 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :record:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.record.component.CustomTooltipBox(messageResId: kotlin.Int): kotlin.Unit + skippable: true + restartable: true + params: + - messageResId: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.feature.record.component.CustomTooltipBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, impressionState: androidx.compose.foundation.text.input.TextFieldState, impressionGuideList: kotlinx.collections.immutable.ImmutableList, beforeSelectedImpressionGuide: kotlin.String, selectedImpressionGuide: kotlin.String, onGuideClick: kotlin.Function1, onCloseButtonClick: kotlin.Function0, onSelectionConfirmButtonClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - impressionState: STABLE (marked @Stable or @Immutable) + - impressionGuideList: STABLE (known stable type) + - beforeSelectedImpressionGuide: STABLE (String is immutable) + - selectedImpressionGuide: STABLE (String is immutable) + - onGuideClick: STABLE (function type) + - onCloseButtonClick: STABLE (function type) + - onSelectionConfirmButtonClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.record.component.ImpressionGuideBox(onClick: kotlin.Function0, impressionText: kotlin.String, modifier: androidx.compose.ui.Modifier, isSelected: kotlin.Boolean): kotlin.Unit + skippable: true + restartable: true + params: + - onClick: STABLE (function type) + - impressionText: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - isSelected: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.feature.record.component.ImpressionGuideBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.record.ocr.CameraPreview(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.ocr.CameraPreviewPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.ocr.HandleOcrSideEffects(state: com.ninecraft.booket.feature.record.ocr.OcrUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +public fun com.ninecraft.booket.feature.record.ocr.OcrPresenter.present(): com.ninecraft.booket.feature.record.ocr.OcrUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.ocr.OcrUi(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.ocr.TextRecognitionResultPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.record.ocr.TextScanResult(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.record.ocr.component.CameraFrame(modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.ocr.component.CameraFramePreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.record.ocr.component.SentenceBox(onClick: kotlin.Function0, sentence: kotlin.String, modifier: androidx.compose.ui.Modifier, isSelected: kotlin.Boolean): kotlin.Unit + skippable: true + restartable: true + params: + - onClick: STABLE (function type) + - sentence: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - isSelected: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.feature.record.ocr.component.SentenceBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.register.HandleRecordRegisterSideEffects(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +public fun com.ninecraft.booket.feature.record.register.RecordRegisterPresenter.present(): com.ninecraft.booket.feature.record.register.RecordRegisterUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.record.register.RecordRegisterPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.register.RecordRegisterUi(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.step.EmotionItem(emotionTag: com.ninecraft.booket.core.designsystem.EmotionTag, onClick: kotlin.Function0, isSelected: kotlin.Boolean, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - emotionTag: STABLE + - onClick: STABLE (function type) + - isSelected: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.record.step.EmotionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.record.step.ImpressionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.step.ImpressionStepPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.step.QuoteStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.step.QuoteStepPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.record.step.RecordRegisterPreview(): kotlin.Unit + skippable: true + restartable: true + params: + 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 05f01d7b..2d057b3b 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 @@ -33,6 +33,9 @@ data object SettingsScreen : ReedScreen(name = ScreenNames.SETTINGS) @Parcelize data object OssLicensesScreen : ReedScreen(name = "OssLicenses()") +@Parcelize +data object NotificationScreen : ReedScreen(name = "Notification()") + @Parcelize data class RecordScreen(val userBookId: String) : ReedScreen(name = ScreenNames.RECORD) @@ -76,5 +79,5 @@ data object SplashScreen : ReedScreen(name = ScreenNames.SPLASH) data class RecordCardScreen( val quote: String, val bookTitle: String, - val emotionTag: String, + val emotion: String, ) : ReedScreen(name = ScreenNames.RECORD_CARD) diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt index c4917736..d068dd9b 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt @@ -1,8 +1,10 @@ package com.ninecraft.booket.feature.screens.arguments import android.os.Parcelable +import androidx.compose.runtime.Immutable import kotlinx.parcelize.Parcelize +@Immutable @Parcelize data class RecordEditArgs( val id: String, diff --git a/feature/screens/stability/screens.stability b/feature/screens/stability/screens.stability new file mode 100644 index 00000000..aaa1f6b6 --- /dev/null +++ b/feature/screens/stability/screens.stability @@ -0,0 +1,31 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :screens:stabilityDump + +@Composable +public fun com.ninecraft.booket.feature.screens.component.MainBottomBar(tabs: kotlinx.collections.immutable.ImmutableList, currentTab: com.ninecraft.booket.feature.screens.component.MainTab?, onTabSelected: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - tabs: STABLE (known stable type) + - currentTab: STABLE + - onTabSelected: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.screens.component.MainBottomBarItem(tab: com.ninecraft.booket.feature.screens.component.MainTab, selected: kotlin.Boolean, onClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - tab: STABLE + - selected: STABLE (primitive type) + - onClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.feature.screens.component.MainBottomBarPreview(): kotlin.Unit + skippable: true + restartable: true + params: + 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 d6e890ec..e78e03f7 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,6 +10,7 @@ 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.event.postLoginRequiredDialog 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 @@ -38,7 +39,6 @@ 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, @@ -71,6 +71,7 @@ class BookSearchPresenter @AssistedInject constructor( var registeredUserBookId by rememberRetained { mutableStateOf("") } var isBookRegisterBottomSheetVisible by rememberRetained { mutableStateOf(false) } var selectedBookStatus by rememberRetained { mutableStateOf(null) } + var upsertedBookStatus by rememberRetained { mutableStateOf(null) } var isBookRegisterSuccessBottomSheetVisible by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } @@ -119,13 +120,18 @@ 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 - } + if (userState is UserState.Guest) { + postLoginRequiredDialog( + onConfirm = { + scope.launch { + navigator.redirectToLogin() + } + }, + ) + return + } + scope.launch { repository.upsertBook(isbn13, bookStatus) .onSuccess { registeredUserBookId = it.userBookId @@ -137,6 +143,7 @@ class BookSearchPresenter @AssistedInject constructor( analyticsHelper.logEvent(REGISTER_BOOK_COMPLETE) selectedBookIsbn = "" + upsertedBookStatus = selectedBookStatus selectedBookStatus = null isBookRegisterBottomSheetVisible = false isBookRegisterSuccessBottomSheetVisible = true @@ -262,6 +269,7 @@ class BookSearchPresenter @AssistedInject constructor( selectedBookIsbn = selectedBookIsbn, isBookRegisterBottomSheetVisible = isBookRegisterBottomSheetVisible, selectedBookStatus = selectedBookStatus, + upsertedBookStatus = upsertedBookStatus, isBookRegisterSuccessBottomSheetVisible = isBookRegisterSuccessBottomSheetVisible, isGuestMode = userState is UserState.Guest, sideEffect = sideEffect, 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 ad1fd7ec..5904f258 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 @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.common.constants.BookStatus +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.component.ReedDivider import com.ninecraft.booket.core.designsystem.component.textfield.ReedTextField @@ -39,12 +40,14 @@ import com.ninecraft.booket.feature.search.book.component.BookRegisterBottomShee import com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessBottomSheet import com.ninecraft.booket.feature.search.common.component.RecentSearchTitle import com.ninecraft.booket.feature.search.common.component.SearchItem +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @CircuitInject(BookSearchScreen::class, ActivityRetainedComponent::class) @Composable internal fun BookSearchUi( @@ -79,6 +82,7 @@ internal fun BookSearchUi( } } +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun BookSearchContent( @@ -117,7 +121,7 @@ internal fun BookSearchContent( is UiState.Error -> { ReedErrorUi( - exception = state.uiState.exception, + errorType = state.uiState.exception.toErrorType(), onRetryClick = { state.eventSink(BookSearchUiEvent.OnRetryClick) }, ) } @@ -265,22 +269,25 @@ internal fun BookSearchContent( } if (state.isBookRegisterSuccessBottomSheetVisible) { - BookRegisterSuccessBottomSheet( - onDismissRequest = { state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessBottomSheetDismiss) }, - sheetState = bookRegisterSuccessBottomSheetState, - onCancelButtonClick = { - coroutineScope.launch { - bookRegisterSuccessBottomSheetState.hide() - state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessBottomSheetDismiss) - } - }, - onOKButtonClick = { - coroutineScope.launch { - bookRegisterSuccessBottomSheetState.hide() - state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick) - } - }, - ) + state.upsertedBookStatus?.let { upsertedBookStatus -> + BookRegisterSuccessBottomSheet( + onDismissRequest = { state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessBottomSheetDismiss) }, + sheetState = bookRegisterSuccessBottomSheetState, + upsertedBookStatus = upsertedBookStatus, + onCancelButtonClick = { + coroutineScope.launch { + bookRegisterSuccessBottomSheetState.hide() + state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessBottomSheetDismiss) + } + }, + onOKButtonClick = { + coroutineScope.launch { + bookRegisterSuccessBottomSheetState.hide() + state.eventSink(BookSearchUiEvent.OnBookRegisterSuccessOkButtonClick) + } + }, + ) + } } } } 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 09641b05..17e15a45 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 @@ -31,6 +31,7 @@ data class BookSearchUiState( val selectedBookIsbn: String = "", val isBookRegisterBottomSheetVisible: Boolean = false, val selectedBookStatus: BookStatus? = null, + val upsertedBookStatus: BookStatus? = null, val isBookRegisterSuccessBottomSheetVisible: Boolean = false, val isGuestMode: Boolean = false, val sideEffect: BookSearchSideEffect? = null, diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookItem.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookItem.kt index 59dcdba8..3db9ef4e 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookItem.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookItem.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.feature.search.book.component import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -31,8 +32,10 @@ import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.core.model.BookSummaryModel import com.ninecraft.booket.feature.search.R +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @Composable fun BookItem( book: BookSummaryModel, @@ -63,7 +66,12 @@ fun BookItem( ) .width(68.dp) .height(100.dp) - .clip(RoundedCornerShape(size = ReedTheme.radius.sm)), + .clip(RoundedCornerShape(size = ReedTheme.radius.sm)) + .border( + width = 1.dp, + color = ReedTheme.colors.borderPrimary, + shape = RoundedCornerShape(ReedTheme.radius.sm), + ), ) { NetworkImage( imageUrl = book.coverImageUrl, diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookRegisterSuccessBottomSheet.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookRegisterSuccessBottomSheet.kt index dd34726a..6e6057a5 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookRegisterSuccessBottomSheet.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/component/BookRegisterSuccessBottomSheet.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.constants.BookStatus import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle @@ -33,8 +34,10 @@ import com.ninecraft.booket.feature.search.R fun BookRegisterSuccessBottomSheet( onDismissRequest: () -> Unit, sheetState: SheetState, + upsertedBookStatus: BookStatus, onCancelButtonClick: () -> Unit, onOKButtonClick: () -> Unit, + modifier: Modifier = Modifier, ) { ReedBottomSheet( onDismissRequest = { @@ -43,7 +46,7 @@ fun BookRegisterSuccessBottomSheet( sheetState = sheetState, ) { Column( - modifier = Modifier + modifier = modifier .padding( start = ReedTheme.spacing.spacing5, top = ReedTheme.spacing.spacing5, @@ -67,38 +70,63 @@ fun BookRegisterSuccessBottomSheet( ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) Text( - text = stringResource(R.string.book_register_success_description), + text = stringResource( + when (upsertedBookStatus) { + BookStatus.BEFORE_READING -> R.string.book_register_success_description_before_reading + BookStatus.READING -> R.string.book_register_success_description + BookStatus.COMPLETED -> R.string.book_register_success_description_completed + }, + ), modifier = Modifier.fillMaxWidth(), color = ReedTheme.colors.contentSecondary, textAlign = TextAlign.Center, style = ReedTheme.typography.body1Medium, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = ReedTheme.spacing.spacing4), - horizontalArrangement = Arrangement.SpaceBetween, - ) { + + if (upsertedBookStatus == BookStatus.BEFORE_READING) { ReedButton( onClick = { onCancelButtonClick() }, sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.SECONDARY, - modifier = Modifier.weight(1f), - text = stringResource(R.string.book_register_success_cancel), - ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) - ReedButton( - onClick = { - onOKButtonClick() - }, - sizeStyle = largeButtonStyle, colorStyle = ReedButtonColorStyle.PRIMARY, - modifier = Modifier.weight(1f), - text = stringResource(R.string.book_register_success_ok), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ReedTheme.spacing.spacing4), + text = stringResource(R.string.book_register_success_ok_before_reading), ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ReedTheme.spacing.spacing4), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ReedButton( + onClick = { + onCancelButtonClick() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.SECONDARY, + modifier = Modifier.weight(1f), + text = stringResource(R.string.book_register_success_cancel), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + ReedButton( + onClick = { + onOKButtonClick() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.PRIMARY, + modifier = Modifier.weight(1f), + text = if (upsertedBookStatus == BookStatus.READING) { + stringResource(R.string.book_register_success_ok) + } else { + stringResource(R.string.book_register_success_ok_completed) + }, + ) + } } } } @@ -107,7 +135,49 @@ fun BookRegisterSuccessBottomSheet( @OptIn(ExperimentalMaterial3Api::class) @ComponentPreview @Composable -private fun BookRegisterSuccessBottomSheetPreview() { +private fun BookRegisterSuccessBeforeReadingBottomSheetPreview() { + val sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + ReedTheme { + BookRegisterSuccessBottomSheet( + onDismissRequest = {}, + sheetState = sheetState, + upsertedBookStatus = BookStatus.BEFORE_READING, + onCancelButtonClick = {}, + onOKButtonClick = {}, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@ComponentPreview +@Composable +private fun BookRegisterSuccessReadingBottomSheetPreview() { + val sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + ReedTheme { + BookRegisterSuccessBottomSheet( + onDismissRequest = {}, + sheetState = sheetState, + upsertedBookStatus = BookStatus.READING, + onCancelButtonClick = {}, + onOKButtonClick = {}, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@ComponentPreview +@Composable +private fun BookRegisterSuccessCompletedBottomSheetPreview() { val sheetState = SheetState( skipPartiallyExpanded = true, initialValue = SheetValue.Expanded, @@ -118,6 +188,7 @@ private fun BookRegisterSuccessBottomSheetPreview() { BookRegisterSuccessBottomSheet( onDismissRequest = {}, sheetState = sheetState, + upsertedBookStatus = BookStatus.COMPLETED, onCancelButtonClick = {}, onOKButtonClick = {}, ) diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt index 148c12fc..227d3ad0 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.component.ReedDivider import com.ninecraft.booket.core.designsystem.component.textfield.ReedTextField @@ -32,9 +33,11 @@ import com.ninecraft.booket.feature.search.R import com.ninecraft.booket.feature.search.common.component.RecentSearchTitle import com.ninecraft.booket.feature.search.common.component.SearchItem import com.ninecraft.booket.feature.search.library.component.LibraryBookItem +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent +@TraceRecomposition @CircuitInject(LibrarySearchScreen::class, ActivityRetainedComponent::class) @Composable internal fun LibrarySearchUi( @@ -54,6 +57,7 @@ internal fun LibrarySearchUi( } } +@TraceRecomposition @Composable internal fun LibrarySearchContent( state: LibrarySearchUiState, @@ -96,7 +100,7 @@ internal fun LibrarySearchContent( is UiState.Error -> { ReedErrorUi( - exception = state.uiState.exception, + errorType = state.uiState.exception.toErrorType(), onRetryClick = { state.eventSink(LibrarySearchUiEvent.OnRetryClick) }, ) } diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt index 96b576a0..3df1d404 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt @@ -28,6 +28,7 @@ data class LibrarySearchUiState( val eventSink: (LibrarySearchUiEvent) -> Unit, ) : CircuitUiState +@Immutable sealed interface LibrarySearchSideEffect { data class ShowToast( val message: String, diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/component/LibraryBookItem.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/component/LibraryBookItem.kt index 8b30433a..14265931 100644 --- a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/component/LibraryBookItem.kt +++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/component/LibraryBookItem.kt @@ -1,5 +1,6 @@ package com.ninecraft.booket.feature.search.library.component +import androidx.compose.foundation.border import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -52,7 +53,12 @@ fun LibraryBookItem( ) .width(68.dp) .height(100.dp) - .clip(RoundedCornerShape(size = ReedTheme.radius.sm)), + .clip(RoundedCornerShape(size = ReedTheme.radius.sm)) + .border( + width = 1.dp, + color = ReedTheme.colors.borderPrimary, + shape = RoundedCornerShape(ReedTheme.radius.sm), + ), placeholder = painterResource(designR.drawable.ic_placeholder), ) Column(modifier = Modifier.weight(1f)) { diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml index d8f946d7..c27ec69f 100644 --- a/feature/search/src/main/res/values/strings.xml +++ b/feature/search/src/main/res/values/strings.xml @@ -9,8 +9,12 @@ 등록 옵션 도서가 등록되었어요! 독서 기록을 바로 시작할까요? + 책을 읽으면서 독서 기록을 남길 수 있어요 + 기억에 남는 문장이나 감상을 기록해보세요 나중에 하기 기록 시작하기 + 확인 + 기록 남기기 도서 등록 최근 검색어 내역이 없습니다. 이미 등록된 책입니다 diff --git a/feature/search/stability/search.stability b/feature/search/stability/search.stability new file mode 100644 index 00000000..b2786505 --- /dev/null +++ b/feature/search/stability/search.stability @@ -0,0 +1,198 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :search:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.search.book.BookSearchContent(state: com.ninecraft.booket.feature.search.book.BookSearchUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.search.book.BookSearchPresenter.present(): com.ninecraft.booket.feature.search.book.BookSearchUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.search.book.BookSearchPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.search.book.BookSearchUi(state: com.ninecraft.booket.feature.search.book.BookSearchUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.search.book.HandleBookSearchSideEffects(state: com.ninecraft.booket.feature.search.book.BookSearchUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +public fun com.ninecraft.booket.feature.search.book.component.BookItem(book: com.ninecraft.booket.core.model.BookSummaryModel, onBookClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean): kotlin.Unit + skippable: true + restartable: true + params: + - book: STABLE (marked @Stable or @Immutable) + - onBookClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + - enabled: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.feature.search.book.component.BookItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.search.book.component.BookRegisterBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, bookStatuses: kotlinx.collections.immutable.ImmutableList, currentBookStatus: com.ninecraft.booket.core.common.constants.BookStatus?, onItemSelected: kotlin.Function1, onBookRegisterButtonClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onCloseButtonClick: STABLE (function type) + - bookStatuses: STABLE (known stable type) + - currentBookStatus: STABLE + - onItemSelected: STABLE (function type) + - onBookRegisterButtonClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.search.book.component.BookRegisterBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessBeforeReadingBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, upsertedBookStatus: com.ninecraft.booket.core.common.constants.BookStatus, onCancelButtonClick: kotlin.Function0, onOKButtonClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - upsertedBookStatus: STABLE + - onCancelButtonClick: STABLE (function type) + - onOKButtonClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessCompletedBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.search.book.component.BookRegisterSuccessReadingBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.search.book.component.BookStatusItem(item: com.ninecraft.booket.core.common.constants.BookStatus, selected: kotlin.Boolean, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - item: STABLE + - selected: STABLE (primitive type) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.search.common.component.RecentSearchTitle(modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.search.common.component.RecentSearchTitlePreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.search.common.component.SearchItem(query: kotlin.String, onQueryClick: kotlin.Function1, onDeleteIconClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - query: STABLE (String is immutable) + - onQueryClick: STABLE (function type) + - onDeleteIconClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.search.common.component.SearchItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.search.library.HandlingLibrarySearchSideEffect(state: com.ninecraft.booket.feature.search.library.LibrarySearchUiState): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + +@Composable +internal fun com.ninecraft.booket.feature.search.library.LibrarySearchContent(state: com.ninecraft.booket.feature.search.library.LibrarySearchUiState, innerPadding: androidx.compose.foundation.layout.PaddingValues, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - innerPadding: STABLE (marked @Stable or @Immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.search.library.LibrarySearchPresenter.present(): com.ninecraft.booket.feature.search.library.LibrarySearchUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.search.library.LibrarySearchPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.search.library.LibrarySearchUi(state: com.ninecraft.booket.feature.search.library.LibrarySearchUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.search.library.component.LibraryBookItem(book: com.ninecraft.booket.core.model.LibraryBookSummaryModel, onBookClick: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - book: STABLE (marked @Stable or @Immutable) + - onBookClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.search.library.component.LibraryBookItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 456eaf80..9ecba147 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -17,5 +17,7 @@ ksp { dependencies { implementations( libs.logger, + + libs.androidx.activity.compose, ) } 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 007b1aa0..003a09bc 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,8 +10,10 @@ 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.data.api.repository.UserRepository import com.ninecraft.booket.core.model.UserState import com.ninecraft.booket.feature.screens.LoginScreen +import com.ninecraft.booket.feature.screens.NotificationScreen import com.ninecraft.booket.feature.screens.OssLicensesScreen import com.ninecraft.booket.feature.screens.SettingsScreen import com.ninecraft.booket.feature.screens.WebViewScreen @@ -33,6 +35,7 @@ import kotlinx.coroutines.launch class SettingsPresenter @AssistedInject constructor( @Assisted val navigator: Navigator, private val authRepository: AuthRepository, + private val userRepository: UserRepository, private val remoteConfigRepository: RemoteConfigRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { @@ -61,6 +64,7 @@ class SettingsPresenter @AssistedInject constructor( isLoading = true authRepository.logout() .onSuccess { + userRepository.resetNotificationData() analyticsHelper.logEvent(SETTINGS_LOGOUT_COMPLETE) navigator.resetRoot(LoginScreen()) } @@ -90,6 +94,7 @@ class SettingsPresenter @AssistedInject constructor( isLoading = true authRepository.withdraw() .onSuccess { + userRepository.resetNotificationData() analyticsHelper.logEvent(SETTINGS_WITHDRAWAL_COMPLETE) navigator.resetRoot(LoginScreen()) } @@ -153,6 +158,10 @@ class SettingsPresenter @AssistedInject constructor( navigator.goTo(WebViewScreen(url = policy.url, title = policy.title)) } + is SettingsUiEvent.OnNotificationClick -> { + navigator.goTo(NotificationScreen) + } + is SettingsUiEvent.OnTermClick -> { val terms = WebViewConstants.TERMS_OF_SERVICE navigator.goTo(WebViewScreen(url = terms.url, title = terms.title)) 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 b5989fdd..8977e64b 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 @@ -23,7 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import com.ninecraft.booket.core.common.util.compareVersions +import com.ninecraft.booket.core.common.utils.compareVersions import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.component.ReedDivider import com.ninecraft.booket.core.designsystem.theme.ReedTheme @@ -35,11 +35,13 @@ import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator import com.ninecraft.booket.feature.screens.SettingsScreen import com.ninecraft.booket.feature.settings.component.SettingItem import com.ninecraft.booket.feature.settings.component.WithdrawConfirmationBottomSheet +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR +@TraceRecomposition @OptIn(ExperimentalMaterial3Api::class) @CircuitInject(SettingsScreen::class, ActivityRetainedComponent::class) @Composable @@ -98,6 +100,21 @@ internal fun SettingsUi( ) }, ) + if (!state.isGuestMode) { + SettingItem( + title = stringResource(R.string.settings_notification), + onItemClick = { + state.eventSink(SettingsUiEvent.OnNotificationClick) + }, + action = { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right), + contentDescription = "Right Chevron Icon", + tint = Color.Unspecified, + ) + }, + ) + } SettingItem( title = stringResource(R.string.settings_terms_of_service), onItemClick = { 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 43b50cb2..be20e619 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 @@ -32,6 +32,7 @@ sealed interface SettingsUiEvent : CircuitUiEvent { data object InitSideEffect : SettingsUiEvent data object OnBackClick : SettingsUiEvent data object OnPolicyClick : SettingsUiEvent + data object OnNotificationClick : SettingsUiEvent data object OnTermClick : SettingsUiEvent data object OnOssLicensesClick : SettingsUiEvent data object OnLogoutClick : SettingsUiEvent diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/ReedSwitch.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/ReedSwitch.kt new file mode 100644 index 00000000..a7a8db08 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/ReedSwitch.kt @@ -0,0 +1,80 @@ +package com.ninecraft.booket.feature.settings.component + +import android.annotation.SuppressLint +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.extensions.noRippleClickable +import com.ninecraft.booket.core.designsystem.DevicePreview +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.skydoves.compose.stability.runtime.TraceRecomposition + +@TraceRecomposition +@SuppressLint("UseOfNonLambdaOffsetOverload") +@Composable +internal fun ReedSwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val transition = updateTransition(checked, label = "switchTransition") + + val trackColor by transition.animateColor(label = "trackColor") { + if (it) ReedTheme.colors.contentBrand else Color(0xFFE9E9EB) + } + + val thumbOffset by transition.animateDp(label = "thumbOffset") { + if (it) 22.dp else 2.dp + } + + Box( + modifier = modifier + .width(51.dp) + .height(31.dp) + .clip(RoundedCornerShape(ReedTheme.radius.full)) + .background(trackColor) + .noRippleClickable { onCheckedChange(!checked) }, + contentAlignment = Alignment.CenterStart, + ) { + Box( + modifier = Modifier + .offset(x = thumbOffset) + .size(27.dp) + .shadow(elevation = 1.dp, shape = CircleShape) + .clip(CircleShape) + .background(ReedTheme.colors.contentInverse), + ) + } +} + +@DevicePreview +@Composable +private fun ReedSwitchPreview() { + var isChecked by remember { mutableStateOf(true) } + + ReedTheme { + ReedSwitch( + checked = isChecked, + onCheckedChange = { isChecked = it }, + ) + } +} diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/ToggleItem.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/ToggleItem.kt new file mode 100644 index 00000000..da7f0f73 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/ToggleItem.kt @@ -0,0 +1,65 @@ +package com.ninecraft.booket.feature.settings.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.ninecraft.booket.core.designsystem.DevicePreview +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.skydoves.compose.stability.runtime.TraceRecomposition + +@TraceRecomposition +@Composable +internal fun ToggleItem( + title: String, + description: String, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding( + vertical = ReedTheme.spacing.spacing4, + horizontal = ReedTheme.spacing.spacing5, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Text( + text = description, + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label1Medium, + ) + } + ReedSwitch( + checked = isChecked, + onCheckedChange = { + onCheckedChange(!isChecked) + }, + ) + } +} + +@DevicePreview +@Composable +private fun ToggleItemPreview() { + ReedTheme { + ToggleItem( + title = "알림 받기", + description = "리드에서 알림을 보내드려요", + isChecked = true, + onCheckedChange = {}, + ) + } +} diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/HandleNotificationSideEffects.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/HandleNotificationSideEffects.kt new file mode 100644 index 00000000..53523b3f --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/HandleNotificationSideEffects.kt @@ -0,0 +1,28 @@ +package com.ninecraft.booket.feature.settings.notification + +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.skydoves.compose.effects.RememberedEffect + +@Composable +internal fun HandleNotificationSideEffects( + state: NotificationUiState, + eventSink: (NotificationUiEvent) -> Unit, +) { + val context = LocalContext.current + + RememberedEffect(state.sideEffect) { + when (state.sideEffect) { + is NotificationSideEffect.ShowToast -> { + Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() + } + + else -> {} + } + + if (state.sideEffect != null) { + eventSink(NotificationUiEvent.InitSideEffect) + } + } +} diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt new file mode 100644 index 00000000..7152080b --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationPresenter.kt @@ -0,0 +1,126 @@ +package com.ninecraft.booket.feature.settings.notification + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.common.utils.shouldSyncNotification +import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.feature.screens.LoginScreen +import com.ninecraft.booket.feature.screens.NotificationScreen +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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.components.ActivityRetainedComponent +import kotlinx.coroutines.launch + +class NotificationPresenter @AssistedInject constructor( + @Assisted val navigator: Navigator, + private val userRepository: UserRepository, +) : Presenter { + @Composable + override fun present(): NotificationUiState { + val scope = rememberCoroutineScope() + val isNotificationEnabled by userRepository.isUserNotificationEnabled.collectAsRetainedState(initial = false) + var sideEffect by rememberRetained { mutableStateOf(null) } + + fun updateNotificationSettings(enabled: Boolean) { + scope.launch { + val prevNotificationEnabled = userRepository.getUserNotificationEnabled() + userRepository.setUserNotificationEnabled(enabled) + + userRepository.updateNotificationSettings(enabled) + .onSuccess { + userRepository.setLastNotificationSyncedEnabled(enabled) + } + .onFailure { exception -> + val handleErrorMessage = { message: String -> + Logger.e(message) + sideEffect = NotificationSideEffect.ShowToast(message) + } + + handleException( + exception = exception, + onError = handleErrorMessage, + onLoginRequired = { + navigator.resetRoot(LoginScreen()) + }, + ) + userRepository.setUserNotificationEnabled(prevNotificationEnabled) + } + } + } + + suspend fun syncNotificationSettings(enabled: Boolean) { + userRepository.updateNotificationSettings(enabled) + .onSuccess { + userRepository.setLastNotificationSyncedEnabled(enabled) + } + .onFailure { exception -> + val handleErrorMessage = { message: String -> + Logger.e(message) + sideEffect = NotificationSideEffect.ShowToast(message) + } + + handleException( + exception = exception, + onError = handleErrorMessage, + onLoginRequired = { + navigator.resetRoot(LoginScreen()) + }, + ) + } + } + + fun handleEvent(event: NotificationUiEvent) { + when (event) { + is NotificationUiEvent.InitSideEffect -> { + sideEffect = null + } + + is NotificationUiEvent.OnBackClick -> { + navigator.pop() + } + + is NotificationUiEvent.OnNotificationPermissionResult -> { + scope.launch { + val isPermissionGranted = event.granted + val userSettingEnabled = userRepository.getUserNotificationEnabled() + val lastSyncedServerEnabled = userRepository.getLastSyncedNotificationEnabled() + + val effectiveNotificationEnabled = userSettingEnabled && isPermissionGranted + + val shouldSync = shouldSyncNotification(effectiveNotificationEnabled, lastSyncedServerEnabled) + + if (shouldSync) { + syncNotificationSettings(effectiveNotificationEnabled) + } + } + } + + is NotificationUiEvent.OnNotificationToggle -> { + updateNotificationSettings(event.enabled) + } + } + } + return NotificationUiState( + isNotificationEnabled = isNotificationEnabled, + sideEffect = sideEffect, + eventSink = ::handleEvent, + ) + } + + @CircuitInject(NotificationScreen::class, ActivityRetainedComponent::class) + @AssistedFactory + fun interface Factory { + fun create(navigator: Navigator): NotificationPresenter + } +} diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt new file mode 100644 index 00000000..66a86385 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUi.kt @@ -0,0 +1,191 @@ +package com.ninecraft.booket.feature.settings.notification + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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 androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.ninecraft.booket.core.common.extensions.noRippleClickable +import com.ninecraft.booket.core.designsystem.DevicePreview +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.ReedBackTopAppBar +import com.ninecraft.booket.feature.screens.NotificationScreen +import com.ninecraft.booket.feature.settings.R +import com.ninecraft.booket.feature.settings.component.ToggleItem +import com.skydoves.compose.stability.runtime.TraceRecomposition +import com.slack.circuit.codegen.annotations.CircuitInject +import dagger.hilt.android.components.ActivityRetainedComponent +import com.ninecraft.booket.core.designsystem.R as designR + +@TraceRecomposition +@CircuitInject(NotificationScreen::class, ActivityRetainedComponent::class) +@Composable +internal fun NotificationUi( + state: NotificationUiState, + modifier: Modifier = Modifier, +) { + HandleNotificationSideEffects( + state = state, + eventSink = state.eventSink, + ) + + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val isGranted by produceState( + initialValue = checkSystemNotificationEnabled(context), + key1 = lifecycleOwner, + ) { + // 포그라운드 복귀 시 OS 권한 동기화 + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + value = checkSystemNotificationEnabled(context) + state.eventSink(NotificationUiEvent.OnNotificationPermissionResult(value)) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + awaitDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + val settingsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { _ -> } + + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + + ReedScaffold( + modifier = modifier.fillMaxSize(), + containerColor = White, + ) { innerPadding -> + Column( + modifier = modifier + .fillMaxSize() + .padding(innerPadding), + ) { + ReedBackTopAppBar( + modifier = modifier.fillMaxWidth(), + title = stringResource(R.string.settings_notification), + onBackClick = { + state.eventSink(NotificationUiEvent.OnBackClick) + }, + ) + if (!isGranted) { + NotificationGuideItem( + onClick = { + settingsLauncher.launch(intent) + }, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ToggleItem( + title = stringResource(R.string.notification_toggle_title), + description = stringResource(R.string.notification_toggle_description), + isChecked = isGranted && state.isNotificationEnabled, + onCheckedChange = { enabled -> + if (isGranted) { + state.eventSink(NotificationUiEvent.OnNotificationToggle(enabled)) + } else { + settingsLauncher.launch(intent) + } + }, + ) + } + } +} + +@Composable +internal fun NotificationGuideItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .padding( + vertical = ReedTheme.spacing.spacing2, + horizontal = ReedTheme.spacing.spacing5, + ) + .fillMaxWidth() + .background( + color = ReedTheme.colors.baseSecondary, + shape = RoundedCornerShape(ReedTheme.radius.md), + ) + .noRippleClickable { onClick() } + .padding( + vertical = ReedTheme.spacing.spacing6, + horizontal = ReedTheme.spacing.spacing5, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.notification_guide_title), + color = ReedTheme.colors.contentBrand, + style = ReedTheme.typography.body1SemiBold, + ) + Text( + text = stringResource(R.string.notification_guide_description), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label2Regular, + ) + } + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right), + contentDescription = "Chevron Right Icon", + tint = ReedTheme.colors.contentBrand, + ) + } +} + +private fun checkSystemNotificationEnabled(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + NotificationManagerCompat.from(context).areNotificationsEnabled() + } +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@DevicePreview +@Composable +private fun NotificationUiPreview() { + ReedTheme { + NotificationUi( + state = NotificationUiState( + eventSink = {}, + ), + ) + } +} diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUiState.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUiState.kt new file mode 100644 index 00000000..7956e7e1 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/notification/NotificationUiState.kt @@ -0,0 +1,27 @@ +package com.ninecraft.booket.feature.settings.notification + +import androidx.compose.runtime.Immutable +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState +import java.util.UUID + +data class NotificationUiState( + val isNotificationEnabled: Boolean = false, + val sideEffect: NotificationSideEffect? = null, + val eventSink: (NotificationUiEvent) -> Unit, +) : CircuitUiState + +@Immutable +sealed interface NotificationSideEffect { + data class ShowToast( + val message: String, + private val key: String = UUID.randomUUID().toString(), + ) : NotificationSideEffect +} + +sealed interface NotificationUiEvent : CircuitUiEvent { + data object InitSideEffect : NotificationUiEvent + data object OnBackClick : NotificationUiEvent + data class OnNotificationPermissionResult(val granted: Boolean) : NotificationUiEvent + data class OnNotificationToggle(val enabled: Boolean) : NotificationUiEvent +} 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 dc804a52..e6380a55 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 @@ -37,6 +37,7 @@ import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.feature.settings.R import com.ninecraft.booket.feature.screens.OssLicensesScreen import com.orhanobut.logger.Logger +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import kotlinx.coroutines.Dispatchers @@ -44,6 +45,7 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.IOException +@TraceRecomposition @CircuitInject(OssLicensesScreen::class, ActivityRetainedComponent::class) @Composable internal fun OssLicenses( diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index 7051c68a..bda599a7 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -19,4 +19,9 @@ 최적의 사용 환경을 위해 업데이트해주세요. 업데이트하기 로그인 + 알림 + 알림을 켜주세요. + 기기 설정에서 Reed 알림을 설정하세요.\n독서 기록에 도움되는 알림을 받을 수 있어요. + 알림 받기 + 리드에서 알림을 보내드려요. diff --git a/feature/settings/stability/settings.stability b/feature/settings/stability/settings.stability new file mode 100644 index 00000000..bb954ee0 --- /dev/null +++ b/feature/settings/stability/settings.stability @@ -0,0 +1,168 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :settings:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.settings.HandleSettingsSideEffects(state: com.ninecraft.booket.feature.settings.SettingsUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +public fun com.ninecraft.booket.feature.settings.SettingsPresenter.present(): com.ninecraft.booket.feature.settings.SettingsUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.settings.SettingsScreenPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.settings.SettingsUi(state: com.ninecraft.booket.feature.settings.SettingsUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.settings.component.ReedSwitch(checked: kotlin.Boolean, onCheckedChange: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - checked: STABLE (primitive type) + - onCheckedChange: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.settings.component.ReedSwitchPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.settings.component.SettingItem(title: kotlin.String, modifier: androidx.compose.ui.Modifier, isClickable: kotlin.Boolean, onItemClick: kotlin.Function0, action: @[Composable] androidx.compose.runtime.internal.ComposableFunction0, description: @[Composable] androidx.compose.runtime.internal.ComposableFunction0): kotlin.Unit + skippable: true + restartable: true + params: + - title: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - isClickable: STABLE (primitive type) + - onItemClick: STABLE (function type) + - action: STABLE (composable function type) + - description: STABLE (composable function type) + +@Composable +private fun com.ninecraft.booket.feature.settings.component.SettingItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.settings.component.ToggleItem(title: kotlin.String, description: kotlin.String, isChecked: kotlin.Boolean, onCheckedChange: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - title: STABLE (String is immutable) + - description: STABLE (String is immutable) + - isChecked: STABLE (primitive type) + - onCheckedChange: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.settings.component.ToggleItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.settings.component.WithdrawConfirmationBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, isCheckBoxChecked: kotlin.Boolean, onCheckBoxCheckedChange: kotlin.Function0, onCancelButtonClick: kotlin.Function0, onWithdrawButtonClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - isCheckBoxChecked: STABLE (primitive type) + - onCheckBoxCheckedChange: STABLE (function type) + - onCancelButtonClick: STABLE (function type) + - onWithdrawButtonClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.feature.settings.component.WithdrawConfirmationBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.settings.notification.HandleNotificationSideEffects(state: com.ninecraft.booket.feature.settings.notification.NotificationUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +internal fun com.ninecraft.booket.feature.settings.notification.NotificationGuideItem(onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.settings.notification.NotificationPresenter.present(): com.ninecraft.booket.feature.settings.notification.NotificationUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.settings.notification.NotificationUi(state: com.ninecraft.booket.feature.settings.notification.NotificationUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.settings.notification.NotificationUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.settings.osslicenses.OssLicenseItem(name: kotlin.String, license: kotlin.String, url: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - name: STABLE (String is immutable) + - license: STABLE (String is immutable) + - url: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +internal fun com.ninecraft.booket.feature.settings.osslicenses.OssLicenses(state: com.ninecraft.booket.feature.settings.osslicenses.OssLicensesUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.settings.osslicenses.OssLicensesPresenter.present(): com.ninecraft.booket.feature.settings.osslicenses.OssLicensesUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.settings.osslicenses.OssLicensesScreenPreview(): kotlin.Unit + skippable: true + restartable: true + params: + 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 6af98e78..02a0761e 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 @@ -8,13 +8,12 @@ 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.ErrorScope -import com.ninecraft.booket.core.common.utils.postErrorDialog +import com.ninecraft.booket.core.common.event.postErrorDialog import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.core.model.AutoLoginState import com.ninecraft.booket.core.model.OnboardingState -import com.ninecraft.booket.core.ui.R import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.OnboardingScreen @@ -54,6 +53,7 @@ class SplashPresenter @AssistedInject constructor( userRepository.getUserProfile() .onSuccess { userProfile -> if (userProfile.termsAgreed) { + userRepository.syncFcmToken() navigator.resetRoot(HomeScreen) } else { navigator.resetRoot(LoginScreen()) @@ -63,8 +63,8 @@ class SplashPresenter @AssistedInject constructor( postErrorDialog( errorScope = ErrorScope.GLOBAL, exception = exception, - buttonLabelResId = R.string.retry, - action = { checkTermsAgreement() }, + confirmLabel = "다시 시도하기", + onConfirm = { checkTermsAgreement() }, ) } } diff --git a/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashUi.kt b/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashUi.kt index dd7f9d80..d60631d0 100644 --- a/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashUi.kt +++ b/feature/splash/src/main/kotlin/com/ninecraft/booket/splash/SplashUi.kt @@ -1,6 +1,5 @@ package com.ninecraft.booket.splash -import android.R.attr.description import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -25,10 +24,12 @@ import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.ui.component.ReedDialog import com.ninecraft.booket.feature.screens.SplashScreen import com.ninecraft.booket.feature.splash.R +import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.android.components.ActivityRetainedComponent import tech.thdev.compose.exteions.system.ui.controller.rememberSystemUiController +@TraceRecomposition @CircuitInject(SplashScreen::class, ActivityRetainedComponent::class) @Composable fun SplashUi( diff --git a/feature/splash/stability/splash.stability b/feature/splash/stability/splash.stability new file mode 100644 index 00000000..f00ec242 --- /dev/null +++ b/feature/splash/stability/splash.stability @@ -0,0 +1,34 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :splash:stabilityDump + +@Composable +internal fun com.ninecraft.booket.splash.HandleSplashSideEffects(state: com.ninecraft.booket.splash.SplashUiState, eventSink: kotlin.Function1): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - eventSink: STABLE (function type) + +@Composable +public fun com.ninecraft.booket.splash.SplashPresenter.present(): com.ninecraft.booket.splash.SplashUiState + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.splash.SplashPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.splash.SplashUi(state: com.ninecraft.booket.splash.SplashUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + diff --git a/feature/webview/stability/webview.stability b/feature/webview/stability/webview.stability new file mode 100644 index 00000000..936e51ef --- /dev/null +++ b/feature/webview/stability/webview.stability @@ -0,0 +1,35 @@ +// This file was automatically generated by Compose Stability Analyzer +// https://github.com/skydoves/compose-stability-analyzer +// +// Do not edit this file directly. To update it, run: +// ./gradlew :webview:stabilityDump + +@Composable +internal fun com.ninecraft.booket.feature.webview.WebViewContent(state: com.ninecraft.booket.feature.webview.WebViewUiState, innerPadding: androidx.compose.foundation.layout.PaddingValues, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - innerPadding: STABLE (marked @Stable or @Immutable) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.feature.webview.WebViewPresenter.present(): com.ninecraft.booket.feature.webview.WebViewUiState + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.webview.WebViewUi(state: com.ninecraft.booket.feature.webview.WebViewUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - state: STABLE + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.webview.WebViewUiPreview(): kotlin.Unit + skippable: true + restartable: true + params: + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf731c1e..53eb5898 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ minSdk = "28" targetSdk = "35" compileSdk = "35" -versionName = "1.2.0" -versionCode = "7" +versionName = "1.3.0" +versionCode = "9" packageName = "com.ninecraft.booket" ## Android gradle plugin @@ -24,12 +24,13 @@ androidx-compose-material3 = "1.4.0-alpha18" compose-stable-marker = "1.0.6" compose-effects = "0.1.1" compose-shadow = "2.0.4" +compose-stability-analyzer = "0.4.2" ## Kotlin Symbol Processing -ksp = "2.2.0-2.0.2" +ksp = "2.3.0" ## Kotlin -kotlin = "2.2.0" +kotlin = "2.2.21" kotlinx-coroutines = "1.10.2" kotlinx-serialization-json = "1.9.0" kotlinx-collections-immutable = "0.4.0" @@ -43,7 +44,7 @@ okhttp = "5.1.0" retrofit = "3.0.0" ## Circuit -circuit = "0.29.1" +circuit = "0.30.0" ## Logging logger = "2.2.0" @@ -149,6 +150,7 @@ androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } firebase-remote-config = { group = "com.google.firebase", name = "firebase-config" } [plugins] @@ -172,6 +174,8 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } google-service = { id = "com.google.gms.google-services", version.ref = "google-service" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" } +compose-stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "compose-stability-analyzer"} + # Plugins defined by this project booket-android-application = { id = "booket.android.application", version = "unspecified" } booket-android-application-compose = { id = "booket.android.application.compose", version = "unspecified" }