Files
LockScreenWallpaper/plugins/android/src/main/java/WallpaperPlugin.kt
T
Christoph e494b17117 feat(wallpapers): add Immich integration with caching and rotation
This patch adds Immich integration for remote images with local
caching and pruning.
It adds image rotation to the crop flow and stores rotation
with the crop state.
It adds progress tracking and size checks during image
download to avoid oversized assets.

- Add Immich caching with size limits and automatic pruning
- Extend crop handling to support rotation and persist it
- Improve download progress and error messaging
2026-08-21 22:01:42 +02:00

191 lines
7.7 KiB
Kotlin

package de.wechselbild.wallpaper
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.result.ActivityResult
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import java.io.File
import java.util.UUID
import java.util.concurrent.Executors
@InvokeArg
class SettingArgs { lateinit var name: String; var value: Boolean = false }
@InvokeArg
class IntervalArgs { var minutes: Int = 0 }
@InvokeArg
class GalleryArgs { var offset: Int = 0; var limit: Int = 48 }
@InvokeArg
class DeleteImageArgs { lateinit var id: String }
@InvokeArg
class DeleteImagesArgs { var ids: Array<String> = emptyArray() }
@InvokeArg
class ImageCropArgs {
lateinit var id: String
lateinit var mode: String
var zoom: Double = 1.0
var positionX: Double = 0.5
var positionY: Double = 0.5
var rotation: Int = 0
}
@InvokeArg
class ImmichConnectArgs { lateinit var serverUrl: String; lateinit var apiKey: String }
@InvokeArg
class ImmichAssetsArgs { var albumId: String? = null; var page: Int = 1; var size: Int = 30 }
@InvokeArg
class ImmichImportArgs { var ids: Array<String> = emptyArray() }
@TauriPlugin
class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
private val io = Executors.newSingleThreadExecutor()
@Command fun getState(invoke: Invoke) {
if (WallpaperStore.enabled(activity)) runCatching { WallpaperRotationService.start(activity) }
io.execute { invoke.resolve(WallpaperStore.state(activity)) }
}
@Command fun getGallery(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(GalleryArgs::class.java)
invoke.resolve(WallpaperStore.gallery(activity, args.offset, args.limit))
} catch (error: Exception) { invoke.reject(error.message ?: "Galerie konnte nicht geladen werden") }
}
@Command fun selectImages(invoke: Invoke) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
startActivityForResult(invoke, intent, "selectedImages")
}
@Command fun deleteImage(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(DeleteImageArgs::class.java)
if (!WallpaperStore.delete(activity, args.id)) throw IllegalArgumentException("Bild wurde nicht gefunden")
invoke.resolve(WallpaperStore.state(activity))
} catch (error: Exception) { invoke.reject(error.message ?: "Bild konnte nicht gelöscht werden") }
}
@Command fun getImageIds(invoke: Invoke) = io.execute {
val ids = JSArray().apply { WallpaperStore.imageIds(activity).forEach { put(it) } }
invoke.resolve(JSObject().apply { put("ids", ids) })
}
@Command fun deleteImages(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(DeleteImagesArgs::class.java)
if (WallpaperStore.deleteMany(activity, args.ids.toList()) == 0) throw IllegalArgumentException("Keine Bilder gefunden")
invoke.resolve(WallpaperStore.state(activity))
} catch (error: Exception) { invoke.reject(error.message ?: "Bilder konnten nicht gelöscht werden") }
}
@Command fun setImageCrop(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(ImageCropArgs::class.java)
val image = WallpaperStore.setCrop(activity, args.id, args.mode, args.zoom, args.positionX, args.positionY, args.rotation)
?: throw IllegalArgumentException("Bild wurde nicht gefunden")
invoke.resolve(image)
} catch (error: Exception) { invoke.reject(error.message ?: "Bildausschnitt konnte nicht gespeichert werden") }
}
@ActivityCallback
fun selectedImages(invoke: Invoke, result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK) { invoke.reject("Bildauswahl abgebrochen"); return }
val uris = mutableListOf<Uri>()
result.data?.data?.let(uris::add)
result.data?.clipData?.let { clip -> for (i in 0 until clip.itemCount) uris.add(clip.getItemAt(i).uri) }
if (uris.isEmpty()) { invoke.reject("Keine Bilder ausgewählt"); return }
io.execute {
try {
uris.forEach { uri ->
val type = activity.contentResolver.getType(uri)
val extension = when (type) { "image/png" -> "png"; "image/webp" -> "webp"; else -> "jpg" }
val target = File(WallpaperStore.directory(activity), "${System.currentTimeMillis()}-${UUID.randomUUID()}.$extension")
activity.contentResolver.openInputStream(uri).use { input -> requireNotNull(input) { "Bild konnte nicht geöffnet werden" }; target.outputStream().use(input::copyTo) }
}
invoke.resolve(WallpaperStore.state(activity))
} catch (error: Exception) { invoke.reject(error.message ?: "Bilder konnten nicht importiert werden") }
}
}
@Command fun setSetting(invoke: Invoke) {
try {
val args = invoke.parseArgs(SettingArgs::class.java)
WallpaperStore.set(activity, args.name, args.value)
invoke.resolve(WallpaperStore.state(activity, includePreviews = false))
} catch (error: Exception) { invoke.reject(error.message ?: "Einstellung konnte nicht gespeichert werden") }
}
@Command fun setInterval(invoke: Invoke) {
try {
val args = invoke.parseArgs(IntervalArgs::class.java)
WallpaperStore.setInterval(activity, args.minutes)
if (args.minutes > 0) WallpaperRotationService.restart(activity) else WallpaperRotationService.stop(activity)
invoke.resolve(WallpaperStore.state(activity, includePreviews = false))
} catch (error: Exception) { invoke.reject(error.message ?: "Wechselintervall konnte nicht gespeichert werden") }
}
@Command fun nextWallpaper(invoke: Invoke) = io.execute {
try {
if (WallpaperStore.applyNext(activity)) invoke.resolve(WallpaperStore.state(activity))
else invoke.reject("Kein verfügbares Bild konnte angewendet werden")
} catch (error: Exception) { invoke.reject(error.message ?: "Das Hintergrundbild konnte nicht geändert werden") }
}
@Command fun getImmichConnection(invoke: Invoke) = io.execute {
invoke.resolve(ImmichClient.connection(activity))
}
@Command fun connectImmich(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(ImmichConnectArgs::class.java)
invoke.resolve(ImmichClient.connect(activity, args.serverUrl, args.apiKey))
} catch (error: Exception) { invoke.reject(error.message ?: "Immich-Verbindung fehlgeschlagen") }
}
@Command fun disconnectImmich(invoke: Invoke) = io.execute {
invoke.resolve(ImmichClient.disconnect(activity))
}
@Command fun getImmichAlbums(invoke: Invoke) = io.execute {
try { invoke.resolve(ImmichClient.albums(activity)) }
catch (error: Exception) { invoke.reject(error.message ?: "Immich-Alben konnten nicht geladen werden") }
}
@Command fun getImmichAssets(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(ImmichAssetsArgs::class.java)
invoke.resolve(ImmichClient.assets(activity, args.albumId, args.page, args.size))
} catch (error: Exception) { invoke.reject(error.message ?: "Immich-Bilder konnten nicht geladen werden") }
}
@Command fun importImmichAssets(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(ImmichImportArgs::class.java)
invoke.resolve(ImmichClient.importAssets(activity, args.ids.toList()))
} catch (error: Exception) { invoke.reject(error.message ?: "Immich-Bilder konnten nicht importiert werden") }
}
@Command fun getImmichImportProgress(invoke: Invoke) {
invoke.resolve(ImmichClient.importProgress())
}
}