mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
add memory widget and cleanup of codebase
This commit is contained in:
parent
6b0be03a4b
commit
ada0f984b8
@ -155,16 +155,16 @@
|
||||
android:resource="@xml/random_widget" />
|
||||
</receiver>
|
||||
|
||||
<!-- <receiver-->
|
||||
<!-- android:name=".widget.MemoryReceiver"-->
|
||||
<!-- android:exported="true">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="android.appwidget.provider"-->
|
||||
<!-- android:resource="@xml/widget" />-->
|
||||
<!-- </receiver>-->
|
||||
<receiver
|
||||
android:name=".widget.MemoryReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/memory_widget" />
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".widget.configure.RandomConfigure"
|
||||
android:exported="true"
|
||||
|
@ -16,6 +16,7 @@ import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import java.time.LocalDate
|
||||
|
||||
class ImageDownloadWorker(
|
||||
private val context: Context,
|
||||
@ -84,8 +85,8 @@ class ImageDownloadWorker(
|
||||
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
||||
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
||||
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||
val currentImgUUID = currentState[kImageUUID]
|
||||
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||
val currentImgUUID = widgetConfig[kImageUUID]
|
||||
|
||||
val serverConfig = ImmichAPI.getServerConfig(context)
|
||||
|
||||
@ -93,15 +94,16 @@ class ImageDownloadWorker(
|
||||
if (serverConfig == null) {
|
||||
if (!currentImgUUID.isNullOrEmpty()) {
|
||||
deleteImage(currentImgUUID)
|
||||
updateWidget(widgetType, glanceId, "", WidgetState.LOG_IN)
|
||||
updateWidget(glanceId, "", "", WidgetState.LOG_IN)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
// fetch new image
|
||||
val newBitmap = when (widgetType) {
|
||||
WidgetType.RANDOM -> 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<Bitmap, String?> {
|
||||
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<Bitmap, String?> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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<MemoryResult> = withContext(Dispatchers.IO) {
|
||||
val iso8601 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(date)
|
||||
suspend fun fetchMemory(date: LocalDate): List<MemoryResult> = 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"
|
||||
|
@ -1,8 +1,22 @@
|
||||
package app.alextran.immich.widget
|
||||
|
||||
import HomeWidgetGlanceWidgetReceiver
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
|
||||
class MemoryReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
|
||||
override val glanceAppWidget = RandomWidget()
|
||||
class MemoryReceiver : HomeWidgetGlanceWidgetReceiver<PhotoWidget>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,12 +40,7 @@ data class Album(
|
||||
// MARK: Widget Specific
|
||||
|
||||
enum class WidgetType {
|
||||
RANDOM;
|
||||
|
||||
val widgetClass: Class<out GlanceAppWidget>
|
||||
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"
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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<MutablePreferences>()
|
||||
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)
|
@ -5,8 +5,8 @@ import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
||||
class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
|
||||
override val glanceAppWidget = RandomWidget()
|
||||
class RandomReceiver : HomeWidgetGlanceWidgetReceiver<PhotoWidget>() {
|
||||
override val glanceAppWidget = PhotoWidget()
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
|
7
mobile/android/app/src/main/res/xml/memory_widget.xml
Normal file
7
mobile/android/app/src/main/res/xml/memory_widget.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="110dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="1200000"
|
||||
/>
|
Loading…
x
Reference in New Issue
Block a user