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" />
|
android:resource="@xml/random_widget" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- <receiver-->
|
<receiver
|
||||||
<!-- android:name=".widget.MemoryReceiver"-->
|
android:name=".widget.MemoryReceiver"
|
||||||
<!-- android:exported="true">-->
|
android:exported="true">
|
||||||
<!-- <intent-filter>-->
|
<intent-filter>
|
||||||
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
<!-- </intent-filter>-->
|
</intent-filter>
|
||||||
<!-- <meta-data-->
|
<meta-data
|
||||||
<!-- android:name="android.appwidget.provider"-->
|
android:name="android.appwidget.provider"
|
||||||
<!-- android:resource="@xml/widget" />-->
|
android:resource="@xml/memory_widget" />
|
||||||
<!-- </receiver>-->
|
</receiver>
|
||||||
|
|
||||||
<activity android:name=".widget.configure.RandomConfigure"
|
<activity android:name=".widget.configure.RandomConfigure"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -16,6 +16,7 @@ import java.util.UUID
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import androidx.glance.appwidget.state.getAppWidgetState
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
class ImageDownloadWorker(
|
class ImageDownloadWorker(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -84,8 +85,8 @@ class ImageDownloadWorker(
|
|||||||
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
||||||
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
||||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
||||||
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
val widgetConfig = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
val currentImgUUID = currentState[kImageUUID]
|
val currentImgUUID = widgetConfig[kImageUUID]
|
||||||
|
|
||||||
val serverConfig = ImmichAPI.getServerConfig(context)
|
val serverConfig = ImmichAPI.getServerConfig(context)
|
||||||
|
|
||||||
@ -93,15 +94,16 @@ class ImageDownloadWorker(
|
|||||||
if (serverConfig == null) {
|
if (serverConfig == null) {
|
||||||
if (!currentImgUUID.isNullOrEmpty()) {
|
if (!currentImgUUID.isNullOrEmpty()) {
|
||||||
deleteImage(currentImgUUID)
|
deleteImage(currentImgUUID)
|
||||||
updateWidget(widgetType, glanceId, "", WidgetState.LOG_IN)
|
updateWidget(glanceId, "", "", WidgetState.LOG_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch new image
|
// fetch new image
|
||||||
val newBitmap = when (widgetType) {
|
val (newBitmap, subtitle) = when (widgetType) {
|
||||||
WidgetType.RANDOM -> fetchRandom(serverConfig, currentState)
|
WidgetType.RANDOM -> fetchRandom(serverConfig, widgetConfig)
|
||||||
|
WidgetType.MEMORIES -> fetchMemory(serverConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear current image if it exists
|
// clear current image if it exists
|
||||||
@ -110,10 +112,11 @@ class ImageDownloadWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save a new image
|
// save a new image
|
||||||
val imgUUID = saveImage(newBitmap)
|
val imgUUID = UUID.randomUUID().toString()
|
||||||
|
saveImage(newBitmap, imgUUID)
|
||||||
|
|
||||||
// trigger the update routine with new image uuid
|
// trigger the update routine with new image uuid
|
||||||
updateWidget(widgetType, glanceId, imgUUID)
|
updateWidget(glanceId, imgUUID, subtitle)
|
||||||
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} 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 ->
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
prefs[kNow] = System.currentTimeMillis()
|
prefs[kNow] = System.currentTimeMillis()
|
||||||
prefs[kImageUUID] = imageUUID
|
prefs[kImageUUID] = imageUUID
|
||||||
prefs[kWidgetState] = widgetState.toString()
|
prefs[kWidgetState] = widgetState.toString()
|
||||||
|
prefs[kSubtitleText] = subtitle ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
when (type) {
|
PhotoWidget().update(context,glanceId)
|
||||||
WidgetType.RANDOM -> RandomWidget().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 api = ImmichAPI(serverConfig)
|
||||||
|
|
||||||
val filters = SearchFilters(AssetType.IMAGE, size=1)
|
val filters = SearchFilters(AssetType.IMAGE, size=1)
|
||||||
val albumId = widgetData[kSelectedAlbum]
|
val albumId = widgetConfig[kSelectedAlbum]
|
||||||
|
val albumName = widgetConfig[kSelectedAlbumName]
|
||||||
|
|
||||||
if (albumId != null) {
|
if (albumId != null) {
|
||||||
filters.albumIds = listOf(albumId)
|
filters.albumIds = listOf(albumId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val random = api.fetchSearchResults(filters)
|
val random = api.fetchSearchResults(filters).first()
|
||||||
val image = api.fetchImage(random[0])
|
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) {
|
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
||||||
@ -159,13 +195,10 @@ class ImageDownloadWorker(
|
|||||||
file.delete()
|
file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveImage(bitmap: Bitmap): String = withContext(Dispatchers.IO) {
|
private suspend fun saveImage(bitmap: Bitmap, uuid: String) = withContext(Dispatchers.IO) {
|
||||||
val uuid = UUID.randomUUID().toString()
|
|
||||||
val file = File(context.cacheDir, imageFilename(uuid))
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
FileOutputStream(file).use { out ->
|
FileOutputStream(file).use { out ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import java.net.HttpURLConnection
|
|||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@ -71,8 +73,8 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
gson.fromJson(response, type)
|
gson.fromJson(response, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchMemory(date: Date): List<MemoryResult> = withContext(Dispatchers.IO) {
|
suspend fun fetchMemory(date: LocalDate): List<MemoryResult> = withContext(Dispatchers.IO) {
|
||||||
val iso8601 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(date)
|
val iso8601 = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
package app.alextran.immich.widget
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
import HomeWidgetGlanceWidgetReceiver
|
import HomeWidgetGlanceWidgetReceiver
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
class MemoryReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
|
class MemoryReceiver : HomeWidgetGlanceWidgetReceiver<PhotoWidget>() {
|
||||||
override val glanceAppWidget = RandomWidget()
|
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
|
// MARK: Widget Specific
|
||||||
|
|
||||||
enum class WidgetType {
|
enum class WidgetType {
|
||||||
RANDOM;
|
RANDOM, MEMORIES;
|
||||||
|
|
||||||
val widgetClass: Class<out GlanceAppWidget>
|
|
||||||
get() = when (this) {
|
|
||||||
RANDOM -> RandomWidget::class.java
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WidgetState {
|
enum class WidgetState {
|
||||||
@ -60,6 +55,7 @@ val kSubtitleText = stringPreferencesKey("subtitle")
|
|||||||
val kNow = longPreferencesKey("now")
|
val kNow = longPreferencesKey("now")
|
||||||
val kWidgetState = stringPreferencesKey("state")
|
val kWidgetState = stringPreferencesKey("state")
|
||||||
val kSelectedAlbum = stringPreferencesKey("albumID")
|
val kSelectedAlbum = stringPreferencesKey("albumID")
|
||||||
|
val kSelectedAlbumName = stringPreferencesKey("albumName")
|
||||||
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
||||||
|
|
||||||
const val kWorkerWidgetType = "widgetType"
|
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.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.datastore.preferences.core.MutablePreferences
|
import androidx.datastore.preferences.core.MutablePreferences
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.glance.appwidget.*
|
import androidx.glance.appwidget.*
|
||||||
import androidx.glance.*
|
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.GlanceStateDefinition
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import app.alextran.immich.R
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class RandomWidget : GlanceAppWidget() {
|
class PhotoWidget : GlanceAppWidget() {
|
||||||
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
|
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
|
||||||
|
|
||||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||||
|
|
||||||
provideContent {
|
provideContent {
|
||||||
val prefs = currentState<MutablePreferences>()
|
val prefs = currentState<MutablePreferences>()
|
||||||
val imageUUID = prefs[kImageUUID]
|
|
||||||
|
|
||||||
|
val imageUUID = prefs[kImageUUID]
|
||||||
val subtitle: String? = prefs[kSubtitleText]
|
val subtitle: String? = prefs[kSubtitleText]
|
||||||
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
|
||||||
val file = File(context.cacheDir, imageFilename(id))
|
val file = File(context.cacheDir, imageFilename(imageUUID))
|
||||||
|
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
bitmap = loadScaledBitmap(file, 500, 500)
|
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) {
|
override suspend fun onDelete(context: Context, glanceId: GlanceId) {
|
||||||
super.onDelete(context, glanceId)
|
super.onDelete(context, glanceId)
|
||||||
ImageDownloadWorker.cancel(context, glanceId)
|
ImageDownloadWorker.cancel(context, glanceId)
|
@ -5,8 +5,8 @@ import android.appwidget.AppWidgetManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
|
class RandomReceiver : HomeWidgetGlanceWidgetReceiver<PhotoWidget>() {
|
||||||
override val glanceAppWidget = RandomWidget()
|
override val glanceAppWidget = PhotoWidget()
|
||||||
|
|
||||||
override fun onUpdate(
|
override fun onUpdate(
|
||||||
context: Context,
|
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