From b42a627c822ae42f56c98aa99c31b6617f79be19 Mon Sep 17 00:00:00 2001 From: Valentin Bischof Date: Sun, 31 Aug 2025 15:21:31 +0200 Subject: [PATCH] add b/w filter to random widget (android) --- .../app/alextran/immich/widget/BitmapUtils.kt | 25 +++++++++++++++++++ .../immich/widget/ImageDownloadWorker.kt | 9 ++++++- .../widget/configure/RandomConfigure.kt | 23 +++++++++++++++++ .../app/alextran/immich/widget/model/Model.kt | 15 +++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt index 9188df1700..14c48c8881 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/BitmapUtils.kt @@ -2,6 +2,10 @@ package app.alextran.immich.widget import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint import java.io.File fun loadScaledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap? { @@ -31,3 +35,24 @@ fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeig return inSampleSize } + +fun applyBlackWhiteFilter(src: Bitmap): Bitmap { + + val config = src.config ?: Bitmap.Config.ARGB_8888 + val outputBitmap = Bitmap.createBitmap(src.width, src.height, config) + outputBitmap.setHasAlpha(src.hasAlpha()) + + val canvas = Canvas(outputBitmap) + + val paint = Paint().apply { + isFilterBitmap = true + val colorMatrix = ColorMatrix().apply { + setSaturation(0f) + } + colorFilter = ColorMatrixColorFilter(colorMatrix) + } + + canvas.drawBitmap(src, 0f, 0f, paint) + + return outputBitmap +} \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt index 25a7ed99f1..2327f02cf5 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImageDownloadWorker.kt @@ -122,6 +122,13 @@ class ImageDownloadWorker( WidgetType.MEMORIES -> fetchMemory(serverConfig) } + // if needed apply our filter + val filter = WidgetImageFilter.fromString(widgetConfig[kImageFilter]) + val filteredImage = when (filter) { + WidgetImageFilter.BLACK_WHITE -> applyBlackWhiteFilter(entry.image) + WidgetImageFilter.NONE -> entry.image + } + // clear current image if it exists if (!currentImgUUID.isNullOrEmpty()) { deleteImage(currentImgUUID) @@ -129,7 +136,7 @@ class ImageDownloadWorker( // save a new image val imgUUID = UUID.randomUUID().toString() - saveImage(entry.image, imgUUID) + saveImage(filteredImage, imgUUID) // trigger the update routine with new image uuid updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt index 83e404a8f1..12fd5b6ed3 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/configure/RandomConfigure.kt @@ -64,6 +64,7 @@ fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, var selectedAlbum by remember { mutableStateOf(null) } var showAlbumName by remember { mutableStateOf(false) } + var selectedFilter by remember { mutableStateOf(null) } var availableAlbums by remember { mutableStateOf>(listOf()) } var state by remember { mutableStateOf(WidgetConfigState.LOADING) } @@ -104,6 +105,16 @@ fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId } selectedAlbum = albumEntity ?: availableAlbums.first() + // load filter configuration + val availableFilters = listOf( + DropdownItem("None", WidgetImageFilter.NONE.name), + DropdownItem("Black & White", WidgetImageFilter.BLACK_WHITE.name) + ) + + val savedFilterId = currentState[kImageFilter] ?: WidgetImageFilter.NONE.name + val selectedFilterItem = availableFilters.firstOrNull { it.id == savedFilterId } + selectedFilter = selectedFilterItem ?: availableFilters.first() + // load showAlbumName showAlbumName = currentState[kShowAlbumName] == true } @@ -113,6 +124,7 @@ fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, prefs[kSelectedAlbum] = selectedAlbum?.id ?: "" prefs[kSelectedAlbumName] = selectedAlbum?.label ?: "" prefs[kShowAlbumName] = showAlbumName + prefs[kImageFilter] = selectedFilter?.id ?: WidgetImageFilter.NONE.name } ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM) @@ -187,6 +199,17 @@ fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, enabled = (state != WidgetConfigState.NO_CONNECTION) ) + Text("Filter") + Dropdown( + items = listOf( + DropdownItem("None", WidgetImageFilter.NONE.name), + DropdownItem("Black & White", WidgetImageFilter.BLACK_WHITE.name) + ), + selectedItem = selectedFilter, + onItemSelected = { selectedFilter = it }, + enabled = (state != WidgetConfigState.NO_CONNECTION) + ) + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt index 545a1edc59..6d9be76a97 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt @@ -49,6 +49,20 @@ enum class WidgetConfigState { LOADING, SUCCESS, LOG_IN, NO_CONNECTION } +enum class WidgetImageFilter { + NONE, BLACK_WHITE; + + companion object { + fun fromString(value: String?): WidgetImageFilter { + return try { + valueOf(value ?: NONE.name) + } catch (e: IllegalArgumentException) { + NONE + } + } + } +} + data class WidgetEntry ( val image: Bitmap, val subtitle: String?, @@ -70,6 +84,7 @@ val kSelectedAlbum = stringPreferencesKey("albumID") val kSelectedAlbumName = stringPreferencesKey("albumName") val kShowAlbumName = booleanPreferencesKey("showAlbumName") val kDeeplinkURL = stringPreferencesKey("deeplink") +val kImageFilter = stringPreferencesKey("imageFiler") const val kWorkerWidgetType = "widgetType" const val kWorkerWidgetID = "widgetId"