From 5b3421161267726645c10a9eb56d032518a00bab Mon Sep 17 00:00:00 2001
From: Max Grakov <grakovne@gmail.com>
Date: Thu, 7 Nov 2024 00:43:23 +0100
Subject: [PATCH] Feature/no issue clean up (#38)

* Clean up ABS responses
* Book title could be nullable (!)
* Remove Lucid Library
* Remove room annotation processor
* Change user Agent
* Stable keys for Library Screen
* Fix time extension locale
* remove broken in-memory cache
* update foreground
---
 app/build.gradle.kts                          |  7 +-
 .../audiobookshelf/AudiobookshelfChannel.kt   |  9 ++-
 .../LibraryItemIdResponseConverter.kt         | 29 ++++---
 .../converter/LibraryItemResponseConverter.kt |  6 +-
 .../converter/LibrarySearchItemsConverter.kt  |  6 +-
 .../model/LibraryItemsResponse.kt             | 45 +----------
 .../audiobookshelf/model/LibraryResponse.kt   | 14 +---
 .../lissen/channel/common/UserAgent.kt        |  3 +-
 .../lissen/content/cache/dao/CachedBookDao.kt |  2 +
 .../lissen/playback/MediaRepository.kt        |  3 +-
 .../ui/components/AsyncShimmeringImage.kt     |  1 -
 .../lissen/ui/components/BookCoverFetcher.kt  | 14 +---
 .../lissen/ui/extensions/TimeExtensions.kt    | 17 ++---
 .../org/grakovne/lissen/ui/icons/Search.kt    | 76 +++++++++++++++++++
 .../ui/screens/library/LibraryScreen.kt       |  4 +-
 .../composables/DefaultActionComposable.kt    |  5 +-
 .../GeneralSettingsItemComposable.kt          |  4 +-
 gradle.properties                             | 19 -----
 gradle/libs.versions.toml                     |  2 -
 19 files changed, 133 insertions(+), 133 deletions(-)
 create mode 100644 app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 713b4d20..49052dfe 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -25,8 +25,8 @@ android {
         applicationId = "org.grakovne.lissen"
         minSdk = 28
         targetSdk = 35
-        versionCode = 23
-        versionName = "1.0.22"
+        versionCode = 24
+        versionName = "1.0.23"
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         vectorDrawables {
@@ -98,8 +98,6 @@ dependencies {
     implementation(libs.androidx.media3.session)
     kapt(libs.hilt.android.compiler)
 
-    implementation(libs.icons.lucide)
-
     implementation(libs.androidx.core.ktx)
     implementation(libs.androidx.lifecycle.runtime.ktx)
     implementation(libs.androidx.activity.compose)
@@ -121,7 +119,6 @@ dependencies {
 
     implementation(libs.androidx.room.runtime)
     implementation(libs.androidx.room.ktx)
-    annotationProcessor(libs.androidx.room.compiler)
     ksp(libs.androidx.room.compiler)
 
     testImplementation(libs.junit)
diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt
index cf37f534..215b5884 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/AudiobookshelfChannel.kt
@@ -4,6 +4,7 @@ import android.net.Uri
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
+import org.grakovne.lissen.BuildConfig
 import org.grakovne.lissen.channel.audiobookshelf.api.AudioBookshelfDataRepository
 import org.grakovne.lissen.channel.audiobookshelf.api.AudioBookshelfMediaRepository
 import org.grakovne.lissen.channel.audiobookshelf.api.AudioBookshelfSyncService
@@ -13,7 +14,9 @@ import org.grakovne.lissen.channel.audiobookshelf.converter.LibraryResponseConve
 import org.grakovne.lissen.channel.audiobookshelf.converter.LibrarySearchItemsConverter
 import org.grakovne.lissen.channel.audiobookshelf.converter.PlaybackSessionResponseConverter
 import org.grakovne.lissen.channel.audiobookshelf.converter.RecentBookResponseConverter
+import org.grakovne.lissen.channel.audiobookshelf.model.DeviceInfo
 import org.grakovne.lissen.channel.audiobookshelf.model.LibraryResponse
+import org.grakovne.lissen.channel.audiobookshelf.model.StartPlaybackRequest
 import org.grakovne.lissen.channel.common.ApiResult
 import org.grakovne.lissen.channel.common.ApiResult.Success
 import org.grakovne.lissen.channel.common.ChannelCode
@@ -147,9 +150,9 @@ class AudiobookshelfChannel @Inject constructor(
         supportedMimeTypes: List<String>,
         deviceId: String
     ): ApiResult<PlaybackSession> {
-        val request = org.grakovne.lissen.channel.audiobookshelf.model.StartPlaybackRequest(
+        val request = StartPlaybackRequest(
             supportedMimeTypes = supportedMimeTypes,
-            deviceInfo = org.grakovne.lissen.channel.audiobookshelf.model.DeviceInfo(
+            deviceInfo = DeviceInfo(
                 clientName = getClientName(),
                 deviceId = deviceId,
                 deviceName = getClientName()
@@ -195,7 +198,7 @@ class AudiobookshelfChannel @Inject constructor(
         password: String
     ): ApiResult<UserAccount> = dataRepository.authorize(host, username, password)
 
-    private fun getClientName() = "Lissen App Android"
+    private fun getClientName() = "Lissen App ${BuildConfig.VERSION_NAME}"
 
     private val supportedLibraryTypes = listOf("book")
 }
diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt
index 598133dc..8c7f49d2 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemIdResponseConverter.kt
@@ -32,19 +32,23 @@ class LibraryItemIdResponseConverter @Inject constructor() {
             }
 
         val filesAsChapters: () -> List<BookChapter> = {
-            item.media.audioFiles.fold(0.0 to mutableListOf<BookChapter>()) { (accDuration, chapters), file ->
-                chapters.add(
-                    BookChapter(
-                        start = accDuration,
-                        end = accDuration + file.duration,
-                        title = file.metaTags?.tagTitle
-                            ?: file.metadata.filename.removeSuffix(file.metadata.ext),
-                        duration = file.duration,
-                        id = file.ino
+            item
+                .media
+                .audioFiles
+                .sortedBy { it.index }
+                .fold(0.0 to mutableListOf<BookChapter>()) { (accDuration, chapters), file ->
+                    chapters.add(
+                        BookChapter(
+                            start = accDuration,
+                            end = accDuration + file.duration,
+                            title = file.metaTags?.tagTitle
+                                ?: file.metadata.filename.removeSuffix(file.metadata.ext),
+                            duration = file.duration,
+                            id = file.ino
+                        )
                     )
-                )
-                accDuration + file.duration to chapters
-            }.second
+                    accDuration + file.duration to chapters
+                }.second
         }
 
         return DetailedBook(
@@ -54,6 +58,7 @@ class LibraryItemIdResponseConverter @Inject constructor() {
             files = item
                 .media
                 .audioFiles
+                .sortedBy { it.index }
                 .map {
                     BookFile(
                         id = it.ino,
diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt
index b2a33ca3..119dea9b 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibraryItemResponseConverter.kt
@@ -12,10 +12,12 @@ class LibraryItemResponseConverter @Inject constructor() {
 
     fun apply(response: LibraryItemsResponse): PagedItems<Book> = response
         .results
-        .map {
+        .mapNotNull {
+            val title = it.media.metadata.title ?: return@mapNotNull null
+
             Book(
                 id = it.id,
-                title = it.media.metadata.title,
+                title = title,
                 author = it.media.metadata.authorName,
                 cachedState = BookCachedState.ABLE_TO_CACHE,
                 duration = it.media.duration.toInt()
diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt
index da6341c5..ac7946c3 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/converter/LibrarySearchItemsConverter.kt
@@ -10,10 +10,12 @@ import javax.inject.Singleton
 class LibrarySearchItemsConverter @Inject constructor() {
     fun apply(response: List<LibraryItem>): List<Book> {
         return response
-            .map {
+            .mapNotNull {
+                val title = it.media.metadata.title ?: return@mapNotNull null
+
                 Book(
                     id = it.id,
-                    title = it.media.metadata.title,
+                    title = title,
                     author = it.media.metadata.authorName,
                     cachedState = BookCachedState.ABLE_TO_CACHE,
                     duration = it.media.duration.toInt()
diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt
index cbcbd8e3..a4657230 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryItemsResponse.kt
@@ -2,57 +2,20 @@ package org.grakovne.lissen.channel.audiobookshelf.model
 
 data class LibraryItemsResponse(
     val results: List<LibraryItem>,
-    val total: Int,
-    val limit: Int,
-    val page: Int,
-    val sortBy: String,
-    val sortDesc: Boolean,
-    val filterBy: String,
-    val mediaType: String,
-    val minified: Boolean,
-    val collapseseries: Boolean,
-    val include: String
+    val page: Int
 )
 
 data class LibraryItem(
     val id: String,
-    val libraryId: String,
-    val folderId: String,
-    val path: String,
-    val relPath: String,
-    val isFile: Boolean,
-    val mtimeMs: Long,
-    val ctimeMs: Long,
-    val birthtimeMs: Long,
-    val addedAt: Long,
-    val updatedAt: Long,
-    val isMissing: Boolean,
-    val isInvalid: Boolean,
-    val mediaType: String,
     val media: Media
 )
 
 data class Media(
-    val numTracks: Int,
-    val numAudioFiles: Int,
-    val numChapters: Int,
     val duration: Double,
-    val metadata: Metadata,
-    val size: Long
+    val metadata: Metadata
 )
 
 data class Metadata(
-    val title: String,
-    val titleIgnorePrefix: String,
-    val subtitle: String?,
-    val authorName: String?,
-    val genres: List<String>,
-    val publishedYear: String?,
-    val publishedDate: String?,
-    val publisher: String?,
-    val description: String?,
-    val isbn: String?,
-    val asin: String?,
-    val language: String?,
-    val explicit: Boolean
+    val title: String?,
+    val authorName: String?
 )
diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryResponse.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryResponse.kt
index e43dba76..51546d2d 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryResponse.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/model/LibraryResponse.kt
@@ -7,17 +7,5 @@ data class LibraryResponse(
 data class Library(
     val id: String,
     val name: String,
-    val folders: List<Folder>,
-    val displayOrder: Int,
-    val icon: String,
-    val mediaType: String,
-    val provider: String,
-    val createdAt: Long,
-    val lastUpdate: Long
-)
-
-data class Folder(
-    val id: String,
-    val fullPath: String,
-    val libraryId: String
+    val mediaType: String
 )
diff --git a/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt b/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt
index b4628328..798a01a6 100644
--- a/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt
+++ b/app/src/main/java/org/grakovne/lissen/channel/common/UserAgent.kt
@@ -1,5 +1,6 @@
 package org.grakovne.lissen.channel.common
 
 import android.os.Build
+import org.grakovne.lissen.BuildConfig
 
-val USER_AGENT = "Lissen (Android ${Build.VERSION.RELEASE}; ${Build.MODEL}"
+val USER_AGENT = "Lissen/${BuildConfig.VERSION_NAME} (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL}) ExoPlayer/1.4.1"
diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt b/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt
index 51e06df5..8da90f76 100644
--- a/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt
+++ b/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt
@@ -5,6 +5,7 @@ import androidx.room.Delete
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
+import androidx.room.RewriteQueriesToDropUnusedColumns
 import androidx.room.Transaction
 import androidx.room.Update
 import org.grakovne.lissen.content.cache.entity.BookChapterEntity
@@ -82,6 +83,7 @@ interface CachedBookDao {
     ): List<BookEntity>
 
     @Transaction
+    @RewriteQueriesToDropUnusedColumns
     @Query(
         """
         SELECT * FROM detailed_books
diff --git a/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt b/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt
index 8c5ff7c6..32aa8b1c 100644
--- a/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt
+++ b/app/src/main/java/org/grakovne/lissen/playback/MediaRepository.kt
@@ -7,7 +7,6 @@ import android.content.Intent
 import android.content.IntentFilter
 import android.os.Handler
 import android.os.Looper
-import androidx.core.content.ContextCompat
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
@@ -130,7 +129,7 @@ class MediaRepository @Inject constructor(
         val intent = Intent(context, PlaybackService::class.java).apply {
             action = PlaybackService.ACTION_PLAY
         }
-        ContextCompat.startForegroundService(context, intent)
+        context.startForegroundService(intent)
     }
 
     fun pauseAudio() {
diff --git a/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt b/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt
index 2cec48ba..a7f976e3 100644
--- a/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt
+++ b/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt
@@ -16,7 +16,6 @@ import androidx.compose.ui.layout.ContentScale
 import coil.ImageLoader
 import coil.compose.AsyncImage
 import coil.request.ImageRequest
-import com.valentinilk.shimmer.shimmer
 
 @Composable
 fun AsyncShimmeringImage(
diff --git a/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt b/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt
index f5ef9f1a..ecba3bc1 100644
--- a/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt
+++ b/app/src/main/java/org/grakovne/lissen/ui/components/BookCoverFetcher.kt
@@ -4,11 +4,9 @@ import android.content.Context
 import android.net.Uri
 import coil.ImageLoader
 import coil.decode.ImageSource
-import coil.disk.DiskCache
 import coil.fetch.FetchResult
 import coil.fetch.Fetcher
 import coil.fetch.SourceResult
-import coil.memory.MemoryCache
 import coil.request.Options
 import dagger.Module
 import dagger.Provides
@@ -72,17 +70,7 @@ object ImageLoaderModule {
     ): ImageLoader {
         return ImageLoader
             .Builder(context)
-            .components {
-                add(bookCoverFetcherFactory)
-            }
-            .memoryCache {
-                MemoryCache.Builder(context).build()
-            }
-            .diskCache {
-                DiskCache.Builder()
-                    .directory(context.cacheDir.resolve("сover_cache"))
-                    .build()
-            }
+            .components { add(bookCoverFetcherFactory) }
             .build()
     }
 }
diff --git a/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt b/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt
index 4abcaa59..4714ac35 100644
--- a/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt
+++ b/app/src/main/java/org/grakovne/lissen/ui/extensions/TimeExtensions.kt
@@ -1,31 +1,28 @@
 package org.grakovne.lissen.ui.extensions
 
-import android.annotation.SuppressLint
+import java.util.Locale
 
-@SuppressLint("DefaultLocale")
 fun Int.formatFully(): String {
     val hours = this / 3600
     val minutes = (this % 3600) / 60
-    val remainingSeconds = this % 60
+    val seconds = this % 60
     return if (hours > 0) {
-        String.format("%02d:%02d:%02d", hours, minutes, remainingSeconds)
+        String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds)
     } else {
-        String.format("%02d:%02d", minutes, remainingSeconds)
+        String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
     }
 }
 
-@SuppressLint("DefaultLocale")
 fun Int.formatShortly(): String {
     val hours = this / 3600
     val minutes = (this % 3600) / 60
 
-    return String.format("%02dh %02dm", hours, minutes)
+    return String.format(Locale.getDefault(), "%02dh %02dm", hours, minutes)
 }
 
-@SuppressLint("DefaultLocale")
 fun Int.formatLeadingMinutes(): String {
     val minutes = this / 60
-    val remainingSeconds = this % 60
+    val seconds = this % 60
 
-    return String.format("%02d:%02d", minutes, remainingSeconds)
+    return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
 }
diff --git a/app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt b/app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt
new file mode 100644
index 00000000..79c9b06a
--- /dev/null
+++ b/app/src/main/java/org/grakovne/lissen/ui/icons/Search.kt
@@ -0,0 +1,76 @@
+/**
+ * Copyright @alexstyl
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * File has been copied from https://composeicons.com/icons/lucide/search-check under ISC Licence
+ */
+package org.grakovne.lissen.ui.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Search: ImageVector
+    get() {
+        if (_Search != null) {
+            return _Search!!
+        }
+        _Search = ImageVector.Builder(
+            name = "Search",
+            defaultWidth = 24.dp,
+            defaultHeight = 24.dp,
+            viewportWidth = 24f,
+            viewportHeight = 24f
+        ).apply {
+            path(
+                fill = null,
+                fillAlpha = 1.0f,
+                stroke = SolidColor(Color(0xFF000000)),
+                strokeAlpha = 1.0f,
+                strokeLineWidth = 2f,
+                strokeLineCap = StrokeCap.Round,
+                strokeLineJoin = StrokeJoin.Round,
+                strokeLineMiter = 1.0f,
+                pathFillType = PathFillType.NonZero
+            ) {
+                moveTo(19f, 11f)
+                arcTo(8f, 8f, 0f, isMoreThanHalf = false, isPositiveArc = true, 11f, 19f)
+                arcTo(8f, 8f, 0f, isMoreThanHalf = false, isPositiveArc = true, 3f, 11f)
+                arcTo(8f, 8f, 0f, isMoreThanHalf = false, isPositiveArc = true, 19f, 11f)
+                close()
+            }
+            path(
+                fill = null,
+                fillAlpha = 1.0f,
+                stroke = SolidColor(Color(0xFF000000)),
+                strokeAlpha = 1.0f,
+                strokeLineWidth = 2f,
+                strokeLineCap = StrokeCap.Round,
+                strokeLineJoin = StrokeJoin.Round,
+                strokeLineMiter = 1.0f,
+                pathFillType = PathFillType.NonZero
+            ) {
+                moveTo(21f, 21f)
+                lineToRelative(-4.3f, -4.3f)
+            }
+        }.build()
+        return _Search!!
+    }
+
+private var _Search: ImageVector? = null
diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt
index 131ed7c2..9f778872 100644
--- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt
+++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt
@@ -312,7 +312,7 @@ fun LibraryScreen(
                         }
                     }
 
-                    item { Spacer(modifier = Modifier.height(8.dp)) }
+                    item(key = "library_spacer") { Spacer(modifier = Modifier.height(8.dp)) }
 
                     when {
                         isPlaceholderRequired -> item { LibraryPlaceholderComposable() }
@@ -326,7 +326,7 @@ fun LibraryScreen(
                             }
                         }
 
-                        else -> items(count = library.itemCount) {
+                        else -> items(count = library.itemCount, key = { "library_item_$it" }) {
                             val book = library[it] ?: return@items
                             val isVisible = remember(hiddenBooks, book.id) {
                                 derivedStateOf { libraryViewModel.isVisible(book.id) }
diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt
index b6ff39ea..e1355da0 100644
--- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt
+++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/DefaultActionComposable.kt
@@ -27,12 +27,11 @@ import androidx.compose.runtime.withFrameNanos
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import com.composables.icons.lucide.Lucide
-import com.composables.icons.lucide.Search
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import org.grakovne.lissen.R
+import org.grakovne.lissen.ui.icons.Search
 import org.grakovne.lissen.ui.navigation.AppNavigationService
 import org.grakovne.lissen.viewmodel.CachingModelView
 import org.grakovne.lissen.viewmodel.LibraryViewModel
@@ -54,7 +53,7 @@ fun DefaultActionComposable(
             modifier = Modifier.offset(x = 4.dp)
         ) {
             Icon(
-                imageVector = Lucide.Search,
+                imageVector = Search,
                 contentDescription = null
             )
         }
diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt
index 90a2ce32..fd0499bc 100644
--- a/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt
+++ b/app/src/main/java/org/grakovne/lissen/ui/screens/settings/composable/GeneralSettingsItemComposable.kt
@@ -10,8 +10,8 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Check
-import androidx.compose.material3.Divider
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Icon
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.MaterialTheme
@@ -65,7 +65,7 @@ fun GeneralSettingsItemComposable(
                                     onDismissRequest()
                                 }
                         )
-                        Divider()
+                        HorizontalDivider()
                     }
                 }
             }
diff --git a/gradle.properties b/gradle.properties
index 132244e5..f0a2e55f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,23 +1,4 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
 org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. For more details, visit
-# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
-# org.gradle.parallel=true
-# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app's APK
-# https://developer.android.com/topic/libraries/support-library/androidx-rn
 android.useAndroidX=true
-# Kotlin code style for this project: "official" or "obsolete":
 kotlin.code.style=official
-# Enables namespacing of each library's R class so that its R class includes only the
-# resources declared in the library itself and none from the library's dependencies,
-# thereby reducing the size of the R class for that library
 android.nonTransitiveRClass=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ed8db1e2..0c3b3a5a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,7 +7,6 @@ composeShimmerAndroid = "1.3.1"
 converterGson = "2.9.0"
 hiltAndroid = "2.52"
 hiltNavigationCompose = "1.2.0"
-iconsLucide = "1.0.0"
 kotlin = "1.9.24"
 coreKtx = "1.15.0"
 junit = "4.13.2"
@@ -58,7 +57,6 @@ compose-shimmer-android = { module = "com.valentinilk.shimmer:compose-shimmer-an
 converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
 hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
 hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
-icons-lucide = { module = "com.composables:icons-lucide", version.ref = "iconsLucide" }
 junit = { group = "junit", name = "junit", version.ref = "junit" }
 androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
 androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }