feat(setup): bootstrap Android plugin and web UI

This initial commit adds a multi-part scaffold for Wechselbild, including a web UI and an Android plugin.
It introduces frontend assets and a package.json-based dev setup, plus plugin scaffolding for Android with Kotlin sources and Cargo.toml for the Tauri plugin.

- Adds Android wallpaper plugin with Kotlin sources and manifest
- Includes web UI scaffold with index.html and package.json
- Adds Tauri plugin scaffold with Cargo.toml and build config
This commit is contained in:
2026-08-20 17:46:28 +02:00
commit 2a85d5f622
148 changed files with 18356 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
/.vs
.DS_Store
.Thumbs.db
*.sublime*
.idea/
debug.log
package-lock.json
.vscode/settings.json
yarn.lock
/.tauri
/target
Cargo.lock
node_modules/
dist-js
dist
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "tauri-plugin-wallpaper"
version = "0.1.0"
authors = [ "Wechselbild" ]
description = ""
edition = "2021"
rust-version = "1.77.2"
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
links = "tauri-plugin-wallpaper"
[dependencies]
tauri = { version = "2.11.3" }
serde = "1.0"
thiserror = "2"
[build-dependencies]
tauri-plugin = { version = "2.6.3", features = ["build"] }
+1
View File
@@ -0,0 +1 @@
# Tauri Plugin wallpaper
+2
View File
@@ -0,0 +1,2 @@
/build
/.tauri
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "de.wechselbild.wallpaper"
compileSdk = 36
defaultConfig {
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation(project(":tauri-android"))
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+31
View File
@@ -0,0 +1,31 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
google()
}
resolutionStrategy {
eachPlugin {
switch (requested.id.id) {
case "com.android.library":
useVersion("8.0.2")
break
case "org.jetbrains.kotlin.android":
useVersion("1.8.20")
break
}
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
google()
}
}
include ':tauri-android'
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<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" />
</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>
</receiver>
</application>
</manifest>
@@ -0,0 +1,11 @@
package de.wechselbild.wallpaper
import android.content.BroadcastReceiver
import android.content.Context
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) }
}
}
@@ -0,0 +1,70 @@
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.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 }
@TauriPlugin
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 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")
}
@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)
if (args.name == "enabled") {
if (args.value) WallpaperRotationService.start(activity) else WallpaperRotationService.stop(activity)
}
invoke.resolve(WallpaperStore.state(activity))
} catch (error: Exception) { invoke.reject(error.message ?: "Einstellung 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")
}
}
@@ -0,0 +1,56 @@
package de.wechselbild.wallpaper
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.IBinder
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) }
}
}
override fun onCreate() {
super.onCreate()
createChannel()
val launch = packageManager.getLaunchIntentForPackage(packageName)
val pending = PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
val notification = NotificationCompat.Builder(this, CHANNEL)
.setSmallIcon(android.R.drawable.ic_menu_gallery)
.setContentTitle("Wechselbild ist aktiv")
.setContentText("Das Motiv wechselt beim Aktivieren des Displays.")
.setOngoing(true).setSilent(true).setContentIntent(pending).build()
startForeground(NOTIFICATION_ID, notification)
ContextCompat.registerReceiver(this, screenReceiver, IntentFilter(Intent.ACTION_SCREEN_ON), ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_STICKY
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { runCatching { unregisterReceiver(screenReceiver) }; worker.shutdown(); super.onDestroy() }
private fun createChannel() {
if (Build.VERSION.SDK_INT >= 26) {
val channel = NotificationChannel(CHANNEL, "Automatischer Bildwechsel", NotificationManager.IMPORTANCE_LOW)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}
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)) }
}
}
@@ -0,0 +1,78 @@
package de.wechselbild.wallpaper
import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.random.Random
object WallpaperStore {
private const val PREFS = "wechselbild"
private const val KEY_INDEX = "current_index"
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 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" }
prefs(context).edit().putBoolean(name, value).apply()
}
fun state(context: Context): 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
return JSObject().apply {
put("imageCount", originals.size)
put("enabled", enabled(context))
put("shuffle", shuffle(context))
put("lockScreenOnly", lockOnly(context))
put("currentIndex", previewIndex)
val previews = JSArray()
previewFiles.forEach { previews.put(thumbnailDataUrl(it)) }
put("imageUrls", previews)
}
}
private fun thumbnailDataUrl(file: File): String {
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 ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 76, out)
bitmap.recycle()
"data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
}
}
@Synchronized
fun applyNext(context: Context): Boolean {
val images = files(context)
if (images.isEmpty()) return false
val preferences = prefs(context)
val previous = preferences.getInt(KEY_INDEX, -1)
val index = if (shuffle(context) && images.size > 1) {
generateSequence { Random.nextInt(images.size) }.first { it != previous }
} else (previous + 1).mod(images.size)
val bitmap = BitmapFactory.decodeFile(images[index].absolutePath) ?: return false
try {
val manager = WallpaperManager.getInstance(context)
if (android.os.Build.VERSION.SDK_INT >= 24 && lockOnly(context)) manager.setBitmap(bitmap, null, true, WallpaperManager.FLAG_LOCK)
else manager.setBitmap(bitmap)
preferences.edit().putInt(KEY_INDEX, index).apply()
return true
} finally { bitmap.recycle() }
}
}
+8
View File
@@ -0,0 +1,8 @@
const COMMANDS: &[&str] = &["get_state", "select_images", "set_setting", "next_wallpaper"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
.android_path("android")
.ios_path("ios")
.build();
}
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-state"
description = "Enables the get_state command without any pre-configured scope."
commands.allow = ["get_state"]
[[permission]]
identifier = "deny-get-state"
description = "Denies the get_state command without any pre-configured scope."
commands.deny = ["get_state"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-next-wallpaper"
description = "Enables the next_wallpaper command without any pre-configured scope."
commands.allow = ["next_wallpaper"]
[[permission]]
identifier = "deny-next-wallpaper"
description = "Denies the next_wallpaper command without any pre-configured scope."
commands.deny = ["next_wallpaper"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-select-images"
description = "Enables the select_images command without any pre-configured scope."
commands.allow = ["select_images"]
[[permission]]
identifier = "deny-select-images"
description = "Denies the select_images command without any pre-configured scope."
commands.deny = ["select_images"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-set-setting"
description = "Enables the set_setting command without any pre-configured scope."
commands.allow = ["set_setting"]
[[permission]]
identifier = "deny-set-setting"
description = "Denies the set_setting command without any pre-configured scope."
commands.deny = ["set_setting"]
@@ -0,0 +1,124 @@
## Default Permission
Allow the Wechselbild app to manage its wallpaper collection
#### This default permission set includes the following:
- `allow-get-state`
- `allow-select-images`
- `allow-set-setting`
- `allow-next-wallpaper`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`wallpaper:allow-get-state`
</td>
<td>
Enables the get_state command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-get-state`
</td>
<td>
Denies the get_state command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-next-wallpaper`
</td>
<td>
Enables the next_wallpaper command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-next-wallpaper`
</td>
<td>
Denies the next_wallpaper command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-select-images`
</td>
<td>
Enables the select_images command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-select-images`
</td>
<td>
Denies the select_images command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-set-setting`
</td>
<td>
Enables the set_setting command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-set-setting`
</td>
<td>
Denies the set_setting command without any pre-configured scope.
</td>
</tr>
</table>
+3
View File
@@ -0,0 +1,3 @@
[default]
description = "Allow the Wechselbild app to manage its wallpaper collection"
permissions = ["allow-get-state", "allow-select-images", "allow-set-setting", "allow-next-wallpaper"]
+354
View File
@@ -0,0 +1,354 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PermissionFile",
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
"type": "object",
"properties": {
"default": {
"description": "The default permission set for the plugin",
"anyOf": [
{
"$ref": "#/definitions/DefaultPermission"
},
{
"type": "null"
}
]
},
"set": {
"description": "A list of permissions sets defined",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionSet"
}
},
"permission": {
"description": "A list of inlined permissions",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Permission"
}
}
},
"definitions": {
"DefaultPermission": {
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
"type": "object",
"required": [
"permissions"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"description": {
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"PermissionSet": {
"description": "A set of direct permissions grouped together under a new name.",
"type": "object",
"required": [
"description",
"identifier",
"permissions"
],
"properties": {
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does.",
"type": "string"
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionKind"
}
}
}
},
"Permission": {
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"commands": {
"description": "Allowed or denied commands when using this permission.",
"default": {
"allow": [],
"deny": []
},
"allOf": [
{
"$ref": "#/definitions/Commands"
}
]
},
"scope": {
"description": "Allowed or denied scoped when using this permission.",
"allOf": [
{
"$ref": "#/definitions/Scopes"
}
]
},
"platforms": {
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Target"
}
}
}
},
"Commands": {
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
"type": "object",
"properties": {
"allow": {
"description": "Allowed command.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"deny": {
"description": "Denied command, which takes priority.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
}
},
"Scopes": {
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
"type": "object",
"properties": {
"allow": {
"description": "Data that defines what is allowed by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
},
"deny": {
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
}
}
},
"Value": {
"description": "All supported ACL values.",
"anyOf": [
{
"description": "Represents a null JSON value.",
"type": "null"
},
{
"description": "Represents a [`bool`].",
"type": "boolean"
},
{
"description": "Represents a valid ACL [`Number`].",
"allOf": [
{
"$ref": "#/definitions/Number"
}
]
},
{
"description": "Represents a [`String`].",
"type": "string"
},
{
"description": "Represents a list of other [`Value`]s.",
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
},
{
"description": "Represents a map of [`String`] keys to [`Value`]s.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Value"
}
}
]
},
"Number": {
"description": "A valid ACL number.",
"anyOf": [
{
"description": "Represents an [`i64`].",
"type": "integer",
"format": "int64"
},
{
"description": "Represents a [`f64`].",
"type": "number",
"format": "double"
}
]
},
"Target": {
"description": "Platform target.",
"oneOf": [
{
"description": "MacOS.",
"type": "string",
"enum": [
"macOS"
]
},
{
"description": "Windows.",
"type": "string",
"enum": [
"windows"
]
},
{
"description": "Linux.",
"type": "string",
"enum": [
"linux"
]
},
{
"description": "Android.",
"type": "string",
"enum": [
"android"
]
},
{
"description": "iOS.",
"type": "string",
"enum": [
"iOS"
]
}
]
},
"PermissionKind": {
"type": "string",
"oneOf": [
{
"description": "Enables the get_state command without any pre-configured scope.",
"type": "string",
"const": "allow-get-state",
"markdownDescription": "Enables the get_state command without any pre-configured scope."
},
{
"description": "Denies the get_state command without any pre-configured scope.",
"type": "string",
"const": "deny-get-state",
"markdownDescription": "Denies the get_state command without any pre-configured scope."
},
{
"description": "Enables the next_wallpaper command without any pre-configured scope.",
"type": "string",
"const": "allow-next-wallpaper",
"markdownDescription": "Enables the next_wallpaper command without any pre-configured scope."
},
{
"description": "Denies the next_wallpaper command without any pre-configured scope.",
"type": "string",
"const": "deny-next-wallpaper",
"markdownDescription": "Denies the next_wallpaper command without any pre-configured scope."
},
{
"description": "Enables the select_images command without any pre-configured scope.",
"type": "string",
"const": "allow-select-images",
"markdownDescription": "Enables the select_images command without any pre-configured scope."
},
{
"description": "Denies the select_images command without any pre-configured scope.",
"type": "string",
"const": "deny-select-images",
"markdownDescription": "Denies the select_images command without any pre-configured scope."
},
{
"description": "Enables the set_setting command without any pre-configured scope.",
"type": "string",
"const": "allow-set-setting",
"markdownDescription": "Enables the set_setting command without any pre-configured scope."
},
{
"description": "Denies the set_setting command without any pre-configured scope.",
"type": "string",
"const": "deny-set-setting",
"markdownDescription": "Denies the set_setting command without any pre-configured scope."
},
{
"description": "Allow the Wechselbild app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-select-images`\n- `allow-set-setting`\n- `allow-next-wallpaper`",
"type": "string",
"const": "default",
"markdownDescription": "Allow the Wechselbild app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-select-images`\n- `allow-set-setting`\n- `allow-next-wallpaper`"
}
]
}
}
}
+10
View File
@@ -0,0 +1,10 @@
use tauri::{AppHandle, command, Runtime};
use crate::models::*;
use crate::Result;
use crate::WallpaperExt;
#[command] pub(crate) async fn get_state<R: Runtime>(app: AppHandle<R>) -> Result<WallpaperState> { app.wallpaper().get_state() }
#[command] pub(crate) async fn select_images<R: Runtime>(app: AppHandle<R>) -> Result<WallpaperState> { app.wallpaper().select_images() }
#[command] pub(crate) async fn set_setting<R: Runtime>(app: AppHandle<R>, name: String, value: bool) -> Result<WallpaperState> { app.wallpaper().set_setting(SettingRequest { name, value }) }
#[command] pub(crate) async fn next_wallpaper<R: Runtime>(app: AppHandle<R>) -> Result<WallpaperState> { app.wallpaper().next_wallpaper() }
+22
View File
@@ -0,0 +1,22 @@
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::models::*;
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Wallpaper<R>> {
Ok(Wallpaper(app.clone()))
}
/// Access to the wallpaper APIs.
pub struct Wallpaper<R: Runtime>(AppHandle<R>);
impl<R: Runtime> Wallpaper<R> {
fn demo() -> WallpaperState { WallpaperState { image_count: 3, enabled: true, shuffle: true, lock_screen_only: true, current_index: 0, image_urls: vec!["/wallpapers/alpine.png".into(), "/wallpapers/waterfall.png".into(), "/wallpapers/coast.png".into()] } }
pub fn get_state(&self) -> crate::Result<WallpaperState> { Ok(Self::demo()) }
pub fn select_images(&self) -> crate::Result<WallpaperState> { Ok(Self::demo()) }
pub fn set_setting(&self, payload: SettingRequest) -> crate::Result<WallpaperState> { let mut state = Self::demo(); match payload.name.as_str() { "enabled" => state.enabled = payload.value, "shuffle" => state.shuffle = payload.value, "lockScreenOnly" => state.lock_screen_only = payload.value, _ => {} }; Ok(state) }
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> { let mut state = Self::demo(); state.current_index = 1; Ok(state) }
}
+21
View File
@@ -0,0 +1,21 @@
use serde::{ser::Serializer, Serialize};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
+48
View File
@@ -0,0 +1,48 @@
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
};
pub use models::*;
#[cfg(desktop)]
mod desktop;
#[cfg(mobile)]
mod mobile;
mod commands;
mod error;
mod models;
pub use error::{Error, Result};
#[cfg(desktop)]
use desktop::Wallpaper;
#[cfg(mobile)]
use mobile::Wallpaper;
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the wallpaper APIs.
pub trait WallpaperExt<R: Runtime> {
fn wallpaper(&self) -> &Wallpaper<R>;
}
impl<R: Runtime, T: Manager<R>> crate::WallpaperExt<R> for T {
fn wallpaper(&self) -> &Wallpaper<R> {
self.state::<Wallpaper<R>>().inner()
}
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("wallpaper")
.invoke_handler(tauri::generate_handler![commands::get_state, commands::select_images, commands::set_setting, commands::next_wallpaper])
.setup(|app, api| {
#[cfg(mobile)]
let wallpaper = mobile::init(app, api)?;
#[cfg(desktop)]
let wallpaper = desktop::init(app, api)?;
app.manage(wallpaper);
Ok(())
})
.build()
}
+32
View File
@@ -0,0 +1,32 @@
use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use crate::models::*;
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_wallpaper);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Wallpaper<R>> {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin("de.wechselbild.wallpaper", "WallpaperPlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_wallpaper)?;
Ok(Wallpaper(handle))
}
/// Access to the wallpaper APIs.
pub struct Wallpaper<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Wallpaper<R> {
pub fn get_state(&self) -> crate::Result<WallpaperState> { self.0.run_mobile_plugin("getState", ()).map_err(Into::into) }
pub fn select_images(&self) -> crate::Result<WallpaperState> { self.0.run_mobile_plugin("selectImages", ()).map_err(Into::into) }
pub fn set_setting(&self, payload: SettingRequest) -> crate::Result<WallpaperState> { self.0.run_mobile_plugin("setSetting", payload).map_err(Into::into) }
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> { self.0.run_mobile_plugin("nextWallpaper", ()).map_err(Into::into) }
}
+16
View File
@@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WallpaperState {
pub image_count: usize,
pub enabled: bool,
pub shuffle: bool,
pub lock_screen_only: bool,
pub current_index: usize,
pub image_urls: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingRequest { pub name: String, pub value: bool }