diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 049e9f8f..e585882f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -54,7 +54,6 @@ dependencies {
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.8.0")
implementation("androidx.paging:paging-common-ktx:3.3.2")
implementation("androidx.paging:paging-runtime-ktx:3.3.2")
implementation("androidx.preference:preference-ktx:1.2.1")
@@ -78,9 +77,9 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.48.1")
ksp("com.google.dagger:hilt-android-compiler:2.48.1")
- ksp("com.github.bumptech.glide:ksp:4.14.2")
- implementation("com.github.bumptech.glide:glide:4.16.0")
- implementation("jp.wasabeef:glide-transformations:4.3.0")
+ implementation("io.coil-kt.coil3:coil:3.0.0-rc01")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01")
implementation("com.github.madrapps:pikolo:2.0.2")
implementation("com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 43854537..175cebd5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -79,7 +79,7 @@
-
+
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt b/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
index 335407e7..4e772a76 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
@@ -6,6 +6,14 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.SingletonImageLoader
+import coil3.disk.DiskCache
+import coil3.disk.directory
+import coil3.memory.MemoryCache
+import coil3.request.allowHardware
+import coil3.request.crossfade
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import com.google.android.material.color.ThemeUtils
@@ -25,7 +33,7 @@ import kotlinx.coroutines.plus
import javax.inject.Inject
@HiltAndroidApp
-class EchoApplication : Application() {
+class EchoApplication : Application(), SingletonImageLoader.Factory {
@Inject
lateinit var settings: SharedPreferences
@@ -100,4 +108,22 @@ class EchoApplication : Application() {
.getPackageInfo(packageName, 0)
.versionName!!
}
+
+ override fun newImageLoader(context: PlatformContext): ImageLoader {
+ return ImageLoader.Builder(context)
+ .memoryCache {
+ MemoryCache.Builder()
+ .maxSizePercent(context, 0.25)
+ .build()
+ }
+ .diskCache {
+ DiskCache.Builder()
+ .directory(cacheDir.resolve("image_cache"))
+ .maxSizeBytes(1024 * 1024 * 100) // 100MB
+ .build()
+ }
+ .allowHardware(false)
+ .crossfade(true)
+ .build()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
index 709090e4..7422eac2 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
@@ -129,7 +129,8 @@ class MainActivity : AppCompatActivity() {
controllerFuture = playerFuture
- intent?.onIntent()
+ addOnNewIntentListener { onIntent(it) }
+ onIntent(intent)
}
override fun onDestroy() {
@@ -137,23 +138,17 @@ class MainActivity : AppCompatActivity() {
controllerFuture?.let { MediaBrowser.releaseFuture(it) }
}
- private fun Intent.onIntent() {
- val fromNotif = hasExtra("fromNotification")
- if (fromNotif) {
- uiViewModel.fromNotification.value = true
- return
- }
- val uri = data
- println("URI: $uri")
- when (uri?.scheme) {
- "echo" -> openItemFragmentFromUri(uri)
- "file" -> openExtensionInstaller(uri)
+ private fun onIntent(intent: Intent?) {
+ this.intent = null
+ intent ?: return
+ val fromNotif = intent.hasExtra("fromNotification")
+ if (fromNotif) uiViewModel.fromNotification.value = true
+ else {
+ val uri = intent.data
+ when (uri?.scheme) {
+ "echo" -> openItemFragmentFromUri(uri)
+ "file" -> openExtensionInstaller(uri)
+ }
}
}
-
- override fun onNewIntent(intent: Intent?) {
- intent?.onIntent()
- super.onNewIntent(intent)
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt
index 6695acc0..84560969 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt
@@ -36,6 +36,7 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId
import dev.brahmkshatriya.echo.playback.MediaItemUtils.track
import dev.brahmkshatriya.echo.playback.listeners.Radio
import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.toExceptionDetails
+import dev.brahmkshatriya.echo.utils.future
import dev.brahmkshatriya.echo.utils.getSerialized
import dev.brahmkshatriya.echo.utils.putSerialized
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel.Companion.noClient
@@ -48,7 +49,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.guava.future
import kotlinx.coroutines.withContext
@UnstableApi
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt
index 34382867..89b74e5e 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt
@@ -7,13 +7,11 @@ import android.net.Uri
import androidx.media3.common.util.BitmapLoader
import androidx.media3.common.util.UnstableApi
import com.google.common.util.concurrent.ListenableFuture
-import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.models.ImageHolder
+import dev.brahmkshatriya.echo.utils.future
import dev.brahmkshatriya.echo.utils.loadBitmap
import dev.brahmkshatriya.echo.utils.toData
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.guava.future
@UnstableApi
class PlayerBitmapLoader(
@@ -23,15 +21,12 @@ class PlayerBitmapLoader(
override fun supportsMimeType(mimeType: String) = true
- override fun decodeBitmap(data: ByteArray) = scope.future(Dispatchers.IO) {
+ override fun decodeBitmap(data: ByteArray) = scope.future {
BitmapFactory.decodeByteArray(data, 0, data.size) ?: error("Failed to decode bitmap")
}
- private val emptyBitmap
- get() = context.loadBitmap(R.drawable.art_music) ?: error("Empty bitmap")
-
override fun loadBitmap(uri: Uri): ListenableFuture = scope.future {
- val cover = runCatching { uri.toString().toData() }.getOrNull()
- cover?.loadBitmap(context) ?: emptyBitmap
+ val cover = uri.toString().toData()
+ cover.loadBitmap(context) ?: error("Failed to load bitmap of $cover")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt
index dbde3bae..efdf58a2 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt
@@ -187,7 +187,7 @@ sealed class MediaItemViewHolder(
fun ItemListsCoverBinding.bind(item: EchoMediaItem.Lists): (Boolean) -> Unit {
playlist.isVisible = item is EchoMediaItem.Lists.PlaylistItem
val cover = item.cover
- cover.loadWith(listImageView) {
+ cover.loadWith(listImageView, null, item.placeHolder()) {
cover.loadInto(listImageView1)
cover.loadInto(listImageView2)
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt
index 6b26eb3f..1f1fd106 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt
@@ -12,7 +12,7 @@ import dev.brahmkshatriya.echo.extensions.isClient
import dev.brahmkshatriya.echo.ui.extension.ExtensionsListBottomSheet
import dev.brahmkshatriya.echo.ui.login.LoginUserBottomSheet
import dev.brahmkshatriya.echo.ui.settings.SettingsFragment
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
import dev.brahmkshatriya.echo.viewmodels.LoginUserViewModel
@@ -26,7 +26,7 @@ fun MaterialToolbar.configureMainMenu(fragment: MainFragment) {
extensions.transitionName = "extensions"
fragment.observe(extensionViewModel.extensionFlow) { client ->
- client?.metadata?.iconUrl?.toImageHolder().loadWith(extensions, R.drawable.ic_extension) {
+ client?.metadata?.iconUrl?.toImageHolder().loadAsCircle(extensions, R.drawable.ic_extension) {
menu.findItem(R.id.menu_extensions).icon = it
}
}
@@ -49,7 +49,7 @@ fun MaterialToolbar.configureMainMenu(fragment: MainFragment) {
val user = u?.toUser()
val isLoginClient = extension?.isClient() ?: false
if (isLoginClient) {
- user?.cover.loadWith(settings, R.drawable.ic_account_circle_48dp) {
+ user?.cover.loadAsCircle(settings, R.drawable.ic_account_circle_48dp) {
menu.findItem(R.id.menu_settings).icon = it
}
} else menu.findItem(R.id.menu_settings).setIcon(R.drawable.ic_settings_outline)
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt
index 6f75fa88..18604e75 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt
@@ -17,7 +17,7 @@ import dev.brahmkshatriya.echo.databinding.ItemDownloadGroupBinding
import dev.brahmkshatriya.echo.ui.adapter.ShelfEmptyAdapter
import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.placeHolder
import dev.brahmkshatriya.echo.utils.loadInto
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
class DownloadingAdapter(
val listener: Listener
@@ -56,7 +56,7 @@ class DownloadingAdapter(
binding.downloadTitle.text = download.item.title
download.item.cover?.loadInto(binding.itemImageView, download.item.placeHolder())
binding.itemExtension.apply {
- download.clientIcon?.toImageHolder().loadWith(this, R.drawable.ic_extension) {
+ download.clientIcon?.toImageHolder().loadAsCircle(this, R.drawable.ic_extension) {
setImageDrawable(it)
}
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt
index 497ad9a2..adc57673 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt
@@ -15,7 +15,7 @@ import dev.brahmkshatriya.echo.common.Extension
import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder
import dev.brahmkshatriya.echo.databinding.ItemExtensionBinding
import dev.brahmkshatriya.echo.ui.adapter.ShelfEmptyAdapter
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
class
ExtensionAdapter(
@@ -52,7 +52,7 @@ ExtensionAdapter(
}
binding.extensionVersion.text = "${metadata.version} • ${metadata.importType.name}"
binding.itemExtension.apply {
- metadata.iconUrl?.toImageHolder().loadWith(this, R.drawable.ic_extension) {
+ metadata.iconUrl?.toImageHolder().loadAsCircle(this, R.drawable.ic_extension) {
setImageDrawable(it)
}
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt
index 3512027c..431b8d01 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt
@@ -23,7 +23,7 @@ import dev.brahmkshatriya.echo.ui.common.openFragment
import dev.brahmkshatriya.echo.ui.login.LoginUserBottomSheet.Companion.bind
import dev.brahmkshatriya.echo.ui.settings.ExtensionFragment
import dev.brahmkshatriya.echo.utils.autoCleared
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.utils.onAppBarChangeListener
import dev.brahmkshatriya.echo.utils.setupTransition
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
@@ -116,7 +116,7 @@ class ExtensionInfoFragment : Fragment() {
}
}
- metadata.iconUrl?.toImageHolder().loadWith(binding.extensionIcon, R.drawable.ic_extension) {
+ metadata.iconUrl?.toImageHolder().loadAsCircle(binding.extensionIcon, R.drawable.ic_extension) {
binding.extensionIcon.setImageDrawable(it)
}
binding.extensionDetails.text =
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt
index b87a81aa..b077b1e3 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt
@@ -25,7 +25,7 @@ import dev.brahmkshatriya.echo.extensions.plugger.ApkPluginSource
import dev.brahmkshatriya.echo.extensions.plugger.AppInfo
import dev.brahmkshatriya.echo.utils.ApkLinkParser
import dev.brahmkshatriya.echo.utils.autoCleared
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
import kotlinx.coroutines.launch
@@ -84,7 +84,7 @@ class ExtensionInstallerBottomSheet : BottomSheetDialogFragment() {
val (extensionType, metadata) = value
binding.extensionTitle.text = metadata.name
- metadata.iconUrl?.toImageHolder().loadWith(binding.extensionIcon, R.drawable.ic_extension) {
+ metadata.iconUrl?.toImageHolder().loadAsCircle(binding.extensionIcon, R.drawable.ic_extension) {
binding.extensionIcon.setImageDrawable(it)
}
binding.extensionDetails.text = metadata.version
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt
index 1d8f9e38..bdd885fc 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt
@@ -19,7 +19,7 @@ import dev.brahmkshatriya.echo.ui.common.openFragment
import dev.brahmkshatriya.echo.ui.player.lyrics.LyricsViewModel
import dev.brahmkshatriya.echo.utils.autoCleared
import dev.brahmkshatriya.echo.utils.collect
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
class ExtensionsListBottomSheet : BottomSheetDialogFragment() {
@@ -85,7 +85,7 @@ class ExtensionsListBottomSheet : BottomSheetDialogFragment() {
button.text = metadata.name
binding.buttonToggleGroup.addView(button)
button.isChecked = metadata.id == viewModel.currentFlow.value
- metadata.iconUrl?.toImageHolder().loadWith(button, R.drawable.ic_extension) {
+ metadata.iconUrl?.toImageHolder().loadAsCircle(button, R.drawable.ic_extension) {
button.icon = it
}
button.id = index
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt
index f231aebe..0f51a7b7 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt
@@ -37,7 +37,7 @@ import dev.brahmkshatriya.echo.ui.editplaylist.AddToPlaylistBottomSheet
import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.copyToClipboard
import dev.brahmkshatriya.echo.utils.autoCleared
import dev.brahmkshatriya.echo.utils.getSerialized
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.utils.putSerialized
import dev.brahmkshatriya.echo.viewmodels.DownloadViewModel
@@ -345,7 +345,7 @@ class ItemBottomSheet : BottomSheetDialogFragment() {
is ItemAction.Custom -> {
binding.textView.text = action.title
- action.image.loadWith(binding.root) {
+ action.image.loadAsCircle(binding.root) {
if (it == null) {
binding.imageView.imageTintList = colorState
binding.imageView.setImageResource(action.placeholder)
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt
index 82a5f055..00cfc50b 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt
@@ -47,7 +47,7 @@ import dev.brahmkshatriya.echo.utils.dpToPx
import dev.brahmkshatriya.echo.utils.getSerialized
import dev.brahmkshatriya.echo.utils.load
import dev.brahmkshatriya.echo.utils.loadInto
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadWithThumb
import dev.brahmkshatriya.echo.utils.onAppBarChangeListener
import dev.brahmkshatriya.echo.utils.putSerialized
import dev.brahmkshatriya.echo.utils.setupTransition
@@ -216,7 +216,7 @@ class ItemFragment : Fragment() {
binding.toolBar.title = it.title.trim()
}
- it.cover.loadWith(binding.cover, item.cover, it.placeHolder())
+ it.cover.loadWithThumb(binding.cover, item.cover, it.placeHolder())
with(viewModel) {
when (it) {
is AlbumItem -> {
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt
index 15d0ae36..277c310b 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt
@@ -32,7 +32,7 @@ import dev.brahmkshatriya.echo.extensions.isClient
import dev.brahmkshatriya.echo.ui.exception.AppException
import dev.brahmkshatriya.echo.utils.autoCleared
import dev.brahmkshatriya.echo.utils.collect
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.utils.onAppBarChangeListener
import dev.brahmkshatriya.echo.utils.setupTransition
@@ -128,7 +128,7 @@ class LoginFragment : Fragment() {
return
}
- metadata.iconUrl?.toImageHolder().loadWith(binding.extensionIcon, R.drawable.ic_extension) {
+ metadata.iconUrl?.toImageHolder().loadAsCircle(binding.extensionIcon, R.drawable.ic_extension) {
binding.extensionIcon.setImageDrawable(it)
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt
index 382f4dbd..85678e5e 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt
@@ -15,7 +15,7 @@ import dev.brahmkshatriya.echo.databinding.DialogLoginUserListBinding
import dev.brahmkshatriya.echo.db.models.UserEntity.Companion.toEntity
import dev.brahmkshatriya.echo.ui.common.openFragment
import dev.brahmkshatriya.echo.utils.autoCleared
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.viewmodels.LoginUserViewModel
@@ -67,7 +67,7 @@ class LoginUserListBottomSheet : BottomSheetDialogFragment() {
).root
button.text = user.name
binding.accountListToggleGroup.addView(button)
- user.cover.loadWith(button, R.drawable.ic_account_circle) { button.icon = it }
+ user.cover.loadAsCircle(button, R.drawable.ic_account_circle) { button.icon = it }
button.id = index
}
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt
index 5e2230ee..aec6db7c 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt
@@ -28,8 +28,6 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM
import androidx.recyclerview.widget.DiffUtil
-import com.bumptech.glide.Glide
-import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
@@ -62,14 +60,14 @@ import dev.brahmkshatriya.echo.utils.dpToPx
import dev.brahmkshatriya.echo.utils.emit
import dev.brahmkshatriya.echo.utils.load
import dev.brahmkshatriya.echo.utils.loadBitmap
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadBlurred
+import dev.brahmkshatriya.echo.utils.loadWithThumb
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.utils.toTimeString
import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel
import dev.brahmkshatriya.echo.viewmodels.UiViewModel
import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.applyInsets
import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.isLandscape
-import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlin.math.max
@@ -134,11 +132,7 @@ class PlayerTrackAdapter(
binding.bgInfoTitle.setTextColor(colors.text)
binding.bgInfoArtist.setTextColor(colors.text)
- runCatching {
- Glide.with(binding.bgImage).load(bitmap)
- .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 4)))
- .into(binding.bgImage)
- }
+ binding.bgImage.loadBlurred(bitmap, 12f)
}
binding.collapsedContainer.root.setOnClickListener {
@@ -418,7 +412,7 @@ class PlayerTrackAdapter(
item: MediaItem,
) {
val track = item.track
- track.cover.loadWith(expandedTrackCover) {
+ track.cover.loadWithThumb(expandedTrackCover) {
collapsedContainer.collapsedTrackCover.load(it)
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt
index df1565e1..759edc0a 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt
@@ -28,7 +28,7 @@ import dev.brahmkshatriya.echo.databinding.ItemLyricsItemBinding
import dev.brahmkshatriya.echo.extensions.isClient
import dev.brahmkshatriya.echo.ui.extension.ExtensionsListBottomSheet
import dev.brahmkshatriya.echo.utils.autoCleared
-import dev.brahmkshatriya.echo.utils.loadWith
+import dev.brahmkshatriya.echo.utils.loadAsCircle
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel
import dev.brahmkshatriya.echo.viewmodels.UiViewModel
@@ -66,7 +66,7 @@ class LyricsFragment : Fragment() {
observe(viewModel.currentExtension) { current ->
binding.searchBar.hint = current?.name
current?.metadata?.iconUrl?.toImageHolder()
- .loadWith(extension, R.drawable.ic_extension) {
+ .loadAsCircle(extension, R.drawable.ic_extension) {
menu.findItem(R.id.menu_lyrics).icon = it
}
val isSearchable = current?.isClient() ?: false
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt
new file mode 100644
index 00000000..ffe9f3f7
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt
@@ -0,0 +1,83 @@
+@file:Suppress("DEPRECATION")
+
+package dev.brahmkshatriya.echo.utils
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Paint
+import android.renderscript.Allocation
+import android.renderscript.Element
+import android.renderscript.RenderScript
+import android.renderscript.ScriptIntrinsicBlur
+import androidx.core.graphics.applyCanvas
+import androidx.core.graphics.createBitmap
+import coil3.size.Size
+import coil3.transform.Transformation
+
+/**
+ * A [Transformation] that applies a Gaussian blur to an image.
+ *
+ * @param context The [Context] used to create a [RenderScript] instance.
+ * @param radius The radius of the blur.
+ * @param sampling The sampling multiplier used to scale the image. Values > 1
+ * will downscale the image. Values between 0 and 1 will upscale the image.
+ */
+class BlurTransformation @JvmOverloads constructor(
+ private val context: Context,
+ private val radius: Float = DEFAULT_RADIUS,
+ private val sampling: Float = DEFAULT_SAMPLING
+) : Transformation() {
+
+ init {
+ require(radius in 0.0..25.0) { "radius must be in [0, 25]." }
+ require(sampling > 0) { "sampling must be > 0." }
+ }
+
+ @Suppress("NullableToStringCall")
+ override val cacheKey = "${BlurTransformation::class.java.name}-$radius-$sampling"
+
+ override suspend fun transform(input: Bitmap, size: Size): Bitmap {
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
+
+ val scaledWidth = (input.width / sampling).toInt()
+ val scaledHeight = (input.height / sampling).toInt()
+ val output =
+ createBitmap(scaledWidth, scaledHeight, input.config ?: Bitmap.Config.ARGB_8888)
+ output.applyCanvas {
+ scale(1 / sampling, 1 / sampling)
+ drawBitmap(input, 0f, 0f, paint)
+ }
+
+ var script: RenderScript? = null
+ var tmpInt: Allocation? = null
+ var tmpOut: Allocation? = null
+ var blur: ScriptIntrinsicBlur? = null
+ try {
+ script = RenderScript.create(context)
+ tmpInt = Allocation.createFromBitmap(
+ script,
+ output,
+ Allocation.MipmapControl.MIPMAP_NONE,
+ Allocation.USAGE_SCRIPT
+ )
+ tmpOut = Allocation.createTyped(script, tmpInt.type)
+ blur = ScriptIntrinsicBlur.create(script, Element.U8_4(script))
+ blur.setRadius(radius)
+ blur.setInput(tmpInt)
+ blur.forEach(tmpOut)
+ tmpOut.copyTo(output)
+ } finally {
+ script?.destroy()
+ tmpInt?.destroy()
+ tmpOut?.destroy()
+ blur?.destroy()
+ }
+
+ return output
+ }
+
+ private companion object {
+ private const val DEFAULT_RADIUS = 10f
+ private const val DEFAULT_SAMPLING = 1f
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt
deleted file mode 100644
index 23598026..00000000
--- a/app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.brahmkshatriya.echo.utils
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.util.Log
-import com.bumptech.glide.GlideBuilder
-import com.bumptech.glide.annotation.GlideModule
-import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
-import com.bumptech.glide.module.AppGlideModule
-
-@GlideModule
-class EchoGlideApp : AppGlideModule() {
- override fun applyOptions(context: Context, builder: GlideBuilder) {
- builder.setLogLevel(Log.ERROR)
- val diskCacheSizeBytes = 1024 * 1024 * 100L // 100 MB
- builder.setDiskCache(
- InternalCacheDiskCacheFactory(
- context,
- "imageCache",
- diskCacheSizeBytes
- )
- )
- builder.setDefaultTransitionOptions(
- Drawable::class.java,
- withCrossFade()
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt
index e72906f3..ac360d37 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt
@@ -3,10 +3,25 @@ package dev.brahmkshatriya.echo.utils
import android.content.Context
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.SettableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
fun Context.listenFuture(future: ListenableFuture, block: (Result) -> Unit) {
future.addListener({
val result = runCatching { future.get() }
block(result)
}, ContextCompat.getMainExecutor(this))
+}
+
+fun CoroutineScope.future(block: suspend () -> T): ListenableFuture {
+ val future = SettableFuture.create()
+ launch {
+ runCatching {
+ future.set(block())
+ }.onFailure {
+ future.setException(it)
+ }
+ }
+ return future
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt
index 3dd9b40a..9103285b 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt
@@ -4,14 +4,24 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
-import com.bumptech.glide.Glide
-import com.bumptech.glide.RequestBuilder
-import com.bumptech.glide.load.model.GlideUrl
-import com.bumptech.glide.request.target.CustomViewTarget
-import com.bumptech.glide.request.transition.Transition
+import coil3.Bitmap
+import coil3.Image
+import coil3.asDrawable
+import coil3.imageLoader
+import coil3.load
+import coil3.network.NetworkHeaders
+import coil3.network.httpHeaders
+import coil3.request.ImageRequest
+import coil3.request.crossfade
+import coil3.request.error
+import coil3.request.placeholder
+import coil3.request.target
+import coil3.request.transformations
+import coil3.target.GenericViewTarget
+import coil3.toBitmap
+import coil3.transform.CircleCropTransformation
+import coil3.transform.Transformation
import dev.brahmkshatriya.echo.common.models.ImageHolder
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
private fun tryWith(print: Boolean = false, block: () -> T): T? {
return try {
@@ -31,102 +41,131 @@ private suspend fun tryWithSuspend(print: Boolean = true, block: suspend ()
}
}
+fun View.enqueue(builder: ImageRequest.Builder) = context.imageLoader.enqueue(builder.build())
+
fun ImageHolder?.loadInto(
imageView: ImageView, placeholder: Int? = null, errorDrawable: Int? = null
) = tryWith {
- val builder = Glide.with(imageView).asDrawable()
- val request = createRequest(builder, placeholder, errorDrawable)
- request.into(imageView)
+ val request = createRequest(imageView.context, placeholder, errorDrawable)
+ request.target(imageView)
+ imageView.enqueue(request)
}
-fun ImageHolder?.loadWith(
+fun ImageHolder?.loadWithThumb(
imageView: ImageView, thumbnail: ImageHolder? = null,
error: Int? = null, onDrawable: (Drawable?) -> Unit = {}
) = tryWith {
- val builder = Glide.with(imageView).asDrawable()
if (this == null) {
- thumbnail.loadInto(imageView, error)
+ thumbnail.loadWith(imageView, null, error, onDrawable)
return@tryWith
}
- val request = createRequest(builder, error)
- request.into(ViewTarget(imageView) {
- imageView.setImageDrawable(it)
+ val request = createRequest(imageView.context, null, error)
+ request.target(ViewTarget(imageView) {
+ imageView.load(it)
tryWith(false) { onDrawable(it) }
})
+ imageView.enqueue(request)
}
-fun ImageHolder?.loadWith(
- view: T, placeholder: Int? = null, errorDrawable: Int? = null, onDrawable: (Drawable?) -> Unit
+fun ImageHolder?.loadWith(
+ imageView: ImageView, placeholder: Int? = null,
+ error: Int? = null, onDrawable: (Drawable?) -> Unit = {}
) = tryWith {
- val builder = Glide.with(view).asDrawable()
- val request = createRequest(builder, placeholder, errorDrawable)
- request.circleCrop().into(ViewTarget(view) {
+ val request = createRequest(imageView.context, placeholder, error)
+ request.target(ViewTarget(imageView) {
+ imageView.load(it)
tryWith(false) { onDrawable(it) }
})
+ imageView.enqueue(request)
+}
+
+class ViewTarget(
+ override val view: View,
+ val onDrawable: (Drawable?) -> Unit,
+) : GenericViewTarget() {
+ private var mDrawable: Drawable? = null
+ override var drawable: Drawable?
+ get() = mDrawable
+ set(value) {
+ mDrawable = value
+ onDrawable(value)
+ }
+}
+
+val circleCrop = CircleCropTransformation()
+val squareCrop = SquareCropTransformation()
+fun ImageHolder?.loadAsCircle(
+ view: T, placeholder: Int? = null, errorDrawable: Int? = null, onDrawable: (Drawable?) -> Unit
+) = tryWith {
+ val request = createRequest(view.context, placeholder, errorDrawable, circleCrop)
+ fun setDrawable(image: Image?) {
+ val drawable = image?.asDrawable(view.resources)
+ tryWith(false) { onDrawable(drawable) }
+ }
+ request.target(::setDrawable, ::setDrawable, ::setDrawable)
+ view.enqueue(request)
}
suspend fun ImageHolder?.loadBitmap(
context: Context, placeholder: Int? = null
) = tryWithSuspend {
- val builder = Glide.with(context).asBitmap()
- val request = createRequest(builder, placeholder)
- withContext(Dispatchers.IO) {
- tryWithSuspend(false) { request.submit().get() }
- }
+ val request = createRequest(context, null, placeholder)
+ context.imageLoader.execute(request.build()).image?.toBitmap()
}
fun ImageView.load(placeHolder: Int) = tryWith {
- Glide.with(this).load(placeHolder).into(this)
+ load(placeHolder)
}
fun ImageView.load(drawable: Drawable?) = tryWith {
- Glide.with(this).load(drawable).into(this)
+ load(drawable)
}
fun ImageView.load(drawable: Drawable?, size: Int) = tryWith {
- Glide.with(this).load(drawable).override(size).into(this)
+ load(drawable) { size(size) }
}
-fun Context.loadBitmap(placeHolder: Int) = tryWith {
- Glide.with(this).asBitmap().load(placeHolder).submit().get()
+fun ImageView.loadBlurred(bitmap: Bitmap?, radius: Float) = tryWith {
+ load(bitmap) {
+ transformations(BlurTransformation(context, radius))
+ crossfade(false)
+ }
}
-private fun createRequest(
+private fun createRequest(
imageHolder: ImageHolder,
- requestBuilder: RequestBuilder,
+ builder: ImageRequest.Builder,
) = imageHolder.run {
when (this) {
- is ImageHolder.UriImageHolder -> requestBuilder.load(uri)
- is ImageHolder.UrlRequestImageHolder ->
- requestBuilder.load(GlideUrl(request.url) { request.headers })
+ is ImageHolder.UriImageHolder -> builder.data(uri)
+ is ImageHolder.UrlRequestImageHolder -> {
+ if (request.headers.isNotEmpty())
+ builder.httpHeaders(NetworkHeaders.Builder().apply {
+ request.headers.forEach { (t, u) -> add(t, u) }
+ }.build())
+ builder.data(request.url)
+ }
}
}
-fun ImageHolder?.createRequest(
- requestBuilder: RequestBuilder, placeholder: Int? = null, errorDrawable: Int? = null
-): RequestBuilder {
+private fun ImageHolder?.createRequest(
+ context: Context,
+ placeholder: Int?,
+ errorDrawable: Int?,
+ vararg transformations: Transformation
+): ImageRequest.Builder {
+ val builder = ImageRequest.Builder(context)
var error = errorDrawable
if (error == null) error = placeholder
- if (this == null) return requestBuilder.load(error)
-
- var request = createRequest(this, requestBuilder)
- request = placeholder?.let { request.placeholder(it) } ?: request
- request = error?.let { request.error(it) } ?: request
- return if (crop) request.transform(SquareBitmapTransformation()) else request
-}
-
-class ViewTarget(val target: T, private val onDrawable: (Drawable?) -> Unit) :
- CustomViewTarget(target) {
- override fun onLoadFailed(errorDrawable: Drawable?) {
- onDrawable(errorDrawable)
- }
-
- override fun onResourceCleared(placeholder: Drawable?) {
- onDrawable(placeholder)
- }
-
- override fun onResourceReady(resource: Drawable, transition: Transition?) {
- onDrawable(resource)
+ if (this == null) {
+ if (error != null) builder.data(error)
+ return builder
}
+ createRequest(this, builder)
+ placeholder?.let { builder.placeholder(it) }
+ error?.let { builder.error(it) }
+ val list = if (crop) listOf(squareCrop, *transformations) else transformations.toList()
+ if (list.isNotEmpty()) builder.transformations(list)
+ return builder
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt
deleted file mode 100644
index 6bbcb09f..00000000
--- a/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package dev.brahmkshatriya.echo.utils
-
-import android.graphics.Bitmap
-import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
-import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
-import java.security.MessageDigest
-
-class SquareBitmapTransformation : BitmapTransformation() {
-
- val version = 1
- val id = "${javaClass.simpleName}.$version".toByteArray()
- override fun updateDiskCacheKey(messageDigest: MessageDigest) = messageDigest.update(id)
-
- override fun transform(
- pool: BitmapPool,
- toTransform: Bitmap,
- outWidth: Int,
- outHeight: Int
- ): Bitmap {
- val size = toTransform.width.coerceAtMost(toTransform.height)
- val x = (toTransform.width - size) / 2
- val y = (toTransform.height - size) / 2
- return Bitmap.createBitmap(toTransform, x, y, size, size)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt
new file mode 100644
index 00000000..5d2dce97
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt
@@ -0,0 +1,16 @@
+package dev.brahmkshatriya.echo.utils
+
+import android.graphics.Bitmap
+import coil3.size.Size
+import coil3.transform.Transformation
+
+class SquareCropTransformation : Transformation() {
+ val version = 1
+ override val cacheKey = "${javaClass.simpleName}.$version"
+ override suspend fun transform(input: Bitmap, size: Size): Bitmap {
+ val max = input.width.coerceAtMost(input.height)
+ val x = (input.width - max) / 2
+ val y = (input.height - max) / 2
+ return Bitmap.createBitmap(input, x, y, max, max)
+ }
+}
\ No newline at end of file