chore: add project scaffolding and resources

This initial commit scaffolds the project with essential docs, assets, and build configs.
It adds Android icons, design assets, and release metadata to support mobile targets.
A fastlane config and F-Droid manifest are included to streamline builds.

- Adds F-Droid configuration for automated builds
- Includes license and privacy policy documents
- Provides app icons and assets for Android and design system
This commit is contained in:
2026-08-21 14:45:18 +02:00
parent 197090ddd2
commit 6e43d52680
169 changed files with 1685 additions and 256 deletions
+1
View File
@@ -3,6 +3,7 @@ name = "tauri-plugin-wallpaper"
version = "0.1.0"
authors = [ "LockScreenWallpaper" ]
description = ""
license = "GPL-3.0-only"
edition = "2021"
rust-version = "1.77.2"
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
+6 -2
View File
@@ -7,10 +7,14 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application>
<service android:name="de.wechselbild.wallpaper.WallpaperRotationService" android:exported="false" android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Changes the user-selected lock-screen wallpaper when the display turns on" />
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Changes the user-selected lock-screen wallpaper at the configured interval" />
</service>
<receiver android:name="de.wechselbild.wallpaper.BootReceiver" android:enabled="true" android:exported="true">
<intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter>
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver android:name="de.wechselbild.wallpaper.WallpaperAlarmReceiver" android:exported="false" />
</application>
</manifest>
@@ -6,6 +6,9 @@ import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED && WallpaperStore.enabled(context)) runCatching { WallpaperRotationService.start(context) }
if (intent.action in setOf(Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED) && WallpaperStore.enabled(context)) {
WallpaperScheduler.scheduleNext(context)
runCatching { WallpaperRotationService.start(context) }
}
}
}
@@ -0,0 +1,34 @@
package de.wechselbild.wallpaper
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import java.util.concurrent.Executors
class WallpaperAlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!WallpaperStore.enabled(context)) {
WallpaperScheduler.cancel(context)
return
}
val applicationContext = context.applicationContext
val result = goAsync()
executor.execute {
try {
if (WallpaperScheduler.claimIfDue(applicationContext)) try {
WallpaperStore.applyNext(applicationContext)
} finally {
WallpaperScheduler.scheduleNext(applicationContext)
}
} finally {
WallpaperScheduler.ensureScheduled(applicationContext)
WallpaperRotationService.rescheduleRunningService()
result.finish()
}
}
}
companion object {
private val executor = Executors.newSingleThreadExecutor()
}
}
@@ -9,6 +9,8 @@ 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
@@ -17,12 +19,18 @@ 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
@@ -36,7 +44,10 @@ class ImageCropArgs {
class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
private val io = Executors.newSingleThreadExecutor()
@Command fun getState(invoke: Invoke) = io.execute { invoke.resolve(WallpaperStore.state(activity)) }
@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 {
@@ -63,6 +74,19 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(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)
@@ -96,13 +120,19 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
try {
val args = invoke.parseArgs(SettingArgs::class.java)
WallpaperStore.set(activity, args.name, args.value)
if (args.name == "enabled") {
if (args.value) WallpaperRotationService.start(activity) else WallpaperRotationService.stop(activity)
}
invoke.resolve(WallpaperStore.state(activity))
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 {
if (WallpaperStore.applyNext(activity)) invoke.resolve(WallpaperStore.state(activity)) else invoke.reject("Bitte wähle zuerst Bilder aus")
}
@@ -4,26 +4,39 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
class WallpaperRotationService : Service() {
private val worker = Executors.newSingleThreadExecutor()
private val screenReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_SCREEN_ON && WallpaperStore.enabled(context)) worker.execute { WallpaperStore.applyNext(context) }
private val handler = Handler(Looper.getMainLooper())
private val rotate = object : Runnable {
override fun run() {
if (!WallpaperStore.enabled(this@WallpaperRotationService)) {
stopSelf()
return
}
worker.execute {
if (WallpaperScheduler.claimIfDue(this@WallpaperRotationService)) try {
WallpaperStore.applyNext(this@WallpaperRotationService)
} finally {
WallpaperScheduler.scheduleNext(this@WallpaperRotationService)
}
handler.post { scheduleLocalTimer() }
}
}
}
override fun onCreate() {
super.onCreate()
runningService = this
createChannel()
val launch = packageManager.getLaunchIntentForPackage(packageName)
val pending = PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
@@ -33,12 +46,34 @@ class WallpaperRotationService : Service() {
.setContentText(getString(R.string.wallpaper_service_text))
.setOngoing(true).setSilent(true).setContentIntent(pending).build()
startForeground(NOTIFICATION_ID, notification)
ContextCompat.registerReceiver(this, screenReceiver, IntentFilter(Intent.ACTION_SCREEN_ON), ContextCompat.RECEIVER_NOT_EXPORTED)
WallpaperScheduler.ensureScheduled(this)
scheduleLocalTimer()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_STICKY
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!WallpaperStore.enabled(this)) {
stopSelf()
return START_NOT_STICKY
}
WallpaperScheduler.ensureScheduled(this)
scheduleLocalTimer()
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { runCatching { unregisterReceiver(screenReceiver) }; worker.shutdown(); super.onDestroy() }
override fun onDestroy() {
handler.removeCallbacks(rotate)
worker.shutdown()
if (runningService === this) runningService = null
super.onDestroy()
}
private fun scheduleLocalTimer() {
handler.removeCallbacks(rotate)
if (WallpaperStore.enabled(this)) {
handler.postDelayed(rotate, WallpaperScheduler.remainingDelay(this))
}
}
private fun createChannel() {
if (Build.VERSION.SDK_INT >= 26) {
@@ -50,7 +85,23 @@ class WallpaperRotationService : Service() {
companion object {
private const val CHANNEL = "wechselbild_rotation"
private const val NOTIFICATION_ID = 4217
fun start(context: Context) = ContextCompat.startForegroundService(context, Intent(context, WallpaperRotationService::class.java))
fun stop(context: Context) { context.stopService(Intent(context, WallpaperRotationService::class.java)) }
@Volatile private var runningService: WallpaperRotationService? = null
fun rescheduleRunningService() {
val service = runningService ?: return
service.handler.post { service.scheduleLocalTimer() }
}
fun start(context: Context) {
WallpaperScheduler.ensureScheduled(context)
ContextCompat.startForegroundService(context, Intent(context, WallpaperRotationService::class.java))
}
fun restart(context: Context) {
WallpaperScheduler.scheduleNext(context)
start(context)
}
fun stop(context: Context) {
WallpaperScheduler.cancel(context)
context.stopService(Intent(context, WallpaperRotationService::class.java))
}
}
}
@@ -0,0 +1,70 @@
package de.wechselbild.wallpaper
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.SystemClock
object WallpaperScheduler {
private const val PREFS = "wechselbild"
private const val KEY_NEXT_TRIGGER = "next_rotation_elapsed"
private const val REQUEST_CODE = 4218
fun ensureScheduled(context: Context) {
if (!WallpaperStore.enabled(context)) {
cancel(context)
return
}
val now = SystemClock.elapsedRealtime()
val storedTrigger = preferences(context).getLong(KEY_NEXT_TRIGGER, 0L)
if (storedTrigger <= now) scheduleNext(context)
}
fun remainingDelay(context: Context): Long {
val trigger = preferences(context).getLong(KEY_NEXT_TRIGGER, 0L)
return (trigger - SystemClock.elapsedRealtime()).coerceAtLeast(0L)
}
@Synchronized
fun claimIfDue(context: Context): Boolean {
val preferences = preferences(context)
val trigger = preferences.getLong(KEY_NEXT_TRIGGER, 0L)
if (trigger <= 0L || trigger > SystemClock.elapsedRealtime() + 1_000L) return false
preferences.edit().putLong(KEY_NEXT_TRIGGER, Long.MAX_VALUE).commit()
return true
}
fun scheduleNext(context: Context) {
val minutes = WallpaperStore.intervalMinutes(context)
if (minutes <= 0) {
cancel(context)
return
}
val trigger = SystemClock.elapsedRealtime() + minutes * 60_000L
val alarmManager = context.getSystemService(AlarmManager::class.java)
val operation = operation(context)
if (Build.VERSION.SDK_INT >= 23) {
alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
} else {
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
}
preferences(context).edit().putLong(KEY_NEXT_TRIGGER, trigger).apply()
}
fun cancel(context: Context) {
context.getSystemService(AlarmManager::class.java).cancel(operation(context))
preferences(context).edit().remove(KEY_NEXT_TRIGGER).apply()
}
private fun operation(context: Context) = PendingIntent.getBroadcast(
context,
REQUEST_CODE,
Intent(context, WallpaperAlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
private fun preferences(context: Context) =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
}
+49 -20
View File
@@ -9,6 +9,7 @@ import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.util.Base64
import android.util.LruCache
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import java.io.ByteArrayOutputStream
@@ -19,34 +20,48 @@ import org.json.JSONObject
object WallpaperStore {
private const val PREFS = "wechselbild"
private const val KEY_INDEX = "current_index"
private const val KEY_INTERVAL = "interval_minutes"
private const val CROP_PREFIX = "crop_"
private const val HOME_PREVIEW_LIMIT = 12
private val thumbnailCache = LruCache<String, String>(48)
private data class CropSettings(val mode: String = "cover", val zoom: Double = 1.0, val x: Double = 0.5, val y: Double = 0.5)
fun directory(context: Context) = File(context.filesDir, "wallpapers").apply { mkdirs() }
fun files(context: Context) = directory(context).listFiles()?.filter { it.isFile }?.sortedBy { it.name } ?: emptyList()
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
fun enabled(context: Context) = prefs(context).getBoolean("enabled", false)
fun intervalMinutes(context: Context): Int {
val preferences = prefs(context)
return if (preferences.contains(KEY_INTERVAL)) preferences.getInt(KEY_INTERVAL, 0)
else if (preferences.getBoolean("enabled", false)) 30 else 0
}
fun enabled(context: Context) = intervalMinutes(context) > 0
fun shuffle(context: Context) = prefs(context).getBoolean("shuffle", true)
fun lockOnly(context: Context) = prefs(context).getBoolean("lockScreenOnly", true)
fun set(context: Context, name: String, value: Boolean) {
require(name in setOf("enabled", "shuffle", "lockScreenOnly")) { "Unbekannte Einstellung" }
require(name in setOf("shuffle", "lockScreenOnly")) { "Unbekannte Einstellung" }
prefs(context).edit().putBoolean(name, value).apply()
}
fun state(context: Context): JSObject {
fun setInterval(context: Context, minutes: Int) {
require(minutes == 0 || minutes in 5..720) { "Ungültiges Wechselintervall" }
prefs(context).edit().putInt(KEY_INTERVAL, minutes).putBoolean("enabled", minutes > 0).apply()
}
fun state(context: Context, includePreviews: Boolean = true): JSObject {
val originals = files(context)
val index = prefs(context).getInt(KEY_INDEX, 0).coerceIn(0, (originals.size - 1).coerceAtLeast(0))
val previewFiles = if (index < 24) originals.take(24) else listOf(originals[index]) + originals.take(23)
val previewIndex = if (index < 24) index else 0
val previewFiles = if (index < HOME_PREVIEW_LIMIT) originals.take(HOME_PREVIEW_LIMIT) else listOf(originals[index]) + originals.take(HOME_PREVIEW_LIMIT - 1)
val previewIndex = if (index < HOME_PREVIEW_LIMIT) index else 0
return JSObject().apply {
put("imageCount", originals.size)
put("enabled", enabled(context))
put("intervalMinutes", intervalMinutes(context))
put("shuffle", shuffle(context))
put("lockScreenOnly", lockOnly(context))
put("currentIndex", previewIndex)
val previews = JSArray()
previewFiles.forEach { previews.put(thumbnailDataUrl(it)) }
if (includePreviews) previewFiles.forEach { previews.put(thumbnailDataUrl(it)) }
put("imageUrls", previews)
}
}
@@ -66,6 +81,8 @@ object WallpaperStore {
}
}
fun imageIds(context: Context) = files(context).map { it.name }
private fun cropKey(id: String) = CROP_PREFIX + id
private fun crop(context: Context, file: File): CropSettings {
@@ -74,7 +91,7 @@ object WallpaperStore {
val json = JSONObject(raw)
CropSettings(
mode = if (json.optString("mode") == "contain") "contain" else "cover",
zoom = json.optDouble("zoom", 1.0).coerceIn(0.35, 3.0),
zoom = json.optDouble("zoom", 1.0).coerceIn(1.0, 3.0),
x = json.optDouble("x", 0.5).coerceIn(0.0, 1.0),
y = json.optDouble("y", 0.5).coerceIn(0.0, 1.0),
)
@@ -100,7 +117,7 @@ object WallpaperStore {
val file = files(context).firstOrNull { it.name == id } ?: return null
val normalized = CropSettings(
mode = if (mode == "contain") "contain" else "cover",
zoom = zoom.coerceIn(0.35, 3.0),
zoom = zoom.coerceIn(1.0, 3.0),
x = x.coerceIn(0.0, 1.0),
y = y.coerceIn(0.0, 1.0),
)
@@ -117,22 +134,30 @@ object WallpaperStore {
@Synchronized
fun delete(context: Context, id: String): Boolean {
if (id.isBlank() || File(id).name != id) return false
val originals = files(context)
val position = originals.indexOfFirst { it.name == id }
if (position < 0 || !originals[position].delete()) return false
return deleteMany(context, listOf(id)) == 1
}
@Synchronized
fun deleteMany(context: Context, ids: List<String>): Int {
val requested = ids.filter { it.isNotBlank() && File(it).name == it }.toSet()
if (requested.isEmpty()) return 0
val originals = files(context)
val preferences = prefs(context)
val previousIndex = preferences.getInt(KEY_INDEX, 0).coerceIn(0, (originals.size - 1).coerceAtLeast(0))
val remaining = originals.size - 1
val currentName = originals.getOrNull(previousIndex)?.name
val deleted = originals.filter { it.name in requested && it.delete() }
if (deleted.isEmpty()) return 0
val remaining = files(context)
val retainedCurrent = currentName?.let { name -> remaining.indexOfFirst { it.name == name } } ?: -1
val nextIndex = when {
remaining <= 0 -> 0
position < previousIndex -> previousIndex - 1
previousIndex >= remaining -> remaining - 1
else -> previousIndex
remaining.isEmpty() -> 0
retainedCurrent >= 0 -> retainedCurrent
else -> previousIndex.coerceAtMost(remaining.lastIndex)
}
preferences.edit().remove(cropKey(id)).putInt(KEY_INDEX, nextIndex).apply()
return true
val editor = preferences.edit().putInt(KEY_INDEX, nextIndex)
deleted.forEach { editor.remove(cropKey(it.name)) }
editor.apply()
return deleted.size
}
private fun renderForScreen(context: Context, source: Bitmap, crop: CropSettings): Bitmap {
@@ -174,16 +199,20 @@ object WallpaperStore {
}
private fun thumbnailDataUrl(file: File): String {
val cacheKey = "${file.absolutePath}:${file.lastModified()}:${file.length()}"
thumbnailCache.get(cacheKey)?.let { return it }
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(file.absolutePath, bounds)
var sample = 1
while (bounds.outWidth / sample > 360 || bounds.outHeight / sample > 480) sample *= 2
val bitmap = BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample }) ?: return ""
return ByteArrayOutputStream().use { out ->
val result = ByteArrayOutputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 76, out)
bitmap.recycle()
"data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
}
thumbnailCache.put(cacheKey, result)
return result
}
@Synchronized
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow ist aktiv</string>
<string name="wallpaper_service_text">Das Motiv wechselt beim Aktivieren des Displays.</string>
<string name="wallpaper_service_text">Das Motiv wechselt nach dem eingestellten Intervall.</string>
<string name="wallpaper_channel_name">Automatischer Bildwechsel</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow está activo</string>
<string name="wallpaper_service_text">El fondo cambia según el intervalo configurado.</string>
<string name="wallpaper_channel_name">Rotación automática de fondos</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow est actif</string>
<string name="wallpaper_service_text">Le fond d’écran change selon lintervalle défini.</string>
<string name="wallpaper_channel_name">Rotation automatique des fonds d’écran</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow è attivo</string>
<string name="wallpaper_service_text">Lo sfondo cambia allintervallo configurato.</string>
<string name="wallpaper_channel_name">Rotazione automatica degli sfondi</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow は実行中です</string>
<string name="wallpaper_service_text">設定した間隔で壁紙が切り替わります。</string>
<string name="wallpaper_channel_name">壁紙の自動切り替え</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow 실행 중</string>
<string name="wallpaper_service_text">설정한 간격에 따라 배경화면이 변경됩니다.</string>
<string name="wallpaper_channel_name">자동 배경화면 변경</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow is actief</string>
<string name="wallpaper_service_text">De achtergrond wisselt volgens het ingestelde interval.</string>
<string name="wallpaper_channel_name">Automatisch achtergronden wisselen</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow jest aktywny</string>
<string name="wallpaper_service_text">Tapeta zmienia się zgodnie z ustawionym interwałem.</string>
<string name="wallpaper_channel_name">Automatyczna zmiana tapety</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow está ativo</string>
<string name="wallpaper_service_text">O fundo muda de acordo com o intervalo definido.</string>
<string name="wallpaper_channel_name">Rotação automática de fundos</string>
</resources>
@@ -0,0 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow 正在运行</string>
<string name="wallpaper_service_text">壁纸会按照设定的间隔更换。</string>
<string name="wallpaper_channel_name">自动更换壁纸</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow is active</string>
<string name="wallpaper_service_text">The wallpaper changes whenever the screen wakes.</string>
<string name="wallpaper_service_text">The wallpaper changes at the configured interval.</string>
<string name="wallpaper_channel_name">Automatic wallpaper rotation</string>
</resources>
+3
View File
@@ -3,8 +3,11 @@ const COMMANDS: &[&str] = &[
"get_gallery",
"select_images",
"delete_image",
"get_image_ids",
"delete_images",
"set_image_crop",
"set_setting",
"set_interval",
"next_wallpaper",
];
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-delete-images"
description = "Enables the delete_images command without any pre-configured scope."
commands.allow = ["delete_images"]
[[permission]]
identifier = "deny-delete-images"
description = "Denies the delete_images command without any pre-configured scope."
commands.deny = ["delete_images"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-image-ids"
description = "Enables the get_image_ids command without any pre-configured scope."
commands.allow = ["get_image_ids"]
[[permission]]
identifier = "deny-get-image-ids"
description = "Denies the get_image_ids command without any pre-configured scope."
commands.deny = ["get_image_ids"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-set-interval"
description = "Enables the set_interval command without any pre-configured scope."
commands.allow = ["set_interval"]
[[permission]]
identifier = "deny-set-interval"
description = "Denies the set_interval command without any pre-configured scope."
commands.deny = ["set_interval"]
@@ -8,8 +8,11 @@ Allow the LockScreenWallpaper app to manage its wallpaper collection
- `allow-get-gallery`
- `allow-select-images`
- `allow-delete-image`
- `allow-get-image-ids`
- `allow-delete-images`
- `allow-set-image-crop`
- `allow-set-setting`
- `allow-set-interval`
- `allow-next-wallpaper`
## Permission Table
@@ -50,6 +53,32 @@ Denies the delete_image command without any pre-configured scope.
<tr>
<td>
`wallpaper:allow-delete-images`
</td>
<td>
Enables the delete_images command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-delete-images`
</td>
<td>
Denies the delete_images command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-get-gallery`
</td>
@@ -76,6 +105,32 @@ Denies the get_gallery command without any pre-configured scope.
<tr>
<td>
`wallpaper:allow-get-image-ids`
</td>
<td>
Enables the get_image_ids command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-get-image-ids`
</td>
<td>
Denies the get_image_ids command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-get-state`
</td>
@@ -180,6 +235,32 @@ Denies the set_image_crop command without any pre-configured scope.
<tr>
<td>
`wallpaper:allow-set-interval`
</td>
<td>
Enables the set_interval command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-set-interval`
</td>
<td>
Denies the set_interval command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-set-setting`
</td>
+1 -1
View File
@@ -1,3 +1,3 @@
[default]
description = "Allow the LockScreenWallpaper app to manage its wallpaper collection"
permissions = ["allow-get-state", "allow-get-gallery", "allow-select-images", "allow-delete-image", "allow-set-image-crop", "allow-set-setting", "allow-next-wallpaper"]
permissions = ["allow-get-state", "allow-get-gallery", "allow-select-images", "allow-delete-image", "allow-get-image-ids", "allow-delete-images", "allow-set-image-crop", "allow-set-setting", "allow-set-interval", "allow-next-wallpaper"]
+38 -2
View File
@@ -306,6 +306,18 @@
"const": "deny-delete-image",
"markdownDescription": "Denies the delete_image command without any pre-configured scope."
},
{
"description": "Enables the delete_images command without any pre-configured scope.",
"type": "string",
"const": "allow-delete-images",
"markdownDescription": "Enables the delete_images command without any pre-configured scope."
},
{
"description": "Denies the delete_images command without any pre-configured scope.",
"type": "string",
"const": "deny-delete-images",
"markdownDescription": "Denies the delete_images command without any pre-configured scope."
},
{
"description": "Enables the get_gallery command without any pre-configured scope.",
"type": "string",
@@ -318,6 +330,18 @@
"const": "deny-get-gallery",
"markdownDescription": "Denies the get_gallery command without any pre-configured scope."
},
{
"description": "Enables the get_image_ids command without any pre-configured scope.",
"type": "string",
"const": "allow-get-image-ids",
"markdownDescription": "Enables the get_image_ids command without any pre-configured scope."
},
{
"description": "Denies the get_image_ids command without any pre-configured scope.",
"type": "string",
"const": "deny-get-image-ids",
"markdownDescription": "Denies the get_image_ids command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
"type": "string",
@@ -366,6 +390,18 @@
"const": "deny-set-image-crop",
"markdownDescription": "Denies the set_image_crop command without any pre-configured scope."
},
{
"description": "Enables the set_interval command without any pre-configured scope.",
"type": "string",
"const": "allow-set-interval",
"markdownDescription": "Enables the set_interval command without any pre-configured scope."
},
{
"description": "Denies the set_interval command without any pre-configured scope.",
"type": "string",
"const": "deny-set-interval",
"markdownDescription": "Denies the set_interval command without any pre-configured scope."
},
{
"description": "Enables the set_setting command without any pre-configured scope.",
"type": "string",
@@ -379,10 +415,10 @@
"markdownDescription": "Denies the set_setting command without any pre-configured scope."
},
{
"description": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-next-wallpaper`",
"description": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-get-image-ids`\n- `allow-delete-images`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-set-interval`\n- `allow-next-wallpaper`",
"type": "string",
"const": "default",
"markdownDescription": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-next-wallpaper`"
"markdownDescription": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-get-image-ids`\n- `allow-delete-images`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-set-interval`\n- `allow-next-wallpaper`"
}
]
}
+12
View File
@@ -29,6 +29,14 @@ pub(crate) async fn delete_image<R: Runtime>(
app.wallpaper().delete_image(DeleteImageRequest { id })
}
#[command]
pub(crate) async fn get_image_ids<R: Runtime>(app: AppHandle<R>) -> Result<Vec<String>> {
app.wallpaper().get_image_ids()
}
#[command]
pub(crate) async fn delete_images<R: Runtime>(app: AppHandle<R>, ids: Vec<String>) -> Result<WallpaperState> {
app.wallpaper().delete_images(DeleteImagesRequest { ids })
}
#[command]
pub(crate) async fn set_image_crop<R: Runtime>(
app: AppHandle<R>,
id: String,
@@ -54,6 +62,10 @@ pub(crate) async fn set_setting<R: Runtime>(
app.wallpaper().set_setting(SettingRequest { name, value })
}
#[command]
pub(crate) async fn set_interval<R: Runtime>(app: AppHandle<R>, minutes: usize) -> Result<WallpaperState> {
app.wallpaper().set_interval(IntervalRequest { minutes })
}
#[command]
pub(crate) async fn next_wallpaper<R: Runtime>(app: AppHandle<R>) -> Result<WallpaperState> {
app.wallpaper().next_wallpaper()
}
+13
View File
@@ -18,6 +18,7 @@ impl<R: Runtime> Wallpaper<R> {
WallpaperState {
image_count: 3,
enabled: true,
interval_minutes: 30,
shuffle: true,
lock_screen_only: true,
current_index: 0,
@@ -56,6 +57,12 @@ impl<R: Runtime> Wallpaper<R> {
pub fn delete_image(&self, _payload: DeleteImageRequest) -> crate::Result<WallpaperState> {
Ok(Self::demo())
}
pub fn get_image_ids(&self) -> crate::Result<Vec<String>> {
Ok((0..Self::demo().image_count).map(|index| format!("demo-{index}")).collect())
}
pub fn delete_images(&self, _payload: DeleteImagesRequest) -> crate::Result<WallpaperState> {
Ok(Self::demo())
}
pub fn set_image_crop(&self, payload: ImageCropRequest) -> crate::Result<GalleryImage> {
let url = Self::demo()
.image_urls
@@ -82,6 +89,12 @@ impl<R: Runtime> Wallpaper<R> {
};
Ok(state)
}
pub fn set_interval(&self, payload: IntervalRequest) -> crate::Result<WallpaperState> {
let mut state = Self::demo();
state.interval_minutes = payload.minutes;
state.enabled = payload.minutes > 0;
Ok(state)
}
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> {
let mut state = Self::demo();
state.current_index = 1;
+3
View File
@@ -40,8 +40,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::get_gallery,
commands::select_images,
commands::delete_image,
commands::get_image_ids,
commands::delete_images,
commands::set_image_crop,
commands::set_setting,
commands::set_interval,
commands::next_wallpaper
])
.setup(|app, api| {
+12
View File
@@ -43,6 +43,13 @@ impl<R: Runtime> Wallpaper<R> {
.run_mobile_plugin("deleteImage", payload)
.map_err(Into::into)
}
pub fn get_image_ids(&self) -> crate::Result<Vec<String>> {
let response: ImageIdsResponse = self.0.run_mobile_plugin("getImageIds", ())?;
Ok(response.ids)
}
pub fn delete_images(&self, payload: DeleteImagesRequest) -> crate::Result<WallpaperState> {
self.0.run_mobile_plugin("deleteImages", payload).map_err(Into::into)
}
pub fn set_image_crop(&self, payload: ImageCropRequest) -> crate::Result<GalleryImage> {
self.0
.run_mobile_plugin("setImageCrop", payload)
@@ -53,6 +60,11 @@ impl<R: Runtime> Wallpaper<R> {
.run_mobile_plugin("setSetting", payload)
.map_err(Into::into)
}
pub fn set_interval(&self, payload: IntervalRequest) -> crate::Result<WallpaperState> {
self.0
.run_mobile_plugin("setInterval", payload)
.map_err(Into::into)
}
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> {
self.0
.run_mobile_plugin("nextWallpaper", ())
+19
View File
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
pub struct WallpaperState {
pub image_count: usize,
pub enabled: bool,
pub interval_minutes: usize,
pub shuffle: bool,
pub lock_screen_only: bool,
pub current_index: usize,
@@ -18,6 +19,12 @@ pub struct SettingRequest {
pub value: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IntervalRequest {
pub minutes: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GalleryRequest {
@@ -31,6 +38,18 @@ pub struct DeleteImageRequest {
pub id: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteImagesRequest {
pub ids: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageIdsResponse {
pub ids: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageCropRequest {