Full Code of igorwojda/android-showcase for AI

main a09e435e70b5 cached
206 files
1.5 MB
232.8k tokens
1 requests
Download .txt
Showing preview only (1,663K chars total). Download the full file or copy to clipboard to get everything.
Repository: igorwojda/android-showcase
Branch: main
Commit: a09e435e70b5
Files: 206
Total size: 1.5 MB

Directory structure:
gitextract_jwuhl568/

├── .editorconfig
├── .github/
│   ├── stale.yml
│   └── workflows/
│       ├── auto-approve.yml
│       ├── check.yml
│       ├── claude-code-review.yml
│       └── claude.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DeveloperReadme.md
├── README.md
├── app/
│   ├── build.gradle.kts
│   ├── proguard-rules.pro
│   └── src/
│       ├── debug/
│       │   ├── AndroidManifest.xml
│       │   └── res/
│       │       └── xml/
│       │           └── network_security_config.xml
│       └── main/
│           ├── AndroidManifest.xml
│           ├── kotlin/
│           │   └── com/
│           │       └── igorwojda/
│           │           └── showcase/
│           │               └── app/
│           │                   ├── AppKoinModule.kt
│           │                   ├── ShowcaseApplication.kt
│           │                   ├── data/
│           │                   │   └── api/
│           │                   │       ├── AuthenticationInterceptor.kt
│           │                   │       └── UserAgentInterceptor.kt
│           │                   └── presentation/
│           │                       ├── BottomNavigationBar.kt
│           │                       ├── MainShowcaseActivity.kt
│           │                       ├── MainShowcaseScreen.kt
│           │                       ├── NavigationRoute.kt
│           │                       └── util/
│           │                           └── NavigationDestinationLogger.kt
│           └── res/
│               ├── drawable/
│               │   ├── ic_favorite.xml
│               │   ├── ic_launcher_foreground.xml
│               │   ├── ic_launcher_foreground_themed.xml
│               │   ├── ic_music_library.xml
│               │   └── ic_settings.xml
│               ├── mipmap-anydpi/
│               │   ├── ic_launcher.xml
│               │   └── ic_launcher_round.xml
│               ├── mipmap-anydpi-v33/
│               │   ├── ic_launcher.xml
│               │   └── ic_launcher_round.xml
│               ├── values/
│               │   ├── colors.xml
│               │   ├── ic_launcher_background.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── xml/
│                   └── data_extraction_rules.xml
├── build-logic/
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src/
│       └── main/
│           └── kotlin/
│               └── com/
│                   └── igorwojda/
│                       └── showcase/
│                           └── buildlogic/
│                               ├── AboutLibrariesConventionPlugin.kt
│                               ├── ApplicationConventionPlugin.kt
│                               ├── DetektConventionPlugin.kt
│                               ├── EasyLauncherConventionPlugin.kt
│                               ├── FeatureConventionPlugin.kt
│                               ├── KotlinConventionPlugin.kt
│                               ├── LibraryConventionPlugin.kt
│                               ├── SpotlessConventionPlugin.kt
│                               ├── TestConventionLibraryPlugin.kt
│                               ├── TestConventionPlugin.kt
│                               ├── config/
│                               │   └── JavaBuildConfig.kt
│                               └── ext/
│                                   ├── BuildConfigExt.kt
│                                   ├── DependencyHandlerExt.kt
│                                   ├── PackagingExt.kt
│                                   └── ProjectExt.kt
├── build.gradle.kts
├── detekt.yml
├── feature/
│   ├── album/
│   │   ├── build.gradle.kts
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── kotlin/
│   │       │   │   └── com/
│   │       │   │       └── igorwojda/
│   │       │   │           └── showcase/
│   │       │   │               └── feature/
│   │       │   │                   └── album/
│   │       │   │                       ├── AlbumKoinModule.kt
│   │       │   │                       ├── data/
│   │       │   │                       │   ├── DataModule.kt
│   │       │   │                       │   ├── datasource/
│   │       │   │                       │   │   ├── api/
│   │       │   │                       │   │   │   ├── model/
│   │       │   │                       │   │   │   │   ├── AlbumApiModel.kt
│   │       │   │                       │   │   │   │   ├── AlbumListApiModel.kt
│   │       │   │                       │   │   │   │   ├── ImageApiModel.kt
│   │       │   │                       │   │   │   │   ├── ImageSizeApiModel.kt
│   │       │   │                       │   │   │   │   ├── SearchAlbumResultsApiModel.kt
│   │       │   │                       │   │   │   │   ├── TagApiModel.kt
│   │       │   │                       │   │   │   │   ├── TagListApiModel.kt
│   │       │   │                       │   │   │   │   ├── TrackApiModel.kt
│   │       │   │                       │   │   │   │   └── TrackListApiModel.kt
│   │       │   │                       │   │   │   ├── response/
│   │       │   │                       │   │   │   │   ├── GetAlbumInfoResponse.kt
│   │       │   │                       │   │   │   │   └── SearchAlbumResponse.kt
│   │       │   │                       │   │   │   └── service/
│   │       │   │                       │   │   │       └── AlbumRetrofitService.kt
│   │       │   │                       │   │   └── database/
│   │       │   │                       │   │       ├── AlbumDao.kt
│   │       │   │                       │   │       ├── AlbumDatabase.kt
│   │       │   │                       │   │       └── model/
│   │       │   │                       │   │           ├── AlbumRoomModel.kt
│   │       │   │                       │   │           ├── ImageRoomModel.kt
│   │       │   │                       │   │           ├── ImageSizeRoomModel.kt
│   │       │   │                       │   │           ├── TagRoomModel.kt
│   │       │   │                       │   │           └── TrackRoomModel.kt
│   │       │   │                       │   ├── mapper/
│   │       │   │                       │   │   ├── AlbumMapper.kt
│   │       │   │                       │   │   ├── ImageMapper.kt
│   │       │   │                       │   │   ├── ImageSizeMapper.kt
│   │       │   │                       │   │   ├── TagMapper.kt
│   │       │   │                       │   │   └── TrackMapper.kt
│   │       │   │                       │   └── repository/
│   │       │   │                       │       └── AlbumRepositoryImpl.kt
│   │       │   │                       ├── domain/
│   │       │   │                       │   ├── DomainModule.kt
│   │       │   │                       │   ├── enum/
│   │       │   │                       │   │   └── ImageSize.kt
│   │       │   │                       │   ├── model/
│   │       │   │                       │   │   ├── Album.kt
│   │       │   │                       │   │   ├── Image.kt
│   │       │   │                       │   │   ├── Tag.kt
│   │       │   │                       │   │   └── Track.kt
│   │       │   │                       │   ├── repository/
│   │       │   │                       │   │   └── AlbumRepository.kt
│   │       │   │                       │   └── usecase/
│   │       │   │                       │       ├── GetAlbumListUseCase.kt
│   │       │   │                       │       └── GetAlbumUseCase.kt
│   │       │   │                       └── presentation/
│   │       │   │                           ├── PresentationModule.kt
│   │       │   │                           ├── composable/
│   │       │   │                           │   └── SearchBarComposable.kt
│   │       │   │                           ├── screen/
│   │       │   │                           │   ├── albumdetail/
│   │       │   │                           │   │   ├── AlbumDetailAction.kt
│   │       │   │                           │   │   ├── AlbumDetailScreen.kt
│   │       │   │                           │   │   ├── AlbumDetailUiState.kt
│   │       │   │                           │   │   └── AlbumDetailViewModel.kt
│   │       │   │                           │   └── albumlist/
│   │       │   │                           │       ├── AlbumListAction.kt
│   │       │   │                           │       ├── AlbumListScreen.kt
│   │       │   │                           │       ├── AlbumListUiState.kt
│   │       │   │                           │       └── AlbumListViewModel.kt
│   │       │   │                           └── util/
│   │       │   │                               └── TimeUtil.kt
│   │       │   └── res/
│   │       │       └── values/
│   │       │           └── strings.xml
│   │       └── test/
│   │           └── kotlin/
│   │               └── com/
│   │                   └── igorwojda/
│   │                       └── showcase/
│   │                           └── feature/
│   │                               └── album/
│   │                                   ├── data/
│   │                                   │   ├── DataFixtures.kt
│   │                                   │   ├── datasource/
│   │                                   │   │   └── api/
│   │                                   │   │       └── model/
│   │                                   │   │           ├── AlbumApiModelTest.kt
│   │                                   │   │           ├── ImageApiModelTest.kt
│   │                                   │   │           └── ImageSizeApiModelTest.kt
│   │                                   │   ├── mapper/
│   │                                   │   │   ├── AlbumMapperTest.kt
│   │                                   │   │   ├── ImageMapperTest.kt
│   │                                   │   │   ├── ImageSizeMapperTest.kt
│   │                                   │   │   ├── TagMapperTest.kt
│   │                                   │   │   └── TrackMapperTest.kt
│   │                                   │   └── repository/
│   │                                   │       └── AlbumRepositoryImplTest.kt
│   │                                   ├── domain/
│   │                                   │   ├── DomainFixtures.kt
│   │                                   │   ├── model/
│   │                                   │   │   └── AlbumTest.kt
│   │                                   │   └── usecase/
│   │                                   │       ├── GetAlbumListUseCaseTest.kt
│   │                                   │       └── GetAlbumUseCaseTest.kt
│   │                                   └── presentation/
│   │                                       └── screen/
│   │                                           ├── albumdetail/
│   │                                           │   └── AlbumDetailViewModelTest.kt
│   │                                           └── albumlist/
│   │                                               └── AlbumListViewModelTest.kt
│   ├── base/
│   │   ├── build.gradle.kts
│   │   ├── consumer-rules.pro
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       └── main/
│   │           ├── AndroidManifest.xml
│   │           ├── kotlin/
│   │           │   └── com/
│   │           │       └── igorwojda/
│   │           │           └── showcase/
│   │           │               └── feature/
│   │           │                   └── base/
│   │           │                       ├── common/
│   │           │                       │   ├── delegate/
│   │           │                       │   │   └── Observer.kt
│   │           │                       │   └── res/
│   │           │                       │       └── Dimen.kt
│   │           │                       ├── data/
│   │           │                       │   └── retrofit/
│   │           │                       │       ├── ApiResult.kt
│   │           │                       │       ├── ApiResultAdapterFactory.kt
│   │           │                       │       ├── ApiResultCall.kt
│   │           │                       │       └── ApiResultCallAdapter.kt
│   │           │                       ├── domain/
│   │           │                       │   └── result/
│   │           │                       │       ├── Result.kt
│   │           │                       │       └── ResultExt.kt
│   │           │                       ├── presentation/
│   │           │                       │   ├── compose/
│   │           │                       │   │   └── composable/
│   │           │                       │   │       ├── ErrorAnim.kt
│   │           │                       │   │       ├── Loading.kt
│   │           │                       │   │       ├── Lottie.kt
│   │           │                       │   │       ├── PlaceholderImage.kt
│   │           │                       │   │       ├── TextTitleLarge.kt
│   │           │                       │   │       ├── TextTitleMedium.kt
│   │           │                       │   │       └── UnderConstructionAnim.kt
│   │           │                       │   └── viewmodel/
│   │           │                       │       ├── BaseAction.kt
│   │           │                       │       ├── BaseState.kt
│   │           │                       │       ├── BaseViewModel.kt
│   │           │                       │       └── StateTimeTravelDebugger.kt
│   │           │                       └── util/
│   │           │                           └── TimberLogTags.kt
│   │           └── res/
│   │               ├── drawable/
│   │               │   ├── ic_search.xml
│   │               │   ├── image_placeholder_1.xml
│   │               │   ├── image_placeholder_2.xml
│   │               │   └── image_placeholder_3.xml
│   │               ├── raw/
│   │               │   ├── lottie_building_screen.json
│   │               │   └── lottie_error_screen.json
│   │               └── values/
│   │                   ├── color_palete.xml
│   │                   ├── ids.xml
│   │                   ├── strings.xml
│   │                   └── styles.xml
│   ├── favourite/
│   │   ├── build.gradle.kts
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       └── main/
│   │           ├── AndroidManifest.xml
│   │           └── kotlin/
│   │               └── com/
│   │                   └── igorwojda/
│   │                       └── showcase/
│   │                           └── feature/
│   │                               └── favourite/
│   │                                   ├── FavouriteKoinModule.kt
│   │                                   ├── data/
│   │                                   │   └── DataModule.kt
│   │                                   ├── domain/
│   │                                   │   └── DomainModule.kt
│   │                                   └── presentation/
│   │                                       ├── PresentationModule.kt
│   │                                       └── screen/
│   │                                           └── favourite/
│   │                                               └── FavouriteScreen.kt
│   └── settings/
│       ├── build.gradle.kts
│       ├── proguard-rules.pro
│       └── src/
│           ├── main/
│           │   ├── AndroidManifest.xml
│           │   ├── kotlin/
│           │   │   └── com/
│           │   │       └── igorwojda/
│           │   │           └── showcase/
│           │   │               └── feature/
│           │   │                   └── settings/
│           │   │                       ├── SettingsKoinModule.kt
│           │   │                       ├── data/
│           │   │                       │   └── DataModule.kt
│           │   │                       ├── domain/
│           │   │                       │   └── DomainModule.kt
│           │   │                       └── presentation/
│           │   │                           ├── PresentationModule.kt
│           │   │                           └── screen/
│           │   │                               ├── aboutlibraries/
│           │   │                               │   ├── AboutLibrariesAction.kt
│           │   │                               │   ├── AboutLibrariesScreen.kt
│           │   │                               │   ├── AboutLibrariesUiState.kt
│           │   │                               │   └── AboutLibrariesViewModel.kt
│           │   │                               └── settings/
│           │   │                                   ├── SettingsAction.kt
│           │   │                                   ├── SettingsScreen.kt
│           │   │                                   ├── SettingsUiState.kt
│           │   │                                   └── SettingsViewModel.kt
│           │   └── res/
│           │       └── values/
│           │           └── strings.xml
│           └── test/
│               └── kotlin/
│                   └── com/
│                       └── igorwojda/
│                           └── showcase/
│                               └── feature/
│                                   └── settings/
│                                       └── presentation/
│                                           └── screen/
│                                               ├── aboutlibraries/
│                                               │   └── AboutLibrariesViewModelTest.kt
│                                               └── settings/
│                                                   └── SettingsViewModelTest.kt
├── gradle/
│   ├── libs.versions.toml
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── konsist-test/
│   ├── build.gradle.kts
│   └── src/
│       └── test/
│           └── kotlin/
│               └── com/
│                   └── igorwojda/
│                       └── showcase/
│                           └── konsisttest/
│                               ├── AndroidKonsistTest.kt
│                               ├── CleanArchitectureKonsistTest.kt
│                               ├── GeneralKonsistTest.kt
│                               ├── ModuleKonsistTest.kt
│                               ├── TestKonsistTest.kt
│                               ├── UseCaseKonsistTest.kt
│                               └── ViewModelKonsistTest.kt
├── library/
│   └── test-utils/
│       ├── build.gradle.kts
│       ├── proguard-rules.pro
│       └── src/
│           └── main/
│               ├── AndroidManifest.xml
│               └── kotlin/
│                   └── com/
│                       └── igorwojda/
│                           └── showcase/
│                               └── library/
│                                   └── testutils/
│                                       ├── CoroutinesTestDispatcherExtension.kt
│                                       └── InstantTaskExecutorExtension.kt
├── renovate.json
└── settings.gradle.kts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*.{kt,kts}]
end_of_line = lf
insert_final_newline = true
max_line_length = 140
ktlint_function_naming_ignore_when_annotated_with=Composable

# Detekt orders imports correctly, while ktlint does not
ktlint_standard_import-ordering = disabled


================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
  - pinned
  - security
  - enhancement
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
  This issue has been automatically marked as stale because it has not had
  recent activity. It will be closed if no further activity occurs. Thank you
  for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: true


================================================
FILE: .github/workflows/auto-approve.yml
================================================
name: Auto Approve

on: pull_request_target

jobs:
  auto-approve:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    if: |
      github.event.pull_request.head.repo.full_name == github.repository &&
      github.actor == github.event.pull_request.user.login &&
      contains(fromJson('["renovate[bot]", "igorwojda"]'), github.actor)
    steps:
      - uses: hmarr/auto-approve-action@v4


================================================
FILE: .github/workflows/check.yml
================================================
name: Check

on:
  push:
    branches: [ main ] # Just in case main was not up to date while merging PR
  pull_request:
    types: [ opened, synchronize ]

# Cancel previous runs on new push
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-debug:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v5

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          java-version: 17
          distribution: 'zulu'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5

      - name: Build App
        run: ./gradlew :app:assembleDebug --no-build-cache

      - uses: actions/upload-artifact@v5
        with:
          name: app-debug
          path: app/build/outputs/apk/debug/app-debug.apk

  android-lint:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v5

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          java-version: 17
          distribution: 'zulu'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5

      - name: lintDebug
        run: ./gradlew lint

      - uses: actions/upload-artifact@v5
        if: always()
        with:
          name: android-lint-report
          path: |
            app/build/reports/lint-results*.html
            feature/*/build/reports/lint-results*.html
            library/*/build/reports/lint-results*.html

  detekt:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v5

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          java-version: 17
          distribution: 'zulu'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5

      - name: detekt
        run: ./gradlew detektCheck

      - uses: actions/upload-artifact@v5
        if: always()
        with:
          name: detekt-report
          path: |
            build/reports/detekt/detekt.*
            app/build/reports/detekt/detekt.*
            feature/*/build/reports/detekt/detekt.*
            library/*/build/reports/detekt/detekt.*
            konsist-test/build/reports/detekt/detekt.*

  konsist:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v5

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          java-version: 17
          distribution: 'zulu'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5

      - name: konsist
        run: ./gradlew konsist-test:test --rerun-tasks

      - uses: actions/upload-artifact@v5
        if: always()
        with:
          name: konsist-report
          path: ./konsist-test/build/reports/*

  spotless:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v5

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          java-version: 17
          distribution: 'zulu'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5

      - name: spotlessCheck
        run: ./gradlew spotlessCheck

      - uses: actions/upload-artifact@v5
        if: always()
        with:
          name: spotless-report
          path: |
            build/reports/spotless/*
            app/build/reports/spotless/*
            feature/*/build/reports/spotless/*
            library/*/build/reports/spotless/*
            konsist-test/build/reports/spotless/*

  #  ui-test:
  #    runs-on: macos-latest
  #    steps:
  #      - name: checkout
  #        uses: actions/checkout@v4
  #
  #      - name: Set up JDK
  #        uses: actions/setup-java@v4
  #        with:
  #          java-version: 17
  #          distribution: 'zulu'
  #
  #      - name: Setup Gradle
  #        uses: gradle/actions/setup-gradle@v4
  #
  #      - name: run ui tests
  #        uses: reactivecircus/android-emulator-runner@v2
  #        with:
  #          api-level: 29
  #          target: default
  #          arch: x86
  #          profile: Nexus 6
  #          disable-animations: true
  #          script: ./gradlew connectedCheck
  #
  #      - uses: actions/upload-artifact@v4
  #        with:
  #          name: ui-test-report
  #          path: ./**/build/reports/androidTests/

  unit-test:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v5

      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          java-version: 17
          distribution: 'zulu'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v5

      - name: unitTest
        run: ./gradlew test -x konsist-test:test

      - uses: actions/upload-artifact@v5
        if: always()
        with:
          name: unit-test-report
          path: |
            app/build/reports/tests/
            feature/*/build/reports/tests/
            library/*/build/reports/tests/


================================================
FILE: .github/workflows/claude-code-review.yml
================================================
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize]
    # Optional: Only run on specific file changes
    # paths:
    #   - "src/**/*.ts"
    #   - "src/**/*.tsx"
    #   - "src/**/*.js"
    #   - "src/**/*.jsx"

jobs:
  claude-review:
    # Optional: Filter by PR author
    # if: |
    #   github.event.pull_request.user.login == 'external-contributor' ||
    #   github.event.pull_request.user.login == 'new-developer' ||
    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
    
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Run Claude Code Review
        id: claude-review
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          prompt: |
            Please review this pull request and provide feedback on:
            - Code quality and best practices
            - Potential bugs or issues
            - Performance considerations
            - Security concerns
            - Test coverage
            
            Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.

            Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
          
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
          claude_args: '--allowed-tools "Bash(./gradlew *),Bash(./gradlew konsist-test:*),Bash(./gradlew library:test-utils:*),Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'



================================================
FILE: .github/workflows/claude.yml
================================================
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
      actions: read # Required for Claude to read CI results on PRs
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Run Claude Code
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          
          # This is an optional setting that allows Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
          # prompt: 'Update the pull request description to include a summary of changes.'

          # Optional: Add claude_args to customize behavior and configuration
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
          # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'



================================================
FILE: .gitignore
================================================
# Built application files
*.apk
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/

# Gradle files
.gradle/

# Build folders
**/build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/

# Google Services (e.g. APIs or Firebase)
google-services.json

# Cache of project
.gradletasknamecache

#MacOS DS_Store
**/.DS_Store

# Kotlin
*.salive

# Claude Code
.claude/


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
  advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at {{ email }}. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

We appreciate contributions of any kind - new contributions
are welcome whether it's through bug reports or new pull requests.

## Tell us about enhancements and bugs

Please add an issue. We'll review it, add labels and reply when we get the chance.

## See an issue you'd like to work on

Comment on the issue that you'd like to work on and we'll add the
`claimed` label.  If you see the `claimed` label already on the issue you
might want to ask the contributor if they'd like some help.

## Documentation needs updating

Go right ahead! Just submit a pull request when you're done.

## Pull Requests

We love pull requests from everyone:

1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) this repository:
2. Clone forked repository `git clone git@github.com:YOUR-USERNAME/android-showcase.git`
3. Branch of the `main` branch.
4. Make changes, push changes to your fork and
   [submit a pull request](https://github.com/igorwojda/android-showcase/compare) against the `main` branch.

At this point you're waiting on us. We like to at least comment on pull requests within few days. We may suggest some
changes or improvements or alternatives.

Some things that will increase the chance that your pull request is accepted:

1. Write a [good commit message](https://chris.beams.io/posts/git-commit/)
2. Make sure all tests and lint checks are passing (review them on the pull request page)
3. Update [README](README.md) with any changes are needed
4. Write tests (if needed)


================================================
FILE: DeveloperReadme.md
================================================
# Developer Readme

## Detekt

- [Detekt configuration](https://detekt.dev/docs/introduction/configurations/) contains link to `default-detekt-config.yml`.

## Known Issues

- AboutLibraries
  - AboutLibraries `12.2.4` Gradle plugin does nto include test dependencies https://github.com/mikepenz/AboutLibraries/issues/1238
  - AboutLibraries `13.0.0-rc01` Gradle plugin required Kotlin 2.2.0 https://github.com/mikepenz/AboutLibraries/issues/1237
- Gradle 
  - Gradle `9.0` - Generated type-safe version catalogs accessors for `projcts` are not avialable inside `build-logic` module
  - Gradle `9.0` - Generated type-safe version catalogs accessors for `libs` are not accessible from precompiled script plugin e.g. add("implementation", libs.koin). Workaround is to use `implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))`.
- Mockk 
  - Unable to mock some methods with implicit `continuation`
  parameter in the `AlbumListViewModelTest` class ([Issue-957](https://github.com/mockk/mockk/issues/957))
- Detekt
  - The `UnnecessaryParentheses` rule was disabled https://github.com/detekt/detekt/issues/8668
- Kotlin Plugin
  - Auto-import (an import intention) for delegate does not work if the variable has the same name https://youtrack.jetbrains.com/issue/KTIJ-17403
- Android Studio 
  - False positive "Unused symbol" for a custom Android application class referenced in `AndroidManifest.xml`
  file ([KT-27971](https://youtrack.jetbrains.net/issue/KT-27971))
- Coil 
  - No way to automatically retry image load, so some images may not be loaded when connection speed
  is low ([Issue 132](https://github.com/coil-kt/coil/issues/132))


================================================
FILE: README.md
================================================
# 💎 Android Showcase 2.0

[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.x-blue.svg)](https://kotlinlang.org)
[![AGP](https://img.shields.io/badge/AGP-8.x-blue?style=flat)](https://developer.android.com/studio/releases/gradle-plugin)
[![Gradle](https://img.shields.io/badge/Gradle-9.x-blue?style=flat)](https://gradle.org)
[![CodeFactor](https://www.codefactor.io/repository/github/igorwojda/android-showcase/badge)](https://www.codefactor.io/repository/github/igorwojda/android-showcase)

A production-ready Android application demonstrating modern development practices and architectural patterns. This project showcases how to build scalable, maintainable, and testable Android applications using industry-standard tools and libraries.

Built with **Clean Architecture** principles, this app serves as a comprehensive example of modular design, advanced Gradle configuration, and robust CI/CD practices. Perfect for teams looking to establish solid architectural foundations for large-scale Android projects.

- [💎 Android Showcase 2.0](#-android-showcase-20)
  - [Application Scope](#application-scope)
  - [Tech-Stack](#tech-stack)
  - [Architecture](#architecture)
    - [Module Types and Dependencies](#module-types-and-dependencies)
    - [Feature Module Structure](#feature-module-structure)
      - [Presentation Layer](#presentation-layer)
      - [Domain Layer](#domain-layer)
      - [Data Layer](#data-layer)
      - [Common Module Components](#common-module-components)
    - [Data Flow](#data-flow)
  - [Project Features](#project-features)
    - [Development \& Debugging](#development--debugging)
    - [Custom Icons For Each Variant](#custom-icons-for-each-variant)
    - [Themed Icons](#themed-icons)
  - [Gradle Config](#gradle-config)
    - [Dependency Management](#dependency-management)
    - [Convention Plugins](#convention-plugins)
    - [Type Safe Project Accessors](#type-safe-project-accessors)
    - [Unified Version Configuration](#unified-version-configuration)
      - [Java/JVM Version Configuration](#javajvm-version-configuration)
    - [Generated type-safe version catalogs accessors in `build-logic` module](#generated-type-safe-version-catalogs-accessors-in-build-logic-module)
    - [Gradle Configuration Cache](#gradle-configuration-cache)
  - [Code Verification](#code-verification)
    - [CI Pipeline](#ci-pipeline)
    - [Pre-push Hooks](#pre-push-hooks)
  - [Project Scope \& Limitations](#project-scope--limitations)
  - [Getting Started](#getting-started)
  - [Roadmap](#roadmap)
  - [Resources](#resources)
  - [Contributing](#contributing)
  - [Author](#author)
  - [License](#license)
  - [Animations License](#animations-license)

## Application Scope

A music discovery app built with Jetpack Compose that displays album information sourced from the [Last.fm API](https://www.last.fm/api). The application demonstrates real-world scenarios including network requests, local caching, navigation, and state management.

**Features:**
- **Album List** - Browse albums with search functionality
- **Album Details** - View detailed album information and track listings  
- **Favorites** - Save preferred albums (WIP)
- **Profile** - User preferences and settings (WIP)

<p>
  <img src="misc/image/screen_album_list.png" width="250" />
  <img src="misc/image/screen_album_detail.png" width="250" />
  <img src="misc/image/screen_favorites.png" width="250" />
  <img src="misc/image/screen_settings.png" width="250" />
  <img src="misc/image/screen_open_source_libraries.png" width="250" />
</p>

## Tech-Stack

Built with modern Android development tools and libraries, prioritizing, project structure stability and production-readiness.

**Core Technologies:**
- **[Kotlin 2.2+](https://kotlinlang.org/)** - Modern, expressive programming language
  - **[Coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** - Asynchronous programming
  - **[Flow](https://kotlinlang.org/docs/flow.html)** - Reactive data streams
  - **[KSP (Kotlin Symbol Processing)](https://kotlinlang.org/docs/ksp-overview.html)** - Kotlin Symbol Processing
  - **[Serialization](https://kotlinlang.org/docs/serialization.html)** - JSON parsing

**Android Jetpack:**
- **[Compose](https://developer.android.com/jetpack/compose)** - Declarative UI framework
- **[Navigation Compose](https://developer.android.com/jetpack/compose/navigation)** - Type-safe navigation
- **[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)** - UI-related data management
- **[Room](https://developer.android.com/jetpack/androidx/releases/room)** - Local database with SQLite
- **[Core Splashscreen](https://developer.android.com/jetpack/androidx/releases/core#core_splashscreen_version_12_2)** - app Splashscreen

**Networking & Images:**
- **[Retrofit](https://square.github.io/retrofit/)** - HTTP client for API communication
- **[Coil](https://github.com/coil-kt/coil)** - Image loading optimized for Compose

**Dependency Injection:**
- **[Koin](https://insert-koin.io/)** - Lightweight dependency injection framework

**Architecture:**
- **[Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)** - Separation of concerns with defined layers
- **Single Activity Architecture** - Modern navigation approach
- **MVVM + MVI** - Reactive presentation layer pattern providing common UI state.
- **Modular Design** - Feature-based modules for scalability

**UI & Design:**
- **[Material Design 3](https://m3.material.io/)** - Latest design system
- **[Dynamic Theming](https://m3.material.io/styles/color/dynamic-color/overview)** - Wallpaper-based themes (Android 12+)
- **[Dark Theme](https://material.io/develop/android/theming/dark)** - System-aware dark mode
- **[Lottie](http://airbnb.io/lottie)** - Vector animations

**Testing:**
- **[JUnit 6](https://junit.org/)** - Modern testing framework
- **[Mockk](https://mockk.io/)** - Kotlin-first mocking library
- **[Kluent](https://github.com/MarkusAmshove/Kluent)** - Fluent assertion library
- **[Espresso](https://developer.android.com/training/testing/espresso)** - UI testing (WIP)

**Code Quality:**
- **[Konsist](https://docs.konsist.lemonappdev.com/)** - Architecture and code structure convention tests
- **[Ktlint](https://github.com/pinterest/ktlint)** - Kotlin code formatting and issue detection
  - **[Ktlint Standard Rules](https://pinterest.github.io/ktlint/0.49.1/rules/standard/)** - set of custom rules for Jetpack Compose
  - **[Nlopez Jetpack Compose Rules](https://mrmans0n.github.io/compose-rules/)** - set of custom rules for Jetpack Compose
  - **[Twitter's Jetpack Compose Rules](https://twitter.github.io/compose-rules/)** - set of custom rules for Jetpack Compose
- **[Detekt](https://github.com/arturbosch/detekt)** - Static analysis and complexity checks
- **[Android Lint](http://tools.android.com/tips/lint)** - Android-specific code analysis
- **[Spotless](https://github.com/diffplug/spotless)** - Code formatting enforcement

**Build & CI:**
- **[Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html)** - Type-safe build scripts
- **[Version Catalogs](https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog)** - Centralized dependency management
- **[Convention Plugins](https://docs.gradle.org/current/samples/sample_convention_plugins.html)** - Shared build logic
- **[Renovate](https://github.com/renovatebot/renovate)** - Automated dependency updates

**GitHub Actions:**
- **[Check](.github/workflows/check.yml)** - CI pipeline with build, lint, test, and code quality checks
- **[Auto Approve](.github/workflows/auto-approve.yml)** - Auto-approval for trusted bot and maintainer PRs
- **[Claude Code](.github/workflows/claude.yml)** - AI-powered code assistance and review
- **[Claude Code Review](.github/workflows/claude-code-review.yml)** - Automated PR reviews using Claude

**Gradle Plugins:**
- **[Android Application](https://developer.android.com/build/releases/gradle-plugin)** (`com.android.application`) - Android app module configuration
- **[Android Library](https://developer.android.com/build/releases/gradle-plugin)** (`com.android.library`) - Android library module configuration
- **[Kotlin Android](https://kotlinlang.org/docs/gradle.html)** (`org.jetbrains.kotlin.android`) - Kotlin compilation for Android
- **[Kotlin Serialization](https://kotlinlang.org/docs/serialization.html)** (`org.jetbrains.kotlin.plugin.serialization`) - JSON serialization support
- **[Kotlin Compose Compiler](https://developer.android.com/jetpack/androidx/releases/compose-kotlin)** (`org.jetbrains.kotlin.plugin.compose`) - Compose compiler plugin
- **[KSP](https://kotlinlang.org/docs/ksp-overview.html)** - Kotlin Symbol Processing
- **[Detekt](https://detekt.dev/)** - Static code analysis
- **[Spotless](https://github.com/diffplug/spotless)** - Code formatting
- **[Test Logger](https://github.com/radarsh/gradle-test-logger-plugin)** - Enhanced test log output
- **[Easylauncher](https://github.com/usefulness/easylauncher-gradle-plugin)** - Modify the launcher icon of each of your app-variants
- **[AboutLibraries](https://github.com/mikepenz/AboutLibraries)** - collects dependency details, including licenses and visualize these in the app

## Architecture

The project implements **Clean Architecture** with a modular approach, treating each feature as an independent, reusable component similar to a microservice. This design enables maintainability and scalability for large development teams.

**Benefits of Modular Architecture:**
- **Reusability** - Shared code across multiple app variants
- **Separation of Concerns** - Clear module boundaries with explicit dependencies
- **Parallel Development** - Teams can work on features independently
- **Faster Build Times** - Incremental compilation and build caching
- **Testability** - Isolated testing of individual components

### Module Types and Dependencies

![Module Dependencies](./misc/image/module_dependencies.png)

**Module Types:**
- **`app`** - Main application module containing navigation setup, DI configuration, and app-level components
- **`feature-*`** - Feature modules (album, profile, favourite) containing feature-specific business logic  
- **`feature-base`** - Shared foundation module providing common utilities and base classes
- **`library-*`** - Utility modules for testing and shared functionality

### Feature Module Structure

`Clean Architecture` is implemented at the module level - each module contains its own set of Clean Architecture layers:

![module_dependencies_layers](./misc/image/module_layers.png)

> Notice that the `app` module and `library_x` modules structure differs a bit from the feature module structure.

Each feature module contains 3 layers with a distinct set of responsibilities and common module components.

![feature_structure](./misc/image/module_layers_details.png)

#### Presentation Layer

This layer is closest to what the user sees on the screen.

The `presentation` layer mixes `MVVM` and `MVI` patterns:

- `MVVM` - Jetpack `ViewModel` is used to encapsulate a `common UI state`. It exposes the `state` via observable state
  holder (`Kotlin Flow`)
- `MVI` - `action` modifies the `common UI state` and emits a new state to a view via `Kotlin Flow`

> The `common state` is a single source of truth for each view. This solution derives from
> [Unidirectional Data Flow](https://en.wikipedia.org/wiki/Unidirectional_Data_Flow_(computer_science)) and [Redux
> principles](https://redux.js.org/introduction/three-principles).

This approach facilitates the creation of consistent states. The state is collected via `collectAsUiStateWithLifecycle`
method. Flows collection happens in a lifecycle-aware manner, so
[no resources are wasted](https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3).

Stated is annotated with [Immutable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Immutable)
annotation that is used by Jetpack compose to enable composition optimizations.

Components:

- **Screen (Composable)** - observes common view state (through `Kotlin Flow`). Compose transform state (emitted by Kotlin
  Flow) into application UI Consumes the state and transforms it into application UI (via `Jetpack Compose`). Pass user
  interactions to `ViewModel`. Views are hard to test, so they should be as simple as possible.
- **ViewModel** - emits (through `Kotlin Flow`) view state changes to the view and deals with user interactions (these
  view models are not simply [POJO classes](https://en.wikipedia.org/wiki/Plain_old_Java_object)).
- **ViewState** - common state for a single view
- **StateTimeTravelDebugger** - logs actions and view state transitions to facilitate debugging.
- **NavManager** - singleton that facilitates handling all navigation events inside `NavHostActivity` (instead of
  separately, inside each view)

#### Domain Layer

This is the core layer of the application. Notice that the `domain` layer is independent of any other layers. This
allows making domain models and business logic independent from other layers. In other words, changes in other layers
will not affect the `domain` layer eg. changing the database (`data` layer) or screen UI (`presentation` layer) ideally will
not result in any code change within the `domain` layer.

Components:

- **UseCase** - contains business logic
- **DomainModel** - defines the core structure of the data that will be used within the application. This is the source
  of truth for application data.
- **Repository interface** - required to keep the `domain` layer independent from
  the `data layer` ([Dependency inversion](https://en.wikipedia.org/wiki/Dependency_inversion_principle)).

#### Data Layer

Encapsulates application data. Provides the data to the `domain` layer eg. retrieves data from the internet and cache the
data in disk cache (when the device is offline).

Components:

- **Repository** is exposing data to the `domain` layer. Depending on the application structure and quality of the
  external API repository can also merge, filter, and transform the data. These operations intend to create
  a high-quality data source for the `domain` layer. It is the responsibility of the Repository (one or more) to construct
  Domain models by reading from the `Data Source` and accepting Domain models to be written to the `Data Source`
- **Mapper** - maps `data model` to `domain model` (to keep `domain` layer independent from the `data` layer).

This application has two `Data Sources` - `Retrofit` (used for network access) and `Room` (local storage used to access
device persistent memory). These data sources can be treated as an implicit sub-layer. Each data source consists of
multiple classes:

- **Retrofit Service** - defines a set of API endpoints
- **Retrofit Response Model** - definition of the network objects for a given endpoint (top-level model for the data
  consists of `ApiModels`)
- **Retrofit Api Data Model** - defines the network objects (sub-objects of the `Response Model`)
- **Room Database** - persistence database to store app data
- **Room DAO** - interact with the stored data
- **Room Entity** - definition of the stored objects

Both `Retrofit API Data Models` and `Room Entities` contain annotations, so the given framework understands how to parse the
data into objects.

#### Common Module Components

Each module in the Android project contains several standard items that provide essential functionality and configuration:

Components:
- **Gradle Build Script** - `build.gradle.kts` defining dependencies, build configurations, and plugins.
- **Koin DI Module** - Dependency injection configuration
- **Tests** - Unit tests (`test/`) and integration tests (`androidTest/`)
- **Android Resources** - resources (`res/`) including strings, drawables, and assets.
- **Android Manifest** - The `AndroidManifest.xml` file declaring module metadata.

### Data Flow

The below diagram presents application data flow when a user interacts with the `album list screen`:

![app_data_flow](./misc/image/app_data_flow.png)

## Project Features

### Development & Debugging

Tags ([LogTags](feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/util/LogTags.kt)) help filter and identify different types of logs during development and debugging.

The app provides detailed logging for development and debugging, with each log easily filterable by its tag:

- `Navigation` - Navigation events and route changes
![Navigation Logs](misc/image/logs_navigation.png)

- `Action` - User actions and UI state modifications
![Action Logs](misc/image/logs_action.png)

- `Network` - Network requests, responses, and HTTP-related logs
![Network Logs](misc/image/logs_network.png)

### Custom Icons For Each Variant

Thanks to [Easylauncher Gradle plugin](https://github.com/usefulness/easylauncher-gradle-plugin) the `debug` build has custom icon label:

<img src="./misc/image/application_icon_label.png" alt="application_icon_label" width="114"/>

### Themed Icons

App supports [Themed Icons](https://medium.com/@enikebraimoh/android-themed-icons-a-comprehensive-guide-3abb33ab51a7).

Left (classic icon), Right (themed icon):

<img src="./misc/image/application_themed_icon.png" alt="application_icon_label" width="225"/>

## Gradle Config

### Dependency Management

Gradle [versions catalog](https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog) is used as a centralized dependency management third-party dependency coordinates (group, artifact, version) are shared across all modules (Gradle projects and subprojects).

Gradle versions catalog consists of a few major sections:

- `[versions]` - declare versions that can be referenced by all dependencies
- `[libraries]` - declare the aliases to library coordinates
- `[bundles]` - declare dependency bundles (groups)
- `[plugins]` - declare Gradle plugin dependencies

Each module uses convention a plugin, so common dependencies are shared without the need to add them explicitly in each module.

### Convention Plugins

[Convention plugins](https://docs.gradle.org/current/samples/sample_convention_plugins.html) standardize build configuration across modules by encapsulating common build logic into reusable plugins:

- **[`Application Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ApplicationConventionPlugin.kt)** - Main application module configuration with Android app setup
- **[`Feature Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/FeatureConventionPlugin.kt)** - Feature module configuration combining library and Kotlin conventions
- **[`Library Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/LibraryConventionPlugin.kt)** - Android library module setup with common Android configuration
- **[`Lotlin Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/KotlinConventionPlugin.kt)** - Kotlin compilation settings, toolchain, and compiler options
- **[`Test Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/TestConventionPlugin.kt)** - Testing framework setup (JUnit, test logging, and test configurations)
- **[`Test Library Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/TestConventionLibraryPlugin.kt)** - Testing setup specifically for library modules
- **[`Detekt Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/DetektConventionPlugin.kt)** - Static code analysis configuration with Detekt
- **[`Spotless Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/SpotlessConventionPlugin.kt)** - Code formatting and style enforcement with Spotless
- **[`Easylauncher Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/EasyLauncherConventionPlugin.kt)** - App icon customization for different build variants
- **[`AboutLibraries Convention`](./build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/AboutLibrariesConventionPlugin.kt)** - About libraries configuration

### Type Safe Project Accessors

Enables type-safe project references instead of error-prone string-based module paths:

```kotlin
// Before
implementation(project(":feature:album"))

// After  
implementation(projects.feature.album)
```

### Unified Version Configuration

All dependency and Gradle plugin versions are defined in the TOML version catalog file ([libs.versions.toml](gradle/libs.versions.toml)).

#### Java/JVM Version Configuration

The Java/JVM version is centralized across the project.
It is defined once in [`libs.versions.toml`](gradle/libs.versions.toml) file under the java entry.
The `generateJavaBuildConfig` task reads this value and generates a `JavaBuildConfig.kt` file with constants.
These constants are then used in Gradle convention plugins to configure both Java and Kotlin consistently:

```kotlin
 compileOptions {
     sourceCompatibility = JavaBuildConfig.JAVA_VERSION
     targetCompatibility = JavaBuildConfig.JAVA_VERSION
 }
 
 kotlin {
     compilerOptions {
         jvmTarget = JavaBuildConfig.jvmTarget
     }

     jvmToolchain(JavaBuildConfig.jvmToolchainVersion)
 }
```

### Generated type-safe version catalogs accessors in `build-logic` module 

The `build-logic` module provides type-safe access to version catalogs from within precompiled script plugins.

This is enabled via the `versionCatalogs` block in `build-logic/settings.gradle.kts`, which references the main version catalog file and `implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))` dependency in `build-logic/build.gradle.kts` file. 

This setup allows you to use catalog dependencies in plugins, for example:

```kotlin
add("implementation", libs.timber)
```

Additionally, extensions defined in [DependencyHandlerScope](build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/DependencyHandlerExt.kt) make the syntax more natural and equivalent to standard Gradle usage:

```kotlin
implementation(libs.timber)
```

### Gradle Configuration Cache

Enabled [Gradle Configuration Cache](https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html).

## Code Verification

**Quality Checks:**
```bash
./gradlew konsist-test:test --rerun-tasks          # Architecture & convention validation
./gradlew lintDebug                               # Android lint analysis  
./gradlew detektCheck                             # Code complexity & style analysis
./gradlew spotlessCheck                           # Code formatting verification
./gradlew testDebugUnitTest -x konsist-test:test   # Unit test execution (without Konsist tests)
./gradlew connectedCheck                          # UI test execution (WIP)
./gradlew :app:bundleDebug                        # Production build verification
```

**Auto-fix Commands:**
```bash
./gradlew detektApply             # Apply Detekt formatting fixes
./gradlew spotlessApply           # Apply code formatting fixes
./gradlew lintDebug               # Update lint baseline
```

### CI Pipeline

[GitHub Actions](https://github.com/features/actions) workflows execute quality checks automatically:
- **PR Validation** - All checks run in parallel on pull requests
- **Main Branch Protection** - Post-merge validation ensures code quality
- **Automated Dependency Updates** - Renovate bot creates PRs for dependency updates

Configuration: [`.github/workflows`](.github/workflows)

### Pre-push Hooks

Optional [Git hooks](https://git-scm.com/docs/githooks#_pre_push) can execute quality checks before pushing code, providing fast feedback during development.

## Project Scope & Limitations

This showcase prioritizes **architecture, tooling, and development practices** over complex UI design. The interface uses Material Design 3 components but remains intentionally straightforward to focus on the underlying technical implementation.

## Getting Started

**Prerequisites:**
- Android Studio Giraffe | 2022.3.1+ 
- JDK 17+
- Android SDK 34+

**Setup:**
```bash
# Clone the repository
git clone https://github.com/igorwojda/android-showcase.git

# Open in Android Studio
# File -> Open -> Select cloned directory
```

**Recommended IDE Plugins:**
- [Detekt](https://plugins.jetbrains.com/plugin/10761-detekt) - Configure with [detekt.yml](detekt.yml)
- [Kotlin](https://plugins.jetbrains.com/plugin/6954-kotlin) - Usually pre-installed
- [Android](https://developer.android.com/studio) - Usually pre-installed

## Roadmap

Active development continues with focus on modern Android practices. View planned [enhancements](https://github.com/igorwojda/android-showcase/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aenhancement) and contribute ideas.

## Resources

**Development Tools:**
- [Material Theme Builder](https://m3.material.io/theme-builder#/dynamic) - Generate Material 3 dynamic themes
- [Compose Material 3 Components](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary) - Component reference
- [Android Ecosystem Cheat Sheet](https://github.com/igorwojda/android-ecosystem-cheat-sheet) - 200+ essential Android tools
- [Kotlin Coroutines Use Cases](https://github.com/LukasLechnerDev/Kotlin-Coroutine-Use-Cases-on-Android) - Practical coroutine examples

**Recommended Projects:**
- [Now in Android](https://github.com/android/nowinandroid) - Google's official modern Android showcase
- [Android Architecture Blueprints](https://github.com/googlesamples/android-architecture) - Architecture pattern examples
- [Compose Samples](https://github.com/android/compose-samples) - Official Jetpack Compose examples
- [Kotlin Android Template](https://github.com/cortinico/kotlin-android-template) - Pre-configured project template
- [Androidify](https://github.com/android/androidify) - Android's official character customization app
- [WeatherXM Android](https://github.com/WeatherXM/wxm-android) - Weather data collection and rewards platform
- [Songify](https://github.com/JamesBuhanan/Songify) - Spotify-inspired music streaming app
- [Alkaa](https://github.com/igorescodro/alkaa) - Task management app with modern architecture
- [KotlinConf App](https://github.com/JetBrains/kotlinconf-app) - JetBrains' official conference app
- [Tivi](https://github.com/chrisbanes/tivi) - TV show tracking app by Chris Banes
- [CatchUp](https://github.com/ZacSweers/CatchUp) - News aggregation app with modular architecture
- [Heron](https://github.com/tunjid/heron) - Social media client showcasing modern Android development

## Contributing

Contributions are welcome! Please check the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines before submitting PRs.

**Areas for Contribution:**
- Feature implementations (Profile, Favorites screens)
- UI/UX improvements and animations  
- Performance optimizations
- Testing coverage expansion
- Documentation improvements

## Author

**Igor Wojda** - Senior Android Engineer

[![Twitter Follow](https://img.shields.io/twitter/follow/igorwojda?style=social)](https://twitter.com/igorwojda)
[![GitHub](https://img.shields.io/github/followers/igorwojda?style=social)](https://github.com/igorwojda)

## License

```
MIT License

Copyright (c) 2025 Igor Wojda

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

## Animations License

Flowing animations are distributed under `Creative Commons License 2.0`:

- [Error screen](https://lottiefiles.com/8049-error-screen) by Chetan Potnuru
- [Building Screen](https://lottiefiles.com/1271-building-screen) by Carolina Cajazeira


================================================
FILE: app/build.gradle.kts
================================================
import com.igorwojda.showcase.buildlogic.ext.buildConfigFieldFromGradleProperty

plugins {
    id("com.igorwojda.showcase.convention.application")
}

android {
    namespace = "com.igorwojda.showcase.app"

    defaultConfig {
        applicationId = "com.igorwojda.showcase"

        versionCode = 1
        versionName = "0.0.1" // SemVer (Major.Minor.Patch)

        buildConfigFieldFromGradleProperty(project, "apiBaseUrl")
        buildConfigFieldFromGradleProperty(project, "apiToken")
    }

    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles("proguard-android.txt", "proguard-rules.pro")
        }
    }
}

dependencies {
    // "projects." Syntax utilizes Gradle TYPESAFE_PROJECT_ACCESSORS feature
    implementation(projects.feature.base)
    implementation(projects.feature.album)
    implementation(projects.feature.settings)
    implementation(projects.feature.favourite)
}


================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: app/src/debug/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application android:networkSecurityConfig="@xml/network_security_config" />

</manifest>


================================================
FILE: app/src/debug/res/xml/network_security_config.xml
================================================
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">ws.audioscrobbler.com</domain>
    </domain-config>
    <debug-overrides>
        <trust-anchors>
            <!-- Trust user added CAs.
            Enables usage of Charles Proxy to review the traffic (debug build variant only).
            See: https://www.charlesproxy.com/documentation/using-charles/ssl-certificates/
            -->
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <!--
    Data backup:
    dataExtractionRules - API 31+
    allowBackup and fullBackupContent - API < 31
    -->
    <application
            android:name=".ShowcaseApplication"
            android:allowBackup="false"
            android:fullBackupContent="false"
            android:dataExtractionRules="@xml/data_extraction_rules"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.Showcase"
            android:usesCleartextTraffic="true">

        <activity
                android:name="com.igorwojda.showcase.app.presentation.MainShowcaseActivity"
                android:exported="true"
                android:theme="@style/Theme.Showcase.Splash">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/AppKoinModule.kt
================================================
package com.igorwojda.showcase.app

import com.igorwojda.showcase.app.data.api.AuthenticationInterceptor
import com.igorwojda.showcase.app.data.api.UserAgentInterceptor
import com.igorwojda.showcase.feature.base.data.retrofit.ApiResultAdapterFactory
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.ExperimentalSerializationApi
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import retrofit2.Retrofit
import timber.log.Timber

val appModule =
    module {

        single { AuthenticationInterceptor(BuildConfig.GRADLE_API_TOKEN) }

        singleOf(::UserAgentInterceptor)

        single {
            HttpLoggingInterceptor { message ->
                Timber.d("Http: $message")
            }.apply {
                level = HttpLoggingInterceptor.Level.BODY
            }
        }

        /*
         * OkHttp logging interceptor with custom Timber logger.
         *
         * By default, HttpLoggingInterceptor uses the calling class name as the log tag which clutters Logcat and makes filtering harder.
         *
         * This custom configuration ensures:
         * - All HTTP logs are tagged consistently as `"Network"`.
         * - Logs are printed through Timber (instead of Android's `Log`).
         * - Logging level is set to BODY to include headers and payloads.
         */
        single {
            HttpLoggingInterceptor { message ->
                Timber.tag("Network").d(message)
            }.apply {
                /*
                Use BODY logging only in debug builds.
                Even if Timber.DebugTree() is planted only in debug, the interceptor still
                reads/constructs request/response bodies when level = BODY.
                This adds unnecessary overhead and may leak sensitive data if any logger
                is active in production. Setting NONE in release avoids both risks.
                 */
                level =
                    if (BuildConfig.DEBUG) {
                        HttpLoggingInterceptor.Level.BODY
                    } else {
                        HttpLoggingInterceptor.Level.NONE
                    }
            }
        }

        single {
            OkHttpClient
                .Builder()
                .apply {
                    if (BuildConfig.DEBUG) {
                        addInterceptor(get<HttpLoggingInterceptor>())
                    }
                    addInterceptor(get<AuthenticationInterceptor>())
                    addInterceptor(get<UserAgentInterceptor>())
                }.build()
        }

        single {
            val contentType = "application/json".toMediaType()

            val json =
                kotlinx.serialization.json.Json {
                    // By default Kotlin serialization will serialize all of the keys present in JSON object and throw an
                    // exception if given key is not present in the Kotlin class. This flag allows to ignore JSON fields
                    ignoreUnknownKeys = true
                }

            @OptIn(ExperimentalSerializationApi::class)
            Retrofit
                .Builder()
                .baseUrl(BuildConfig.GRADLE_API_BASE_URL)
                .client(get())
                .addConverterFactory(json.asConverterFactory(contentType))
                .addCallAdapterFactory(ApiResultAdapterFactory())
                .build()
        }
    }


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/ShowcaseApplication.kt
================================================
package com.igorwojda.showcase.app

import android.app.Application
import com.igorwojda.showcase.feature.album.featureAlbumModules
import com.igorwojda.showcase.feature.favourite.featureFavouriteModules
import com.igorwojda.showcase.feature.settings.featureSettingsModules
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext
import timber.log.Timber

class ShowcaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        initKoin()
        initTimber()
    }

    private fun initKoin() {
        GlobalContext.startKoin {
            androidLogger()
            androidContext(this@ShowcaseApplication)

            modules(appModule)
            modules(featureFavouriteModules)
            modules(featureAlbumModules)
            modules(featureSettingsModules)
        }
    }

    private fun initTimber() {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        }
    }
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/data/api/AuthenticationInterceptor.kt
================================================
package com.igorwojda.showcase.app.data.api

import okhttp3.Interceptor
import okhttp3.Response

class AuthenticationInterceptor(
    private val apiKey: String,
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response =
        chain.request().let {
            val url =
                it.url
                    .newBuilder()
                    .addQueryParameter("api_key", apiKey)
                    .addQueryParameter("format", "json")
                    .build()

            val newRequest =
                it
                    .newBuilder()
                    .url(url)
                    .build()

            chain.proceed(newRequest)
        }
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/data/api/UserAgentInterceptor.kt
================================================
package com.igorwojda.showcase.app.data.api

import com.igorwojda.showcase.app.BuildConfig
import okhttp3.Interceptor
import okhttp3.Response

/*
 * Adds a User-Agent header to the request. The header follows this format:
 * <AppName>/<version> Dalvik/<version> (Linux; U; Android <android version>; <device ID> Build/<buildtag>)
 *
 * See user agents in mobile apps: https://www.scientiamobile.com/correctly-form-user-agents-for-mobile-apps
 * See testing user agent: https://faisalman.github.io/ua-parser-js/
 */
class UserAgentInterceptor : Interceptor {
    private val userAgent = "showcase/${BuildConfig.VERSION_NAME} ${System.getProperty("http.agent")}"

    override fun intercept(chain: Interceptor.Chain): Response =
        chain
            .request()
            .newBuilder()
            .header("User-Agent", userAgent)
            .build()
            .let { chain.proceed(it) }
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/presentation/BottomNavigationBar.kt
================================================
package com.igorwojda.showcase.app.presentation

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.igorwojda.showcase.app.R

@Composable
fun BottomNavigationBar(
    navController: NavController,
    modifier: Modifier = Modifier,
) {
    val navigationItems = getBottomNavigationItems()

    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route

    val selectedNavigationIndex = getSelectedNavigationIndex(currentRoute, navigationItems)

    NavigationBar(
        modifier = modifier,
    ) {
        navigationItems.forEachIndexed { index, item ->
            NavigationBarItem(
                selected = selectedNavigationIndex == index,
                onClick = {
                    navController.navigate(item.route) {
                        popUpTo(0)
                        restoreState = true // Restores previous state if returning
                    }
                },
                icon = {
                    Icon(
                        painter = painterResource(item.iconRes),
                        contentDescription = stringResource(item.titleRes),
                    )
                },
                label = {
                    Text(
                        stringResource(item.titleRes),
                    )
                },
                colors =
                    NavigationBarItemDefaults.colors(
                        selectedIconColor = MaterialTheme.colorScheme.surface,
                        indicatorColor = MaterialTheme.colorScheme.primary,
                    ),
            )
        }
    }
}

private fun getBottomNavigationItems() =
    listOf(
        NavigationBarItem(
            R.string.bottom_navigation_albums,
            R.drawable.ic_music_library,
            NavigationRoute.AlbumList,
        ),
        NavigationBarItem(
            R.string.bottom_navigation_favorites,
            R.drawable.ic_favorite,
            NavigationRoute.Favourites,
        ),
        NavigationBarItem(
            R.string.bottom_navigation_settings,
            R.drawable.ic_settings,
            NavigationRoute.Settings,
        ),
    )

/*
Returns the index of the selected bottom menu item based on the current route.
If no match is found, it defaults to the first item (index 0).
*/
private fun getSelectedNavigationIndex(
    currentRoute: String?,
    navigationItems: List<NavigationBarItem>,
): Int =
    navigationItems
        .indexOfFirst { item ->
            when (currentRoute) {
                null -> false
                NavigationRoute.AlbumDetail::class.qualifiedName -> item.route is NavigationRoute.AlbumList
                NavigationRoute.AboutLibraries::class.qualifiedName -> item.route is NavigationRoute.Settings
                else -> item.route::class.qualifiedName == currentRoute
            }
        }.takeIf { it >= 0 } ?: 0

data class NavigationBarItem(
    @StringRes val titleRes: Int,
    @DrawableRes val iconRes: Int,
    val route: NavigationRoute,
)

@Preview
@Composable
private fun BottomNavigationBarPreview() {
    BottomNavigationBar(
        navController = rememberNavController(),
    )
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/presentation/MainShowcaseActivity.kt
================================================
package com.igorwojda.showcase.app.presentation

import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen

class MainShowcaseActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()

        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme(colorScheme = getColorScheme()) {
                MainShowcaseScreen()
            }
        }
    }

    @Composable
    private fun getColorScheme(): ColorScheme {
        val darkTheme: Boolean = isSystemInDarkTheme()
        val dynamicColor: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

        val context = LocalContext.current
        return when {
            dynamicColor -> if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
            darkTheme -> darkColorScheme()
            else -> lightColorScheme()
        }
    }
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/presentation/MainShowcaseScreen.kt
================================================
package com.igorwojda.showcase.app.presentation

import android.os.Bundle
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.createGraph
import androidx.navigation.toRoute
import com.igorwojda.showcase.app.BuildConfig
import com.igorwojda.showcase.app.presentation.util.NavigationDestinationLogger
import com.igorwojda.showcase.feature.album.presentation.screen.albumdetail.AlbumDetailScreen
import com.igorwojda.showcase.feature.album.presentation.screen.albumlist.AlbumListScreen
import com.igorwojda.showcase.feature.favourite.presentation.screen.favourite.FavouriteScreen
import com.igorwojda.showcase.feature.settings.presentation.screen.aboutlibraries.AboutLibrariesScreen
import com.igorwojda.showcase.feature.settings.presentation.screen.settings.SettingsScreen

@Composable
fun MainShowcaseScreen(modifier: Modifier = Modifier) {
    val navController = rememberNavController()

    if (BuildConfig.DEBUG) {
        addOnDestinationChangedListener(navController)
    }

    Scaffold(
        modifier = modifier.fillMaxSize(),
        bottomBar = { BottomNavigationBar(navController) },
    ) { innerPadding ->

        val graph =
            navController.createGraph(startDestination = NavigationRoute.AlbumList) {
                composable<NavigationRoute.AlbumList> {
                    AlbumListScreen(
                        // artistName: String, albumName: String, mbId: String?
                        onNavigateToAlbumDetail = { artistName, albumName, albumMbId ->
                            navController.navigate(
                                NavigationRoute.AlbumDetail(artistName, albumName, albumMbId),
                            )
                        },
                    )
                }
                composable<NavigationRoute.AlbumDetail> { backStackEntry ->
                    // Retrieve typed args
                    val args = backStackEntry.toRoute<NavigationRoute.AlbumDetail>()

                    AlbumDetailScreen(
                        albumName = args.albumName,
                        artistName = args.artistName,
                        albumMbId = args.albumMbId,
                        onBackClick = {
                            navController.popBackStack()
                        },
                    )
                }
                composable<NavigationRoute.Favourites> {
                    FavouriteScreen()
                }
                composable<NavigationRoute.Settings> {
                    SettingsScreen(
                        onNavigateToAboutLibraries = {
                            navController.navigate(NavigationRoute.AboutLibraries)
                        },
                    )
                }
                composable<NavigationRoute.AboutLibraries> {
                    AboutLibrariesScreen(
                        onBackClick = {
                            navController.popBackStack()
                        },
                    )
                }
            }
        NavHost(
            navController = navController,
            graph = graph,
            modifier = Modifier.padding(innerPadding),
        )
    }
}

private fun addOnDestinationChangedListener(navController: NavController) {
    navController.addOnDestinationChangedListener(
        object : NavController.OnDestinationChangedListener {
            override fun onDestinationChanged(
                controller: NavController,
                destination: NavDestination,
                arguments: Bundle?,
            ) {
                NavigationDestinationLogger.logDestinationChange(destination, arguments)
            }
        },
    )
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/presentation/NavigationRoute.kt
================================================
package com.igorwojda.showcase.app.presentation

import kotlinx.serialization.Serializable

sealed interface NavigationRoute {
    @Serializable
    data object AlbumList : NavigationRoute

    @Serializable
    data class AlbumDetail(
        val albumName: String,
        val artistName: String,
        val albumMbId: String?,
    ) : NavigationRoute

    @Serializable
    data object Favourites : NavigationRoute

    @Serializable
    data object Settings : NavigationRoute

    @Serializable
    data object AboutLibraries : NavigationRoute
}


================================================
FILE: app/src/main/kotlin/com/igorwojda/showcase/app/presentation/util/NavigationDestinationLogger.kt
================================================
package com.igorwojda.showcase.app.presentation.util

import android.os.Bundle
import androidx.navigation.NavDestination
import com.igorwojda.showcase.app.presentation.NavigationRoute
import com.igorwojda.showcase.feature.base.util.TimberLogTags
import timber.log.Timber

object NavigationDestinationLogger {
    fun logDestinationChange(
        destination: NavDestination,
        arguments: Bundle?,
    ) {
        val className = NavigationRoute::class.simpleName
        val destinationRoute = destination.route?.substringAfter("$className.") ?: "Unknown"
        val destinationId = destination.id
        val destinationLabel = destination.label ?: "No Label"

        val logMessage =
            buildString {
                appendLine("Navigation destination changed:")
                appendLine("\tRoute: $destinationRoute")
                appendLine("\tID: $destinationId")
                appendLine("\tLabel: $destinationLabel")

                arguments?.let { bundle ->
                    if (!bundle.isEmpty) {
                        appendLine("   Arguments:")
                        bundle.keySet().forEach { key ->
                            val value = getValueFromBundle(bundle, key) ?: "null"
                            appendLine("\t\t$key: $value")
                        }
                    }
                }
            }

        Timber.tag(TimberLogTags.NAVIGATION).d(logMessage)
    }

    /**
     * Retrieves a value from Bundle using Android Navigation supported types.
     * Navigation supports: String, Int, Long, Float, Boolean, Parcelable, Serializable, and their arrays.
     *
     * @return String representation of the value, or null if no matching type found
     */
    private fun getValueFromBundle(
        bundle: Bundle,
        key: String,
    ): String? =
        bundle.getString(key)?.let { "\"$it\"" }
            ?: runCatching { bundle.getInt(key) }.getOrNull()?.toString()
            ?: runCatching { bundle.getLong(key) }.getOrNull()?.toString()
            ?: runCatching { bundle.getFloat(key) }.getOrNull()?.toString()
            ?: runCatching { bundle.getBoolean(key) }.getOrNull()?.toString()
            ?: bundle.getStringArray(key)?.contentToString()
            ?: bundle.getIntArray(key)?.contentToString()
            ?: bundle.getLongArray(key)?.contentToString()
            ?: bundle.getFloatArray(key)?.contentToString()
            ?: bundle.getBooleanArray(key)?.contentToString()
}


================================================
FILE: app/src/main/res/drawable/ic_favorite.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960">
  <path
      android:pathData="m480,840 l-58,-52q-101,-91 -167,-157T150,512.5Q111,460 95.5,416T80,326q0,-94 63,-157t157,-63q52,0 99,22t81,62q34,-40 81,-62t99,-22q94,0 157,63t63,157q0,46 -15.5,90T810,512.5Q771,565 705,631T538,788l-58,52ZM480,732q96,-86 158,-147.5t98,-107q36,-45.5 50,-81t14,-70.5q0,-60 -40,-100t-100,-40q-47,0 -87,26.5T518,280h-76q-15,-41 -55,-67.5T300,186q-60,0 -100,40t-40,100q0,35 14,70.5t50,81q36,45.5 98,107T480,732ZM480,459Z"
      android:fillColor="#e3e3e3"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="108dp"
        android:height="108dp"
        android:viewportWidth="45.47177"
        android:viewportHeight="45.47177"
        android:tint="#FB3430">
    <group android:translateX="10.735885"
           android:translateY="10.735885">
        <path
            android:fillColor="#FF000000"
            android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
    </group>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground_themed.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="108dp"
        android:height="108dp"
        android:viewportWidth="45.47177"
        android:viewportHeight="45.47177">
    <group android:translateX="10.735885"
           android:translateY="10.735885">
        <path
            android:fillColor="#FFFFFF"
            android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
    </group>
</vector>

================================================
FILE: app/src/main/res/drawable/ic_music_library.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960">
  <path
      android:pathData="M500,600q42,0 71,-29t29,-71v-220h120v-80L560,200v220q-13,-10 -28,-15t-32,-5q-42,0 -71,29t-29,71q0,42 29,71t71,29ZM320,720q-33,0 -56.5,-23.5T240,640v-480q0,-33 23.5,-56.5T320,80h480q33,0 56.5,23.5T880,160v480q0,33 -23.5,56.5T800,720L320,720ZM320,640h480v-480L320,160v480ZM160,880q-33,0 -56.5,-23.5T80,800v-560h80v560h560v80L160,880ZM320,160v480,-480Z"
      android:fillColor="#e3e3e3"/>
</vector>


================================================
FILE: app/src/main/res/drawable/ic_settings.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="960"
    android:viewportHeight="960">
  <path
      android:pathData="m370,880 l-16,-128q-13,-5 -24.5,-12T307,725l-119,50L78,585l103,-78q-1,-7 -1,-13.5v-27q0,-6.5 1,-13.5L78,375l110,-190 119,50q11,-8 23,-15t24,-12l16,-128h220l16,128q13,5 24.5,12t22.5,15l119,-50 110,190 -103,78q1,7 1,13.5v27q0,6.5 -2,13.5l103,78 -110,190 -118,-50q-11,8 -23,15t-24,12L590,880L370,880ZM440,800h79l14,-106q31,-8 57.5,-23.5T639,633l99,41 39,-68 -86,-65q5,-14 7,-29.5t2,-31.5q0,-16 -2,-31.5t-7,-29.5l86,-65 -39,-68 -99,42q-22,-23 -48.5,-38.5T533,266l-13,-106h-79l-14,106q-31,8 -57.5,23.5T321,327l-99,-41 -39,68 86,64q-5,15 -7,30t-2,32q0,16 2,31t7,30l-86,65 39,68 99,-42q22,23 48.5,38.5T427,694l13,106ZM482,620q58,0 99,-41t41,-99q0,-58 -41,-99t-99,-41q-59,0 -99.5,41T342,480q0,58 40.5,99t99.5,41ZM480,480Z"
      android:fillColor="#e3e3e3"/>
</vector>


================================================
FILE: app/src/main/res/mipmap-anydpi/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- References the base adaptive icon without monochrome support -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background"/>
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>


================================================
FILE: app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Base round adaptive icon without monochrome support -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background"/>
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>


================================================
FILE: app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Extends the base adaptive icon with monochrome support for Android 13+ -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background"/>
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
    <monochrome android:drawable="@drawable/ic_launcher_foreground_themed"/>
</adaptive-icon>

================================================
FILE: app/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Round adaptive icon with monochrome support for Android 13+ -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background"/>
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
    <monochrome android:drawable="@drawable/ic_launcher_foreground_themed"/>
</adaptive-icon>

================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Splash Screen Colors -->
    <color name="splash_background">#FFFFFF</color>
    <color name="splash_icon_background">#3DDC84</color>
</resources>

================================================
FILE: app/src/main/res/values/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="ic_launcher_background">#000000</color>
</resources>

================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">Showcase</string>

    <string name="bottom_navigation_albums">Albums</string>
    <string name="bottom_navigation_favorites">Favorites</string>
    <string name="bottom_navigation_settings">Settings</string>
</resources>


================================================
FILE: app/src/main/res/values/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Splash Screen theme for pre-Android 12 compatibility -->
    <style name="Theme.Showcase.Splash" parent="Theme.SplashScreen">
        <item name="windowSplashScreenBackground">@color/splash_background</item>
        <item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
        <item name="windowSplashScreenAnimationDuration">1000</item>
        <item name="windowSplashScreenIconBackgroundColor">@color/splash_icon_background</item>
        <item name="postSplashScreenTheme">@style/Theme.Showcase</item>
    </style>
</resources>


================================================
FILE: app/src/main/res/xml/data_extraction_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
   Sample data extraction rules file; uncomment and customize as necessary.
   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
   for details.
-->
<data-extraction-rules>
    <cloud-backup>
        <exclude domain="root" />
        <exclude domain="file" />
        <exclude domain="database" />
        <exclude domain="sharedpref" />
        <exclude domain="external" />
    </cloud-backup>
    <device-transfer>
        <exclude domain="root" />
        <exclude domain="file" />
        <exclude domain="database" />
        <exclude domain="sharedpref" />
        <exclude domain="external" />
    </device-transfer>
</data-extraction-rules>


================================================
FILE: build-logic/build.gradle.kts
================================================
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
    `kotlin-dsl`
}

group = "com.igorwojda.showcase.buildlogic"

/*
Configure the build-logic plugins to target JDK from version catalog
This matches the JDK used to build the project, and is not related to what is running on device.
*/
val javaVersion =
    libs
        .versions
        .java
        .get()

kotlin {
    compilerOptions {
        jvmTarget = JvmTarget.fromTarget(javaVersion)
    }

    jvmToolchain(javaVersion.toInt())
}

dependencies {
    implementation(libs.android.gradlePlugin)
    implementation(libs.kotlin.gradlePlugin)
    implementation(libs.ksp.gradlePlugin)
    implementation(libs.spotless.gradlePlugin)
    implementation(libs.detekt.gradlePlugin)
    implementation(libs.test.logger.gradlePlugin)
    implementation(libs.compose.gradlePlugin)
    implementation(libs.junit5.gradlePlugin)
    implementation(libs.easy.launcher.gradlePlugin)
    implementation(libs.about.libraries.gradlePlugin)

    /*
    Expose generated type-safe version catalogs accessors accessible from precompiled script plugins
    e.g. add("implementation", libs.koin)
    https://github.com/gradle/gradle/issues/15383
     */
    implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}

tasks {
    validatePlugins {
        enableStricterValidation = true
        failOnWarning = true
    }
}

gradlePlugin {
    plugins {
        register("applicationConvention") {
            id = "com.igorwojda.showcase.convention.application"
            implementationClass = "com.igorwojda.showcase.buildlogic.ApplicationConventionPlugin"
        }

        register("featureConvention") {
            id = "com.igorwojda.showcase.convention.feature"
            implementationClass = "com.igorwojda.showcase.buildlogic.FeatureConventionPlugin"
        }

        register("libraryConvention") {
            id = "com.igorwojda.showcase.convention.library"
            implementationClass = "com.igorwojda.showcase.buildlogic.LibraryConventionPlugin"
        }

        register("kotlinConvention") {
            id = "com.igorwojda.showcase.convention.kotlin"
            implementationClass = "com.igorwojda.showcase.buildlogic.KotlinConventionPlugin"
        }

        register("testConvention") {
            id = "com.igorwojda.showcase.convention.test"
            implementationClass = "com.igorwojda.showcase.buildlogic.TestConventionPlugin"
        }

        register("testLibraryConvention") {
            id = "com.igorwojda.showcase.convention.test.library"
            implementationClass = "com.igorwojda.showcase.buildlogic.TestConventionLibraryPlugin"
        }

        register("spotlessConvention") {
            id = "com.igorwojda.showcase.convention.spotless"
            implementationClass = "com.igorwojda.showcase.buildlogic.SpotlessConventionPlugin"
        }

        register("detektConvention") {
            id = "com.igorwojda.showcase.convention.detekt"
            implementationClass = "com.igorwojda.showcase.buildlogic.DetektConventionPlugin"
        }

        register("easyLauncherConvention") {
            id = "com.igorwojda.showcase.convention.easylauncher"
            implementationClass = "com.igorwojda.showcase.buildlogic.EasyLauncherConventionPlugin"
        }

        register("aboutLibrariesConvention") {
            id = "com.igorwojda.showcase.convention.aboutlibraries"
            implementationClass = "com.igorwojda.showcase.buildlogic.AboutLibrariesConventionPlugin"
        }
    }
}


================================================
FILE: build-logic/settings.gradle.kts
================================================
rootProject.name = "build-logic"

@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }

    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/AboutLibrariesConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.mikepenz.aboutlibraries.plugin.AboutLibrariesExtension
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class AboutLibrariesConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.mikepenz.aboutlibraries.plugin.android")
            }

            extensions.configure<AboutLibrariesExtension> {
                library {
                    // Avoids duplicate entries in the generated about libraries screen
                    duplicationMode.set(DuplicateMode.MERGE)
                }

                collect {
                    all.set(true)
                }
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ApplicationConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.android.build.api.dsl.ApplicationExtension
import com.igorwojda.showcase.buildlogic.config.JavaBuildConfig
import com.igorwojda.showcase.buildlogic.ext.debugImplementation
import com.igorwojda.showcase.buildlogic.ext.excludeLicenseAndMetaFiles
import com.igorwojda.showcase.buildlogic.ext.implementation
import com.igorwojda.showcase.buildlogic.ext.libs
import com.igorwojda.showcase.buildlogic.ext.versions
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies

@Suppress("detekt.LongMethod")
class ApplicationConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.application")
                apply("com.google.devtools.ksp")
                apply("org.jetbrains.kotlin.plugin.compose")
                apply<KotlinConventionPlugin>()
                apply<SpotlessConventionPlugin>()
                apply<EasyLauncherConventionPlugin>()
                apply<AboutLibrariesConventionPlugin>()
            }

            extensions.configure<ApplicationExtension> {
                compileSdk =
                    versions
                        .compile
                        .sdk
                        .get()
                        .toInt()

                defaultConfig {
                    applicationId = "com.igorwojda.showcase"

                    minSdk =
                        versions
                            .min
                            .sdk
                            .get()
                            .toInt()

                    targetSdk =
                        versions
                            .target
                            .sdk
                            .get()
                            .toInt()

                    versionCode = 1
                    versionName = "1.0"
                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                    multiDexEnabled = true

                    vectorDrawables {
                        useSupportLibrary = true
                    }
                }

                buildFeatures {
                    viewBinding = true
                    buildConfig = true
                    compose = true
                }

                compileOptions {
                    sourceCompatibility = JavaBuildConfig.JAVA_VERSION
                    targetCompatibility = JavaBuildConfig.JAVA_VERSION
                }

                packaging {
                    excludeLicenseAndMetaFiles()
                }

                testOptions {
                    unitTests.isReturnDefaultValues = true
                }
            }

            dependencies {
                implementation(libs.kotlin.reflect)
                implementation(libs.core.ktx)
                implementation(libs.timber)
                implementation(libs.coroutines)
                implementation(libs.material.material)
                implementation(libs.compose.material)
                implementation(libs.material.icons)

                // Compose dependencies
                implementation(platform(libs.compose.bom))
                implementation(libs.tooling.preview)
                debugImplementation(libs.compose.ui.tooling)
                debugImplementation(libs.compose.ui.test.manifest)
                implementation(libs.navigation.compose)

                // Koin
                implementation(platform(libs.koin.bom))
                implementation(libs.bundles.koin)

                implementation(libs.bundles.retrofit)
                implementation(libs.viewmodel.ktx)
                implementation(libs.core.splashscreen)
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/DetektConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import io.gitlab.arturbosch.detekt.Detekt
import org.gradle.api.Plugin
import org.gradle.api.Project

class DetektConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("io.gitlab.arturbosch.detekt")

            repositories.mavenCentral()

            val detektCheck =
                tasks.register("detektCheck", Detekt::class.java) {
                    description = "Checks that sourcecode satisfies detekt rules."
                    autoCorrect = false
                }

            val detektApply =
                tasks.register("detektApply", Detekt::class.java) {
                    description = "Applies code formatting rules to sourcecode in-place."
                    autoCorrect = true
                }

            listOf(detektCheck, detektApply).forEach { taskProvider ->
                taskProvider.configure {
                    group = "verification"
                    parallel = true
                    ignoreFailures = false
                    setSource(file(rootDir))

                    // Custom detekt config
                    config.setFrom("$rootDir/detekt.yml")

                    // Use default configuration on top of custom config
                    buildUponDefaultConfig = true

                    // Runs detekt for all files in the Gradle project and all subprojects
                    include("**/*.kt", "**/*.kts")
                    exclude("**/resources/**", "**/build/**", "**/generated/**")

                    reports {
                        html.required.set(true)
                        xml.required.set(true)
                    }
                }
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/EasyLauncherConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.project.starter.easylauncher.filter.ChromeLikeFilter
import com.project.starter.easylauncher.plugin.EasyLauncherExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

@Suppress("detekt.LongMethod")
class EasyLauncherConventionPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        with(project) {
            with(pluginManager) {
                apply("com.starter.easylauncher")
            }

            extensions.configure<EasyLauncherExtension> {
                defaultFlavorNaming(true)

                buildTypes.create("debug") {
                    setFilters(
                        chromeLike(
                            ribbonColor = OVERLAY_COLOR_BACKGROUND_DEBUG,
                            labelColor = OVERLAY_COLOR_TEXT,
                            gravity = ChromeLikeFilter.Gravity.BOTTOM,
                            overlayHeight = OVERLAY_HEIGHT,
                            textSizeRatio = 0.2F,
                        ),
                    )
                }
            }
        }
    }

    companion object {
        private const val OVERLAY_COLOR_BACKGROUND_DEBUG = "#99AD0000"
        private const val OVERLAY_COLOR_TEXT = "#FFFFFF"
        private const val OVERLAY_HEIGHT = 0.25F
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/FeatureConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.android.build.api.dsl.LibraryExtension
import com.igorwojda.showcase.buildlogic.config.JavaBuildConfig
import com.igorwojda.showcase.buildlogic.ext.debugImplementation
import com.igorwojda.showcase.buildlogic.ext.excludeLicenseAndMetaFiles
import com.igorwojda.showcase.buildlogic.ext.implementation
import com.igorwojda.showcase.buildlogic.ext.ksp
import com.igorwojda.showcase.buildlogic.ext.libs
import com.igorwojda.showcase.buildlogic.ext.testImplementation
import com.igorwojda.showcase.buildlogic.ext.testRuntimeOnly
import com.igorwojda.showcase.buildlogic.ext.versions
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies

@Suppress("detekt.LongMethod")
class FeatureConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply<KotlinConventionPlugin>()
                apply<TestConventionPlugin>()
                apply<AboutLibrariesPlugin>()
                apply("com.google.devtools.ksp")
                apply("org.jetbrains.kotlin.plugin.compose")
            }

            extensions.configure<LibraryExtension> {
                compileSdk =
                    versions
                        .compile
                        .sdk
                        .get()
                        .toInt()

                defaultConfig {
                    minSdk =
                        versions
                            .min
                            .sdk
                            .get()
                            .toInt()

                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                    consumerProguardFiles("consumer-rules.pro")
                }

                buildFeatures {
                    viewBinding = true
                    buildConfig = true
                    compose = true
                }

                compileOptions {
                    sourceCompatibility = JavaBuildConfig.JAVA_VERSION
                    targetCompatibility = JavaBuildConfig.JAVA_VERSION
                }

                testOptions {
                    unitTests.isReturnDefaultValues = true
                }

                packaging {
                    excludeLicenseAndMetaFiles()
                }
            }

            dependencies {
                // Add feature:base dependency only for non-base feature modules
                if (project.path != ":feature:base") {
                    implementation(project(":feature:base"))
                }

                implementation(libs.kotlin.reflect)
                implementation(libs.core.ktx)
                implementation(libs.timber)
                implementation(libs.coroutines)
                implementation(libs.material.material)
                implementation(libs.compose.material)
                implementation(libs.material.icons)

                // Compose dependencies
                implementation(platform(libs.compose.bom))
                implementation(libs.bundles.compose)
                debugImplementation(libs.compose.ui.tooling)
                debugImplementation(libs.compose.ui.test.manifest)

                // Koin
                implementation(platform(libs.koin.bom))
                implementation(libs.bundles.koin)

                implementation(libs.bundles.retrofit)
                implementation(libs.viewmodel.ktx)

                // Room
                implementation(libs.bundles.room)
                ksp(libs.room.compiler)

                // Test dependencies
                testImplementation(project(":library:test-utils"))
                testImplementation(libs.bundles.test)
                testRuntimeOnly(libs.junit.jupiter.engine)
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/KotlinConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.igorwojda.showcase.buildlogic.config.JavaBuildConfig
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

class KotlinConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("org.jetbrains.kotlin.android")
                apply("org.jetbrains.kotlin.plugin.serialization")
            }

            kotlinExtension.jvmToolchain(JavaBuildConfig.JVM_TOOLCHAIN_VERSION)

            tasks.withType<KotlinCompile>().configureEach {
                compilerOptions {
                    freeCompilerArgs.addAll(
                        /*
                        This ensures annotations on data class constructor parameters are applied to both
                        the parameter and the backing field, preventing future breaking changes.
                        See https://youtrack.jetbrains.com/issue/KT-73255: for more details.
                         */
                        "-Xannotation-default-target=param-property",
                    )
                }
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/LibraryConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.android.build.api.dsl.LibraryExtension
import com.igorwojda.showcase.buildlogic.config.JavaBuildConfig
import com.igorwojda.showcase.buildlogic.ext.excludeLicenseAndMetaFiles
import com.igorwojda.showcase.buildlogic.ext.versions
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure

class LibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply<KotlinConventionPlugin>()
                apply<TestConventionPlugin>()
                apply<AboutLibrariesPlugin>()
                apply("com.google.devtools.ksp")
                apply("org.jetbrains.kotlin.plugin.compose")
            }

            extensions.configure<LibraryExtension> {
                compileSdk =
                    versions
                        .compile
                        .sdk
                        .get()
                        .toInt()

                defaultConfig {
                    minSdk =
                        versions
                            .min
                            .sdk
                            .get()
                            .toInt()

                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                    consumerProguardFiles("consumer-rules.pro")
                }

                buildFeatures {
                    viewBinding = true
                    buildConfig = true
                    compose = true
                }

                compileOptions {
                    sourceCompatibility = JavaBuildConfig.JAVA_VERSION
                    targetCompatibility = JavaBuildConfig.JAVA_VERSION
                }

                testOptions {
                    unitTests.isReturnDefaultValues = true
                }

                packaging {
                    excludeLicenseAndMetaFiles()
                }
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/SpotlessConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.diffplug.gradle.spotless.SpotlessExtension
import com.igorwojda.showcase.buildlogic.ext.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class SpotlessConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("com.diffplug.spotless")

            extensions.configure<SpotlessExtension> {
                kotlin {
                    target("**/*.kt", "**/*.kts")

                    // Some rules are disabled in .editorconfig to avoid conflicts with detekt
                    val customRuleSets =
                        listOf(
                            libs.ktlint.ruleset.standard,
                            libs.nlopez.compose.rules,
                            libs.twitter.compose.rules,
                        ).map {
                            it.get().toString()
                        }

                    ktlint()
                        .customRuleSets(customRuleSets)

                    endWithNewline()
                }

                // Don't add spotless as dependency for the Gradle's check task to facilitate separated codebase checks
                isEnforceCheck = false
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/TestConventionLibraryPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.android.build.api.dsl.LibraryExtension
import com.igorwojda.showcase.buildlogic.config.JavaBuildConfig
import com.igorwojda.showcase.buildlogic.ext.versions
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure

class TestConventionLibraryPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply<KotlinConventionPlugin>()
                apply<TestConventionPlugin>()
                apply<AboutLibrariesPlugin>()
                apply("com.google.devtools.ksp")
            }

            extensions.configure<LibraryExtension> {
                compileSdk =
                    versions
                        .compile
                        .sdk
                        .get()
                        .toInt()

                defaultConfig {
                    minSdk =
                        versions
                            .min
                            .sdk
                            .get()
                            .toInt()

                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                    consumerProguardFiles("consumer-rules.pro")
                }

                buildFeatures {
                    viewBinding = false
                    buildConfig = false
                    compose = false
                }

                compileOptions {
                    sourceCompatibility = JavaBuildConfig.JAVA_VERSION
                    targetCompatibility = JavaBuildConfig.JAVA_VERSION
                }

                testOptions {
                    unitTests.isReturnDefaultValues = true
                }

                packaging {
                    resources.excludes +=
                        setOf(
                            "META-INF/AL2.0",
                            "META-INF/licenses/**",
                            "**/attach_hotspot_windows.dll",
                            "META-INF/LGPL2.1",
                        )
                }
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/TestConventionPlugin.kt
================================================
package com.igorwojda.showcase.buildlogic

import com.adarshr.gradle.testlogger.TestLoggerExtension
import com.adarshr.gradle.testlogger.theme.ThemeType
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType

class TestConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("com.adarshr.test-logger")

            tasks.withType<Test> {
                useJUnitPlatform()

                // Enable parallel test execution
                systemProperties =
                    mapOf(
                        "junit.jupiter.execution.parallel.enabled" to "true",
                        "junit.jupiter.execution.parallel.mode.default " to "concurrent",
                    )
            }

            extensions.configure<TestLoggerExtension> {
                theme = ThemeType.MOCHA
            }
        }
    }
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/config/JavaBuildConfig.kt
================================================
package com.igorwojda.showcase.buildlogic.config

import org.gradle.api.JavaVersion
import java.io.File

object JavaBuildConfig {
    /**
     * Reads the Java version from the `gradle/libs.versions.toml` file.
     * (VersionCatalogsExtension is not available at this stage).
     */
    private val tomlJavaVersion by lazy {
        File(System.getProperty("user.dir"))
            .resolve("gradle/libs.versions.toml")
            .readLines()
            .firstOrNull { it.trim().startsWith("java") }
            ?.substringAfter("=")
            ?.trim('"', ' ')
            ?: error("❌ Could not find 'java' version in libs.versions.toml file")
    }

    /*
    Configure the buildLogic config to target JDK from version catalog
    This matches the JDK used to build the project.
     */
    val JAVA_VERSION: JavaVersion = JavaVersion.toVersion(tomlJavaVersion)
    val JVM_TOOLCHAIN_VERSION: Int = tomlJavaVersion.toInt()
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/BuildConfigExt.kt
================================================
package com.igorwojda.showcase.buildlogic.ext

import com.android.build.api.dsl.ApplicationDefaultConfig
import com.android.build.api.dsl.LibraryDefaultConfig
import org.gradle.api.Project
import java.util.Locale

/**
 * Takes value from Gradle project property and sets it as Android build config property.
 * Example: apiToken variable present in the settings.gradle file will be accessible
 * as BuildConfig.GRADLE_API_TOKEN in the app.
 */
fun ApplicationDefaultConfig.buildConfigFieldFromGradleProperty(
    project: Project,
    gradlePropertyName: String,
) {
    val (androidResourceName, propertyValue) = extractBuildConfigField(project, gradlePropertyName)
    buildConfigField("String", androidResourceName, propertyValue)
}

/**
 * Takes value from Gradle project property and sets it as Android build config property.
 * Example: apiToken variable present in the settings.gradle file will be accessible
 * as BuildConfig.GRADLE_API_TOKEN in the library.
 */
fun LibraryDefaultConfig.buildConfigFieldFromGradleProperty(
    project: Project,
    gradlePropertyName: String,
) {
    val (androidResourceName, propertyValue) = extractBuildConfigField(project, gradlePropertyName)
    buildConfigField("String", androidResourceName, propertyValue)
}

private fun extractBuildConfigField(
    project: Project,
    gradlePropertyName: String,
): Pair<String, String> {
    val propertyValue = project.properties[gradlePropertyName] as? String
    checkNotNull(propertyValue) { "Gradle property $gradlePropertyName is null" }

    val androidResourceName = "GRADLE_${gradlePropertyName.toSnakeCase()}".uppercase(Locale.getDefault())
    return androidResourceName to propertyValue
}

private fun String.toSnakeCase() = this.split(Regex("(?=[A-Z])")).joinToString("_") { it.lowercase(Locale.getDefault()) }


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/DependencyHandlerExt.kt
================================================
package com.igorwojda.showcase.buildlogic.ext

import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.DependencyHandlerScope

private const val IMPLEMENTATION = "implementation"
private const val DEBUG_IMPLEMENTATION = "debugImplementation"
private const val TEST_IMPLEMENTATION = "testImplementation"
private const val TEST_RUNTIME_ONLY = "testRuntimeOnly"
private const val KSP = "ksp"

fun DependencyHandlerScope.implementation(provider: Provider<out Any>) {
    add(IMPLEMENTATION, provider)
}

fun DependencyHandlerScope.implementation(project: Project) {
    add(IMPLEMENTATION, project)
}

fun DependencyHandlerScope.implementation(provider: LibrariesForLibs.KotlinLibraryAccessors) {
    add(IMPLEMENTATION, provider)
}

fun DependencyHandlerScope.debugImplementation(provider: Provider<out Any>) {
    add(DEBUG_IMPLEMENTATION, provider)
}

fun DependencyHandlerScope.ksp(provider: Provider<out Any>) {
    add(KSP, provider)
}

fun DependencyHandlerScope.testImplementation(project: Project) {
    add(TEST_IMPLEMENTATION, project)
}

fun DependencyHandlerScope.testImplementation(provider: Provider<out Any>) {
    add(TEST_IMPLEMENTATION, provider)
}

fun DependencyHandlerScope.testRuntimeOnly(provider: Provider<out Any>) {
    add(TEST_RUNTIME_ONLY, provider)
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/PackagingExt.kt
================================================
package com.igorwojda.showcase.buildlogic.ext

import com.android.build.api.dsl.Packaging

fun Packaging.excludeLicenseAndMetaFiles() {
    resources.excludes +=
        setOf(
            "META-INF/AL2.0",
            "META-INF/licenses/**",
            "**/attach_hotspot_windows.dll",
            "META-INF/LGPL2.1",
        )
}


================================================
FILE: build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/ProjectExt.kt
================================================
package com.igorwojda.showcase.buildlogic.ext

import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Project
import org.gradle.kotlin.dsl.the

/**
 * Returns "libs" from version catalog.
 */
val Project.libs: LibrariesForLibs
    get() = the<LibrariesForLibs>()

/**
 * Returns "versions" from version catalog.
 */
val Project.versions: LibrariesForLibs.VersionAccessors
    get() = the<LibrariesForLibs>().versions


================================================
FILE: build.gradle.kts
================================================
plugins {
    // Convention plugins
    id("com.igorwojda.showcase.convention.detekt")
    id("com.igorwojda.showcase.convention.spotless")

    // Core Android and Kotlin plugins using version catalog
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.android.library) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.serialization) apply false
    alias(libs.plugins.kotlin.symbol.processing) apply false
    alias(libs.plugins.compose) apply false
    alias(libs.plugins.test.logger) apply false
    alias(libs.plugins.detekt) apply false
    alias(libs.plugins.spotless) apply false
    alias(libs.plugins.junit5.android) apply false
}


================================================
FILE: detekt.yml
================================================
build:
  maxIssues: 0
  excludeCorrectable: false
  weights:
    # complexity: 2
    # LongParameterList: 1
    # style: 1
    # comments: 1

config:
  validation: true
  warningsAsErrors: true
  checkExhaustiveness: true
  # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
  excludes: ''

processors:
  active: true
  exclude:
    - 'DetektProgressListener'
  # - 'KtFileCountProcessor'
  # - 'PackageCountProcessor'
  # - 'ClassCountProcessor'
  # - 'FunctionCountProcessor'
  # - 'PropertyCountProcessor'
  # - 'ProjectComplexityProcessor'
  # - 'ProjectCognitiveComplexityProcessor'
  # - 'ProjectLLOCProcessor'
  # - 'ProjectCLOCProcessor'
  # - 'ProjectLOCProcessor'
  # - 'ProjectSLOCProcessor'
  # - 'LicenseHeaderLoaderExtension'

console-reports:
  active: true
  exclude:
     - 'ProjectStatisticsReport'
     - 'ComplexityReport'
     - 'NotificationReport'
     - 'FindingsReport'
     - 'FileBasedFindingsReport'
  #  - 'LiteFindingsReport'

output-reports:
  active: true
  exclude:
  # - 'TxtOutputReport'
  # - 'XmlOutputReport'
  # - 'HtmlOutputReport'
  # - 'MdOutputReport'
  # - 'SarifOutputReport'

comments:
  active: true
  AbsentOrWrongFileLicense:
    active: false
    licenseTemplateFile: 'license.template'
    licenseTemplateIsRegex: false
  CommentOverPrivateFunction:
    active: false
  CommentOverPrivateProperty:
    active: false
  DeprecatedBlockTag:
    active: true
  EndOfSentenceFormat:
    active: true
    endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
  KDocReferencesNonPublicProperty:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  OutdatedDocumentation:
    active: true
    matchTypeParameters: true
    matchDeclarationsOrder: true
    allowParamOnConstructorProperties: false
  UndocumentedPublicClass:
    active: false
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    searchInNestedClass: true
    searchInInnerClass: true
    searchInInnerObject: true
    searchInInnerInterface: true
    searchInProtectedClass: false
    ignoreDefaultCompanionObject: false
  UndocumentedPublicFunction:
    active: false
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    searchProtectedFunction: false
  UndocumentedPublicProperty:
    active: false
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    searchProtectedProperty: false

complexity:
  active: true
  CognitiveComplexMethod:
    active: true
    threshold: 15
  ComplexCondition:
    active: true
    threshold: 4
  ComplexInterface:
    active: true
    threshold: 10
    includeStaticDeclarations: false
    includePrivateDeclarations: false
    ignoreOverloaded: false
  CyclomaticComplexMethod:
    active: true
    threshold: 15
    ignoreSingleWhenExpression: false
    ignoreSimpleWhenEntries: false
    ignoreNestingFunctions: false
    nestingFunctions:
      - 'also'
      - 'apply'
      - 'forEach'
      - 'isNotNull'
      - 'ifNull'
      - 'let'
      - 'run'
      - 'use'
      - 'with'
  LabeledExpression:
    active: true
    ignoredLabels: []
  LargeClass:
    active: true
    threshold: 600
  LongMethod:
    active: true
    threshold: 60
  LongParameterList:
    active: true
    functionThreshold: 6
    constructorThreshold: 7
    ignoreDefaultParameters: false
    ignoreDataClasses: true
    ignoreAnnotatedParameter: []
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  MethodOverloading:
    active: true
    threshold: 6
  NamedArguments:
    active: true
    threshold: 3
    ignoreArgumentsMatchingNames: false
  NestedBlockDepth:
    active: true
    threshold: 4
  NestedScopeFunctions:
    active: true
    threshold: 1
    functions:
      - 'kotlin.apply'
      - 'kotlin.run'
      - 'kotlin.with'
      - 'kotlin.let'
      - 'kotlin.also'
  ReplaceSafeCallChainWithRun:
    active: true
  StringLiteralDuplication:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    threshold: 3
    ignoreAnnotation: true
    excludeStringsWithLessThan5Characters: true
    ignoreStringsRegex: '$^'
  TooManyFunctions:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    thresholdInFiles: 11
    thresholdInClasses: 11
    thresholdInInterfaces: 11
    thresholdInObjects: 11
    thresholdInEnums: 11
    ignoreDeprecated: false
    ignorePrivate: false
    ignoreOverridden: false
    ignoreAnnotatedFunctions: []

coroutines:
  active: true
  GlobalCoroutineUsage:
    active: true
  InjectDispatcher:
    active: true
    dispatcherNames:
      - 'IO'
      - 'Default'
      - 'Unconfined'
  RedundantSuspendModifier:
    active: true
  SleepInsteadOfDelay:
    active: true
  SuspendFunSwallowedCancellation:
    active: true
  SuspendFunWithCoroutineScopeReceiver:
    active: true
  SuspendFunWithFlowReturnType:
    active: true

empty-blocks:
  active: true
  EmptyCatchBlock:
    active: true
    allowedExceptionNameRegex: '_|(ignore|expected).*'
  EmptyClassBlock:
    active: true
  EmptyDefaultConstructor:
    active: true
  EmptyDoWhileBlock:
    active: true
  EmptyElseBlock:
    active: true
  EmptyFinallyBlock:
    active: true
  EmptyForBlock:
    active: true
  EmptyFunctionBlock:
    active: true
    ignoreOverridden: false
  EmptyIfBlock:
    active: true
  EmptyInitBlock:
    active: true
  EmptyKtFile:
    active: true
  EmptySecondaryConstructor:
    active: true
  EmptyTryBlock:
    active: true
  EmptyWhenBlock:
    active: true
  EmptyWhileBlock:
    active: true

exceptions:
  active: true
  ExceptionRaisedInUnexpectedLocation:
    active: true
    methodNames:
      - 'equals'
      - 'finalize'
      - 'hashCode'
      - 'toString'
  InstanceOfCheckForException:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  NotImplementedDeclaration:
    active: true
  ObjectExtendsThrowable:
    active: true
  PrintStackTrace:
    active: true
  RethrowCaughtException:
    active: true
  ReturnFromFinally:
    active: true
    ignoreLabeled: false
  SwallowedException:
    active: true
    ignoredExceptionTypes:
      - 'InterruptedException'
      - 'MalformedURLException'
      - 'NumberFormatException'
      - 'ParseException'
    allowedExceptionNameRegex: '_|(ignore|expected).*'
  ThrowingExceptionFromFinally:
    active: true
  ThrowingExceptionInMain:
    active: true
  ThrowingExceptionsWithoutMessageOrCause:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    exceptions:
      - 'ArrayIndexOutOfBoundsException'
      - 'Exception'
      - 'IllegalArgumentException'
      - 'IllegalMonitorStateException'
      - 'IllegalStateException'
      - 'IndexOutOfBoundsException'
      - 'NullPointerException'
      - 'RuntimeException'
      - 'Throwable'
  ThrowingNewInstanceOfSameException:
    active: true
  TooGenericExceptionCaught:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    exceptionNames:
      - 'ArrayIndexOutOfBoundsException'
      - 'Error'
      - 'Exception'
      - 'IllegalMonitorStateException'
      - 'IndexOutOfBoundsException'
      - 'NullPointerException'
      - 'RuntimeException'
      - 'Throwable'
    allowedExceptionNameRegex: '_|(ignore|expected).*'
  TooGenericExceptionThrown:
    active: true
    exceptionNames:
      - 'Error'
      - 'Exception'
      - 'RuntimeException'
      - 'Throwable'

naming:
  active: true
  BooleanPropertyNaming:
    active: true
    allowedPattern: '^(is|has|are)'
  ClassNaming:
    active: true
    classPattern: '[A-Z][a-zA-Z0-9]*'
  ConstructorParameterNaming:
    active: true
    parameterPattern: '[a-z][A-Za-z0-9]*'
    privateParameterPattern: '[a-z][A-Za-z0-9]*'
    excludeClassPattern: '$^'
  EnumNaming:
    active: true
    enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
  ForbiddenClassName:
    active: true
    forbiddenName: []
  FunctionMaxLength:
    active: false
    maximumFunctionNameLength: 50
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  FunctionMinLength:
    active: true
    minimumFunctionNameLength: 3
  FunctionNaming:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    functionPattern: '[a-z][a-zA-Z0-9]*'
    excludeClassPattern: '$^'
    ignoreAnnotated: ['Composable']
  FunctionParameterNaming:
    active: true
    parameterPattern: '[a-z][A-Za-z0-9]*'
    excludeClassPattern: '$^'
  InvalidPackageDeclaration:
    active: true
    rootPackage: ''
    requireRootInDeclaration: false
  LambdaParameterNaming:
    active: true
    parameterPattern: '[a-z][A-Za-z0-9]*|_'
  MatchingDeclarationName:
    active: true
    mustBeFirst: true
    multiplatformTargets:
      - 'ios'
      - 'android'
      - 'js'
      - 'jvm'
      - 'native'
      - 'iosArm64'
      - 'iosX64'
      - 'macosX64'
      - 'mingwX64'
      - 'linuxX64'
  MemberNameEqualsClassName:
    active: true
    ignoreOverridden: true
  NoNameShadowing:
    active: true
  NonBooleanPropertyPrefixedWithIs:
    active: true
  ObjectPropertyNaming:
    active: true
    constantPattern: '[A-Za-z][_A-Za-z0-9]*'
    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
    privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
  PackageNaming:
    active: true
    packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
  TopLevelPropertyNaming:
    active: true
    constantPattern: '[A-Z][_A-Z0-9]*'
    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
    privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
  VariableMaxLength:
    active: true
    maximumVariableNameLength: 64
  VariableMinLength:
    active: true
    minimumVariableNameLength: 1
  VariableNaming:
    active: true
    variablePattern: '[a-z][A-Za-z0-9]*'
    privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
    excludeClassPattern: '$^'

performance:
  active: true
  ArrayPrimitive:
    active: true
  CouldBeSequence:
    active: true
    threshold: 3
  ForEachOnRange:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  SpreadOperator:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  UnnecessaryPartOfBinaryExpression:
    active: true
  UnnecessaryTemporaryInstantiation:
    active: true

potential-bugs:
  active: true
  AvoidReferentialEquality:
    active: true
    forbiddenTypePatterns:
      - 'kotlin.String'
  CastNullableToNonNullableType:
    active: true
  CastToNullableType:
    active: true
  Deprecation:
    active: true
  DontDowncastCollectionTypes:
    active: true
  DoubleMutabilityForCollection:
    active: true
    mutableTypes:
      - 'kotlin.collections.MutableList'
      - 'kotlin.collections.MutableMap'
      - 'kotlin.collections.MutableSet'
      - 'java.util.ArrayList'
      - 'java.util.LinkedHashSet'
      - 'java.util.HashSet'
      - 'java.util.LinkedHashMap'
      - 'java.util.HashMap'
  ElseCaseInsteadOfExhaustiveWhen:
    active: true
    ignoredSubjectTypes: []
  EqualsAlwaysReturnsTrueOrFalse:
    active: true
  EqualsWithHashCodeExist:
    active: true
  ExitOutsideMain:
    active: true
  ExplicitGarbageCollectionCall:
    active: true
  HasPlatformType:
    active: true
  IgnoredReturnValue:
    active: true
    restrictToConfig: true
    returnValueAnnotations:
      - 'CheckResult'
      - '*.CheckResult'
      - 'CheckReturnValue'
      - '*.CheckReturnValue'
    ignoreReturnValueAnnotations:
      - 'CanIgnoreReturnValue'
      - '*.CanIgnoreReturnValue'
    returnValueTypes:
      - 'kotlin.sequences.Sequence'
      - 'kotlinx.coroutines.flow.*Flow'
      - 'java.util.stream.*Stream'
    ignoreFunctionCall: []
  ImplicitDefaultLocale:
    active: true
  ImplicitUnitReturnType:
    active: true
    allowExplicitReturnType: true
  InvalidRange:
    active: true
  IteratorHasNextCallsNextMethod:
    active: true
  IteratorNotThrowingNoSuchElementException:
    active: true
  LateinitUsage:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    ignoreOnClassesPattern: ''
  MapGetWithNotNullAssertionOperator:
    active: true
  MissingPackageDeclaration:
    active: true
    excludes: ['**/*.kts']
  NullCheckOnMutableProperty:
    active: true
  NullableToStringCall:
    active: true
  PropertyUsedBeforeDeclaration:
    active: true
  UnconditionalJumpStatementInLoop:
    active: true
  UnnecessaryNotNullCheck:
    active: true
  UnnecessaryNotNullOperator:
    active: true
  UnnecessarySafeCall:
    active: true
  UnreachableCatchBlock:
    active: true
  UnreachableCode:
    active: true
  UnsafeCallOnNullableType:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
  UnsafeCast:
    active: true
  UnusedUnaryOperator:
    active: true
  UselessPostfixExpression:
    active: true
  WrongEqualsTypeParameter:
    active: true

style:
  active: true
  AlsoCouldBeApply:
    active: true
  BracesOnIfStatements:
    active: true
    singleLine: 'never'
    multiLine: 'always'
  BracesOnWhenStatements:
    active: true
    singleLine: 'necessary'
    multiLine: 'consistent'
  CanBeNonNullable:
    active: true
  CascadingCallWrapping:
    active: false
    includeElvis: true
  ClassOrdering:
    active: true
  CollapsibleIfStatements:
    active: true
  DataClassContainsFunctions:
    active: false
    conversionFunctionPrefix:
      - 'to'
    allowOperators: false
  DataClassShouldBeImmutable:
    active: true
  DestructuringDeclarationWithTooManyEntries:
    active: true
    maxDestructuringEntries: 3
  DoubleNegativeLambda:
    active: true
    negativeFunctions:
      - reason: 'Use `takeIf` instead.'
        value: 'takeUnless'
      - reason: 'Use `all` instead.'
        value: 'none'
    negativeFunctionNameParts:
      - 'not'
      - 'non'
  EqualsNullCall:
    active: true
  EqualsOnSignatureLine:
    active: true
  ExplicitCollectionElementAccessMethod:
    active: true
  ExplicitItLambdaParameter:
    active: true
  ExpressionBodySyntax:
    active: true
    includeLineWrapping: false
  ForbiddenAnnotation:
    active: true
    annotations:
      - reason: 'it is a java annotation. Use `Suppress` instead.'
        value: 'java.lang.SuppressWarnings'
      - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
        value: 'java.lang.Deprecated'
      - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
        value: 'java.lang.annotation.Documented'
      - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
        value: 'java.lang.annotation.Target'
      - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
        value: 'java.lang.annotation.Retention'
      - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
        value: 'java.lang.annotation.Repeatable'
      - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
        value: 'java.lang.annotation.Inherited'
  ForbiddenComment:
    active: true
    comments:
      - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
        value: 'FIXME:'
      - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
        value: 'STOPSHIP:'
      - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
        value: 'TODO:'
    allowedPatterns: ''
  ForbiddenImport:
    active: true
    imports: []
    forbiddenPatterns: ''
  ForbiddenMethodCall:
    active: true
    methods:
      - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
        value: 'kotlin.io.print'
      - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
        value: 'kotlin.io.println'
  ForbiddenSuppress:
    active: true
    rules: []
  ForbiddenVoid:
    active: true
    ignoreOverridden: false
    ignoreUsageInGenerics: false
  FunctionOnlyReturningConstant:
    active: true
    ignoreOverridableFunction: true
    ignoreActualFunction: true
    excludedFunctions: []
  LoopWithTooManyJumpStatements:
    active: true
    maxJumpCount: 1
  MagicNumber:
    active: true
    excludes: ['**/[Tt]est/**', '**/*Test.kt']
    ignoreNumbers:
      - '-1'
      - '0'
      - '1'
      - '2'
      - '60'  # Common for seconds/minutes
      - '300'  # Common for milliseconds delay
      - '1000'  # Common for milliseconds
      - '3600'  # Seconds in hour
    ignoreHashCodeFunction: true
    ignorePropertyDeclaration: false
    ignoreLocalVariableDeclaration: false
    ignoreConstantDeclaration: true
    ignoreCompanionObjectPropertyDeclaration: true
    ignoreAnnotation: false
    ignoreNamedArgument: true
    ignoreEnums: false
    ignoreRanges: false
    ignoreExtensionFunctions: true
  MandatoryBracesLoops:
    active: true
  MaxChainedCallsOnSameLine:
    active: true
    maxChainedCalls: 5
  MaxLineLength:
    active: true
    maxLineLength: 140
    excludePackageStatements: true
    excludeImportStatements: true
    excludeCommentStatements: false
    excludeRawStrings: true
  MayBeConst:
    active: true
  ModifierOrder:
    active: true
  MultilineLambdaItParameter:
    active: true
  MultilineRawStringIndentation:
    active: true
    indentSize: 4
    trimmingMethods:
      - 'trimIndent'
      - 'trimMargin'
  NestedClassesVisibility:
    active: true
  NewLineAtEndOfFile:
    active: true
  NoTabs:
    active: true
  NullableBooleanCheck:
    active: true
  ObjectLiteralToLambda:
    active: true
  OptionalAbstractKeyword:
    active: true
  OptionalUnit:
    active: true
  PreferToOverPairSyntax:
    active: true
  ProtectedMemberInFinalClass:
    active: true
  RedundantExplicitType:
    active: true
  RedundantHigherOrderMapUsage:
    active: true
  RedundantVisibilityModifierRule:
    active: true
  ReturnCount:
    active: true
    max: 3
    excludedFunctions:
      - 'equals'
    excludeLabeled: false
    excludeReturnFromLambda: true
    excludeGuardClauses: false
  SafeCast:
    active: true
  SerialVersionUIDInSerializableClass:
    active: true
  SpacingBetweenPackageAndImports:
    active: true
  StringShouldBeRawString:
    active: true
    maxEscapedCharacterCount: 2
    ignoredCharacters: []
  ThrowsCount:
    active: true
    max: 2
    excludeGuardClauses: false
  TrailingWhitespace:
    active: true
  TrimMultilineRawString:
    active: true
    trimmingMethods:
      - 'trimIndent'
      - 'trimMargin'
  UnderscoresInNumericLiterals:
    active: true
    acceptableLength: 4
    allowNonStandardGrouping: false
  UnnecessaryAbstractClass:
    active: true
  UnnecessaryAnnotationUseSiteTarget:
    active: true
  UnnecessaryApply:
    active: true
  UnnecessaryBackticks:
    active: true
  UnnecessaryBracesAroundTrailingLambda:
    active: true
  UnnecessaryFilter:
    active: true
  UnnecessaryInheritance:
    active: true
  UnnecessaryInnerClass:
    active: true
  UnnecessaryLet:
    active: true
  UnnecessaryParentheses:
    active: false
    allowForUnclearPrecedence: false
  UntilInsteadOfRangeTo:
    active: true
  UnusedImports:
    active: true
  UnusedParameter:
    active: true
    allowedNames: 'ignored|expected'
  UnusedPrivateClass:
    active: true
  UnusedPrivateMember:
    active: true
    allowedNames: ''
    ignoreAnnotated: ['Preview']
  UnusedPrivateProperty:
    active: true
    allowedNames: '_|ignored|expected|serialVersionUID'
  UseAnyOrNoneInsteadOfFind:
    active: true
  UseArrayLiteralsInAnnotations:
    active: true
  UseCheckNotNull:
    active: true
  UseCheckOrError:
    active: true
  UseDataClass:
    active: true
    allowVars: false
  UseEmptyCounterpart:
    active: true
  UseIfEmptyOrIfBlank:
    active: true
  UseIfInsteadOfWhen:
    active: true
    ignoreWhenContainingVariableDeclaration: false
  UseIsNullOrEmpty:
    active: true
  UseLet:
    active: true
  UseOrEmpty:
    active: true
  UseRequire:
    active: true
  UseRequireNotNull:
    active: true
  UseSumOfInsteadOfFlatMapSize:
    active: true
  UselessCallOnNotNull:
    active: true
  UtilityClassWithPublicConstructor:
    active: true
  VarCouldBeVal:
    active: true
    ignoreLateinitVar: false
  WildcardImport:
    active: true
    excludeImports:
      - 'java.util.*'


================================================
FILE: feature/album/build.gradle.kts
================================================
plugins {
    id("com.igorwojda.showcase.convention.feature")
}

android {
    namespace = "com.igorwojda.showcase.feature.album"
}


================================================
FILE: feature/album/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: feature/album/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest />


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/AlbumKoinModule.kt
================================================
package com.igorwojda.showcase.feature.album

import com.igorwojda.showcase.feature.album.data.dataModule
import com.igorwojda.showcase.feature.album.domain.domainModule
import com.igorwojda.showcase.feature.album.presentation.presentationModule

val featureAlbumModules =
    listOf(
        presentationModule,
        domainModule,
        dataModule,
    )


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/DataModule.kt
================================================
package com.igorwojda.showcase.feature.album.data

import androidx.room.Room
import com.igorwojda.showcase.feature.album.data.datasource.api.service.AlbumRetrofitService
import com.igorwojda.showcase.feature.album.data.datasource.database.AlbumDatabase
import com.igorwojda.showcase.feature.album.data.mapper.AlbumMapper
import com.igorwojda.showcase.feature.album.data.mapper.ImageMapper
import com.igorwojda.showcase.feature.album.data.mapper.ImageSizeMapper
import com.igorwojda.showcase.feature.album.data.mapper.TagMapper
import com.igorwojda.showcase.feature.album.data.mapper.TrackMapper
import com.igorwojda.showcase.feature.album.data.repository.AlbumRepositoryImpl
import com.igorwojda.showcase.feature.album.domain.repository.AlbumRepository
import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import retrofit2.Retrofit

internal val dataModule =
    module {

        singleOf(::AlbumRepositoryImpl) { bind<AlbumRepository>() }

        single { get<Retrofit>().create(AlbumRetrofitService::class.java) }

        single {
            Room
                .databaseBuilder(
                    get(),
                    AlbumDatabase::class.java,
                    "Albums.db",
                ).build()
        }

        single { get<AlbumDatabase>().albums() }

        singleOf(::ImageSizeMapper)
        singleOf(::ImageMapper)
        singleOf(::TrackMapper)
        singleOf(::TagMapper)
        singleOf(::AlbumMapper)
    }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/AlbumApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import com.igorwojda.showcase.feature.album.data.datasource.database.model.AlbumRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Album
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class AlbumApiModel(
    @SerialName("mbid") val mbId: String? = null,
    @SerialName("name") val name: String,
    @SerialName("artist") val artist: String,
    @SerialName("image") val images: List<ImageApiModel>? = null,
    @SerialName("tracks") val tracks: TrackListApiModel? = null,
    @SerialName("tags") val tags: TagListApiModel? = null,
)

internal fun AlbumApiModel.toRoomModel() =
    AlbumRoomModel(
        mbId = this.mbId ?: "",
        name = this.name,
        artist = this.artist,
        images = this.images?.mapNotNull { it.toRoomModel() } ?: listOf(),
        tracks = this.tracks?.track?.map { it.toRoomModel() },
        tags = this.tags?.tag?.map { it.toRoomModel() },
    )

internal fun AlbumApiModel.toDomainModel(): Album {
    val images =
        this.images
            ?.filterNot { it.size == ImageSizeApiModel.UNKNOWN || it.url.isBlank() }
            ?.map { it.toDomainModel() }

    return Album(
        mbId = this.mbId,
        name = this.name,
        artist = this.artist,
        images = images ?: listOf(),
        tracks = this.tracks?.track?.map { it.toDomainModel() },
        tags = this.tags?.tag?.map { it.toDomainModel() },
    )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/AlbumListApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class AlbumListApiModel(
    @SerialName("album") val album: List<AlbumApiModel>,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/ImageApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import com.igorwojda.showcase.feature.album.data.datasource.database.model.ImageRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Image
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class ImageApiModel(
    @SerialName("#text") val url: String,
    @SerialName("size") val size: ImageSizeApiModel,
)

internal fun ImageApiModel.toDomainModel() =
    Image(
        url = this.url,
        size = this.size.toDomainModel(),
    )

internal fun ImageApiModel.toRoomModel() = this.size.toRoomModel()?.let { ImageRoomModel(this.url, it) }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/ImageSizeApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import com.igorwojda.showcase.feature.album.data.datasource.database.model.ImageSizeRoomModel
import com.igorwojda.showcase.feature.album.domain.enum.ImageSize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal enum class ImageSizeApiModel {
    @SerialName("medium")
    MEDIUM,

    @SerialName("small")
    SMALL,

    @SerialName("large")
    LARGE,

    @SerialName("extralarge")
    EXTRA_LARGE,

    @SerialName("mega")
    MEGA,

    @SerialName("")
    UNKNOWN,
}

internal fun ImageSizeApiModel.toDomainModel() = ImageSize.valueOf(this.name)

internal fun ImageSizeApiModel.toRoomModel(): ImageSizeRoomModel? =
    when (this) {
        ImageSizeApiModel.MEDIUM -> ImageSizeRoomModel.MEDIUM
        ImageSizeApiModel.SMALL -> ImageSizeRoomModel.SMALL
        ImageSizeApiModel.LARGE -> ImageSizeRoomModel.LARGE
        ImageSizeApiModel.EXTRA_LARGE -> ImageSizeRoomModel.EXTRA_LARGE
        ImageSizeApiModel.MEGA -> ImageSizeRoomModel.MEGA
        ImageSizeApiModel.UNKNOWN -> null
    }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/SearchAlbumResultsApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class SearchAlbumResultsApiModel(
    @SerialName("albummatches") val albumMatches: AlbumListApiModel,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TagApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import com.igorwojda.showcase.feature.album.data.datasource.database.model.TagRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Tag
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class TagApiModel(
    @SerialName("name") val name: String,
)

internal fun TagApiModel.toDomainModel() =
    Tag(
        name = this.name,
    )

internal fun TagApiModel.toRoomModel() =
    TagRoomModel(
        name = this.name,
    )


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TagListApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class TagListApiModel(
    @SerialName("tag") val tag: List<TagApiModel>,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TrackApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import com.igorwojda.showcase.feature.album.data.datasource.database.model.TrackRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Track
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class TrackApiModel(
    @SerialName("name") val name: String,
    @SerialName("duration") val duration: Int? = null,
)

internal fun TrackApiModel.toDomainModel() =
    Track(
        name = this.name,
        duration = this.duration,
    )

internal fun TrackApiModel.toRoomModel() =
    TrackRoomModel(
        name = this.name,
        duration = this.duration,
    )


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TrackListApiModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class TrackListApiModel(
    @SerialName("track") val track: List<TrackApiModel>,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/response/GetAlbumInfoResponse.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.response

import com.igorwojda.showcase.feature.album.data.datasource.api.model.AlbumApiModel
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class GetAlbumInfoResponse(
    @SerialName("album") val album: AlbumApiModel,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/response/SearchAlbumResponse.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.response

import com.igorwojda.showcase.feature.album.data.datasource.api.model.SearchAlbumResultsApiModel
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class SearchAlbumResponse(
    @SerialName("results") val results: SearchAlbumResultsApiModel,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/service/AlbumRetrofitService.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.service

import com.igorwojda.showcase.feature.album.data.datasource.api.response.GetAlbumInfoResponse
import com.igorwojda.showcase.feature.album.data.datasource.api.response.SearchAlbumResponse
import com.igorwojda.showcase.feature.base.data.retrofit.ApiResult
import retrofit2.http.POST
import retrofit2.http.Query

internal interface AlbumRetrofitService {
    @POST("./?method=album.search")
    suspend fun searchAlbumAsync(
        @Query("album") phrase: String?,
        @Query("limit") limit: Int = 60,
    ): ApiResult<SearchAlbumResponse>

    @POST("./?method=album.getInfo")
    suspend fun getAlbumInfoAsync(
        @Query("artist") artistName: String,
        @Query("album") albumName: String,
        @Query("mbid") mbId: String?,
    ): ApiResult<GetAlbumInfoResponse>
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/AlbumDao.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.igorwojda.showcase.feature.album.data.datasource.database.model.AlbumRoomModel

@Dao
internal interface AlbumDao {
    @Query("SELECT * FROM albums")
    suspend fun getAll(): List<AlbumRoomModel>

    @Query("SELECT * FROM albums where artist = :artistName and name = :albumName and mbId = :mbId")
    suspend fun getAlbum(
        artistName: String,
        albumName: String,
        mbId: String?,
    ): AlbumRoomModel

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAlbums(albums: List<AlbumRoomModel>)
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/AlbumDatabase.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database

import androidx.room.Database
import androidx.room.RoomDatabase
import com.igorwojda.showcase.feature.album.data.datasource.database.model.AlbumRoomModel

@Database(entities = [AlbumRoomModel::class], version = 1, exportSchema = false)
internal abstract class AlbumDatabase : RoomDatabase() {
    abstract fun albums(): AlbumDao
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/AlbumRoomModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database.model

import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import com.igorwojda.showcase.feature.album.domain.model.Album
import kotlinx.serialization.json.Json

@Entity(tableName = "albums")
@TypeConverters(
    AlbumImageRoomTypeConverter::class,
    AlbumTrackRoomTypeConverter::class,
    AlbumTagRoomTypeConverter::class,
)
internal data class AlbumRoomModel(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val mbId: String,
    val name: String,
    val artist: String,
    val images: List<ImageRoomModel> = listOf(),
    val tracks: List<TrackRoomModel>?,
    val tags: List<TagRoomModel>?,
)

internal fun AlbumRoomModel.toDomainModel() =
    Album(
        this.name,
        this.artist,
        this.mbId,
        this.images.mapNotNull { it.toDomainModel() },
        this.tracks?.map { it.toDomainModel() },
        this.tags?.map { it.toDomainModel() },
    )

internal class AlbumImageRoomTypeConverter {
    @TypeConverter
    fun stringToList(data: String?) = data?.let { Json.decodeFromString<List<ImageRoomModel>>(it) } ?: listOf()

    @TypeConverter
    fun listToString(someObjects: List<ImageRoomModel>): String = Json.encodeToString(someObjects)
}

internal class AlbumTrackRoomTypeConverter {
    @TypeConverter
    fun stringToList(data: String?) = data?.let { Json.decodeFromString<List<TrackRoomModel>>(it) } ?: listOf()

    @TypeConverter
    fun listToString(someObjects: List<TrackRoomModel>): String = Json.encodeToString(someObjects)
}

internal class AlbumTagRoomTypeConverter {
    @TypeConverter
    fun stringToList(data: String?) = data?.let { Json.decodeFromString<List<TagRoomModel>>(it) } ?: listOf()

    @TypeConverter
    fun listToString(someObjects: List<TagRoomModel>): String = Json.encodeToString(someObjects)
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/ImageRoomModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database.model

import com.igorwojda.showcase.feature.album.domain.model.Image
import kotlinx.serialization.Serializable

@Serializable
internal data class ImageRoomModel(
    val url: String,
    val size: ImageSizeRoomModel,
)

internal fun ImageRoomModel.toDomainModel() = this.size.toDomainModel()?.let { Image(this.url, it) }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/ImageSizeRoomModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database.model

import com.igorwojda.showcase.feature.album.domain.enum.ImageSize

internal enum class ImageSizeRoomModel {
    MEDIUM,
    SMALL,
    LARGE,
    EXTRA_LARGE,
    MEGA,
}

internal fun ImageSizeRoomModel.toDomainModel(): ImageSize? =
    when (this) {
        ImageSizeRoomModel.MEDIUM -> ImageSize.MEDIUM
        ImageSizeRoomModel.SMALL -> ImageSize.SMALL
        ImageSizeRoomModel.LARGE -> ImageSize.LARGE
        ImageSizeRoomModel.EXTRA_LARGE -> ImageSize.EXTRA_LARGE
        ImageSizeRoomModel.MEGA -> ImageSize.MEGA
    }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/TagRoomModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database.model

import com.igorwojda.showcase.feature.album.domain.model.Tag
import kotlinx.serialization.Serializable

@Serializable
internal data class TagRoomModel(
    val name: String,
)

internal fun TagRoomModel.toDomainModel() =
    Tag(
        name = this.name,
    )


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/TrackRoomModel.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.database.model

import com.igorwojda.showcase.feature.album.domain.model.Track
import kotlinx.serialization.Serializable

@Serializable
internal data class TrackRoomModel(
    val name: String,
    val duration: Int? = null,
)

internal fun TrackRoomModel.toDomainModel() =
    Track(
        name = this.name,
        duration = this.duration,
    )


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/AlbumMapper.kt
================================================
package com.igorwojda.showcase.feature.album.data.mapper

import com.igorwojda.showcase.feature.album.data.datasource.api.model.AlbumApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.ImageSizeApiModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.AlbumRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Album

internal class AlbumMapper(
    private val imageMapper: ImageMapper,
    private val trackMapper: TrackMapper,
    private val tagMapper: TagMapper,
) {
    fun apiToRoom(apiModel: AlbumApiModel) =
        AlbumRoomModel(
            mbId = apiModel.mbId ?: "",
            name = apiModel.name,
            artist = apiModel.artist,
            images = apiModel.images?.mapNotNull { imageMapper.apiToRoom(it) } ?: listOf(),
            tracks = apiModel.tracks?.track?.map { trackMapper.apiToRoom(it) },
            tags = apiModel.tags?.tag?.map { tagMapper.apiToRoom(it) },
        )

    fun apiToDomain(apiModel: AlbumApiModel): Album {
        val images =
            apiModel.images
                ?.filterNot { it.size == ImageSizeApiModel.UNKNOWN || it.url.isBlank() }
                ?.map { imageMapper.apiToDomain(it) }

        return Album(
            mbId = apiModel.mbId,
            name = apiModel.name,
            artist = apiModel.artist,
            images = images ?: listOf(),
            tracks = apiModel.tracks?.track?.map { trackMapper.apiToDomain(it) },
            tags = apiModel.tags?.tag?.map { tagMapper.apiToDomain(it) },
        )
    }

    fun roomToDomain(roomModel: AlbumRoomModel) =
        Album(
            roomModel.name,
            roomModel.artist,
            roomModel.mbId,
            roomModel.images.mapNotNull { imageMapper.roomToDomain(it) },
            roomModel.tracks?.map { trackMapper.roomToDomain(it) },
            roomModel.tags?.map { tagMapper.roomToDomain(it) },
        )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/ImageMapper.kt
================================================
package com.igorwojda.showcase.feature.album.data.mapper

import com.igorwojda.showcase.feature.album.data.datasource.api.model.ImageApiModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.ImageRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Image

internal class ImageMapper(
    private val imageSizeMapper: ImageSizeMapper,
) {
    fun apiToDomain(apiModel: ImageApiModel) =
        Image(
            url = apiModel.url,
            size = imageSizeMapper.apiToDomain(apiModel.size),
        )

    fun apiToRoom(apiModel: ImageApiModel) =
        imageSizeMapper.apiToRoom(apiModel.size)?.let {
            ImageRoomModel(apiModel.url, it)
        }

    fun roomToDomain(roomModel: ImageRoomModel) =
        imageSizeMapper.roomToDomain(roomModel.size)?.let {
            Image(roomModel.url, it)
        }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/ImageSizeMapper.kt
================================================
package com.igorwojda.showcase.feature.album.data.mapper

import com.igorwojda.showcase.feature.album.data.datasource.api.model.ImageSizeApiModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.ImageSizeRoomModel
import com.igorwojda.showcase.feature.album.domain.enum.ImageSize

internal class ImageSizeMapper {
    fun apiToDomain(apiModel: ImageSizeApiModel): ImageSize = ImageSize.valueOf(apiModel.name)

    fun apiToRoom(apiModel: ImageSizeApiModel): ImageSizeRoomModel? =
        when (apiModel) {
            ImageSizeApiModel.MEDIUM -> ImageSizeRoomModel.MEDIUM
            ImageSizeApiModel.SMALL -> ImageSizeRoomModel.SMALL
            ImageSizeApiModel.LARGE -> ImageSizeRoomModel.LARGE
            ImageSizeApiModel.EXTRA_LARGE -> ImageSizeRoomModel.EXTRA_LARGE
            ImageSizeApiModel.MEGA -> ImageSizeRoomModel.MEGA
            ImageSizeApiModel.UNKNOWN -> null
        }

    fun roomToDomain(roomModel: ImageSizeRoomModel): ImageSize? =
        when (roomModel) {
            ImageSizeRoomModel.MEDIUM -> ImageSize.MEDIUM
            ImageSizeRoomModel.SMALL -> ImageSize.SMALL
            ImageSizeRoomModel.LARGE -> ImageSize.LARGE
            ImageSizeRoomModel.EXTRA_LARGE -> ImageSize.EXTRA_LARGE
            ImageSizeRoomModel.MEGA -> ImageSize.MEGA
        }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/TagMapper.kt
================================================
package com.igorwojda.showcase.feature.album.data.mapper

import com.igorwojda.showcase.feature.album.data.datasource.api.model.TagApiModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.TagRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Tag

internal class TagMapper {
    fun apiToDomain(apiModel: TagApiModel): Tag =
        Tag(
            name = apiModel.name,
        )

    fun apiToRoom(apiModel: TagApiModel): TagRoomModel =
        TagRoomModel(
            name = apiModel.name,
        )

    fun roomToDomain(roomModel: TagRoomModel): Tag =
        Tag(
            name = roomModel.name,
        )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/TrackMapper.kt
================================================
package com.igorwojda.showcase.feature.album.data.mapper

import com.igorwojda.showcase.feature.album.data.datasource.api.model.TrackApiModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.TrackRoomModel
import com.igorwojda.showcase.feature.album.domain.model.Track

internal class TrackMapper {
    fun apiToDomain(apiModel: TrackApiModel): Track =
        Track(
            name = apiModel.name,
            duration = apiModel.duration,
        )

    fun apiToRoom(apiModel: TrackApiModel): TrackRoomModel =
        TrackRoomModel(
            name = apiModel.name,
            duration = apiModel.duration,
        )

    fun roomToDomain(roomModel: TrackRoomModel): Track =
        Track(
            name = roomModel.name,
            duration = roomModel.duration,
        )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/repository/AlbumRepositoryImpl.kt
================================================
package com.igorwojda.showcase.feature.album.data.repository

import com.igorwojda.showcase.feature.album.data.datasource.api.service.AlbumRetrofitService
import com.igorwojda.showcase.feature.album.data.datasource.database.AlbumDao
import com.igorwojda.showcase.feature.album.data.mapper.AlbumMapper
import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.album.domain.repository.AlbumRepository
import com.igorwojda.showcase.feature.base.data.retrofit.ApiResult
import com.igorwojda.showcase.feature.base.domain.result.Result
import timber.log.Timber

internal class AlbumRepositoryImpl(
    private val albumRetrofitService: AlbumRetrofitService,
    private val albumDao: AlbumDao,
    private val albumMapper: AlbumMapper,
) : AlbumRepository {
    override suspend fun searchAlbum(phrase: String?): Result<List<Album>> =
        when (val apiResult = albumRetrofitService.searchAlbumAsync(phrase)) {
            is ApiResult.Success -> {
                val albums =
                    apiResult
                        .data
                        .results
                        .albumMatches
                        .album
                        .also { albumsApiModels ->
                            val albumsRoomModels = albumsApiModels.map { albumMapper.apiToRoom(it) }
                            albumDao.insertAlbums(albumsRoomModels)
                        }.map { albumMapper.apiToDomain(it) }

                Result.Success(albums)
            }
            is ApiResult.Error -> {
                Result.Failure()
            }
            is ApiResult.Exception -> {
                Timber.e(apiResult.throwable)

                val albums =
                    albumDao
                        .getAll()
                        .map { albumMapper.roomToDomain(it) }

                Result.Success(albums)
            }
        }

    override suspend fun getAlbumInfo(
        artistName: String,
        albumName: String,
        mbId: String?,
    ): Result<Album> =
        when (val apiResult = albumRetrofitService.getAlbumInfoAsync(artistName, albumName, mbId)) {
            is ApiResult.Success -> {
                val album =
                    apiResult
                        .data
                        .album
                        .let { albumMapper.apiToDomain(it) }

                Result.Success(album)
            }
            is ApiResult.Error -> {
                Result.Failure()
            }
            is ApiResult.Exception -> {
                Timber.e(apiResult.throwable)

                val album =
                    albumDao
                        .getAlbum(artistName, albumName, mbId)
                        .let { albumMapper.roomToDomain(it) }

                Result.Success(album)
            }
        }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/DomainModule.kt
================================================
package com.igorwojda.showcase.feature.album.domain

import com.igorwojda.showcase.feature.album.domain.usecase.GetAlbumListUseCase
import com.igorwojda.showcase.feature.album.domain.usecase.GetAlbumUseCase
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module

internal val domainModule =
    module {

        singleOf(::GetAlbumListUseCase)

        singleOf(::GetAlbumUseCase)
    }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/enum/ImageSize.kt
================================================
package com.igorwojda.showcase.feature.album.domain.enum

internal enum class ImageSize {
    SMALL,
    MEDIUM,
    LARGE,
    EXTRA_LARGE,
    MEGA,
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Album.kt
================================================
package com.igorwojda.showcase.feature.album.domain.model

import com.igorwojda.showcase.feature.album.domain.enum.ImageSize

// Images are loaded for both album list and album detail instance
// Tracks and Tags are only loaded for album detail instance (not album list instance)
internal data class Album(
    val name: String,
    val artist: String,
    val mbId: String? = null,
    val images: List<Image> = emptyList(),
    val tracks: List<Track>? = null,
    val tags: List<Tag>? = null,
) {
    val id: String = "$artist - $name"

    fun getDefaultImageUrl() = images.firstOrNull { it.size == ImageSize.EXTRA_LARGE }?.url
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Image.kt
================================================
package com.igorwojda.showcase.feature.album.domain.model

import com.igorwojda.showcase.feature.album.domain.enum.ImageSize

internal data class Image(
    val url: String,
    val size: ImageSize,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Tag.kt
================================================
package com.igorwojda.showcase.feature.album.domain.model

internal data class Tag(
    val name: String,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Track.kt
================================================
package com.igorwojda.showcase.feature.album.domain.model

internal data class Track(
    val name: String,
    val duration: Int? = null,
)


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/repository/AlbumRepository.kt
================================================
package com.igorwojda.showcase.feature.album.domain.repository

import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.base.domain.result.Result

internal interface AlbumRepository {
    suspend fun getAlbumInfo(
        artistName: String,
        albumName: String,
        mbId: String?,
    ): Result<Album>

    suspend fun searchAlbum(phrase: String?): Result<List<Album>>
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/usecase/GetAlbumListUseCase.kt
================================================
package com.igorwojda.showcase.feature.album.domain.usecase

import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.album.domain.repository.AlbumRepository
import com.igorwojda.showcase.feature.base.domain.result.Result
import com.igorwojda.showcase.feature.base.domain.result.mapSuccess

internal class GetAlbumListUseCase(
    private val albumRepository: AlbumRepository,
) {
    suspend operator fun invoke(query: String?): Result<List<Album>> {
        val result =
            albumRepository
                .searchAlbum(query)
                .mapSuccess {
                    val albumsWithImages = value.filter { it.getDefaultImageUrl() != null }

                    copy(value = albumsWithImages)
                }

        return result
    }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/usecase/GetAlbumUseCase.kt
================================================
package com.igorwojda.showcase.feature.album.domain.usecase

import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.album.domain.repository.AlbumRepository
import com.igorwojda.showcase.feature.base.domain.result.Result

internal class GetAlbumUseCase(
    private val albumRepository: AlbumRepository,
) {
    suspend operator fun invoke(
        artistName: String,
        albumName: String,
        mbId: String?,
    ): Result<Album> = albumRepository.getAlbumInfo(artistName, albumName, mbId)
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/PresentationModule.kt
================================================
package com.igorwojda.showcase.feature.album.presentation

import coil.ImageLoader
import com.igorwojda.showcase.feature.album.presentation.screen.albumdetail.AlbumDetailViewModel
import com.igorwojda.showcase.feature.album.presentation.screen.albumlist.AlbumListViewModel
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module

internal val presentationModule =
    module {

        // AlbumList
        viewModelOf(::AlbumListViewModel)

        singleOf(::ImageLoader)

        // AlbumDetails
        viewModelOf(::AlbumDetailViewModel)
    }


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/composable/SearchBarComposable.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.composable

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import com.igorwojda.showcase.feature.album.R
import com.igorwojda.showcase.feature.base.common.res.Dimen
import kotlinx.coroutines.delay

@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    onSearch: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    val minimumProductQuerySize = 1
    val delayBeforeSubmittingQuery = 300L

    var textFieldValue by remember(query) { mutableStateOf(TextFieldValue(query)) }

    // Debounce search - only trigger search after user stops typing
    LaunchedEffect(textFieldValue.text, onSearch, onQueryChange) {
        if (textFieldValue.text.length >= minimumProductQuerySize) {
            delay(delayBeforeSubmittingQuery)
            onSearch(textFieldValue.text)
            onQueryChange(textFieldValue.text)
        } else if (textFieldValue.text.isEmpty()) {
            // Immediately search when query is cleared
            onSearch("")
            onQueryChange("")
        }
    }

    OutlinedTextField(
        value = textFieldValue,
        modifier =
            modifier
                .fillMaxWidth()
                .padding(Dimen.spaceM),
        onValueChange = { newValue ->
            textFieldValue = newValue
        },
        placeholder = {
            Text(stringResource(R.string.album_list_search_placeholder))
        },
        leadingIcon = {
            Icon(
                imageVector = Icons.Default.Search,
                contentDescription = "Search",
            )
        },
        trailingIcon =
            if (textFieldValue.text.isNotEmpty()) {
                {
                    IconButton(
                        onClick = {
                            textFieldValue = TextFieldValue("")
                            onSearch("")
                            onQueryChange("")
                        },
                    ) {
                        Icon(
                            imageVector = Icons.Default.Clear,
                            contentDescription = "Clear search",
                        )
                    }
                }
            } else {
                null
            },
        singleLine = true,
        colors =
            OutlinedTextFieldDefaults.colors(
                unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
                focusedBorderColor = MaterialTheme.colorScheme.primary,
            ),
    )
}

@Preview
@Composable
private fun SearchBarPreview() {
    SearchBar(
        query = "Sample query",
        onQueryChange = { },
        onSearch = { },
    )
}

@Preview
@Composable
private fun SearchBarEmptyPreview() {
    SearchBar(
        query = "",
        onQueryChange = { },
        onSearch = { },
    )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailAction.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail

import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseAction

internal sealed interface AlbumDetailAction : BaseAction<AlbumDetailUiState> {
    object AlbumLoadStart : AlbumDetailAction {
        override fun reduce(state: AlbumDetailUiState) = AlbumDetailUiState.Loading
    }

    class AlbumLoadSuccess(
        private val album: Album,
    ) : AlbumDetailAction {
        override fun reduce(state: AlbumDetailUiState) =
            AlbumDetailUiState.Content(
                artistName = album.artist,
                albumName = album.name,
                coverImageUrl = album.getDefaultImageUrl() ?: "",
                tracks = album.tracks,
                tags = album.tags,
            )
    }

    object AlbumLoadFailure : AlbumDetailAction {
        override fun reduce(state: AlbumDetailUiState) = AlbumDetailUiState.Error
    }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailScreen.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail

import androidx.compose.foundation.layout.Arrangement
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.igorwojda.showcase.feature.album.R
import com.igorwojda.showcase.feature.album.domain.model.Tag
import com.igorwojda.showcase.feature.album.domain.model.Track
import com.igorwojda.showcase.feature.album.presentation.util.TimeUtil
import com.igorwojda.showcase.feature.base.common.res.Dimen
import com.igorwojda.showcase.feature.base.presentation.compose.composable.ErrorAnim
import com.igorwojda.showcase.feature.base.presentation.compose.composable.LoadingIndicator
import com.igorwojda.showcase.feature.base.presentation.compose.composable.PlaceholderImage
import com.igorwojda.showcase.feature.base.presentation.compose.composable.TextTitleLarge
import com.igorwojda.showcase.feature.base.presentation.compose.composable.TextTitleMedium
import org.koin.androidx.compose.koinViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlbumDetailScreen(
    albumName: String,
    artistName: String,
    albumMbId: String?,
    modifier: Modifier = Modifier,
    onBackClick: () -> Unit = {},
) {
    val viewModel: AlbumDetailViewModel = koinViewModel()
    // Initialize the viewModel with args when the composable enters composition
    LaunchedEffect(Unit) {
        viewModel.onInit(albumName, artistName, albumMbId)
    }

    val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()

    Scaffold(
        modifier = modifier,
        topBar = {
            TopAppBar(
                title = { Text(text = albumName) },
                navigationIcon = {
                    IconButton(onClick = onBackClick) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = stringResource(R.string.album_detail_back),
                        )
                    }
                },
            )
        },
    ) { innerPadding ->
        when (val currentUiState = uiState) {
            AlbumDetailUiState.Error -> {
                ErrorAnim()
            }
            AlbumDetailUiState.Loading -> {
                LoadingIndicator()
            }
            is AlbumDetailUiState.Content -> {
                AlbumDetailContent(
                    content = currentUiState,
                    modifier = Modifier.padding(innerPadding),
                )
            }
        }
    }
}

@Composable
private fun AlbumDetailContent(
    content: AlbumDetailUiState.Content,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier =
            modifier
                .padding(horizontal = Dimen.screenContentPadding)
                .verticalScroll(rememberScrollState()),
    ) {
        ElevatedCard(
            modifier =
                Modifier
                    .padding(Dimen.spaceM)
                    .wrapContentSize()
                    .size(320.dp)
                    .align(CenterHorizontally),
        ) {
            PlaceholderImage(
                url = content.coverImageUrl,
                contentDescription = stringResource(id = R.string.album_detail_cover_content_description),
                modifier = Modifier.fillMaxWidth(),
            )
        }
        Spacer(modifier = Modifier.height(Dimen.spaceL))
        TextTitleLarge(text = content.albumName)
        TextTitleMedium(text = content.artistName)
        Spacer(modifier = Modifier.height(Dimen.spaceL))

        if (content.tags?.isNotEmpty() == true) {
            Tags(content.tags)
            Spacer(modifier = Modifier.height(Dimen.spaceL))
        }

        if (content.tracks?.isNotEmpty() == true) {
            TextTitleMedium(text = stringResource(id = R.string.album_detail_tracks))
            Spacer(modifier = Modifier.height(Dimen.spaceS))
            Tracks(content.tracks)
        }
    }
}

@Composable
private fun Tags(tags: List<Tag>?) {
    FlowRow(
        horizontalArrangement = Arrangement.spacedBy(Dimen.spaceS),
        verticalArrangement = Arrangement.spacedBy(Dimen.spaceS),
    ) {
        tags?.forEach { tag ->
            SuggestionChip(
                onClick = { },
                label = { Text(tag.name) },
            )
        }
    }
}

@Composable
internal fun Tracks(tracks: List<Track>?) {
    tracks?.forEach { track ->
        TrackItem(track)
    }
}

@Composable
internal fun TrackItem(track: Track) {
    Row {
        Icon(Icons.Outlined.Star, contentDescription = null)
        Spacer(modifier = Modifier.width(Dimen.spaceS))

        val text =
            buildString {
                append(track.name)
                track.duration?.let { duration ->
                    append(" ${TimeUtil.formatTime(duration)}")
                }
            }

        Text(text = text)
    }
}

@Preview
@Composable
private fun TrackItemPreview() {
    TrackItem(
        track =
            Track(
                name = "Sample Track",
                duration = 180, // 3 minutes in seconds
            ),
    )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailUiState.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail

import androidx.compose.runtime.Immutable
import com.igorwojda.showcase.feature.album.domain.model.Tag
import com.igorwojda.showcase.feature.album.domain.model.Track
import com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseState

@Immutable
internal sealed interface AlbumDetailUiState : BaseState {
    @Immutable
    data class Content(
        val albumName: String = "",
        val artistName: String = "",
        val coverImageUrl: String = "",
        val tracks: List<Track>? = emptyList(),
        val tags: List<Tag>? = emptyList(),
    ) : AlbumDetailUiState

    @Immutable
    data object Loading : AlbumDetailUiState

    @Immutable
    data object Error : AlbumDetailUiState
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailViewModel.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail

import androidx.lifecycle.viewModelScope
import com.igorwojda.showcase.feature.album.domain.usecase.GetAlbumUseCase
import com.igorwojda.showcase.feature.base.domain.result.Result.Failure
import com.igorwojda.showcase.feature.base.domain.result.Result.Success
import com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseViewModel
import kotlinx.coroutines.launch

internal class AlbumDetailViewModel(
    private val getAlbumUseCase: GetAlbumUseCase,
) : BaseViewModel<AlbumDetailUiState, AlbumDetailAction>(AlbumDetailUiState.Loading) {
    fun onInit(
        albumName: String,
        artistName: String,
        albumMbId: String?,
    ) {
        getAlbum(albumName, artistName, albumMbId)
    }

    private fun getAlbum(
        albumName: String,
        artistName: String,
        albumMbId: String?,
    ) {
        sendAction(AlbumDetailAction.AlbumLoadStart)

        viewModelScope.launch {
            getAlbumUseCase(artistName, albumName, albumMbId).also {
                when (it) {
                    is Success -> {
                        sendAction(AlbumDetailAction.AlbumLoadSuccess(it.value))
                    }
                    is Failure -> {
                        sendAction(AlbumDetailAction.AlbumLoadFailure)
                    }
                }
            }
        }
    }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListAction.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumlist

import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseAction

internal sealed interface AlbumListAction : BaseAction<AlbumListUiState> {
    object AlbumListLoadStart : AlbumListAction {
        override fun reduce(state: AlbumListUiState) = AlbumListUiState.Loading
    }

    class AlbumListLoadSuccess(
        private val albums: List<Album>,
    ) : AlbumListAction {
        override fun reduce(state: AlbumListUiState) = AlbumListUiState.Content(albums)
    }

    object AlbumListLoadFailure : AlbumListAction {
        override fun reduce(state: AlbumListUiState) = AlbumListUiState.Error
    }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListScreen.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumlist

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.ElevatedCard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.igorwojda.showcase.feature.album.R
import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.album.presentation.composable.SearchBar
import com.igorwojda.showcase.feature.base.common.res.Dimen
import com.igorwojda.showcase.feature.base.presentation.compose.composable.ErrorAnim
import com.igorwojda.showcase.feature.base.presentation.compose.composable.LoadingIndicator
import com.igorwojda.showcase.feature.base.presentation.compose.composable.PlaceholderImage
import org.koin.androidx.compose.koinViewModel

@Composable
fun AlbumListScreen(
    modifier: Modifier = Modifier,
    onNavigateToAlbumDetail: ((artistName: String, albumName: String, albumMbId: String?) -> Unit)? = null,
) {
    val viewModel: AlbumListViewModel = koinViewModel()

    val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()

    var searchQuery by remember { mutableStateOf("") }

    LaunchedEffect(Unit) {
        viewModel.onInit()
    }

    Column(modifier = modifier.fillMaxSize()) {
        // Search bar
        SearchBar(
            query = searchQuery,
            onQueryChange = { newQuery ->
                searchQuery = newQuery
            },
            onSearch = { query ->
                if (query.isNotEmpty()) {
                    viewModel.onInit(query)
                } else {
                    viewModel.onInit()
                }
            },
        )

        // Content
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center,
        ) {
            when (val currentUiState = uiState) { // Extract to local variable for smart casting
                AlbumListUiState.Error -> ErrorAnim()
                AlbumListUiState.Loading -> LoadingIndicator()
                is AlbumListUiState.Content -> AlbumListContent(currentUiState, onNavigateToAlbumDetail)
            }
        }
    }
}

@Composable
private fun AlbumListContent(
    uiState: AlbumListUiState.Content,
    onNavigateToAlbumDetail: ((String, String, String?) -> Unit)?,
) {
    AlbumGrid(
        albums = uiState.albums,
        onAlbumClick = { album ->
            onNavigateToAlbumDetail?.invoke(album.artist, album.name, album.mbId)
        },
    )
}

@Composable
private fun AlbumGrid(
    albums: List<Album>,
    onAlbumClick: (Album) -> Unit,
) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(Dimen.imageSize),
    ) {
        items(items = albums, key = { it.id }) { album ->
            ElevatedCard(
                modifier =
                    Modifier
                        .padding(Dimen.spaceS)
                        .wrapContentSize(),
                onClick = { onAlbumClick(album) },
            ) {
                PlaceholderImage(
                    url = album.getDefaultImageUrl(),
                    contentDescription = stringResource(id = R.string.album_detail_cover_content_description),
                    modifier = Modifier.size(Dimen.imageSize),
                )
            }
        }
    }
}

@Preview
@Composable
private fun AlbumGridPreview() {
    val sampleAlbums =
        listOf(
            Album(
                name = "Sample Album 1",
                artist = "Sample Artist",
                mbId = null,
                images = emptyList(),
            ),
            Album(
                name = "Sample Album 2",
                artist = "Sample Artist 2",
                mbId = null,
                images = emptyList(),
            ),
        )

    AlbumGrid(
        albums = sampleAlbums,
        onAlbumClick = { },
    )
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListUiState.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumlist

import androidx.compose.runtime.Immutable
import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseState

@Immutable
internal sealed interface AlbumListUiState : BaseState {
    @Immutable
    data class Content(
        val albums: List<Album>,
    ) : AlbumListUiState

    @Immutable
    data object Loading : AlbumListUiState

    @Immutable
    data object Error : AlbumListUiState
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListViewModel.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.screen.albumlist

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.igorwojda.showcase.feature.album.domain.usecase.GetAlbumListUseCase
import com.igorwojda.showcase.feature.base.domain.result.Result
import com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

internal class AlbumListViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val getAlbumListUseCase: GetAlbumListUseCase,
) : BaseViewModel<AlbumListUiState, AlbumListAction>(AlbumListUiState.Loading) {
    private var job: Job? = null

    fun onInit(query: String? = (savedStateHandle[SAVED_QUERY_KEY] as? String) ?: DEFAULT_QUERY_NAME) {
        getAlbumList(query)
    }

    private fun getAlbumList(query: String?) {
        if (job != null) {
            job?.cancel()
            job = null
        }

        savedStateHandle[SAVED_QUERY_KEY] = query

        sendAction(AlbumListAction.AlbumListLoadStart)

        job =
            viewModelScope.launch {
                getAlbumListUseCase(query).also { result ->
                    val albumListAction =
                        when (result) {
                            is Result.Success -> {
                                AlbumListAction.AlbumListLoadSuccess(result.value)
                            }
                            is Result.Failure -> {
                                AlbumListAction.AlbumListLoadFailure
                            }
                        }

                    sendAction(albumListAction)
                }
            }
    }

    companion object {
        const val DEFAULT_QUERY_NAME = "Jackson"
        private const val SAVED_QUERY_KEY = "query"
    }
}


================================================
FILE: feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/util/TimeUtil.kt
================================================
package com.igorwojda.showcase.feature.album.presentation.util

object TimeUtil {
    /**
     * provides a String representation of the given time.
     * @return `seconds` in mm:ss format
     */
    internal fun formatTime(seconds: Int): String {
        val secondsInMinute = 60
        val secondsInHour = 3600

        @Suppress("detekt.ImplicitDefaultLocale")
        return String.format("%02d:%02d", seconds % secondsInHour / secondsInMinute, seconds % secondsInMinute)
    }
}


================================================
FILE: feature/album/src/main/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="album_list_search_hint">Search Album</string>
    <string name="album_list_title">Albums</string>
    <string name="album_list_search_placeholder">Search albums…</string>

    <string name="album_detail_cover_content_description">Album Cover</string>
    <string name="album_detail_tracks">Tracks</string>
    <string name="album_detail_back">Back</string>
</resources>


================================================
FILE: feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/DataFixtures.kt
================================================
package com.igorwojda.showcase.feature.album.data

import com.igorwojda.showcase.feature.album.data.datasource.api.model.AlbumApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.AlbumListApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.ImageApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.ImageSizeApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.SearchAlbumResultsApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.TagApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.TagListApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.TrackApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.model.TrackListApiModel
import com.igorwojda.showcase.feature.album.data.datasource.api.response.SearchAlbumResponse
import com.igorwojda.showcase.feature.album.data.datasource.database.model.AlbumRoomModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.ImageRoomModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.ImageSizeRoomModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.TagRoomModel
import com.igorwojda.showcase.feature.album.data.datasource.database.model.TrackRoomModel

object DataFixtures {
    internal fun getAlbumsApiModel() =
        listOf(
            getAlbumApiModel("mbid1", "album1", "artist1"),
        )

    internal fun getAlbumsRoomModels() =
        listOf(
            getAlbumRoomModel(1, "mbid1", "album1", "artist1"),
            getAlbumRoomModel(2, "mbid2", "album2", "artist2"),
        )

    internal fun getAlbumApiModel(
        mbId: String = "mbId",
        name: String = "album",
        artist: String = "artist",
        images: List<ImageApiModel>? = listOf(getImageModelApiModel()),
        tracks: TrackListApiModel = TrackListApiModel(getTrackModelApiModel()),
        tags: TagListApiModel = TagListApiModel(getTagModelApiModel()),
    ): AlbumApiModel =
        AlbumApiModel(
            mbId,
            name,
            artist,
            images,
            tracks,
            tags,
        )

    internal fun getImageModelApiModel(
        url: String = "url_${ImageSizeApiModel.EXTRA_LARGE}",
        size: ImageSizeApiModel = ImageSizeApiModel.EXTRA_LARGE,
    ) = ImageApiModel(url, size)

    private fun getTrackModelApiModel(
        name: String = "track",
        duration: Int? = 12,
    ) = listOf(TrackApiModel(name, duration))

    private fun getTagModelApiModel(name: String = "tag") = listOf(TagApiModel(name))

    private fun getAlbumRoomModel(
        id: Int = 0,
        mbId: String = "mbId",
        name: String = "album",
        artist: String = "artist",
        images: List<ImageRoomModel> = listOf(getImageRoomModel()),
        tracks: List<TrackRoomModel> = listOf(getTrackRoomModel()),
        tags: List<TagRoomModel> = listOf(getTagRoomModel()),
    ): AlbumRoomModel =
        AlbumRoomModel(
            id,
            mbId,
            name,
            artist,
            images,
            tracks,
            tags,
        )

    private fun getImageRoomModel(
        url: String = "url_${ImageSizeApiModel.EXTRA_LARGE}",
        size: ImageSizeRoomModel = ImageSizeRoomModel.EXTRA_LARGE,
    ) = ImageRoomModel(url, size)

    private fun getTrackRoomModel(
        name: String = "track",
        duration: Int = 12,
    ) = TrackRoomModel(name, duration)

    private fun getTagRoomModel(name: String = "tag") = TagRoomModel(name)

    object ApiResponse {
        internal fun getSearchAlbum() =
            SearchAlbumResponse(
                SearchAlbumResultsApiModel(
                    AlbumListApiModel(getAlbumsApiModel()),
                ),
            )
    }
}


================================================
FILE: feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/AlbumApiModelTest.kt
================================================
package com.igorwojda.showcase.feature.album.data.datasource.api.model

import com.igorwojda.showcase.feature.album.data.DataFixtures
import com.igorwojda.showcase.feature.album.domain.enum.ImageSize
import com.igorwojda.showcase.feature.album.domain.model.Album
import com.igorwojda.showcase.feature.album.domain.model.Tag
import com.igorwojda.showcase.feature.album.domain.model.Track
import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test

class AlbumApiModelTest {
    @Test
    fun `data model with full data maps to AlbumDomainModel`() {
        // given
        val sut = DataFixtures.getAlbumApiModel()

        // when
        val domainModel = sut.toDomainModel()

        // then
        domainModel shouldBeEqualTo
            Album(
                sut.name,
                sut.artist,
                sut.mbId,
                sut.images?.map { it.toDomainModel() } ?: listOf(),
                sut.tracks?.track?.map { it.toDomainModel() },
                sut.tags?.tag?.map { it.toDomainModel() },
            )
    }

    @Test
    fun `data model with missing data maps
Download .txt
gitextract_jwuhl568/

├── .editorconfig
├── .github/
│   ├── stale.yml
│   └── workflows/
│       ├── auto-approve.yml
│       ├── check.yml
│       ├── claude-code-review.yml
│       └── claude.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DeveloperReadme.md
├── README.md
├── app/
│   ├── build.gradle.kts
│   ├── proguard-rules.pro
│   └── src/
│       ├── debug/
│       │   ├── AndroidManifest.xml
│       │   └── res/
│       │       └── xml/
│       │           └── network_security_config.xml
│       └── main/
│           ├── AndroidManifest.xml
│           ├── kotlin/
│           │   └── com/
│           │       └── igorwojda/
│           │           └── showcase/
│           │               └── app/
│           │                   ├── AppKoinModule.kt
│           │                   ├── ShowcaseApplication.kt
│           │                   ├── data/
│           │                   │   └── api/
│           │                   │       ├── AuthenticationInterceptor.kt
│           │                   │       └── UserAgentInterceptor.kt
│           │                   └── presentation/
│           │                       ├── BottomNavigationBar.kt
│           │                       ├── MainShowcaseActivity.kt
│           │                       ├── MainShowcaseScreen.kt
│           │                       ├── NavigationRoute.kt
│           │                       └── util/
│           │                           └── NavigationDestinationLogger.kt
│           └── res/
│               ├── drawable/
│               │   ├── ic_favorite.xml
│               │   ├── ic_launcher_foreground.xml
│               │   ├── ic_launcher_foreground_themed.xml
│               │   ├── ic_music_library.xml
│               │   └── ic_settings.xml
│               ├── mipmap-anydpi/
│               │   ├── ic_launcher.xml
│               │   └── ic_launcher_round.xml
│               ├── mipmap-anydpi-v33/
│               │   ├── ic_launcher.xml
│               │   └── ic_launcher_round.xml
│               ├── values/
│               │   ├── colors.xml
│               │   ├── ic_launcher_background.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── xml/
│                   └── data_extraction_rules.xml
├── build-logic/
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src/
│       └── main/
│           └── kotlin/
│               └── com/
│                   └── igorwojda/
│                       └── showcase/
│                           └── buildlogic/
│                               ├── AboutLibrariesConventionPlugin.kt
│                               ├── ApplicationConventionPlugin.kt
│                               ├── DetektConventionPlugin.kt
│                               ├── EasyLauncherConventionPlugin.kt
│                               ├── FeatureConventionPlugin.kt
│                               ├── KotlinConventionPlugin.kt
│                               ├── LibraryConventionPlugin.kt
│                               ├── SpotlessConventionPlugin.kt
│                               ├── TestConventionLibraryPlugin.kt
│                               ├── TestConventionPlugin.kt
│                               ├── config/
│                               │   └── JavaBuildConfig.kt
│                               └── ext/
│                                   ├── BuildConfigExt.kt
│                                   ├── DependencyHandlerExt.kt
│                                   ├── PackagingExt.kt
│                                   └── ProjectExt.kt
├── build.gradle.kts
├── detekt.yml
├── feature/
│   ├── album/
│   │   ├── build.gradle.kts
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── kotlin/
│   │       │   │   └── com/
│   │       │   │       └── igorwojda/
│   │       │   │           └── showcase/
│   │       │   │               └── feature/
│   │       │   │                   └── album/
│   │       │   │                       ├── AlbumKoinModule.kt
│   │       │   │                       ├── data/
│   │       │   │                       │   ├── DataModule.kt
│   │       │   │                       │   ├── datasource/
│   │       │   │                       │   │   ├── api/
│   │       │   │                       │   │   │   ├── model/
│   │       │   │                       │   │   │   │   ├── AlbumApiModel.kt
│   │       │   │                       │   │   │   │   ├── AlbumListApiModel.kt
│   │       │   │                       │   │   │   │   ├── ImageApiModel.kt
│   │       │   │                       │   │   │   │   ├── ImageSizeApiModel.kt
│   │       │   │                       │   │   │   │   ├── SearchAlbumResultsApiModel.kt
│   │       │   │                       │   │   │   │   ├── TagApiModel.kt
│   │       │   │                       │   │   │   │   ├── TagListApiModel.kt
│   │       │   │                       │   │   │   │   ├── TrackApiModel.kt
│   │       │   │                       │   │   │   │   └── TrackListApiModel.kt
│   │       │   │                       │   │   │   ├── response/
│   │       │   │                       │   │   │   │   ├── GetAlbumInfoResponse.kt
│   │       │   │                       │   │   │   │   └── SearchAlbumResponse.kt
│   │       │   │                       │   │   │   └── service/
│   │       │   │                       │   │   │       └── AlbumRetrofitService.kt
│   │       │   │                       │   │   └── database/
│   │       │   │                       │   │       ├── AlbumDao.kt
│   │       │   │                       │   │       ├── AlbumDatabase.kt
│   │       │   │                       │   │       └── model/
│   │       │   │                       │   │           ├── AlbumRoomModel.kt
│   │       │   │                       │   │           ├── ImageRoomModel.kt
│   │       │   │                       │   │           ├── ImageSizeRoomModel.kt
│   │       │   │                       │   │           ├── TagRoomModel.kt
│   │       │   │                       │   │           └── TrackRoomModel.kt
│   │       │   │                       │   ├── mapper/
│   │       │   │                       │   │   ├── AlbumMapper.kt
│   │       │   │                       │   │   ├── ImageMapper.kt
│   │       │   │                       │   │   ├── ImageSizeMapper.kt
│   │       │   │                       │   │   ├── TagMapper.kt
│   │       │   │                       │   │   └── TrackMapper.kt
│   │       │   │                       │   └── repository/
│   │       │   │                       │       └── AlbumRepositoryImpl.kt
│   │       │   │                       ├── domain/
│   │       │   │                       │   ├── DomainModule.kt
│   │       │   │                       │   ├── enum/
│   │       │   │                       │   │   └── ImageSize.kt
│   │       │   │                       │   ├── model/
│   │       │   │                       │   │   ├── Album.kt
│   │       │   │                       │   │   ├── Image.kt
│   │       │   │                       │   │   ├── Tag.kt
│   │       │   │                       │   │   └── Track.kt
│   │       │   │                       │   ├── repository/
│   │       │   │                       │   │   └── AlbumRepository.kt
│   │       │   │                       │   └── usecase/
│   │       │   │                       │       ├── GetAlbumListUseCase.kt
│   │       │   │                       │       └── GetAlbumUseCase.kt
│   │       │   │                       └── presentation/
│   │       │   │                           ├── PresentationModule.kt
│   │       │   │                           ├── composable/
│   │       │   │                           │   └── SearchBarComposable.kt
│   │       │   │                           ├── screen/
│   │       │   │                           │   ├── albumdetail/
│   │       │   │                           │   │   ├── AlbumDetailAction.kt
│   │       │   │                           │   │   ├── AlbumDetailScreen.kt
│   │       │   │                           │   │   ├── AlbumDetailUiState.kt
│   │       │   │                           │   │   └── AlbumDetailViewModel.kt
│   │       │   │                           │   └── albumlist/
│   │       │   │                           │       ├── AlbumListAction.kt
│   │       │   │                           │       ├── AlbumListScreen.kt
│   │       │   │                           │       ├── AlbumListUiState.kt
│   │       │   │                           │       └── AlbumListViewModel.kt
│   │       │   │                           └── util/
│   │       │   │                               └── TimeUtil.kt
│   │       │   └── res/
│   │       │       └── values/
│   │       │           └── strings.xml
│   │       └── test/
│   │           └── kotlin/
│   │               └── com/
│   │                   └── igorwojda/
│   │                       └── showcase/
│   │                           └── feature/
│   │                               └── album/
│   │                                   ├── data/
│   │                                   │   ├── DataFixtures.kt
│   │                                   │   ├── datasource/
│   │                                   │   │   └── api/
│   │                                   │   │       └── model/
│   │                                   │   │           ├── AlbumApiModelTest.kt
│   │                                   │   │           ├── ImageApiModelTest.kt
│   │                                   │   │           └── ImageSizeApiModelTest.kt
│   │                                   │   ├── mapper/
│   │                                   │   │   ├── AlbumMapperTest.kt
│   │                                   │   │   ├── ImageMapperTest.kt
│   │                                   │   │   ├── ImageSizeMapperTest.kt
│   │                                   │   │   ├── TagMapperTest.kt
│   │                                   │   │   └── TrackMapperTest.kt
│   │                                   │   └── repository/
│   │                                   │       └── AlbumRepositoryImplTest.kt
│   │                                   ├── domain/
│   │                                   │   ├── DomainFixtures.kt
│   │                                   │   ├── model/
│   │                                   │   │   └── AlbumTest.kt
│   │                                   │   └── usecase/
│   │                                   │       ├── GetAlbumListUseCaseTest.kt
│   │                                   │       └── GetAlbumUseCaseTest.kt
│   │                                   └── presentation/
│   │                                       └── screen/
│   │                                           ├── albumdetail/
│   │                                           │   └── AlbumDetailViewModelTest.kt
│   │                                           └── albumlist/
│   │                                               └── AlbumListViewModelTest.kt
│   ├── base/
│   │   ├── build.gradle.kts
│   │   ├── consumer-rules.pro
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       └── main/
│   │           ├── AndroidManifest.xml
│   │           ├── kotlin/
│   │           │   └── com/
│   │           │       └── igorwojda/
│   │           │           └── showcase/
│   │           │               └── feature/
│   │           │                   └── base/
│   │           │                       ├── common/
│   │           │                       │   ├── delegate/
│   │           │                       │   │   └── Observer.kt
│   │           │                       │   └── res/
│   │           │                       │       └── Dimen.kt
│   │           │                       ├── data/
│   │           │                       │   └── retrofit/
│   │           │                       │       ├── ApiResult.kt
│   │           │                       │       ├── ApiResultAdapterFactory.kt
│   │           │                       │       ├── ApiResultCall.kt
│   │           │                       │       └── ApiResultCallAdapter.kt
│   │           │                       ├── domain/
│   │           │                       │   └── result/
│   │           │                       │       ├── Result.kt
│   │           │                       │       └── ResultExt.kt
│   │           │                       ├── presentation/
│   │           │                       │   ├── compose/
│   │           │                       │   │   └── composable/
│   │           │                       │   │       ├── ErrorAnim.kt
│   │           │                       │   │       ├── Loading.kt
│   │           │                       │   │       ├── Lottie.kt
│   │           │                       │   │       ├── PlaceholderImage.kt
│   │           │                       │   │       ├── TextTitleLarge.kt
│   │           │                       │   │       ├── TextTitleMedium.kt
│   │           │                       │   │       └── UnderConstructionAnim.kt
│   │           │                       │   └── viewmodel/
│   │           │                       │       ├── BaseAction.kt
│   │           │                       │       ├── BaseState.kt
│   │           │                       │       ├── BaseViewModel.kt
│   │           │                       │       └── StateTimeTravelDebugger.kt
│   │           │                       └── util/
│   │           │                           └── TimberLogTags.kt
│   │           └── res/
│   │               ├── drawable/
│   │               │   ├── ic_search.xml
│   │               │   ├── image_placeholder_1.xml
│   │               │   ├── image_placeholder_2.xml
│   │               │   └── image_placeholder_3.xml
│   │               ├── raw/
│   │               │   ├── lottie_building_screen.json
│   │               │   └── lottie_error_screen.json
│   │               └── values/
│   │                   ├── color_palete.xml
│   │                   ├── ids.xml
│   │                   ├── strings.xml
│   │                   └── styles.xml
│   ├── favourite/
│   │   ├── build.gradle.kts
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       └── main/
│   │           ├── AndroidManifest.xml
│   │           └── kotlin/
│   │               └── com/
│   │                   └── igorwojda/
│   │                       └── showcase/
│   │                           └── feature/
│   │                               └── favourite/
│   │                                   ├── FavouriteKoinModule.kt
│   │                                   ├── data/
│   │                                   │   └── DataModule.kt
│   │                                   ├── domain/
│   │                                   │   └── DomainModule.kt
│   │                                   └── presentation/
│   │                                       ├── PresentationModule.kt
│   │                                       └── screen/
│   │                                           └── favourite/
│   │                                               └── FavouriteScreen.kt
│   └── settings/
│       ├── build.gradle.kts
│       ├── proguard-rules.pro
│       └── src/
│           ├── main/
│           │   ├── AndroidManifest.xml
│           │   ├── kotlin/
│           │   │   └── com/
│           │   │       └── igorwojda/
│           │   │           └── showcase/
│           │   │               └── feature/
│           │   │                   └── settings/
│           │   │                       ├── SettingsKoinModule.kt
│           │   │                       ├── data/
│           │   │                       │   └── DataModule.kt
│           │   │                       ├── domain/
│           │   │                       │   └── DomainModule.kt
│           │   │                       └── presentation/
│           │   │                           ├── PresentationModule.kt
│           │   │                           └── screen/
│           │   │                               ├── aboutlibraries/
│           │   │                               │   ├── AboutLibrariesAction.kt
│           │   │                               │   ├── AboutLibrariesScreen.kt
│           │   │                               │   ├── AboutLibrariesUiState.kt
│           │   │                               │   └── AboutLibrariesViewModel.kt
│           │   │                               └── settings/
│           │   │                                   ├── SettingsAction.kt
│           │   │                                   ├── SettingsScreen.kt
│           │   │                                   ├── SettingsUiState.kt
│           │   │                                   └── SettingsViewModel.kt
│           │   └── res/
│           │       └── values/
│           │           └── strings.xml
│           └── test/
│               └── kotlin/
│                   └── com/
│                       └── igorwojda/
│                           └── showcase/
│                               └── feature/
│                                   └── settings/
│                                       └── presentation/
│                                           └── screen/
│                                               ├── aboutlibraries/
│                                               │   └── AboutLibrariesViewModelTest.kt
│                                               └── settings/
│                                                   └── SettingsViewModelTest.kt
├── gradle/
│   ├── libs.versions.toml
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── konsist-test/
│   ├── build.gradle.kts
│   └── src/
│       └── test/
│           └── kotlin/
│               └── com/
│                   └── igorwojda/
│                       └── showcase/
│                           └── konsisttest/
│                               ├── AndroidKonsistTest.kt
│                               ├── CleanArchitectureKonsistTest.kt
│                               ├── GeneralKonsistTest.kt
│                               ├── ModuleKonsistTest.kt
│                               ├── TestKonsistTest.kt
│                               ├── UseCaseKonsistTest.kt
│                               └── ViewModelKonsistTest.kt
├── library/
│   └── test-utils/
│       ├── build.gradle.kts
│       ├── proguard-rules.pro
│       └── src/
│           └── main/
│               ├── AndroidManifest.xml
│               └── kotlin/
│                   └── com/
│                       └── igorwojda/
│                           └── showcase/
│                               └── library/
│                                   └── testutils/
│                                       ├── CoroutinesTestDispatcherExtension.kt
│                                       └── InstantTaskExecutorExtension.kt
├── renovate.json
└── settings.gradle.kts
Condensed preview — 206 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,703K chars).
[
  {
    "path": ".editorconfig",
    "chars": 255,
    "preview": "root = true\n\n[*.{kt,kts}]\nend_of_line = lf\ninsert_final_newline = true\nmax_line_length = 140\nktlint_function_naming_igno"
  },
  {
    "path": ".github/stale.yml",
    "chars": 699,
    "preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
  },
  {
    "path": ".github/workflows/auto-approve.yml",
    "chars": 413,
    "preview": "name: Auto Approve\n\non: pull_request_target\n\njobs:\n  auto-approve:\n    runs-on: ubuntu-latest\n    permissions:\n      pul"
  },
  {
    "path": ".github/workflows/check.yml",
    "chars": 4959,
    "preview": "name: Check\n\non:\n  push:\n    branches: [ main ] # Just in case main was not up to date while merging PR\n  pull_request:\n"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "chars": 1979,
    "preview": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    # Optional: Only run on specific file"
  },
  {
    "path": ".github/workflows/claude.yml",
    "chars": 1947,
    "preview": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issue"
  },
  {
    "path": ".gitignore",
    "chars": 632,
    "preview": "# Built application files\n*.apk\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generate"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3219,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1516,
    "preview": "# Contributing\n\nWe appreciate contributions of any kind - new contributions\nare welcome whether it's through bug reports"
  },
  {
    "path": "DeveloperReadme.md",
    "chars": 1675,
    "preview": "# Developer Readme\n\n## Detekt\n\n- [Detekt configuration](https://detekt.dev/docs/introduction/configurations/) contains l"
  },
  {
    "path": "README.md",
    "chars": 28609,
    "preview": "# 💎 Android Showcase 2.0\n\n[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.x-blue.svg)](https://kotlinlang.org)\n"
  },
  {
    "path": "app/build.gradle.kts",
    "chars": 945,
    "preview": "import com.igorwojda.showcase.buildlogic.ext.buildConfigFieldFromGradleProperty\n\nplugins {\n    id(\"com.igorwojda.showcas"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/debug/AndroidManifest.xml",
    "chars": 204,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <appli"
  },
  {
    "path": "app/src/debug/res/xml/network_security_config.xml",
    "chars": 573,
    "preview": "<network-security-config>\n    <domain-config cleartextTrafficPermitted=\"true\">\n        <domain includeSubdomains=\"true\">"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 1342,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/AppKoinModule.kt",
    "chars": 3574,
    "preview": "package com.igorwojda.showcase.app\n\nimport com.igorwojda.showcase.app.data.api.AuthenticationInterceptor\nimport com.igor"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/ShowcaseApplication.kt",
    "chars": 1034,
    "preview": "package com.igorwojda.showcase.app\n\nimport android.app.Application\nimport com.igorwojda.showcase.feature.album.featureAl"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/data/api/AuthenticationInterceptor.kt",
    "chars": 691,
    "preview": "package com.igorwojda.showcase.app.data.api\n\nimport okhttp3.Interceptor\nimport okhttp3.Response\n\nclass AuthenticationInt"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/data/api/UserAgentInterceptor.kt",
    "chars": 897,
    "preview": "package com.igorwojda.showcase.app.data.api\n\nimport com.igorwojda.showcase.app.BuildConfig\nimport okhttp3.Interceptor\nim"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/presentation/BottomNavigationBar.kt",
    "chars": 3942,
    "preview": "package com.igorwojda.showcase.app.presentation\n\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.Strin"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/presentation/MainShowcaseActivity.kt",
    "chars": 1533,
    "preview": "package com.igorwojda.showcase.app.presentation\n\nimport android.os.Build\nimport android.os.Bundle\nimport androidx.activi"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/presentation/MainShowcaseScreen.kt",
    "chars": 4078,
    "preview": "package com.igorwojda.showcase.app.presentation\n\nimport android.os.Bundle\nimport androidx.compose.foundation.layout.fill"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/presentation/NavigationRoute.kt",
    "chars": 551,
    "preview": "package com.igorwojda.showcase.app.presentation\n\nimport kotlinx.serialization.Serializable\n\nsealed interface NavigationR"
  },
  {
    "path": "app/src/main/kotlin/com/igorwojda/showcase/app/presentation/util/NavigationDestinationLogger.kt",
    "chars": 2478,
    "preview": "package com.igorwojda.showcase.app.presentation.util\n\nimport android.os.Bundle\nimport androidx.navigation.NavDestination"
  },
  {
    "path": "app/src/main/res/drawable/ic_favorite.xml",
    "chars": 679,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "chars": 540,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"108dp\"\n        android:height="
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground_themed.xml",
    "chars": 506,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"108dp\"\n        android:height="
  },
  {
    "path": "app/src/main/res/drawable/ic_music_library.xml",
    "chars": 613,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_settings.xml",
    "chars": 977,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/mipmap-anydpi/ic_launcher.xml",
    "chars": 338,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- References the base adaptive icon without monochrome support -->\n<adaptive-i"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi/ic_launcher_round.xml",
    "chars": 329,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Base round adaptive icon without monochrome support -->\n<adaptive-icon xmlns"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml",
    "chars": 424,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Extends the base adaptive icon with monochrome support for Android 13+ -->\n<"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml",
    "chars": 413,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Round adaptive icon with monochrome support for Android 13+ -->\n<adaptive-ic"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 206,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Splash Screen Colors -->\n    <color name=\"splash_background\""
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "chars": 120,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#000000</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 262,
    "preview": "<resources>\n    <string name=\"app_name\">Showcase</string>\n\n    <string name=\"bottom_navigation_albums\">Albums</string>\n "
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 623,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Splash Screen theme for pre-Android 12 compatibility -->\n   "
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "chars": 722,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n "
  },
  {
    "path": "build-logic/build.gradle.kts",
    "chars": 3547,
    "preview": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    `kotlin-dsl`\n}\n\ngroup = \"com.igorwojda.showcase.buildlog"
  },
  {
    "path": "build-logic/settings.gradle.kts",
    "chars": 379,
    "preview": "rootProject.name = \"build-logic\"\n\n@Suppress(\"UnstableApiUsage\")\ndependencyResolutionManagement {\n    repositoriesMode.se"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/AboutLibrariesConventionPlugin.kt",
    "chars": 867,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.mikepenz.aboutlibraries.plugin.AboutLibrariesExtension\nimport com."
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ApplicationConventionPlugin.kt",
    "chars": 3901,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.android.build.api.dsl.ApplicationExtension\nimport com.igorwojda.sh"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/DetektConventionPlugin.kt",
    "chars": 1778,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport io.gitlab.arturbosch.detekt.Detekt\nimport org.gradle.api.Plugin\nimport"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/EasyLauncherConventionPlugin.kt",
    "chars": 1363,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.project.starter.easylauncher.filter.ChromeLikeFilter\nimport com.pr"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/FeatureConventionPlugin.kt",
    "chars": 4058,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.android.build.api.dsl.LibraryExtension\nimport com.igorwojda.showca"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/KotlinConventionPlugin.kt",
    "chars": 1312,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.igorwojda.showcase.buildlogic.config.JavaBuildConfig\nimport org.gr"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/LibraryConventionPlugin.kt",
    "chars": 2176,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.android.build.api.dsl.LibraryExtension\nimport com.igorwojda.showca"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/SpotlessConventionPlugin.kt",
    "chars": 1327,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.diffplug.gradle.spotless.SpotlessExtension\nimport com.igorwojda.sh"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/TestConventionLibraryPlugin.kt",
    "chars": 2307,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.android.build.api.dsl.LibraryExtension\nimport com.igorwojda.showca"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/TestConventionPlugin.kt",
    "chars": 1019,
    "preview": "package com.igorwojda.showcase.buildlogic\n\nimport com.adarshr.gradle.testlogger.TestLoggerExtension\nimport com.adarshr.g"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/config/JavaBuildConfig.kt",
    "chars": 934,
    "preview": "package com.igorwojda.showcase.buildlogic.config\n\nimport org.gradle.api.JavaVersion\nimport java.io.File\n\nobject JavaBuil"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/BuildConfigExt.kt",
    "chars": 1813,
    "preview": "package com.igorwojda.showcase.buildlogic.ext\n\nimport com.android.build.api.dsl.ApplicationDefaultConfig\nimport com.andr"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/DependencyHandlerExt.kt",
    "chars": 1369,
    "preview": "package com.igorwojda.showcase.buildlogic.ext\n\nimport org.gradle.accessors.dm.LibrariesForLibs\nimport org.gradle.api.Pro"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/PackagingExt.kt",
    "chars": 332,
    "preview": "package com.igorwojda.showcase.buildlogic.ext\n\nimport com.android.build.api.dsl.Packaging\n\nfun Packaging.excludeLicenseA"
  },
  {
    "path": "build-logic/src/main/kotlin/com/igorwojda/showcase/buildlogic/ext/ProjectExt.kt",
    "chars": 432,
    "preview": "package com.igorwojda.showcase.buildlogic.ext\n\nimport org.gradle.accessors.dm.LibrariesForLibs\nimport org.gradle.api.Pro"
  },
  {
    "path": "build.gradle.kts",
    "chars": 712,
    "preview": "plugins {\n    // Convention plugins\n    id(\"com.igorwojda.showcase.convention.detekt\")\n    id(\"com.igorwojda.showcase.co"
  },
  {
    "path": "detekt.yml",
    "chars": 20056,
    "preview": "build:\n  maxIssues: 0\n  excludeCorrectable: false\n  weights:\n    # complexity: 2\n    # LongParameterList: 1\n    # style:"
  },
  {
    "path": "feature/album/build.gradle.kts",
    "chars": 132,
    "preview": "plugins {\n    id(\"com.igorwojda.showcase.convention.feature\")\n}\n\nandroid {\n    namespace = \"com.igorwojda.showcase.featu"
  },
  {
    "path": "feature/album/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "feature/album/src/main/AndroidManifest.xml",
    "chars": 52,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/AlbumKoinModule.kt",
    "chars": 361,
    "preview": "package com.igorwojda.showcase.feature.album\n\nimport com.igorwojda.showcase.feature.album.data.dataModule\nimport com.igo"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/DataModule.kt",
    "chars": 1501,
    "preview": "package com.igorwojda.showcase.feature.album.data\n\nimport androidx.room.Room\nimport com.igorwojda.showcase.feature.album"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/AlbumApiModel.kt",
    "chars": 1526,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/AlbumListApiModel.kt",
    "chars": 267,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport kotlinx.serialization.SerialName\nimport k"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/ImageApiModel.kt",
    "chars": 689,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/ImageSizeApiModel.kt",
    "chars": 1126,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/SearchAlbumResultsApiModel.kt",
    "chars": 288,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport kotlinx.serialization.SerialName\nimport k"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TagApiModel.kt",
    "chars": 572,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TagListApiModel.kt",
    "chars": 259,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport kotlinx.serialization.SerialName\nimport k"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TrackApiModel.kt",
    "chars": 709,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/TrackListApiModel.kt",
    "chars": 267,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport kotlinx.serialization.SerialName\nimport k"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/response/GetAlbumInfoResponse.kt",
    "chars": 351,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.response\n\nimport com.igorwojda.showcase.feature.album.d"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/response/SearchAlbumResponse.kt",
    "chars": 380,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.response\n\nimport com.igorwojda.showcase.feature.album.d"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/service/AlbumRetrofitService.kt",
    "chars": 856,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.service\n\nimport com.igorwojda.showcase.feature.album.da"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/AlbumDao.kt",
    "chars": 742,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database\n\nimport androidx.room.Dao\nimport androidx.room.Ins"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/AlbumDatabase.kt",
    "chars": 402,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database\n\nimport androidx.room.Database\nimport androidx.roo"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/AlbumRoomModel.kt",
    "chars": 1923,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database.model\n\nimport androidx.room.Entity\nimport androidx"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/ImageRoomModel.kt",
    "chars": 392,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database.model\n\nimport com.igorwojda.showcase.feature.album"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/ImageSizeRoomModel.kt",
    "chars": 607,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database.model\n\nimport com.igorwojda.showcase.feature.album"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/TagRoomModel.kt",
    "chars": 339,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database.model\n\nimport com.igorwojda.showcase.feature.album"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/datasource/database/model/TrackRoomModel.kt",
    "chars": 412,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.database.model\n\nimport com.igorwojda.showcase.feature.album"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/AlbumMapper.kt",
    "chars": 1932,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/ImageMapper.kt",
    "chars": 862,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/ImageSizeMapper.kt",
    "chars": 1318,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/TagMapper.kt",
    "chars": 662,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/mapper/TrackMapper.kt",
    "chars": 815,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/data/repository/AlbumRepositoryImpl.kt",
    "chars": 2834,
    "preview": "package com.igorwojda.showcase.feature.album.data.repository\n\nimport com.igorwojda.showcase.feature.album.data.datasourc"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/DomainModule.kt",
    "chars": 401,
    "preview": "package com.igorwojda.showcase.feature.album.domain\n\nimport com.igorwojda.showcase.feature.album.domain.usecase.GetAlbum"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/enum/ImageSize.kt",
    "chars": 153,
    "preview": "package com.igorwojda.showcase.feature.album.domain.enum\n\ninternal enum class ImageSize {\n    SMALL,\n    MEDIUM,\n    LAR"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Album.kt",
    "chars": 634,
    "preview": "package com.igorwojda.showcase.feature.album.domain.model\n\nimport com.igorwojda.showcase.feature.album.domain.enum.Image"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Image.kt",
    "chars": 201,
    "preview": "package com.igorwojda.showcase.feature.album.domain.model\n\nimport com.igorwojda.showcase.feature.album.domain.enum.Image"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Tag.kt",
    "chars": 108,
    "preview": "package com.igorwojda.showcase.feature.album.domain.model\n\ninternal data class Tag(\n    val name: String,\n)\n"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/model/Track.kt",
    "chars": 141,
    "preview": "package com.igorwojda.showcase.feature.album.domain.model\n\ninternal data class Track(\n    val name: String,\n    val dura"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/repository/AlbumRepository.kt",
    "chars": 427,
    "preview": "package com.igorwojda.showcase.feature.album.domain.repository\n\nimport com.igorwojda.showcase.feature.album.domain.model"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/usecase/GetAlbumListUseCase.kt",
    "chars": 804,
    "preview": "package com.igorwojda.showcase.feature.album.domain.usecase\n\nimport com.igorwojda.showcase.feature.album.domain.model.Al"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/domain/usecase/GetAlbumUseCase.kt",
    "chars": 547,
    "preview": "package com.igorwojda.showcase.feature.album.domain.usecase\n\nimport com.igorwojda.showcase.feature.album.domain.model.Al"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/PresentationModule.kt",
    "chars": 605,
    "preview": "package com.igorwojda.showcase.feature.album.presentation\n\nimport coil.ImageLoader\nimport com.igorwojda.showcase.feature"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/composable/SearchBarComposable.kt",
    "chars": 3800,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.composable\n\nimport androidx.compose.foundation.layout.fillMaxW"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailAction.kt",
    "chars": 1008,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail\n\nimport com.igorwojda.showcase.feature.albu"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailScreen.kt",
    "chars": 6617,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail\n\nimport androidx.compose.foundation.layout."
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailUiState.kt",
    "chars": 784,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail\n\nimport androidx.compose.runtime.Immutable\n"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailViewModel.kt",
    "chars": 1409,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail\n\nimport androidx.lifecycle.viewModelScope\ni"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListAction.kt",
    "chars": 761,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumlist\n\nimport com.igorwojda.showcase.feature.album."
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListScreen.kt",
    "chars": 4702,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumlist\n\nimport androidx.compose.foundation.layout.Bo"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListUiState.kt",
    "chars": 541,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumlist\n\nimport androidx.compose.runtime.Immutable\nim"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListViewModel.kt",
    "chars": 1839,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumlist\n\nimport androidx.lifecycle.SavedStateHandle\ni"
  },
  {
    "path": "feature/album/src/main/kotlin/com/igorwojda/showcase/feature/album/presentation/util/TimeUtil.kt",
    "chars": 487,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.util\n\nobject TimeUtil {\n    /**\n     * provides a String repre"
  },
  {
    "path": "feature/album/src/main/res/values/strings.xml",
    "chars": 439,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"album_list_search_hint\">Search Album</string>\n    <"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/DataFixtures.kt",
    "chars": 3879,
    "preview": "package com.igorwojda.showcase.feature.album.data\n\nimport com.igorwojda.showcase.feature.album.data.datasource.api.model"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/AlbumApiModelTest.kt",
    "chars": 2538,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/ImageApiModelTest.kt",
    "chars": 1064,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport com.igorwojda.showcase.feature.album.data"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/datasource/api/model/ImageSizeApiModelTest.kt",
    "chars": 583,
    "preview": "package com.igorwojda.showcase.feature.album.data.datasource.api.model\n\nimport org.junit.jupiter.api.Test\n\nclass ImageSi"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/mapper/AlbumMapperTest.kt",
    "chars": 5978,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/mapper/ImageMapperTest.kt",
    "chars": 2146,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/mapper/ImageSizeMapperTest.kt",
    "chars": 1490,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/mapper/TagMapperTest.kt",
    "chars": 1172,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/mapper/TrackMapperTest.kt",
    "chars": 1298,
    "preview": "package com.igorwojda.showcase.feature.album.data.mapper\n\nimport com.igorwojda.showcase.feature.album.data.datasource.ap"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/data/repository/AlbumRepositoryImplTest.kt",
    "chars": 5639,
    "preview": "package com.igorwojda.showcase.feature.album.data.repository\n\nimport com.igorwojda.showcase.feature.album.data.DataFixtu"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/domain/DomainFixtures.kt",
    "chars": 1068,
    "preview": "package com.igorwojda.showcase.feature.album.domain\n\nimport com.igorwojda.showcase.feature.album.domain.enum.ImageSize\ni"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/domain/model/AlbumTest.kt",
    "chars": 732,
    "preview": "package com.igorwojda.showcase.feature.album.domain.model\n\nimport com.igorwojda.showcase.feature.album.domain.DomainFixt"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/domain/usecase/GetAlbumListUseCaseTest.kt",
    "chars": 2294,
    "preview": "package com.igorwojda.showcase.feature.album.domain.usecase\n\nimport com.igorwojda.showcase.feature.album.data.repository"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/domain/usecase/GetAlbumUseCaseTest.kt",
    "chars": 1649,
    "preview": "package com.igorwojda.showcase.feature.album.domain.usecase\n\nimport com.igorwojda.showcase.feature.album.data.repository"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumdetail/AlbumDetailViewModelTest.kt",
    "chars": 2468,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumdetail\n\nimport com.igorwojda.showcase.feature.albu"
  },
  {
    "path": "feature/album/src/test/kotlin/com/igorwojda/showcase/feature/album/presentation/screen/albumlist/AlbumListViewModelTest.kt",
    "chars": 2156,
    "preview": "package com.igorwojda.showcase.feature.album.presentation.screen.albumlist\n\nimport androidx.lifecycle.SavedStateHandle\ni"
  },
  {
    "path": "feature/base/build.gradle.kts",
    "chars": 131,
    "preview": "plugins {\n    id(\"com.igorwojda.showcase.convention.feature\")\n}\n\nandroid {\n    namespace = \"com.igorwojda.showcase.featu"
  },
  {
    "path": "feature/base/consumer-rules.pro",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "feature/base/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "feature/base/src/main/AndroidManifest.xml",
    "chars": 52,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/common/delegate/Observer.kt",
    "chars": 516,
    "preview": "package com.igorwojda.showcase.feature.base.common.delegate\n\nimport kotlin.properties.ObservableProperty\nimport kotlin.p"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/common/res/Dimen.kt",
    "chars": 290,
    "preview": "package com.igorwojda.showcase.feature.base.common.res\n\nimport androidx.compose.ui.unit.dp\n\nobject Dimen {\n    val space"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/data/retrofit/ApiResult.kt",
    "chars": 758,
    "preview": "package com.igorwojda.showcase.feature.base.data.retrofit\n\nsealed interface ApiResult<T> {\n    /**\n     * Represents a n"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/data/retrofit/ApiResultAdapterFactory.kt",
    "chars": 849,
    "preview": "package com.igorwojda.showcase.feature.base.data.retrofit\n\nimport retrofit2.Call\nimport retrofit2.CallAdapter\nimport ret"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/data/retrofit/ApiResultCall.kt",
    "chars": 2203,
    "preview": "package com.igorwojda.showcase.feature.base.data.retrofit\n\nimport okhttp3.Request\nimport okio.Timeout\nimport retrofit2.C"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/data/retrofit/ApiResultCallAdapter.kt",
    "chars": 392,
    "preview": "package com.igorwojda.showcase.feature.base.data.retrofit\n\nimport retrofit2.Call\nimport retrofit2.CallAdapter\nimport jav"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/domain/result/Result.kt",
    "chars": 252,
    "preview": "package com.igorwojda.showcase.feature.base.domain.result\n\nsealed interface Result<out T> {\n    data class Success<T>(\n "
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/domain/result/ResultExt.kt",
    "chars": 253,
    "preview": "package com.igorwojda.showcase.feature.base.domain.result\n\ninline fun <T> Result<T>.mapSuccess(crossinline onResult: Res"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/ErrorAnim.kt",
    "chars": 719,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.compose.foundation.layout.B"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/Loading.kt",
    "chars": 914,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.compose.foundation.layout.B"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/Lottie.kt",
    "chars": 2115,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.annotation.RawRes\nimport an"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/PlaceholderImage.kt",
    "chars": 1659,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.compose.material3.Surface\ni"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/TextTitleLarge.kt",
    "chars": 615,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.compose.material3.MaterialT"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/TextTitleMedium.kt",
    "chars": 620,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.compose.material3.MaterialT"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/compose/composable/UnderConstructionAnim.kt",
    "chars": 445,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.compose.composable\n\nimport androidx.compose.runtime.Composable\n"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/viewmodel/BaseAction.kt",
    "chars": 136,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.viewmodel\n\ninterface BaseAction<State> {\n    fun reduce(state: "
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/viewmodel/BaseState.kt",
    "chars": 88,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.viewmodel\n\ninterface BaseState\n"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/viewmodel/BaseViewModel.kt",
    "chars": 1330,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.viewmodel\n\nimport androidx.lifecycle.ViewModel\nimport com.igorw"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/presentation/viewmodel/StateTimeTravelDebugger.kt",
    "chars": 3206,
    "preview": "package com.igorwojda.showcase.feature.base.presentation.viewmodel\n\nimport com.igorwojda.showcase.feature.base.util.Timb"
  },
  {
    "path": "feature/base/src/main/kotlin/com/igorwojda/showcase/feature/base/util/TimberLogTags.kt",
    "chars": 693,
    "preview": "package com.igorwojda.showcase.feature.base.util\n\n/**\n * Centralized log tags for consistent logging throughout the appl"
  },
  {
    "path": "feature/base/src/main/res/drawable/ic_search.xml",
    "chars": 528,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "feature/base/src/main/res/drawable/image_placeholder_1.xml",
    "chars": 1100,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"88dp\"\n    android:height=\"88dp\"\n  "
  },
  {
    "path": "feature/base/src/main/res/drawable/image_placeholder_2.xml",
    "chars": 1086,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"88dp\"\n    android:height=\"88dp\"\n  "
  },
  {
    "path": "feature/base/src/main/res/drawable/image_placeholder_3.xml",
    "chars": 988,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"88dp\"\n    android:height=\"88dp\"\n  "
  },
  {
    "path": "feature/base/src/main/res/raw/lottie_building_screen.json",
    "chars": 142018,
    "preview": "{\n    \"v\": \"4.11.0\",\n    \"fr\": 29.9700012207031,\n    \"ip\": 0,\n    \"op\": 150.000006109625,\n    \"w\": 800,\n    \"h\": 800,\n  "
  },
  {
    "path": "feature/base/src/main/res/raw/lottie_error_screen.json",
    "chars": 1177782,
    "preview": "{\n    \"v\": \"5.2.1\",\n    \"fr\": 30,\n    \"ip\": 0,\n    \"op\": 30,\n    \"w\": 296,\n    \"h\": 296,\n    \"nm\": \"error screen\",\n    \""
  },
  {
    "path": "feature/base/src/main/res/values/color_palete.xml",
    "chars": 280,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"outer_space\">#2B2E30</color>\n    <color name=\"shark\""
  },
  {
    "path": "feature/base/src/main/res/values/ids.xml",
    "chars": 123,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item name=\"tag_navigation_destination_id\" type=\"id\"/>\n</resource"
  },
  {
    "path": "feature/base/src/main/res/values/strings.xml",
    "chars": 163,
    "preview": "<resources>\n    <string name=\"common_under_construction\">Under construction</string>\n    <string name=\"common_data_not_f"
  },
  {
    "path": "feature/base/src/main/res/values/styles.xml",
    "chars": 217,
    "preview": "<resources>\n    <style name=\"Theme.Showcase\" parent=\"Theme.Material3.DynamicColors.DayNight\">\n        <item name=\"window"
  },
  {
    "path": "feature/favourite/build.gradle.kts",
    "chars": 136,
    "preview": "plugins {\n    id(\"com.igorwojda.showcase.convention.feature\")\n}\n\nandroid {\n    namespace = \"com.igorwojda.showcase.featu"
  },
  {
    "path": "feature/favourite/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "feature/favourite/src/main/AndroidManifest.xml",
    "chars": 52,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "feature/favourite/src/main/kotlin/com/igorwojda/showcase/feature/favourite/FavouriteKoinModule.kt",
    "chars": 381,
    "preview": "package com.igorwojda.showcase.feature.favourite\n\nimport com.igorwojda.showcase.feature.favourite.data.dataModule\nimport"
  },
  {
    "path": "feature/favourite/src/main/kotlin/com/igorwojda/showcase/feature/favourite/data/DataModule.kt",
    "chars": 120,
    "preview": "package com.igorwojda.showcase.feature.favourite.data\n\nimport org.koin.dsl.module\n\ninternal val dataModule = module { }\n"
  },
  {
    "path": "feature/favourite/src/main/kotlin/com/igorwojda/showcase/feature/favourite/domain/DomainModule.kt",
    "chars": 124,
    "preview": "package com.igorwojda.showcase.feature.favourite.domain\n\nimport org.koin.dsl.module\n\ninternal val domainModule = module "
  },
  {
    "path": "feature/favourite/src/main/kotlin/com/igorwojda/showcase/feature/favourite/presentation/PresentationModule.kt",
    "chars": 136,
    "preview": "package com.igorwojda.showcase.feature.favourite.presentation\n\nimport org.koin.dsl.module\n\ninternal val presentationModu"
  },
  {
    "path": "feature/favourite/src/main/kotlin/com/igorwojda/showcase/feature/favourite/presentation/screen/favourite/FavouriteScreen.kt",
    "chars": 740,
    "preview": "package com.igorwojda.showcase.feature.favourite.presentation.screen.favourite\n\nimport androidx.compose.foundation.layou"
  },
  {
    "path": "feature/settings/build.gradle.kts",
    "chars": 201,
    "preview": "plugins {\n    id(\"com.igorwojda.showcase.convention.feature\")\n}\n\nandroid {\n    namespace = \"com.igorwojda.showcase.featu"
  },
  {
    "path": "feature/settings/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "feature/settings/src/main/AndroidManifest.xml",
    "chars": 52,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/SettingsKoinModule.kt",
    "chars": 376,
    "preview": "package com.igorwojda.showcase.feature.settings\n\nimport com.igorwojda.showcase.feature.settings.data.dataModule\nimport c"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/data/DataModule.kt",
    "chars": 119,
    "preview": "package com.igorwojda.showcase.feature.settings.data\n\nimport org.koin.dsl.module\n\ninternal val dataModule = module { }\n"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/domain/DomainModule.kt",
    "chars": 123,
    "preview": "package com.igorwojda.showcase.feature.settings.domain\n\nimport org.koin.dsl.module\n\ninternal val domainModule = module {"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/PresentationModule.kt",
    "chars": 475,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation\n\nimport com.igorwojda.showcase.feature.settings.presentatio"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/aboutlibraries/AboutLibrariesAction.kt",
    "chars": 241,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.aboutlibraries\n\nimport com.igorwojda.showcase.featur"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/aboutlibraries/AboutLibrariesScreen.kt",
    "chars": 2560,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.aboutlibraries\n\nimport androidx.compose.foundation.l"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/aboutlibraries/AboutLibrariesUiState.kt",
    "chars": 341,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.aboutlibraries\n\nimport androidx.compose.runtime.Immu"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/aboutlibraries/AboutLibrariesViewModel.kt",
    "chars": 296,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.aboutlibraries\n\nimport com.igorwojda.showcase.featur"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/settings/SettingsAction.kt",
    "chars": 223,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.settings\n\nimport com.igorwojda.showcase.feature.base"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/settings/SettingsScreen.kt",
    "chars": 4284,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.settings\n\nimport androidx.compose.foundation.clickab"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/settings/SettingsUiState.kt",
    "chars": 323,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.settings\n\nimport androidx.compose.runtime.Immutable\n"
  },
  {
    "path": "feature/settings/src/main/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/settings/SettingsViewModel.kt",
    "chars": 266,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.settings\n\nimport com.igorwojda.showcase.feature.base"
  },
  {
    "path": "feature/settings/src/main/res/values/strings.xml",
    "chars": 638,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"settings_screen_title\">Settings</string>\n    <strin"
  },
  {
    "path": "feature/settings/src/test/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/aboutlibraries/AboutLibrariesViewModelTest.kt",
    "chars": 864,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.aboutlibraries\n\nimport com.igorwojda.showcase.librar"
  },
  {
    "path": "feature/settings/src/test/kotlin/com/igorwojda/showcase/feature/settings/presentation/screen/settings/SettingsViewModelTest.kt",
    "chars": 840,
    "preview": "package com.igorwojda.showcase.feature.settings.presentation.screen.settings\n\nimport com.igorwojda.showcase.library.test"
  },
  {
    "path": "gradle/libs.versions.toml",
    "chars": 8296,
    "preview": "[versions]\nkotlin = \"2.3.20\"\n\n# KSP depends on specific Kotlin version, so it must be upgraded together with Kotlin (dis"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 252,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 1661,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 8739,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2966,
    "preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": "konsist-test/build.gradle.kts",
    "chars": 370,
    "preview": "plugins {\n    id(\"com.igorwojda.showcase.convention.test.library\")\n}\n\nandroid {\n    namespace = \"com.igorwojda.showcase."
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/AndroidKonsistTest.kt",
    "chars": 611,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport androidx.lifecycle.ViewModel\nimport com.lemonappdev.konsist.api.Konsi"
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/CleanArchitectureKonsistTest.kt",
    "chars": 1025,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport com.lemonappdev.konsist.api.Konsist\nimport com.lemonappdev.konsist.ap"
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/GeneralKonsistTest.kt",
    "chars": 1267,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport com.lemonappdev.konsist.api.Konsist\nimport com.lemonappdev.konsist.ap"
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/ModuleKonsistTest.kt",
    "chars": 794,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport com.lemonappdev.konsist.api.Konsist\nimport com.lemonappdev.konsist.ap"
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/TestKonsistTest.kt",
    "chars": 565,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport com.lemonappdev.konsist.api.Konsist\nimport com.lemonappdev.konsist.ap"
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/UseCaseKonsistTest.kt",
    "chars": 2424,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport com.lemonappdev.konsist.api.Konsist\nimport com.lemonappdev.konsist.ap"
  },
  {
    "path": "konsist-test/src/test/kotlin/com/igorwojda/showcase/konsisttest/ViewModelKonsistTest.kt",
    "chars": 1556,
    "preview": "package com.igorwojda.showcase.konsisttest\n\nimport com.igorwojda.showcase.feature.base.presentation.viewmodel.BaseViewMo"
  },
  {
    "path": "library/test-utils/build.gradle.kts",
    "chars": 552,
    "preview": "plugins {\n    id(\"com.igorwojda.showcase.convention.library\")\n}\n\nandroid {\n    namespace = \"com.igorwojda.showcase.libra"
  },
  {
    "path": "library/test-utils/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  }
]

// ... and 6 more files (download for full content)

About this extraction

This page contains the full source code of the igorwojda/android-showcase GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 206 files (1.5 MB), approximately 232.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!