feat(immich): add prefetch and mobile data controls

Adds Immich image prefetch and a mobile data option to the UI.
Android now ships ImmichPrefetchWorker to fetch originals in the background.
Frontend and desktop code are updated to expose and persist new settings.
Translations cover the new labels and hints in multiple languages.

- Introduce ImmichPrefetchWorker for background prefetch
- Add allowMobileData and prefetchImmich UI controls
- Wire changes to desktop state and translations
This commit is contained in:
2026-08-22 23:18:08 +02:00
parent 75c79a8344
commit a5f71b576b
10 changed files with 140 additions and 10 deletions
@@ -11,5 +11,6 @@ class BootReceiver : BroadcastReceiver() {
WallpaperStore.screenOnEnabled(context) -> runCatching { ScreenOnRotationService.start(context) } WallpaperStore.screenOnEnabled(context) -> runCatching { ScreenOnRotationService.start(context) }
WallpaperStore.timedEnabled(context) -> WallpaperScheduler.scheduleNext(context) WallpaperStore.timedEnabled(context) -> WallpaperScheduler.scheduleNext(context)
} }
if (ImmichClient.configured(context) && WallpaperStore.prefetchImmich(context)) ImmichPrefetchWorker.enqueue(context)
} }
} }
@@ -119,6 +119,11 @@ object ImmichClient {
fun connection(context: Context) = connectionObject(context) fun connection(context: Context) = connectionObject(context)
fun configured(context: Context): Boolean {
val preferences = prefs(context)
return preferences.getString(KEY_SERVER_URL, "").orEmpty().isNotBlank() && !decryptApiKey(context).isNullOrBlank()
}
fun connect(context: Context, serverUrl: String, apiKey: String): JSObject { fun connect(context: Context, serverUrl: String, apiKey: String): JSObject {
val normalized = normalizeServerUrl(serverUrl) val normalized = normalizeServerUrl(serverUrl)
require(apiKey.isNotBlank()) { "Bitte gib deinen Immich API-Key ein." } require(apiKey.isNotBlank()) { "Bitte gib deinen Immich API-Key ein." }
@@ -314,10 +319,10 @@ object ImmichClient {
return bounds.outWidth > 0 && bounds.outHeight > 0 return bounds.outWidth > 0 && bounds.outHeight > 0
} }
fun automaticDownloadsAllowed(context: Context): Boolean { fun automaticDownloadsAllowed(context: Context, allowMetered: Boolean): Boolean {
val connectivity = context.getSystemService(ConnectivityManager::class.java) val connectivity = context.getSystemService(ConnectivityManager::class.java)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
return connectivity.activeNetworkInfo?.isConnected == true && !connectivity.isActiveNetworkMetered return connectivity.activeNetworkInfo?.isConnected == true && (allowMetered || !connectivity.isActiveNetworkMetered)
} }
@Synchronized @Synchronized
@@ -0,0 +1,37 @@
package de.wechselbild.wallpaper
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.util.concurrent.TimeUnit
class ImmichPrefetchWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
override fun doWork(): Result {
WallpaperStore.prefetchImmichOriginals(applicationContext)
return Result.success()
}
companion object {
private const val WORK_NAME = "immich-wifi-prefetch"
fun enqueue(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.build()
val request = PeriodicWorkRequestBuilder<ImmichPrefetchWorker>(6, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, request)
}
fun cancel(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
}
}
}
@@ -170,11 +170,14 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
@Command fun connectImmich(invoke: Invoke) = io.execute { @Command fun connectImmich(invoke: Invoke) = io.execute {
try { try {
val args = invoke.parseArgs(ImmichConnectArgs::class.java) val args = invoke.parseArgs(ImmichConnectArgs::class.java)
invoke.resolve(ImmichClient.connect(activity, args.serverUrl, args.apiKey)) val result = ImmichClient.connect(activity, args.serverUrl, args.apiKey)
if (WallpaperStore.prefetchImmich(activity)) ImmichPrefetchWorker.enqueue(activity)
invoke.resolve(result)
} catch (error: Exception) { invoke.reject(error.message ?: "Immich-Verbindung fehlgeschlagen") } } catch (error: Exception) { invoke.reject(error.message ?: "Immich-Verbindung fehlgeschlagen") }
} }
@Command fun disconnectImmich(invoke: Invoke) = io.execute { @Command fun disconnectImmich(invoke: Invoke) = io.execute {
ImmichPrefetchWorker.cancel(activity)
invoke.resolve(ImmichClient.disconnect(activity)) invoke.resolve(ImmichClient.disconnect(activity))
} }
@@ -102,10 +102,16 @@ object WallpaperStore {
fun screenOnEnabled(context: Context) = intervalMinutes(context) == -1 fun screenOnEnabled(context: Context) = intervalMinutes(context) == -1
fun shuffle(context: Context) = prefs(context).getBoolean("shuffle", true) fun shuffle(context: Context) = prefs(context).getBoolean("shuffle", true)
fun lockOnly(context: Context) = prefs(context).getBoolean("lockScreenOnly", true) fun lockOnly(context: Context) = prefs(context).getBoolean("lockScreenOnly", true)
fun allowMobileData(context: Context) = prefs(context).getBoolean("allowMobileData", false)
fun prefetchImmich(context: Context) = prefs(context).getBoolean("prefetchImmich", true)
fun set(context: Context, name: String, value: Boolean) { fun set(context: Context, name: String, value: Boolean) {
require(name in setOf("shuffle", "lockScreenOnly")) { "Unbekannte Einstellung" } require(name in setOf("shuffle", "lockScreenOnly", "allowMobileData", "prefetchImmich")) { "Unbekannte Einstellung" }
prefs(context).edit().putBoolean(name, value).apply() prefs(context).edit().putBoolean(name, value).apply()
if (name == "prefetchImmich") {
if (value && ImmichClient.configured(context)) ImmichPrefetchWorker.enqueue(context)
else if (!value) ImmichPrefetchWorker.cancel(context)
}
} }
fun setInterval(context: Context, minutes: Int) { fun setInterval(context: Context, minutes: Int) {
@@ -125,6 +131,8 @@ object WallpaperStore {
put("intervalMinutes", intervalMinutes(context)) put("intervalMinutes", intervalMinutes(context))
put("shuffle", shuffle(context)) put("shuffle", shuffle(context))
put("lockScreenOnly", lockOnly(context)) put("lockScreenOnly", lockOnly(context))
put("allowMobileData", allowMobileData(context))
put("prefetchImmich", prefetchImmich(context))
put("currentIndex", previewIndex) put("currentIndex", previewIndex)
put("currentId", items.getOrNull(index)?.id ?: JSONObject.NULL) put("currentId", items.getOrNull(index)?.id ?: JSONObject.NULL)
val ids = JSArray() val ids = JSArray()
@@ -152,6 +160,16 @@ object WallpaperStore {
@Synchronized @Synchronized
fun imageIds(context: Context) = entries(context).map { it.id } fun imageIds(context: Context) = entries(context).map { it.id }
fun prefetchImmichOriginals(context: Context) {
val unavailableServers = mutableSetOf<String>()
entries(context).filterIsInstance<Entry.Immich>().forEach { entry ->
if (entry.serverUrl in unavailableServers) return@forEach
if (ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, allowNetwork = true) == null) {
unavailableServers.add(entry.serverUrl)
}
}
}
private fun cropKey(id: String) = CROP_PREFIX + id private fun cropKey(id: String) = CROP_PREFIX + id
private fun crop(context: Context, entry: Entry): CropSettings { private fun crop(context: Context, entry: Entry): CropSettings {
val raw = prefs(context).getString(cropKey(entry.id), null) ?: return CropSettings() val raw = prefs(context).getString(cropKey(entry.id), null) ?: return CropSettings()
@@ -401,7 +419,7 @@ object WallpaperStore {
val sourceFile = when (entry) { val sourceFile = when (entry) {
is Entry.Local -> entry.file is Entry.Local -> entry.file
is Entry.Immich -> { is Entry.Immich -> {
val allowNetwork = entry.serverUrl !in unavailableServers && (!automatic || ImmichClient.automaticDownloadsAllowed(context)) val allowNetwork = entry.serverUrl !in unavailableServers && (!automatic || ImmichClient.automaticDownloadsAllowed(context, allowMobileData(context)))
val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, allowNetwork) val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, allowNetwork)
if (source == null) { if (source == null) {
if (allowNetwork) unavailableServers.add(entry.serverUrl) if (allowNetwork) unavailableServers.add(entry.serverUrl)
+4
View File
@@ -21,6 +21,8 @@ impl<R: Runtime> Wallpaper<R> {
interval_minutes: 30, interval_minutes: 30,
shuffle: true, shuffle: true,
lock_screen_only: true, lock_screen_only: true,
allow_mobile_data: false,
prefetch_immich: true,
current_index: 0, current_index: 0,
current_id: Some("demo-0".into()), current_id: Some("demo-0".into()),
image_ids: vec!["demo-0".into(), "demo-1".into(), "demo-2".into()], image_ids: vec!["demo-0".into(), "demo-1".into(), "demo-2".into()],
@@ -89,6 +91,8 @@ impl<R: Runtime> Wallpaper<R> {
"enabled" => state.enabled = payload.value, "enabled" => state.enabled = payload.value,
"shuffle" => state.shuffle = payload.value, "shuffle" => state.shuffle = payload.value,
"lockScreenOnly" => state.lock_screen_only = payload.value, "lockScreenOnly" => state.lock_screen_only = payload.value,
"allowMobileData" => state.allow_mobile_data = payload.value,
"prefetchImmich" => state.prefetch_immich = payload.value,
_ => {} _ => {}
}; };
Ok(state) Ok(state)
+2
View File
@@ -8,6 +8,8 @@ pub struct WallpaperState {
pub interval_minutes: i32, pub interval_minutes: i32,
pub shuffle: bool, pub shuffle: bool,
pub lock_screen_only: bool, pub lock_screen_only: bool,
pub allow_mobile_data: bool,
pub prefetch_immich: bool,
pub current_index: usize, pub current_index: usize,
pub current_id: Option<String>, pub current_id: Option<String>,
pub image_ids: Vec<String>, pub image_ids: Vec<String>,
+12 -4
View File
@@ -1,10 +1,10 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { ArrowLeft, Check, ChevronRight, Cloud, CloudDownload, Crop, Home, Images, Languages, Link2Off, LockKeyhole, Plus, RotateCcw, RotateCw, Save, Server, Settings, Shuffle, Smartphone, Sparkles, Trash2 } from "lucide-react"; import { ArrowLeft, Check, ChevronRight, Cloud, CloudDownload, Crop, Download, Home, Images, Languages, Link2Off, LockKeyhole, Plus, RotateCcw, RotateCw, Save, Server, Settings, Shuffle, Smartphone, Sparkles, Trash2, Wifi } from "lucide-react";
import { applyWallpaper, connectImmich, deleteImages, disconnectImmich, getGallery, getImageIds, getImmichAlbums, getImmichAssets, getImmichConnection, getImmichImportProgress, getState, importImmichAssets, nextWallpaper, selectImages, setImageCrop, setIntervalMinutes, setSetting, type GalleryImage, type ImmichAlbum, type ImmichAsset, type ImmichConnection, type ImmichImportProgress, type WallpaperState } from "./native"; import { applyWallpaper, connectImmich, deleteImages, disconnectImmich, getGallery, getImageIds, getImmichAlbums, getImmichAssets, getImmichConnection, getImmichImportProgress, getState, importImmichAssets, nextWallpaper, selectImages, setImageCrop, setIntervalMinutes, setSetting, type GalleryImage, type ImmichAlbum, type ImmichAsset, type ImmichConnection, type ImmichImportProgress, type WallpaperState } from "./native";
import { initialLanguage, languageNames, languages, translations, type Language } from "./i18n-local"; import { initialLanguage, languageNames, languages, translations, type Language } from "./i18n-local";
import { immichTranslations } from "./immich-i18n"; import { immichTranslations } from "./immich-i18n";
const initial: WallpaperState = { imageCount: 0, enabled: false, intervalMinutes: 0, shuffle: true, lockScreenOnly: true, currentIndex: 0, currentId: null, imageIds: [], imageUrls: [] }; const initial: WallpaperState = { imageCount: 0, enabled: false, intervalMinutes: 0, shuffle: true, lockScreenOnly: true, allowMobileData: false, prefetchImmich: true, currentIndex: 0, currentId: null, imageIds: [], imageUrls: [] };
function Switch({ checked, onChange, label }: { checked: boolean; onChange: (value: boolean) => void; label: string }) { function Switch({ checked, onChange, label }: { checked: boolean; onChange: (value: boolean) => void; label: string }) {
return <button className={`switch ${checked ? "on" : ""}`} role="switch" aria-checked={checked} aria-label={label} onClick={() => onChange(!checked)}><span /></button>; return <button className={`switch ${checked ? "on" : ""}`} role="switch" aria-checked={checked} aria-label={label} onClick={() => onChange(!checked)}><span /></button>;
@@ -107,13 +107,17 @@ export default function App() {
finally { setGalleryLoading(false); } finally { setGalleryLoading(false); }
} }
async function update(name: "shuffle" | "lockScreenOnly", value: boolean) { async function update(name: "shuffle" | "lockScreenOnly" | "allowMobileData" | "prefetchImmich", value: boolean) {
const previous = state[name];
setState(prev => ({ ...prev, [name]: value })); setState(prev => ({ ...prev, [name]: value }));
try { try {
const saved = await setSetting(name, value); const saved = await setSetting(name, value);
setState(saved); setState(saved);
setNotice(t.settingSaved); setNotice(t.settingSaved);
} catch { setNotice(t.androidOnly); } } catch (error) {
setState(prev => ({ ...prev, [name]: previous }));
setNotice(String(error).replace(/^Error:\s*/, "") || t.androidOnly);
}
} }
async function updateInterval(minutes: number) { async function updateInterval(minutes: number) {
@@ -425,6 +429,10 @@ export default function App() {
<div className="immich-title"><span><Cloud /></span><div><h3>{it.immich}</h3><p>{it.immichIntro}</p></div></div> <div className="immich-title"><span><Cloud /></span><div><h3>{it.immich}</h3><p>{it.immichIntro}</p></div></div>
{immichConnection.configured ? <> {immichConnection.configured ? <>
<div className="immich-connected"><strong>{it.connectedAs} {immichConnection.userName}</strong><span>{immichConnection.serverUrl}</span></div> <div className="immich-connected"><strong>{it.connectedAs} {immichConnection.userName}</strong><span>{immichConnection.serverUrl}</span></div>
<SettingRow icon={<Download />} label={it.prefetchImmich} value={state.prefetchImmich} onChange={v => update("prefetchImmich", v)} />
<p className="immich-help">{it.prefetchImmichHint}</p>
<SettingRow icon={<Wifi />} label={it.allowMobileData} value={state.allowMobileData} onChange={v => update("allowMobileData", v)} />
<p className="immich-help">{it.allowMobileDataHint}</p>
<button className="immich-open" onClick={openImmich} disabled={busy}><Images /> {it.openImmich} <ChevronRight /></button> <button className="immich-open" onClick={openImmich} disabled={busy}><Images /> {it.openImmich} <ChevronRight /></button>
<button className="immich-disconnect" onClick={removeImmichConnection} disabled={busy}><Link2Off /> {it.disconnect}</button> <button className="immich-disconnect" onClick={removeImmichConnection} disabled={busy}><Link2Off /> {it.disconnect}</button>
</> : <> </> : <>
+48
View File
@@ -22,6 +22,10 @@ export type ImmichText = {
importSuccess: string; importSuccess: string;
immichLoadFailed: string; immichLoadFailed: string;
noImmichPhotos: string; noImmichPhotos: string;
allowMobileData: string;
allowMobileDataHint: string;
prefetchImmich: string;
prefetchImmichHint: string;
}; };
export const immichTranslations: Record<Language, ImmichText> = { export const immichTranslations: Record<Language, ImmichText> = {
@@ -47,6 +51,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
importSuccess: "Immich-Bilder wurden zur Sammlung hinzugefügt", importSuccess: "Immich-Bilder wurden zur Sammlung hinzugefügt",
immichLoadFailed: "Immich-Bilder konnten nicht geladen werden", immichLoadFailed: "Immich-Bilder konnten nicht geladen werden",
noImmichPhotos: "Keine Bilder gefunden", noImmichPhotos: "Keine Bilder gefunden",
allowMobileData: "Auch über mobile Daten laden",
allowMobileDataHint: "Ist das ausgeschaltet, lädt der automatische Wechsel Immich-Bilder nur über WLAN und überspringt sie sonst. Manuelles Wechseln lädt immer.",
prefetchImmich: "Immich-Bilder immer vorladen",
prefetchImmichHint: "Lädt neue Immich-Bilder automatisch im Hintergrund herunter, sobald WLAN verfügbar ist nie über mobile Daten. Ausgeschaltet werden Bilder nur bei Bedarf geladen.",
}, },
en: { en: {
immich: "Immich", immichIntro: "Import images from your own Immich server.", serverUrl: "Server URL", apiKey: "API key", immich: "Immich", immichIntro: "Import images from your own Immich server.", serverUrl: "Server URL", apiKey: "API key",
@@ -56,6 +64,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP is unencrypted. Use HTTPS outside your home network.", allPhotos: "All photos", httpWarning: "HTTP is unencrypted. Use HTTPS outside your home network.", allPhotos: "All photos",
selectImmichPhotos: "Select images to import", importSelected: "Import selection", importing: "Importing images…", selectImmichPhotos: "Select images to import", importSelected: "Import selection", importing: "Importing images…",
importSuccess: "Immich images imported", immichLoadFailed: "Could not load Immich images", noImmichPhotos: "No images found", importSuccess: "Immich images imported", immichLoadFailed: "Could not load Immich images", noImmichPhotos: "No images found",
allowMobileData: "Allow downloads over mobile data",
allowMobileDataHint: "When off, automatic rotation only downloads Immich images over Wi-Fi and skips them otherwise. Manually changing the wallpaper always downloads.",
prefetchImmich: "Always prefetch Immich images",
prefetchImmichHint: "Downloads new Immich images in the background as soon as Wi-Fi is available never over mobile data. When off, images are only downloaded when needed.",
}, },
fr: { fr: {
immich: "Immich", immichIntro: "Importez des images depuis votre propre serveur Immich.", serverUrl: "URL du serveur", apiKey: "Clé API", immich: "Immich", immichIntro: "Importez des images depuis votre propre serveur Immich.", serverUrl: "URL du serveur", apiKey: "Clé API",
@@ -65,6 +77,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP nest pas chiffré. Utilisez HTTPS hors de votre réseau domestique.", allPhotos: "Toutes les photos", httpWarning: "HTTP nest pas chiffré. Utilisez HTTPS hors de votre réseau domestique.", allPhotos: "Toutes les photos",
selectImmichPhotos: "Sélectionnez les images à importer", importSelected: "Importer la sélection", importing: "Importation des images…", selectImmichPhotos: "Sélectionnez les images à importer", importSelected: "Importer la sélection", importing: "Importation des images…",
importSuccess: "Images Immich importées", immichLoadFailed: "Impossible de charger les images Immich", noImmichPhotos: "Aucune image trouvée", importSuccess: "Images Immich importées", immichLoadFailed: "Impossible de charger les images Immich", noImmichPhotos: "Aucune image trouvée",
allowMobileData: "Autoriser les données mobiles",
allowMobileDataHint: "Désactivé, la rotation automatique ne télécharge les images Immich que via le Wi-Fi et les ignore sinon. Un changement manuel télécharge toujours.",
prefetchImmich: "Toujours précharger les images Immich",
prefetchImmichHint: "Télécharge les nouvelles images Immich en arrière-plan dès que le Wi-Fi est disponible jamais via les données mobiles. Désactivé, les images ne sont téléchargées quen cas de besoin.",
}, },
es: { es: {
immich: "Immich", immichIntro: "Importa imágenes desde tu propio servidor Immich.", serverUrl: "URL del servidor", apiKey: "Clave API", immich: "Immich", immichIntro: "Importa imágenes desde tu propio servidor Immich.", serverUrl: "URL del servidor", apiKey: "Clave API",
@@ -74,6 +90,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP no está cifrado. Usa HTTPS fuera de tu red doméstica.", allPhotos: "Todas las fotos", httpWarning: "HTTP no está cifrado. Usa HTTPS fuera de tu red doméstica.", allPhotos: "Todas las fotos",
selectImmichPhotos: "Selecciona imágenes para importar", importSelected: "Importar selección", importing: "Importando imágenes…", selectImmichPhotos: "Selecciona imágenes para importar", importSelected: "Importar selección", importing: "Importando imágenes…",
importSuccess: "Imágenes de Immich importadas", immichLoadFailed: "No se pudieron cargar las imágenes de Immich", noImmichPhotos: "No se encontraron imágenes", importSuccess: "Imágenes de Immich importadas", immichLoadFailed: "No se pudieron cargar las imágenes de Immich", noImmichPhotos: "No se encontraron imágenes",
allowMobileData: "Permitir datos móviles",
allowMobileDataHint: "Si está desactivado, la rotación automática solo descarga imágenes de Immich por Wi-Fi y las omite si no hay Wi-Fi. Cambiar manualmente siempre descarga.",
prefetchImmich: "Precargar siempre las imágenes de Immich",
prefetchImmichHint: "Descarga las nuevas imágenes de Immich en segundo plano en cuanto haya Wi-Fi disponible, nunca con datos móviles. Si está desactivado, las imágenes solo se descargan cuando se necesitan.",
}, },
it: { it: {
immich: "Immich", immichIntro: "Importa immagini dal tuo server Immich.", serverUrl: "URL del server", apiKey: "Chiave API", immich: "Immich", immichIntro: "Importa immagini dal tuo server Immich.", serverUrl: "URL del server", apiKey: "Chiave API",
@@ -83,6 +103,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP non è crittografato. Usa HTTPS fuori dalla rete domestica.", allPhotos: "Tutte le foto", httpWarning: "HTTP non è crittografato. Usa HTTPS fuori dalla rete domestica.", allPhotos: "Tutte le foto",
selectImmichPhotos: "Seleziona le immagini da importare", importSelected: "Importa selezione", importing: "Importazione immagini…", selectImmichPhotos: "Seleziona le immagini da importare", importSelected: "Importa selezione", importing: "Importazione immagini…",
importSuccess: "Immagini Immich importate", immichLoadFailed: "Impossibile caricare le immagini Immich", noImmichPhotos: "Nessuna immagine trovata", importSuccess: "Immagini Immich importate", immichLoadFailed: "Impossibile caricare le immagini Immich", noImmichPhotos: "Nessuna immagine trovata",
allowMobileData: "Consenti dati mobili",
allowMobileDataHint: "Se disattivato, la rotazione automatica scarica le immagini Immich solo via Wi-Fi e le salta altrimenti. Il cambio manuale scarica sempre.",
prefetchImmich: "Precarica sempre le immagini Immich",
prefetchImmichHint: "Scarica le nuove immagini Immich in background non appena il Wi-Fi è disponibile, mai con i dati mobili. Se disattivato, le immagini vengono scaricate solo quando servono.",
}, },
nl: { nl: {
immich: "Immich", immichIntro: "Importeer afbeeldingen van je eigen Immich-server.", serverUrl: "Server-URL", apiKey: "API-sleutel", immich: "Immich", immichIntro: "Importeer afbeeldingen van je eigen Immich-server.", serverUrl: "Server-URL", apiKey: "API-sleutel",
@@ -92,6 +116,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP is niet versleuteld. Gebruik HTTPS buiten je thuisnetwerk.", allPhotos: "Alle fotos", httpWarning: "HTTP is niet versleuteld. Gebruik HTTPS buiten je thuisnetwerk.", allPhotos: "Alle fotos",
selectImmichPhotos: "Selecteer afbeeldingen om te importeren", importSelected: "Selectie importeren", importing: "Afbeeldingen importeren…", selectImmichPhotos: "Selecteer afbeeldingen om te importeren", importSelected: "Selectie importeren", importing: "Afbeeldingen importeren…",
importSuccess: "Immich-afbeeldingen geïmporteerd", immichLoadFailed: "Immich-afbeeldingen konden niet worden geladen", noImmichPhotos: "Geen afbeeldingen gevonden", importSuccess: "Immich-afbeeldingen geïmporteerd", immichLoadFailed: "Immich-afbeeldingen konden niet worden geladen", noImmichPhotos: "Geen afbeeldingen gevonden",
allowMobileData: "Mobiele data toestaan",
allowMobileDataHint: "Indien uit, worden Immich-afbeeldingen bij automatisch wisselen alleen via wifi gedownload en anders overgeslagen. Handmatig wisselen downloadt altijd.",
prefetchImmich: "Immich-afbeeldingen altijd vooraf laden",
prefetchImmichHint: "Downloadt nieuwe Immich-afbeeldingen op de achtergrond zodra wifi beschikbaar is nooit via mobiele data. Indien uit, worden afbeeldingen alleen bij gebruik gedownload.",
}, },
pl: { pl: {
immich: "Immich", immichIntro: "Importuj obrazy z własnego serwera Immich.", serverUrl: "Adres serwera", apiKey: "Klucz API", immich: "Immich", immichIntro: "Importuj obrazy z własnego serwera Immich.", serverUrl: "Adres serwera", apiKey: "Klucz API",
@@ -101,6 +129,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP nie jest szyfrowany. Poza siecią domową używaj HTTPS.", allPhotos: "Wszystkie zdjęcia", httpWarning: "HTTP nie jest szyfrowany. Poza siecią domową używaj HTTPS.", allPhotos: "Wszystkie zdjęcia",
selectImmichPhotos: "Wybierz obrazy do importu", importSelected: "Importuj wybrane", importing: "Importowanie obrazów…", selectImmichPhotos: "Wybierz obrazy do importu", importSelected: "Importuj wybrane", importing: "Importowanie obrazów…",
importSuccess: "Zaimportowano obrazy Immich", immichLoadFailed: "Nie udało się wczytać obrazów Immich", noImmichPhotos: "Nie znaleziono obrazów", importSuccess: "Zaimportowano obrazy Immich", immichLoadFailed: "Nie udało się wczytać obrazów Immich", noImmichPhotos: "Nie znaleziono obrazów",
allowMobileData: "Zezwól na dane mobilne",
allowMobileDataHint: "Gdy wyłączone, automatyczna zmiana pobiera obrazy Immich tylko przez Wi-Fi, w przeciwnym razie je pomija. Ręczna zmiana zawsze pobiera.",
prefetchImmich: "Zawsze pobieraj obrazy Immich z wyprzedzeniem",
prefetchImmichHint: "Pobiera nowe obrazy Immich w tle, gdy tylko dostępne jest Wi-Fi nigdy przez dane mobilne. Gdy wyłączone, obrazy są pobierane tylko w razie potrzeby.",
}, },
pt: { pt: {
immich: "Immich", immichIntro: "Importe imagens do seu próprio servidor Immich.", serverUrl: "URL do servidor", apiKey: "Chave de API", immich: "Immich", immichIntro: "Importe imagens do seu próprio servidor Immich.", serverUrl: "URL do servidor", apiKey: "Chave de API",
@@ -110,6 +142,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP não é encriptado. Use HTTPS fora da sua rede doméstica.", allPhotos: "Todas as fotos", httpWarning: "HTTP não é encriptado. Use HTTPS fora da sua rede doméstica.", allPhotos: "Todas as fotos",
selectImmichPhotos: "Selecione imagens para importar", importSelected: "Importar seleção", importing: "A importar imagens…", selectImmichPhotos: "Selecione imagens para importar", importSelected: "Importar seleção", importing: "A importar imagens…",
importSuccess: "Imagens do Immich importadas", immichLoadFailed: "Não foi possível carregar imagens do Immich", noImmichPhotos: "Nenhuma imagem encontrada", importSuccess: "Imagens do Immich importadas", immichLoadFailed: "Não foi possível carregar imagens do Immich", noImmichPhotos: "Nenhuma imagem encontrada",
allowMobileData: "Permitir dados móveis",
allowMobileDataHint: "Quando desativado, a rotação automática só transfere imagens do Immich por Wi-Fi e ignora-as caso contrário. A troca manual transfere sempre.",
prefetchImmich: "Pré-carregar sempre imagens do Immich",
prefetchImmichHint: "Transfere novas imagens do Immich em segundo plano assim que o Wi-Fi estiver disponível nunca com dados móveis. Quando desativado, as imagens só são transferidas quando necessário.",
}, },
ja: { ja: {
immich: "Immich", immichIntro: "自分の Immich サーバーから画像を取り込みます。", serverUrl: "サーバー URL", apiKey: "API キー", immich: "Immich", immichIntro: "自分の Immich サーバーから画像を取り込みます。", serverUrl: "サーバー URL", apiKey: "API キー",
@@ -119,6 +155,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP は暗号化されません。自宅ネットワーク外では HTTPS を使用してください。", allPhotos: "すべての写真", httpWarning: "HTTP は暗号化されません。自宅ネットワーク外では HTTPS を使用してください。", allPhotos: "すべての写真",
selectImmichPhotos: "取り込む画像を選択", importSelected: "選択項目を取り込む", importing: "画像を取り込み中…", selectImmichPhotos: "取り込む画像を選択", importSelected: "選択項目を取り込む", importing: "画像を取り込み中…",
importSuccess: "Immich の画像を取り込みました", immichLoadFailed: "Immich の画像を読み込めません", noImmichPhotos: "画像が見つかりません", importSuccess: "Immich の画像を取り込みました", immichLoadFailed: "Immich の画像を読み込めません", noImmichPhotos: "画像が見つかりません",
allowMobileData: "モバイルデータでのダウンロードを許可",
allowMobileDataHint: "オフの場合、自動切り替えは Wi-Fi 接続時のみ Immich の画像をダウンロードし、それ以外はスキップします。手動での切り替えは常にダウンロードします。",
prefetchImmich: "Immich画像を常に先読みする",
prefetchImmichHint: "Wi-Fi が利用可能になり次第、新しい Immich 画像をバックグラウンドでダウンロードします(モバイルデータは使用しません)。オフの場合、画像は必要なときのみダウンロードされます。",
}, },
ko: { ko: {
immich: "Immich", immichIntro: "내 Immich 서버에서 이미지를 가져옵니다.", serverUrl: "서버 URL", apiKey: "API 키", immich: "Immich", immichIntro: "내 Immich 서버에서 이미지를 가져옵니다.", serverUrl: "서버 URL", apiKey: "API 키",
@@ -128,6 +168,10 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP는 암호화되지 않습니다. 홈 네트워크 밖에서는 HTTPS를 사용하세요.", allPhotos: "모든 사진", httpWarning: "HTTP는 암호화되지 않습니다. 홈 네트워크 밖에서는 HTTPS를 사용하세요.", allPhotos: "모든 사진",
selectImmichPhotos: "가져올 이미지를 선택하세요", importSelected: "선택 항목 가져오기", importing: "이미지 가져오는 중…", selectImmichPhotos: "가져올 이미지를 선택하세요", importSelected: "선택 항목 가져오기", importing: "이미지 가져오는 중…",
importSuccess: "Immich 이미지를 가져왔습니다", immichLoadFailed: "Immich 이미지를 불러올 수 없습니다", noImmichPhotos: "이미지를 찾을 수 없습니다", importSuccess: "Immich 이미지를 가져왔습니다", immichLoadFailed: "Immich 이미지를 불러올 수 없습니다", noImmichPhotos: "이미지를 찾을 수 없습니다",
allowMobileData: "모바일 데이터 다운로드 허용",
allowMobileDataHint: "꺼져 있으면 자동 변경 시 Wi-Fi에서만 Immich 이미지를 다운로드하고 그렇지 않으면 건너뜁니다. 수동 변경은 항상 다운로드합니다.",
prefetchImmich: "Immich 이미지 항상 미리 다운로드",
prefetchImmichHint: "Wi-Fi를 사용할 수 있게 되면 새 Immich 이미지를 백그라운드에서 다운로드합니다(모바일 데이터는 사용하지 않음). 꺼져 있으면 필요할 때만 이미지를 다운로드합니다.",
}, },
"zh-CN": { "zh-CN": {
immich: "Immich", immichIntro: "从你自己的 Immich 服务器导入图片。", serverUrl: "服务器 URL", apiKey: "API 密钥", immich: "Immich", immichIntro: "从你自己的 Immich 服务器导入图片。", serverUrl: "服务器 URL", apiKey: "API 密钥",
@@ -137,5 +181,9 @@ export const immichTranslations: Record<Language, ImmichText> = {
httpWarning: "HTTP 未加密。在家庭网络之外请使用 HTTPS。", allPhotos: "所有照片", httpWarning: "HTTP 未加密。在家庭网络之外请使用 HTTPS。", allPhotos: "所有照片",
selectImmichPhotos: "选择要导入的图片", importSelected: "导入所选图片", importing: "正在导入图片…", selectImmichPhotos: "选择要导入的图片", importSelected: "导入所选图片", importing: "正在导入图片…",
importSuccess: "已导入 Immich 图片", immichLoadFailed: "无法加载 Immich 图片", noImmichPhotos: "未找到图片", importSuccess: "已导入 Immich 图片", immichLoadFailed: "无法加载 Immich 图片", noImmichPhotos: "未找到图片",
allowMobileData: "允许使用移动数据下载",
allowMobileDataHint: "关闭时,自动更换仅在 Wi-Fi 下下载 Immich 图片,否则将跳过;手动更换始终会下载。",
prefetchImmich: "始终预下载 Immich 图片",
prefetchImmichHint: "只要有 Wi-Fi 可用,就会在后台下载新的 Immich 图片——绝不使用移动数据。关闭时,图片仅在需要时才会下载。",
}, },
}; };
+5 -1
View File
@@ -6,6 +6,8 @@ export type WallpaperState = {
intervalMinutes: number; intervalMinutes: number;
shuffle: boolean; shuffle: boolean;
lockScreenOnly: boolean; lockScreenOnly: boolean;
allowMobileData: boolean;
prefetchImmich: boolean;
currentIndex: number; currentIndex: number;
currentId: string | null; currentId: string | null;
imageIds: string[]; imageIds: string[];
@@ -68,6 +70,8 @@ const demoState: WallpaperState = {
intervalMinutes: 30, intervalMinutes: 30,
shuffle: true, shuffle: true,
lockScreenOnly: true, lockScreenOnly: true,
allowMobileData: false,
prefetchImmich: true,
currentIndex: 0, currentIndex: 0,
currentId: "demo-0", currentId: "demo-0",
imageIds: ["demo-0", "demo-1", "demo-2"], imageIds: ["demo-0", "demo-1", "demo-2"],
@@ -148,7 +152,7 @@ export async function setImageCrop(image: GalleryImage): Promise<GalleryImage> {
return { ...image }; return { ...image };
} }
export async function setSetting(name: "shuffle" | "lockScreenOnly", value: boolean) { export async function setSetting(name: "shuffle" | "lockScreenOnly" | "allowMobileData" | "prefetchImmich", value: boolean) {
if (!inTauri()) return { ...demoState, [name]: value }; if (!inTauri()) return { ...demoState, [name]: value };
return invoke<WallpaperState>("plugin:wallpaper|set_setting", { name, value }); return invoke<WallpaperState>("plugin:wallpaper|set_setting", { name, value });
} }