feat(android): switch to screen-on rotation service and updated scheduler
The rotation service now responds to screen on/off events via a dedicated ScreenOnRotationService, reducing background work and avoiding perpetual wakeful states. Manifest and receiver logic were adjusted to align with the new behavior and energy constraints. Scheduling and enablement now consistently use timed-based rules and screen-on preferences, with the boot and alarm paths updated to honor these settings. This avoids unnecessary foreground services and keeps the app compliant with Doze and battery optimizations. Immich image handling improvements include throttled progress updates, larger cache allowances, and a connectivity-aware automatic download check to avoid unwanted network usage. - Use ScreenOnRotationService for screen-on based rotation scheduling - Centralize enablement via timed-enabled paths and AlarmManager windowing - Improve Immich cache and progress reporting with connectivity checks
This commit is contained in:
@@ -10,12 +10,15 @@ WallpaperFlow ist eine freie Tauri-2-App für Android, die aus einer frei wählb
|
|||||||
- zufällige oder fortlaufende Reihenfolge
|
- zufällige oder fortlaufende Reihenfolge
|
||||||
- manueller Wechsel über „Nächstes Motiv“
|
- manueller Wechsel über „Nächstes Motiv“
|
||||||
- automatischer Wechsel alle 5, 15 oder 30 Minuten beziehungsweise alle 1, 3, 6 oder 12 Stunden
|
- automatischer Wechsel alle 5, 15 oder 30 Minuten beziehungsweise alle 1, 3, 6 oder 12 Stunden
|
||||||
- Neustart des Dienstes nach einem Geräte-Neustart, soweit die Android-Version Hintergrundstarts zulässt
|
- optionaler Wechsel bei jedem Einschalten des Displays
|
||||||
|
- erneute Planung des automatischen Wechsels nach einem Geräte-Neustart
|
||||||
- lokale Vorschaubilder; die Originalbilder verlassen das Gerät nicht
|
- lokale Vorschaubilder; die Originalbilder verlassen das Gerät nicht
|
||||||
- optionaler Import aus einem selbst gehosteten Immich-Server über einen eingeschränkten API-Key
|
- optionaler Import aus einem selbst gehosteten Immich-Server über einen eingeschränkten API-Key
|
||||||
- vollständig lokale Oberfläche in Deutsch, Englisch, Französisch, Spanisch, Italienisch, Niederländisch, Polnisch, Portugiesisch, Japanisch, Koreanisch und vereinfachtem Chinesisch
|
- vollständig lokale Oberfläche in Deutsch, Englisch, Französisch, Spanisch, Italienisch, Niederländisch, Polnisch, Portugiesisch, Japanisch, Koreanisch und vereinfachtem Chinesisch
|
||||||
|
|
||||||
Android hält den Zeitgeber über einen Foreground Service aktiv. Solange ein Wechselintervall ausgewählt ist, zeigt das System deshalb eine dauerhafte, stille Benachrichtigung. Auf Geräten mit aggressivem Energiesparen muss LockScreenWallpaper gegebenenfalls von der Akku-Optimierung ausgenommen werden.
|
Der automatische Wechsel verwendet einen ungenauen Android-Alarm und respektiert Doze sowie den Energiesparmodus. Dadurch gibt es keinen dauerhaft laufenden Dienst und keine permanente Benachrichtigung; Wechsel können im Ruhezustand etwas später erfolgen. Automatische Immich-Downloads erfolgen nur über eine nicht getaktete Verbindung und bei ausreichendem Akkustand.
|
||||||
|
|
||||||
|
Der optionale Modus „Beim Einschalten des Displays“ benötigt dagegen einen kleinen Foreground Service mit permanenter Benachrichtigung, damit Android die Display-Ereignisse zuverlässig zustellt. Das nächste Bild wird bereits beim Ausschalten gesetzt und ist dadurch beim folgenden Einschalten ohne sichtbare Verzögerung vorhanden. Der Dienst hält keinen CPU-Wake-Lock.
|
||||||
|
|
||||||
## Entwicklung
|
## Entwicklung
|
||||||
|
|
||||||
@@ -46,9 +49,9 @@ Der native Android-Code liegt als lokales Tauri-Plugin in `plugins/android`. Er
|
|||||||
|
|
||||||
## Wichtige Android-Hinweise
|
## Wichtige Android-Hinweise
|
||||||
|
|
||||||
- Android erlaubt keinen komplett unsichtbaren, dauerhaft laufenden Zeitgeber. Der automatische Wechsel verwendet deshalb einen Foreground Service.
|
- Android darf ungenaue Alarme im Ruhemodus verzögern. Der nächste Wechsel wird ausgeführt, sobald das Gerät wieder aktiv ist.
|
||||||
- `WallpaperManager.FLAG_LOCK` ist ab Android 7 verfügbar. Auf älteren Geräten setzt die App das allgemeine Hintergrundbild.
|
- `WallpaperManager.FLAG_LOCK` ist ab Android 7 verfügbar. Auf älteren Geräten setzt die App das allgemeine Hintergrundbild.
|
||||||
- Hersteller können Hintergrunddienste zusätzlich einschränken. Besonders bei Samsung/Xiaomi kann eine Ausnahme von der Akku-Optimierung nötig sein.
|
- Hersteller können Hintergrundarbeit zusätzlich verzögern. Eine Ausnahme von der Akku-Optimierung ist für den normalen Betrieb nicht vorgesehen.
|
||||||
- „Ohne Limit“ bedeutet: kein App-Zähler wie Samsungs 15-Bilder-Grenze. Praktisch begrenzen freier Gerätespeicher und Dateisystem die Sammlung.
|
- „Ohne Limit“ bedeutet: kein App-Zähler wie Samsungs 15-Bilder-Grenze. Praktisch begrenzen freier Gerätespeicher und Dateisystem die Sammlung.
|
||||||
|
|
||||||
## Datenschutz
|
## Datenschutz
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<application>
|
<application>
|
||||||
<service android:name="de.wechselbild.wallpaper.WallpaperRotationService" android:exported="false" android:foregroundServiceType="specialUse">
|
<service android:name="de.wechselbild.wallpaper.ScreenOnRotationService" 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 at the configured interval" />
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Changes the user-selected lock-screen wallpaper when the screen turns on" />
|
||||||
</service>
|
</service>
|
||||||
<receiver android:name="de.wechselbild.wallpaper.BootReceiver" android:enabled="true" android:exported="true">
|
<receiver android:name="de.wechselbild.wallpaper.BootReceiver" android:enabled="true" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import android.content.Intent
|
|||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action in setOf(Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED) && WallpaperStore.enabled(context)) {
|
if (intent.action !in setOf(Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED)) return
|
||||||
WallpaperScheduler.scheduleNext(context)
|
when {
|
||||||
runCatching { WallpaperRotationService.start(context) }
|
WallpaperStore.screenOnEnabled(context) -> runCatching { ScreenOnRotationService.start(context) }
|
||||||
|
WallpaperStore.timedEnabled(context) -> WallpaperScheduler.scheduleNext(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.wechselbild.wallpaper
|
package de.wechselbild.wallpaper
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
@@ -33,9 +34,9 @@ object ImmichClient {
|
|||||||
private const val KEY_API_KEY_DATA = "immich_api_key_data"
|
private const val KEY_API_KEY_DATA = "immich_api_key_data"
|
||||||
private const val KEY_API_KEY_IV = "immich_api_key_iv"
|
private const val KEY_API_KEY_IV = "immich_api_key_iv"
|
||||||
private const val KEY_ALIAS = "wallpaperflow_immich_api_key"
|
private const val KEY_ALIAS = "wallpaperflow_immich_api_key"
|
||||||
private const val CACHE_MAX_FILES = 3
|
private const val CACHE_MAX_BYTES = 512L * 1024 * 1024
|
||||||
private const val CACHE_MAX_BYTES = 256L * 1024 * 1024
|
|
||||||
private const val DOWNLOAD_MAX_BYTES = 128L * 1024 * 1024
|
private const val DOWNLOAD_MAX_BYTES = 128L * 1024 * 1024
|
||||||
|
private const val PROGRESS_STEP_BYTES = 256L * 1024
|
||||||
private val assetIdPattern = Regex("^[0-9a-fA-F-]{36}$")
|
private val assetIdPattern = Regex("^[0-9a-fA-F-]{36}$")
|
||||||
private val thumbnailPool = Executors.newFixedThreadPool(4)
|
private val thumbnailPool = Executors.newFixedThreadPool(4)
|
||||||
private val importProgress = AtomicReference(ImportProgress())
|
private val importProgress = AtomicReference(ImportProgress())
|
||||||
@@ -204,6 +205,7 @@ object ImmichClient {
|
|||||||
val total = connection.contentLengthLong.coerceAtLeast(0)
|
val total = connection.contentLengthLong.coerceAtLeast(0)
|
||||||
require(total == 0L || total <= DOWNLOAD_MAX_BYTES) { "Das Immich-Bild ist zu groß." }
|
require(total == 0L || total <= DOWNLOAD_MAX_BYTES) { "Das Immich-Bild ist zu groß." }
|
||||||
var downloaded = 0L
|
var downloaded = 0L
|
||||||
|
var lastProgressUpdate = 0L
|
||||||
if (trackProgress) importProgress.updateAndGet { it.copy(bytesDownloaded = 0, bytesTotal = total) }
|
if (trackProgress) importProgress.updateAndGet { it.copy(bytesDownloaded = 0, bytesTotal = total) }
|
||||||
connection.inputStream.use { input ->
|
connection.inputStream.use { input ->
|
||||||
target.outputStream().use { output ->
|
target.outputStream().use { output ->
|
||||||
@@ -214,7 +216,10 @@ object ImmichClient {
|
|||||||
output.write(buffer, 0, count)
|
output.write(buffer, 0, count)
|
||||||
downloaded += count
|
downloaded += count
|
||||||
require(downloaded <= DOWNLOAD_MAX_BYTES) { "Das Immich-Bild ist zu groß." }
|
require(downloaded <= DOWNLOAD_MAX_BYTES) { "Das Immich-Bild ist zu groß." }
|
||||||
if (trackProgress) importProgress.updateAndGet { it.copy(bytesDownloaded = downloaded, bytesTotal = total) }
|
if (trackProgress && (downloaded - lastProgressUpdate >= PROGRESS_STEP_BYTES || downloaded == total)) {
|
||||||
|
lastProgressUpdate = downloaded
|
||||||
|
importProgress.updateAndGet { it.copy(bytesDownloaded = downloaded, bytesTotal = total) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,6 +314,12 @@ object ImmichClient {
|
|||||||
return bounds.outWidth > 0 && bounds.outHeight > 0
|
return bounds.outWidth > 0 && bounds.outHeight > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun automaticDownloadsAllowed(context: Context): Boolean {
|
||||||
|
val connectivity = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
return connectivity.activeNetworkInfo?.isConnected == true && !connectivity.isActiveNetworkMetered
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun cachedOriginal(context: Context, serverUrl: String, assetId: String, allowNetwork: Boolean = true): File? {
|
fun cachedOriginal(context: Context, serverUrl: String, assetId: String, allowNetwork: Boolean = true): File? {
|
||||||
val target = File(cacheDirectory(context), "${cacheKey(serverUrl, assetId)}.image")
|
val target = File(cacheDirectory(context), "${cacheKey(serverUrl, assetId)}.image")
|
||||||
@@ -347,13 +358,13 @@ object ImmichClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun trimCache(context: Context, protected: File) {
|
private fun trimCache(context: Context, protected: File) {
|
||||||
val files = cacheDirectory(context).listFiles()?.filter { it.isFile && !it.name.startsWith(".") }?.sortedByDescending { it.lastModified() } ?: return
|
val files = cacheDirectory(context).listFiles()?.filter { it.isFile && !it.name.startsWith(".") }?.sortedBy { it.lastModified() } ?: return
|
||||||
var bytes = 0L
|
var bytes = files.sumOf { it.length() }
|
||||||
files.forEachIndexed { index, file ->
|
for (file in files) {
|
||||||
bytes += file.length()
|
if (bytes <= CACHE_MAX_BYTES) break
|
||||||
if (file != protected && (index >= CACHE_MAX_FILES || bytes > CACHE_MAX_BYTES)) {
|
if (file != protected) {
|
||||||
bytes -= file.length()
|
val length = file.length()
|
||||||
file.delete()
|
if (file.delete()) bytes -= length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-50
@@ -4,37 +4,46 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class WallpaperRotationService : Service() {
|
class ScreenOnRotationService : Service() {
|
||||||
private val worker = Executors.newSingleThreadExecutor()
|
private val worker = Executors.newSingleThreadExecutor()
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val rotating = AtomicBoolean(false)
|
||||||
private val rotate = object : Runnable {
|
private val changedWhileScreenOff = AtomicBoolean(false)
|
||||||
override fun run() {
|
private val screenReceiver = object : BroadcastReceiver() {
|
||||||
if (!WallpaperStore.enabled(this@WallpaperRotationService)) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
stopSelf()
|
if (!WallpaperStore.screenOnEnabled(context)) return
|
||||||
return
|
when (intent.action) {
|
||||||
}
|
Intent.ACTION_SCREEN_OFF -> rotate(context, preparedForScreenOn = true)
|
||||||
worker.execute {
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
if (WallpaperScheduler.claimIfDue(this@WallpaperRotationService)) {
|
if (!changedWhileScreenOff.getAndSet(false)) rotate(context, preparedForScreenOn = false)
|
||||||
WallpaperRotationWorker.enqueue(this@WallpaperRotationService)
|
|
||||||
}
|
}
|
||||||
handler.post { scheduleLocalTimer() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun rotate(context: Context, preparedForScreenOn: Boolean) {
|
||||||
|
if (!rotating.compareAndSet(false, true)) return
|
||||||
|
worker.execute {
|
||||||
|
try {
|
||||||
|
if (WallpaperStore.applyNext(context, automatic = true) && preparedForScreenOn) {
|
||||||
|
changedWhileScreenOff.set(true)
|
||||||
|
}
|
||||||
|
} finally { rotating.set(false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
runningService = this
|
|
||||||
createChannel()
|
createChannel()
|
||||||
val launch = packageManager.getLaunchIntentForPackage(packageName)
|
val launch = packageManager.getLaunchIntentForPackage(packageName)
|
||||||
val pending = PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
val pending = PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
@@ -42,34 +51,49 @@ class WallpaperRotationService : Service() {
|
|||||||
.setSmallIcon(android.R.drawable.ic_menu_gallery)
|
.setSmallIcon(android.R.drawable.ic_menu_gallery)
|
||||||
.setContentTitle(getString(R.string.wallpaper_service_title))
|
.setContentTitle(getString(R.string.wallpaper_service_title))
|
||||||
.setContentText(getString(R.string.wallpaper_service_text))
|
.setContentText(getString(R.string.wallpaper_service_text))
|
||||||
.setOngoing(true).setSilent(true).setContentIntent(pending).build()
|
.setOngoing(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.setContentIntent(pending)
|
||||||
|
.build()
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
WallpaperScheduler.ensureScheduled(this)
|
val filter = IntentFilter().apply {
|
||||||
scheduleLocalTimer()
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
|
registerReceiver(screenReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
registerReceiver(screenReceiver, filter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (!WallpaperStore.enabled(this)) {
|
if (!WallpaperStore.screenOnEnabled(this)) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
WallpaperScheduler.ensureScheduled(this)
|
|
||||||
scheduleLocalTimer()
|
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
handler.removeCallbacks(rotate)
|
runCatching { unregisterReceiver(screenReceiver) }
|
||||||
worker.shutdown()
|
worker.shutdown()
|
||||||
if (runningService === this) runningService = null
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleLocalTimer() {
|
companion object {
|
||||||
handler.removeCallbacks(rotate)
|
private const val CHANNEL = "wechselbild_screen_on"
|
||||||
if (WallpaperStore.enabled(this)) {
|
private const val NOTIFICATION_ID = 4217
|
||||||
handler.postDelayed(rotate, WallpaperScheduler.remainingDelay(this))
|
|
||||||
|
fun start(context: Context) {
|
||||||
|
ContextCompat.startForegroundService(context, Intent(context, ScreenOnRotationService::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, ScreenOnRotationService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,27 +103,4 @@ class WallpaperRotationService : Service() {
|
|||||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CHANNEL = "wechselbild_rotation"
|
|
||||||
private const val NOTIFICATION_ID = 4217
|
|
||||||
@Volatile private var runningService: WallpaperRotationService? = null
|
|
||||||
|
|
||||||
fun rescheduleRunningService() {
|
|
||||||
val service = runningService ?: return
|
|
||||||
service.handler.post { service.scheduleLocalTimer() }
|
|
||||||
}
|
|
||||||
fun start(context: Context) {
|
|
||||||
WallpaperScheduler.ensureScheduled(context)
|
|
||||||
ContextCompat.startForegroundService(context, Intent(context, WallpaperRotationService::class.java))
|
|
||||||
}
|
|
||||||
fun restart(context: Context) {
|
|
||||||
WallpaperScheduler.scheduleNext(context)
|
|
||||||
start(context)
|
|
||||||
}
|
|
||||||
fun stop(context: Context) {
|
|
||||||
WallpaperScheduler.cancel(context)
|
|
||||||
context.stopService(Intent(context, WallpaperRotationService::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import android.content.Intent
|
|||||||
|
|
||||||
class WallpaperAlarmReceiver : BroadcastReceiver() {
|
class WallpaperAlarmReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (!WallpaperStore.enabled(context)) {
|
if (!WallpaperStore.timedEnabled(context)) {
|
||||||
WallpaperScheduler.cancel(context)
|
WallpaperScheduler.cancel(context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,5 @@ class WallpaperAlarmReceiver : BroadcastReceiver() {
|
|||||||
WallpaperRotationWorker.enqueue(context)
|
WallpaperRotationWorker.enqueue(context)
|
||||||
}
|
}
|
||||||
WallpaperScheduler.ensureScheduled(context)
|
WallpaperScheduler.ensureScheduled(context)
|
||||||
WallpaperRotationService.rescheduleRunningService()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
|
|||||||
private val io = Executors.newSingleThreadExecutor()
|
private val io = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
@Command fun getState(invoke: Invoke) {
|
@Command fun getState(invoke: Invoke) {
|
||||||
if (WallpaperStore.enabled(activity)) runCatching { WallpaperRotationService.start(activity) }
|
|
||||||
io.execute { invoke.resolve(WallpaperStore.state(activity)) }
|
io.execute { invoke.resolve(WallpaperStore.state(activity)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +140,10 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
|
|||||||
try {
|
try {
|
||||||
val args = invoke.parseArgs(IntervalArgs::class.java)
|
val args = invoke.parseArgs(IntervalArgs::class.java)
|
||||||
WallpaperStore.setInterval(activity, args.minutes)
|
WallpaperStore.setInterval(activity, args.minutes)
|
||||||
if (args.minutes > 0) WallpaperRotationService.restart(activity) else WallpaperRotationService.stop(activity)
|
WallpaperScheduler.cancel(activity)
|
||||||
|
ScreenOnRotationService.stop(activity)
|
||||||
|
if (args.minutes == -1) ScreenOnRotationService.start(activity)
|
||||||
|
else if (args.minutes > 0) WallpaperScheduler.scheduleNext(activity)
|
||||||
invoke.resolve(WallpaperStore.state(activity))
|
invoke.resolve(WallpaperStore.state(activity))
|
||||||
} catch (error: Exception) { invoke.reject(error.message ?: "Wechselintervall konnte nicht gespeichert werden") }
|
} catch (error: Exception) { invoke.reject(error.message ?: "Wechselintervall konnte nicht gespeichert werden") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package de.wechselbild.wallpaper
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.Constraints
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
@@ -9,9 +10,9 @@ import androidx.work.WorkerParameters
|
|||||||
|
|
||||||
class WallpaperRotationWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
|
class WallpaperRotationWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
if (!WallpaperStore.enabled(applicationContext)) return Result.success()
|
if (!WallpaperStore.timedEnabled(applicationContext)) return Result.success()
|
||||||
if (WallpaperStore.applyNext(applicationContext)) return Result.success()
|
WallpaperStore.applyNext(applicationContext, automatic = true)
|
||||||
return if (runAttemptCount < 2) Result.retry() else Result.failure()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -21,7 +22,9 @@ class WallpaperRotationWorker(context: Context, parameters: WorkerParameters) :
|
|||||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||||
WORK_NAME,
|
WORK_NAME,
|
||||||
ExistingWorkPolicy.KEEP,
|
ExistingWorkPolicy.KEEP,
|
||||||
OneTimeWorkRequestBuilder<WallpaperRotationWorker>().build(),
|
OneTimeWorkRequestBuilder<WallpaperRotationWorker>()
|
||||||
|
.setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.app.AlarmManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
|
||||||
object WallpaperScheduler {
|
object WallpaperScheduler {
|
||||||
@@ -13,7 +12,7 @@ object WallpaperScheduler {
|
|||||||
private const val REQUEST_CODE = 4218
|
private const val REQUEST_CODE = 4218
|
||||||
|
|
||||||
fun ensureScheduled(context: Context) {
|
fun ensureScheduled(context: Context) {
|
||||||
if (!WallpaperStore.enabled(context)) {
|
if (!WallpaperStore.timedEnabled(context)) {
|
||||||
cancel(context)
|
cancel(context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -23,11 +22,6 @@ object WallpaperScheduler {
|
|||||||
if (storedTrigger <= now || storedTrigger > now + interval) scheduleNext(context)
|
if (storedTrigger <= now || storedTrigger > now + interval) scheduleNext(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remainingDelay(context: Context): Long {
|
|
||||||
val trigger = preferences(context).getLong(KEY_NEXT_TRIGGER, 0L)
|
|
||||||
return (trigger - SystemClock.elapsedRealtime()).coerceAtLeast(0L)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun claimIfDue(context: Context): Boolean {
|
fun claimIfDue(context: Context): Boolean {
|
||||||
val preferences = preferences(context)
|
val preferences = preferences(context)
|
||||||
@@ -47,11 +41,8 @@ object WallpaperScheduler {
|
|||||||
val trigger = SystemClock.elapsedRealtime() + minutes * 60_000L
|
val trigger = SystemClock.elapsedRealtime() + minutes * 60_000L
|
||||||
val alarmManager = context.getSystemService(AlarmManager::class.java)
|
val alarmManager = context.getSystemService(AlarmManager::class.java)
|
||||||
val operation = operation(context)
|
val operation = operation(context)
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
val window = (minutes * 60_000L / 10).coerceIn(60_000L, 15 * 60_000L)
|
||||||
alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
|
alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME, trigger, window, operation)
|
||||||
} else {
|
|
||||||
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
|
|
||||||
}
|
|
||||||
preferences(context).edit().putLong(KEY_NEXT_TRIGGER, trigger).commit()
|
preferences(context).edit().putLong(KEY_NEXT_TRIGGER, trigger).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ object WallpaperStore {
|
|||||||
private const val KEY_CURRENT_ID = "current_entry_id"
|
private const val KEY_CURRENT_ID = "current_entry_id"
|
||||||
private const val KEY_INTERVAL = "interval_minutes"
|
private const val KEY_INTERVAL = "interval_minutes"
|
||||||
private const val CROP_PREFIX = "crop_"
|
private const val CROP_PREFIX = "crop_"
|
||||||
|
private const val INVALID_PREFIX = "invalid_"
|
||||||
|
private const val RENDER_CACHE_MAX_BYTES = 128L * 1024 * 1024
|
||||||
|
private const val THUMBNAIL_CACHE_MAX_BYTES = 64L * 1024 * 1024
|
||||||
private const val HOME_PREVIEW_LIMIT = 12
|
private const val HOME_PREVIEW_LIMIT = 12
|
||||||
private val thumbnailCache = LruCache<String, String>(48)
|
private val thumbnailCache = LruCache<String, String>(48)
|
||||||
|
|
||||||
@@ -47,6 +50,8 @@ object WallpaperStore {
|
|||||||
private fun immichEntries(context: Context) = File(immichRoot(context), "entries").apply { mkdirs() }
|
private fun immichEntries(context: Context) = File(immichRoot(context), "entries").apply { mkdirs() }
|
||||||
private fun immichPreviews(context: Context) = File(immichRoot(context), "previews").apply { mkdirs() }
|
private fun immichPreviews(context: Context) = File(immichRoot(context), "previews").apply { mkdirs() }
|
||||||
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
private fun renderedDirectory(context: Context) = File(context.cacheDir, "rendered-wallpapers").apply { mkdirs() }
|
||||||
|
private fun thumbnailDirectory(context: Context) = File(context.cacheDir, "wallpaper-thumbnails").apply { mkdirs() }
|
||||||
|
|
||||||
private fun serverKey(serverUrl: String): String = MessageDigest.getInstance("SHA-256")
|
private fun serverKey(serverUrl: String): String = MessageDigest.getInstance("SHA-256")
|
||||||
.digest(serverUrl.toByteArray(Charsets.UTF_8)).take(6).joinToString("") { "%02x".format(it) }
|
.digest(serverUrl.toByteArray(Charsets.UTF_8)).take(6).joinToString("") { "%02x".format(it) }
|
||||||
@@ -92,7 +97,9 @@ object WallpaperStore {
|
|||||||
return if (preferences.contains(KEY_INTERVAL)) preferences.getInt(KEY_INTERVAL, 0)
|
return if (preferences.contains(KEY_INTERVAL)) preferences.getInt(KEY_INTERVAL, 0)
|
||||||
else if (preferences.getBoolean("enabled", false)) 30 else 0
|
else if (preferences.getBoolean("enabled", false)) 30 else 0
|
||||||
}
|
}
|
||||||
fun enabled(context: Context) = intervalMinutes(context) > 0
|
fun enabled(context: Context) = intervalMinutes(context) != 0
|
||||||
|
fun timedEnabled(context: Context) = intervalMinutes(context) > 0
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -102,8 +109,8 @@ object WallpaperStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setInterval(context: Context, minutes: Int) {
|
fun setInterval(context: Context, minutes: Int) {
|
||||||
require(minutes == 0 || minutes in 5..720) { "Ungültiges Wechselintervall" }
|
require(minutes == -1 || minutes == 0 || minutes in 5..720) { "Ungültiges Wechselintervall" }
|
||||||
prefs(context).edit().putInt(KEY_INTERVAL, minutes).putBoolean("enabled", minutes > 0).apply()
|
prefs(context).edit().putInt(KEY_INTERVAL, minutes).putBoolean("enabled", minutes != 0).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -124,7 +131,7 @@ object WallpaperStore {
|
|||||||
previewItems.forEach { ids.put(it.id) }
|
previewItems.forEach { ids.put(it.id) }
|
||||||
put("imageIds", ids)
|
put("imageIds", ids)
|
||||||
val previews = JSArray()
|
val previews = JSArray()
|
||||||
if (includePreviews) previewItems.forEach { previews.put(thumbnailDataUrl(it.preview)) }
|
if (includePreviews) previewItems.forEach { previews.put(thumbnailDataUrl(context, it.preview)) }
|
||||||
put("imageUrls", previews)
|
put("imageUrls", previews)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +171,7 @@ object WallpaperStore {
|
|||||||
val crop = crop(context, entry)
|
val crop = crop(context, entry)
|
||||||
return JSObject().apply {
|
return JSObject().apply {
|
||||||
put("id", entry.id)
|
put("id", entry.id)
|
||||||
put("url", thumbnailDataUrl(entry.preview))
|
put("url", thumbnailDataUrl(context, entry.preview))
|
||||||
put("selected", selected)
|
put("selected", selected)
|
||||||
put("cropMode", crop.mode)
|
put("cropMode", crop.mode)
|
||||||
put("cropZoom", crop.zoom)
|
put("cropZoom", crop.zoom)
|
||||||
@@ -294,9 +301,16 @@ object WallpaperStore {
|
|||||||
return BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample; inPreferredConfig = Bitmap.Config.ARGB_8888 })
|
return BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample; inPreferredConfig = Bitmap.Config.ARGB_8888 })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun thumbnailDataUrl(file: File): String {
|
private fun thumbnailDataUrl(context: Context, file: File): String {
|
||||||
val cacheKey = "${file.absolutePath}:${file.lastModified()}:${file.length()}"
|
val cacheKey = "${file.absolutePath}:${file.lastModified()}:${file.length()}"
|
||||||
thumbnailCache.get(cacheKey)?.let { return it }
|
thumbnailCache.get(cacheKey)?.let { return it }
|
||||||
|
val cached = File(thumbnailDirectory(context), "${digest(cacheKey)}.jpg")
|
||||||
|
if (cached.isFile) {
|
||||||
|
cached.setLastModified(System.currentTimeMillis())
|
||||||
|
val result = "data:image/jpeg;base64," + Base64.encodeToString(cached.readBytes(), Base64.NO_WRAP)
|
||||||
|
thumbnailCache.put(cacheKey, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
BitmapFactory.decodeFile(file.absolutePath, bounds)
|
BitmapFactory.decodeFile(file.absolutePath, bounds)
|
||||||
var sample = 1
|
var sample = 1
|
||||||
@@ -304,7 +318,9 @@ object WallpaperStore {
|
|||||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample }) ?: return ""
|
val bitmap = BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample }) ?: return ""
|
||||||
val result = ByteArrayOutputStream().use { out ->
|
val result = ByteArrayOutputStream().use { out ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 76, out); bitmap.recycle()
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 76, out); bitmap.recycle()
|
||||||
"data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
|
val bytes = out.toByteArray()
|
||||||
|
runCatching { cached.writeBytes(bytes); trimDirectory(thumbnailDirectory(context), THUMBNAIL_CACHE_MAX_BYTES, cached) }
|
||||||
|
"data:image/jpeg;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||||
}
|
}
|
||||||
thumbnailCache.put(cacheKey, result)
|
thumbnailCache.put(cacheKey, result)
|
||||||
return result
|
return result
|
||||||
@@ -314,15 +330,52 @@ object WallpaperStore {
|
|||||||
thumbnailCache.remove("${file.absolutePath}:${file.lastModified()}:${file.length()}")
|
thumbnailCache.remove("${file.absolutePath}:${file.lastModified()}:${file.length()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun digest(value: String): String = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(value.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
private fun renderedFile(context: Context, entry: Entry): File {
|
||||||
|
val metrics = context.resources.displayMetrics
|
||||||
|
val sourceVersion = when (entry) {
|
||||||
|
is Entry.Local -> "${entry.file.lastModified()}:${entry.file.length()}"
|
||||||
|
is Entry.Immich -> entry.assetId
|
||||||
|
}
|
||||||
|
val key = "${entry.id}:$sourceVersion:${metrics.widthPixels}x${metrics.heightPixels}:${prefs(context).getString(cropKey(entry.id), "")}"
|
||||||
|
return File(renderedDirectory(context), "${digest(key)}.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sourceVersion(entry: Entry): String = when (entry) {
|
||||||
|
is Entry.Local -> "${entry.file.lastModified()}:${entry.file.length()}"
|
||||||
|
is Entry.Immich -> entry.assetId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validImage(file: File): Boolean {
|
||||||
|
if (!file.isFile || file.length() <= 0) return false
|
||||||
|
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
|
BitmapFactory.decodeFile(file.absolutePath, bounds)
|
||||||
|
return bounds.outWidth > 0 && bounds.outHeight > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trimDirectory(directory: File, maxBytes: Long, protected: File) {
|
||||||
|
val files = directory.listFiles()?.filter { it.isFile }?.sortedBy { it.lastModified() } ?: return
|
||||||
|
var bytes = files.sumOf { it.length() }
|
||||||
|
for (file in files) {
|
||||||
|
if (bytes <= maxBytes) break
|
||||||
|
if (file != protected) {
|
||||||
|
val length = file.length()
|
||||||
|
if (file.delete()) bytes -= length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun applyNext(context: Context): Boolean {
|
fun applyNext(context: Context, automatic: Boolean = false): Boolean {
|
||||||
val items = entries(context)
|
val items = entries(context)
|
||||||
if (items.isEmpty()) return false
|
if (items.isEmpty()) return false
|
||||||
val previous = currentIndex(context, items)
|
val previous = currentIndex(context, items)
|
||||||
val candidates = if (shuffle(context) && items.size > 1) {
|
val candidates = if (shuffle(context) && items.size > 1) {
|
||||||
items.indices.filter { it != previous }.shuffled() + previous
|
items.indices.filter { it != previous }.shuffled() + previous
|
||||||
} else (1..items.size).map { (previous + it).mod(items.size) }
|
} else (1..items.size).map { (previous + it).mod(items.size) }
|
||||||
return applyCandidates(context, items, candidates)
|
return applyCandidates(context, items, candidates, automatic)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -330,37 +383,58 @@ object WallpaperStore {
|
|||||||
val items = entries(context)
|
val items = entries(context)
|
||||||
val index = items.indexOfFirst { it.id == id }
|
val index = items.indexOfFirst { it.id == id }
|
||||||
if (index < 0) return false
|
if (index < 0) return false
|
||||||
return applyCandidates(context, items, listOf(index))
|
return applyCandidates(context, items, listOf(index), automatic = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyCandidates(context: Context, items: List<Entry>, candidates: List<Int>): Boolean {
|
private fun applyCandidates(context: Context, items: List<Entry>, candidates: List<Int>, automatic: Boolean): Boolean {
|
||||||
val unavailableServers = mutableSetOf<String>()
|
val unavailableServers = mutableSetOf<String>()
|
||||||
for (index in candidates) {
|
for (index in candidates) {
|
||||||
val entry = items[index]
|
val entry = items[index]
|
||||||
val sourceFile = when (entry) {
|
val renderedFile = renderedFile(context, entry)
|
||||||
is Entry.Local -> entry.file
|
var rendered = if (validImage(renderedFile)) {
|
||||||
is Entry.Immich -> {
|
renderedFile.setLastModified(System.currentTimeMillis())
|
||||||
val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, entry.serverUrl !in unavailableServers)
|
BitmapFactory.decodeFile(renderedFile.absolutePath)
|
||||||
if (source == null) {
|
} else null
|
||||||
unavailableServers.add(entry.serverUrl)
|
if (rendered == null) {
|
||||||
continue
|
renderedFile.delete()
|
||||||
|
if (prefs(context).getString(INVALID_PREFIX + entry.id, null) == sourceVersion(entry)) continue
|
||||||
|
val sourceFile = when (entry) {
|
||||||
|
is Entry.Local -> entry.file
|
||||||
|
is Entry.Immich -> {
|
||||||
|
val allowNetwork = entry.serverUrl !in unavailableServers && (!automatic || ImmichClient.automaticDownloadsAllowed(context))
|
||||||
|
val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, allowNetwork)
|
||||||
|
if (source == null) {
|
||||||
|
if (allowNetwork) unavailableServers.add(entry.serverUrl)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
source
|
||||||
}
|
}
|
||||||
source
|
|
||||||
}
|
}
|
||||||
|
val bitmap = decodeForScreen(context, sourceFile)
|
||||||
|
if (bitmap == null) {
|
||||||
|
prefs(context).edit().putString(INVALID_PREFIX + entry.id, sourceVersion(entry)).apply()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val generated = runCatching { renderForScreen(context, bitmap, crop(context, entry)) }.getOrNull()
|
||||||
|
bitmap.recycle()
|
||||||
|
if (generated == null) continue
|
||||||
|
runCatching {
|
||||||
|
renderedFile.outputStream().use { generated.compress(Bitmap.CompressFormat.JPEG, 92, it) }
|
||||||
|
trimDirectory(renderedDirectory(context), RENDER_CACHE_MAX_BYTES, renderedFile)
|
||||||
|
}
|
||||||
|
rendered = generated
|
||||||
}
|
}
|
||||||
val bitmap = decodeForScreen(context, sourceFile) ?: continue
|
val wallpaper = rendered ?: continue
|
||||||
val rendered = runCatching { renderForScreen(context, bitmap, crop(context, entry)) }.getOrNull()
|
|
||||||
if (rendered == null) { bitmap.recycle(); continue }
|
|
||||||
try {
|
try {
|
||||||
val manager = WallpaperManager.getInstance(context)
|
val manager = WallpaperManager.getInstance(context)
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 24 && lockOnly(context)) manager.setBitmap(rendered, null, true, WallpaperManager.FLAG_LOCK)
|
if (android.os.Build.VERSION.SDK_INT >= 24 && lockOnly(context)) manager.setBitmap(wallpaper, null, true, WallpaperManager.FLAG_LOCK)
|
||||||
else manager.setBitmap(rendered)
|
else manager.setBitmap(wallpaper)
|
||||||
prefs(context).edit().putString(KEY_CURRENT_ID, entry.id).putInt(KEY_INDEX, index).apply()
|
prefs(context).edit().remove(INVALID_PREFIX + entry.id).putString(KEY_CURRENT_ID, entry.id).putInt(KEY_INDEX, index).apply()
|
||||||
return true
|
return true
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Try the next usable entry without changing the current selection.
|
// Try the next usable entry without changing the current selection.
|
||||||
} finally {
|
} finally {
|
||||||
rendered.recycle(); bitmap.recycle()
|
wallpaper.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow ist aktiv</string>
|
<string name="wallpaper_service_title">WallpaperFlow ist aktiv</string>
|
||||||
<string name="wallpaper_service_text">Das Motiv wechselt nach dem eingestellten Intervall.</string>
|
<string name="wallpaper_service_text">Das nächste Motiv wird beim Ausschalten des Displays vorbereitet.</string>
|
||||||
<string name="wallpaper_channel_name">Automatischer Bildwechsel</string>
|
<string name="wallpaper_channel_name">Bildwechsel bei Display-Aktivierung</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow está activo</string>
|
<string name="wallpaper_service_title">WallpaperFlow está activo</string>
|
||||||
<string name="wallpaper_service_text">El fondo cambia según el intervalo configurado.</string>
|
<string name="wallpaper_service_text">El fondo cambia cuando se enciende la pantalla.</string>
|
||||||
<string name="wallpaper_channel_name">Rotación automática de fondos</string>
|
<string name="wallpaper_channel_name">Rotación al encender la pantalla</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow est actif</string>
|
<string name="wallpaper_service_title">WallpaperFlow est actif</string>
|
||||||
<string name="wallpaper_service_text">Le fond d’écran change selon l’intervalle défini.</string>
|
<string name="wallpaper_service_text">Le fond d’écran change lorsque l’écran s’allume.</string>
|
||||||
<string name="wallpaper_channel_name">Rotation automatique des fonds d’écran</string>
|
<string name="wallpaper_channel_name">Rotation à l’allumage de l’écran</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow è attivo</string>
|
<string name="wallpaper_service_title">WallpaperFlow è attivo</string>
|
||||||
<string name="wallpaper_service_text">Lo sfondo cambia all’intervallo configurato.</string>
|
<string name="wallpaper_service_text">Lo sfondo cambia quando si accende lo schermo.</string>
|
||||||
<string name="wallpaper_channel_name">Rotazione automatica degli sfondi</string>
|
<string name="wallpaper_channel_name">Rotazione all’accensione dello schermo</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow は実行中です</string>
|
<string name="wallpaper_service_title">WallpaperFlow は実行中です</string>
|
||||||
<string name="wallpaper_service_text">設定した間隔で壁紙が切り替わります。</string>
|
<string name="wallpaper_service_text">画面が点灯すると壁紙が切り替わります。</string>
|
||||||
<string name="wallpaper_channel_name">壁紙の自動切り替え</string>
|
<string name="wallpaper_channel_name">画面点灯時の壁紙切り替え</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow 실행 중</string>
|
<string name="wallpaper_service_title">WallpaperFlow 실행 중</string>
|
||||||
<string name="wallpaper_service_text">설정한 간격에 따라 배경화면이 변경됩니다.</string>
|
<string name="wallpaper_service_text">화면이 켜질 때 배경화면이 변경됩니다.</string>
|
||||||
<string name="wallpaper_channel_name">자동 배경화면 변경</string>
|
<string name="wallpaper_channel_name">화면 켜짐 시 배경화면 변경</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow is actief</string>
|
<string name="wallpaper_service_title">WallpaperFlow is actief</string>
|
||||||
<string name="wallpaper_service_text">De achtergrond wisselt volgens het ingestelde interval.</string>
|
<string name="wallpaper_service_text">De achtergrond wisselt wanneer het scherm aangaat.</string>
|
||||||
<string name="wallpaper_channel_name">Automatisch achtergronden wisselen</string>
|
<string name="wallpaper_channel_name">Wisselen bij scherminschakeling</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow jest aktywny</string>
|
<string name="wallpaper_service_title">WallpaperFlow jest aktywny</string>
|
||||||
<string name="wallpaper_service_text">Tapeta zmienia się zgodnie z ustawionym interwałem.</string>
|
<string name="wallpaper_service_text">Tapeta zmienia się po włączeniu ekranu.</string>
|
||||||
<string name="wallpaper_channel_name">Automatyczna zmiana tapety</string>
|
<string name="wallpaper_channel_name">Zmiana po włączeniu ekranu</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow está ativo</string>
|
<string name="wallpaper_service_title">WallpaperFlow está ativo</string>
|
||||||
<string name="wallpaper_service_text">O fundo muda de acordo com o intervalo definido.</string>
|
<string name="wallpaper_service_text">O fundo muda quando o ecrã é ligado.</string>
|
||||||
<string name="wallpaper_channel_name">Rotação automática de fundos</string>
|
<string name="wallpaper_channel_name">Rotação ao ligar o ecrã</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow 正在运行</string>
|
<string name="wallpaper_service_title">WallpaperFlow 正在运行</string>
|
||||||
<string name="wallpaper_service_text">壁纸会按照设定的间隔更换。</string>
|
<string name="wallpaper_service_text">屏幕亮起时会更换壁纸。</string>
|
||||||
<string name="wallpaper_channel_name">自动更换壁纸</string>
|
<string name="wallpaper_channel_name">亮屏时更换壁纸</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="wallpaper_service_title">WallpaperFlow is active</string>
|
<string name="wallpaper_service_title">WallpaperFlow is active</string>
|
||||||
<string name="wallpaper_service_text">The wallpaper changes at the configured interval.</string>
|
<string name="wallpaper_service_text">The next wallpaper is prepared when the screen turns off.</string>
|
||||||
<string name="wallpaper_channel_name">Automatic wallpaper rotation</string>
|
<string name="wallpaper_channel_name">Screen-on wallpaper rotation</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub(crate) async fn set_setting<R: Runtime>(
|
|||||||
app.wallpaper().set_setting(SettingRequest { name, value })
|
app.wallpaper().set_setting(SettingRequest { name, value })
|
||||||
}
|
}
|
||||||
#[command]
|
#[command]
|
||||||
pub(crate) async fn set_interval<R: Runtime>(app: AppHandle<R>, minutes: usize) -> Result<WallpaperState> {
|
pub(crate) async fn set_interval<R: Runtime>(app: AppHandle<R>, minutes: i32) -> Result<WallpaperState> {
|
||||||
app.wallpaper().set_interval(IntervalRequest { minutes })
|
app.wallpaper().set_interval(IntervalRequest { minutes })
|
||||||
}
|
}
|
||||||
#[command]
|
#[command]
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ impl<R: Runtime> Wallpaper<R> {
|
|||||||
pub fn set_interval(&self, payload: IntervalRequest) -> crate::Result<WallpaperState> {
|
pub fn set_interval(&self, payload: IntervalRequest) -> crate::Result<WallpaperState> {
|
||||||
let mut state = Self::demo();
|
let mut state = Self::demo();
|
||||||
state.interval_minutes = payload.minutes;
|
state.interval_minutes = payload.minutes;
|
||||||
state.enabled = payload.minutes > 0;
|
state.enabled = payload.minutes != 0;
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> {
|
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct WallpaperState {
|
pub struct WallpaperState {
|
||||||
pub image_count: usize,
|
pub image_count: usize,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub interval_minutes: usize,
|
pub interval_minutes: i32,
|
||||||
pub shuffle: bool,
|
pub shuffle: bool,
|
||||||
pub lock_screen_only: bool,
|
pub lock_screen_only: bool,
|
||||||
pub current_index: usize,
|
pub current_index: usize,
|
||||||
@@ -24,7 +24,7 @@ pub struct SettingRequest {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct IntervalRequest {
|
pub struct IntervalRequest {
|
||||||
pub minutes: usize,
|
pub minutes: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
+7
-8
@@ -14,9 +14,10 @@ function SettingRow({ icon, label, value, onChange }: { icon: React.ReactNode; l
|
|||||||
return <div className="setting-row"><div className="setting-icon">{icon}</div><span>{label}</span><Switch checked={value} onChange={onChange} label={label} /></div>;
|
return <div className="setting-row"><div className="setting-icon">{icon}</div><span>{label}</span><Switch checked={value} onChange={onChange} label={label} /></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IntervalRow({ value, onChange, label, paused, everyMinutes }: { value: number; onChange: (value: number) => void; label: string; paused: string; everyMinutes: (minutes: number) => string }) {
|
function IntervalRow({ value, onChange, label, paused, screenOn, everyMinutes }: { value: number; onChange: (value: number) => void; label: string; paused: string; screenOn: string; everyMinutes: (minutes: number) => string }) {
|
||||||
return <label className="setting-row interval-row"><div className="setting-icon"><Smartphone /></div><span>{label}</span><select value={value} onChange={event => onChange(Number(event.target.value))} aria-label={label}>
|
return <label className="setting-row interval-row"><div className="setting-icon"><Smartphone /></div><span>{label}</span><select value={value} onChange={event => onChange(Number(event.target.value))} aria-label={label}>
|
||||||
<option value={0}>{paused}</option>
|
<option value={0}>{paused}</option>
|
||||||
|
<option value={-1}>{screenOn}</option>
|
||||||
{[5, 15, 30, 60, 180, 360, 720].map(minutes => <option value={minutes} key={minutes}>{everyMinutes(minutes)}</option>)}
|
{[5, 15, 30, 60, 180, 360, 720].map(minutes => <option value={minutes} key={minutes}>{everyMinutes(minutes)}</option>)}
|
||||||
</select></label>;
|
</select></label>;
|
||||||
}
|
}
|
||||||
@@ -54,11 +55,9 @@ export default function App() {
|
|||||||
const refresh = () => { void getState().then(setState).catch(() => undefined); };
|
const refresh = () => { void getState().then(setState).catch(() => undefined); };
|
||||||
refresh();
|
refresh();
|
||||||
const onVisibilityChange = () => { if (!document.hidden) refresh(); };
|
const onVisibilityChange = () => { if (!document.hidden) refresh(); };
|
||||||
const refreshInterval = window.setInterval(() => { if (!document.hidden) refresh(); }, 15000);
|
|
||||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||||
window.addEventListener("focus", refresh);
|
window.addEventListener("focus", refresh);
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(refreshInterval);
|
|
||||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||||
window.removeEventListener("focus", refresh);
|
window.removeEventListener("focus", refresh);
|
||||||
};
|
};
|
||||||
@@ -118,11 +117,11 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateInterval(minutes: number) {
|
async function updateInterval(minutes: number) {
|
||||||
setState(prev => ({ ...prev, intervalMinutes: minutes, enabled: minutes > 0 }));
|
setState(prev => ({ ...prev, intervalMinutes: minutes, enabled: minutes !== 0 }));
|
||||||
try {
|
try {
|
||||||
const saved = await setIntervalMinutes(minutes);
|
const saved = await setIntervalMinutes(minutes);
|
||||||
setState(saved);
|
setState(saved);
|
||||||
setNotice(minutes > 0 ? t.intervalSaved : t.automaticDisabled);
|
setNotice(minutes !== 0 ? t.intervalSaved : t.automaticDisabled);
|
||||||
}
|
}
|
||||||
catch { setNotice(t.androidOnly); }
|
catch { setNotice(t.androidOnly); }
|
||||||
}
|
}
|
||||||
@@ -249,7 +248,7 @@ export default function App() {
|
|||||||
setNotice(it.importing);
|
setNotice(it.importing);
|
||||||
const poll = window.setInterval(() => {
|
const poll = window.setInterval(() => {
|
||||||
void getImmichImportProgress().then(setImmichImportProgress).catch(() => undefined);
|
void getImmichImportProgress().then(setImmichImportProgress).catch(() => undefined);
|
||||||
}, 250);
|
}, 750);
|
||||||
try {
|
try {
|
||||||
setState(await importImmichAssets([...immichSelected]));
|
setState(await importImmichAssets([...immichSelected]));
|
||||||
setImmichSelected(new Set());
|
setImmichSelected(new Set());
|
||||||
@@ -408,7 +407,7 @@ export default function App() {
|
|||||||
<div className="hero-meta"><span><Sparkles size={16} /> {t.currentImage}</span><button onClick={next} disabled={busy}>{t.nextImage} <ChevronRight size={18} /></button></div>
|
<div className="hero-meta"><span><Sparkles size={16} /> {t.currentImage}</span><button onClick={next} disabled={busy}>{t.nextImage} <ChevronRight size={18} /></button></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} everyMinutes={t.everyMinutes} />
|
<IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} screenOn={t.screenOn} everyMinutes={t.everyMinutes} />
|
||||||
|
|
||||||
<button className="primary" onClick={choose} disabled={busy}><Images />{busy ? t.pleaseWait : t.selectImages}</button>
|
<button className="primary" onClick={choose} disabled={busy}><Images />{busy ? t.pleaseWait : t.selectImages}</button>
|
||||||
|
|
||||||
@@ -421,7 +420,7 @@ export default function App() {
|
|||||||
</> : tab === "settings" ? <section className="settings-page">
|
</> : tab === "settings" ? <section className="settings-page">
|
||||||
<h2>{t.settings}</h2><p>{t.settingsIntro}</p>
|
<h2>{t.settings}</h2><p>{t.settingsIntro}</p>
|
||||||
<label className="language-setting"><div className="setting-icon"><Languages /></div><div><strong>{t.language}</strong><span>{languageNames[language]}</span></div><select className="locale-select" value={language} onChange={event => setLanguage(event.target.value as Language)} aria-label={t.language}>{languages.map(locale => <option value={locale} key={locale}>{languageNames[locale]}</option>)}</select></label>
|
<label className="language-setting"><div className="setting-icon"><Languages /></div><div><strong>{t.language}</strong><span>{languageNames[language]}</span></div><select className="locale-select" value={language} onChange={event => setLanguage(event.target.value as Language)} aria-label={t.language}>{languages.map(locale => <option value={locale} key={locale}>{languageNames[locale]}</option>)}</select></label>
|
||||||
<div className="settings-list"><IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} everyMinutes={t.everyMinutes} /><SettingRow icon={<Shuffle />} label={t.shuffle} value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label={t.lockScreenOnly} value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div>
|
<div className="settings-list"><IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} screenOn={t.screenOn} everyMinutes={t.everyMinutes} /><SettingRow icon={<Shuffle />} label={t.shuffle} value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label={t.lockScreenOnly} value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div>
|
||||||
<section className="immich-settings">
|
<section className="immich-settings">
|
||||||
<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 ? <>
|
||||||
|
|||||||
+11
-11
@@ -9,7 +9,7 @@ export const languageNames: Record<Language, string> = {
|
|||||||
const en = {
|
const en = {
|
||||||
automaticActive: "Your wallpapers change automatically", automaticPaused: "Automatic rotation is paused",
|
automaticActive: "Your wallpapers change automatically", automaticPaused: "Automatic rotation is paused",
|
||||||
settings: "Settings", currentWallpaper: "Current wallpaper", currentImage: "Current image", nextImage: "Next image",
|
settings: "Settings", currentWallpaper: "Current wallpaper", currentImage: "Current image", nextImage: "Next image",
|
||||||
changeInterval: "Change image", paused: "Paused", selectImages: "Select images", pleaseWait: "Please wait …",
|
changeInterval: "Change image", paused: "Paused", screenOn: "When screen turns on", selectImages: "Select images", pleaseWait: "Please wait …",
|
||||||
collection: "My collection", image: "image", images: "images", motif: "Image", noImages: "No images selected yet",
|
collection: "My collection", image: "image", images: "images", motif: "Image", noImages: "No images selected yet",
|
||||||
collectionHint: "Tap the image count to open your gallery.", shuffle: "Shuffle order", lockScreenOnly: "Lock screen only",
|
collectionHint: "Tap the image count to open your gallery.", shuffle: "Shuffle order", lockScreenOnly: "Lock screen only",
|
||||||
settingsIntro: "Choose how WallpaperFlow works in the background.", language: "Language", noImageLimit: "No image limit",
|
settingsIntro: "Choose how WallpaperFlow works in the background.", language: "Language", noImageLimit: "No image limit",
|
||||||
@@ -35,7 +35,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
de: {
|
de: {
|
||||||
automaticActive: "Deine Motive wechseln automatisch", automaticPaused: "Automatischer Wechsel ist pausiert",
|
automaticActive: "Deine Motive wechseln automatisch", automaticPaused: "Automatischer Wechsel ist pausiert",
|
||||||
settings: "Einstellungen", currentWallpaper: "Aktuelles Hintergrundbild", currentImage: "Aktuelles Motiv", nextImage: "Nächstes Motiv",
|
settings: "Einstellungen", currentWallpaper: "Aktuelles Hintergrundbild", currentImage: "Aktuelles Motiv", nextImage: "Nächstes Motiv",
|
||||||
changeInterval: "Bild wechseln", paused: "Pausiert", selectImages: "Bilder auswählen", pleaseWait: "Bitte warten …",
|
changeInterval: "Bild wechseln", paused: "Pausiert", screenOn: "Beim Einschalten des Displays", selectImages: "Bilder auswählen", pleaseWait: "Bitte warten …",
|
||||||
collection: "Meine Sammlung", image: "Bild", images: "Bilder", motif: "Motiv", noImages: "Noch keine Bilder ausgewählt",
|
collection: "Meine Sammlung", image: "Bild", images: "Bilder", motif: "Motiv", noImages: "Noch keine Bilder ausgewählt",
|
||||||
collectionHint: "Tippe auf die Bildanzahl, um deine Galerie zu öffnen.", shuffle: "Zufällige Reihenfolge", lockScreenOnly: "Nur Sperrbildschirm",
|
collectionHint: "Tippe auf die Bildanzahl, um deine Galerie zu öffnen.", shuffle: "Zufällige Reihenfolge", lockScreenOnly: "Nur Sperrbildschirm",
|
||||||
settingsIntro: "Lege fest, wie WallpaperFlow im Hintergrund arbeitet.", language: "Sprache", noImageLimit: "Ohne Bilderlimit",
|
settingsIntro: "Lege fest, wie WallpaperFlow im Hintergrund arbeitet.", language: "Sprache", noImageLimit: "Ohne Bilderlimit",
|
||||||
@@ -56,7 +56,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
fr: {
|
fr: {
|
||||||
automaticActive: "Vos fonds d’écran changent automatiquement", automaticPaused: "La rotation automatique est en pause",
|
automaticActive: "Vos fonds d’écran changent automatiquement", automaticPaused: "La rotation automatique est en pause",
|
||||||
settings: "Paramètres", currentWallpaper: "Fond d’écran actuel", currentImage: "Image actuelle", nextImage: "Image suivante",
|
settings: "Paramètres", currentWallpaper: "Fond d’écran actuel", currentImage: "Image actuelle", nextImage: "Image suivante",
|
||||||
changeInterval: "Changer l’image", paused: "En pause", selectImages: "Sélectionner des images", pleaseWait: "Veuillez patienter…",
|
changeInterval: "Changer l’image", paused: "En pause", screenOn: "À l’allumage de l’écran", selectImages: "Sélectionner des images", pleaseWait: "Veuillez patienter…",
|
||||||
collection: "Ma collection", image: "image", images: "images", motif: "Image", noImages: "Aucune image sélectionnée",
|
collection: "Ma collection", image: "image", images: "images", motif: "Image", noImages: "Aucune image sélectionnée",
|
||||||
collectionHint: "Touchez le nombre d’images pour ouvrir votre galerie.", shuffle: "Ordre aléatoire", lockScreenOnly: "Écran de verrouillage uniquement",
|
collectionHint: "Touchez le nombre d’images pour ouvrir votre galerie.", shuffle: "Ordre aléatoire", lockScreenOnly: "Écran de verrouillage uniquement",
|
||||||
settingsIntro: "Choisissez comment WallpaperFlow fonctionne en arrière-plan.", language: "Langue", noImageLimit: "Aucune limite d’images",
|
settingsIntro: "Choisissez comment WallpaperFlow fonctionne en arrière-plan.", language: "Langue", noImageLimit: "Aucune limite d’images",
|
||||||
@@ -77,7 +77,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
es: {
|
es: {
|
||||||
automaticActive: "Tus fondos cambian automáticamente", automaticPaused: "La rotación automática está en pausa",
|
automaticActive: "Tus fondos cambian automáticamente", automaticPaused: "La rotación automática está en pausa",
|
||||||
settings: "Ajustes", currentWallpaper: "Fondo actual", currentImage: "Imagen actual", nextImage: "Imagen siguiente",
|
settings: "Ajustes", currentWallpaper: "Fondo actual", currentImage: "Imagen actual", nextImage: "Imagen siguiente",
|
||||||
changeInterval: "Cambiar imagen", paused: "En pausa", selectImages: "Seleccionar imágenes", pleaseWait: "Espera…",
|
changeInterval: "Cambiar imagen", paused: "En pausa", screenOn: "Al encender la pantalla", selectImages: "Seleccionar imágenes", pleaseWait: "Espera…",
|
||||||
collection: "Mi colección", image: "imagen", images: "imágenes", motif: "Imagen", noImages: "Aún no hay imágenes seleccionadas",
|
collection: "Mi colección", image: "imagen", images: "imágenes", motif: "Imagen", noImages: "Aún no hay imágenes seleccionadas",
|
||||||
collectionHint: "Toca el número de imágenes para abrir la galería.", shuffle: "Orden aleatorio", lockScreenOnly: "Solo pantalla de bloqueo",
|
collectionHint: "Toca el número de imágenes para abrir la galería.", shuffle: "Orden aleatorio", lockScreenOnly: "Solo pantalla de bloqueo",
|
||||||
settingsIntro: "Elige cómo funciona WallpaperFlow en segundo plano.", language: "Idioma", noImageLimit: "Sin límite de imágenes",
|
settingsIntro: "Elige cómo funciona WallpaperFlow en segundo plano.", language: "Idioma", noImageLimit: "Sin límite de imágenes",
|
||||||
@@ -98,7 +98,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
it: {
|
it: {
|
||||||
automaticActive: "Gli sfondi cambiano automaticamente", automaticPaused: "La rotazione automatica è in pausa",
|
automaticActive: "Gli sfondi cambiano automaticamente", automaticPaused: "La rotazione automatica è in pausa",
|
||||||
settings: "Impostazioni", currentWallpaper: "Sfondo attuale", currentImage: "Immagine attuale", nextImage: "Immagine successiva",
|
settings: "Impostazioni", currentWallpaper: "Sfondo attuale", currentImage: "Immagine attuale", nextImage: "Immagine successiva",
|
||||||
changeInterval: "Cambia immagine", paused: "In pausa", selectImages: "Seleziona immagini", pleaseWait: "Attendi…",
|
changeInterval: "Cambia immagine", paused: "In pausa", screenOn: "All’accensione dello schermo", selectImages: "Seleziona immagini", pleaseWait: "Attendi…",
|
||||||
collection: "La mia raccolta", image: "immagine", images: "immagini", motif: "Immagine", noImages: "Nessuna immagine selezionata",
|
collection: "La mia raccolta", image: "immagine", images: "immagini", motif: "Immagine", noImages: "Nessuna immagine selezionata",
|
||||||
collectionHint: "Tocca il numero di immagini per aprire la galleria.", shuffle: "Ordine casuale", lockScreenOnly: "Solo schermata di blocco",
|
collectionHint: "Tocca il numero di immagini per aprire la galleria.", shuffle: "Ordine casuale", lockScreenOnly: "Solo schermata di blocco",
|
||||||
settingsIntro: "Scegli come funziona WallpaperFlow in background.", language: "Lingua", noImageLimit: "Nessun limite di immagini",
|
settingsIntro: "Scegli come funziona WallpaperFlow in background.", language: "Lingua", noImageLimit: "Nessun limite di immagini",
|
||||||
@@ -119,7 +119,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
nl: {
|
nl: {
|
||||||
automaticActive: "Je achtergronden wisselen automatisch", automaticPaused: "Automatisch wisselen is gepauzeerd",
|
automaticActive: "Je achtergronden wisselen automatisch", automaticPaused: "Automatisch wisselen is gepauzeerd",
|
||||||
settings: "Instellingen", currentWallpaper: "Huidige achtergrond", currentImage: "Huidige afbeelding", nextImage: "Volgende afbeelding",
|
settings: "Instellingen", currentWallpaper: "Huidige achtergrond", currentImage: "Huidige afbeelding", nextImage: "Volgende afbeelding",
|
||||||
changeInterval: "Afbeelding wisselen", paused: "Gepauzeerd", selectImages: "Afbeeldingen kiezen", pleaseWait: "Even geduld…",
|
changeInterval: "Afbeelding wisselen", paused: "Gepauzeerd", screenOn: "Wanneer het scherm aangaat", selectImages: "Afbeeldingen kiezen", pleaseWait: "Even geduld…",
|
||||||
collection: "Mijn collectie", image: "afbeelding", images: "afbeeldingen", motif: "Afbeelding", noImages: "Nog geen afbeeldingen gekozen",
|
collection: "Mijn collectie", image: "afbeelding", images: "afbeeldingen", motif: "Afbeelding", noImages: "Nog geen afbeeldingen gekozen",
|
||||||
collectionHint: "Tik op het aantal afbeeldingen om je galerij te openen.", shuffle: "Willekeurige volgorde", lockScreenOnly: "Alleen vergrendelscherm",
|
collectionHint: "Tik op het aantal afbeeldingen om je galerij te openen.", shuffle: "Willekeurige volgorde", lockScreenOnly: "Alleen vergrendelscherm",
|
||||||
settingsIntro: "Kies hoe WallpaperFlow op de achtergrond werkt.", language: "Taal", noImageLimit: "Geen afbeeldingslimiet",
|
settingsIntro: "Kies hoe WallpaperFlow op de achtergrond werkt.", language: "Taal", noImageLimit: "Geen afbeeldingslimiet",
|
||||||
@@ -140,7 +140,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
pl: {
|
pl: {
|
||||||
automaticActive: "Tapety zmieniają się automatycznie", automaticPaused: "Automatyczna zmiana jest wstrzymana",
|
automaticActive: "Tapety zmieniają się automatycznie", automaticPaused: "Automatyczna zmiana jest wstrzymana",
|
||||||
settings: "Ustawienia", currentWallpaper: "Bieżąca tapeta", currentImage: "Bieżący obraz", nextImage: "Następny obraz",
|
settings: "Ustawienia", currentWallpaper: "Bieżąca tapeta", currentImage: "Bieżący obraz", nextImage: "Następny obraz",
|
||||||
changeInterval: "Zmieniaj obraz", paused: "Wstrzymano", selectImages: "Wybierz obrazy", pleaseWait: "Proszę czekać…",
|
changeInterval: "Zmieniaj obraz", paused: "Wstrzymano", screenOn: "Po włączeniu ekranu", selectImages: "Wybierz obrazy", pleaseWait: "Proszę czekać…",
|
||||||
collection: "Moja kolekcja", image: "obraz", images: "obrazy", motif: "Obraz", noImages: "Nie wybrano jeszcze obrazów",
|
collection: "Moja kolekcja", image: "obraz", images: "obrazy", motif: "Obraz", noImages: "Nie wybrano jeszcze obrazów",
|
||||||
collectionHint: "Dotknij liczby obrazów, aby otworzyć galerię.", shuffle: "Losowa kolejność", lockScreenOnly: "Tylko ekran blokady",
|
collectionHint: "Dotknij liczby obrazów, aby otworzyć galerię.", shuffle: "Losowa kolejność", lockScreenOnly: "Tylko ekran blokady",
|
||||||
settingsIntro: "Wybierz sposób działania WallpaperFlow w tle.", language: "Język", noImageLimit: "Bez limitu obrazów",
|
settingsIntro: "Wybierz sposób działania WallpaperFlow w tle.", language: "Język", noImageLimit: "Bez limitu obrazów",
|
||||||
@@ -161,7 +161,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
pt: {
|
pt: {
|
||||||
automaticActive: "Os seus fundos mudam automaticamente", automaticPaused: "A rotação automática está em pausa",
|
automaticActive: "Os seus fundos mudam automaticamente", automaticPaused: "A rotação automática está em pausa",
|
||||||
settings: "Definições", currentWallpaper: "Fundo atual", currentImage: "Imagem atual", nextImage: "Imagem seguinte",
|
settings: "Definições", currentWallpaper: "Fundo atual", currentImage: "Imagem atual", nextImage: "Imagem seguinte",
|
||||||
changeInterval: "Mudar imagem", paused: "Em pausa", selectImages: "Selecionar imagens", pleaseWait: "Aguarde…",
|
changeInterval: "Mudar imagem", paused: "Em pausa", screenOn: "Ao ligar o ecrã", selectImages: "Selecionar imagens", pleaseWait: "Aguarde…",
|
||||||
collection: "A minha coleção", image: "imagem", images: "imagens", motif: "Imagem", noImages: "Ainda não há imagens selecionadas",
|
collection: "A minha coleção", image: "imagem", images: "imagens", motif: "Imagem", noImages: "Ainda não há imagens selecionadas",
|
||||||
collectionHint: "Toque no número de imagens para abrir a galeria.", shuffle: "Ordem aleatória", lockScreenOnly: "Apenas ecrã de bloqueio",
|
collectionHint: "Toque no número de imagens para abrir a galeria.", shuffle: "Ordem aleatória", lockScreenOnly: "Apenas ecrã de bloqueio",
|
||||||
settingsIntro: "Escolha como o WallpaperFlow funciona em segundo plano.", language: "Idioma", noImageLimit: "Sem limite de imagens",
|
settingsIntro: "Escolha como o WallpaperFlow funciona em segundo plano.", language: "Idioma", noImageLimit: "Sem limite de imagens",
|
||||||
@@ -182,7 +182,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
ja: {
|
ja: {
|
||||||
automaticActive: "壁紙は自動的に切り替わります", automaticPaused: "自動切り替えは一時停止中です",
|
automaticActive: "壁紙は自動的に切り替わります", automaticPaused: "自動切り替えは一時停止中です",
|
||||||
settings: "設定", currentWallpaper: "現在の壁紙", currentImage: "現在の画像", nextImage: "次の画像",
|
settings: "設定", currentWallpaper: "現在の壁紙", currentImage: "現在の画像", nextImage: "次の画像",
|
||||||
changeInterval: "画像を切り替え", paused: "一時停止", selectImages: "画像を選択", pleaseWait: "お待ちください…",
|
changeInterval: "画像を切り替え", paused: "一時停止", screenOn: "画面点灯時", selectImages: "画像を選択", pleaseWait: "お待ちください…",
|
||||||
collection: "マイコレクション", image: "枚", images: "枚", motif: "画像", noImages: "画像がまだ選択されていません",
|
collection: "マイコレクション", image: "枚", images: "枚", motif: "画像", noImages: "画像がまだ選択されていません",
|
||||||
collectionHint: "画像数をタップしてギャラリーを開きます。", shuffle: "ランダムな順序", lockScreenOnly: "ロック画面のみ",
|
collectionHint: "画像数をタップしてギャラリーを開きます。", shuffle: "ランダムな順序", lockScreenOnly: "ロック画面のみ",
|
||||||
settingsIntro: "WallpaperFlow のバックグラウンド動作を設定します。", language: "言語", noImageLimit: "画像数の制限なし",
|
settingsIntro: "WallpaperFlow のバックグラウンド動作を設定します。", language: "言語", noImageLimit: "画像数の制限なし",
|
||||||
@@ -203,7 +203,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
ko: {
|
ko: {
|
||||||
automaticActive: "배경화면이 자동으로 변경됩니다", automaticPaused: "자동 변경이 일시 중지되었습니다",
|
automaticActive: "배경화면이 자동으로 변경됩니다", automaticPaused: "자동 변경이 일시 중지되었습니다",
|
||||||
settings: "설정", currentWallpaper: "현재 배경화면", currentImage: "현재 이미지", nextImage: "다음 이미지",
|
settings: "설정", currentWallpaper: "현재 배경화면", currentImage: "현재 이미지", nextImage: "다음 이미지",
|
||||||
changeInterval: "이미지 변경", paused: "일시 중지", selectImages: "이미지 선택", pleaseWait: "잠시 기다려 주세요…",
|
changeInterval: "이미지 변경", paused: "일시 중지", screenOn: "화면이 켜질 때", selectImages: "이미지 선택", pleaseWait: "잠시 기다려 주세요…",
|
||||||
collection: "내 컬렉션", image: "이미지", images: "이미지", motif: "이미지", noImages: "선택한 이미지가 없습니다",
|
collection: "내 컬렉션", image: "이미지", images: "이미지", motif: "이미지", noImages: "선택한 이미지가 없습니다",
|
||||||
collectionHint: "이미지 수를 탭하여 갤러리를 여세요.", shuffle: "무작위 순서", lockScreenOnly: "잠금 화면만",
|
collectionHint: "이미지 수를 탭하여 갤러리를 여세요.", shuffle: "무작위 순서", lockScreenOnly: "잠금 화면만",
|
||||||
settingsIntro: "WallpaperFlow의 백그라운드 작동 방식을 선택하세요.", language: "언어", noImageLimit: "이미지 수 제한 없음",
|
settingsIntro: "WallpaperFlow의 백그라운드 작동 방식을 선택하세요.", language: "언어", noImageLimit: "이미지 수 제한 없음",
|
||||||
@@ -224,7 +224,7 @@ const copies: Record<Exclude<Language, "en">, Copy> = {
|
|||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
automaticActive: "壁纸会自动更换", automaticPaused: "自动更换已暂停",
|
automaticActive: "壁纸会自动更换", automaticPaused: "自动更换已暂停",
|
||||||
settings: "设置", currentWallpaper: "当前壁纸", currentImage: "当前图片", nextImage: "下一张图片",
|
settings: "设置", currentWallpaper: "当前壁纸", currentImage: "当前图片", nextImage: "下一张图片",
|
||||||
changeInterval: "更换图片", paused: "已暂停", selectImages: "选择图片", pleaseWait: "请稍候…",
|
changeInterval: "更换图片", paused: "已暂停", screenOn: "屏幕亮起时", selectImages: "选择图片", pleaseWait: "请稍候…",
|
||||||
collection: "我的收藏", image: "张图片", images: "张图片", motif: "图片", noImages: "尚未选择图片",
|
collection: "我的收藏", image: "张图片", images: "张图片", motif: "图片", noImages: "尚未选择图片",
|
||||||
collectionHint: "点击图片数量打开图库。", shuffle: "随机顺序", lockScreenOnly: "仅锁屏",
|
collectionHint: "点击图片数量打开图库。", shuffle: "随机顺序", lockScreenOnly: "仅锁屏",
|
||||||
settingsIntro: "选择 WallpaperFlow 在后台的工作方式。", language: "语言", noImageLimit: "不限图片数量",
|
settingsIntro: "选择 WallpaperFlow 在后台的工作方式。", language: "语言", noImageLimit: "不限图片数量",
|
||||||
|
|||||||
Reference in New Issue
Block a user