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:
@@ -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
|
||||
@@ -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"] }
|
||||
@@ -0,0 +1 @@
|
||||
# Tauri Plugin wallpaper
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -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"))
|
||||
}
|
||||
Vendored
+21
@@ -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
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"]
|
||||
@@ -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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user