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:
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 l’intervalle 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 all’intervallo 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,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,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"]
|
||||
|
||||
@@ -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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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", ())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user