feat(import-progress): track and expose Immich import progress

This change introduces a cross-language import progress API for Immich
imports and exposes a new query command to report live progress. A
new ImportProgress model captures active state, totals, and bytes
downloaded, allowing consuming code to display import status to users.

- Adds ImportProgress data type and progress tracking during import
- Exposes get_immich_import_progress command in Android and Rust
- Updates permissions and schemas to include allow-get-immich-import-progress
This commit is contained in:
2026-08-21 21:34:01 +02:00
parent 8e5a53ade8
commit 789b58398a
20 changed files with 218 additions and 21 deletions
+45 -3
View File
@@ -17,6 +17,7 @@ import java.nio.charset.StandardCharsets
import java.security.KeyStore
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
@@ -33,8 +34,16 @@ object ImmichClient {
private const val KEY_ALIAS = "wallpaperflow_immich_api_key"
private val assetIdPattern = Regex("^[0-9a-fA-F-]{36}$")
private val thumbnailPool = Executors.newFixedThreadPool(4)
private val importProgress = AtomicReference(ImportProgress())
private data class Credentials(val serverUrl: String, val apiKey: String)
private data class ImportProgress(
val active: Boolean = false,
val completed: Int = 0,
val total: Int = 0,
val bytesDownloaded: Long = 0,
val bytesTotal: Long = 0,
)
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
@@ -188,7 +197,21 @@ object ImmichClient {
val connection = openConnection(serverUrl, apiKey, path)
try {
if (connection.responseCode !in 200..299) throw IOException(errorMessage(connection))
connection.inputStream.use { input -> target.outputStream().use { output -> input.copyTo(output) } }
val total = connection.contentLengthLong.coerceAtLeast(0)
var downloaded = 0L
importProgress.updateAndGet { it.copy(bytesDownloaded = 0, bytesTotal = total) }
connection.inputStream.use { input ->
target.outputStream().use { output ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
val count = input.read(buffer)
if (count < 0) break
output.write(buffer, 0, count)
downloaded += count
importProgress.updateAndGet { it.copy(bytesDownloaded = downloaded, bytesTotal = total) }
}
}
}
return connection.contentType.orEmpty().substringBefore(';')
} catch (error: Exception) {
target.delete()
@@ -285,7 +308,26 @@ object ImmichClient {
require(ids.isNotEmpty()) { "Wähle mindestens ein Immich-Bild aus." }
require(ids.size <= 100) { "Bitte importiere höchstens 100 Bilder auf einmal." }
val credentials = credentials(context)
ids.distinct().forEach { importOne(context, credentials, it) }
return WallpaperStore.state(context)
val uniqueIds = ids.distinct()
importProgress.set(ImportProgress(active = true, total = uniqueIds.size))
try {
uniqueIds.forEachIndexed { index, id ->
importOne(context, credentials, id)
importProgress.updateAndGet { it.copy(completed = index + 1, bytesDownloaded = 0, bytesTotal = 0) }
}
return WallpaperStore.state(context)
} finally {
importProgress.updateAndGet { it.copy(active = false) }
}
}
fun importProgress(): JSObject = importProgress.get().let { progress ->
JSObject().apply {
put("active", progress.active)
put("completed", progress.completed)
put("total", progress.total)
put("bytesDownloaded", progress.bytesDownloaded)
put("bytesTotal", progress.bytesTotal)
}
}
}
@@ -179,4 +179,8 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
invoke.resolve(ImmichClient.importAssets(activity, args.ids.toList()))
} catch (error: Exception) { invoke.reject(error.message ?: "Immich-Bilder konnten nicht importiert werden") }
}
@Command fun getImmichImportProgress(invoke: Invoke) {
invoke.resolve(ImmichClient.importProgress())
}
}
+1
View File
@@ -15,6 +15,7 @@ const COMMANDS: &[&str] = &[
"get_immich_albums",
"get_immich_assets",
"import_immich_assets",
"get_immich_import_progress",
];
fn main() {
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-immich-import-progress"
description = "Enables the get_immich_import_progress command without any pre-configured scope."
commands.allow = ["get_immich_import_progress"]
[[permission]]
identifier = "deny-get-immich-import-progress"
description = "Denies the get_immich_import_progress command without any pre-configured scope."
commands.deny = ["get_immich_import_progress"]
@@ -20,6 +20,7 @@ Allow the LockScreenWallpaper app to manage its wallpaper collection
- `allow-get-immich-albums`
- `allow-get-immich-assets`
- `allow-import-immich-assets`
- `allow-get-immich-import-progress`
## Permission Table
@@ -267,6 +268,32 @@ Denies the get_immich_connection command without any pre-configured scope.
<tr>
<td>
`wallpaper:allow-get-immich-import-progress`
</td>
<td>
Enables the get_immich_import_progress command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:deny-get-immich-import-progress`
</td>
<td>
Denies the get_immich_import_progress command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`wallpaper:allow-get-state`
</td>
+1 -1
View File
@@ -1,3 +1,3 @@
[default]
description = "Allow the LockScreenWallpaper app to manage its wallpaper collection"
permissions = ["allow-get-state", "allow-get-gallery", "allow-select-images", "allow-delete-image", "allow-get-image-ids", "allow-delete-images", "allow-set-image-crop", "allow-set-setting", "allow-set-interval", "allow-next-wallpaper", "allow-get-immich-connection", "allow-connect-immich", "allow-disconnect-immich", "allow-get-immich-albums", "allow-get-immich-assets", "allow-import-immich-assets"]
permissions = ["allow-get-state", "allow-get-gallery", "allow-select-images", "allow-delete-image", "allow-get-image-ids", "allow-delete-images", "allow-set-image-crop", "allow-set-setting", "allow-set-interval", "allow-next-wallpaper", "allow-get-immich-connection", "allow-connect-immich", "allow-disconnect-immich", "allow-get-immich-albums", "allow-get-immich-assets", "allow-import-immich-assets", "allow-get-immich-import-progress"]
+14 -2
View File
@@ -402,6 +402,18 @@
"const": "deny-get-immich-connection",
"markdownDescription": "Denies the get_immich_connection command without any pre-configured scope."
},
{
"description": "Enables the get_immich_import_progress command without any pre-configured scope.",
"type": "string",
"const": "allow-get-immich-import-progress",
"markdownDescription": "Enables the get_immich_import_progress command without any pre-configured scope."
},
{
"description": "Denies the get_immich_import_progress command without any pre-configured scope.",
"type": "string",
"const": "deny-get-immich-import-progress",
"markdownDescription": "Denies the get_immich_import_progress command without any pre-configured scope."
},
{
"description": "Enables the get_state command without any pre-configured scope.",
"type": "string",
@@ -487,10 +499,10 @@
"markdownDescription": "Denies the set_setting command without any pre-configured scope."
},
{
"description": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-get-image-ids`\n- `allow-delete-images`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-set-interval`\n- `allow-next-wallpaper`\n- `allow-get-immich-connection`\n- `allow-connect-immich`\n- `allow-disconnect-immich`\n- `allow-get-immich-albums`\n- `allow-get-immich-assets`\n- `allow-import-immich-assets`",
"description": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-get-image-ids`\n- `allow-delete-images`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-set-interval`\n- `allow-next-wallpaper`\n- `allow-get-immich-connection`\n- `allow-connect-immich`\n- `allow-disconnect-immich`\n- `allow-get-immich-albums`\n- `allow-get-immich-assets`\n- `allow-import-immich-assets`\n- `allow-get-immich-import-progress`",
"type": "string",
"const": "default",
"markdownDescription": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-get-image-ids`\n- `allow-delete-images`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-set-interval`\n- `allow-next-wallpaper`\n- `allow-get-immich-connection`\n- `allow-connect-immich`\n- `allow-disconnect-immich`\n- `allow-get-immich-albums`\n- `allow-get-immich-assets`\n- `allow-import-immich-assets`"
"markdownDescription": "Allow the LockScreenWallpaper app to manage its wallpaper collection\n#### This default permission set includes:\n\n- `allow-get-state`\n- `allow-get-gallery`\n- `allow-select-images`\n- `allow-delete-image`\n- `allow-get-image-ids`\n- `allow-delete-images`\n- `allow-set-image-crop`\n- `allow-set-setting`\n- `allow-set-interval`\n- `allow-next-wallpaper`\n- `allow-get-immich-connection`\n- `allow-connect-immich`\n- `allow-disconnect-immich`\n- `allow-get-immich-albums`\n- `allow-get-immich-assets`\n- `allow-import-immich-assets`\n- `allow-get-immich-import-progress`"
}
]
}
+5
View File
@@ -111,3 +111,8 @@ pub(crate) async fn import_immich_assets<R: Runtime>(
) -> Result<WallpaperState> {
app.wallpaper().import_immich_assets(ImmichImportRequest { ids })
}
#[command]
pub(crate) async fn get_immich_import_progress<R: Runtime>(app: AppHandle<R>) -> Result<ImmichImportProgress> {
app.wallpaper().get_immich_import_progress()
}
+3
View File
@@ -143,4 +143,7 @@ impl<R: Runtime> Wallpaper<R> {
pub fn import_immich_assets(&self, _payload: ImmichImportRequest) -> crate::Result<WallpaperState> {
Ok(Self::demo())
}
pub fn get_immich_import_progress(&self) -> crate::Result<ImmichImportProgress> {
Ok(ImmichImportProgress::default())
}
}
+2 -1
View File
@@ -51,7 +51,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::disconnect_immich,
commands::get_immich_albums,
commands::get_immich_assets,
commands::import_immich_assets
commands::import_immich_assets,
commands::get_immich_import_progress
])
.setup(|app, api| {
#[cfg(mobile)]
+3
View File
@@ -88,4 +88,7 @@ impl<R: Runtime> Wallpaper<R> {
pub fn import_immich_assets(&self, payload: ImmichImportRequest) -> crate::Result<WallpaperState> {
self.0.run_mobile_plugin("importImmichAssets", payload).map_err(Into::into)
}
pub fn get_immich_import_progress(&self) -> crate::Result<ImmichImportProgress> {
self.0.run_mobile_plugin("getImmichImportProgress", ()).map_err(Into::into)
}
}
+10
View File
@@ -139,3 +139,13 @@ pub struct ImmichAssetsPage {
pub struct ImmichImportRequest {
pub ids: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImmichImportProgress {
pub active: bool,
pub completed: usize,
pub total: usize,
pub bytes_downloaded: u64,
pub bytes_total: u64,
}