This commit is contained in:
bwees 2025-07-01 16:13:51 -05:00
parent 7f7a3bc6ef
commit 0f06193664
No known key found for this signature in database
6 changed files with 78 additions and 80 deletions

View File

@ -1,31 +1,14 @@
package app.alextran.immich.widget package app.alextran.immich.widget
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.util.Log import android.util.Log
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.* import androidx.glance.*
import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.work.* import androidx.work.*
import com.google.gson.Gson
import es.antonborri.home_widget.HomeWidgetPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -45,7 +28,7 @@ class ImageDownloadWorker(
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
fun enqueue(context: Context, appWidgetId: Int, config: WidgetConfig) { fun enqueue(context: Context, appWidgetId: Int, widgetType: WidgetType) {
val manager = WorkManager.getInstance(context) val manager = WorkManager.getInstance(context)
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>( val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
@ -58,7 +41,7 @@ class ImageDownloadWorker(
) )
.setInputData( .setInputData(
Data.Builder() Data.Builder()
.putString("config", Gson().toJson(config)) .putString("widgetType", widgetType.toString())
.putInt("widgetId", appWidgetId) .putInt("widgetId", appWidgetId)
.build() .build()
) )
@ -78,45 +61,43 @@ class ImageDownloadWorker(
} }
} }
private fun getServerConfig(): ServerConfig? {
val prefs = HomeWidgetPlugin.getData(context)
val serverURL = prefs.getString("widget_server_url", "") ?: ""
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
if (serverURL == "" || sessionKey == "") {
return null
}
return ServerConfig(
serverURL,
sessionKey
)
}
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
return try { return try {
val configString = inputData.getString("config") val widgetType = WidgetType.valueOf(inputData.getString("config") ?: "")
val config = Gson().fromJson(configString, WidgetConfig::class.java)
val widgetId = inputData.getInt("widgetId", -1) val widgetId = inputData.getInt("widgetId", -1)
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId) val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
val serverConfig = getServerConfig() ?: return Result.success() val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
val currentImgUUID = currentState[stringPreferencesKey("uuid")]
// clear current image if it exists val serverConfig = ImmichAPI.getServerConfig(context)
val state = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
val currentImgUUID = state[stringPreferencesKey("uuid")] // clear any image caches and go to "login" state if no credentials
if (currentImgUUID != null) { if (serverConfig == null) {
deleteImage(currentImgUUID) if (!currentImgUUID.isNullOrEmpty()) {
deleteImage(currentImgUUID)
updateWidget(widgetType, glanceId, "", WidgetState.LOG_IN)
}
return Result.success()
} }
// fetch new image // fetch new image
val newBitmap = when (config.widgetType) { val newBitmap = when (widgetType) {
WidgetType.RANDOM -> fetchRandom(serverConfig) WidgetType.RANDOM -> fetchRandom(serverConfig, currentState)
} }
// clear current image if it exists
if (!currentImgUUID.isNullOrEmpty()) {
deleteImage(currentImgUUID)
}
// save a new image
val imgUUID = saveImage(newBitmap) val imgUUID = saveImage(newBitmap)
// trigger the update routine with new image uuid // trigger the update routine with new image uuid
updateWidget(config, glanceId, imgUUID) updateWidget(widgetType, glanceId, imgUUID)
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
@ -129,19 +110,29 @@ class ImageDownloadWorker(
} }
} }
private suspend fun updateWidget(config: WidgetConfig, glanceId: GlanceId, imageUUID: String) { private suspend fun updateWidget(type: WidgetType, glanceId: GlanceId, imageUUID: String, widgetState: WidgetState = WidgetState.SUCCESS) {
updateAppWidgetState(context, glanceId) { prefs -> updateAppWidgetState(context, glanceId) { prefs ->
prefs[longPreferencesKey("now")] = System.currentTimeMillis() prefs[longPreferencesKey("now")] = System.currentTimeMillis()
prefs[stringPreferencesKey("uuid")] = imageUUID prefs[stringPreferencesKey("uuid")] = imageUUID
prefs[stringPreferencesKey("state")] = widgetState.toString()
} }
RandomWidget().update(context,glanceId) when (type) {
WidgetType.RANDOM -> RandomWidget().update(context,glanceId)
}
} }
private suspend fun fetchRandom(serverConfig: ServerConfig): Bitmap { private suspend fun fetchRandom(serverConfig: ServerConfig, widgetData: Preferences): Bitmap {
val api = ImmichAPI(serverConfig) val api = ImmichAPI(serverConfig)
val random = api.fetchSearchResults(SearchFilters(AssetType.IMAGE, size=1)) val filters = SearchFilters(AssetType.IMAGE, size=1)
val albumId = widgetData[stringPreferencesKey("albumID")]
if (albumId != null) {
filters.albumIds = listOf(albumId)
}
val random = api.fetchSearchResults(filters)
val image = api.fetchImage(random[0]) val image = api.fetchImage(random[0])
return image return image

View File

@ -6,6 +6,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import es.antonborri.home_widget.HomeWidgetPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
@ -18,6 +19,25 @@ import java.util.*
class ImmichAPI(cfg: ServerConfig) { class ImmichAPI(cfg: ServerConfig) {
companion object {
fun getServerConfig(context: Context): ServerConfig? {
val prefs = HomeWidgetPlugin.getData(context)
val serverURL = prefs.getString("widget_server_url", "") ?: ""
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
if (serverURL.isBlank() || sessionKey.isBlank()) {
return null
}
return ServerConfig(
serverURL,
sessionKey
)
}
}
private val gson = Gson() private val gson = Gson()
private val serverConfig = cfg private val serverConfig = cfg

View File

@ -3,6 +3,8 @@ package app.alextran.immich.widget
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidget
// MARK: Immich Entities
enum class AssetType { enum class AssetType {
IMAGE, VIDEO, AUDIO, OTHER IMAGE, VIDEO, AUDIO, OTHER
} }
@ -32,6 +34,8 @@ data class Album(
val albumName: String val albumName: String
) )
// MARK: Widget Specific
enum class WidgetType { enum class WidgetType {
RANDOM; RANDOM;
@ -41,15 +45,9 @@ enum class WidgetType {
} }
} }
data class WidgetConfig( enum class WidgetState {
val widgetType: WidgetType, LOADING, SUCCESS, LOG_IN;
val params: Map<String, String>, }
)
data class ServerConfig(val serverEndpoint: String, val sessionKey: String) data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
// this value is in HomeWidgetPlugin.PREFERENCES but is marked internal
const val WIDGET_PREFERENCES = "HomeWidgetPreferences"
val loggedInKey = stringPreferencesKey("isLoggedIn")
val lastUpdatedKey = stringPreferencesKey("lastUpdated")

View File

@ -14,7 +14,7 @@ import androidx.glance.text.Text
import app.alextran.immich.R import app.alextran.immich.R
@Composable @Composable
fun PhotoWidget(image: Bitmap?, error: String?, subtitle: String?) { fun PhotoView(image: Bitmap?, subtitle: String?, loggedIn: Boolean) {
Box( Box(
modifier = GlanceModifier modifier = GlanceModifier
@ -28,12 +28,14 @@ fun PhotoWidget(image: Bitmap?, error: String?, subtitle: String?) {
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = GlanceModifier.fillMaxSize() modifier = GlanceModifier.fillMaxSize()
) )
if (subtitle != null)
Text(subtitle)
} else { } else {
Image( Image(
provider = ImageProvider(R.drawable.splash), provider = ImageProvider(R.drawable.splash),
contentDescription = null, contentDescription = null,
) )
Text(error ?: "NOPERS")
} }
} }
} }

View File

@ -3,17 +3,7 @@ package app.alextran.immich.widget
import HomeWidgetGlanceWidgetReceiver import HomeWidgetGlanceWidgetReceiver
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import androidx.glance.GlanceId
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() { class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
override val glanceAppWidget = RandomWidget() override val glanceAppWidget = RandomWidget()
@ -25,11 +15,8 @@ class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
) { ) {
super.onUpdate(context, appWidgetManager, appWidgetIds) super.onUpdate(context, appWidgetManager, appWidgetIds)
val cfg = WidgetConfig(WidgetType.RANDOM, HashMap())
appWidgetIds.forEach { widgetID -> appWidgetIds.forEach { widgetID ->
ImageDownloadWorker.enqueue(context, widgetID, cfg) ImageDownloadWorker.enqueue(context, widgetID, WidgetType.RANDOM)
Log.w("WIDGET_UPDATE", "WORKER ENQUEUE CALLED: $widgetID")
} }
} }
} }

View File

@ -1,19 +1,14 @@
package app.alextran.immich.widget package app.alextran.immich.widget
import HomeWidgetGlanceState
import HomeWidgetGlanceStateDefinition
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.util.Log
import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.appwidget.* import androidx.glance.appwidget.*
import androidx.glance.* import androidx.glance.*
import androidx.glance.state.GlanceStateDefinition import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition import androidx.glance.state.PreferencesGlanceStateDefinition
import java.io.File import java.io.File
import java.util.prefs.Preferences
class RandomWidget : GlanceAppWidget() { class RandomWidget : GlanceAppWidget() {
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
@ -23,7 +18,10 @@ class RandomWidget : GlanceAppWidget() {
provideContent { provideContent {
val prefs = currentState<MutablePreferences>() val prefs = currentState<MutablePreferences>()
val imageUUID = prefs[stringPreferencesKey("uuid")] val imageUUID = prefs[stringPreferencesKey("uuid")]
val subtitle: String? = prefs[stringPreferencesKey("subtitle")]
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
var loggedIn = true
if (imageUUID != null) { if (imageUUID != null) {
// fetch a random photo from server // fetch a random photo from server
@ -32,9 +30,11 @@ class RandomWidget : GlanceAppWidget() {
if (file.exists()) { if (file.exists()) {
bitmap = loadScaledBitmap(file, 500, 500) bitmap = loadScaledBitmap(file, 500, 500)
} }
} else if (ImmichAPI.getServerConfig(context) == null) {
loggedIn = false
} }
PhotoWidget(image = bitmap, error = "NOPE", subtitle = "hello") PhotoView(image = bitmap, subtitle = subtitle, loggedIn = loggedIn)
} }
} }