add deeplinks

This commit is contained in:
bwees 2025-07-03 11:12:42 -05:00
parent 42e53232c3
commit 403b5dd930
No known key found for this signature in database
4 changed files with 53 additions and 17 deletions

View File

@ -94,14 +94,20 @@ class ImageDownloadWorker(
if (serverConfig == null) { if (serverConfig == null) {
if (!currentImgUUID.isNullOrEmpty()) { if (!currentImgUUID.isNullOrEmpty()) {
deleteImage(currentImgUUID) deleteImage(currentImgUUID)
updateWidget(glanceId, "", "", WidgetState.LOG_IN) updateWidget(
glanceId,
"",
"",
"immich://",
WidgetState.LOG_IN
)
} }
return Result.success() return Result.success()
} }
// fetch new image // fetch new image
val (newBitmap, subtitle) = when (widgetType) { val entry = when (widgetType) {
WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig) WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
WidgetType.MEMORIES -> fetchMemory(serverConfig) WidgetType.MEMORIES -> fetchMemory(serverConfig)
} }
@ -113,10 +119,10 @@ class ImageDownloadWorker(
// save a new image // save a new image
val imgUUID = UUID.randomUUID().toString() val imgUUID = UUID.randomUUID().toString()
saveImage(newBitmap, imgUUID) saveImage(entry.image, imgUUID)
// trigger the update routine with new image uuid // trigger the update routine with new image uuid
updateWidget(glanceId, imgUUID, subtitle) updateWidget(glanceId, imgUUID, entry.subtitle, entry.deeplink)
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
@ -133,6 +139,7 @@ class ImageDownloadWorker(
glanceId: GlanceId, glanceId: GlanceId,
imageUUID: String, imageUUID: String,
subtitle: String?, subtitle: String?,
deeplink: String?,
widgetState: WidgetState = WidgetState.SUCCESS widgetState: WidgetState = WidgetState.SUCCESS
) { ) {
updateAppWidgetState(context, glanceId) { prefs -> updateAppWidgetState(context, glanceId) { prefs ->
@ -140,6 +147,7 @@ class ImageDownloadWorker(
prefs[kImageUUID] = imageUUID prefs[kImageUUID] = imageUUID
prefs[kWidgetState] = widgetState.toString() prefs[kWidgetState] = widgetState.toString()
prefs[kSubtitleText] = subtitle ?: "" prefs[kSubtitleText] = subtitle ?: ""
prefs[kDeeplinkURL] = deeplink ?: ""
} }
PhotoWidget().update(context,glanceId) PhotoWidget().update(context,glanceId)
@ -148,7 +156,7 @@ class ImageDownloadWorker(
private suspend fun fetchRandom( private suspend fun fetchRandom(
serverConfig: ServerConfig, serverConfig: ServerConfig,
widgetConfig: Preferences widgetConfig: Preferences
): Pair<Bitmap, String?> { ): WidgetEntry {
val api = ImmichAPI(serverConfig) val api = ImmichAPI(serverConfig)
val filters = SearchFilters(AssetType.IMAGE, size=1) val filters = SearchFilters(AssetType.IMAGE, size=1)
@ -164,17 +172,21 @@ class ImageDownloadWorker(
val random = api.fetchSearchResults(filters).first() val random = api.fetchSearchResults(filters).first()
val image = api.fetchImage(random) val image = api.fetchImage(random)
return Pair(image, subtitle) return WidgetEntry(
image,
subtitle,
assetDeeplink(random)
)
} }
private suspend fun fetchMemory( private suspend fun fetchMemory(
serverConfig: ServerConfig serverConfig: ServerConfig
): Pair<Bitmap, String?> { ): WidgetEntry {
val api = ImmichAPI(serverConfig) val api = ImmichAPI(serverConfig)
val today = LocalDate.now() val today = LocalDate.now()
val memories = api.fetchMemory(today) val memories = api.fetchMemory(today)
val asset: SearchResult val asset: Asset
var subtitle: String? = null var subtitle: String? = null
if (memories.isNotEmpty()) { if (memories.isNotEmpty()) {
@ -182,14 +194,18 @@ class ImageDownloadWorker(
val memory = memories.random() val memory = memories.random()
asset = memory.assets.random() asset = memory.assets.random()
subtitle = "${today.year-memory.data.year} years ago" subtitle = "${today.year - memory.data.year} years ago"
} else { } else {
val filters = SearchFilters(AssetType.IMAGE, size=1) val filters = SearchFilters(AssetType.IMAGE, size=1)
asset = api.fetchSearchResults(filters).first() asset = api.fetchSearchResults(filters).first()
} }
val image = api.fetchImage(asset) val image = api.fetchImage(asset)
return Pair(image, subtitle) return WidgetEntry(
image,
subtitle,
assetDeeplink(asset)
)
} }
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) { private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {

View File

@ -53,7 +53,7 @@ class ImmichAPI(cfg: ServerConfig) {
return URL(urlString.toString()) return URL(urlString.toString())
} }
suspend fun fetchSearchResults(filters: SearchFilters): List<SearchResult> = withContext(Dispatchers.IO) { suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
val url = buildRequestURL("/search/random") val url = buildRequestURL("/search/random")
val connection = (url.openConnection() as HttpURLConnection).apply { val connection = (url.openConnection() as HttpURLConnection).apply {
requestMethod = "POST" requestMethod = "POST"
@ -69,7 +69,7 @@ class ImmichAPI(cfg: ServerConfig) {
} }
val response = connection.inputStream.bufferedReader().readText() val response = connection.inputStream.bufferedReader().readText()
val type = object : TypeToken<List<SearchResult>>() {}.type val type = object : TypeToken<List<Asset>>() {}.type
gson.fromJson(response, type) gson.fromJson(response, type)
} }
@ -85,7 +85,7 @@ class ImmichAPI(cfg: ServerConfig) {
gson.fromJson(response, type) gson.fromJson(response, type)
} }
suspend fun fetchImage(asset: SearchResult): Bitmap = withContext(Dispatchers.IO) { suspend fun fetchImage(asset: Asset): Bitmap = withContext(Dispatchers.IO) {
val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview")) val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview"))
val connection = url.openConnection() val connection = url.openConnection()
val data = connection.getInputStream().readBytes() val data = connection.getInputStream().readBytes()

View File

@ -1,5 +1,6 @@
package app.alextran.immich.widget package app.alextran.immich.widget
import android.graphics.Bitmap
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
@ -12,9 +13,9 @@ enum class AssetType {
IMAGE, VIDEO, AUDIO, OTHER IMAGE, VIDEO, AUDIO, OTHER
} }
data class SearchResult( data class Asset(
val id: String, val id: String,
val type: AssetType val type: AssetType,
) )
data class SearchFilters( data class SearchFilters(
@ -25,7 +26,7 @@ data class SearchFilters(
data class MemoryResult( data class MemoryResult(
val id: String, val id: String,
var assets: List<SearchResult>, var assets: List<Asset>,
val type: String, val type: String,
val data: MemoryData val data: MemoryData
) { ) {
@ -47,6 +48,12 @@ enum class WidgetState {
LOADING, SUCCESS, LOG_IN; LOADING, SUCCESS, LOG_IN;
} }
data class WidgetEntry (
val image: Bitmap,
val subtitle: String?,
val deeplink: String?
)
data class ServerConfig(val serverEndpoint: String, val sessionKey: String) data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
// MARK: Widget State Keys // MARK: Widget State Keys
@ -57,6 +64,7 @@ val kWidgetState = stringPreferencesKey("state")
val kSelectedAlbum = stringPreferencesKey("albumID") val kSelectedAlbum = stringPreferencesKey("albumID")
val kSelectedAlbumName = stringPreferencesKey("albumName") val kSelectedAlbumName = stringPreferencesKey("albumName")
val kShowAlbumName = booleanPreferencesKey("showAlbumName") val kShowAlbumName = booleanPreferencesKey("showAlbumName")
val kDeeplinkURL = stringPreferencesKey("deeplink")
const val kWorkerWidgetType = "widgetType" const val kWorkerWidgetType = "widgetType"
const val kWorkerWidgetID = "widgetId" const val kWorkerWidgetID = "widgetId"
@ -64,3 +72,7 @@ const val kWorkerWidgetID = "widgetId"
fun imageFilename(id: String): String { fun imageFilename(id: String): String {
return "widget_image_$id.jpg" return "widget_image_$id.jpg"
} }
fun assetDeeplink(asset: Asset): String {
return "immich://asset?id=${asset.id}"
}

View File

@ -1,9 +1,11 @@
package app.alextran.immich.widget package app.alextran.immich.widget
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.core.net.toUri
import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.MutablePreferences
import androidx.glance.appwidget.* import androidx.glance.appwidget.*
import androidx.glance.* import androidx.glance.*
@ -25,7 +27,8 @@ class PhotoWidget : GlanceAppWidget() {
val prefs = currentState<MutablePreferences>() val prefs = currentState<MutablePreferences>()
val imageUUID = prefs[kImageUUID] val imageUUID = prefs[kImageUUID]
val subtitle: String? = prefs[kSubtitleText] val subtitle = prefs[kSubtitleText]
val deeplinkURL = prefs[kDeeplinkURL]?.toUri()
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
if (imageUUID != null) { if (imageUUID != null) {
@ -42,6 +45,11 @@ class PhotoWidget : GlanceAppWidget() {
modifier = GlanceModifier modifier = GlanceModifier
.fillMaxSize() .fillMaxSize()
.background(Color.White) .background(Color.White)
.clickable {
val intent = Intent(Intent.ACTION_VIEW, deeplinkURL ?: "immich://".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
) { ) {
if (bitmap != null) { if (bitmap != null) {
Image( Image(