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" }