feat(wallpaper): extend commands with gallery, delete image, and crop

The plugin now exposes gallery retrieval, image deletion and image crop
across Android, desktop and mobile. The Rust core and Android
bridge are wired to handle these commands, updating state as needed.

- Add gallery, delete_image, and set_image_crop commands
- across Android, desktop and mobile.
- Update permissions, defaults, and schema to enable new commands.
This commit is contained in:
2026-08-20 22:06:00 +02:00
parent 734c757c67
commit 2cfc7c5dcb
22 changed files with 649 additions and 89 deletions
@@ -17,12 +17,25 @@ import java.util.concurrent.Executors
@InvokeArg
class SettingArgs { lateinit var name: String; var value: Boolean = false }
@InvokeArg
class GalleryArgs { var offset: Int = 0; var limit: Int = 48 }
@InvokeArg
class DeleteImageArgs { lateinit var id: String }
@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 getGallery(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(GalleryArgs::class.java)
invoke.resolve(WallpaperStore.gallery(activity, args.offset, args.limit))
} catch (error: Exception) { invoke.reject(error.message ?: "Galerie konnte nicht geladen werden") }
}
@Command fun selectImages(invoke: Invoke) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@@ -33,6 +46,14 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
startActivityForResult(invoke, intent, "selectedImages")
}
@Command fun deleteImage(invoke: Invoke) = io.execute {
try {
val args = invoke.parseArgs(DeleteImageArgs::class.java)
if (!WallpaperStore.delete(activity, args.id)) throw IllegalArgumentException("Bild wurde nicht gefunden")
invoke.resolve(WallpaperStore.state(activity))
} catch (error: Exception) { invoke.reject(error.message ?: "Bild konnte nicht gelöscht werden") }
}
@ActivityCallback
fun selectedImages(invoke: Invoke, result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK) { invoke.reject("Bildauswahl abgebrochen"); return }
@@ -44,6 +44,45 @@ object WallpaperStore {
}
}
fun gallery(context: Context, offset: Int, limit: Int): JSObject {
val originals = files(context)
val selectedIndex = prefs(context).getInt(KEY_INDEX, 0).coerceIn(0, (originals.size - 1).coerceAtLeast(0))
val safeOffset = offset.coerceAtLeast(0).coerceAtMost(originals.size)
val safeLimit = limit.coerceIn(1, 100)
val items = JSArray()
originals.drop(safeOffset).take(safeLimit).forEachIndexed { pageIndex, file ->
items.put(JSObject().apply {
put("id", file.name)
put("url", thumbnailDataUrl(file))
put("selected", safeOffset + pageIndex == selectedIndex)
})
}
return JSObject().apply {
put("total", originals.size)
put("items", items)
}
}
@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
val preferences = prefs(context)
val previousIndex = preferences.getInt(KEY_INDEX, 0).coerceIn(0, (originals.size - 1).coerceAtLeast(0))
val remaining = originals.size - 1
val nextIndex = when {
remaining <= 0 -> 0
position < previousIndex -> previousIndex - 1
previousIndex >= remaining -> remaining - 1
else -> previousIndex
}
preferences.edit().putInt(KEY_INDEX, nextIndex).apply()
return true
}
private fun thumbnailDataUrl(file: File): String {
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(file.absolutePath, bounds)
+9 -1
View File
@@ -1,4 +1,12 @@
const COMMANDS: &[&str] = &["get_state", "select_images", "set_setting", "next_wallpaper"];
const COMMANDS: &[&str] = &[
"get_state",
"get_gallery",
"select_images",
"delete_image",
"set_image_crop",
"set_setting",
"next_wallpaper",
];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-delete-image"
description = "Enables the delete_image command without any pre-configured scope."
commands.allow = ["delete_image"]
[[permission]]
identifier = "deny-delete-image"
description = "Denies the delete_image command without any pre-configured scope."
commands.deny = ["delete_image"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-gallery"
description = "Enables the get_gallery command without any pre-configured scope."
commands.allow = ["get_gallery"]
[[permission]]
identifier = "deny-get-gallery"
description = "Denies the get_gallery command without any pre-configured scope."
commands.deny = ["get_gallery"]
@@ -5,7 +5,9 @@ Allow the LockScreenWallpaper app to manage its wallpaper collection
#### This default permission set includes the following:
- `allow-get-state`
- `allow-get-gallery`
- `allow-select-images`
- `allow-delete-image`
- `allow-set-setting`
- `allow-next-wallpaper`
@@ -18,6 +20,58 @@ Allow the LockScreenWallpaper app to manage its wallpaper collection
</tr>
<tr>
<td>
`wallpaper:allow-delete-image`
</td>
<td>
Enables the delete_image command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-delete-image`
</td>
<td>
Denies the delete_image command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-get-gallery`
</td>
<td>
Enables the get_gallery command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-get-gallery`
</td>
<td>
Denies the get_gallery command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
+1 -1
View File
@@ -1,3 +1,3 @@
[default]
description = "Allow the LockScreenWallpaper app to manage its wallpaper collection"
permissions = ["allow-get-state", "allow-select-images", "allow-set-setting", "allow-next-wallpaper"]
permissions = ["allow-get-state", "allow-get-gallery", "allow-select-images", "allow-delete-image", "allow-set-image-crop", "allow-set-setting", "allow-next-wallpaper"]
+26 -2
View File
@@ -294,6 +294,30 @@
"PermissionKind": {
"type": "string",
"oneOf": [
{
"description": "Enables the delete_image command without any pre-configured scope.",
"type": "string",
"const": "allow-delete-image",
"markdownDescription": "Enables the delete_image command without any pre-configured scope."
},
{
"description": "Denies the delete_image command without any pre-configured scope.",
"type": "string",
"const": "deny-delete-image",
"markdownDescription": "Denies the delete_image command without any pre-configured scope."
},
{
"description": "Enables the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "allow-get-gallery",
"markdownDescription": "Enables the get_gallery command without any pre-configured scope."
},
{
"description": "Denies the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "deny-get-gallery",
"markdownDescription": "Denies the get_gallery command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
"type": "string",
@@ -343,10 +367,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-select-images`\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-set-setting`\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-select-images`\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-set-setting`\n- `allow-next-wallpaper`"
}
]
}
+54 -5
View File
@@ -1,10 +1,59 @@
use tauri::{AppHandle, command, Runtime};
use tauri::{command, AppHandle, 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() }
#[command]
pub(crate) async fn get_state<R: Runtime>(app: AppHandle<R>) -> Result<WallpaperState> {
app.wallpaper().get_state()
}
#[command]
pub(crate) async fn get_gallery<R: Runtime>(
app: AppHandle<R>,
offset: usize,
limit: usize,
) -> Result<GalleryPage> {
app.wallpaper()
.get_gallery(GalleryRequest { offset, limit })
}
#[command]
pub(crate) async fn select_images<R: Runtime>(app: AppHandle<R>) -> Result<WallpaperState> {
app.wallpaper().select_images()
}
#[command]
pub(crate) async fn delete_image<R: Runtime>(
app: AppHandle<R>,
id: String,
) -> Result<WallpaperState> {
app.wallpaper().delete_image(DeleteImageRequest { id })
}
#[command]
pub(crate) async fn set_image_crop<R: Runtime>(
app: AppHandle<R>,
id: String,
mode: String,
zoom: f64,
position_x: f64,
position_y: f64,
) -> Result<GalleryImage> {
app.wallpaper().set_image_crop(ImageCropRequest {
id,
mode,
zoom,
position_x,
position_y,
})
}
#[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()
}
+73 -5
View File
@@ -14,9 +14,77 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
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) }
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 get_gallery(&self, payload: GalleryRequest) -> crate::Result<GalleryPage> {
let urls = Self::demo().image_urls;
let items = urls
.into_iter()
.enumerate()
.skip(payload.offset)
.take(payload.limit)
.map(|(index, url)| GalleryImage {
id: format!("demo-{index}"),
url,
selected: index == 0,
crop_mode: "cover".into(),
crop_zoom: 1.0,
crop_position_x: 0.5,
crop_position_y: 0.5,
})
.collect();
Ok(GalleryPage { total: 3, items })
}
pub fn select_images(&self) -> crate::Result<WallpaperState> {
Ok(Self::demo())
}
pub fn delete_image(&self, _payload: DeleteImageRequest) -> crate::Result<WallpaperState> {
Ok(Self::demo())
}
pub fn set_image_crop(&self, payload: ImageCropRequest) -> crate::Result<GalleryImage> {
let url = Self::demo()
.image_urls
.into_iter()
.nth(payload.id.trim_start_matches("demo-").parse().unwrap_or(0))
.unwrap_or_default();
Ok(GalleryImage {
id: payload.id,
url,
selected: false,
crop_mode: payload.mode,
crop_zoom: payload.zoom,
crop_position_x: payload.position_x,
crop_position_y: payload.position_y,
})
}
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)
}
}
+9 -1
View File
@@ -35,7 +35,15 @@ impl<R: Runtime, T: Manager<R>> crate::WallpaperExt<R> for T {
/// 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])
.invoke_handler(tauri::generate_handler![
commands::get_state,
commands::get_gallery,
commands::select_images,
commands::delete_image,
commands::set_image_crop,
commands::set_setting,
commands::next_wallpaper
])
.setup(|app, api| {
#[cfg(mobile)]
let wallpaper = mobile::init(app, api)?;
+33 -4
View File
@@ -25,8 +25,37 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
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) }
pub fn get_state(&self) -> crate::Result<WallpaperState> {
self.0.run_mobile_plugin("getState", ()).map_err(Into::into)
}
pub fn get_gallery(&self, payload: GalleryRequest) -> crate::Result<GalleryPage> {
self.0
.run_mobile_plugin("getGallery", payload)
.map_err(Into::into)
}
pub fn select_images(&self) -> crate::Result<WallpaperState> {
self.0
.run_mobile_plugin("selectImages", ())
.map_err(Into::into)
}
pub fn delete_image(&self, payload: DeleteImageRequest) -> crate::Result<WallpaperState> {
self.0
.run_mobile_plugin("deleteImage", payload)
.map_err(Into::into)
}
pub fn set_image_crop(&self, payload: ImageCropRequest) -> crate::Result<GalleryImage> {
self.0
.run_mobile_plugin("setImageCrop", payload)
.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)
}
}
+46 -1
View File
@@ -13,4 +13,49 @@ pub struct WallpaperState {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingRequest { pub name: String, pub value: bool }
pub struct SettingRequest {
pub name: String,
pub value: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GalleryRequest {
pub offset: usize,
pub limit: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteImageRequest {
pub id: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageCropRequest {
pub id: String,
pub mode: String,
pub zoom: f64,
pub position_x: f64,
pub position_y: f64,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GalleryImage {
pub id: String,
pub url: String,
pub selected: bool,
pub crop_mode: String,
pub crop_zoom: f64,
pub crop_position_x: f64,
pub crop_position_y: f64,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GalleryPage {
pub total: usize,
pub items: Vec<GalleryImage>,
}
File diff suppressed because one or more lines are too long
+26 -2
View File
@@ -2193,10 +2193,22 @@
"markdownDescription": "Denies the unminimize 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`",
"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-setting`\n- `allow-next-wallpaper`",
"type": "string",
"const": "wallpaper: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`"
"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-setting`\n- `allow-next-wallpaper`"
},
{
"description": "Enables the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-delete-image",
"markdownDescription": "Enables the delete_image command without any pre-configured scope."
},
{
"description": "Enables the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-get-gallery",
"markdownDescription": "Enables the get_gallery command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
@@ -2222,6 +2234,18 @@
"const": "wallpaper:allow-set-setting",
"markdownDescription": "Enables the set_setting command without any pre-configured scope."
},
{
"description": "Denies the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-delete-image",
"markdownDescription": "Denies the delete_image command without any pre-configured scope."
},
{
"description": "Denies the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-get-gallery",
"markdownDescription": "Denies the get_gallery command without any pre-configured scope."
},
{
"description": "Denies the get_state command without any pre-configured scope.",
"type": "string",
+26 -2
View File
@@ -2193,10 +2193,22 @@
"markdownDescription": "Denies the unminimize 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-select-images`\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-set-setting`\n- `allow-next-wallpaper`",
"type": "string",
"const": "wallpaper:default",
"markdownDescription": "Allow the LockScreenWallpaper 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`"
"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-setting`\n- `allow-next-wallpaper`"
},
{
"description": "Enables the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-delete-image",
"markdownDescription": "Enables the delete_image command without any pre-configured scope."
},
{
"description": "Enables the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-get-gallery",
"markdownDescription": "Enables the get_gallery command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
@@ -2222,6 +2234,18 @@
"const": "wallpaper:allow-set-setting",
"markdownDescription": "Enables the set_setting command without any pre-configured scope."
},
{
"description": "Denies the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-delete-image",
"markdownDescription": "Denies the delete_image command without any pre-configured scope."
},
{
"description": "Denies the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-get-gallery",
"markdownDescription": "Denies the get_gallery command without any pre-configured scope."
},
{
"description": "Denies the get_state command without any pre-configured scope.",
"type": "string",
+26 -2
View File
@@ -2193,10 +2193,22 @@
"markdownDescription": "Denies the unminimize 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`",
"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-setting`\n- `allow-next-wallpaper`",
"type": "string",
"const": "wallpaper: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`"
"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-setting`\n- `allow-next-wallpaper`"
},
{
"description": "Enables the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-delete-image",
"markdownDescription": "Enables the delete_image command without any pre-configured scope."
},
{
"description": "Enables the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-get-gallery",
"markdownDescription": "Enables the get_gallery command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
@@ -2222,6 +2234,18 @@
"const": "wallpaper:allow-set-setting",
"markdownDescription": "Enables the set_setting command without any pre-configured scope."
},
{
"description": "Denies the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-delete-image",
"markdownDescription": "Denies the delete_image command without any pre-configured scope."
},
{
"description": "Denies the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-get-gallery",
"markdownDescription": "Denies the get_gallery command without any pre-configured scope."
},
{
"description": "Denies the get_state command without any pre-configured scope.",
"type": "string",
+26 -2
View File
@@ -2193,10 +2193,22 @@
"markdownDescription": "Denies the unminimize 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`",
"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-setting`\n- `allow-next-wallpaper`",
"type": "string",
"const": "wallpaper: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`"
"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-setting`\n- `allow-next-wallpaper`"
},
{
"description": "Enables the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-delete-image",
"markdownDescription": "Enables the delete_image command without any pre-configured scope."
},
{
"description": "Enables the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:allow-get-gallery",
"markdownDescription": "Enables the get_gallery command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
@@ -2222,6 +2234,18 @@
"const": "wallpaper:allow-set-setting",
"markdownDescription": "Enables the set_setting command without any pre-configured scope."
},
{
"description": "Denies the delete_image command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-delete-image",
"markdownDescription": "Denies the delete_image command without any pre-configured scope."
},
{
"description": "Denies the get_gallery command without any pre-configured scope.",
"type": "string",
"const": "wallpaper:deny-get-gallery",
"markdownDescription": "Denies the get_gallery command without any pre-configured scope."
},
{
"description": "Denies the get_state command without any pre-configured scope.",
"type": "string",
+47 -11
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { Check, ChevronRight, Home, Images, LockKeyhole, Settings, Shuffle, Smartphone, Sparkles } from "lucide-react";
import { getState, nextWallpaper, selectImages, setSetting, type WallpaperState } from "./native";
import { ArrowLeft, Check, ChevronRight, Home, Images, LockKeyhole, Plus, Settings, Shuffle, Smartphone, Sparkles, Trash2 } from "lucide-react";
import { deleteImage, getGallery, getState, nextWallpaper, selectImages, setSetting, type GalleryImage, type WallpaperState } from "./native";
const initial: WallpaperState = { imageCount: 0, enabled: false, shuffle: true, lockScreenOnly: true, currentIndex: 0, imageUrls: [] };
@@ -14,13 +14,27 @@ function SettingRow({ icon, label, value, onChange }: { icon: React.ReactNode; l
export default function App() {
const [state, setState] = useState(initial);
const [tab, setTab] = useState<"home" | "settings">("home");
const [tab, setTab] = useState<"home" | "settings" | "gallery">("home");
const [busy, setBusy] = useState(false);
const [notice, setNotice] = useState("");
const [gallery, setGallery] = useState<GalleryImage[]>([]);
const [galleryTotal, setGalleryTotal] = useState(0);
const [galleryLoading, setGalleryLoading] = useState(false);
const [deletingId, setDeletingId] = useState("");
useEffect(() => { getState().then(setState).catch(() => setState(initial)); }, []);
const current = state.imageUrls[state.currentIndex] ?? "/wallpapers/alpine.png";
const photos = useMemo(() => state.imageUrls.length ? state.imageUrls : ["/wallpapers/alpine.png", "/wallpapers/waterfall.png", "/wallpapers/coast.png"], [state.imageUrls]);
const photos = useMemo(() => state.imageUrls, [state.imageUrls]);
async function loadGallery(offset = 0, append = false) {
setGalleryLoading(true);
try {
const page = await getGallery(offset, 48);
setGallery(previous => append ? [...previous, ...page.items] : page.items);
setGalleryTotal(page.total);
} catch { setNotice("Galerie konnte nicht geladen werden"); }
finally { setGalleryLoading(false); }
}
async function update(name: "enabled" | "shuffle" | "lockScreenOnly", value: boolean) {
setState(prev => ({ ...prev, [name]: value }));
@@ -29,10 +43,26 @@ export default function App() {
async function choose() {
setBusy(true);
try { setState(await selectImages()); setNotice("Bilder wurden zur Sammlung hinzugefügt"); } catch (error) { setNotice(String(error).includes("cancel") ? "Auswahl abgebrochen" : "Bildauswahl ist auf Android verfügbar"); }
try {
setState(await selectImages());
if (tab === "gallery") await loadGallery();
setNotice("Bilder wurden zur Sammlung hinzugefügt");
} catch (error) { setNotice(String(error).includes("cancel") ? "Auswahl abgebrochen" : "Bildauswahl ist auf Android verfügbar"); }
finally { setBusy(false); }
}
async function remove(image: GalleryImage) {
if (!window.confirm("Möchtest du dieses Bild wirklich aus deiner Sammlung löschen?")) return;
setDeletingId(image.id);
try {
setState(await deleteImage(image.id));
setGallery(previous => previous.filter(item => item.id !== image.id));
setGalleryTotal(previous => Math.max(0, previous - 1));
setNotice("Bild wurde gelöscht");
} catch { setNotice("Bild konnte nicht gelöscht werden"); }
finally { setDeletingId(""); }
}
async function next() {
setBusy(true);
try { setState(await nextWallpaper()); setNotice("Sperrbildschirm wurde aktualisiert"); } catch { setState(prev => ({ ...prev, currentIndex: (prev.currentIndex + 1) % Math.max(1, photos.length) })); }
@@ -40,7 +70,8 @@ export default function App() {
}
return <main className="app-shell">
<header><div><h1>LockScreenWallpaper</h1><p>{state.enabled ? "Deine Motive wechseln automatisch" : "Automatischer Wechsel ist pausiert"}</p></div><button className="icon-button" aria-label="Einstellungen" onClick={() => setTab("settings")}><Settings /></button></header>
{tab === "gallery" ? <header className="gallery-header"><button className="icon-button" aria-label="Zurück" onClick={() => setTab("home")}><ArrowLeft /></button><div><h1>Meine Sammlung</h1><p>{galleryTotal} {galleryTotal === 1 ? "Bild" : "Bilder"}</p></div><button className="icon-button" aria-label="Bilder hinzufügen" onClick={choose} disabled={busy}><Plus /></button></header> :
<header><div><h1>LockScreenWallpaper</h1><p>{state.enabled ? "Deine Motive wechseln automatisch" : "Automatischer Wechsel ist pausiert"}</p></div><button className="icon-button" aria-label="Einstellungen" onClick={() => setTab("settings")}><Settings /></button></header>}
<div className="content">
{tab === "home" ? <>
@@ -55,16 +86,21 @@ export default function App() {
<button className="primary" onClick={choose} disabled={busy}><Images />{busy ? "Bitte warten …" : "Bilder auswählen"}</button>
<section className="collection"><div className="section-heading"><h2>Meine Sammlung</h2><button>{state.imageCount || photos.length} Bilder <ChevronRight /></button></div>
<div className="photo-rail">{photos.map((photo, index) => <button key={`${photo}-${index}`} className={index === state.currentIndex ? "selected" : ""} onClick={() => setState(prev => ({ ...prev, currentIndex: index }))}><img src={photo} alt={`Motiv ${index + 1}`} />{index === state.currentIndex && <span><Check /></span>}</button>)}</div>
<p className="hint">Tippe auf ein Bild, um es als Vorschau zu sehen.</p>
<section className="collection"><div className="section-heading"><h2>Meine Sammlung</h2><button onClick={() => { setTab("gallery"); void loadGallery(); }}>{state.imageCount} {state.imageCount === 1 ? "Bild" : "Bilder"} <ChevronRight /></button></div>
{photos.length ? <div className="photo-rail">{photos.map((photo, index) => <button key={`${photo}-${index}`} className={index === state.currentIndex ? "selected" : ""} onClick={() => setState(prev => ({ ...prev, currentIndex: index }))}><img src={photo} alt={`Motiv ${index + 1}`} />{index === state.currentIndex && <span><Check /></span>}</button>)}</div> : <button className="empty-collection" onClick={choose}><Images /><span>Noch keine Bilder ausgewählt</span></button>}
<p className="hint">Tippe auf die Bildanzahl, um deine Galerie zu öffnen.</p>
</section>
<section className="settings-list"><SettingRow icon={<Shuffle />} label="Zufällige Reihenfolge" value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label="Nur Sperrbildschirm" value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></section>
</> : <section className="settings-page"><h2>Einstellungen</h2><p>Lege fest, wie LockScreenWallpaper im Hintergrund arbeitet.</p><div className="settings-list"><SettingRow icon={<Smartphone />} label="Bei jedem Aktivieren wechseln" value={state.enabled} onChange={v => update("enabled", v)} /><SettingRow icon={<Shuffle />} label="Zufällige Reihenfolge" value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label="Nur Sperrbildschirm" value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div><div className="info"><h3>Ohne Bilderlimit</h3><p>Deine Auswahl wird privat auf dem Gerät gespeichert. Die einzige Grenze ist der freie Speicherplatz.</p></div></section>}
</> : tab === "settings" ? <section className="settings-page"><h2>Einstellungen</h2><p>Lege fest, wie LockScreenWallpaper im Hintergrund arbeitet.</p><div className="settings-list"><SettingRow icon={<Smartphone />} label="Bei jedem Aktivieren wechseln" value={state.enabled} onChange={v => update("enabled", v)} /><SettingRow icon={<Shuffle />} label="Zufällige Reihenfolge" value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label="Nur Sperrbildschirm" value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div><div className="info"><h3>Ohne Bilderlimit</h3><p>Deine Auswahl wird privat auf dem Gerät gespeichert. Die einzige Grenze ist der freie Speicherplatz.</p></div></section> :
<section className="gallery-page" aria-label="Meine Bilder">
{gallery.length ? <div className="gallery-grid">{gallery.map((image, index) => <article className={image.selected ? "current" : ""} key={image.id}><img src={image.url} alt={`Bild ${index + 1}`} />{image.selected && <span className="current-badge"><Check /> Aktuell</span>}<button className="delete-button" aria-label={`Bild ${index + 1} löschen`} onClick={() => remove(image)} disabled={deletingId === image.id}><Trash2 /></button></article>)}</div> : !galleryLoading && <div className="gallery-empty"><Images /><h2>Deine Sammlung ist leer</h2><p>Füge Bilder hinzu, die automatisch als Sperrbildschirm wechseln sollen.</p><button className="primary" onClick={choose}><Plus /> Bilder hinzufügen</button></div>}
{galleryLoading && <p className="gallery-status">Galerie wird geladen </p>}
{!galleryLoading && gallery.length < galleryTotal && <button className="load-more" onClick={() => loadGallery(gallery.length, true)}>Weitere Bilder laden</button>}
</section>}
</div>
{notice && <button className="snackbar" onClick={() => setNotice("")}>{notice}</button>}
<nav><button className={tab === "home" ? "active" : ""} onClick={() => setTab("home")}><Home /><span>Start</span></button><button className={tab === "settings" ? "active" : ""} onClick={() => setTab("settings")}><Settings /><span>Einstellungen</span></button></nav>
{tab !== "gallery" && <nav><button className={tab === "home" ? "active" : ""} onClick={() => setTab("home")}><Home /><span>Start</span></button><button className={tab === "settings" ? "active" : ""} onClick={() => setTab("settings")}><Settings /><span>Einstellungen</span></button></nav>}
</main>;
}
+34
View File
@@ -9,6 +9,17 @@ export type WallpaperState = {
imageUrls: string[];
};
export type GalleryImage = {
id: string;
url: string;
selected: boolean;
};
export type GalleryPage = {
total: number;
items: GalleryImage[];
};
const demoState: WallpaperState = {
imageCount: 3,
enabled: true,
@@ -24,11 +35,34 @@ export async function getState(): Promise<WallpaperState> {
return inTauri() ? invoke<WallpaperState>("plugin:wallpaper|get_state") : demoState;
}
export async function getGallery(offset = 0, limit = 48): Promise<GalleryPage> {
if (inTauri()) return invoke<GalleryPage>("plugin:wallpaper|get_gallery", { offset, limit });
return {
total: demoState.imageUrls.length,
items: demoState.imageUrls.slice(offset, offset + limit).map((url, index) => ({
id: `demo-${offset + index}`,
url,
selected: offset + index === demoState.currentIndex,
})),
};
}
export async function selectImages(): Promise<WallpaperState> {
if (!inTauri()) return demoState;
return invoke<WallpaperState>("plugin:wallpaper|select_images");
}
export async function deleteImage(id: string): Promise<WallpaperState> {
if (inTauri()) return invoke<WallpaperState>("plugin:wallpaper|delete_image", { id });
const index = Number(id.replace("demo-", ""));
if (Number.isInteger(index) && index >= 0 && index < demoState.imageUrls.length) {
demoState.imageUrls.splice(index, 1);
demoState.imageCount = demoState.imageUrls.length;
demoState.currentIndex = Math.min(demoState.currentIndex, Math.max(0, demoState.imageCount - 1));
}
return { ...demoState, imageUrls: [...demoState.imageUrls] };
}
export async function setSetting(name: "enabled" | "shuffle" | "lockScreenOnly", value: boolean) {
if (!inTauri()) return { ...demoState, [name]: value };
return invoke<WallpaperState>("plugin:wallpaper|set_setting", { name, value });
+23
View File
@@ -8,6 +8,10 @@ h1 { font-size: 34px; letter-spacing: -1.7px; line-height: 1.05; margin: 0 0 6px
header p { margin: 0; font-size: 13px; color: #657068; font-weight: 500; }
.icon-button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: #edf2eb; color: #31523b; display: grid; place-items: center; }
.icon-button svg { width: 21px; }
.icon-button:disabled { opacity: .55; }
.gallery-header { gap: 14px; }
.gallery-header > div { flex: 1; }
.gallery-header h1 { font-size: 25px; letter-spacing: -.9px; margin-bottom: 3px; }
.content { padding: 0 20px; }
.hero { position: relative; height: 294px; margin: 0 0 15px; border-radius: 26px; isolation: isolate; }
.hero img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; z-index: 1; }
@@ -38,6 +42,8 @@ h2 { margin: 0; font-size: 21px; letter-spacing: -.6px; }
.photo-rail img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-rail span { position: absolute; left: 7px; top: 7px; width: 23px; height: 23px; background: var(--green); color: white; border-radius: 50%; display: grid; place-items: center; }
.photo-rail span svg { width: 14px; }
.empty-collection { width: 100%; min-height: 84px; border: 1px dashed #b8c6ba; border-radius: 15px; background: #f1f5ef; color: #667069; display: flex; align-items: center; justify-content: center; gap: 9px; font-size: 13px; font-weight: 650; }
.empty-collection svg { width: 20px; color: var(--green); }
.hint { margin: 8px 0 12px; text-align: center; font-size: 11px; color: #7a837d; }
.settings-list { border-top: 1px solid var(--line); }
.settings-page > p { color: #667069; margin: 8px 0 24px; font-size: 14px; }
@@ -50,6 +56,23 @@ nav button { border: 0; color: #657068; background: transparent; border-radius:
nav button.active { color: var(--green); background: var(--sage); }
nav svg { width: 21px; }
.snackbar { position: fixed; z-index: 20; left: 50%; transform: translateX(-50%); bottom: 92px; max-width: calc(100% - 40px); background: #26312a; color: white; border: 0; border-radius: 12px; padding: 13px 18px; font-size: 12px; box-shadow: 0 8px 26px rgba(0,0,0,.22); }
.gallery-page { padding-bottom: max(24px, env(safe-area-inset-bottom)); }
.gallery-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; }
.gallery-grid article { position: relative; aspect-ratio: 3 / 4; overflow: hidden; border-radius: 14px; background: #e3e9e2; border: 2px solid transparent; }
.gallery-grid article.current { border-color: #73a17c; }
.gallery-grid article > img { width: 100%; height: 100%; object-fit: cover; display: block; }
.delete-button { position: absolute; top: 7px; right: 7px; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center; color: white; background: rgba(94, 20, 20, .86); box-shadow: 0 3px 12px rgba(0,0,0,.24); }
.delete-button svg { width: 16px; }
.delete-button:disabled { opacity: .5; }
.current-badge { position: absolute; left: 6px; bottom: 6px; display: flex; align-items: center; gap: 3px; padding: 5px 7px; border-radius: 99px; background: rgba(20, 93, 50, .9); color: white; font-size: 9px; font-weight: 750; }
.current-badge svg { width: 11px; height: 11px; }
.gallery-empty { min-height: 58vh; padding: 40px 18px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; color: #68736b; }
.gallery-empty > svg { width: 48px; height: 48px; padding: 11px; border-radius: 50%; color: var(--green); background: var(--sage); }
.gallery-empty h2 { margin-top: 16px; color: #26312a; }
.gallery-empty p { max-width: 290px; margin: 8px 0 2px; font-size: 13px; line-height: 1.5; }
.gallery-empty .primary { max-width: 260px; }
.gallery-status { padding: 28px 0; text-align: center; color: #6d776f; font-size: 13px; }
.load-more { width: 100%; height: 48px; margin-top: 18px; border: 1px solid #b9c8bb; border-radius: 15px; color: var(--green); background: white; font-weight: 750; }
@media (min-width: 700px) { .app-shell { margin-top: 20px; min-height: calc(100vh - 40px); border-radius: 32px; overflow: hidden; } nav { bottom: 20px; border-radius: 0 0 32px 32px; } }
@media (max-height: 740px) { .hero { height: 235px; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; transition: none !important; } }