diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 3fcc44ad3f..0d25c658e0 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -155,16 +155,16 @@ android:resource="@xml/random_widget" /> - - - - - - - - - - + + + + + + fetchRandom(serverConfig, currentState) + val (newBitmap, subtitle) = when (widgetType) { + WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig) + WidgetType.MEMORIES -> fetchMemory(serverConfig) } // clear current image if it exists @@ -110,10 +112,11 @@ class ImageDownloadWorker( } // save a new image - val imgUUID = saveImage(newBitmap) + val imgUUID = UUID.randomUUID().toString() + saveImage(newBitmap, imgUUID) // trigger the update routine with new image uuid - updateWidget(widgetType, glanceId, imgUUID) + updateWidget(glanceId, imgUUID, subtitle) Result.success() } catch (e: Exception) { @@ -126,32 +129,65 @@ class ImageDownloadWorker( } } - private suspend fun updateWidget(type: WidgetType, glanceId: GlanceId, imageUUID: String, widgetState: WidgetState = WidgetState.SUCCESS) { + private suspend fun updateWidget( + glanceId: GlanceId, + imageUUID: String, + subtitle: String?, + widgetState: WidgetState = WidgetState.SUCCESS + ) { updateAppWidgetState(context, glanceId) { prefs -> prefs[kNow] = System.currentTimeMillis() prefs[kImageUUID] = imageUUID prefs[kWidgetState] = widgetState.toString() + prefs[kSubtitleText] = subtitle ?: "" } - when (type) { - WidgetType.RANDOM -> RandomWidget().update(context,glanceId) - } + PhotoWidget().update(context,glanceId) } - private suspend fun fetchRandom(serverConfig: ServerConfig, widgetData: Preferences): Bitmap { + private suspend fun fetchRandom( + serverConfig: ServerConfig, + widgetConfig: Preferences + ): Pair { val api = ImmichAPI(serverConfig) val filters = SearchFilters(AssetType.IMAGE, size=1) - val albumId = widgetData[kSelectedAlbum] + val albumId = widgetConfig[kSelectedAlbum] + val albumName = widgetConfig[kSelectedAlbumName] if (albumId != null) { filters.albumIds = listOf(albumId) } - val random = api.fetchSearchResults(filters) - val image = api.fetchImage(random[0]) + val random = api.fetchSearchResults(filters).first() + val image = api.fetchImage(random) - return image + return Pair(image, albumName) + } + + private suspend fun fetchMemory( + serverConfig: ServerConfig + ): Pair { + val api = ImmichAPI(serverConfig) + + val today = LocalDate.now() + val memories = api.fetchMemory(today) + val asset: SearchResult + var subtitle: String? = null + + if (memories.isNotEmpty()) { + // pick a random asset from a random memory + val memory = memories.random() + asset = memory.assets.random() + + subtitle = "${today.year-memory.data.year} years ago" + } else { + val filters = SearchFilters(AssetType.IMAGE, size=1) + asset = api.fetchSearchResults(filters).first() + } + + val image = api.fetchImage(asset) + return Pair(image, subtitle) } private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) { @@ -159,13 +195,10 @@ class ImageDownloadWorker( file.delete() } - private suspend fun saveImage(bitmap: Bitmap): String = withContext(Dispatchers.IO) { - val uuid = UUID.randomUUID().toString() + private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) { val file = File(context.cacheDir, imageFilename(uuid)) FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) } - - uuid } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt index 4ae35421ef..68f09ba846 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt @@ -14,6 +14,8 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder import java.text.SimpleDateFormat +import java.time.LocalDate +import java.time.format.DateTimeFormatter import java.util.* @@ -71,8 +73,8 @@ class ImmichAPI(cfg: ServerConfig) { gson.fromJson(response, type) } - suspend fun fetchMemory(date: Date): List = withContext(Dispatchers.IO) { - val iso8601 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(date) + suspend fun fetchMemory(date: LocalDate): List = withContext(Dispatchers.IO) { + val iso8601 = date.format(DateTimeFormatter.ISO_LOCAL_DATE) val url = buildRequestURL("/memories", listOf("for" to iso8601)) val connection = (url.openConnection() as HttpURLConnection).apply { requestMethod = "GET" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt index b92bfbcf82..46a8972258 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/MemoryReceiver.kt @@ -1,8 +1,22 @@ package app.alextran.immich.widget import HomeWidgetGlanceWidgetReceiver +import android.appwidget.AppWidgetManager +import android.content.Context -class MemoryReceiver : HomeWidgetGlanceWidgetReceiver() { - override val glanceAppWidget = RandomWidget() +class MemoryReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = PhotoWidget() + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + + appWidgetIds.forEach { widgetID -> + ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.MEMORIES) + } + } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt index 22ff710d55..7f78883e49 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/Model.kt @@ -40,12 +40,7 @@ data class Album( // MARK: Widget Specific enum class WidgetType { - RANDOM; - - val widgetClass: Class - get() = when (this) { - RANDOM -> RandomWidget::class.java - } + RANDOM, MEMORIES; } enum class WidgetState { @@ -60,6 +55,7 @@ val kSubtitleText = stringPreferencesKey("subtitle") val kNow = longPreferencesKey("now") val kWidgetState = stringPreferencesKey("state") val kSelectedAlbum = stringPreferencesKey("albumID") +val kSelectedAlbumName = stringPreferencesKey("albumName") val kShowAlbumName = booleanPreferencesKey("showAlbumName") const val kWorkerWidgetType = "widgetType" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoView.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoView.kt deleted file mode 100644 index 9501091df4..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoView.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.alextran.immich.widget - -import android.graphics.Bitmap -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.glance.GlanceModifier -import androidx.glance.Image -import androidx.glance.ImageProvider -import androidx.glance.background -import androidx.glance.layout.Box -import androidx.glance.layout.ContentScale -import androidx.glance.layout.fillMaxSize -import androidx.glance.text.Text -import app.alextran.immich.R - -@Composable -fun PhotoView(image: Bitmap?, subtitle: String?, loggedIn: Boolean) { - - Box( - modifier = GlanceModifier - .fillMaxSize() - .background(Color.White) // your color here - ) { - if (image != null) { - Image( - provider = ImageProvider(image), - contentDescription = "Widget Image", - contentScale = ContentScale.Crop, - modifier = GlanceModifier.fillMaxSize() - ) - if (subtitle != null) - Text(subtitle) - } else { - Image( - provider = ImageProvider(R.drawable.splash), - contentDescription = null, - ) - - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt similarity index 52% rename from mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt index c6fbf8be99..1120950c7c 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomWidget.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/PhotoWidget.kt @@ -2,43 +2,64 @@ package app.alextran.immich.widget import android.content.Context import android.graphics.Bitmap +import androidx.compose.ui.graphics.Color import androidx.datastore.preferences.core.MutablePreferences -import androidx.datastore.preferences.core.stringPreferencesKey import androidx.glance.appwidget.* import androidx.glance.* +import androidx.glance.layout.Box +import androidx.glance.layout.ContentScale +import androidx.glance.layout.fillMaxSize import androidx.glance.state.GlanceStateDefinition import androidx.glance.state.PreferencesGlanceStateDefinition +import androidx.glance.text.Text +import app.alextran.immich.R import java.io.File -class RandomWidget : GlanceAppWidget() { +class PhotoWidget : GlanceAppWidget() { override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition override suspend fun provideGlance(context: Context, id: GlanceId) { - provideContent { val prefs = currentState() - val imageUUID = prefs[kImageUUID] + val imageUUID = prefs[kImageUUID] val subtitle: String? = prefs[kSubtitleText] var bitmap: Bitmap? = null - var loggedIn = true if (imageUUID != null) { // fetch a random photo from server - val file = File(context.cacheDir, imageFilename(id)) + val file = File(context.cacheDir, imageFilename(imageUUID)) if (file.exists()) { bitmap = loadScaledBitmap(file, 500, 500) } - } else if (ImmichAPI.getServerConfig(context) == null) { - loggedIn = false } - PhotoView(image = bitmap, subtitle = subtitle, loggedIn = loggedIn) + // WIDGET CONTENT + Box( + modifier = GlanceModifier + .fillMaxSize() + .background(Color.White) + ) { + if (bitmap != null) { + Image( + provider = ImageProvider(bitmap), + contentDescription = "Widget Image", + contentScale = ContentScale.Crop, + modifier = GlanceModifier.fillMaxSize() + ) + if (subtitle != null) + Text(subtitle) + } else { + Image( + provider = ImageProvider(R.drawable.splash), + contentDescription = null, + ) + } + } } } - override suspend fun onDelete(context: Context, glanceId: GlanceId) { super.onDelete(context, glanceId) ImageDownloadWorker.cancel(context, glanceId) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt index e7b2291a12..687cb87b36 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/RandomReceiver.kt @@ -5,8 +5,8 @@ import android.appwidget.AppWidgetManager import android.content.Context import android.util.Log -class RandomReceiver : HomeWidgetGlanceWidgetReceiver() { - override val glanceAppWidget = RandomWidget() +class RandomReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = PhotoWidget() override fun onUpdate( context: Context, diff --git a/mobile/android/app/src/main/res/xml/memory_widget.xml b/mobile/android/app/src/main/res/xml/memory_widget.xml new file mode 100644 index 0000000000..84847f6bd6 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/memory_widget.xml @@ -0,0 +1,7 @@ +