From e251273322088cf93d68d323e3ea60058d579d38 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:53:58 +0200 Subject: [PATCH] feat(downloads): warn about Android background restrictions --- android/app/build.gradle.kts | 30 + .../plezy/BackgroundWorkDiagnostics.kt | 256 +++++++++ .../kotlin/com/edde746/plezy/MainActivity.kt | 7 + .../plezy/BackgroundWorkDiagnosticsTest.kt | 279 ++++++++++ lib/i18n/bg.i18n.json | 30 +- lib/i18n/da.i18n.json | 30 +- lib/i18n/de.i18n.json | 30 +- lib/i18n/en.i18n.json | 30 +- lib/i18n/es.i18n.json | 30 +- lib/i18n/fr.i18n.json | 30 +- lib/i18n/hu.i18n.json | 30 +- lib/i18n/it.i18n.json | 30 +- lib/i18n/ja.i18n.json | 30 +- lib/i18n/ko.i18n.json | 30 +- lib/i18n/nb.i18n.json | 30 +- lib/i18n/nl.i18n.json | 30 +- lib/i18n/pl.i18n.json | 30 +- lib/i18n/pt.i18n.json | 30 +- lib/i18n/ru.i18n.json | 30 +- lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_bg.g.dart | 62 +++ lib/i18n/strings_da.g.dart | 62 +++ lib/i18n/strings_de.g.dart | 62 +++ lib/i18n/strings_en.g.dart | 115 ++++ lib/i18n/strings_es.g.dart | 62 +++ lib/i18n/strings_fr.g.dart | 62 +++ lib/i18n/strings_hu.g.dart | 62 +++ lib/i18n/strings_it.g.dart | 62 +++ lib/i18n/strings_ja.g.dart | 62 +++ lib/i18n/strings_ko.g.dart | 62 +++ lib/i18n/strings_nb.g.dart | 62 +++ lib/i18n/strings_nl.g.dart | 62 +++ lib/i18n/strings_pl.g.dart | 62 +++ lib/i18n/strings_pt.g.dart | 62 +++ lib/i18n/strings_ru.g.dart | 62 +++ lib/i18n/strings_sv.g.dart | 62 +++ lib/i18n/strings_zh.g.dart | 62 +++ lib/i18n/strings_zh_Hant.g.dart | 62 +++ lib/i18n/sv.i18n.json | 30 +- lib/i18n/zh-Hant.i18n.json | 30 +- lib/i18n/zh.i18n.json | 30 +- lib/providers/download_provider.dart | 91 ++- lib/screens/downloads/downloads_screen.dart | 12 + lib/screens/media_detail/action_buttons.dart | 6 + lib/screens/media_detail_screen.dart | 1 + lib/screens/music/album_detail_screen.dart | 3 + lib/screens/settings/logs_screen.dart | 7 + lib/screens/settings/settings_screen.dart | 54 ++ .../background_work_diagnostics_service.dart | 517 ++++++++++++++++++ lib/services/download_manager_service.dart | 5 + .../music/music_playback_service_impl.dart | 2 +- lib/services/settings_service.dart | 6 + lib/utils/download_utils.dart | 5 + lib/utils/notification_permission.dart | 71 ++- .../background_download_warning_banner.dart | 246 +++++++++ test/i18n/locale_fallback_source_test.dart | 26 + test/providers/download_provider_test.dart | 190 +++++++ .../settings/settings_screen_test.dart | 56 ++ ...kground_work_diagnostics_service_test.dart | 505 +++++++++++++++++ test/utils/notification_permission_test.dart | 53 ++ ...ckground_download_warning_banner_test.dart | 295 ++++++++++ 61 files changed, 4388 insertions(+), 46 deletions(-) create mode 100644 android/app/src/main/kotlin/com/edde746/plezy/BackgroundWorkDiagnostics.kt create mode 100644 android/app/src/test/kotlin/com/edde746/plezy/BackgroundWorkDiagnosticsTest.kt create mode 100644 lib/services/background_work_diagnostics_service.dart create mode 100644 lib/widgets/background_download_warning_banner.dart create mode 100644 test/services/background_work_diagnostics_service_test.dart create mode 100644 test/utils/notification_permission_test.dart create mode 100644 test/widgets/background_download_warning_banner_test.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 884827cb..0b83ca8d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -417,6 +417,36 @@ android { } } +// BackgroundWorkDiagnostics routes users to background_downloader's private +// notification channel. Fail the build if an upstream ref changes that ID. +val verifyBackgroundDownloaderNotificationChannel = tasks.register("verifyBackgroundDownloaderNotificationChannel") { + val expectedChannelId = "background_downloader" + val downloaderProject = rootProject.findProject(":background_downloader") + val notificationsSource = downloaderProject?.projectDir?.resolve( + "src/main/kotlin/com/bbflight/background_downloader/Notifications.kt" + ) + notificationsSource?.let(inputs::file) + doLast { + if (notificationsSource == null) { + logger.lifecycle("Skipping downloader channel verification: Flutter plugin project is not configured") + return@doLast + } + val actualChannelId = Regex( + """private const val notificationChannelId\s*=\s*"([^"]+)"""" + ).find(notificationsSource.readText())?.groupValues?.get(1) + ?: throw GradleException("Could not locate background_downloader's notification channel ID") + if (actualChannelId != expectedChannelId) { + throw GradleException( + "background_downloader channel ID changed from $expectedChannelId to $actualChannelId; " + + "update BackgroundWorkDiagnostics and its tests" + ) + } + } +} +tasks.named("preBuild").configure { + dependsOn(verifyBackgroundDownloaderNotificationChannel) +} + kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/BackgroundWorkDiagnostics.kt b/android/app/src/main/kotlin/com/edde746/plezy/BackgroundWorkDiagnostics.kt new file mode 100644 index 00000000..6792012f --- /dev/null +++ b/android/app/src/main/kotlin/com/edde746/plezy/BackgroundWorkDiagnostics.kt @@ -0,0 +1,256 @@ +package com.edde746.plezy + +import android.app.ActivityManager +import android.app.NotificationManager +import android.app.usage.UsageStatsManager +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings + +/** + * Raw per-signal snapshot of everything the OS is willing to tell us about + * whether our own background work will be allowed to run. + * + * Every field is nullable: a null means "this API level cannot answer", not + * "unrestricted". [BackgroundWorkClassifier] treats unknown as OK, because a + * warning we cannot substantiate is worse than no warning at all. + */ +internal data class BackgroundWorkSignals( + val sdkInt: Int? = null, + val backgroundRestricted: Boolean? = null, + val standbyBucket: Int? = null, + val notificationsEnabled: Boolean? = null, + val downloadChannelBlocked: Boolean? = null, + val dataSaverRestricted: Boolean? = null, + val ignoringBatteryOptimizations: Boolean? = null +) + +/** + * Turns [BackgroundWorkSignals] into a verdict plus ordered reasons. + * + * Deliberately pure and free of `Build.VERSION` reads so the whole decision + * table is exercisable from plain JVM unit tests. + */ +internal object BackgroundWorkClassifier { + const val VERDICT_OK = "ok" + const val VERDICT_DEGRADED = "degraded" + const val VERDICT_BLOCKED = "blocked" + + const val REASON_BACKGROUND_RESTRICTED = "background_restricted" + const val REASON_STANDBY_RESTRICTED = "standby_restricted" + const val REASON_DOWNLOAD_CHANNEL_BLOCKED = "download_channel_blocked" + const val REASON_NOTIFICATIONS_DISABLED = "notifications_disabled" + const val REASON_DATA_SAVER = "data_saver" + + /** + * `STANDBY_BUCKET_RESTRICTED` (API 30) and `STANDBY_BUCKET_NEVER`. Declared + * literally so the classifier stays compilable and testable below those API + * levels. `RARE` (40) is intentionally absent — it is reached by ordinary + * disuse and says nothing about a user-imposed restriction, so warning on it + * would fire on healthy devices. + */ + const val BUCKET_RESTRICTED = 45 + const val BUCKET_NEVER = 50 + + private val blockingReasons = setOf( + REASON_BACKGROUND_RESTRICTED, + REASON_STANDBY_RESTRICTED + ) + + /** Ordered most- to least-actionable; the UI leads with the first entry. */ + fun reasons(signals: BackgroundWorkSignals): List = buildList { + if (signals.backgroundRestricted == true) add(REASON_BACKGROUND_RESTRICTED) + if (signals.standbyBucket != null && isRestrictedBucket(signals.standbyBucket)) add(REASON_STANDBY_RESTRICTED) + // App-wide denial is more fundamental than a muted channel and routes to + // a screen that also exists before the channel has been created. + if (signals.notificationsEnabled == false) { + add(REASON_NOTIFICATIONS_DISABLED) + } else if (signals.downloadChannelBlocked == true) { + add(REASON_DOWNLOAD_CHANNEL_BLOCKED) + } + if (signals.dataSaverRestricted == true) add(REASON_DATA_SAVER) + } + + fun verdict(signals: BackgroundWorkSignals, reasons: List = reasons(signals)): String = when { + reasons.any(blockingReasons::contains) -> VERDICT_BLOCKED + signals.sdkInt != null && + signals.sdkInt >= Build.VERSION_CODES.TIRAMISU && + signals.notificationsEnabled == false -> VERDICT_BLOCKED + reasons.isNotEmpty() -> VERDICT_DEGRADED + else -> VERDICT_OK + } + + fun isRestrictedBucket(bucket: Int): Boolean = bucket == BUCKET_RESTRICTED || bucket == BUCKET_NEVER + + fun toMap(signals: BackgroundWorkSignals): Map { + val reasons = reasons(signals) + return mapOf( + "verdict" to verdict(signals, reasons), + "reasons" to reasons, + "sdkInt" to signals.sdkInt, + "backgroundRestricted" to signals.backgroundRestricted, + "standbyBucket" to signals.standbyBucket, + "notificationsEnabled" to signals.notificationsEnabled, + "downloadChannelBlocked" to signals.downloadChannelBlocked, + "dataSaverRestricted" to signals.dataSaverRestricted, + "ignoringBatteryOptimizations" to signals.ignoringBatteryOptimizations + ) + } +} + +/** Settings screen a remedy button can send the user to. */ +internal enum class BackgroundSettingsTarget(val id: String) { + APP_DETAILS("app_details"), + APP_NOTIFICATIONS("app_notifications"), + NOTIFICATION_CHANNEL("notification_channel"); + + companion object { + fun fromId(raw: String?): BackgroundSettingsTarget? = entries.firstOrNull { it.id == raw } + } +} + +/** Declarative intent description, kept Intent-free so it unit-tests on the JVM. */ +internal data class SettingsIntentSpec( + val action: String, + val data: String? = null, + val stringExtras: Map = emptyMap() +) + +/** + * Ordered candidate intents per target, each falling back to the app details + * page — the one Settings screen that exists on every Android build and that + * reaches Battery → Unrestricted on One UI, Pixel, and AOSP alike. + * + * Samsung's own "Background usage limits" screen lives in an unexported + * `com.samsung.android.lool` activity that moves between One UI releases, so + * it is deliberately not targeted; the user gets written directions instead. + */ +internal object BackgroundSettingsIntents { + fun specsFor( + target: BackgroundSettingsTarget, + packageName: String, + sdkInt: Int + ): List { + val appDetails = SettingsIntentSpec( + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + data = "package:$packageName" + ) + if (sdkInt < Build.VERSION_CODES.O) return listOf(appDetails) + return when (target) { + BackgroundSettingsTarget.APP_DETAILS -> listOf(appDetails) + BackgroundSettingsTarget.APP_NOTIFICATIONS -> listOf( + SettingsIntentSpec( + action = Settings.ACTION_APP_NOTIFICATION_SETTINGS, + stringExtras = mapOf(Settings.EXTRA_APP_PACKAGE to packageName) + ), + appDetails + ) + BackgroundSettingsTarget.NOTIFICATION_CHANNEL -> listOf( + SettingsIntentSpec( + action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS, + stringExtras = mapOf( + Settings.EXTRA_APP_PACKAGE to packageName, + Settings.EXTRA_CHANNEL_ID to BackgroundWorkDiagnostics.DOWNLOAD_NOTIFICATION_CHANNEL_ID + ) + ), + SettingsIntentSpec( + action = Settings.ACTION_APP_NOTIFICATION_SETTINGS, + stringExtras = mapOf(Settings.EXTRA_APP_PACKAGE to packageName) + ), + appDetails + ) + } + } +} + +/** + * Reads the OS-visible reasons background downloads may be blocked. + * + * Every read is for our own package and needs no permission. Notably absent is + * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`: it is a Play-policy-restricted + * permission whose qualifying use cases do not include media downloading. + * Whitelist state is reported for support diagnostics only. + */ +internal object BackgroundWorkDiagnostics { + /** `background_downloader`'s channel id (`Notifications.kt`, `nId`). */ + const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "background_downloader" + + fun read(context: Context): BackgroundWorkSignals = BackgroundWorkSignals( + sdkInt = Build.VERSION.SDK_INT, + backgroundRestricted = readBackgroundRestricted(context), + standbyBucket = readStandbyBucket(context), + notificationsEnabled = readNotificationsEnabled(context), + downloadChannelBlocked = readDownloadChannelBlocked(context), + dataSaverRestricted = readDataSaverRestricted(context), + ignoringBatteryOptimizations = readIgnoringBatteryOptimizations(context) + ) + + private fun readBackgroundRestricted(context: Context): Boolean? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return null + return runCatching { + (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).isBackgroundRestricted + }.getOrNull() + } + + private fun readStandbyBucket(context: Context): Int? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return null + return runCatching { + (context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager).appStandbyBucket + }.getOrNull() + } + + private fun readNotificationsEnabled(context: Context): Boolean? = runCatching { + (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled() + }.getOrNull() + + /** + * Null until the plugin has created its channel (first download), which is + * correct — there is nothing to warn about before then. + */ + private fun readDownloadChannelBlocked(context: Context): Boolean? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null + return runCatching { + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = manager.getNotificationChannel(DOWNLOAD_NOTIFICATION_CHANNEL_ID) ?: return null + channel.importance == NotificationManager.IMPORTANCE_NONE + }.getOrNull() + } + + private fun readDataSaverRestricted(context: Context): Boolean? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return null + return runCatching { + val connectivity = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivity.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED + }.getOrNull() + } + + private fun readIgnoringBatteryOptimizations(context: Context): Boolean? = runCatching { + (context.getSystemService(Context.POWER_SERVICE) as PowerManager) + .isIgnoringBatteryOptimizations(context.packageName) + }.getOrNull() + + /** + * Launches the first candidate Settings screen that resolves. Returns false + * when the device has none of them — some TV and Fire OS builds ship without + * a battery settings activity entirely. + */ + fun openSettings(context: Context, target: BackgroundSettingsTarget): Boolean { + for (spec in BackgroundSettingsIntents.specsFor(target, context.packageName, Build.VERSION.SDK_INT)) { + val launched = runCatching { + val intent = Intent(spec.action).apply { + spec.data?.let { data = Uri.parse(it) } + spec.stringExtras.forEach { (key, value) -> putExtra(key, value) } + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + true + }.getOrDefault(false) + if (launched) return true + } + return false + } +} diff --git a/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt b/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt index 6dc4d995..fed723c3 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt @@ -573,6 +573,13 @@ class MainActivity : FlutterActivity() { "getTvDetection" -> result.success(getAndroidTvDetection()) "getDeviceName" -> result.success(getDeviceName()) "getPerformanceSignals" -> result.success(getPerformanceSignals()) + "getBackgroundWorkSignals" -> result.success( + BackgroundWorkClassifier.toMap(BackgroundWorkDiagnostics.read(this)) + ) + "openBackgroundSettings" -> { + val target = BackgroundSettingsTarget.fromId(call.arguments as? String) + result.success(target != null && BackgroundWorkDiagnostics.openSettings(this, target)) + } "getPreviousExit" -> handlePreviousExit(result) "setStartupPhase" -> queueStartupPhase(call.arguments as? String, result) "setRuntimeUiState" -> { diff --git a/android/app/src/test/kotlin/com/edde746/plezy/BackgroundWorkDiagnosticsTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/BackgroundWorkDiagnosticsTest.kt new file mode 100644 index 00000000..602ee4b3 --- /dev/null +++ b/android/app/src/test/kotlin/com/edde746/plezy/BackgroundWorkDiagnosticsTest.kt @@ -0,0 +1,279 @@ +package com.edde746.plezy + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class BackgroundWorkDiagnosticsTest { + + @Test + fun unknownSignalsAreTreatedAsHealthy() { + val signals = BackgroundWorkSignals() + + assertEquals(emptyList(), BackgroundWorkClassifier.reasons(signals)) + assertEquals(BackgroundWorkClassifier.VERDICT_OK, verdictFor(signals)) + } + + @Test + fun unrestrictedDeviceIsHealthy() { + val signals = BackgroundWorkSignals( + backgroundRestricted = false, + standbyBucket = 10, + notificationsEnabled = true, + downloadChannelBlocked = false, + dataSaverRestricted = false, + ignoringBatteryOptimizations = false + ) + + assertEquals(emptyList(), BackgroundWorkClassifier.reasons(signals)) + assertEquals(BackgroundWorkClassifier.VERDICT_OK, verdictFor(signals)) + } + + @Test + fun missingBatteryWhitelistAloneNeverWarns() { + // The overwhelming majority of healthy installs are not whitelisted; a + // warning keyed off this would fire on essentially every device. + val signals = BackgroundWorkSignals( + backgroundRestricted = false, + standbyBucket = 20, + notificationsEnabled = true, + ignoringBatteryOptimizations = false + ) + + assertEquals(BackgroundWorkClassifier.VERDICT_OK, verdictFor(signals)) + } + + @Test + fun rareBucketIsNotRestricted() { + // RARE is reached by ordinary disuse and is not a user-imposed limit. + assertFalse(BackgroundWorkClassifier.isRestrictedBucket(10)) + assertFalse(BackgroundWorkClassifier.isRestrictedBucket(20)) + assertFalse(BackgroundWorkClassifier.isRestrictedBucket(30)) + assertFalse(BackgroundWorkClassifier.isRestrictedBucket(40)) + + val signals = BackgroundWorkSignals(standbyBucket = 40, notificationsEnabled = true) + assertEquals(BackgroundWorkClassifier.VERDICT_OK, verdictFor(signals)) + } + + @Test + fun restrictedAndNeverBucketsBlock() { + assertTrue(BackgroundWorkClassifier.isRestrictedBucket(BackgroundWorkClassifier.BUCKET_RESTRICTED)) + assertTrue(BackgroundWorkClassifier.isRestrictedBucket(BackgroundWorkClassifier.BUCKET_NEVER)) + + for (bucket in listOf(BackgroundWorkClassifier.BUCKET_RESTRICTED, BackgroundWorkClassifier.BUCKET_NEVER)) { + val signals = BackgroundWorkSignals(standbyBucket = bucket, notificationsEnabled = true) + assertEquals( + listOf(BackgroundWorkClassifier.REASON_STANDBY_RESTRICTED), + BackgroundWorkClassifier.reasons(signals) + ) + assertEquals(BackgroundWorkClassifier.VERDICT_BLOCKED, verdictFor(signals)) + } + } + + @Test + fun backgroundRestrictionBlocksAndLeadsTheReasons() { + // The Samsung "Background usage limits" case that motivated this: both the + // AOSP restriction flag and the standby bucket flip together. + val signals = BackgroundWorkSignals( + backgroundRestricted = true, + standbyBucket = BackgroundWorkClassifier.BUCKET_RESTRICTED, + notificationsEnabled = true, + downloadChannelBlocked = false + ) + + assertEquals( + listOf( + BackgroundWorkClassifier.REASON_BACKGROUND_RESTRICTED, + BackgroundWorkClassifier.REASON_STANDBY_RESTRICTED + ), + BackgroundWorkClassifier.reasons(signals) + ) + assertEquals(BackgroundWorkClassifier.VERDICT_BLOCKED, verdictFor(signals)) + } + + @Test + fun disabledNotificationsBlockOnAndroid13AndNewer() { + val signals = BackgroundWorkSignals( + sdkInt = 33, + backgroundRestricted = false, + notificationsEnabled = false + ) + + assertEquals( + listOf(BackgroundWorkClassifier.REASON_NOTIFICATIONS_DISABLED), + BackgroundWorkClassifier.reasons(signals) + ) + assertEquals(BackgroundWorkClassifier.VERDICT_BLOCKED, verdictFor(signals)) + } + + @Test + fun disabledNotificationsOnlyDegradeBeforeAndroid13() { + val signals = BackgroundWorkSignals( + sdkInt = 32, + backgroundRestricted = false, + notificationsEnabled = false + ) + + assertEquals(BackgroundWorkClassifier.VERDICT_DEGRADED, verdictFor(signals)) + } + + @Test + fun mutedDownloadChannelIsDegraded() { + val signals = BackgroundWorkSignals(notificationsEnabled = true, downloadChannelBlocked = true) + + assertEquals( + listOf(BackgroundWorkClassifier.REASON_DOWNLOAD_CHANNEL_BLOCKED), + BackgroundWorkClassifier.reasons(signals) + ) + assertEquals(BackgroundWorkClassifier.VERDICT_DEGRADED, verdictFor(signals)) + } + + @Test + fun appWideNotificationDenialSupersedesTheChannelReason() { + val signals = BackgroundWorkSignals(sdkInt = 33, notificationsEnabled = false, downloadChannelBlocked = true) + + assertEquals( + listOf(BackgroundWorkClassifier.REASON_NOTIFICATIONS_DISABLED), + BackgroundWorkClassifier.reasons(signals) + ) + assertEquals(BackgroundWorkClassifier.VERDICT_BLOCKED, verdictFor(signals)) + } + + @Test + fun absentChannelIsNotAFault() { + // The plugin creates its channel on first download; before then there is + // nothing to warn about. + val signals = BackgroundWorkSignals(notificationsEnabled = true, downloadChannelBlocked = null) + + assertEquals(BackgroundWorkClassifier.VERDICT_OK, verdictFor(signals)) + } + + @Test + fun dataSaverOnlyDegrades() { + val signals = BackgroundWorkSignals( + backgroundRestricted = false, + standbyBucket = 20, + notificationsEnabled = true, + dataSaverRestricted = true + ) + + assertEquals(listOf(BackgroundWorkClassifier.REASON_DATA_SAVER), BackgroundWorkClassifier.reasons(signals)) + assertEquals(BackgroundWorkClassifier.VERDICT_DEGRADED, verdictFor(signals)) + } + + @Test + fun blockingReasonOutranksDataSaver() { + val signals = BackgroundWorkSignals(backgroundRestricted = true, dataSaverRestricted = true) + + assertEquals(BackgroundWorkClassifier.VERDICT_BLOCKED, verdictFor(signals)) + } + + @Test + fun mapCarriesVerdictReasonsAndRawSignals() { + val signals = BackgroundWorkSignals( + sdkInt = 33, + backgroundRestricted = true, + standbyBucket = BackgroundWorkClassifier.BUCKET_RESTRICTED, + notificationsEnabled = false, + downloadChannelBlocked = true, + dataSaverRestricted = false, + ignoringBatteryOptimizations = false + ) + + val map = BackgroundWorkClassifier.toMap(signals) + + assertEquals(BackgroundWorkClassifier.VERDICT_BLOCKED, map["verdict"]) + assertEquals( + listOf( + BackgroundWorkClassifier.REASON_BACKGROUND_RESTRICTED, + BackgroundWorkClassifier.REASON_STANDBY_RESTRICTED, + BackgroundWorkClassifier.REASON_NOTIFICATIONS_DISABLED + ), + map["reasons"] + ) + assertEquals(33, map["sdkInt"]) + assertEquals(true, map["backgroundRestricted"]) + assertEquals(BackgroundWorkClassifier.BUCKET_RESTRICTED, map["standbyBucket"]) + assertEquals(false, map["notificationsEnabled"]) + assertEquals(true, map["downloadChannelBlocked"]) + assertEquals(false, map["dataSaverRestricted"]) + assertEquals(false, map["ignoringBatteryOptimizations"]) + } + + @Test + fun unknownSignalsSurviveAsNullsInTheMap() { + val map = BackgroundWorkClassifier.toMap(BackgroundWorkSignals()) + + assertEquals(BackgroundWorkClassifier.VERDICT_OK, map["verdict"]) + assertTrue(map.containsKey("standbyBucket")) + assertEquals(null, map["standbyBucket"]) + assertEquals(null, map["backgroundRestricted"]) + } + + @Test + fun settingsTargetsParseFromTheirWireIds() { + assertEquals(BackgroundSettingsTarget.APP_DETAILS, BackgroundSettingsTarget.fromId("app_details")) + assertEquals( + BackgroundSettingsTarget.APP_NOTIFICATIONS, + BackgroundSettingsTarget.fromId("app_notifications") + ) + assertEquals( + BackgroundSettingsTarget.NOTIFICATION_CHANNEL, + BackgroundSettingsTarget.fromId("notification_channel") + ) + assertEquals(null, BackgroundSettingsTarget.fromId("nonsense")) + assertEquals(null, BackgroundSettingsTarget.fromId(null)) + } + + @Test + fun everyTargetFallsBackToTheAppDetailsPage() { + for (target in BackgroundSettingsTarget.entries) { + val specs = BackgroundSettingsIntents.specsFor(target, "com.edde746.plezy", 35) + assertTrue(specs.isNotEmpty()) + val last = specs.last() + assertEquals("android.settings.APPLICATION_DETAILS_SETTINGS", last.action) + assertEquals("package:com.edde746.plezy", last.data) + } + } + + @Test + fun notificationChannelTargetAddressesTheDownloaderChannel() { + val first = BackgroundSettingsIntents.specsFor( + BackgroundSettingsTarget.NOTIFICATION_CHANNEL, + "com.edde746.plezy", + 35 + ).first() + + assertEquals("android.settings.CHANNEL_NOTIFICATION_SETTINGS", first.action) + assertEquals("com.edde746.plezy", first.stringExtras["android.provider.extra.APP_PACKAGE"]) + assertEquals( + BackgroundWorkDiagnostics.DOWNLOAD_NOTIFICATION_CHANNEL_ID, + first.stringExtras["android.provider.extra.CHANNEL_ID"] + ) + } + + @Test + fun appNotificationTargetDoesNotRequireAnExistingChannel() { + val first = BackgroundSettingsIntents.specsFor( + BackgroundSettingsTarget.APP_NOTIFICATIONS, + "com.edde746.plezy", + 35 + ).first() + + assertEquals("android.settings.APP_NOTIFICATION_SETTINGS", first.action) + assertEquals("com.edde746.plezy", first.stringExtras["android.provider.extra.APP_PACKAGE"]) + assertEquals(null, first.stringExtras["android.provider.extra.CHANNEL_ID"]) + } + + @Test + fun preOreoNotificationRemediesFallBackToAppDetails() { + for (target in listOf(BackgroundSettingsTarget.APP_NOTIFICATIONS, BackgroundSettingsTarget.NOTIFICATION_CHANNEL)) { + val first = BackgroundSettingsIntents.specsFor(target, "com.edde746.plezy", 25).first() + assertEquals("android.settings.APPLICATION_DETAILS_SETTINGS", first.action) + assertEquals("package:com.edde746.plezy", first.data) + } + } + + private fun verdictFor(signals: BackgroundWorkSignals): String = BackgroundWorkClassifier.verdict(signals) +} diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 57ed5a39..707628da 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Изисква се вход", "syncRuleNotAvailableForProfile": "Не е налично за текущия профил", "syncRuleUnknownServer": "Неизвестен сървър", - "syncRuleListCreated": "Правилото за синхронизация е създадено" + "syncRuleListCreated": "Правилото за синхронизация е създадено", + "backgroundWarning": { + "bannerBlocked": "Изтеглянията ще спрат, когато излезете от приложението", + "bannerDegraded": "Изтеглянията във фонов режим може да бъдат ограничени", + "bannerAction": "Подробности", + "sheetTitle": "Изтеглянията във фонов режим са блокирани", + "sheetTitleDegraded": "Изтеглянията във фонов режим може да бъдат ограничени", + "sheetIntro": "Android не позволява на Plezy да изтегля надеждно във фонов режим.", + "sheetIntroDegraded": "Устройството ви ограничава кога Plezy може да изтегля във фонов режим.", + "reasonBackgroundRestricted": "Работата на Plezy във фонов режим е ограничена. Задайте използването на батерията или работата във фонов режим на „Без ограничения“.", + "reasonStandbyRestricted": "Android е поставил Plezy в ограничено състояние на готовност. Задайте използването на батерията на „Без ограничения“.", + "reasonDownloadChannelBlocked": "Известията за изтегляния са изключени, затова напредъкът и контролите може да не са достъпни.", + "reasonNotificationsDisabled": "Известията са изключени. В Android 13 или по-нова версия те са необходими за продължителни изтегляния във фонов режим.", + "reasonDataSaver": "„Икономия на данни“ е включена и блокира изтеглянията във фонов режим през мобилни данни. Изтеглянията би трябвало да продължат през Wi-Fi.", + "reasonOemUnknown": "Изтеглянията спираха многократно, докато Plezy беше във фонов режим. Проверете настройките за батерията или работата на Plezy във фонов режим.", + "openSettings": "Отвори настройките", + "stillNotWorking": "Помощ за конкретното устройство", + "stillNotWorkingDescription": "Вижте стъпките за устройството си или изпратете лог от Настройки › Виж логовете, ако проблемът продължи.", + "dialogTitle": "Изтеглянията може да не завършат", + "dialogDownloadAnyway": "Изтегли въпреки това", + "dialogFixFirst": "Първо отстрани проблема", + "statusTile": "Изтегляния във фонов режим", + "statusOk": "Разрешена е работа във фонов режим", + "statusBlocked": "Блокирани от системните настройки", + "statusDegraded": "Ограничени от системните настройки", + "statusUnknown": "Все още не е проверено", + "settingsUnavailable": "Системните настройки не можаха да се отворят на това устройство", + "linkUnavailable": "dontkillmyapp.com не можа да се отвори на това устройство" + } }, "shaders": { "title": "Шейдъри", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index ed1ed8ad..657a0541 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Login påkrævet", "syncRuleNotAvailableForProfile": "Ikke tilgængelig for nuværende profil", "syncRuleUnknownServer": "Ukendt server", - "syncRuleListCreated": "Synkroniseringsregel oprettet" + "syncRuleListCreated": "Synkroniseringsregel oprettet", + "backgroundWarning": { + "bannerBlocked": "Downloads stopper, når du forlader appen", + "bannerDegraded": "Downloads i baggrunden kan være begrænsede", + "bannerAction": "Detaljer", + "sheetTitle": "Downloads i baggrunden er blokeret", + "sheetTitleDegraded": "Downloads i baggrunden kan være begrænsede", + "sheetIntro": "Android forhindrer Plezy i at downloade stabilt i baggrunden.", + "sheetIntroDegraded": "Din enhed begrænser, hvornår Plezy kan downloade i baggrunden.", + "reasonBackgroundRestricted": "Plezys baggrundsaktivitet er begrænset. Indstil batteriforbruget eller baggrundsaktiviteten til \"Ubegrænset\".", + "reasonStandbyRestricted": "Android har sat Plezy i begrænset standbytilstand. Indstil batteriforbruget til \"Ubegrænset\".", + "reasonDownloadChannelBlocked": "Notifikationer om downloads er slået fra, så status og betjeningsknapper muligvis ikke er tilgængelige.", + "reasonNotificationsDisabled": "Notifikationer er slået fra. På Android 13 eller nyere er de nødvendige ved lange downloads i baggrunden.", + "reasonDataSaver": "Datasparefunktionen er slået til, hvilket blokerer downloads i baggrunden via mobildata. Downloads bør stadig køre på Wi-Fi.", + "reasonOemUnknown": "Downloads stoppede gentagne gange, mens Plezy var i baggrunden. Tjek Plezys indstillinger for batteriforbrug eller baggrundsaktivitet.", + "openSettings": "Åbn indstillinger", + "stillNotWorking": "Enhedsspecifik hjælp", + "stillNotWorkingDescription": "Se vejledningen til din enhed, eller send en logfil fra Indstillinger › Vis logfiler, hvis problemet fortsætter.", + "dialogTitle": "Downloads bliver muligvis ikke færdige", + "dialogDownloadAnyway": "Download alligevel", + "dialogFixFirst": "Løs dette først", + "statusTile": "Downloads i baggrunden", + "statusOk": "Må køre i baggrunden", + "statusBlocked": "Blokeret af systemindstillinger", + "statusDegraded": "Begrænset af systemindstillinger", + "statusUnknown": "Endnu ikke kontrolleret", + "settingsUnavailable": "Kunne ikke åbne systemindstillingerne på denne enhed", + "linkUnavailable": "Kunne ikke åbne dontkillmyapp.com på denne enhed" + } }, "shaders": { "title": "Shadere", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 5172c982..0f7e8591 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Anmeldung erforderlich", "syncRuleNotAvailableForProfile": "Für das aktuelle Profil nicht verfügbar", "syncRuleUnknownServer": "Unbekannter Server", - "syncRuleListCreated": "Synchronisierungsregel erstellt" + "syncRuleListCreated": "Synchronisierungsregel erstellt", + "backgroundWarning": { + "bannerBlocked": "Downloads werden gestoppt, wenn du die App verlässt", + "bannerDegraded": "Downloads im Hintergrund sind möglicherweise eingeschränkt", + "bannerAction": "Details", + "sheetTitle": "Downloads im Hintergrund sind blockiert", + "sheetTitleDegraded": "Downloads im Hintergrund sind möglicherweise eingeschränkt", + "sheetIntro": "Android verhindert, dass Plezy zuverlässig im Hintergrund herunterlädt.", + "sheetIntroDegraded": "Dein Gerät schränkt ein, wann Plezy im Hintergrund herunterladen kann.", + "reasonBackgroundRestricted": "Die Hintergrundnutzung von Plezy ist eingeschränkt. Stelle die Akku- oder Hintergrundnutzung auf „Uneingeschränkt“.", + "reasonStandbyRestricted": "Android hat Plezy in einen eingeschränkten Standby-Modus versetzt. Stelle die Akkunutzung auf „Uneingeschränkt“.", + "reasonDownloadChannelBlocked": "Download-Benachrichtigungen sind deaktiviert. Fortschritt und Steuerelemente sind daher möglicherweise nicht verfügbar.", + "reasonNotificationsDisabled": "Benachrichtigungen sind deaktiviert. Ab Android 13 sind sie für lange Downloads im Hintergrund erforderlich.", + "reasonDataSaver": "Der Datensparmodus ist aktiviert und blockiert Downloads im Hintergrund über mobile Daten. Über Wi-Fi sollten Downloads weiterhin funktionieren.", + "reasonOemUnknown": "Downloads wurden wiederholt gestoppt, während Plezy im Hintergrund war. Prüfe die Einstellungen zur Akku- oder Hintergrundnutzung von Plezy.", + "openSettings": "Einstellungen öffnen", + "stillNotWorking": "Gerätespezifische Hilfe", + "stillNotWorkingDescription": "Sieh dir die Schritte für dein Gerät an oder sende bei anhaltendem Problem ein Protokoll über Einstellungen › Protokolle anzeigen.", + "dialogTitle": "Downloads werden möglicherweise nicht abgeschlossen", + "dialogDownloadAnyway": "Trotzdem herunterladen", + "dialogFixFirst": "Zuerst beheben", + "statusTile": "Downloads im Hintergrund", + "statusOk": "Ausführung im Hintergrund erlaubt", + "statusBlocked": "Durch Systemeinstellungen blockiert", + "statusDegraded": "Durch Systemeinstellungen eingeschränkt", + "statusUnknown": "Noch nicht geprüft", + "settingsUnavailable": "Die Systemeinstellungen konnten auf diesem Gerät nicht geöffnet werden", + "linkUnavailable": "dontkillmyapp.com konnte auf diesem Gerät nicht geöffnet werden" + } }, "shaders": { "title": "Shader", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 476b05f7..3e5822b2 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Sign in required", "syncRuleNotAvailableForProfile": "Not available for current profile", "syncRuleUnknownServer": "Unknown server", - "syncRuleListCreated": "Sync rule created" + "syncRuleListCreated": "Sync rule created", + "backgroundWarning": { + "bannerBlocked": "Downloads will stop when you leave the app", + "bannerDegraded": "Background downloads may be limited", + "bannerAction": "Details", + "sheetTitle": "Background downloads are blocked", + "sheetTitleDegraded": "Background downloads may be limited", + "sheetIntro": "Android is preventing Plezy from downloading reliably in the background.", + "sheetIntroDegraded": "Your device is limiting when Plezy can download in the background.", + "reasonBackgroundRestricted": "Plezy's background usage is restricted. Set its battery or background usage to \"Unrestricted\".", + "reasonStandbyRestricted": "Android has put Plezy in a restricted standby state. Set its battery usage to \"Unrestricted\".", + "reasonDownloadChannelBlocked": "Download notifications are turned off, so progress and controls may be unavailable.", + "reasonNotificationsDisabled": "Notifications are turned off. On Android 13 or newer, they are required for long background downloads.", + "reasonDataSaver": "Data Saver is on, which blocks background downloads on mobile data. Downloads should still run on Wi-Fi.", + "reasonOemUnknown": "Downloads repeatedly stopped while Plezy was in the background. Check Plezy's battery or background usage settings.", + "openSettings": "Open settings", + "stillNotWorking": "Device-specific help", + "stillNotWorkingDescription": "See steps for your device, or send a log from Settings › View Logs if the issue continues.", + "dialogTitle": "Downloads may not finish", + "dialogDownloadAnyway": "Download anyway", + "dialogFixFirst": "Fix this first", + "statusTile": "Background downloads", + "statusOk": "Allowed to run in the background", + "statusBlocked": "Blocked by system settings", + "statusDegraded": "Limited by system settings", + "statusUnknown": "Not checked yet", + "settingsUnavailable": "Couldn't open system settings on this device", + "linkUnavailable": "Couldn't open dontkillmyapp.com on this device" + } }, "shaders": { "title": "Shaders", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 0ec9a15d..be1b6299 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Se requiere iniciar sesión", "syncRuleNotAvailableForProfile": "No disponible para el perfil actual", "syncRuleUnknownServer": "Servidor desconocido", - "syncRuleListCreated": "Regla de sincronización creada" + "syncRuleListCreated": "Regla de sincronización creada", + "backgroundWarning": { + "bannerBlocked": "Las descargas se detendrán al salir de la app", + "bannerDegraded": "Las descargas en segundo plano pueden estar limitadas", + "bannerAction": "Detalles", + "sheetTitle": "Las descargas en segundo plano están bloqueadas", + "sheetTitleDegraded": "Las descargas en segundo plano pueden estar limitadas", + "sheetIntro": "Android impide que Plezy descargue de forma fiable en segundo plano.", + "sheetIntroDegraded": "Tu dispositivo limita cuándo puede descargar Plezy en segundo plano.", + "reasonBackgroundRestricted": "El uso en segundo plano de Plezy está restringido. Configura el uso de batería o en segundo plano como \"Sin restricciones\".", + "reasonStandbyRestricted": "Android ha puesto a Plezy en un estado de espera restringido. Configura el uso de batería como \"Sin restricciones\".", + "reasonDownloadChannelBlocked": "Las notificaciones de descargas están desactivadas, por lo que el progreso y los controles podrían no estar disponibles.", + "reasonNotificationsDisabled": "Las notificaciones están desactivadas. En Android 13 o versiones posteriores, son necesarias para las descargas largas en segundo plano.", + "reasonDataSaver": "El Ahorro de datos está activado y bloquea las descargas en segundo plano con datos móviles. Las descargas deberían seguir funcionando con Wi-Fi.", + "reasonOemUnknown": "Las descargas se detuvieron repetidamente mientras Plezy estaba en segundo plano. Revisa la configuración de batería o de uso en segundo plano de Plezy.", + "openSettings": "Abrir configuración", + "stillNotWorking": "Ayuda específica para tu dispositivo", + "stillNotWorkingDescription": "Consulta los pasos para tu dispositivo o envía un registro desde Configuración › Ver registros si el problema continúa.", + "dialogTitle": "Es posible que las descargas no finalicen", + "dialogDownloadAnyway": "Descargar de todos modos", + "dialogFixFirst": "Solucionarlo primero", + "statusTile": "Descargas en segundo plano", + "statusOk": "Permitidas en segundo plano", + "statusBlocked": "Bloqueadas por la configuración del sistema", + "statusDegraded": "Limitadas por la configuración del sistema", + "statusUnknown": "Aún no se ha comprobado", + "settingsUnavailable": "No se pudo abrir la configuración del sistema en este dispositivo", + "linkUnavailable": "No se pudo abrir dontkillmyapp.com en este dispositivo" + } }, "shaders": { "title": "Shaders", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index d18359ed..8096ef78 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Connexion requise", "syncRuleNotAvailableForProfile": "Non disponible pour le profil actuel", "syncRuleUnknownServer": "Serveur inconnu", - "syncRuleListCreated": "Règle de synchronisation créée" + "syncRuleListCreated": "Règle de synchronisation créée", + "backgroundWarning": { + "bannerBlocked": "Les téléchargements s’arrêteront lorsque vous quitterez l’application", + "bannerDegraded": "Les téléchargements en arrière-plan peuvent être limités", + "bannerAction": "Détails", + "sheetTitle": "Les téléchargements en arrière-plan sont bloqués", + "sheetTitleDegraded": "Les téléchargements en arrière-plan peuvent être limités", + "sheetIntro": "Android empêche Plezy de télécharger de façon fiable en arrière-plan.", + "sheetIntroDegraded": "Votre appareil limite les moments où Plezy peut télécharger en arrière-plan.", + "reasonBackgroundRestricted": "L’utilisation de Plezy en arrière-plan est restreinte. Dans les paramètres de batterie ou d’utilisation en arrière-plan, sélectionnez « Sans restriction ».", + "reasonStandbyRestricted": "Android a placé Plezy en veille restreinte. Réglez l’utilisation de la batterie sur « Sans restriction ».", + "reasonDownloadChannelBlocked": "Les notifications de téléchargement sont désactivées. La progression et les commandes peuvent donc être indisponibles.", + "reasonNotificationsDisabled": "Les notifications sont désactivées. Sur Android 13 ou version ultérieure, elles sont nécessaires pour les longs téléchargements en arrière-plan.", + "reasonDataSaver": "L’Économiseur de données est activé et bloque les téléchargements en arrière-plan via les données mobiles. Ils devraient toujours fonctionner en Wi-Fi.", + "reasonOemUnknown": "Les téléchargements se sont arrêtés plusieurs fois lorsque Plezy était en arrière-plan. Vérifiez les paramètres de batterie ou d’utilisation en arrière-plan de Plezy.", + "openSettings": "Ouvrir les paramètres", + "stillNotWorking": "Aide spécifique à l’appareil", + "stillNotWorkingDescription": "Consultez les étapes adaptées à votre appareil ou, si le problème persiste, envoyez un journal depuis Paramètres › Voir les journaux.", + "dialogTitle": "Les téléchargements risquent de ne pas aboutir", + "dialogDownloadAnyway": "Télécharger quand même", + "dialogFixFirst": "Corriger d’abord", + "statusTile": "Téléchargements en arrière-plan", + "statusOk": "Exécution en arrière-plan autorisée", + "statusBlocked": "Bloqués par les paramètres système", + "statusDegraded": "Limités par les paramètres système", + "statusUnknown": "Pas encore vérifié", + "settingsUnavailable": "Impossible d’ouvrir les paramètres système sur cet appareil", + "linkUnavailable": "Impossible d’ouvrir dontkillmyapp.com sur cet appareil" + } }, "shaders": { "title": "Shaders", diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index fc981163..d7d182c6 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Bejelentkezés szükséges", "syncRuleNotAvailableForProfile": "Nem érhető el a jelenlegi profilhoz", "syncRuleUnknownServer": "Ismeretlen szerver", - "syncRuleListCreated": "Szinkronizálási szabály létrehozva" + "syncRuleListCreated": "Szinkronizálási szabály létrehozva", + "backgroundWarning": { + "bannerBlocked": "A letöltések leállnak, ha kilépsz az alkalmazásból", + "bannerDegraded": "A háttérbeli letöltések korlátozottak lehetnek", + "bannerAction": "Részletek", + "sheetTitle": "A háttérbeli letöltések le vannak tiltva", + "sheetTitleDegraded": "A háttérbeli letöltések korlátozottak lehetnek", + "sheetIntro": "Az Android megakadályozza, hogy a Plezy megbízhatóan töltsön le a háttérben.", + "sheetIntroDegraded": "Az eszközöd korlátozza, hogy a Plezy mikor tölthet le a háttérben.", + "reasonBackgroundRestricted": "A Plezy háttérbeli használata korlátozva van. Állítsd az akkumulátor- vagy háttérhasználatát „Korlátlan” értékre.", + "reasonStandbyRestricted": "Az Android korlátozott készenléti állapotba helyezte a Plezyt. Állítsd az akkumulátorhasználatát „Korlátlan” értékre.", + "reasonDownloadChannelBlocked": "A letöltési értesítések ki vannak kapcsolva, ezért előfordulhat, hogy a folyamatjelzés és a vezérlők nem érhetők el.", + "reasonNotificationsDisabled": "Az értesítések ki vannak kapcsolva. Android 13 vagy újabb rendszeren szükségesek a hosszú háttérbeli letöltésekhez.", + "reasonDataSaver": "Az Adatforgalom-csökkentő be van kapcsolva, ezért mobiladat-kapcsolaton nem működnek a háttérbeli letöltések. Wi-Fi-n továbbra is működniük kell.", + "reasonOemUnknown": "A letöltések többször leálltak, miközben a Plezy a háttérben futott. Ellenőrizd a Plezy akkumulátor- vagy háttérhasználati beállításait.", + "openSettings": "Beállítások megnyitása", + "stillNotWorking": "Eszközspecifikus segítség", + "stillNotWorkingDescription": "Nézd meg az eszközödhöz tartozó lépéseket, vagy ha a probléma továbbra is fennáll, küldj naplót a Beállítások › Naplók megtekintése menüből.", + "dialogTitle": "A letöltések nem biztos, hogy befejeződnek", + "dialogDownloadAnyway": "Letöltés mégis", + "dialogFixFirst": "Előbb javítom", + "statusTile": "Háttérbeli letöltések", + "statusOk": "Futhat a háttérben", + "statusBlocked": "A rendszerbeállítások blokkolják", + "statusDegraded": "A rendszerbeállítások korlátozzák", + "statusUnknown": "Még nincs ellenőrizve", + "settingsUnavailable": "Ezen az eszközön nem sikerült megnyitni a rendszerbeállításokat", + "linkUnavailable": "Ezen az eszközön nem sikerült megnyitni a dontkillmyapp.com webhelyet" + } }, "shaders": { "title": "Shaderek", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 1c429cca..f4430f09 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Accesso richiesto", "syncRuleNotAvailableForProfile": "Non disponibile per il profilo attuale", "syncRuleUnknownServer": "Server sconosciuto", - "syncRuleListCreated": "Regola di sincronizzazione creata" + "syncRuleListCreated": "Regola di sincronizzazione creata", + "backgroundWarning": { + "bannerBlocked": "I download si interromperanno quando esci dall’app", + "bannerDegraded": "I download in background potrebbero essere limitati", + "bannerAction": "Dettagli", + "sheetTitle": "I download in background sono bloccati", + "sheetTitleDegraded": "I download in background potrebbero essere limitati", + "sheetIntro": "Android impedisce a Plezy di scaricare in modo affidabile in background.", + "sheetIntroDegraded": "Il dispositivo limita i momenti in cui Plezy può scaricare in background.", + "reasonBackgroundRestricted": "L’uso in background di Plezy è limitato. Nelle impostazioni della batteria o dell’uso in background, seleziona «Senza restrizioni».", + "reasonStandbyRestricted": "Android ha messo Plezy in uno stato di standby con restrizioni. Imposta l’uso della batteria su «Senza restrizioni».", + "reasonDownloadChannelBlocked": "Le notifiche dei download sono disattivate, quindi l’avanzamento e i controlli potrebbero non essere disponibili.", + "reasonNotificationsDisabled": "Le notifiche sono disattivate. Su Android 13 o versioni successive sono necessarie per i download prolungati in background.", + "reasonDataSaver": "Il Risparmio dati è attivo e blocca i download in background tramite dati mobili. I download dovrebbero comunque funzionare su Wi-Fi.", + "reasonOemUnknown": "I download si sono interrotti più volte mentre Plezy era in background. Controlla le impostazioni della batteria o dell’uso in background di Plezy.", + "openSettings": "Apri le impostazioni", + "stillNotWorking": "Guida specifica per il dispositivo", + "stillNotWorkingDescription": "Consulta la procedura per il tuo dispositivo oppure invia un log da Impostazioni › Visualizza i log se il problema persiste.", + "dialogTitle": "I download potrebbero non terminare", + "dialogDownloadAnyway": "Scarica comunque", + "dialogFixFirst": "Risolvi prima", + "statusTile": "Download in background", + "statusOk": "Esecuzione in background consentita", + "statusBlocked": "Bloccati dalle impostazioni di sistema", + "statusDegraded": "Limitati dalle impostazioni di sistema", + "statusUnknown": "Non ancora verificato", + "settingsUnavailable": "Impossibile aprire le impostazioni di sistema su questo dispositivo", + "linkUnavailable": "Impossibile aprire dontkillmyapp.com su questo dispositivo" + } }, "shaders": { "title": "Shader", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 085fe414..011922e8 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -1203,7 +1203,35 @@ "syncRuleSignInRequired": "サインインが必要", "syncRuleNotAvailableForProfile": "現在のプロフィールでは利用できません", "syncRuleUnknownServer": "不明なサーバー", - "syncRuleListCreated": "同期ルールを作成しました" + "syncRuleListCreated": "同期ルールを作成しました", + "backgroundWarning": { + "bannerBlocked": "アプリの画面を離れると、ダウンロードが停止します", + "bannerDegraded": "バックグラウンドダウンロードが制限される場合があります", + "bannerAction": "詳細", + "sheetTitle": "バックグラウンドダウンロードはブロックされています", + "sheetTitleDegraded": "バックグラウンドダウンロードが制限される場合があります", + "sheetIntro": "Androidにより、Plezyはバックグラウンドで安定してダウンロードできません。", + "sheetIntroDegraded": "端末により、Plezyがバックグラウンドでダウンロードできるタイミングが制限されています。", + "reasonBackgroundRestricted": "Plezyのバックグラウンド使用が制限されています。バッテリー使用量またはバックグラウンド使用を「制限なし」に設定してください。", + "reasonStandbyRestricted": "Androidにより、Plezyが制限付きのスタンバイ状態に設定されています。バッテリー使用量を「制限なし」に設定してください。", + "reasonDownloadChannelBlocked": "ダウンロード通知がオフのため、進行状況や操作ボタンを利用できない場合があります。", + "reasonNotificationsDisabled": "通知がオフです。Android 13以降では、長時間のバックグラウンドダウンロードに通知が必要です。", + "reasonDataSaver": "データセーバーがオンのため、モバイルデータ通信ではバックグラウンドダウンロードがブロックされます。Wi-Fiでは引き続きダウンロードできるはずです。", + "reasonOemUnknown": "Plezyがバックグラウンドで動作中に、ダウンロードが繰り返し停止しました。Plezyのバッテリー使用量またはバックグラウンド使用の設定を確認してください。", + "openSettings": "設定を開く", + "stillNotWorking": "端末別のヘルプ", + "stillNotWorkingDescription": "お使いの端末向けの手順を確認してください。問題が続く場合は、設定 › ログを表示 からログを送信してください。", + "dialogTitle": "ダウンロードが完了しない可能性があります", + "dialogDownloadAnyway": "このままダウンロード", + "dialogFixFirst": "先に設定を修正", + "statusTile": "バックグラウンドダウンロード", + "statusOk": "バックグラウンドで実行可能", + "statusBlocked": "システム設定によりブロック", + "statusDegraded": "システム設定により制限", + "statusUnknown": "未確認", + "settingsUnavailable": "この端末ではシステム設定を開けませんでした", + "linkUnavailable": "この端末ではdontkillmyapp.comを開けませんでした" + } }, "shaders": { "title": "シェーダー", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 4a3b4adc..41e99f49 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -1203,7 +1203,35 @@ "syncRuleSignInRequired": "로그인 필요", "syncRuleNotAvailableForProfile": "현재 프로필에서 사용할 수 없음", "syncRuleUnknownServer": "알 수 없는 서버", - "syncRuleListCreated": "동기화 규칙이 생성되었습니다" + "syncRuleListCreated": "동기화 규칙이 생성되었습니다", + "backgroundWarning": { + "bannerBlocked": "앱에서 나가면 다운로드가 중지됩니다", + "bannerDegraded": "백그라운드 다운로드가 제한될 수 있습니다", + "bannerAction": "세부정보", + "sheetTitle": "백그라운드 다운로드가 차단됨", + "sheetTitleDegraded": "백그라운드 다운로드가 제한될 수 있음", + "sheetIntro": "Android가 Plezy의 안정적인 백그라운드 다운로드를 차단하고 있습니다.", + "sheetIntroDegraded": "기기에서 Plezy가 백그라운드로 다운로드할 수 있는 시점을 제한하고 있습니다.", + "reasonBackgroundRestricted": "Plezy의 백그라운드 사용이 제한되어 있습니다. 배터리 또는 백그라운드 사용을 \"제한 없음\"으로 설정하세요.", + "reasonStandbyRestricted": "Android가 Plezy를 제한된 대기 상태로 전환했습니다. 배터리 사용량을 \"제한 없음\"으로 설정하세요.", + "reasonDownloadChannelBlocked": "다운로드 알림이 꺼져 있어 진행 상황과 제어 기능을 사용하지 못할 수 있습니다.", + "reasonNotificationsDisabled": "알림이 꺼져 있습니다. Android 13 이상에서는 장시간 백그라운드 다운로드에 알림이 필요합니다.", + "reasonDataSaver": "데이터 절약 모드가 켜져 있어 모바일 데이터로 백그라운드 다운로드를 할 수 없습니다. Wi-Fi에서는 계속 다운로드할 수 있습니다.", + "reasonOemUnknown": "Plezy가 백그라운드에 있을 때 다운로드가 반복해서 중지되었습니다. Plezy의 배터리 또는 백그라운드 사용 설정을 확인하세요.", + "openSettings": "설정 열기", + "stillNotWorking": "기기별 도움말", + "stillNotWorkingDescription": "기기에 맞는 단계를 확인하세요. 문제가 계속되면 설정 › 로그 보기에서 로그를 보내 주세요.", + "dialogTitle": "다운로드가 완료되지 않을 수 있음", + "dialogDownloadAnyway": "그래도 다운로드", + "dialogFixFirst": "먼저 문제 해결", + "statusTile": "백그라운드 다운로드", + "statusOk": "백그라운드 실행 허용됨", + "statusBlocked": "시스템 설정에 의해 차단됨", + "statusDegraded": "시스템 설정에 의해 제한됨", + "statusUnknown": "아직 확인하지 않음", + "settingsUnavailable": "이 기기에서 시스템 설정을 열 수 없습니다", + "linkUnavailable": "이 기기에서 dontkillmyapp.com을 열 수 없습니다" + } }, "shaders": { "title": "셰이더", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index d416bda8..22110408 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Innlogging kreves", "syncRuleNotAvailableForProfile": "Ikke tilgjengelig for gjeldende profil", "syncRuleUnknownServer": "Ukjent server", - "syncRuleListCreated": "Synkroniseringsregel opprettet" + "syncRuleListCreated": "Synkroniseringsregel opprettet", + "backgroundWarning": { + "bannerBlocked": "Nedlastinger stopper når du forlater appen", + "bannerDegraded": "Bakgrunnsnedlastinger kan være begrenset", + "bannerAction": "Detaljer", + "sheetTitle": "Bakgrunnsnedlastinger er blokkert", + "sheetTitleDegraded": "Bakgrunnsnedlastinger kan være begrenset", + "sheetIntro": "Android hindrer Plezy i å laste ned pålitelig i bakgrunnen.", + "sheetIntroDegraded": "Enheten din begrenser når Plezy kan laste ned i bakgrunnen.", + "reasonBackgroundRestricted": "Bakgrunnsbruken til Plezy er begrenset. Sett batteribruk eller bakgrunnsbruk til «Ubegrenset».", + "reasonStandbyRestricted": "Android har satt Plezy i begrenset hvilemodus. Sett batteribruken til «Ubegrenset».", + "reasonDownloadChannelBlocked": "Varsler om nedlastinger er slått av, så fremdrift og kontroller kan være utilgjengelige.", + "reasonNotificationsDisabled": "Varsler er slått av. På Android 13 eller nyere kreves de for lange bakgrunnsnedlastinger.", + "reasonDataSaver": "Datasparing er slått på og blokkerer bakgrunnsnedlastinger via mobildata. Nedlastinger skal fortsatt fungere på Wi-Fi.", + "reasonOemUnknown": "Nedlastinger har stoppet gjentatte ganger mens Plezy var i bakgrunnen. Sjekk innstillingene for batteribruk eller bakgrunnsbruk for Plezy.", + "openSettings": "Åpne innstillinger", + "stillNotWorking": "Enhetsspesifikk hjelp", + "stillNotWorkingDescription": "Se fremgangsmåten for enheten din, eller send en logg fra Innstillinger › Vis logger hvis problemet vedvarer.", + "dialogTitle": "Nedlastinger blir kanskje ikke fullført", + "dialogDownloadAnyway": "Last ned likevel", + "dialogFixFirst": "Løs dette først", + "statusTile": "Bakgrunnsnedlastinger", + "statusOk": "Kan kjøre i bakgrunnen", + "statusBlocked": "Blokkert av systeminnstillinger", + "statusDegraded": "Begrenset av systeminnstillinger", + "statusUnknown": "Ikke sjekket ennå", + "settingsUnavailable": "Kunne ikke åpne systeminnstillingene på denne enheten", + "linkUnavailable": "Kunne ikke åpne dontkillmyapp.com på denne enheten" + } }, "shaders": { "title": "Shadere", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index fac4ed21..674fa91e 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Inloggen vereist", "syncRuleNotAvailableForProfile": "Niet beschikbaar voor huidig profiel", "syncRuleUnknownServer": "Onbekende server", - "syncRuleListCreated": "Synchronisatieregel aangemaakt" + "syncRuleListCreated": "Synchronisatieregel aangemaakt", + "backgroundWarning": { + "bannerBlocked": "Downloads stoppen zodra je de app verlaat", + "bannerDegraded": "Downloads op de achtergrond kunnen beperkt zijn", + "bannerAction": "Details", + "sheetTitle": "Downloads op de achtergrond zijn geblokkeerd", + "sheetTitleDegraded": "Downloads op de achtergrond kunnen beperkt zijn", + "sheetIntro": "Android verhindert dat Plezy betrouwbaar op de achtergrond downloadt.", + "sheetIntroDegraded": "Je apparaat beperkt wanneer Plezy op de achtergrond kan downloaden.", + "reasonBackgroundRestricted": "Het achtergrondgebruik van Plezy is beperkt. Stel het batterij- of achtergrondgebruik in op \"Onbeperkt\".", + "reasonStandbyRestricted": "Android heeft Plezy in een beperkte stand-bymodus geplaatst. Stel het batterijgebruik in op \"Onbeperkt\".", + "reasonDownloadChannelBlocked": "Downloadmeldingen zijn uitgeschakeld, waardoor voortgang en bediening mogelijk niet beschikbaar zijn.", + "reasonNotificationsDisabled": "Meldingen zijn uitgeschakeld. Op Android 13 of nieuwer zijn ze vereist voor langdurige downloads op de achtergrond.", + "reasonDataSaver": "Databesparing is ingeschakeld en blokkeert downloads op de achtergrond via mobiele data. Via Wi-Fi zouden downloads nog wel moeten werken.", + "reasonOemUnknown": "Downloads zijn herhaaldelijk gestopt terwijl Plezy op de achtergrond draaide. Controleer de instellingen voor het batterij- of achtergrondgebruik van Plezy.", + "openSettings": "Instellingen openen", + "stillNotWorking": "Apparaatspecifieke hulp", + "stillNotWorkingDescription": "Bekijk de stappen voor je apparaat of stuur een logbestand vanuit Instellingen › Logbestanden bekijken als het probleem aanhoudt.", + "dialogTitle": "Downloads worden mogelijk niet voltooid", + "dialogDownloadAnyway": "Toch downloaden", + "dialogFixFirst": "Dit eerst oplossen", + "statusTile": "Downloads op de achtergrond", + "statusOk": "Mag op de achtergrond worden uitgevoerd", + "statusBlocked": "Geblokkeerd door systeeminstellingen", + "statusDegraded": "Beperkt door systeeminstellingen", + "statusUnknown": "Nog niet gecontroleerd", + "settingsUnavailable": "Kan de systeeminstellingen niet openen op dit apparaat", + "linkUnavailable": "Kan dontkillmyapp.com niet openen op dit apparaat" + } }, "shaders": { "title": "Shaders", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index fff8e8f1..ba1ac2f3 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -1212,7 +1212,35 @@ "syncRuleSignInRequired": "Wymagane logowanie", "syncRuleNotAvailableForProfile": "Niedostępne dla bieżącego profilu", "syncRuleUnknownServer": "Nieznany serwer", - "syncRuleListCreated": "Utworzono regułę synchronizacji" + "syncRuleListCreated": "Utworzono regułę synchronizacji", + "backgroundWarning": { + "bannerBlocked": "Po opuszczeniu aplikacji pobieranie zostanie zatrzymane", + "bannerDegraded": "Pobieranie w tle może być ograniczone", + "bannerAction": "Szczegóły", + "sheetTitle": "Pobieranie w tle jest zablokowane", + "sheetTitleDegraded": "Pobieranie w tle może być ograniczone", + "sheetIntro": "Android uniemożliwia Plezy niezawodne pobieranie plików w tle.", + "sheetIntroDegraded": "Twoje urządzenie ogranicza możliwość pobierania w tle przez Plezy.", + "reasonBackgroundRestricted": "Działanie Plezy w tle jest ograniczone. Ustaw użycie baterii lub działanie w tle na „Bez ograniczeń”.", + "reasonStandbyRestricted": "Android umieścił Plezy w ograniczonym trybie gotowości. Ustaw użycie baterii na „Bez ograniczeń”.", + "reasonDownloadChannelBlocked": "Powiadomienia o pobieraniu są wyłączone, więc postęp i opcje sterowania mogą być niedostępne.", + "reasonNotificationsDisabled": "Powiadomienia są wyłączone. W Android 13 lub nowszym są wymagane przy długim pobieraniu w tle.", + "reasonDataSaver": "Oszczędzanie danych jest włączone, co blokuje pobieranie w tle przez mobilną transmisję danych. Pobieranie powinno nadal działać przez Wi-Fi.", + "reasonOemUnknown": "Pobieranie wielokrotnie przerywało się, gdy Plezy działało w tle. Sprawdź ustawienia baterii lub działania w tle dla Plezy.", + "openSettings": "Otwórz ustawienia", + "stillNotWorking": "Pomoc dla Twojego urządzenia", + "stillNotWorkingDescription": "Zobacz instrukcje dla swojego urządzenia lub, jeśli problem nadal występuje, wyślij log przez Ustawienia › Pokaż logi.", + "dialogTitle": "Pobieranie może się nie zakończyć", + "dialogDownloadAnyway": "Pobierz mimo to", + "dialogFixFirst": "Najpierw rozwiąż problem", + "statusTile": "Pobieranie w tle", + "statusOk": "Działanie w tle jest dozwolone", + "statusBlocked": "Zablokowane przez ustawienia systemu", + "statusDegraded": "Ograniczone przez ustawienia systemu", + "statusUnknown": "Jeszcze nie sprawdzono", + "settingsUnavailable": "Nie udało się otworzyć ustawień systemowych na tym urządzeniu", + "linkUnavailable": "Nie udało się otworzyć dontkillmyapp.com na tym urządzeniu" + } }, "shaders": { "title": "Shadery", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 4dbe215d..3d2c2ccd 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "É necessário entrar", "syncRuleNotAvailableForProfile": "Indisponível para o perfil atual", "syncRuleUnknownServer": "Servidor desconhecido", - "syncRuleListCreated": "Regra de sincronização criada" + "syncRuleListCreated": "Regra de sincronização criada", + "backgroundWarning": { + "bannerBlocked": "Os downloads serão interrompidos ao sair do app", + "bannerDegraded": "Os downloads em segundo plano podem ser limitados", + "bannerAction": "Detalhes", + "sheetTitle": "Os downloads em segundo plano estão bloqueados", + "sheetTitleDegraded": "Os downloads em segundo plano podem ser limitados", + "sheetIntro": "O Android está impedindo que o Plezy faça downloads de forma confiável em segundo plano.", + "sheetIntroDegraded": "Seu dispositivo está limitando quando o Plezy pode fazer downloads em segundo plano.", + "reasonBackgroundRestricted": "O uso em segundo plano do Plezy está restrito. Defina o uso da bateria ou o uso em segundo plano como \"Sem restrições\".", + "reasonStandbyRestricted": "O Android colocou o Plezy em um modo de espera restrito. Defina o uso da bateria como \"Sem restrições\".", + "reasonDownloadChannelBlocked": "As notificações de download estão desativadas; por isso, o progresso e os controles podem ficar indisponíveis.", + "reasonNotificationsDisabled": "As notificações estão desativadas. No Android 13 ou mais recente, elas são necessárias para downloads longos em segundo plano.", + "reasonDataSaver": "A Economia de dados está ativada e bloqueia downloads em segundo plano usando dados móveis. Os downloads ainda devem funcionar no Wi-Fi.", + "reasonOemUnknown": "Os downloads foram interrompidos várias vezes enquanto o Plezy estava em segundo plano. Verifique as configurações de bateria ou uso em segundo plano do Plezy.", + "openSettings": "Abrir configurações", + "stillNotWorking": "Ajuda específica para o dispositivo", + "stillNotWorkingDescription": "Veja as instruções para seu dispositivo ou, se o problema persistir, envie um log em Configurações › Ver Logs.", + "dialogTitle": "Os downloads podem não ser concluídos", + "dialogDownloadAnyway": "Baixar mesmo assim", + "dialogFixFirst": "Corrigir primeiro", + "statusTile": "Downloads em segundo plano", + "statusOk": "Execução em segundo plano permitida", + "statusBlocked": "Bloqueado pelas configurações do sistema", + "statusDegraded": "Limitado pelas configurações do sistema", + "statusUnknown": "Ainda não verificado", + "settingsUnavailable": "Não foi possível abrir as configurações do sistema neste dispositivo", + "linkUnavailable": "Não foi possível abrir dontkillmyapp.com neste dispositivo" + } }, "shaders": { "title": "Shaders", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 10549c3f..fbefd1e9 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -1212,7 +1212,35 @@ "syncRuleSignInRequired": "Требуется вход", "syncRuleNotAvailableForProfile": "Недоступно для текущего профиля", "syncRuleUnknownServer": "Неизвестный сервер", - "syncRuleListCreated": "Правило синхронизации создано" + "syncRuleListCreated": "Правило синхронизации создано", + "backgroundWarning": { + "bannerBlocked": "Загрузки остановятся, когда вы выйдете из приложения", + "bannerDegraded": "Фоновые загрузки могут быть ограничены", + "bannerAction": "Подробнее", + "sheetTitle": "Фоновые загрузки заблокированы", + "sheetTitleDegraded": "Фоновые загрузки могут быть ограничены", + "sheetIntro": "Android не позволяет Plezy стабильно выполнять загрузки в фоновом режиме.", + "sheetIntroDegraded": "Ваше устройство ограничивает время, когда Plezy может выполнять загрузки в фоновом режиме.", + "reasonBackgroundRestricted": "Фоновая работа Plezy ограничена. В настройках расхода заряда батареи или фоновой работы выберите «Без ограничений».", + "reasonStandbyRestricted": "Android перевёл Plezy в режим ожидания с ограничениями. Для расхода заряда батареи выберите «Без ограничений».", + "reasonDownloadChannelBlocked": "Уведомления о загрузках отключены, поэтому ход загрузок и элементы управления могут быть недоступны.", + "reasonNotificationsDisabled": "Уведомления отключены. В Android 13 и более новых версиях они необходимы для длительных фоновых загрузок.", + "reasonDataSaver": "Экономия трафика включена, поэтому фоновые загрузки через мобильную сеть заблокированы. Загрузки по Wi-Fi должны работать.", + "reasonOemUnknown": "Загрузки неоднократно останавливались, пока Plezy работал в фоне. Проверьте настройки расхода заряда батареи или фоновой работы для Plezy.", + "openSettings": "Открыть настройки", + "stillNotWorking": "Помощь для вашего устройства", + "stillNotWorkingDescription": "Посмотрите инструкции для своего устройства. Если проблема не исчезнет, отправьте лог через Настройки › Просмотр логов.", + "dialogTitle": "Загрузки могут не завершиться", + "dialogDownloadAnyway": "Всё равно скачать", + "dialogFixFirst": "Сначала исправить", + "statusTile": "Фоновые загрузки", + "statusOk": "Работа в фоне разрешена", + "statusBlocked": "Заблокировано настройками системы", + "statusDegraded": "Ограничено настройками системы", + "statusUnknown": "Ещё не проверено", + "settingsUnavailable": "Не удалось открыть системные настройки на этом устройстве", + "linkUnavailable": "Не удалось открыть dontkillmyapp.com на этом устройстве" + } }, "shaders": { "title": "Шейдеры", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index d4b9687a..89de0e64 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 18 -/// Strings: 26031 (1446 per locale) +/// Strings: 26499 (1472 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_bg.g.dart b/lib/i18n/strings_bg.g.dart index 6b9ad49f..213684e8 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$bg extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Не е налично за текущия профил'; @override String get syncRuleUnknownServer => 'Неизвестен сървър'; @override String get syncRuleListCreated => 'Правилото за синхронизация е създадено'; + @override late final _Translations$downloads$backgroundWarning$bg backgroundWarning = _Translations$downloads$backgroundWarning$bg._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$bg extends Translations$explore$status$en { @override String get upcoming => 'Предстоящ'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$bg extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$bg._(TranslationsBg root) : this._root = root, super.internal(root); + + final TranslationsBg _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Изтеглянията ще спрат, когато излезете от приложението'; + @override String get bannerDegraded => 'Изтеглянията във фонов режим може да бъдат ограничени'; + @override String get bannerAction => 'Подробности'; + @override String get sheetTitle => 'Изтеглянията във фонов режим са блокирани'; + @override String get sheetTitleDegraded => 'Изтеглянията във фонов режим може да бъдат ограничени'; + @override String get sheetIntro => 'Android не позволява на Plezy да изтегля надеждно във фонов режим.'; + @override String get sheetIntroDegraded => 'Устройството ви ограничава кога Plezy може да изтегля във фонов режим.'; + @override String get reasonBackgroundRestricted => 'Работата на Plezy във фонов режим е ограничена. Задайте използването на батерията или работата във фонов режим на „Без ограничения“.'; + @override String get reasonStandbyRestricted => 'Android е поставил Plezy в ограничено състояние на готовност. Задайте използването на батерията на „Без ограничения“.'; + @override String get reasonDownloadChannelBlocked => 'Известията за изтегляния са изключени, затова напредъкът и контролите може да не са достъпни.'; + @override String get reasonNotificationsDisabled => 'Известията са изключени. В Android 13 или по-нова версия те са необходими за продължителни изтегляния във фонов режим.'; + @override String get reasonDataSaver => '„Икономия на данни“ е включена и блокира изтеглянията във фонов режим през мобилни данни. Изтеглянията би трябвало да продължат през Wi-Fi.'; + @override String get reasonOemUnknown => 'Изтеглянията спираха многократно, докато Plezy беше във фонов режим. Проверете настройките за батерията или работата на Plezy във фонов режим.'; + @override String get openSettings => 'Отвори настройките'; + @override String get stillNotWorking => 'Помощ за конкретното устройство'; + @override String get stillNotWorkingDescription => 'Вижте стъпките за устройството си или изпратете лог от Настройки › Виж логовете, ако проблемът продължи.'; + @override String get dialogTitle => 'Изтеглянията може да не завършат'; + @override String get dialogDownloadAnyway => 'Изтегли въпреки това'; + @override String get dialogFixFirst => 'Първо отстрани проблема'; + @override String get statusTile => 'Изтегляния във фонов режим'; + @override String get statusOk => 'Разрешена е работа във фонов режим'; + @override String get statusBlocked => 'Блокирани от системните настройки'; + @override String get statusDegraded => 'Ограничени от системните настройки'; + @override String get statusUnknown => 'Все още не е проверено'; + @override String get settingsUnavailable => 'Системните настройки не можаха да се отворят на това устройство'; + @override String get linkUnavailable => 'dontkillmyapp.com не можа да се отвори на това устройство'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$bg extends Translations$companionRemote$session$en { _Translations$companionRemote$session$bg._(TranslationsBg root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsBg { 'downloads.syncRuleNotAvailableForProfile' => 'Не е налично за текущия профил', 'downloads.syncRuleUnknownServer' => 'Неизвестен сървър', 'downloads.syncRuleListCreated' => 'Правилото за синхронизация е създадено', + 'downloads.backgroundWarning.bannerBlocked' => 'Изтеглянията ще спрат, когато излезете от приложението', + 'downloads.backgroundWarning.bannerDegraded' => 'Изтеглянията във фонов режим може да бъдат ограничени', + 'downloads.backgroundWarning.bannerAction' => 'Подробности', + 'downloads.backgroundWarning.sheetTitle' => 'Изтеглянията във фонов режим са блокирани', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Изтеглянията във фонов режим може да бъдат ограничени', + 'downloads.backgroundWarning.sheetIntro' => 'Android не позволява на Plezy да изтегля надеждно във фонов режим.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Устройството ви ограничава кога Plezy може да изтегля във фонов режим.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Работата на Plezy във фонов режим е ограничена. Задайте използването на батерията или работата във фонов режим на „Без ограничения“.', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android е поставил Plezy в ограничено състояние на готовност. Задайте използването на батерията на „Без ограничения“.', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Известията за изтегляния са изключени, затова напредъкът и контролите може да не са достъпни.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Известията са изключени. В Android 13 или по-нова версия те са необходими за продължителни изтегляния във фонов режим.', + 'downloads.backgroundWarning.reasonDataSaver' => '„Икономия на данни“ е включена и блокира изтеглянията във фонов режим през мобилни данни. Изтеглянията би трябвало да продължат през Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Изтеглянията спираха многократно, докато Plezy беше във фонов режим. Проверете настройките за батерията или работата на Plezy във фонов режим.', + 'downloads.backgroundWarning.openSettings' => 'Отвори настройките', + 'downloads.backgroundWarning.stillNotWorking' => 'Помощ за конкретното устройство', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Вижте стъпките за устройството си или изпратете лог от Настройки › Виж логовете, ако проблемът продължи.', + 'downloads.backgroundWarning.dialogTitle' => 'Изтеглянията може да не завършат', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Изтегли въпреки това', + 'downloads.backgroundWarning.dialogFixFirst' => 'Първо отстрани проблема', + 'downloads.backgroundWarning.statusTile' => 'Изтегляния във фонов режим', + 'downloads.backgroundWarning.statusOk' => 'Разрешена е работа във фонов режим', + 'downloads.backgroundWarning.statusBlocked' => 'Блокирани от системните настройки', + 'downloads.backgroundWarning.statusDegraded' => 'Ограничени от системните настройки', + 'downloads.backgroundWarning.statusUnknown' => 'Все още не е проверено', + 'downloads.backgroundWarning.settingsUnavailable' => 'Системните настройки не можаха да се отворят на това устройство', + 'downloads.backgroundWarning.linkUnavailable' => 'dontkillmyapp.com не можа да се отвори на това устройство', 'shaders.title' => 'Шейдъри', 'shaders.noShaderDescription' => 'Без видео подобрение', 'shaders.nvscalerDescription' => 'Мащабиране на изображението чрез NVIDIA за по-рязко видео', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index 911d0e37..ad1381e1 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$da extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Ikke tilgængelig for nuværende profil'; @override String get syncRuleUnknownServer => 'Ukendt server'; @override String get syncRuleListCreated => 'Synkroniseringsregel oprettet'; + @override late final _Translations$downloads$backgroundWarning$da backgroundWarning = _Translations$downloads$backgroundWarning$da._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$da extends Translations$explore$status$en { @override String get upcoming => 'Kommende'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$da extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$da._(TranslationsDa root) : this._root = root, super.internal(root); + + final TranslationsDa _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Downloads stopper, når du forlader appen'; + @override String get bannerDegraded => 'Downloads i baggrunden kan være begrænsede'; + @override String get bannerAction => 'Detaljer'; + @override String get sheetTitle => 'Downloads i baggrunden er blokeret'; + @override String get sheetTitleDegraded => 'Downloads i baggrunden kan være begrænsede'; + @override String get sheetIntro => 'Android forhindrer Plezy i at downloade stabilt i baggrunden.'; + @override String get sheetIntroDegraded => 'Din enhed begrænser, hvornår Plezy kan downloade i baggrunden.'; + @override String get reasonBackgroundRestricted => 'Plezys baggrundsaktivitet er begrænset. Indstil batteriforbruget eller baggrundsaktiviteten til "Ubegrænset".'; + @override String get reasonStandbyRestricted => 'Android har sat Plezy i begrænset standbytilstand. Indstil batteriforbruget til "Ubegrænset".'; + @override String get reasonDownloadChannelBlocked => 'Notifikationer om downloads er slået fra, så status og betjeningsknapper muligvis ikke er tilgængelige.'; + @override String get reasonNotificationsDisabled => 'Notifikationer er slået fra. På Android 13 eller nyere er de nødvendige ved lange downloads i baggrunden.'; + @override String get reasonDataSaver => 'Datasparefunktionen er slået til, hvilket blokerer downloads i baggrunden via mobildata. Downloads bør stadig køre på Wi-Fi.'; + @override String get reasonOemUnknown => 'Downloads stoppede gentagne gange, mens Plezy var i baggrunden. Tjek Plezys indstillinger for batteriforbrug eller baggrundsaktivitet.'; + @override String get openSettings => 'Åbn indstillinger'; + @override String get stillNotWorking => 'Enhedsspecifik hjælp'; + @override String get stillNotWorkingDescription => 'Se vejledningen til din enhed, eller send en logfil fra Indstillinger › Vis logfiler, hvis problemet fortsætter.'; + @override String get dialogTitle => 'Downloads bliver muligvis ikke færdige'; + @override String get dialogDownloadAnyway => 'Download alligevel'; + @override String get dialogFixFirst => 'Løs dette først'; + @override String get statusTile => 'Downloads i baggrunden'; + @override String get statusOk => 'Må køre i baggrunden'; + @override String get statusBlocked => 'Blokeret af systemindstillinger'; + @override String get statusDegraded => 'Begrænset af systemindstillinger'; + @override String get statusUnknown => 'Endnu ikke kontrolleret'; + @override String get settingsUnavailable => 'Kunne ikke åbne systemindstillingerne på denne enhed'; + @override String get linkUnavailable => 'Kunne ikke åbne dontkillmyapp.com på denne enhed'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$da extends Translations$companionRemote$session$en { _Translations$companionRemote$session$da._(TranslationsDa root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsDa { 'downloads.syncRuleNotAvailableForProfile' => 'Ikke tilgængelig for nuværende profil', 'downloads.syncRuleUnknownServer' => 'Ukendt server', 'downloads.syncRuleListCreated' => 'Synkroniseringsregel oprettet', + 'downloads.backgroundWarning.bannerBlocked' => 'Downloads stopper, når du forlader appen', + 'downloads.backgroundWarning.bannerDegraded' => 'Downloads i baggrunden kan være begrænsede', + 'downloads.backgroundWarning.bannerAction' => 'Detaljer', + 'downloads.backgroundWarning.sheetTitle' => 'Downloads i baggrunden er blokeret', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Downloads i baggrunden kan være begrænsede', + 'downloads.backgroundWarning.sheetIntro' => 'Android forhindrer Plezy i at downloade stabilt i baggrunden.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Din enhed begrænser, hvornår Plezy kan downloade i baggrunden.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezys baggrundsaktivitet er begrænset. Indstil batteriforbruget eller baggrundsaktiviteten til "Ubegrænset".', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android har sat Plezy i begrænset standbytilstand. Indstil batteriforbruget til "Ubegrænset".', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Notifikationer om downloads er slået fra, så status og betjeningsknapper muligvis ikke er tilgængelige.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Notifikationer er slået fra. På Android 13 eller nyere er de nødvendige ved lange downloads i baggrunden.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Datasparefunktionen er slået til, hvilket blokerer downloads i baggrunden via mobildata. Downloads bør stadig køre på Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Downloads stoppede gentagne gange, mens Plezy var i baggrunden. Tjek Plezys indstillinger for batteriforbrug eller baggrundsaktivitet.', + 'downloads.backgroundWarning.openSettings' => 'Åbn indstillinger', + 'downloads.backgroundWarning.stillNotWorking' => 'Enhedsspecifik hjælp', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Se vejledningen til din enhed, eller send en logfil fra Indstillinger › Vis logfiler, hvis problemet fortsætter.', + 'downloads.backgroundWarning.dialogTitle' => 'Downloads bliver muligvis ikke færdige', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Download alligevel', + 'downloads.backgroundWarning.dialogFixFirst' => 'Løs dette først', + 'downloads.backgroundWarning.statusTile' => 'Downloads i baggrunden', + 'downloads.backgroundWarning.statusOk' => 'Må køre i baggrunden', + 'downloads.backgroundWarning.statusBlocked' => 'Blokeret af systemindstillinger', + 'downloads.backgroundWarning.statusDegraded' => 'Begrænset af systemindstillinger', + 'downloads.backgroundWarning.statusUnknown' => 'Endnu ikke kontrolleret', + 'downloads.backgroundWarning.settingsUnavailable' => 'Kunne ikke åbne systemindstillingerne på denne enhed', + 'downloads.backgroundWarning.linkUnavailable' => 'Kunne ikke åbne dontkillmyapp.com på denne enhed', 'shaders.title' => 'Shadere', 'shaders.noShaderDescription' => 'Ingen videoforbedring', 'shaders.nvscalerDescription' => 'NVIDIA-billedskalering for skarpere video', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index bdc04483..3a9e9320 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$de extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Für das aktuelle Profil nicht verfügbar'; @override String get syncRuleUnknownServer => 'Unbekannter Server'; @override String get syncRuleListCreated => 'Synchronisierungsregel erstellt'; + @override late final _Translations$downloads$backgroundWarning$de backgroundWarning = _Translations$downloads$backgroundWarning$de._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$de extends Translations$explore$status$en { @override String get upcoming => 'Demnächst'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$de extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$de._(TranslationsDe root) : this._root = root, super.internal(root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Downloads werden gestoppt, wenn du die App verlässt'; + @override String get bannerDegraded => 'Downloads im Hintergrund sind möglicherweise eingeschränkt'; + @override String get bannerAction => 'Details'; + @override String get sheetTitle => 'Downloads im Hintergrund sind blockiert'; + @override String get sheetTitleDegraded => 'Downloads im Hintergrund sind möglicherweise eingeschränkt'; + @override String get sheetIntro => 'Android verhindert, dass Plezy zuverlässig im Hintergrund herunterlädt.'; + @override String get sheetIntroDegraded => 'Dein Gerät schränkt ein, wann Plezy im Hintergrund herunterladen kann.'; + @override String get reasonBackgroundRestricted => 'Die Hintergrundnutzung von Plezy ist eingeschränkt. Stelle die Akku- oder Hintergrundnutzung auf „Uneingeschränkt“.'; + @override String get reasonStandbyRestricted => 'Android hat Plezy in einen eingeschränkten Standby-Modus versetzt. Stelle die Akkunutzung auf „Uneingeschränkt“.'; + @override String get reasonDownloadChannelBlocked => 'Download-Benachrichtigungen sind deaktiviert. Fortschritt und Steuerelemente sind daher möglicherweise nicht verfügbar.'; + @override String get reasonNotificationsDisabled => 'Benachrichtigungen sind deaktiviert. Ab Android 13 sind sie für lange Downloads im Hintergrund erforderlich.'; + @override String get reasonDataSaver => 'Der Datensparmodus ist aktiviert und blockiert Downloads im Hintergrund über mobile Daten. Über Wi-Fi sollten Downloads weiterhin funktionieren.'; + @override String get reasonOemUnknown => 'Downloads wurden wiederholt gestoppt, während Plezy im Hintergrund war. Prüfe die Einstellungen zur Akku- oder Hintergrundnutzung von Plezy.'; + @override String get openSettings => 'Einstellungen öffnen'; + @override String get stillNotWorking => 'Gerätespezifische Hilfe'; + @override String get stillNotWorkingDescription => 'Sieh dir die Schritte für dein Gerät an oder sende bei anhaltendem Problem ein Protokoll über Einstellungen › Protokolle anzeigen.'; + @override String get dialogTitle => 'Downloads werden möglicherweise nicht abgeschlossen'; + @override String get dialogDownloadAnyway => 'Trotzdem herunterladen'; + @override String get dialogFixFirst => 'Zuerst beheben'; + @override String get statusTile => 'Downloads im Hintergrund'; + @override String get statusOk => 'Ausführung im Hintergrund erlaubt'; + @override String get statusBlocked => 'Durch Systemeinstellungen blockiert'; + @override String get statusDegraded => 'Durch Systemeinstellungen eingeschränkt'; + @override String get statusUnknown => 'Noch nicht geprüft'; + @override String get settingsUnavailable => 'Die Systemeinstellungen konnten auf diesem Gerät nicht geöffnet werden'; + @override String get linkUnavailable => 'dontkillmyapp.com konnte auf diesem Gerät nicht geöffnet werden'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$de extends Translations$companionRemote$session$en { _Translations$companionRemote$session$de._(TranslationsDe root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsDe { 'downloads.syncRuleNotAvailableForProfile' => 'Für das aktuelle Profil nicht verfügbar', 'downloads.syncRuleUnknownServer' => 'Unbekannter Server', 'downloads.syncRuleListCreated' => 'Synchronisierungsregel erstellt', + 'downloads.backgroundWarning.bannerBlocked' => 'Downloads werden gestoppt, wenn du die App verlässt', + 'downloads.backgroundWarning.bannerDegraded' => 'Downloads im Hintergrund sind möglicherweise eingeschränkt', + 'downloads.backgroundWarning.bannerAction' => 'Details', + 'downloads.backgroundWarning.sheetTitle' => 'Downloads im Hintergrund sind blockiert', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Downloads im Hintergrund sind möglicherweise eingeschränkt', + 'downloads.backgroundWarning.sheetIntro' => 'Android verhindert, dass Plezy zuverlässig im Hintergrund herunterlädt.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Dein Gerät schränkt ein, wann Plezy im Hintergrund herunterladen kann.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Die Hintergrundnutzung von Plezy ist eingeschränkt. Stelle die Akku- oder Hintergrundnutzung auf „Uneingeschränkt“.', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android hat Plezy in einen eingeschränkten Standby-Modus versetzt. Stelle die Akkunutzung auf „Uneingeschränkt“.', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Download-Benachrichtigungen sind deaktiviert. Fortschritt und Steuerelemente sind daher möglicherweise nicht verfügbar.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Benachrichtigungen sind deaktiviert. Ab Android 13 sind sie für lange Downloads im Hintergrund erforderlich.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Der Datensparmodus ist aktiviert und blockiert Downloads im Hintergrund über mobile Daten. Über Wi-Fi sollten Downloads weiterhin funktionieren.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Downloads wurden wiederholt gestoppt, während Plezy im Hintergrund war. Prüfe die Einstellungen zur Akku- oder Hintergrundnutzung von Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Einstellungen öffnen', + 'downloads.backgroundWarning.stillNotWorking' => 'Gerätespezifische Hilfe', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Sieh dir die Schritte für dein Gerät an oder sende bei anhaltendem Problem ein Protokoll über Einstellungen › Protokolle anzeigen.', + 'downloads.backgroundWarning.dialogTitle' => 'Downloads werden möglicherweise nicht abgeschlossen', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Trotzdem herunterladen', + 'downloads.backgroundWarning.dialogFixFirst' => 'Zuerst beheben', + 'downloads.backgroundWarning.statusTile' => 'Downloads im Hintergrund', + 'downloads.backgroundWarning.statusOk' => 'Ausführung im Hintergrund erlaubt', + 'downloads.backgroundWarning.statusBlocked' => 'Durch Systemeinstellungen blockiert', + 'downloads.backgroundWarning.statusDegraded' => 'Durch Systemeinstellungen eingeschränkt', + 'downloads.backgroundWarning.statusUnknown' => 'Noch nicht geprüft', + 'downloads.backgroundWarning.settingsUnavailable' => 'Die Systemeinstellungen konnten auf diesem Gerät nicht geöffnet werden', + 'downloads.backgroundWarning.linkUnavailable' => 'dontkillmyapp.com konnte auf diesem Gerät nicht geöffnet werden', 'shaders.title' => 'Shader', 'shaders.noShaderDescription' => 'Keine Videoverbesserung', 'shaders.nvscalerDescription' => 'NVIDIA-Bildskalierung für schärferes Video', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 4a111dcf..3157693a 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -3497,6 +3497,8 @@ class Translations$downloads$en { /// en: 'Sync rule created' String get syncRuleListCreated => 'Sync rule created'; + + late final Translations$downloads$backgroundWarning$en backgroundWarning = Translations$downloads$backgroundWarning$en.internal(_root); } // Path: shaders @@ -4709,6 +4711,93 @@ class Translations$explore$status$en { String get upcoming => 'Upcoming'; } +// Path: downloads.backgroundWarning +class Translations$downloads$backgroundWarning$en { + Translations$downloads$backgroundWarning$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Downloads will stop when you leave the app' + String get bannerBlocked => 'Downloads will stop when you leave the app'; + + /// en: 'Background downloads may be limited' + String get bannerDegraded => 'Background downloads may be limited'; + + /// en: 'Details' + String get bannerAction => 'Details'; + + /// en: 'Background downloads are blocked' + String get sheetTitle => 'Background downloads are blocked'; + + /// en: 'Background downloads may be limited' + String get sheetTitleDegraded => 'Background downloads may be limited'; + + /// en: 'Android is preventing Plezy from downloading reliably in the background.' + String get sheetIntro => 'Android is preventing Plezy from downloading reliably in the background.'; + + /// en: 'Your device is limiting when Plezy can download in the background.' + String get sheetIntroDegraded => 'Your device is limiting when Plezy can download in the background.'; + + /// en: 'Plezy's background usage is restricted. Set its battery or background usage to "Unrestricted".' + String get reasonBackgroundRestricted => 'Plezy\'s background usage is restricted. Set its battery or background usage to "Unrestricted".'; + + /// en: 'Android has put Plezy in a restricted standby state. Set its battery usage to "Unrestricted".' + String get reasonStandbyRestricted => 'Android has put Plezy in a restricted standby state. Set its battery usage to "Unrestricted".'; + + /// en: 'Download notifications are turned off, so progress and controls may be unavailable.' + String get reasonDownloadChannelBlocked => 'Download notifications are turned off, so progress and controls may be unavailable.'; + + /// en: 'Notifications are turned off. On Android 13 or newer, they are required for long background downloads.' + String get reasonNotificationsDisabled => 'Notifications are turned off. On Android 13 or newer, they are required for long background downloads.'; + + /// en: 'Data Saver is on, which blocks background downloads on mobile data. Downloads should still run on Wi-Fi.' + String get reasonDataSaver => 'Data Saver is on, which blocks background downloads on mobile data. Downloads should still run on Wi-Fi.'; + + /// en: 'Downloads repeatedly stopped while Plezy was in the background. Check Plezy's battery or background usage settings.' + String get reasonOemUnknown => 'Downloads repeatedly stopped while Plezy was in the background. Check Plezy\'s battery or background usage settings.'; + + /// en: 'Open settings' + String get openSettings => 'Open settings'; + + /// en: 'Device-specific help' + String get stillNotWorking => 'Device-specific help'; + + /// en: 'See steps for your device, or send a log from Settings › View Logs if the issue continues.' + String get stillNotWorkingDescription => 'See steps for your device, or send a log from Settings › View Logs if the issue continues.'; + + /// en: 'Downloads may not finish' + String get dialogTitle => 'Downloads may not finish'; + + /// en: 'Download anyway' + String get dialogDownloadAnyway => 'Download anyway'; + + /// en: 'Fix this first' + String get dialogFixFirst => 'Fix this first'; + + /// en: 'Background downloads' + String get statusTile => 'Background downloads'; + + /// en: 'Allowed to run in the background' + String get statusOk => 'Allowed to run in the background'; + + /// en: 'Blocked by system settings' + String get statusBlocked => 'Blocked by system settings'; + + /// en: 'Limited by system settings' + String get statusDegraded => 'Limited by system settings'; + + /// en: 'Not checked yet' + String get statusUnknown => 'Not checked yet'; + + /// en: 'Couldn't open system settings on this device' + String get settingsUnavailable => 'Couldn\'t open system settings on this device'; + + /// en: 'Couldn't open dontkillmyapp.com on this device' + String get linkUnavailable => 'Couldn\'t open dontkillmyapp.com on this device'; +} + // Path: companionRemote.session class Translations$companionRemote$session$en { Translations$companionRemote$session$en.internal(this._root); @@ -6166,6 +6255,32 @@ extension on Translations { 'downloads.syncRuleNotAvailableForProfile' => 'Not available for current profile', 'downloads.syncRuleUnknownServer' => 'Unknown server', 'downloads.syncRuleListCreated' => 'Sync rule created', + 'downloads.backgroundWarning.bannerBlocked' => 'Downloads will stop when you leave the app', + 'downloads.backgroundWarning.bannerDegraded' => 'Background downloads may be limited', + 'downloads.backgroundWarning.bannerAction' => 'Details', + 'downloads.backgroundWarning.sheetTitle' => 'Background downloads are blocked', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Background downloads may be limited', + 'downloads.backgroundWarning.sheetIntro' => 'Android is preventing Plezy from downloading reliably in the background.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Your device is limiting when Plezy can download in the background.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezy\'s background usage is restricted. Set its battery or background usage to "Unrestricted".', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android has put Plezy in a restricted standby state. Set its battery usage to "Unrestricted".', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Download notifications are turned off, so progress and controls may be unavailable.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Notifications are turned off. On Android 13 or newer, they are required for long background downloads.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Data Saver is on, which blocks background downloads on mobile data. Downloads should still run on Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Downloads repeatedly stopped while Plezy was in the background. Check Plezy\'s battery or background usage settings.', + 'downloads.backgroundWarning.openSettings' => 'Open settings', + 'downloads.backgroundWarning.stillNotWorking' => 'Device-specific help', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'See steps for your device, or send a log from Settings › View Logs if the issue continues.', + 'downloads.backgroundWarning.dialogTitle' => 'Downloads may not finish', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Download anyway', + 'downloads.backgroundWarning.dialogFixFirst' => 'Fix this first', + 'downloads.backgroundWarning.statusTile' => 'Background downloads', + 'downloads.backgroundWarning.statusOk' => 'Allowed to run in the background', + 'downloads.backgroundWarning.statusBlocked' => 'Blocked by system settings', + 'downloads.backgroundWarning.statusDegraded' => 'Limited by system settings', + 'downloads.backgroundWarning.statusUnknown' => 'Not checked yet', + 'downloads.backgroundWarning.settingsUnavailable' => 'Couldn\'t open system settings on this device', + 'downloads.backgroundWarning.linkUnavailable' => 'Couldn\'t open dontkillmyapp.com on this device', 'shaders.title' => 'Shaders', 'shaders.noShaderDescription' => 'No video enhancement', 'shaders.nvscalerDescription' => 'NVIDIA image scaling for sharper video', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 6917e1e7..2e2b2de2 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$es extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'No disponible para el perfil actual'; @override String get syncRuleUnknownServer => 'Servidor desconocido'; @override String get syncRuleListCreated => 'Regla de sincronización creada'; + @override late final _Translations$downloads$backgroundWarning$es backgroundWarning = _Translations$downloads$backgroundWarning$es._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$es extends Translations$explore$status$en { @override String get upcoming => 'Próximamente'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$es extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Las descargas se detendrán al salir de la app'; + @override String get bannerDegraded => 'Las descargas en segundo plano pueden estar limitadas'; + @override String get bannerAction => 'Detalles'; + @override String get sheetTitle => 'Las descargas en segundo plano están bloqueadas'; + @override String get sheetTitleDegraded => 'Las descargas en segundo plano pueden estar limitadas'; + @override String get sheetIntro => 'Android impide que Plezy descargue de forma fiable en segundo plano.'; + @override String get sheetIntroDegraded => 'Tu dispositivo limita cuándo puede descargar Plezy en segundo plano.'; + @override String get reasonBackgroundRestricted => 'El uso en segundo plano de Plezy está restringido. Configura el uso de batería o en segundo plano como "Sin restricciones".'; + @override String get reasonStandbyRestricted => 'Android ha puesto a Plezy en un estado de espera restringido. Configura el uso de batería como "Sin restricciones".'; + @override String get reasonDownloadChannelBlocked => 'Las notificaciones de descargas están desactivadas, por lo que el progreso y los controles podrían no estar disponibles.'; + @override String get reasonNotificationsDisabled => 'Las notificaciones están desactivadas. En Android 13 o versiones posteriores, son necesarias para las descargas largas en segundo plano.'; + @override String get reasonDataSaver => 'El Ahorro de datos está activado y bloquea las descargas en segundo plano con datos móviles. Las descargas deberían seguir funcionando con Wi-Fi.'; + @override String get reasonOemUnknown => 'Las descargas se detuvieron repetidamente mientras Plezy estaba en segundo plano. Revisa la configuración de batería o de uso en segundo plano de Plezy.'; + @override String get openSettings => 'Abrir configuración'; + @override String get stillNotWorking => 'Ayuda específica para tu dispositivo'; + @override String get stillNotWorkingDescription => 'Consulta los pasos para tu dispositivo o envía un registro desde Configuración › Ver registros si el problema continúa.'; + @override String get dialogTitle => 'Es posible que las descargas no finalicen'; + @override String get dialogDownloadAnyway => 'Descargar de todos modos'; + @override String get dialogFixFirst => 'Solucionarlo primero'; + @override String get statusTile => 'Descargas en segundo plano'; + @override String get statusOk => 'Permitidas en segundo plano'; + @override String get statusBlocked => 'Bloqueadas por la configuración del sistema'; + @override String get statusDegraded => 'Limitadas por la configuración del sistema'; + @override String get statusUnknown => 'Aún no se ha comprobado'; + @override String get settingsUnavailable => 'No se pudo abrir la configuración del sistema en este dispositivo'; + @override String get linkUnavailable => 'No se pudo abrir dontkillmyapp.com en este dispositivo'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$es extends Translations$companionRemote$session$en { _Translations$companionRemote$session$es._(TranslationsEs root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsEs { 'downloads.syncRuleNotAvailableForProfile' => 'No disponible para el perfil actual', 'downloads.syncRuleUnknownServer' => 'Servidor desconocido', 'downloads.syncRuleListCreated' => 'Regla de sincronización creada', + 'downloads.backgroundWarning.bannerBlocked' => 'Las descargas se detendrán al salir de la app', + 'downloads.backgroundWarning.bannerDegraded' => 'Las descargas en segundo plano pueden estar limitadas', + 'downloads.backgroundWarning.bannerAction' => 'Detalles', + 'downloads.backgroundWarning.sheetTitle' => 'Las descargas en segundo plano están bloqueadas', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Las descargas en segundo plano pueden estar limitadas', + 'downloads.backgroundWarning.sheetIntro' => 'Android impide que Plezy descargue de forma fiable en segundo plano.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Tu dispositivo limita cuándo puede descargar Plezy en segundo plano.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'El uso en segundo plano de Plezy está restringido. Configura el uso de batería o en segundo plano como "Sin restricciones".', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android ha puesto a Plezy en un estado de espera restringido. Configura el uso de batería como "Sin restricciones".', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Las notificaciones de descargas están desactivadas, por lo que el progreso y los controles podrían no estar disponibles.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Las notificaciones están desactivadas. En Android 13 o versiones posteriores, son necesarias para las descargas largas en segundo plano.', + 'downloads.backgroundWarning.reasonDataSaver' => 'El Ahorro de datos está activado y bloquea las descargas en segundo plano con datos móviles. Las descargas deberían seguir funcionando con Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Las descargas se detuvieron repetidamente mientras Plezy estaba en segundo plano. Revisa la configuración de batería o de uso en segundo plano de Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Abrir configuración', + 'downloads.backgroundWarning.stillNotWorking' => 'Ayuda específica para tu dispositivo', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Consulta los pasos para tu dispositivo o envía un registro desde Configuración › Ver registros si el problema continúa.', + 'downloads.backgroundWarning.dialogTitle' => 'Es posible que las descargas no finalicen', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Descargar de todos modos', + 'downloads.backgroundWarning.dialogFixFirst' => 'Solucionarlo primero', + 'downloads.backgroundWarning.statusTile' => 'Descargas en segundo plano', + 'downloads.backgroundWarning.statusOk' => 'Permitidas en segundo plano', + 'downloads.backgroundWarning.statusBlocked' => 'Bloqueadas por la configuración del sistema', + 'downloads.backgroundWarning.statusDegraded' => 'Limitadas por la configuración del sistema', + 'downloads.backgroundWarning.statusUnknown' => 'Aún no se ha comprobado', + 'downloads.backgroundWarning.settingsUnavailable' => 'No se pudo abrir la configuración del sistema en este dispositivo', + 'downloads.backgroundWarning.linkUnavailable' => 'No se pudo abrir dontkillmyapp.com en este dispositivo', 'shaders.title' => 'Shaders', 'shaders.noShaderDescription' => 'Sin mejora de video', 'shaders.nvscalerDescription' => 'Escalado de imagen NVIDIA para un video más nítido', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index cda389f9..c2d30554 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$fr extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Non disponible pour le profil actuel'; @override String get syncRuleUnknownServer => 'Serveur inconnu'; @override String get syncRuleListCreated => 'Règle de synchronisation créée'; + @override late final _Translations$downloads$backgroundWarning$fr backgroundWarning = _Translations$downloads$backgroundWarning$fr._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$fr extends Translations$explore$status$en { @override String get upcoming => 'À venir'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$fr extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$fr._(TranslationsFr root) : this._root = root, super.internal(root); + + final TranslationsFr _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Les téléchargements s’arrêteront lorsque vous quitterez l’application'; + @override String get bannerDegraded => 'Les téléchargements en arrière-plan peuvent être limités'; + @override String get bannerAction => 'Détails'; + @override String get sheetTitle => 'Les téléchargements en arrière-plan sont bloqués'; + @override String get sheetTitleDegraded => 'Les téléchargements en arrière-plan peuvent être limités'; + @override String get sheetIntro => 'Android empêche Plezy de télécharger de façon fiable en arrière-plan.'; + @override String get sheetIntroDegraded => 'Votre appareil limite les moments où Plezy peut télécharger en arrière-plan.'; + @override String get reasonBackgroundRestricted => 'L’utilisation de Plezy en arrière-plan est restreinte. Dans les paramètres de batterie ou d’utilisation en arrière-plan, sélectionnez « Sans restriction ».'; + @override String get reasonStandbyRestricted => 'Android a placé Plezy en veille restreinte. Réglez l’utilisation de la batterie sur « Sans restriction ».'; + @override String get reasonDownloadChannelBlocked => 'Les notifications de téléchargement sont désactivées. La progression et les commandes peuvent donc être indisponibles.'; + @override String get reasonNotificationsDisabled => 'Les notifications sont désactivées. Sur Android 13 ou version ultérieure, elles sont nécessaires pour les longs téléchargements en arrière-plan.'; + @override String get reasonDataSaver => 'L’Économiseur de données est activé et bloque les téléchargements en arrière-plan via les données mobiles. Ils devraient toujours fonctionner en Wi-Fi.'; + @override String get reasonOemUnknown => 'Les téléchargements se sont arrêtés plusieurs fois lorsque Plezy était en arrière-plan. Vérifiez les paramètres de batterie ou d’utilisation en arrière-plan de Plezy.'; + @override String get openSettings => 'Ouvrir les paramètres'; + @override String get stillNotWorking => 'Aide spécifique à l’appareil'; + @override String get stillNotWorkingDescription => 'Consultez les étapes adaptées à votre appareil ou, si le problème persiste, envoyez un journal depuis Paramètres › Voir les journaux.'; + @override String get dialogTitle => 'Les téléchargements risquent de ne pas aboutir'; + @override String get dialogDownloadAnyway => 'Télécharger quand même'; + @override String get dialogFixFirst => 'Corriger d’abord'; + @override String get statusTile => 'Téléchargements en arrière-plan'; + @override String get statusOk => 'Exécution en arrière-plan autorisée'; + @override String get statusBlocked => 'Bloqués par les paramètres système'; + @override String get statusDegraded => 'Limités par les paramètres système'; + @override String get statusUnknown => 'Pas encore vérifié'; + @override String get settingsUnavailable => 'Impossible d’ouvrir les paramètres système sur cet appareil'; + @override String get linkUnavailable => 'Impossible d’ouvrir dontkillmyapp.com sur cet appareil'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$fr extends Translations$companionRemote$session$en { _Translations$companionRemote$session$fr._(TranslationsFr root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsFr { 'downloads.syncRuleNotAvailableForProfile' => 'Non disponible pour le profil actuel', 'downloads.syncRuleUnknownServer' => 'Serveur inconnu', 'downloads.syncRuleListCreated' => 'Règle de synchronisation créée', + 'downloads.backgroundWarning.bannerBlocked' => 'Les téléchargements s’arrêteront lorsque vous quitterez l’application', + 'downloads.backgroundWarning.bannerDegraded' => 'Les téléchargements en arrière-plan peuvent être limités', + 'downloads.backgroundWarning.bannerAction' => 'Détails', + 'downloads.backgroundWarning.sheetTitle' => 'Les téléchargements en arrière-plan sont bloqués', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Les téléchargements en arrière-plan peuvent être limités', + 'downloads.backgroundWarning.sheetIntro' => 'Android empêche Plezy de télécharger de façon fiable en arrière-plan.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Votre appareil limite les moments où Plezy peut télécharger en arrière-plan.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'L’utilisation de Plezy en arrière-plan est restreinte. Dans les paramètres de batterie ou d’utilisation en arrière-plan, sélectionnez « Sans restriction ».', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android a placé Plezy en veille restreinte. Réglez l’utilisation de la batterie sur « Sans restriction ».', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Les notifications de téléchargement sont désactivées. La progression et les commandes peuvent donc être indisponibles.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Les notifications sont désactivées. Sur Android 13 ou version ultérieure, elles sont nécessaires pour les longs téléchargements en arrière-plan.', + 'downloads.backgroundWarning.reasonDataSaver' => 'L’Économiseur de données est activé et bloque les téléchargements en arrière-plan via les données mobiles. Ils devraient toujours fonctionner en Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Les téléchargements se sont arrêtés plusieurs fois lorsque Plezy était en arrière-plan. Vérifiez les paramètres de batterie ou d’utilisation en arrière-plan de Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Ouvrir les paramètres', + 'downloads.backgroundWarning.stillNotWorking' => 'Aide spécifique à l’appareil', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Consultez les étapes adaptées à votre appareil ou, si le problème persiste, envoyez un journal depuis Paramètres › Voir les journaux.', + 'downloads.backgroundWarning.dialogTitle' => 'Les téléchargements risquent de ne pas aboutir', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Télécharger quand même', + 'downloads.backgroundWarning.dialogFixFirst' => 'Corriger d’abord', + 'downloads.backgroundWarning.statusTile' => 'Téléchargements en arrière-plan', + 'downloads.backgroundWarning.statusOk' => 'Exécution en arrière-plan autorisée', + 'downloads.backgroundWarning.statusBlocked' => 'Bloqués par les paramètres système', + 'downloads.backgroundWarning.statusDegraded' => 'Limités par les paramètres système', + 'downloads.backgroundWarning.statusUnknown' => 'Pas encore vérifié', + 'downloads.backgroundWarning.settingsUnavailable' => 'Impossible d’ouvrir les paramètres système sur cet appareil', + 'downloads.backgroundWarning.linkUnavailable' => 'Impossible d’ouvrir dontkillmyapp.com sur cet appareil', 'shaders.title' => 'Shaders', 'shaders.noShaderDescription' => 'Aucune amélioration vidéo', 'shaders.nvscalerDescription' => 'Mise à l\'échelle NVIDIA pour une vidéo plus nette', diff --git a/lib/i18n/strings_hu.g.dart b/lib/i18n/strings_hu.g.dart index dc4aa6b4..71aa89b2 100644 --- a/lib/i18n/strings_hu.g.dart +++ b/lib/i18n/strings_hu.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$hu extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Nem érhető el a jelenlegi profilhoz'; @override String get syncRuleUnknownServer => 'Ismeretlen szerver'; @override String get syncRuleListCreated => 'Szinkronizálási szabály létrehozva'; + @override late final _Translations$downloads$backgroundWarning$hu backgroundWarning = _Translations$downloads$backgroundWarning$hu._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$hu extends Translations$explore$status$en { @override String get upcoming => 'Közelgő'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$hu extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$hu._(TranslationsHu root) : this._root = root, super.internal(root); + + final TranslationsHu _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'A letöltések leállnak, ha kilépsz az alkalmazásból'; + @override String get bannerDegraded => 'A háttérbeli letöltések korlátozottak lehetnek'; + @override String get bannerAction => 'Részletek'; + @override String get sheetTitle => 'A háttérbeli letöltések le vannak tiltva'; + @override String get sheetTitleDegraded => 'A háttérbeli letöltések korlátozottak lehetnek'; + @override String get sheetIntro => 'Az Android megakadályozza, hogy a Plezy megbízhatóan töltsön le a háttérben.'; + @override String get sheetIntroDegraded => 'Az eszközöd korlátozza, hogy a Plezy mikor tölthet le a háttérben.'; + @override String get reasonBackgroundRestricted => 'A Plezy háttérbeli használata korlátozva van. Állítsd az akkumulátor- vagy háttérhasználatát „Korlátlan” értékre.'; + @override String get reasonStandbyRestricted => 'Az Android korlátozott készenléti állapotba helyezte a Plezyt. Állítsd az akkumulátorhasználatát „Korlátlan” értékre.'; + @override String get reasonDownloadChannelBlocked => 'A letöltési értesítések ki vannak kapcsolva, ezért előfordulhat, hogy a folyamatjelzés és a vezérlők nem érhetők el.'; + @override String get reasonNotificationsDisabled => 'Az értesítések ki vannak kapcsolva. Android 13 vagy újabb rendszeren szükségesek a hosszú háttérbeli letöltésekhez.'; + @override String get reasonDataSaver => 'Az Adatforgalom-csökkentő be van kapcsolva, ezért mobiladat-kapcsolaton nem működnek a háttérbeli letöltések. Wi-Fi-n továbbra is működniük kell.'; + @override String get reasonOemUnknown => 'A letöltések többször leálltak, miközben a Plezy a háttérben futott. Ellenőrizd a Plezy akkumulátor- vagy háttérhasználati beállításait.'; + @override String get openSettings => 'Beállítások megnyitása'; + @override String get stillNotWorking => 'Eszközspecifikus segítség'; + @override String get stillNotWorkingDescription => 'Nézd meg az eszközödhöz tartozó lépéseket, vagy ha a probléma továbbra is fennáll, küldj naplót a Beállítások › Naplók megtekintése menüből.'; + @override String get dialogTitle => 'A letöltések nem biztos, hogy befejeződnek'; + @override String get dialogDownloadAnyway => 'Letöltés mégis'; + @override String get dialogFixFirst => 'Előbb javítom'; + @override String get statusTile => 'Háttérbeli letöltések'; + @override String get statusOk => 'Futhat a háttérben'; + @override String get statusBlocked => 'A rendszerbeállítások blokkolják'; + @override String get statusDegraded => 'A rendszerbeállítások korlátozzák'; + @override String get statusUnknown => 'Még nincs ellenőrizve'; + @override String get settingsUnavailable => 'Ezen az eszközön nem sikerült megnyitni a rendszerbeállításokat'; + @override String get linkUnavailable => 'Ezen az eszközön nem sikerült megnyitni a dontkillmyapp.com webhelyet'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$hu extends Translations$companionRemote$session$en { _Translations$companionRemote$session$hu._(TranslationsHu root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsHu { 'downloads.syncRuleNotAvailableForProfile' => 'Nem érhető el a jelenlegi profilhoz', 'downloads.syncRuleUnknownServer' => 'Ismeretlen szerver', 'downloads.syncRuleListCreated' => 'Szinkronizálási szabály létrehozva', + 'downloads.backgroundWarning.bannerBlocked' => 'A letöltések leállnak, ha kilépsz az alkalmazásból', + 'downloads.backgroundWarning.bannerDegraded' => 'A háttérbeli letöltések korlátozottak lehetnek', + 'downloads.backgroundWarning.bannerAction' => 'Részletek', + 'downloads.backgroundWarning.sheetTitle' => 'A háttérbeli letöltések le vannak tiltva', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'A háttérbeli letöltések korlátozottak lehetnek', + 'downloads.backgroundWarning.sheetIntro' => 'Az Android megakadályozza, hogy a Plezy megbízhatóan töltsön le a háttérben.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Az eszközöd korlátozza, hogy a Plezy mikor tölthet le a háttérben.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'A Plezy háttérbeli használata korlátozva van. Állítsd az akkumulátor- vagy háttérhasználatát „Korlátlan” értékre.', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Az Android korlátozott készenléti állapotba helyezte a Plezyt. Állítsd az akkumulátorhasználatát „Korlátlan” értékre.', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'A letöltési értesítések ki vannak kapcsolva, ezért előfordulhat, hogy a folyamatjelzés és a vezérlők nem érhetők el.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Az értesítések ki vannak kapcsolva. Android 13 vagy újabb rendszeren szükségesek a hosszú háttérbeli letöltésekhez.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Az Adatforgalom-csökkentő be van kapcsolva, ezért mobiladat-kapcsolaton nem működnek a háttérbeli letöltések. Wi-Fi-n továbbra is működniük kell.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'A letöltések többször leálltak, miközben a Plezy a háttérben futott. Ellenőrizd a Plezy akkumulátor- vagy háttérhasználati beállításait.', + 'downloads.backgroundWarning.openSettings' => 'Beállítások megnyitása', + 'downloads.backgroundWarning.stillNotWorking' => 'Eszközspecifikus segítség', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Nézd meg az eszközödhöz tartozó lépéseket, vagy ha a probléma továbbra is fennáll, küldj naplót a Beállítások › Naplók megtekintése menüből.', + 'downloads.backgroundWarning.dialogTitle' => 'A letöltések nem biztos, hogy befejeződnek', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Letöltés mégis', + 'downloads.backgroundWarning.dialogFixFirst' => 'Előbb javítom', + 'downloads.backgroundWarning.statusTile' => 'Háttérbeli letöltések', + 'downloads.backgroundWarning.statusOk' => 'Futhat a háttérben', + 'downloads.backgroundWarning.statusBlocked' => 'A rendszerbeállítások blokkolják', + 'downloads.backgroundWarning.statusDegraded' => 'A rendszerbeállítások korlátozzák', + 'downloads.backgroundWarning.statusUnknown' => 'Még nincs ellenőrizve', + 'downloads.backgroundWarning.settingsUnavailable' => 'Ezen az eszközön nem sikerült megnyitni a rendszerbeállításokat', + 'downloads.backgroundWarning.linkUnavailable' => 'Ezen az eszközön nem sikerült megnyitni a dontkillmyapp.com webhelyet', 'shaders.title' => 'Shaderek', 'shaders.noShaderDescription' => 'Nincs videójavítás', 'shaders.nvscalerDescription' => 'NVIDIA képskálázás az élesebb videóért', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 8cd2afca..728e22c1 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$it extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Non disponibile per il profilo attuale'; @override String get syncRuleUnknownServer => 'Server sconosciuto'; @override String get syncRuleListCreated => 'Regola di sincronizzazione creata'; + @override late final _Translations$downloads$backgroundWarning$it backgroundWarning = _Translations$downloads$backgroundWarning$it._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$it extends Translations$explore$status$en { @override String get upcoming => 'In arrivo'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$it extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$it._(TranslationsIt root) : this._root = root, super.internal(root); + + final TranslationsIt _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'I download si interromperanno quando esci dall’app'; + @override String get bannerDegraded => 'I download in background potrebbero essere limitati'; + @override String get bannerAction => 'Dettagli'; + @override String get sheetTitle => 'I download in background sono bloccati'; + @override String get sheetTitleDegraded => 'I download in background potrebbero essere limitati'; + @override String get sheetIntro => 'Android impedisce a Plezy di scaricare in modo affidabile in background.'; + @override String get sheetIntroDegraded => 'Il dispositivo limita i momenti in cui Plezy può scaricare in background.'; + @override String get reasonBackgroundRestricted => 'L’uso in background di Plezy è limitato. Nelle impostazioni della batteria o dell’uso in background, seleziona «Senza restrizioni».'; + @override String get reasonStandbyRestricted => 'Android ha messo Plezy in uno stato di standby con restrizioni. Imposta l’uso della batteria su «Senza restrizioni».'; + @override String get reasonDownloadChannelBlocked => 'Le notifiche dei download sono disattivate, quindi l’avanzamento e i controlli potrebbero non essere disponibili.'; + @override String get reasonNotificationsDisabled => 'Le notifiche sono disattivate. Su Android 13 o versioni successive sono necessarie per i download prolungati in background.'; + @override String get reasonDataSaver => 'Il Risparmio dati è attivo e blocca i download in background tramite dati mobili. I download dovrebbero comunque funzionare su Wi-Fi.'; + @override String get reasonOemUnknown => 'I download si sono interrotti più volte mentre Plezy era in background. Controlla le impostazioni della batteria o dell’uso in background di Plezy.'; + @override String get openSettings => 'Apri le impostazioni'; + @override String get stillNotWorking => 'Guida specifica per il dispositivo'; + @override String get stillNotWorkingDescription => 'Consulta la procedura per il tuo dispositivo oppure invia un log da Impostazioni › Visualizza i log se il problema persiste.'; + @override String get dialogTitle => 'I download potrebbero non terminare'; + @override String get dialogDownloadAnyway => 'Scarica comunque'; + @override String get dialogFixFirst => 'Risolvi prima'; + @override String get statusTile => 'Download in background'; + @override String get statusOk => 'Esecuzione in background consentita'; + @override String get statusBlocked => 'Bloccati dalle impostazioni di sistema'; + @override String get statusDegraded => 'Limitati dalle impostazioni di sistema'; + @override String get statusUnknown => 'Non ancora verificato'; + @override String get settingsUnavailable => 'Impossibile aprire le impostazioni di sistema su questo dispositivo'; + @override String get linkUnavailable => 'Impossibile aprire dontkillmyapp.com su questo dispositivo'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$it extends Translations$companionRemote$session$en { _Translations$companionRemote$session$it._(TranslationsIt root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsIt { 'downloads.syncRuleNotAvailableForProfile' => 'Non disponibile per il profilo attuale', 'downloads.syncRuleUnknownServer' => 'Server sconosciuto', 'downloads.syncRuleListCreated' => 'Regola di sincronizzazione creata', + 'downloads.backgroundWarning.bannerBlocked' => 'I download si interromperanno quando esci dall’app', + 'downloads.backgroundWarning.bannerDegraded' => 'I download in background potrebbero essere limitati', + 'downloads.backgroundWarning.bannerAction' => 'Dettagli', + 'downloads.backgroundWarning.sheetTitle' => 'I download in background sono bloccati', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'I download in background potrebbero essere limitati', + 'downloads.backgroundWarning.sheetIntro' => 'Android impedisce a Plezy di scaricare in modo affidabile in background.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Il dispositivo limita i momenti in cui Plezy può scaricare in background.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'L’uso in background di Plezy è limitato. Nelle impostazioni della batteria o dell’uso in background, seleziona «Senza restrizioni».', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android ha messo Plezy in uno stato di standby con restrizioni. Imposta l’uso della batteria su «Senza restrizioni».', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Le notifiche dei download sono disattivate, quindi l’avanzamento e i controlli potrebbero non essere disponibili.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Le notifiche sono disattivate. Su Android 13 o versioni successive sono necessarie per i download prolungati in background.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Il Risparmio dati è attivo e blocca i download in background tramite dati mobili. I download dovrebbero comunque funzionare su Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'I download si sono interrotti più volte mentre Plezy era in background. Controlla le impostazioni della batteria o dell’uso in background di Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Apri le impostazioni', + 'downloads.backgroundWarning.stillNotWorking' => 'Guida specifica per il dispositivo', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Consulta la procedura per il tuo dispositivo oppure invia un log da Impostazioni › Visualizza i log se il problema persiste.', + 'downloads.backgroundWarning.dialogTitle' => 'I download potrebbero non terminare', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Scarica comunque', + 'downloads.backgroundWarning.dialogFixFirst' => 'Risolvi prima', + 'downloads.backgroundWarning.statusTile' => 'Download in background', + 'downloads.backgroundWarning.statusOk' => 'Esecuzione in background consentita', + 'downloads.backgroundWarning.statusBlocked' => 'Bloccati dalle impostazioni di sistema', + 'downloads.backgroundWarning.statusDegraded' => 'Limitati dalle impostazioni di sistema', + 'downloads.backgroundWarning.statusUnknown' => 'Non ancora verificato', + 'downloads.backgroundWarning.settingsUnavailable' => 'Impossibile aprire le impostazioni di sistema su questo dispositivo', + 'downloads.backgroundWarning.linkUnavailable' => 'Impossibile aprire dontkillmyapp.com su questo dispositivo', 'shaders.title' => 'Shader', 'shaders.noShaderDescription' => 'Nessun miglioramento video', 'shaders.nvscalerDescription' => 'Ridimensionamento NVIDIA per video più nitido', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index f2f5a4fd..ef6a30fc 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -1450,6 +1450,7 @@ class _Translations$downloads$ja extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => '現在のプロフィールでは利用できません'; @override String get syncRuleUnknownServer => '不明なサーバー'; @override String get syncRuleListCreated => '同期ルールを作成しました'; + @override late final _Translations$downloads$backgroundWarning$ja backgroundWarning = _Translations$downloads$backgroundWarning$ja._(_root); } // Path: shaders @@ -1980,6 +1981,41 @@ class _Translations$explore$status$ja extends Translations$explore$status$en { @override String get upcoming => '放送予定'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$ja extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$ja._(TranslationsJa root) : this._root = root, super.internal(root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'アプリの画面を離れると、ダウンロードが停止します'; + @override String get bannerDegraded => 'バックグラウンドダウンロードが制限される場合があります'; + @override String get bannerAction => '詳細'; + @override String get sheetTitle => 'バックグラウンドダウンロードはブロックされています'; + @override String get sheetTitleDegraded => 'バックグラウンドダウンロードが制限される場合があります'; + @override String get sheetIntro => 'Androidにより、Plezyはバックグラウンドで安定してダウンロードできません。'; + @override String get sheetIntroDegraded => '端末により、Plezyがバックグラウンドでダウンロードできるタイミングが制限されています。'; + @override String get reasonBackgroundRestricted => 'Plezyのバックグラウンド使用が制限されています。バッテリー使用量またはバックグラウンド使用を「制限なし」に設定してください。'; + @override String get reasonStandbyRestricted => 'Androidにより、Plezyが制限付きのスタンバイ状態に設定されています。バッテリー使用量を「制限なし」に設定してください。'; + @override String get reasonDownloadChannelBlocked => 'ダウンロード通知がオフのため、進行状況や操作ボタンを利用できない場合があります。'; + @override String get reasonNotificationsDisabled => '通知がオフです。Android 13以降では、長時間のバックグラウンドダウンロードに通知が必要です。'; + @override String get reasonDataSaver => 'データセーバーがオンのため、モバイルデータ通信ではバックグラウンドダウンロードがブロックされます。Wi-Fiでは引き続きダウンロードできるはずです。'; + @override String get reasonOemUnknown => 'Plezyがバックグラウンドで動作中に、ダウンロードが繰り返し停止しました。Plezyのバッテリー使用量またはバックグラウンド使用の設定を確認してください。'; + @override String get openSettings => '設定を開く'; + @override String get stillNotWorking => '端末別のヘルプ'; + @override String get stillNotWorkingDescription => 'お使いの端末向けの手順を確認してください。問題が続く場合は、設定 › ログを表示 からログを送信してください。'; + @override String get dialogTitle => 'ダウンロードが完了しない可能性があります'; + @override String get dialogDownloadAnyway => 'このままダウンロード'; + @override String get dialogFixFirst => '先に設定を修正'; + @override String get statusTile => 'バックグラウンドダウンロード'; + @override String get statusOk => 'バックグラウンドで実行可能'; + @override String get statusBlocked => 'システム設定によりブロック'; + @override String get statusDegraded => 'システム設定により制限'; + @override String get statusUnknown => '未確認'; + @override String get settingsUnavailable => 'この端末ではシステム設定を開けませんでした'; + @override String get linkUnavailable => 'この端末ではdontkillmyapp.comを開けませんでした'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$ja extends Translations$companionRemote$session$en { _Translations$companionRemote$session$ja._(TranslationsJa root) : this._root = root, super.internal(root); @@ -3258,6 +3294,32 @@ extension on TranslationsJa { 'downloads.syncRuleNotAvailableForProfile' => '現在のプロフィールでは利用できません', 'downloads.syncRuleUnknownServer' => '不明なサーバー', 'downloads.syncRuleListCreated' => '同期ルールを作成しました', + 'downloads.backgroundWarning.bannerBlocked' => 'アプリの画面を離れると、ダウンロードが停止します', + 'downloads.backgroundWarning.bannerDegraded' => 'バックグラウンドダウンロードが制限される場合があります', + 'downloads.backgroundWarning.bannerAction' => '詳細', + 'downloads.backgroundWarning.sheetTitle' => 'バックグラウンドダウンロードはブロックされています', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'バックグラウンドダウンロードが制限される場合があります', + 'downloads.backgroundWarning.sheetIntro' => 'Androidにより、Plezyはバックグラウンドで安定してダウンロードできません。', + 'downloads.backgroundWarning.sheetIntroDegraded' => '端末により、Plezyがバックグラウンドでダウンロードできるタイミングが制限されています。', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezyのバックグラウンド使用が制限されています。バッテリー使用量またはバックグラウンド使用を「制限なし」に設定してください。', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Androidにより、Plezyが制限付きのスタンバイ状態に設定されています。バッテリー使用量を「制限なし」に設定してください。', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'ダウンロード通知がオフのため、進行状況や操作ボタンを利用できない場合があります。', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => '通知がオフです。Android 13以降では、長時間のバックグラウンドダウンロードに通知が必要です。', + 'downloads.backgroundWarning.reasonDataSaver' => 'データセーバーがオンのため、モバイルデータ通信ではバックグラウンドダウンロードがブロックされます。Wi-Fiでは引き続きダウンロードできるはずです。', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Plezyがバックグラウンドで動作中に、ダウンロードが繰り返し停止しました。Plezyのバッテリー使用量またはバックグラウンド使用の設定を確認してください。', + 'downloads.backgroundWarning.openSettings' => '設定を開く', + 'downloads.backgroundWarning.stillNotWorking' => '端末別のヘルプ', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'お使いの端末向けの手順を確認してください。問題が続く場合は、設定 › ログを表示 からログを送信してください。', + 'downloads.backgroundWarning.dialogTitle' => 'ダウンロードが完了しない可能性があります', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'このままダウンロード', + 'downloads.backgroundWarning.dialogFixFirst' => '先に設定を修正', + 'downloads.backgroundWarning.statusTile' => 'バックグラウンドダウンロード', + 'downloads.backgroundWarning.statusOk' => 'バックグラウンドで実行可能', + 'downloads.backgroundWarning.statusBlocked' => 'システム設定によりブロック', + 'downloads.backgroundWarning.statusDegraded' => 'システム設定により制限', + 'downloads.backgroundWarning.statusUnknown' => '未確認', + 'downloads.backgroundWarning.settingsUnavailable' => 'この端末ではシステム設定を開けませんでした', + 'downloads.backgroundWarning.linkUnavailable' => 'この端末ではdontkillmyapp.comを開けませんでした', 'shaders.title' => 'シェーダー', 'shaders.noShaderDescription' => '映像補正なし', 'shaders.nvscalerDescription' => 'NVIDIA画像スケーリングで映像をより鮮明にします', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 3f02b78d..711b0f40 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -1450,6 +1450,7 @@ class _Translations$downloads$ko extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => '현재 프로필에서 사용할 수 없음'; @override String get syncRuleUnknownServer => '알 수 없는 서버'; @override String get syncRuleListCreated => '동기화 규칙이 생성되었습니다'; + @override late final _Translations$downloads$backgroundWarning$ko backgroundWarning = _Translations$downloads$backgroundWarning$ko._(_root); } // Path: shaders @@ -1980,6 +1981,41 @@ class _Translations$explore$status$ko extends Translations$explore$status$en { @override String get upcoming => '방영 예정'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$ko extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$ko._(TranslationsKo root) : this._root = root, super.internal(root); + + final TranslationsKo _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => '앱에서 나가면 다운로드가 중지됩니다'; + @override String get bannerDegraded => '백그라운드 다운로드가 제한될 수 있습니다'; + @override String get bannerAction => '세부정보'; + @override String get sheetTitle => '백그라운드 다운로드가 차단됨'; + @override String get sheetTitleDegraded => '백그라운드 다운로드가 제한될 수 있음'; + @override String get sheetIntro => 'Android가 Plezy의 안정적인 백그라운드 다운로드를 차단하고 있습니다.'; + @override String get sheetIntroDegraded => '기기에서 Plezy가 백그라운드로 다운로드할 수 있는 시점을 제한하고 있습니다.'; + @override String get reasonBackgroundRestricted => 'Plezy의 백그라운드 사용이 제한되어 있습니다. 배터리 또는 백그라운드 사용을 "제한 없음"으로 설정하세요.'; + @override String get reasonStandbyRestricted => 'Android가 Plezy를 제한된 대기 상태로 전환했습니다. 배터리 사용량을 "제한 없음"으로 설정하세요.'; + @override String get reasonDownloadChannelBlocked => '다운로드 알림이 꺼져 있어 진행 상황과 제어 기능을 사용하지 못할 수 있습니다.'; + @override String get reasonNotificationsDisabled => '알림이 꺼져 있습니다. Android 13 이상에서는 장시간 백그라운드 다운로드에 알림이 필요합니다.'; + @override String get reasonDataSaver => '데이터 절약 모드가 켜져 있어 모바일 데이터로 백그라운드 다운로드를 할 수 없습니다. Wi-Fi에서는 계속 다운로드할 수 있습니다.'; + @override String get reasonOemUnknown => 'Plezy가 백그라운드에 있을 때 다운로드가 반복해서 중지되었습니다. Plezy의 배터리 또는 백그라운드 사용 설정을 확인하세요.'; + @override String get openSettings => '설정 열기'; + @override String get stillNotWorking => '기기별 도움말'; + @override String get stillNotWorkingDescription => '기기에 맞는 단계를 확인하세요. 문제가 계속되면 설정 › 로그 보기에서 로그를 보내 주세요.'; + @override String get dialogTitle => '다운로드가 완료되지 않을 수 있음'; + @override String get dialogDownloadAnyway => '그래도 다운로드'; + @override String get dialogFixFirst => '먼저 문제 해결'; + @override String get statusTile => '백그라운드 다운로드'; + @override String get statusOk => '백그라운드 실행 허용됨'; + @override String get statusBlocked => '시스템 설정에 의해 차단됨'; + @override String get statusDegraded => '시스템 설정에 의해 제한됨'; + @override String get statusUnknown => '아직 확인하지 않음'; + @override String get settingsUnavailable => '이 기기에서 시스템 설정을 열 수 없습니다'; + @override String get linkUnavailable => '이 기기에서 dontkillmyapp.com을 열 수 없습니다'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$ko extends Translations$companionRemote$session$en { _Translations$companionRemote$session$ko._(TranslationsKo root) : this._root = root, super.internal(root); @@ -3258,6 +3294,32 @@ extension on TranslationsKo { 'downloads.syncRuleNotAvailableForProfile' => '현재 프로필에서 사용할 수 없음', 'downloads.syncRuleUnknownServer' => '알 수 없는 서버', 'downloads.syncRuleListCreated' => '동기화 규칙이 생성되었습니다', + 'downloads.backgroundWarning.bannerBlocked' => '앱에서 나가면 다운로드가 중지됩니다', + 'downloads.backgroundWarning.bannerDegraded' => '백그라운드 다운로드가 제한될 수 있습니다', + 'downloads.backgroundWarning.bannerAction' => '세부정보', + 'downloads.backgroundWarning.sheetTitle' => '백그라운드 다운로드가 차단됨', + 'downloads.backgroundWarning.sheetTitleDegraded' => '백그라운드 다운로드가 제한될 수 있음', + 'downloads.backgroundWarning.sheetIntro' => 'Android가 Plezy의 안정적인 백그라운드 다운로드를 차단하고 있습니다.', + 'downloads.backgroundWarning.sheetIntroDegraded' => '기기에서 Plezy가 백그라운드로 다운로드할 수 있는 시점을 제한하고 있습니다.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezy의 백그라운드 사용이 제한되어 있습니다. 배터리 또는 백그라운드 사용을 "제한 없음"으로 설정하세요.', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android가 Plezy를 제한된 대기 상태로 전환했습니다. 배터리 사용량을 "제한 없음"으로 설정하세요.', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => '다운로드 알림이 꺼져 있어 진행 상황과 제어 기능을 사용하지 못할 수 있습니다.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => '알림이 꺼져 있습니다. Android 13 이상에서는 장시간 백그라운드 다운로드에 알림이 필요합니다.', + 'downloads.backgroundWarning.reasonDataSaver' => '데이터 절약 모드가 켜져 있어 모바일 데이터로 백그라운드 다운로드를 할 수 없습니다. Wi-Fi에서는 계속 다운로드할 수 있습니다.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Plezy가 백그라운드에 있을 때 다운로드가 반복해서 중지되었습니다. Plezy의 배터리 또는 백그라운드 사용 설정을 확인하세요.', + 'downloads.backgroundWarning.openSettings' => '설정 열기', + 'downloads.backgroundWarning.stillNotWorking' => '기기별 도움말', + 'downloads.backgroundWarning.stillNotWorkingDescription' => '기기에 맞는 단계를 확인하세요. 문제가 계속되면 설정 › 로그 보기에서 로그를 보내 주세요.', + 'downloads.backgroundWarning.dialogTitle' => '다운로드가 완료되지 않을 수 있음', + 'downloads.backgroundWarning.dialogDownloadAnyway' => '그래도 다운로드', + 'downloads.backgroundWarning.dialogFixFirst' => '먼저 문제 해결', + 'downloads.backgroundWarning.statusTile' => '백그라운드 다운로드', + 'downloads.backgroundWarning.statusOk' => '백그라운드 실행 허용됨', + 'downloads.backgroundWarning.statusBlocked' => '시스템 설정에 의해 차단됨', + 'downloads.backgroundWarning.statusDegraded' => '시스템 설정에 의해 제한됨', + 'downloads.backgroundWarning.statusUnknown' => '아직 확인하지 않음', + 'downloads.backgroundWarning.settingsUnavailable' => '이 기기에서 시스템 설정을 열 수 없습니다', + 'downloads.backgroundWarning.linkUnavailable' => '이 기기에서 dontkillmyapp.com을 열 수 없습니다', 'shaders.title' => '셰이더', 'shaders.noShaderDescription' => '비디오 향상 없음', 'shaders.nvscalerDescription' => '더 선명한 비디오를 위한 NVIDIA 이미지 스케일링', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index a6e4ecc1..fd565cf7 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$nb extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Ikke tilgjengelig for gjeldende profil'; @override String get syncRuleUnknownServer => 'Ukjent server'; @override String get syncRuleListCreated => 'Synkroniseringsregel opprettet'; + @override late final _Translations$downloads$backgroundWarning$nb backgroundWarning = _Translations$downloads$backgroundWarning$nb._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$nb extends Translations$explore$status$en { @override String get upcoming => 'Kommende'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$nb extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$nb._(TranslationsNb root) : this._root = root, super.internal(root); + + final TranslationsNb _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Nedlastinger stopper når du forlater appen'; + @override String get bannerDegraded => 'Bakgrunnsnedlastinger kan være begrenset'; + @override String get bannerAction => 'Detaljer'; + @override String get sheetTitle => 'Bakgrunnsnedlastinger er blokkert'; + @override String get sheetTitleDegraded => 'Bakgrunnsnedlastinger kan være begrenset'; + @override String get sheetIntro => 'Android hindrer Plezy i å laste ned pålitelig i bakgrunnen.'; + @override String get sheetIntroDegraded => 'Enheten din begrenser når Plezy kan laste ned i bakgrunnen.'; + @override String get reasonBackgroundRestricted => 'Bakgrunnsbruken til Plezy er begrenset. Sett batteribruk eller bakgrunnsbruk til «Ubegrenset».'; + @override String get reasonStandbyRestricted => 'Android har satt Plezy i begrenset hvilemodus. Sett batteribruken til «Ubegrenset».'; + @override String get reasonDownloadChannelBlocked => 'Varsler om nedlastinger er slått av, så fremdrift og kontroller kan være utilgjengelige.'; + @override String get reasonNotificationsDisabled => 'Varsler er slått av. På Android 13 eller nyere kreves de for lange bakgrunnsnedlastinger.'; + @override String get reasonDataSaver => 'Datasparing er slått på og blokkerer bakgrunnsnedlastinger via mobildata. Nedlastinger skal fortsatt fungere på Wi-Fi.'; + @override String get reasonOemUnknown => 'Nedlastinger har stoppet gjentatte ganger mens Plezy var i bakgrunnen. Sjekk innstillingene for batteribruk eller bakgrunnsbruk for Plezy.'; + @override String get openSettings => 'Åpne innstillinger'; + @override String get stillNotWorking => 'Enhetsspesifikk hjelp'; + @override String get stillNotWorkingDescription => 'Se fremgangsmåten for enheten din, eller send en logg fra Innstillinger › Vis logger hvis problemet vedvarer.'; + @override String get dialogTitle => 'Nedlastinger blir kanskje ikke fullført'; + @override String get dialogDownloadAnyway => 'Last ned likevel'; + @override String get dialogFixFirst => 'Løs dette først'; + @override String get statusTile => 'Bakgrunnsnedlastinger'; + @override String get statusOk => 'Kan kjøre i bakgrunnen'; + @override String get statusBlocked => 'Blokkert av systeminnstillinger'; + @override String get statusDegraded => 'Begrenset av systeminnstillinger'; + @override String get statusUnknown => 'Ikke sjekket ennå'; + @override String get settingsUnavailable => 'Kunne ikke åpne systeminnstillingene på denne enheten'; + @override String get linkUnavailable => 'Kunne ikke åpne dontkillmyapp.com på denne enheten'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$nb extends Translations$companionRemote$session$en { _Translations$companionRemote$session$nb._(TranslationsNb root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsNb { 'downloads.syncRuleNotAvailableForProfile' => 'Ikke tilgjengelig for gjeldende profil', 'downloads.syncRuleUnknownServer' => 'Ukjent server', 'downloads.syncRuleListCreated' => 'Synkroniseringsregel opprettet', + 'downloads.backgroundWarning.bannerBlocked' => 'Nedlastinger stopper når du forlater appen', + 'downloads.backgroundWarning.bannerDegraded' => 'Bakgrunnsnedlastinger kan være begrenset', + 'downloads.backgroundWarning.bannerAction' => 'Detaljer', + 'downloads.backgroundWarning.sheetTitle' => 'Bakgrunnsnedlastinger er blokkert', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Bakgrunnsnedlastinger kan være begrenset', + 'downloads.backgroundWarning.sheetIntro' => 'Android hindrer Plezy i å laste ned pålitelig i bakgrunnen.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Enheten din begrenser når Plezy kan laste ned i bakgrunnen.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Bakgrunnsbruken til Plezy er begrenset. Sett batteribruk eller bakgrunnsbruk til «Ubegrenset».', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android har satt Plezy i begrenset hvilemodus. Sett batteribruken til «Ubegrenset».', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Varsler om nedlastinger er slått av, så fremdrift og kontroller kan være utilgjengelige.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Varsler er slått av. På Android 13 eller nyere kreves de for lange bakgrunnsnedlastinger.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Datasparing er slått på og blokkerer bakgrunnsnedlastinger via mobildata. Nedlastinger skal fortsatt fungere på Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Nedlastinger har stoppet gjentatte ganger mens Plezy var i bakgrunnen. Sjekk innstillingene for batteribruk eller bakgrunnsbruk for Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Åpne innstillinger', + 'downloads.backgroundWarning.stillNotWorking' => 'Enhetsspesifikk hjelp', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Se fremgangsmåten for enheten din, eller send en logg fra Innstillinger › Vis logger hvis problemet vedvarer.', + 'downloads.backgroundWarning.dialogTitle' => 'Nedlastinger blir kanskje ikke fullført', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Last ned likevel', + 'downloads.backgroundWarning.dialogFixFirst' => 'Løs dette først', + 'downloads.backgroundWarning.statusTile' => 'Bakgrunnsnedlastinger', + 'downloads.backgroundWarning.statusOk' => 'Kan kjøre i bakgrunnen', + 'downloads.backgroundWarning.statusBlocked' => 'Blokkert av systeminnstillinger', + 'downloads.backgroundWarning.statusDegraded' => 'Begrenset av systeminnstillinger', + 'downloads.backgroundWarning.statusUnknown' => 'Ikke sjekket ennå', + 'downloads.backgroundWarning.settingsUnavailable' => 'Kunne ikke åpne systeminnstillingene på denne enheten', + 'downloads.backgroundWarning.linkUnavailable' => 'Kunne ikke åpne dontkillmyapp.com på denne enheten', 'shaders.title' => 'Shadere', 'shaders.noShaderDescription' => 'Ingen videoforbedring', 'shaders.nvscalerDescription' => 'NVIDIA bildeskalering for skarpere video', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index be08609c..41ea0c1d 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$nl extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Niet beschikbaar voor huidig profiel'; @override String get syncRuleUnknownServer => 'Onbekende server'; @override String get syncRuleListCreated => 'Synchronisatieregel aangemaakt'; + @override late final _Translations$downloads$backgroundWarning$nl backgroundWarning = _Translations$downloads$backgroundWarning$nl._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$nl extends Translations$explore$status$en { @override String get upcoming => 'Binnenkort'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$nl extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$nl._(TranslationsNl root) : this._root = root, super.internal(root); + + final TranslationsNl _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Downloads stoppen zodra je de app verlaat'; + @override String get bannerDegraded => 'Downloads op de achtergrond kunnen beperkt zijn'; + @override String get bannerAction => 'Details'; + @override String get sheetTitle => 'Downloads op de achtergrond zijn geblokkeerd'; + @override String get sheetTitleDegraded => 'Downloads op de achtergrond kunnen beperkt zijn'; + @override String get sheetIntro => 'Android verhindert dat Plezy betrouwbaar op de achtergrond downloadt.'; + @override String get sheetIntroDegraded => 'Je apparaat beperkt wanneer Plezy op de achtergrond kan downloaden.'; + @override String get reasonBackgroundRestricted => 'Het achtergrondgebruik van Plezy is beperkt. Stel het batterij- of achtergrondgebruik in op "Onbeperkt".'; + @override String get reasonStandbyRestricted => 'Android heeft Plezy in een beperkte stand-bymodus geplaatst. Stel het batterijgebruik in op "Onbeperkt".'; + @override String get reasonDownloadChannelBlocked => 'Downloadmeldingen zijn uitgeschakeld, waardoor voortgang en bediening mogelijk niet beschikbaar zijn.'; + @override String get reasonNotificationsDisabled => 'Meldingen zijn uitgeschakeld. Op Android 13 of nieuwer zijn ze vereist voor langdurige downloads op de achtergrond.'; + @override String get reasonDataSaver => 'Databesparing is ingeschakeld en blokkeert downloads op de achtergrond via mobiele data. Via Wi-Fi zouden downloads nog wel moeten werken.'; + @override String get reasonOemUnknown => 'Downloads zijn herhaaldelijk gestopt terwijl Plezy op de achtergrond draaide. Controleer de instellingen voor het batterij- of achtergrondgebruik van Plezy.'; + @override String get openSettings => 'Instellingen openen'; + @override String get stillNotWorking => 'Apparaatspecifieke hulp'; + @override String get stillNotWorkingDescription => 'Bekijk de stappen voor je apparaat of stuur een logbestand vanuit Instellingen › Logbestanden bekijken als het probleem aanhoudt.'; + @override String get dialogTitle => 'Downloads worden mogelijk niet voltooid'; + @override String get dialogDownloadAnyway => 'Toch downloaden'; + @override String get dialogFixFirst => 'Dit eerst oplossen'; + @override String get statusTile => 'Downloads op de achtergrond'; + @override String get statusOk => 'Mag op de achtergrond worden uitgevoerd'; + @override String get statusBlocked => 'Geblokkeerd door systeeminstellingen'; + @override String get statusDegraded => 'Beperkt door systeeminstellingen'; + @override String get statusUnknown => 'Nog niet gecontroleerd'; + @override String get settingsUnavailable => 'Kan de systeeminstellingen niet openen op dit apparaat'; + @override String get linkUnavailable => 'Kan dontkillmyapp.com niet openen op dit apparaat'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$nl extends Translations$companionRemote$session$en { _Translations$companionRemote$session$nl._(TranslationsNl root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsNl { 'downloads.syncRuleNotAvailableForProfile' => 'Niet beschikbaar voor huidig profiel', 'downloads.syncRuleUnknownServer' => 'Onbekende server', 'downloads.syncRuleListCreated' => 'Synchronisatieregel aangemaakt', + 'downloads.backgroundWarning.bannerBlocked' => 'Downloads stoppen zodra je de app verlaat', + 'downloads.backgroundWarning.bannerDegraded' => 'Downloads op de achtergrond kunnen beperkt zijn', + 'downloads.backgroundWarning.bannerAction' => 'Details', + 'downloads.backgroundWarning.sheetTitle' => 'Downloads op de achtergrond zijn geblokkeerd', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Downloads op de achtergrond kunnen beperkt zijn', + 'downloads.backgroundWarning.sheetIntro' => 'Android verhindert dat Plezy betrouwbaar op de achtergrond downloadt.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Je apparaat beperkt wanneer Plezy op de achtergrond kan downloaden.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Het achtergrondgebruik van Plezy is beperkt. Stel het batterij- of achtergrondgebruik in op "Onbeperkt".', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android heeft Plezy in een beperkte stand-bymodus geplaatst. Stel het batterijgebruik in op "Onbeperkt".', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Downloadmeldingen zijn uitgeschakeld, waardoor voortgang en bediening mogelijk niet beschikbaar zijn.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Meldingen zijn uitgeschakeld. Op Android 13 of nieuwer zijn ze vereist voor langdurige downloads op de achtergrond.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Databesparing is ingeschakeld en blokkeert downloads op de achtergrond via mobiele data. Via Wi-Fi zouden downloads nog wel moeten werken.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Downloads zijn herhaaldelijk gestopt terwijl Plezy op de achtergrond draaide. Controleer de instellingen voor het batterij- of achtergrondgebruik van Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Instellingen openen', + 'downloads.backgroundWarning.stillNotWorking' => 'Apparaatspecifieke hulp', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Bekijk de stappen voor je apparaat of stuur een logbestand vanuit Instellingen › Logbestanden bekijken als het probleem aanhoudt.', + 'downloads.backgroundWarning.dialogTitle' => 'Downloads worden mogelijk niet voltooid', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Toch downloaden', + 'downloads.backgroundWarning.dialogFixFirst' => 'Dit eerst oplossen', + 'downloads.backgroundWarning.statusTile' => 'Downloads op de achtergrond', + 'downloads.backgroundWarning.statusOk' => 'Mag op de achtergrond worden uitgevoerd', + 'downloads.backgroundWarning.statusBlocked' => 'Geblokkeerd door systeeminstellingen', + 'downloads.backgroundWarning.statusDegraded' => 'Beperkt door systeeminstellingen', + 'downloads.backgroundWarning.statusUnknown' => 'Nog niet gecontroleerd', + 'downloads.backgroundWarning.settingsUnavailable' => 'Kan de systeeminstellingen niet openen op dit apparaat', + 'downloads.backgroundWarning.linkUnavailable' => 'Kan dontkillmyapp.com niet openen op dit apparaat', 'shaders.title' => 'Shaders', 'shaders.noShaderDescription' => 'Geen videoverbetering', 'shaders.nvscalerDescription' => 'NVIDIA-beeldschaling voor scherpere video', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index d3714f58..a1d23e2f 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -1459,6 +1459,7 @@ class _Translations$downloads$pl extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Niedostępne dla bieżącego profilu'; @override String get syncRuleUnknownServer => 'Nieznany serwer'; @override String get syncRuleListCreated => 'Utworzono regułę synchronizacji'; + @override late final _Translations$downloads$backgroundWarning$pl backgroundWarning = _Translations$downloads$backgroundWarning$pl._(_root); } // Path: shaders @@ -1989,6 +1990,41 @@ class _Translations$explore$status$pl extends Translations$explore$status$en { @override String get upcoming => 'Nadchodzący'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$pl extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$pl._(TranslationsPl root) : this._root = root, super.internal(root); + + final TranslationsPl _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Po opuszczeniu aplikacji pobieranie zostanie zatrzymane'; + @override String get bannerDegraded => 'Pobieranie w tle może być ograniczone'; + @override String get bannerAction => 'Szczegóły'; + @override String get sheetTitle => 'Pobieranie w tle jest zablokowane'; + @override String get sheetTitleDegraded => 'Pobieranie w tle może być ograniczone'; + @override String get sheetIntro => 'Android uniemożliwia Plezy niezawodne pobieranie plików w tle.'; + @override String get sheetIntroDegraded => 'Twoje urządzenie ogranicza możliwość pobierania w tle przez Plezy.'; + @override String get reasonBackgroundRestricted => 'Działanie Plezy w tle jest ograniczone. Ustaw użycie baterii lub działanie w tle na „Bez ograniczeń”.'; + @override String get reasonStandbyRestricted => 'Android umieścił Plezy w ograniczonym trybie gotowości. Ustaw użycie baterii na „Bez ograniczeń”.'; + @override String get reasonDownloadChannelBlocked => 'Powiadomienia o pobieraniu są wyłączone, więc postęp i opcje sterowania mogą być niedostępne.'; + @override String get reasonNotificationsDisabled => 'Powiadomienia są wyłączone. W Android 13 lub nowszym są wymagane przy długim pobieraniu w tle.'; + @override String get reasonDataSaver => 'Oszczędzanie danych jest włączone, co blokuje pobieranie w tle przez mobilną transmisję danych. Pobieranie powinno nadal działać przez Wi-Fi.'; + @override String get reasonOemUnknown => 'Pobieranie wielokrotnie przerywało się, gdy Plezy działało w tle. Sprawdź ustawienia baterii lub działania w tle dla Plezy.'; + @override String get openSettings => 'Otwórz ustawienia'; + @override String get stillNotWorking => 'Pomoc dla Twojego urządzenia'; + @override String get stillNotWorkingDescription => 'Zobacz instrukcje dla swojego urządzenia lub, jeśli problem nadal występuje, wyślij log przez Ustawienia › Pokaż logi.'; + @override String get dialogTitle => 'Pobieranie może się nie zakończyć'; + @override String get dialogDownloadAnyway => 'Pobierz mimo to'; + @override String get dialogFixFirst => 'Najpierw rozwiąż problem'; + @override String get statusTile => 'Pobieranie w tle'; + @override String get statusOk => 'Działanie w tle jest dozwolone'; + @override String get statusBlocked => 'Zablokowane przez ustawienia systemu'; + @override String get statusDegraded => 'Ograniczone przez ustawienia systemu'; + @override String get statusUnknown => 'Jeszcze nie sprawdzono'; + @override String get settingsUnavailable => 'Nie udało się otworzyć ustawień systemowych na tym urządzeniu'; + @override String get linkUnavailable => 'Nie udało się otworzyć dontkillmyapp.com na tym urządzeniu'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$pl extends Translations$companionRemote$session$en { _Translations$companionRemote$session$pl._(TranslationsPl root) : this._root = root, super.internal(root); @@ -3267,6 +3303,32 @@ extension on TranslationsPl { 'downloads.syncRuleNotAvailableForProfile' => 'Niedostępne dla bieżącego profilu', 'downloads.syncRuleUnknownServer' => 'Nieznany serwer', 'downloads.syncRuleListCreated' => 'Utworzono regułę synchronizacji', + 'downloads.backgroundWarning.bannerBlocked' => 'Po opuszczeniu aplikacji pobieranie zostanie zatrzymane', + 'downloads.backgroundWarning.bannerDegraded' => 'Pobieranie w tle może być ograniczone', + 'downloads.backgroundWarning.bannerAction' => 'Szczegóły', + 'downloads.backgroundWarning.sheetTitle' => 'Pobieranie w tle jest zablokowane', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Pobieranie w tle może być ograniczone', + 'downloads.backgroundWarning.sheetIntro' => 'Android uniemożliwia Plezy niezawodne pobieranie plików w tle.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Twoje urządzenie ogranicza możliwość pobierania w tle przez Plezy.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Działanie Plezy w tle jest ograniczone. Ustaw użycie baterii lub działanie w tle na „Bez ograniczeń”.', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android umieścił Plezy w ograniczonym trybie gotowości. Ustaw użycie baterii na „Bez ograniczeń”.', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Powiadomienia o pobieraniu są wyłączone, więc postęp i opcje sterowania mogą być niedostępne.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Powiadomienia są wyłączone. W Android 13 lub nowszym są wymagane przy długim pobieraniu w tle.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Oszczędzanie danych jest włączone, co blokuje pobieranie w tle przez mobilną transmisję danych. Pobieranie powinno nadal działać przez Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Pobieranie wielokrotnie przerywało się, gdy Plezy działało w tle. Sprawdź ustawienia baterii lub działania w tle dla Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Otwórz ustawienia', + 'downloads.backgroundWarning.stillNotWorking' => 'Pomoc dla Twojego urządzenia', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Zobacz instrukcje dla swojego urządzenia lub, jeśli problem nadal występuje, wyślij log przez Ustawienia › Pokaż logi.', + 'downloads.backgroundWarning.dialogTitle' => 'Pobieranie może się nie zakończyć', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Pobierz mimo to', + 'downloads.backgroundWarning.dialogFixFirst' => 'Najpierw rozwiąż problem', + 'downloads.backgroundWarning.statusTile' => 'Pobieranie w tle', + 'downloads.backgroundWarning.statusOk' => 'Działanie w tle jest dozwolone', + 'downloads.backgroundWarning.statusBlocked' => 'Zablokowane przez ustawienia systemu', + 'downloads.backgroundWarning.statusDegraded' => 'Ograniczone przez ustawienia systemu', + 'downloads.backgroundWarning.statusUnknown' => 'Jeszcze nie sprawdzono', + 'downloads.backgroundWarning.settingsUnavailable' => 'Nie udało się otworzyć ustawień systemowych na tym urządzeniu', + 'downloads.backgroundWarning.linkUnavailable' => 'Nie udało się otworzyć dontkillmyapp.com na tym urządzeniu', 'shaders.title' => 'Shadery', 'shaders.noShaderDescription' => 'Bez ulepszenia wideo', 'shaders.nvscalerDescription' => 'Skalowanie obrazu NVIDIA dla ostrzejszego wideo', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 31b40075..277756ac 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$pt extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Indisponível para o perfil atual'; @override String get syncRuleUnknownServer => 'Servidor desconhecido'; @override String get syncRuleListCreated => 'Regra de sincronização criada'; + @override late final _Translations$downloads$backgroundWarning$pt backgroundWarning = _Translations$downloads$backgroundWarning$pt._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$pt extends Translations$explore$status$en { @override String get upcoming => 'Em breve'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$pt extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$pt._(TranslationsPt root) : this._root = root, super.internal(root); + + final TranslationsPt _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Os downloads serão interrompidos ao sair do app'; + @override String get bannerDegraded => 'Os downloads em segundo plano podem ser limitados'; + @override String get bannerAction => 'Detalhes'; + @override String get sheetTitle => 'Os downloads em segundo plano estão bloqueados'; + @override String get sheetTitleDegraded => 'Os downloads em segundo plano podem ser limitados'; + @override String get sheetIntro => 'O Android está impedindo que o Plezy faça downloads de forma confiável em segundo plano.'; + @override String get sheetIntroDegraded => 'Seu dispositivo está limitando quando o Plezy pode fazer downloads em segundo plano.'; + @override String get reasonBackgroundRestricted => 'O uso em segundo plano do Plezy está restrito. Defina o uso da bateria ou o uso em segundo plano como "Sem restrições".'; + @override String get reasonStandbyRestricted => 'O Android colocou o Plezy em um modo de espera restrito. Defina o uso da bateria como "Sem restrições".'; + @override String get reasonDownloadChannelBlocked => 'As notificações de download estão desativadas; por isso, o progresso e os controles podem ficar indisponíveis.'; + @override String get reasonNotificationsDisabled => 'As notificações estão desativadas. No Android 13 ou mais recente, elas são necessárias para downloads longos em segundo plano.'; + @override String get reasonDataSaver => 'A Economia de dados está ativada e bloqueia downloads em segundo plano usando dados móveis. Os downloads ainda devem funcionar no Wi-Fi.'; + @override String get reasonOemUnknown => 'Os downloads foram interrompidos várias vezes enquanto o Plezy estava em segundo plano. Verifique as configurações de bateria ou uso em segundo plano do Plezy.'; + @override String get openSettings => 'Abrir configurações'; + @override String get stillNotWorking => 'Ajuda específica para o dispositivo'; + @override String get stillNotWorkingDescription => 'Veja as instruções para seu dispositivo ou, se o problema persistir, envie um log em Configurações › Ver Logs.'; + @override String get dialogTitle => 'Os downloads podem não ser concluídos'; + @override String get dialogDownloadAnyway => 'Baixar mesmo assim'; + @override String get dialogFixFirst => 'Corrigir primeiro'; + @override String get statusTile => 'Downloads em segundo plano'; + @override String get statusOk => 'Execução em segundo plano permitida'; + @override String get statusBlocked => 'Bloqueado pelas configurações do sistema'; + @override String get statusDegraded => 'Limitado pelas configurações do sistema'; + @override String get statusUnknown => 'Ainda não verificado'; + @override String get settingsUnavailable => 'Não foi possível abrir as configurações do sistema neste dispositivo'; + @override String get linkUnavailable => 'Não foi possível abrir dontkillmyapp.com neste dispositivo'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$pt extends Translations$companionRemote$session$en { _Translations$companionRemote$session$pt._(TranslationsPt root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsPt { 'downloads.syncRuleNotAvailableForProfile' => 'Indisponível para o perfil atual', 'downloads.syncRuleUnknownServer' => 'Servidor desconhecido', 'downloads.syncRuleListCreated' => 'Regra de sincronização criada', + 'downloads.backgroundWarning.bannerBlocked' => 'Os downloads serão interrompidos ao sair do app', + 'downloads.backgroundWarning.bannerDegraded' => 'Os downloads em segundo plano podem ser limitados', + 'downloads.backgroundWarning.bannerAction' => 'Detalhes', + 'downloads.backgroundWarning.sheetTitle' => 'Os downloads em segundo plano estão bloqueados', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Os downloads em segundo plano podem ser limitados', + 'downloads.backgroundWarning.sheetIntro' => 'O Android está impedindo que o Plezy faça downloads de forma confiável em segundo plano.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Seu dispositivo está limitando quando o Plezy pode fazer downloads em segundo plano.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'O uso em segundo plano do Plezy está restrito. Defina o uso da bateria ou o uso em segundo plano como "Sem restrições".', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'O Android colocou o Plezy em um modo de espera restrito. Defina o uso da bateria como "Sem restrições".', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'As notificações de download estão desativadas; por isso, o progresso e os controles podem ficar indisponíveis.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'As notificações estão desativadas. No Android 13 ou mais recente, elas são necessárias para downloads longos em segundo plano.', + 'downloads.backgroundWarning.reasonDataSaver' => 'A Economia de dados está ativada e bloqueia downloads em segundo plano usando dados móveis. Os downloads ainda devem funcionar no Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Os downloads foram interrompidos várias vezes enquanto o Plezy estava em segundo plano. Verifique as configurações de bateria ou uso em segundo plano do Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Abrir configurações', + 'downloads.backgroundWarning.stillNotWorking' => 'Ajuda específica para o dispositivo', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Veja as instruções para seu dispositivo ou, se o problema persistir, envie um log em Configurações › Ver Logs.', + 'downloads.backgroundWarning.dialogTitle' => 'Os downloads podem não ser concluídos', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Baixar mesmo assim', + 'downloads.backgroundWarning.dialogFixFirst' => 'Corrigir primeiro', + 'downloads.backgroundWarning.statusTile' => 'Downloads em segundo plano', + 'downloads.backgroundWarning.statusOk' => 'Execução em segundo plano permitida', + 'downloads.backgroundWarning.statusBlocked' => 'Bloqueado pelas configurações do sistema', + 'downloads.backgroundWarning.statusDegraded' => 'Limitado pelas configurações do sistema', + 'downloads.backgroundWarning.statusUnknown' => 'Ainda não verificado', + 'downloads.backgroundWarning.settingsUnavailable' => 'Não foi possível abrir as configurações do sistema neste dispositivo', + 'downloads.backgroundWarning.linkUnavailable' => 'Não foi possível abrir dontkillmyapp.com neste dispositivo', 'shaders.title' => 'Shaders', 'shaders.noShaderDescription' => 'Sem aprimoramento de vídeo', 'shaders.nvscalerDescription' => 'Dimensionamento de imagem da NVIDIA para vídeos mais nítidos', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 99072444..ecd6df9a 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -1459,6 +1459,7 @@ class _Translations$downloads$ru extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Недоступно для текущего профиля'; @override String get syncRuleUnknownServer => 'Неизвестный сервер'; @override String get syncRuleListCreated => 'Правило синхронизации создано'; + @override late final _Translations$downloads$backgroundWarning$ru backgroundWarning = _Translations$downloads$backgroundWarning$ru._(_root); } // Path: shaders @@ -1989,6 +1990,41 @@ class _Translations$explore$status$ru extends Translations$explore$status$en { @override String get upcoming => 'Скоро'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$ru extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$ru._(TranslationsRu root) : this._root = root, super.internal(root); + + final TranslationsRu _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Загрузки остановятся, когда вы выйдете из приложения'; + @override String get bannerDegraded => 'Фоновые загрузки могут быть ограничены'; + @override String get bannerAction => 'Подробнее'; + @override String get sheetTitle => 'Фоновые загрузки заблокированы'; + @override String get sheetTitleDegraded => 'Фоновые загрузки могут быть ограничены'; + @override String get sheetIntro => 'Android не позволяет Plezy стабильно выполнять загрузки в фоновом режиме.'; + @override String get sheetIntroDegraded => 'Ваше устройство ограничивает время, когда Plezy может выполнять загрузки в фоновом режиме.'; + @override String get reasonBackgroundRestricted => 'Фоновая работа Plezy ограничена. В настройках расхода заряда батареи или фоновой работы выберите «Без ограничений».'; + @override String get reasonStandbyRestricted => 'Android перевёл Plezy в режим ожидания с ограничениями. Для расхода заряда батареи выберите «Без ограничений».'; + @override String get reasonDownloadChannelBlocked => 'Уведомления о загрузках отключены, поэтому ход загрузок и элементы управления могут быть недоступны.'; + @override String get reasonNotificationsDisabled => 'Уведомления отключены. В Android 13 и более новых версиях они необходимы для длительных фоновых загрузок.'; + @override String get reasonDataSaver => 'Экономия трафика включена, поэтому фоновые загрузки через мобильную сеть заблокированы. Загрузки по Wi-Fi должны работать.'; + @override String get reasonOemUnknown => 'Загрузки неоднократно останавливались, пока Plezy работал в фоне. Проверьте настройки расхода заряда батареи или фоновой работы для Plezy.'; + @override String get openSettings => 'Открыть настройки'; + @override String get stillNotWorking => 'Помощь для вашего устройства'; + @override String get stillNotWorkingDescription => 'Посмотрите инструкции для своего устройства. Если проблема не исчезнет, отправьте лог через Настройки › Просмотр логов.'; + @override String get dialogTitle => 'Загрузки могут не завершиться'; + @override String get dialogDownloadAnyway => 'Всё равно скачать'; + @override String get dialogFixFirst => 'Сначала исправить'; + @override String get statusTile => 'Фоновые загрузки'; + @override String get statusOk => 'Работа в фоне разрешена'; + @override String get statusBlocked => 'Заблокировано настройками системы'; + @override String get statusDegraded => 'Ограничено настройками системы'; + @override String get statusUnknown => 'Ещё не проверено'; + @override String get settingsUnavailable => 'Не удалось открыть системные настройки на этом устройстве'; + @override String get linkUnavailable => 'Не удалось открыть dontkillmyapp.com на этом устройстве'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$ru extends Translations$companionRemote$session$en { _Translations$companionRemote$session$ru._(TranslationsRu root) : this._root = root, super.internal(root); @@ -3267,6 +3303,32 @@ extension on TranslationsRu { 'downloads.syncRuleNotAvailableForProfile' => 'Недоступно для текущего профиля', 'downloads.syncRuleUnknownServer' => 'Неизвестный сервер', 'downloads.syncRuleListCreated' => 'Правило синхронизации создано', + 'downloads.backgroundWarning.bannerBlocked' => 'Загрузки остановятся, когда вы выйдете из приложения', + 'downloads.backgroundWarning.bannerDegraded' => 'Фоновые загрузки могут быть ограничены', + 'downloads.backgroundWarning.bannerAction' => 'Подробнее', + 'downloads.backgroundWarning.sheetTitle' => 'Фоновые загрузки заблокированы', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Фоновые загрузки могут быть ограничены', + 'downloads.backgroundWarning.sheetIntro' => 'Android не позволяет Plezy стабильно выполнять загрузки в фоновом режиме.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Ваше устройство ограничивает время, когда Plezy может выполнять загрузки в фоновом режиме.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Фоновая работа Plezy ограничена. В настройках расхода заряда батареи или фоновой работы выберите «Без ограничений».', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android перевёл Plezy в режим ожидания с ограничениями. Для расхода заряда батареи выберите «Без ограничений».', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Уведомления о загрузках отключены, поэтому ход загрузок и элементы управления могут быть недоступны.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Уведомления отключены. В Android 13 и более новых версиях они необходимы для длительных фоновых загрузок.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Экономия трафика включена, поэтому фоновые загрузки через мобильную сеть заблокированы. Загрузки по Wi-Fi должны работать.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Загрузки неоднократно останавливались, пока Plezy работал в фоне. Проверьте настройки расхода заряда батареи или фоновой работы для Plezy.', + 'downloads.backgroundWarning.openSettings' => 'Открыть настройки', + 'downloads.backgroundWarning.stillNotWorking' => 'Помощь для вашего устройства', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Посмотрите инструкции для своего устройства. Если проблема не исчезнет, отправьте лог через Настройки › Просмотр логов.', + 'downloads.backgroundWarning.dialogTitle' => 'Загрузки могут не завершиться', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Всё равно скачать', + 'downloads.backgroundWarning.dialogFixFirst' => 'Сначала исправить', + 'downloads.backgroundWarning.statusTile' => 'Фоновые загрузки', + 'downloads.backgroundWarning.statusOk' => 'Работа в фоне разрешена', + 'downloads.backgroundWarning.statusBlocked' => 'Заблокировано настройками системы', + 'downloads.backgroundWarning.statusDegraded' => 'Ограничено настройками системы', + 'downloads.backgroundWarning.statusUnknown' => 'Ещё не проверено', + 'downloads.backgroundWarning.settingsUnavailable' => 'Не удалось открыть системные настройки на этом устройстве', + 'downloads.backgroundWarning.linkUnavailable' => 'Не удалось открыть dontkillmyapp.com на этом устройстве', 'shaders.title' => 'Шейдеры', 'shaders.noShaderDescription' => 'Без улучшения видео', 'shaders.nvscalerDescription' => 'Масштабирование NVIDIA для более чёткого видео', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index f8906615..8917d6f3 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -1453,6 +1453,7 @@ class _Translations$downloads$sv extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => 'Inte tillgänglig för aktuell profil'; @override String get syncRuleUnknownServer => 'Okänd server'; @override String get syncRuleListCreated => 'Synkroniseringsregel skapad'; + @override late final _Translations$downloads$backgroundWarning$sv backgroundWarning = _Translations$downloads$backgroundWarning$sv._(_root); } // Path: shaders @@ -1983,6 +1984,41 @@ class _Translations$explore$status$sv extends Translations$explore$status$en { @override String get upcoming => 'Kommande'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$sv extends Translations$downloads$backgroundWarning$en { + _Translations$downloads$backgroundWarning$sv._(TranslationsSv root) : this._root = root, super.internal(root); + + final TranslationsSv _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => 'Nedladdningar stoppas när du lämnar appen'; + @override String get bannerDegraded => 'Bakgrundsnedladdningar kan begränsas'; + @override String get bannerAction => 'Detaljer'; + @override String get sheetTitle => 'Bakgrundsnedladdningar är blockerade'; + @override String get sheetTitleDegraded => 'Bakgrundsnedladdningar kan begränsas'; + @override String get sheetIntro => 'Android hindrar Plezy från att ladda ned tillförlitligt i bakgrunden.'; + @override String get sheetIntroDegraded => 'Din enhet begränsar när Plezy kan ladda ned i bakgrunden.'; + @override String get reasonBackgroundRestricted => 'Plezys bakgrundsanvändning är begränsad. Ställ in batteri- eller bakgrundsanvändningen på "Obegränsad".'; + @override String get reasonStandbyRestricted => 'Android har satt Plezy i ett begränsat vänteläge. Ställ in batterianvändningen på "Obegränsad".'; + @override String get reasonDownloadChannelBlocked => 'Aviseringar om nedladdningar är avstängda, så förlopp och kontroller kanske inte är tillgängliga.'; + @override String get reasonNotificationsDisabled => 'Aviseringar är avstängda. På Android 13 eller senare krävs de för långa bakgrundsnedladdningar.'; + @override String get reasonDataSaver => 'Databesparing är aktiverad, vilket blockerar bakgrundsnedladdningar via mobildata. Nedladdningar bör fortfarande fungera via Wi-Fi.'; + @override String get reasonOemUnknown => 'Nedladdningar stoppades upprepade gånger när Plezy kördes i bakgrunden. Kontrollera Plezys inställningar för batteri- eller bakgrundsanvändning.'; + @override String get openSettings => 'Öppna inställningar'; + @override String get stillNotWorking => 'Enhetsspecifik hjälp'; + @override String get stillNotWorkingDescription => 'Se anvisningar för din enhet eller skicka en logg från Inställningar › Visa loggar om problemet kvarstår.'; + @override String get dialogTitle => 'Nedladdningar kanske inte slutförs'; + @override String get dialogDownloadAnyway => 'Ladda ned ändå'; + @override String get dialogFixFirst => 'Åtgärda först'; + @override String get statusTile => 'Bakgrundsnedladdningar'; + @override String get statusOk => 'Får köras i bakgrunden'; + @override String get statusBlocked => 'Blockeras av systeminställningar'; + @override String get statusDegraded => 'Begränsas av systeminställningar'; + @override String get statusUnknown => 'Inte kontrollerat än'; + @override String get settingsUnavailable => 'Det gick inte att öppna systeminställningarna på den här enheten'; + @override String get linkUnavailable => 'Det gick inte att öppna dontkillmyapp.com på den här enheten'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$sv extends Translations$companionRemote$session$en { _Translations$companionRemote$session$sv._(TranslationsSv root) : this._root = root, super.internal(root); @@ -3261,6 +3297,32 @@ extension on TranslationsSv { 'downloads.syncRuleNotAvailableForProfile' => 'Inte tillgänglig för aktuell profil', 'downloads.syncRuleUnknownServer' => 'Okänd server', 'downloads.syncRuleListCreated' => 'Synkroniseringsregel skapad', + 'downloads.backgroundWarning.bannerBlocked' => 'Nedladdningar stoppas när du lämnar appen', + 'downloads.backgroundWarning.bannerDegraded' => 'Bakgrundsnedladdningar kan begränsas', + 'downloads.backgroundWarning.bannerAction' => 'Detaljer', + 'downloads.backgroundWarning.sheetTitle' => 'Bakgrundsnedladdningar är blockerade', + 'downloads.backgroundWarning.sheetTitleDegraded' => 'Bakgrundsnedladdningar kan begränsas', + 'downloads.backgroundWarning.sheetIntro' => 'Android hindrar Plezy från att ladda ned tillförlitligt i bakgrunden.', + 'downloads.backgroundWarning.sheetIntroDegraded' => 'Din enhet begränsar när Plezy kan ladda ned i bakgrunden.', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezys bakgrundsanvändning är begränsad. Ställ in batteri- eller bakgrundsanvändningen på "Obegränsad".', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android har satt Plezy i ett begränsat vänteläge. Ställ in batterianvändningen på "Obegränsad".', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Aviseringar om nedladdningar är avstängda, så förlopp och kontroller kanske inte är tillgängliga.', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Aviseringar är avstängda. På Android 13 eller senare krävs de för långa bakgrundsnedladdningar.', + 'downloads.backgroundWarning.reasonDataSaver' => 'Databesparing är aktiverad, vilket blockerar bakgrundsnedladdningar via mobildata. Nedladdningar bör fortfarande fungera via Wi-Fi.', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Nedladdningar stoppades upprepade gånger när Plezy kördes i bakgrunden. Kontrollera Plezys inställningar för batteri- eller bakgrundsanvändning.', + 'downloads.backgroundWarning.openSettings' => 'Öppna inställningar', + 'downloads.backgroundWarning.stillNotWorking' => 'Enhetsspecifik hjälp', + 'downloads.backgroundWarning.stillNotWorkingDescription' => 'Se anvisningar för din enhet eller skicka en logg från Inställningar › Visa loggar om problemet kvarstår.', + 'downloads.backgroundWarning.dialogTitle' => 'Nedladdningar kanske inte slutförs', + 'downloads.backgroundWarning.dialogDownloadAnyway' => 'Ladda ned ändå', + 'downloads.backgroundWarning.dialogFixFirst' => 'Åtgärda först', + 'downloads.backgroundWarning.statusTile' => 'Bakgrundsnedladdningar', + 'downloads.backgroundWarning.statusOk' => 'Får köras i bakgrunden', + 'downloads.backgroundWarning.statusBlocked' => 'Blockeras av systeminställningar', + 'downloads.backgroundWarning.statusDegraded' => 'Begränsas av systeminställningar', + 'downloads.backgroundWarning.statusUnknown' => 'Inte kontrollerat än', + 'downloads.backgroundWarning.settingsUnavailable' => 'Det gick inte att öppna systeminställningarna på den här enheten', + 'downloads.backgroundWarning.linkUnavailable' => 'Det gick inte att öppna dontkillmyapp.com på den här enheten', 'shaders.title' => 'Shaders', 'shaders.noShaderDescription' => 'Ingen videoförbättring', 'shaders.nvscalerDescription' => 'NVIDIA-bildskalning för skarpare video', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index c44a45da..fc7add1e 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -1450,6 +1450,7 @@ class Translations$downloads$zh extends Translations$downloads$en { @override String get syncRuleNotAvailableForProfile => '当前用户资料不可用'; @override String get syncRuleUnknownServer => '未知服务器'; @override String get syncRuleListCreated => '同步规则已创建'; + @override late final Translations$downloads$backgroundWarning$zh backgroundWarning = Translations$downloads$backgroundWarning$zh.internal(_root); } // Path: shaders @@ -1980,6 +1981,41 @@ class Translations$explore$status$zh extends Translations$explore$status$en { @override String get upcoming => '即将上线'; } +// Path: downloads.backgroundWarning +class Translations$downloads$backgroundWarning$zh extends Translations$downloads$backgroundWarning$en { + Translations$downloads$backgroundWarning$zh.internal(TranslationsZh root) : this._root = root, super.internal(root); + + final TranslationsZh _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => '离开应用后,下载将停止'; + @override String get bannerDegraded => '后台下载可能受限'; + @override String get bannerAction => '详情'; + @override String get sheetTitle => '后台下载已被阻止'; + @override String get sheetTitleDegraded => '后台下载可能受限'; + @override String get sheetIntro => 'Android 正在阻止 Plezy 在后台稳定下载。'; + @override String get sheetIntroDegraded => '你的设备限制了 Plezy 在后台下载的时机。'; + @override String get reasonBackgroundRestricted => 'Plezy 的后台使用受到限制。请将其电池用量或后台使用设置为“不受限制”。'; + @override String get reasonStandbyRestricted => 'Android 已将 Plezy 置于受限待机状态。请将其电池用量设为“不受限制”。'; + @override String get reasonDownloadChannelBlocked => '下载通知已关闭,因此可能无法查看进度或进行控制。'; + @override String get reasonNotificationsDisabled => '通知已关闭。在 Android 13 或更高版本中,长时间后台下载需要开启通知。'; + @override String get reasonDataSaver => '流量节省程序已开启,会阻止使用移动数据进行后台下载。使用 Wi-Fi 时下载应仍可进行。'; + @override String get reasonOemUnknown => 'Plezy 在后台时,下载曾多次停止。请检查 Plezy 的电池用量或后台使用设置。'; + @override String get openSettings => '打开设置'; + @override String get stillNotWorking => '设备专属帮助'; + @override String get stillNotWorkingDescription => '查看适用于你设备的操作步骤;如果问题仍然存在,请通过设置 › 查看日志发送日志。'; + @override String get dialogTitle => '下载可能无法完成'; + @override String get dialogDownloadAnyway => '仍要下载'; + @override String get dialogFixFirst => '先解决此问题'; + @override String get statusTile => '后台下载'; + @override String get statusOk => '允许在后台运行'; + @override String get statusBlocked => '已被系统设置阻止'; + @override String get statusDegraded => '受系统设置限制'; + @override String get statusUnknown => '尚未检查'; + @override String get settingsUnavailable => '无法在此设备上打开系统设置'; + @override String get linkUnavailable => '无法在此设备上打开 dontkillmyapp.com'; +} + // Path: companionRemote.session class Translations$companionRemote$session$zh extends Translations$companionRemote$session$en { Translations$companionRemote$session$zh.internal(TranslationsZh root) : this._root = root, super.internal(root); @@ -3258,6 +3294,32 @@ extension on TranslationsZh { 'downloads.syncRuleNotAvailableForProfile' => '当前用户资料不可用', 'downloads.syncRuleUnknownServer' => '未知服务器', 'downloads.syncRuleListCreated' => '同步规则已创建', + 'downloads.backgroundWarning.bannerBlocked' => '离开应用后,下载将停止', + 'downloads.backgroundWarning.bannerDegraded' => '后台下载可能受限', + 'downloads.backgroundWarning.bannerAction' => '详情', + 'downloads.backgroundWarning.sheetTitle' => '后台下载已被阻止', + 'downloads.backgroundWarning.sheetTitleDegraded' => '后台下载可能受限', + 'downloads.backgroundWarning.sheetIntro' => 'Android 正在阻止 Plezy 在后台稳定下载。', + 'downloads.backgroundWarning.sheetIntroDegraded' => '你的设备限制了 Plezy 在后台下载的时机。', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezy 的后台使用受到限制。请将其电池用量或后台使用设置为“不受限制”。', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android 已将 Plezy 置于受限待机状态。请将其电池用量设为“不受限制”。', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => '下载通知已关闭,因此可能无法查看进度或进行控制。', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => '通知已关闭。在 Android 13 或更高版本中,长时间后台下载需要开启通知。', + 'downloads.backgroundWarning.reasonDataSaver' => '流量节省程序已开启,会阻止使用移动数据进行后台下载。使用 Wi-Fi 时下载应仍可进行。', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Plezy 在后台时,下载曾多次停止。请检查 Plezy 的电池用量或后台使用设置。', + 'downloads.backgroundWarning.openSettings' => '打开设置', + 'downloads.backgroundWarning.stillNotWorking' => '设备专属帮助', + 'downloads.backgroundWarning.stillNotWorkingDescription' => '查看适用于你设备的操作步骤;如果问题仍然存在,请通过设置 › 查看日志发送日志。', + 'downloads.backgroundWarning.dialogTitle' => '下载可能无法完成', + 'downloads.backgroundWarning.dialogDownloadAnyway' => '仍要下载', + 'downloads.backgroundWarning.dialogFixFirst' => '先解决此问题', + 'downloads.backgroundWarning.statusTile' => '后台下载', + 'downloads.backgroundWarning.statusOk' => '允许在后台运行', + 'downloads.backgroundWarning.statusBlocked' => '已被系统设置阻止', + 'downloads.backgroundWarning.statusDegraded' => '受系统设置限制', + 'downloads.backgroundWarning.statusUnknown' => '尚未检查', + 'downloads.backgroundWarning.settingsUnavailable' => '无法在此设备上打开系统设置', + 'downloads.backgroundWarning.linkUnavailable' => '无法在此设备上打开 dontkillmyapp.com', 'shaders.title' => '着色器', 'shaders.noShaderDescription' => '无视频增强', 'shaders.nvscalerDescription' => 'NVIDIA 图像缩放,使视频更清晰', diff --git a/lib/i18n/strings_zh_Hant.g.dart b/lib/i18n/strings_zh_Hant.g.dart index a5c7df34..7d8e5d64 100644 --- a/lib/i18n/strings_zh_Hant.g.dart +++ b/lib/i18n/strings_zh_Hant.g.dart @@ -1451,6 +1451,7 @@ class _Translations$downloads$zh_Hant extends Translations$downloads$zh { @override String get syncRuleNotAvailableForProfile => '目前使用者設定檔無法使用'; @override String get syncRuleUnknownServer => '未知伺服器'; @override String get syncRuleListCreated => '同步規則已建立'; + @override late final _Translations$downloads$backgroundWarning$zh_Hant backgroundWarning = _Translations$downloads$backgroundWarning$zh_Hant._(_root); } // Path: shaders @@ -1981,6 +1982,41 @@ class _Translations$explore$status$zh_Hant extends Translations$explore$status$z @override String get upcoming => '即將上線'; } +// Path: downloads.backgroundWarning +class _Translations$downloads$backgroundWarning$zh_Hant extends Translations$downloads$backgroundWarning$zh { + _Translations$downloads$backgroundWarning$zh_Hant._(TranslationsZhHant root) : this._root = root, super.internal(root); + + final TranslationsZhHant _root; // ignore: unused_field + + // Translations + @override String get bannerBlocked => '離開應用程式後,下載將會停止'; + @override String get bannerDegraded => '背景下載可能受限'; + @override String get bannerAction => '詳細資料'; + @override String get sheetTitle => '背景下載遭到封鎖'; + @override String get sheetTitleDegraded => '背景下載可能受限'; + @override String get sheetIntro => 'Android 正在阻止 Plezy 在背景穩定下載。'; + @override String get sheetIntroDegraded => '你的裝置限制了 Plezy 可在背景下載的時機。'; + @override String get reasonBackgroundRestricted => 'Plezy 的背景使用受限。請將其電池用量或背景使用設定為「無限制」。'; + @override String get reasonStandbyRestricted => 'Android 已將 Plezy 設為受限待命狀態。請將電池用量設為「無限制」。'; + @override String get reasonDownloadChannelBlocked => '下載通知已關閉,因此可能無法查看進度或使用控制項。'; + @override String get reasonNotificationsDisabled => '通知已關閉。在 Android 13 或更新版本中,長時間背景下載需要啟用通知。'; + @override String get reasonDataSaver => '已開啟數據節省模式,因此系統會封鎖使用行動數據的背景下載。透過 Wi-Fi 下載應仍可正常執行。'; + @override String get reasonOemUnknown => 'Plezy 在背景執行時,下載屢次停止。請檢查 Plezy 的電池用量或背景使用設定。'; + @override String get openSettings => '開啟設定'; + @override String get stillNotWorking => '裝置專屬說明'; + @override String get stillNotWorkingDescription => '查看適用於你裝置的步驟;若問題持續發生,也可從設定 › 查看日誌傳送日誌。'; + @override String get dialogTitle => '下載可能無法完成'; + @override String get dialogDownloadAnyway => '仍要下載'; + @override String get dialogFixFirst => '先修正設定'; + @override String get statusTile => '背景下載'; + @override String get statusOk => '可在背景執行'; + @override String get statusBlocked => '遭系統設定封鎖'; + @override String get statusDegraded => '受系統設定限制'; + @override String get statusUnknown => '尚未檢查'; + @override String get settingsUnavailable => '無法在這部裝置上開啟系統設定'; + @override String get linkUnavailable => '無法在這部裝置上開啟 dontkillmyapp.com'; +} + // Path: companionRemote.session class _Translations$companionRemote$session$zh_Hant extends Translations$companionRemote$session$zh { _Translations$companionRemote$session$zh_Hant._(TranslationsZhHant root) : this._root = root, super.internal(root); @@ -3259,6 +3295,32 @@ extension on TranslationsZhHant { 'downloads.syncRuleNotAvailableForProfile' => '目前使用者設定檔無法使用', 'downloads.syncRuleUnknownServer' => '未知伺服器', 'downloads.syncRuleListCreated' => '同步規則已建立', + 'downloads.backgroundWarning.bannerBlocked' => '離開應用程式後,下載將會停止', + 'downloads.backgroundWarning.bannerDegraded' => '背景下載可能受限', + 'downloads.backgroundWarning.bannerAction' => '詳細資料', + 'downloads.backgroundWarning.sheetTitle' => '背景下載遭到封鎖', + 'downloads.backgroundWarning.sheetTitleDegraded' => '背景下載可能受限', + 'downloads.backgroundWarning.sheetIntro' => 'Android 正在阻止 Plezy 在背景穩定下載。', + 'downloads.backgroundWarning.sheetIntroDegraded' => '你的裝置限制了 Plezy 可在背景下載的時機。', + 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezy 的背景使用受限。請將其電池用量或背景使用設定為「無限制」。', + 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android 已將 Plezy 設為受限待命狀態。請將電池用量設為「無限制」。', + 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => '下載通知已關閉,因此可能無法查看進度或使用控制項。', + 'downloads.backgroundWarning.reasonNotificationsDisabled' => '通知已關閉。在 Android 13 或更新版本中,長時間背景下載需要啟用通知。', + 'downloads.backgroundWarning.reasonDataSaver' => '已開啟數據節省模式,因此系統會封鎖使用行動數據的背景下載。透過 Wi-Fi 下載應仍可正常執行。', + 'downloads.backgroundWarning.reasonOemUnknown' => 'Plezy 在背景執行時,下載屢次停止。請檢查 Plezy 的電池用量或背景使用設定。', + 'downloads.backgroundWarning.openSettings' => '開啟設定', + 'downloads.backgroundWarning.stillNotWorking' => '裝置專屬說明', + 'downloads.backgroundWarning.stillNotWorkingDescription' => '查看適用於你裝置的步驟;若問題持續發生,也可從設定 › 查看日誌傳送日誌。', + 'downloads.backgroundWarning.dialogTitle' => '下載可能無法完成', + 'downloads.backgroundWarning.dialogDownloadAnyway' => '仍要下載', + 'downloads.backgroundWarning.dialogFixFirst' => '先修正設定', + 'downloads.backgroundWarning.statusTile' => '背景下載', + 'downloads.backgroundWarning.statusOk' => '可在背景執行', + 'downloads.backgroundWarning.statusBlocked' => '遭系統設定封鎖', + 'downloads.backgroundWarning.statusDegraded' => '受系統設定限制', + 'downloads.backgroundWarning.statusUnknown' => '尚未檢查', + 'downloads.backgroundWarning.settingsUnavailable' => '無法在這部裝置上開啟系統設定', + 'downloads.backgroundWarning.linkUnavailable' => '無法在這部裝置上開啟 dontkillmyapp.com', 'shaders.title' => '著色器', 'shaders.noShaderDescription' => '無影片增強效果', 'shaders.nvscalerDescription' => 'NVIDIA 圖像縮放技術,使影片邊緣更清晰', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 66fdf70c..ef269a85 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -1206,7 +1206,35 @@ "syncRuleSignInRequired": "Inloggning krävs", "syncRuleNotAvailableForProfile": "Inte tillgänglig för aktuell profil", "syncRuleUnknownServer": "Okänd server", - "syncRuleListCreated": "Synkroniseringsregel skapad" + "syncRuleListCreated": "Synkroniseringsregel skapad", + "backgroundWarning": { + "bannerBlocked": "Nedladdningar stoppas när du lämnar appen", + "bannerDegraded": "Bakgrundsnedladdningar kan begränsas", + "bannerAction": "Detaljer", + "sheetTitle": "Bakgrundsnedladdningar är blockerade", + "sheetTitleDegraded": "Bakgrundsnedladdningar kan begränsas", + "sheetIntro": "Android hindrar Plezy från att ladda ned tillförlitligt i bakgrunden.", + "sheetIntroDegraded": "Din enhet begränsar när Plezy kan ladda ned i bakgrunden.", + "reasonBackgroundRestricted": "Plezys bakgrundsanvändning är begränsad. Ställ in batteri- eller bakgrundsanvändningen på \"Obegränsad\".", + "reasonStandbyRestricted": "Android har satt Plezy i ett begränsat vänteläge. Ställ in batterianvändningen på \"Obegränsad\".", + "reasonDownloadChannelBlocked": "Aviseringar om nedladdningar är avstängda, så förlopp och kontroller kanske inte är tillgängliga.", + "reasonNotificationsDisabled": "Aviseringar är avstängda. På Android 13 eller senare krävs de för långa bakgrundsnedladdningar.", + "reasonDataSaver": "Databesparing är aktiverad, vilket blockerar bakgrundsnedladdningar via mobildata. Nedladdningar bör fortfarande fungera via Wi-Fi.", + "reasonOemUnknown": "Nedladdningar stoppades upprepade gånger när Plezy kördes i bakgrunden. Kontrollera Plezys inställningar för batteri- eller bakgrundsanvändning.", + "openSettings": "Öppna inställningar", + "stillNotWorking": "Enhetsspecifik hjälp", + "stillNotWorkingDescription": "Se anvisningar för din enhet eller skicka en logg från Inställningar › Visa loggar om problemet kvarstår.", + "dialogTitle": "Nedladdningar kanske inte slutförs", + "dialogDownloadAnyway": "Ladda ned ändå", + "dialogFixFirst": "Åtgärda först", + "statusTile": "Bakgrundsnedladdningar", + "statusOk": "Får köras i bakgrunden", + "statusBlocked": "Blockeras av systeminställningar", + "statusDegraded": "Begränsas av systeminställningar", + "statusUnknown": "Inte kontrollerat än", + "settingsUnavailable": "Det gick inte att öppna systeminställningarna på den här enheten", + "linkUnavailable": "Det gick inte att öppna dontkillmyapp.com på den här enheten" + } }, "shaders": { "title": "Shaders", diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 6bac7e12..cc866811 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -1203,7 +1203,35 @@ "syncRuleSignInRequired": "需要登入", "syncRuleNotAvailableForProfile": "目前使用者設定檔無法使用", "syncRuleUnknownServer": "未知伺服器", - "syncRuleListCreated": "同步規則已建立" + "syncRuleListCreated": "同步規則已建立", + "backgroundWarning": { + "bannerBlocked": "離開應用程式後,下載將會停止", + "bannerDegraded": "背景下載可能受限", + "bannerAction": "詳細資料", + "sheetTitle": "背景下載遭到封鎖", + "sheetTitleDegraded": "背景下載可能受限", + "sheetIntro": "Android 正在阻止 Plezy 在背景穩定下載。", + "sheetIntroDegraded": "你的裝置限制了 Plezy 可在背景下載的時機。", + "reasonBackgroundRestricted": "Plezy 的背景使用受限。請將其電池用量或背景使用設定為「無限制」。", + "reasonStandbyRestricted": "Android 已將 Plezy 設為受限待命狀態。請將電池用量設為「無限制」。", + "reasonDownloadChannelBlocked": "下載通知已關閉,因此可能無法查看進度或使用控制項。", + "reasonNotificationsDisabled": "通知已關閉。在 Android 13 或更新版本中,長時間背景下載需要啟用通知。", + "reasonDataSaver": "已開啟數據節省模式,因此系統會封鎖使用行動數據的背景下載。透過 Wi-Fi 下載應仍可正常執行。", + "reasonOemUnknown": "Plezy 在背景執行時,下載屢次停止。請檢查 Plezy 的電池用量或背景使用設定。", + "openSettings": "開啟設定", + "stillNotWorking": "裝置專屬說明", + "stillNotWorkingDescription": "查看適用於你裝置的步驟;若問題持續發生,也可從設定 › 查看日誌傳送日誌。", + "dialogTitle": "下載可能無法完成", + "dialogDownloadAnyway": "仍要下載", + "dialogFixFirst": "先修正設定", + "statusTile": "背景下載", + "statusOk": "可在背景執行", + "statusBlocked": "遭系統設定封鎖", + "statusDegraded": "受系統設定限制", + "statusUnknown": "尚未檢查", + "settingsUnavailable": "無法在這部裝置上開啟系統設定", + "linkUnavailable": "無法在這部裝置上開啟 dontkillmyapp.com" + } }, "shaders": { "title": "著色器", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index deb964ee..91e4e29a 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -1203,7 +1203,35 @@ "syncRuleSignInRequired": "需要登录", "syncRuleNotAvailableForProfile": "当前用户资料不可用", "syncRuleUnknownServer": "未知服务器", - "syncRuleListCreated": "同步规则已创建" + "syncRuleListCreated": "同步规则已创建", + "backgroundWarning": { + "bannerBlocked": "离开应用后,下载将停止", + "bannerDegraded": "后台下载可能受限", + "bannerAction": "详情", + "sheetTitle": "后台下载已被阻止", + "sheetTitleDegraded": "后台下载可能受限", + "sheetIntro": "Android 正在阻止 Plezy 在后台稳定下载。", + "sheetIntroDegraded": "你的设备限制了 Plezy 在后台下载的时机。", + "reasonBackgroundRestricted": "Plezy 的后台使用受到限制。请将其电池用量或后台使用设置为“不受限制”。", + "reasonStandbyRestricted": "Android 已将 Plezy 置于受限待机状态。请将其电池用量设为“不受限制”。", + "reasonDownloadChannelBlocked": "下载通知已关闭,因此可能无法查看进度或进行控制。", + "reasonNotificationsDisabled": "通知已关闭。在 Android 13 或更高版本中,长时间后台下载需要开启通知。", + "reasonDataSaver": "流量节省程序已开启,会阻止使用移动数据进行后台下载。使用 Wi-Fi 时下载应仍可进行。", + "reasonOemUnknown": "Plezy 在后台时,下载曾多次停止。请检查 Plezy 的电池用量或后台使用设置。", + "openSettings": "打开设置", + "stillNotWorking": "设备专属帮助", + "stillNotWorkingDescription": "查看适用于你设备的操作步骤;如果问题仍然存在,请通过设置 › 查看日志发送日志。", + "dialogTitle": "下载可能无法完成", + "dialogDownloadAnyway": "仍要下载", + "dialogFixFirst": "先解决此问题", + "statusTile": "后台下载", + "statusOk": "允许在后台运行", + "statusBlocked": "已被系统设置阻止", + "statusDegraded": "受系统设置限制", + "statusUnknown": "尚未检查", + "settingsUnavailable": "无法在此设备上打开系统设置", + "linkUnavailable": "无法在此设备上打开 dontkillmyapp.com" + } }, "shaders": { "title": "着色器", diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index bf50e991..284be360 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -13,6 +13,7 @@ import '../models/download_models.dart'; import '../utils/download_version_utils.dart'; import '../database/app_database.dart'; import '../database/download_operations.dart'; +import '../services/background_work_diagnostics_service.dart'; import '../services/download_manager_service.dart'; import '../services/api_cache.dart'; import '../services/download_artwork_service.dart'; @@ -28,6 +29,7 @@ import '../utils/deletion_notifier.dart'; import '../utils/downloaded_version_match.dart'; import '../media/episode_collection.dart'; import '../utils/global_key_utils.dart'; +import '../utils/notification_permission.dart'; import '../utils/watch_state_notifier.dart'; import '../mixins/disposable_change_notifier_mixin.dart'; @@ -105,6 +107,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin _activeProfileId == ownership.profileId && _profileGeneration == ownership.generation; OfflineModeSource? _offlineSource; + int _networkStateGeneration = 0; DownloadProvider({required this._downloadManager, required this._database}) : _syncRuleExecutor = SyncRuleExecutor(database: _database) { @@ -117,6 +120,10 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin // Load persisted downloads from database _initFuture = _loadPersistedDownloads(); + + // Lets the diagnostics service score whether downloads actually advance + // while the app is backgrounded, without it reaching into providers. + BackgroundWorkDiagnosticsService.instance.bindActivitySource(downloadActivitySnapshot); } /// Test-only constructor that skips the heavy initial load (artwork dir, @@ -141,7 +148,12 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin /// the device has no Plex connectivity. Sync-rule execution receives a /// snapshot of this state when invoked, keeping this provider as the owner. void setOfflineSource(OfflineModeSource? source) { - _offlineSource = source; + if (!identical(_offlineSource, source)) { + _offlineSource?.removeListener(_onOfflineSourceChanged); + _offlineSource = source; + _offlineSource?.addListener(_onOfflineSourceChanged); + _networkStateGeneration++; + } _downloadManager.setOfflineSource(source); } @@ -498,11 +510,32 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin void _onProgressUpdate(DownloadProgress progress) { appLogger.d('Progress update received: ${progress.globalKey} - ${progress.status} - ${progress.progress}%'); final ownedByActiveProfile = _ownsDownloadKey(progress.globalKey); - _downloads[progress.globalKey] = progress; + final previous = _downloads[progress.globalKey]; + final terminalUpdateOmittedBytes = + previous != null && + progress.downloadedBytes == 0 && + previous.downloadedBytes > 0 && + switch (progress.status) { + DownloadStatus.completed || + DownloadStatus.failed || + DownloadStatus.cancelled || + DownloadStatus.partial => true, + DownloadStatus.queued || DownloadStatus.downloading || DownloadStatus.paused => false, + }; + // Terminal status-only events omit byte fields. Preserve only those omitted + // counters; live progress and explicit retry resets must remain authoritative. + final merged = terminalUpdateOmittedBytes + ? progress.copyWith( + progress: progress.progress == 0 ? previous.progress : progress.progress, + downloadedBytes: previous.downloadedBytes, + totalBytes: progress.totalBytes == 0 ? previous.totalBytes : progress.totalBytes, + ) + : progress; + _downloads[progress.globalKey] = merged; // Sync artwork paths when they are available. - if (progress.hasArtworkPaths) { - _artworkPaths[progress.globalKey] = DownloadedArtwork(thumbPath: progress.thumbPath); + if (merged.hasArtworkPaths) { + _artworkPaths[merged.globalKey] = DownloadedArtwork(thumbPath: merged.thumbPath); } if (ownedByActiveProfile) safeNotifyListeners(); @@ -510,6 +543,8 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin @override void dispose() { + BackgroundWorkDiagnosticsService.instance.unbindActivitySource(downloadActivitySnapshot); + _offlineSource?.removeListener(_onOfflineSourceChanged); _progressSubscription?.cancel(); _deletionProgressSubscription?.cancel(); _metadataStore @@ -519,6 +554,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin } void _onMetadataStoreChanged() => safeNotifyListeners(); + void _onOfflineSourceChanged() => _networkStateGeneration++; /// Ensure metadata has a serverId, falling back to a parent's serverId. MediaItem _ensureServerId(MediaItem metadata, String? fallbackServerId) => @@ -528,6 +564,49 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin Map get downloads => Map.unmodifiable(Map.fromEntries(_downloads.entries.where(_ownsProgressEntry))); + /// Aggregate transfer activity for [BackgroundWorkDiagnosticsService]. + /// + /// Deliberately unfiltered by profile: the OS restricts the process, not a + /// profile. Byte and percentage totals are merged monotonically per row; + /// status counters distinguish a drained queue from a killed task. + /// + /// A method rather than a getter so the tear-off handed to + /// [BackgroundWorkDiagnosticsService.bindActivitySource] compares equal at + /// unbind time. + DownloadActivitySnapshot downloadActivitySnapshot() { + var activeTasks = 0; + var completedTasks = 0; + var failedTasks = 0; + var downloadedBytes = 0; + var progressUnits = 0; + for (final progress in _downloads.values) { + switch (progress.status) { + case DownloadStatus.downloading: + activeTasks++; + case DownloadStatus.completed: + completedTasks++; + case DownloadStatus.failed: + failedTasks++; + case DownloadStatus.queued: + case DownloadStatus.paused: + case DownloadStatus.cancelled: + case DownloadStatus.partial: + break; + } + downloadedBytes += progress.downloadedBytes; + progressUnits += progress.progress; + } + return ( + activeTasks: activeTasks, + completedTasks: completedTasks, + failedTasks: failedTasks, + downloadedBytes: downloadedBytes, + progressUnits: progressUnits, + networkAvailable: _offlineSource == null ? null : !_offlineSource!.isOffline, + networkStateGeneration: _networkStateGeneration, + ); + } + /// All metadata for downloads Map get metadata => _metadataStore.resolvedItems; @@ -996,6 +1075,10 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin throw CellularDownloadBlockedException(); } if (!_isQueueOwnershipCurrent(ownership)) return 0; + // The queueing claim above remains held while Android's permission + // dialog is open, so a second tap cannot launch duplicate expansion. + await NotificationPermission.ensure(); + if (!_isQueueOwnershipCurrent(ownership)) return 0; if (metadata.isMovie || metadata.isEpisode || metadata.kind == MediaKind.track) { final queued = await _queueSingleDownload( diff --git a/lib/screens/downloads/downloads_screen.dart b/lib/screens/downloads/downloads_screen.dart index ce9f3b01..963407ad 100644 --- a/lib/screens/downloads/downloads_screen.dart +++ b/lib/screens/downloads/downloads_screen.dart @@ -11,7 +11,9 @@ import '../../providers/multi_server_provider.dart'; import '../../services/music/music_playback_service.dart'; import '../../theme/mono_tokens.dart'; import '../../utils/music_navigation.dart'; +import '../../models/download_models.dart'; import '../../widgets/app_icon.dart'; +import '../../widgets/background_download_warning_banner.dart'; import '../../widgets/focusable_tab_chip.dart'; import '../../widgets/music/mini_player.dart'; import '../../widgets/music/track_row.dart'; @@ -165,6 +167,16 @@ class DownloadsScreenState extends State SliverFillRemaining( child: Column( children: [ + // Only while something is actually queued or transferring — + // an idle Downloads screen has nothing to warn about. + Selector( + selector: (_, provider) => provider.downloads.values.any( + (progress) => + progress.status == DownloadStatus.downloading || progress.status == DownloadStatus.queued, + ), + builder: (context, hasPendingDownloads, _) => + BackgroundDownloadWarningBanner(hasPendingDownloads: hasPendingDownloads), + ), // Tab selector chips (only on mobile - desktop has them in app bar) if (!PlatformDetector.shouldUseSideNavigation(context)) Container( diff --git a/lib/screens/media_detail/action_buttons.dart b/lib/screens/media_detail/action_buttons.dart index 66d1efac..bcbaf7c2 100644 --- a/lib/screens/media_detail/action_buttons.dart +++ b/lib/screens/media_detail/action_buttons.dart @@ -473,6 +473,10 @@ extension _MediaDetailActionButtons on _MediaDetailScreenState { } if (progress?.status == DownloadStatus.failed) { + // A failed download is the likeliest moment for the restriction to be + // the actual cause, so check before spending another attempt on it. + if (!await confirmBackgroundDownloadRestrictions(context) || !mounted) return; + final client = _getMediaClientForMetadata(context); if (client == null) return; @@ -502,6 +506,7 @@ extension _MediaDetailActionButtons on _MediaDetailScreenState { await downloadProvider.deleteDownload(globalKey); if (mounted) showSuccessSnackBar(context, t.downloads.downloadDeleted); } else if (retry && mounted) { + if (!await confirmBackgroundDownloadRestrictions(context) || !mounted) return; final client = _getMediaClientForMetadata(context); if (client == null) return; @@ -524,6 +529,7 @@ extension _MediaDetailActionButtons on _MediaDetailScreenState { await _showSyncRuleActions(context, downloadProvider, metadata, ruleKey: ruleKey, downloadGlobalKey: globalKey); return; } + if (!await confirmBackgroundDownloadRestrictions(context) || !mounted) return; final client = _getMediaClientForMetadata(context); if (client == null) return; diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index d4e1fc15..16e3b26a 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -12,6 +12,7 @@ import '../services/fullscreen_state_manager.dart'; import 'package:flutter/services.dart'; import 'package:plezy/utils/platform_detector.dart'; import 'package:plezy/widgets/app_icon.dart'; +import 'package:plezy/widgets/background_download_warning_banner.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../widgets/collapsible_text.dart'; diff --git a/lib/screens/music/album_detail_screen.dart b/lib/screens/music/album_detail_screen.dart index c096137b..6e5679fd 100644 --- a/lib/screens/music/album_detail_screen.dart +++ b/lib/screens/music/album_detail_screen.dart @@ -25,6 +25,7 @@ import '../../utils/platform_detector.dart'; import '../../utils/provider_extensions.dart'; import '../../utils/snackbar_helper.dart'; import '../../widgets/app_icon.dart'; +import '../../widgets/background_download_warning_banner.dart'; import '../../widgets/desktop_app_bar.dart'; import '../../widgets/download_status_icon.dart'; import '../../widgets/ios_status_bar_tap_scroll_to_top.dart'; @@ -178,6 +179,8 @@ class _AlbumDetailScreenState extends BaseMediaListDetailScreen with MountedSetStateMixin { renderer = 'unknown'; } buffer.writeln('Renderer: $renderer'); + // "Are downloads allowed to run in the background" is the single most + // asked support question; answering it from the uploaded log turns a + // four-message thread into one reply. + final backgroundWork = BackgroundWorkDiagnosticsService.instance; + await backgroundWork.refresh(); + buffer.writeln('Background: ${backgroundWork.describeSync()}'); } else if (Platform.isIOS) { final info = await deviceInfo.iosInfo; buffer.writeln('iOS ${info.systemVersion}'); diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 0954ad35..aab599b2 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -29,7 +29,9 @@ import '../../providers/seerr_account_provider.dart'; import '../../providers/trackers_provider.dart'; import '../../providers/trakt_account_provider.dart'; import '../../services/keyboard_shortcuts_service.dart'; +import '../../services/background_work_diagnostics_service.dart'; import '../../services/settings_service.dart' as settings; +import '../../widgets/background_download_warning_banner.dart'; import '../../services/update_service.dart'; import '../../utils/app_logger.dart'; import '../../utils/dialogs.dart'; @@ -63,6 +65,7 @@ class SettingsScreen extends StatefulWidget { this.downloadDirectoryWritableChecker, this.settingsExporter, this.settingsImporter, + this.backgroundWorkDiagnosticsService, }); @visibleForTesting @@ -73,12 +76,16 @@ class SettingsScreen extends StatefulWidget { @visibleForTesting final Future Function()? settingsImporter; + @visibleForTesting + final BackgroundWorkDiagnosticsService? backgroundWorkDiagnosticsService; @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State with FocusableTab, MountedSetStateMixin { + BackgroundWorkDiagnosticsService get _backgroundWorkDiagnostics => + widget.backgroundWorkDiagnosticsService ?? BackgroundWorkDiagnosticsService.instance; late final FocusMemoryTracker _focusTracker; // Focus tracking keys @@ -90,6 +97,7 @@ class _SettingsScreenState extends State with FocusableTab, Moun static const _kDownloadLocation = 'download_location'; static const _kDownloadOnWifiOnly = 'download_on_wifi_only'; static const _kAutoRemoveWatchedDownloads = 'auto_remove_watched_downloads'; + static const _kBackgroundDownloads = 'background_downloads'; static const _kVideoPlayerControls = 'video_player_controls'; static const _kVideoPlayerNavigation = 'video_player_navigation'; static const _kCrashReporting = 'crash_reporting'; @@ -376,10 +384,56 @@ class _SettingsScreenState extends State with FocusableTab, Moun title: t.settings.autoRemoveWatchedDownloads, subtitle: t.settings.autoRemoveWatchedDownloadsDescription, ), + if (_backgroundWorkDiagnostics.isSupported) _buildBackgroundDownloadsTile(), ], ); } + /// Standing answer to "why do my downloads stop when I leave the app" — also + /// what support can ask a user to read out without needing a log upload. + Widget _buildBackgroundDownloadsTile() { + final diagnostics = _backgroundWorkDiagnostics; + return ListenableBuilder( + listenable: diagnostics, + builder: (context, _) { + final status = diagnostics.status; + final scheme = Theme.of(context).colorScheme; + final (icon, color, summary) = switch (status) { + _ when !status.probed => (Symbols.help_rounded, null, t.downloads.backgroundWarning.statusUnknown), + _ when status.isBlocked => ( + Symbols.battery_alert_rounded, + scheme.error, + t.downloads.backgroundWarning.statusBlocked, + ), + _ when !status.isHealthy => ( + Symbols.info_rounded, + scheme.tertiary, + t.downloads.backgroundWarning.statusDegraded, + ), + _ => (Symbols.check_circle_rounded, null, t.downloads.backgroundWarning.statusOk), + }; + return FocusableListTile( + focusNode: _focusTracker.get(_kBackgroundDownloads), + leading: AppIcon(icon, fill: 1, color: color), + title: Text(t.downloads.backgroundWarning.statusTile, style: settingsOptionTitleStyle(context)), + subtitle: Text(summary), + trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), + onTap: () async { + await diagnostics.refresh(); + if (!context.mounted) return; + if (diagnostics.status.isHealthy) { + showAppSnackBar(context, t.downloads.backgroundWarning.statusOk); + return; + } + await showBackgroundDownloadWarningDialog(context, service: diagnostics); + }, + dense: false, + visualDensity: VisualDensity.standard, + ); + }, + ); + } + Widget _buildKeyboardShortcutsSection() { if (_keyboardService == null) return const SizedBox.shrink(); diff --git a/lib/services/background_work_diagnostics_service.dart b/lib/services/background_work_diagnostics_service.dart new file mode 100644 index 00000000..0c1c7424 --- /dev/null +++ b/lib/services/background_work_diagnostics_service.dart @@ -0,0 +1,517 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../mixins/disposable_change_notifier_mixin.dart'; +import 'download_manager_service.dart'; +import '../utils/app_logger.dart'; +import '../utils/platform_detector.dart'; + +/// Why background downloads are expected to fail on this device. +/// +/// Ordered by how actionable the remedy is; [BackgroundWorkStatus.reasons] +/// preserves the order the native classifier emits. +enum BackgroundWorkReason { + /// AOSP per-app "Restricted" background usage. Samsung's "Background usage + /// limits" (Sleeping apps / Deep sleeping apps) lands here too. + backgroundRestricted('background_restricted'), + + /// App standby bucket is RESTRICTED or NEVER — deferred indefinitely. + standbyRestricted('standby_restricted'), + + /// The downloader's notification channel is muted, which removes the + /// foreground-service anchor keeping downloads alive across screen-off. + downloadChannelBlocked('download_channel_blocked'), + + /// Notifications are off app-wide. + notificationsDisabled('notifications_disabled'), + + /// Data Saver blocks metered background traffic. + dataSaver('data_saver'), + + /// No OS-visible restriction, but downloads empirically stop making progress + /// whenever the app is backgrounded. Covers OEMs that block background work + /// while reporting a clean bucket. + oemUnknown('oem_unknown'); + + const BackgroundWorkReason(this.id); + + final String id; + + static BackgroundWorkReason? fromId(String id) { + for (final reason in values) { + if (reason.id == id) return reason; + } + return null; + } +} + +/// Severity of the detected restrictions. +enum BackgroundWorkVerdict { + /// Nothing detected, or the platform cannot tell us. + ok, + + /// Downloads will run, but not under every network condition. + degraded, + + /// Downloads are expected to stall the moment the app leaves the foreground. + blocked, +} + +/// Settings screen a remedy button routes to. Mirrors `BackgroundSettingsTarget`. +enum BackgroundSettingsTarget { + appDetails('app_details'), + appNotifications('app_notifications'), + notificationChannel('notification_channel'); + + const BackgroundSettingsTarget(this.id); + + final String id; +} + +/// Immutable result of one probe. +@immutable +class BackgroundWorkStatus { + const BackgroundWorkStatus({ + this.verdict = BackgroundWorkVerdict.ok, + this.reasons = const [], + this.standbyBucket, + this.ignoringBatteryOptimizations, + this.probed = false, + }); + + final BackgroundWorkVerdict verdict; + final List reasons; + final int? standbyBucket; + final bool? ignoringBatteryOptimizations; + + /// False before the first successful native probe (and on every non-Android + /// platform), so callers can distinguish "healthy" from "never asked". + final bool probed; + + bool get isBlocked => verdict == BackgroundWorkVerdict.blocked; + bool get isHealthy => verdict == BackgroundWorkVerdict.ok; + + /// The reason a single-line summary should lead with, if any. + BackgroundWorkReason? get primaryReason => reasons.isEmpty ? null : reasons.first; + + /// Which Settings screen best addresses [primaryReason]. + BackgroundSettingsTarget get remedyTarget => switch (primaryReason) { + BackgroundWorkReason.notificationsDisabled => BackgroundSettingsTarget.appNotifications, + BackgroundWorkReason.downloadChannelBlocked => BackgroundSettingsTarget.notificationChannel, + _ => BackgroundSettingsTarget.appDetails, + }; + + BackgroundWorkStatus withReason(BackgroundWorkReason reason) { + if (reasons.contains(reason)) return this; + return BackgroundWorkStatus( + verdict: BackgroundWorkVerdict.blocked, + reasons: [...reasons, reason], + standbyBucket: standbyBucket, + ignoringBatteryOptimizations: ignoringBatteryOptimizations, + probed: probed, + ); + } + + /// Compact form for the log upload header — the whole point of Phase 2 is + /// that a support thread can be answered from one uploaded log. + String describe() { + if (!probed) return 'unknown'; + final parts = [ + verdict.name, + if (reasons.isNotEmpty) reasons.map((r) => r.id).join('+'), + if (standbyBucket != null) 'bucket:$standbyBucket', + if (ignoringBatteryOptimizations != null) 'batteryWhitelist:$ignoringBatteryOptimizations', + ]; + return parts.join(' '); + } + + @override + bool operator ==(Object other) => + other is BackgroundWorkStatus && + other.verdict == verdict && + listEquals(other.reasons, reasons) && + other.standbyBucket == standbyBucket && + other.ignoringBatteryOptimizations == ignoringBatteryOptimizations && + other.probed == probed; + + @override + int get hashCode => + Object.hash(verdict, Object.hashAll(reasons), standbyBucket, ignoringBatteryOptimizations, probed); +} + +/// Aggregate process-wide download activity at a single instant. +typedef DownloadActivitySnapshot = ({ + int activeTasks, + int completedTasks, + int failedTasks, + int downloadedBytes, + int progressUnits, + bool? networkAvailable, + int networkStateGeneration, +}); + +typedef DownloadStallObservation = ({ + DownloadActivitySnapshot paused, + DownloadActivitySnapshot resumed, + Duration backgroundGap, +}); + +/// Detects OEMs that silently kill background work while reporting a clean +/// standby bucket (Xiaomi, Oppo, Vivo, Huawei), by watching whether downloads +/// actually advance while the app sits in the background. +/// +/// Pure and clock-injected so the whole state machine is unit-testable. +class DownloadStallDetector { + DownloadStallDetector({ + this.minimumBackgroundGap = const Duration(seconds: 60), + this.minimumProgressBytes = 64 * 1024, + this.observationsToConfirm = 2, + }); + + /// Shorter background trips prove nothing — the OS grants a grace period + /// before it starts deferring work. + final Duration minimumBackgroundGap; + + /// Below this, "progress" is indistinguishable from a buffer flush. + final int minimumProgressBytes; + + /// Two independent observations before accusing the OEM, so one flaky + /// network blip cannot produce a false warning. + final int observationsToConfirm; + + DownloadActivitySnapshot? _pausedSnapshot; + DateTime? _pausedAt; + int _consecutiveStalls = 0; + + int get consecutiveStalls => _consecutiveStalls; + + bool get isStalled => _consecutiveStalls >= observationsToConfirm; + + /// Arms the detector. Only meaningful while something is actually downloading. + void onPaused(DownloadActivitySnapshot snapshot, DateTime at) { + if (snapshot.activeTasks <= 0) { + _clearArmedSnapshot(); + return; + } + _pausedSnapshot = snapshot; + _pausedAt = at; + } + + /// Captures the resume state synchronously, before any awaited platform or + /// settings work can allow later progress to leak into this observation. + DownloadStallObservation? captureResumed(DownloadActivitySnapshot snapshot, DateTime at) { + final pausedSnapshot = _pausedSnapshot; + final pausedAt = _pausedAt; + _clearArmedSnapshot(); + if (pausedSnapshot == null || pausedAt == null) return null; + return (paused: pausedSnapshot, resumed: snapshot, backgroundGap: at.difference(pausedAt)); + } + + /// Scores a captured background trip. Returns true only when this trip has + /// just confirmed a repeated OEM stall. + bool evaluate(DownloadStallObservation? observation, {required bool networkAvailable}) { + if (observation == null) return false; + final pausedSnapshot = observation.paused; + final snapshot = observation.resumed; + + final completedInBackground = snapshot.completedTasks > pausedSnapshot.completedTasks; + final byteDelta = snapshot.downloadedBytes - pausedSnapshot.downloadedBytes; + final progressDelta = snapshot.progressUnits - pausedSnapshot.progressUnits; + + // Real transfer progress clears prior evidence even when the network or + // policy check below is inconclusive. A terminal event may omit byte data, + // so completion and percentage progress are independent evidence. + if (completedInBackground || byteDelta >= minimumProgressBytes || progressDelta > 0) { + _consecutiveStalls = 0; + return false; + } + + // Removed rows and other backwards counters are not evidence of a stall. + if (byteDelta < 0 || progressDelta < 0) return false; + if (observation.backgroundGap < minimumBackgroundGap) return false; + + // Require known-good endpoints, no network/server transition anywhere in + // the background window, and an eligible download policy at resume. + if (!networkAvailable || + pausedSnapshot.networkAvailable != true || + snapshot.networkAvailable != true || + snapshot.networkStateGeneration != pausedSnapshot.networkStateGeneration) { + return false; + } + + final activeTasksDropped = snapshot.activeTasks < pausedSnapshot.activeTasks; + final taskFailed = snapshot.failedTasks > pausedSnapshot.failedTasks; + if (activeTasksDropped && !taskFailed) { + // A paused/cancelled/removed task is not evidence of an OEM restriction. + return false; + } + + final wasConfirmed = isStalled; + _consecutiveStalls++; + return !wasConfirmed && isStalled; + } + + /// Convenience wrapper for direct callers and unit tests. + bool onResumed(DownloadActivitySnapshot snapshot, DateTime at, {required bool networkAvailable}) { + return evaluate(captureResumed(snapshot, at), networkAvailable: networkAvailable); + } + + void reset() { + _clearArmedSnapshot(); + _consecutiveStalls = 0; + } + + void _clearArmedSnapshot() { + _pausedSnapshot = null; + _pausedAt = null; + } +} + +/// Probes the OS for reasons background downloads will not run, and watches for +/// the symptom on OEMs that will not admit to it. +/// +/// Every failure path resolves to [BackgroundWorkVerdict.ok]: a warning we +/// cannot substantiate trains users to ignore the ones we can. +class BackgroundWorkDiagnosticsService extends ChangeNotifier + with WidgetsBindingObserver, DisposableChangeNotifierMixin { + BackgroundWorkDiagnosticsService._() + : _channelOverride = null, + _clockOverride = null, + _networkProbeOverride = null, + _supportedOverride = null; + + @visibleForTesting + BackgroundWorkDiagnosticsService.forTesting({ + MethodChannel? channel, + DownloadStallDetector? stallDetector, + DateTime Function()? clock, + Future Function()? networkProbe, + bool supported = true, + }) : _channelOverride = channel, + _stallDetector = stallDetector ?? DownloadStallDetector(), + _clockOverride = clock, + _networkProbeOverride = networkProbe, + _supportedOverride = supported; + + static final BackgroundWorkDiagnosticsService instance = BackgroundWorkDiagnosticsService._(); + + static const MethodChannel _deviceChannel = MethodChannel('com.plezy/device'); + + final MethodChannel? _channelOverride; + final DateTime Function()? _clockOverride; + final Future Function()? _networkProbeOverride; + final bool? _supportedOverride; + + DownloadStallDetector _stallDetector = DownloadStallDetector(); + DownloadActivitySnapshot Function()? _activitySource; + BackgroundWorkStatus _status = const BackgroundWorkStatus(); + BackgroundWorkStatus _lastProbedStatus = const BackgroundWorkStatus(); + Future? _inFlightRefresh; + bool _observing = false; + bool _channelMissing = false; + + MethodChannel get _channel => _channelOverride ?? _deviceChannel; + + DateTime _now() => (_clockOverride ?? DateTime.now)(); + + /// Android phone/tablet only. TV boxes are mains-powered, and surfacing + /// mobile battery/data restrictions there is noise. + bool get isSupported => _supportedOverride ?? (Platform.isAndroid && !PlatformDetector.isTV()); + + BackgroundWorkStatus get status => _status; + + /// One-line summary for the log upload header. + String describeSync() => isSupported ? _status.describe() : 'n/a'; + + /// Lets the download layer publish aggregate progress without this service + /// reaching into providers. + void bindActivitySource(DownloadActivitySnapshot Function() source) { + _activitySource = source; + _startObserving(); + // Populate the banner, Settings tile, and log header on a cold start. + // Concurrent profile binds coalesce through [refresh]. + unawaited(refresh()); + } + + void unbindActivitySource(DownloadActivitySnapshot Function() source) { + // Compared by `==` rather than `identical` so an instance-method tear-off + // from the same provider matches on every SDK. + if (_activitySource != source) return; + _activitySource = null; + } + + void _startObserving() { + if (_observing || !isSupported) return; + final binding = WidgetsBinding.instance; + _observing = true; + binding.addObserver(this); + } + + /// Re-probes the OS. Coalesces concurrent calls — the bucket is cheap to read + /// but there is no reason to read it three times on a single resume. + Future refresh() async { + if (!isSupported || _channelMissing || isDisposed) return _status; + final inFlight = _inFlightRefresh; + if (inFlight != null) { + await inFlight; + return _status; + } + final refresh = _refresh(); + _inFlightRefresh = refresh; + try { + await refresh; + } finally { + if (identical(_inFlightRefresh, refresh)) _inFlightRefresh = null; + } + return _status; + } + + Future _refresh() async { + try { + final raw = await _channel.invokeMapMethod('getBackgroundWorkSignals'); + if (isDisposed) return; + if (raw == null) return; + _applyStatus(_parse(raw)); + } on MissingPluginException { + // Stale native build — never warn on a signal we could not read. + _channelMissing = true; + } catch (e) { + appLogger.d('Background work probe failed: $e'); + } + } + + BackgroundWorkStatus _parse(Map raw) { + final verdict = switch (raw['verdict']) { + 'blocked' => BackgroundWorkVerdict.blocked, + 'degraded' => BackgroundWorkVerdict.degraded, + _ => BackgroundWorkVerdict.ok, + }; + final reasons = []; + final rawReasons = raw['reasons']; + if (rawReasons is List) { + for (final entry in rawReasons) { + if (entry is! String) continue; + final reason = BackgroundWorkReason.fromId(entry); + // oem_unknown is ours, never the platform's — refuse it over the wire. + if (reason != null && reason != BackgroundWorkReason.oemUnknown) reasons.add(reason); + } + } + final bucket = raw['standbyBucket']; + final whitelisted = raw['ignoringBatteryOptimizations']; + return BackgroundWorkStatus( + // Trust our own reason parse over the reported verdict: an unknown reason + // id from a newer native build must not silently downgrade to blocked. + verdict: reasons.isEmpty ? BackgroundWorkVerdict.ok : verdict, + reasons: reasons, + standbyBucket: bucket is int ? bucket : null, + ignoringBatteryOptimizations: whitelisted is bool ? whitelisted : null, + probed: true, + ); + } + + void _applyStatus(BackgroundWorkStatus probed) { + if (isDisposed) return; + _lastProbedStatus = probed; + _applyCurrentStatus(); + } + + void _applyCurrentStatus() { + // A confirmed empirical stall outlives any single probe: the OS keeps + // reporting healthy on exactly the devices this catches. + final next = _stallDetector.isStalled + ? _lastProbedStatus.withReason(BackgroundWorkReason.oemUnknown) + : _lastProbedStatus; + if (next == _status) return; + final previous = _status; + _status = next; + if (previous.verdict != next.verdict || !listEquals(previous.reasons, next.reasons)) { + appLogger.i('Background work status: ${next.describe()}'); + } + safeNotifyListeners(); + } + + /// Opens the Settings screen that addresses [target]. Returns false when the + /// device has no such screen, so the caller can fall back to written steps. + Future openSettings(BackgroundSettingsTarget target) async { + if (!isSupported) return false; + try { + return await _channel.invokeMethod('openBackgroundSettings', target.id) ?? false; + } catch (e) { + appLogger.w('Opening background settings failed', error: e); + return false; + } + } + + /// Clears an empirical stall verdict after the user reports having fixed it. + Future clearStallEvidence() async { + if (isDisposed) return; + _stallDetector.reset(); + _applyCurrentStatus(); + await refresh(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final source = _activitySource; + switch (state) { + case AppLifecycleState.paused: + case AppLifecycleState.detached: + if (source != null) _stallDetector.onPaused(source(), _now()); + case AppLifecycleState.resumed: + final snapshot = source?.call(); + final observation = snapshot == null ? null : _stallDetector.captureResumed(snapshot, _now()); + unawaited(_onResumed(observation)); + case AppLifecycleState.inactive: + case AppLifecycleState.hidden: + break; + } + } + + Future _onResumed(DownloadStallObservation? observation) async { + try { + if (observation != null) { + final networkAvailable = await _networkAllowsStallScoring(); + if (isDisposed) return; + final wasStalled = _stallDetector.isStalled; + final confirmed = _stallDetector.evaluate(observation, networkAvailable: networkAvailable); + if (wasStalled && !_stallDetector.isStalled) _applyCurrentStatus(); + if (confirmed) { + appLogger.w( + 'Background downloads made no progress across ${_stallDetector.observationsToConfirm} ' + 'backgrounded periods — treating as an OEM background restriction', + ); + } + } + await refresh(); + } catch (e) { + appLogger.d('Background work resume check failed: $e'); + } + } + + Future _networkAllowsStallScoring() async { + final probe = _networkProbeOverride; + if (probe != null) return probe(); + try { + final connectivity = await Connectivity().checkConnectivity(); + if (connectivity.isEmpty || connectivity.contains(ConnectivityResult.none)) return false; + return !await DownloadManagerService.shouldBlockDownloadOnCellularWith(connectivity); + } catch (_) { + // Unknown connectivity or policy is inconclusive, not proof of a stall. + return false; + } + } + + @override + void dispose() { + if (_observing) { + WidgetsBinding.instance.removeObserver(this); + _observing = false; + } + super.dispose(); + } +} diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index e9d04e40..6fccc8eb 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -107,6 +107,11 @@ class DownloadManagerService { final _progressController = StreamController.broadcast(); Stream get progressStream => _progressController.stream; + @visibleForTesting + void debugEmitProgress(DownloadProgress progress) { + if (!_disposed) _progressController.add(progress); + } + final _deletionProgressController = StreamController.broadcast(); Stream get deletionProgressStream => _deletionProgressController.stream; diff --git a/lib/services/music/music_playback_service_impl.dart b/lib/services/music/music_playback_service_impl.dart index 86215b56..8062bd9e 100644 --- a/lib/services/music/music_playback_service_impl.dart +++ b/lib/services/music/music_playback_service_impl.dart @@ -289,7 +289,7 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO // Android 13+: the background playback notification needs // POST_NOTIFICATIONS. Fire-and-forget — playback and the foreground // service run regardless; a denial only hides the notification. - unawaited(ensureNotificationPermission()); + unawaited(NotificationPermission.ensure()); final generation = ++_generation; _invalidateArmRequests(); _finalizeCurrentTrack(); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index ed651dd2..fbbdf590 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -383,6 +383,11 @@ class SettingsService extends BaseSharedPreferencesService { static const downloadOnWifiOnly = BoolPref('download_on_wifi_only'); static const autoRemoveWatchedDownloads = BoolPref('auto_remove_watched_downloads'); + /// Set once the user has seen the pre-flight "background downloads are + /// blocked" dialog. The persistent Downloads-screen banner covers repeat + /// offenders, so the interrupting dialog is shown exactly once. + static const backgroundDownloadWarningAcknowledged = BoolPref('background_download_warning_ack'); + /// Remembered state of the "Include Specials" toggle on the show download /// dialog. Defaults to true (include) so existing behavior is unchanged; /// turning it off persists so the next download keeps the choice. @@ -858,6 +863,7 @@ class SettingsService extends BaseSharedPreferencesService { rememberTrackSelections, customDownloadPathType, downloadOnWifiOnly, + backgroundDownloadWarningAcknowledged, downloadIncludeSpecials, autoCheckUpdatesOnStartup, showPerformanceOverlay, diff --git a/lib/utils/download_utils.dart b/lib/utils/download_utils.dart index 0e0c3fd3..1647da89 100644 --- a/lib/utils/download_utils.dart +++ b/lib/utils/download_utils.dart @@ -10,6 +10,7 @@ import '../database/app_database.dart'; import '../providers/download_provider.dart'; import '../services/settings_service.dart'; import '../services/sync_rule_executor.dart'; +import '../widgets/background_download_warning_banner.dart'; import 'content_utils.dart'; import 'dialogs.dart'; import 'download_version_utils.dart'; @@ -77,6 +78,8 @@ Future showDownloadOptionsAndQueue( required DownloadProvider downloadProvider, Future Function()? onDelete, }) async { + if (!await confirmBackgroundDownloadRestrictions(context) || !context.mounted) return null; + final kind = metadata.kind; var filter = DownloadFilter.all; @@ -220,6 +223,8 @@ Future showListDownloadOptionsAndQueue( }) async { assert(targetType == ContentTypes.collection || targetType == ContentTypes.playlist); + if (!await confirmBackgroundDownloadRestrictions(context) || !context.mounted) return null; + final selectedFilter = await showOptionPickerDialog( context, title: t.downloads.downloadNow, diff --git a/lib/utils/notification_permission.dart b/lib/utils/notification_permission.dart index a335e194..441348b5 100644 --- a/lib/utils/notification_permission.dart +++ b/lib/utils/notification_permission.dart @@ -1,31 +1,58 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/foundation.dart'; import 'app_logger.dart'; -bool _requested = false; +/// Android 13+ `POST_NOTIFICATIONS` runtime permission, routed through +/// background_downloader's permissions API (which the app already ships for +/// download notifications). +abstract final class NotificationPermission { + static Future? _requestFuture; -/// Best-effort request for the Android 13+ POST_NOTIFICATIONS runtime -/// permission (routed through background_downloader's permissions API, which -/// the app already ships for download notifications). -/// -/// Needed so the background music playback notification is visible; playback -/// and its foreground service run regardless of the outcome, so a denial only -/// costs notification visibility. Asked at most once per app run — Android -/// remembers a real denial, so re-prompting is a no-op anyway. -/// -/// No-op off Android: iOS/macOS media controls don't use notifications. -Future ensureNotificationPermission() async { - if (_requested || !Platform.isAndroid) return; - _requested = true; - try { - final permissions = FileDownloader().permissions; - final status = await permissions.status(PermissionType.notifications); - if (status == PermissionStatus.granted) return; - final result = await permissions.request(PermissionType.notifications); - appLogger.d('Notification permission request result: $result'); - } catch (e) { - appLogger.w('Notification permission request failed', error: e); + /// Replaces the platform request in tests. Reset to null in `tearDown`. + @visibleForTesting + static Future Function()? debugRequestOverride; + + /// Clears the once-per-run future so each test starts from a clean slate. + @visibleForTesting + static void debugReset() => _requestFuture = null; + + /// Best-effort permission request. + /// + /// Both consumers need it for the same reason: the notification is what + /// anchors the foreground service. For music that costs only visibility, but + /// for downloads a denial lets the OS kill the transfer as soon as the screen + /// goes off — which is exactly the failure users report as "downloads only + /// work with the screen on". + /// + /// Concurrent callers await the same request. The completed future stays + /// cached for the app run because Android remembers a real denial. + /// + /// No-op off Android: iOS/macOS media controls don't use notifications. + static Future ensure() => _requestFuture ??= _request(); + + static Future _request() async { + try { + // Checked before the platform gate so tests exercise the once-per-run + // contract on a host that is not Android. + final override = debugRequestOverride; + if (override != null) { + await override(); + return; + } + if (!Platform.isAndroid) return; + + final permissions = FileDownloader().permissions; + final status = await permissions.status(PermissionType.notifications); + if (status == PermissionStatus.granted) return; + final result = await permissions.request(PermissionType.notifications); + appLogger.d('Notification permission request result: $result'); + } catch (e, stackTrace) { + // Permission prompts are best-effort prerequisites. A platform failure + // must not poison the shared future or fail playback/download queueing. + appLogger.w('Notification permission request failed', error: e, stackTrace: stackTrace); + } } } diff --git a/lib/widgets/background_download_warning_banner.dart b/lib/widgets/background_download_warning_banner.dart new file mode 100644 index 00000000..2a99e3ca --- /dev/null +++ b/lib/widgets/background_download_warning_banner.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../focus/focusable_button.dart'; +import '../i18n/strings.g.dart'; +import '../services/background_work_diagnostics_service.dart'; +import '../services/settings_service.dart'; +import '../utils/notification_permission.dart'; +import '../utils/dialogs.dart'; +import '../utils/snackbar_helper.dart'; +import 'app_icon.dart'; +import 'dialog_action_button.dart'; + +/// Community-maintained per-vendor instructions for OEM background-work +/// settings. The site root asks the user to pick their manufacturer, which is +/// more reliable than us guessing a slug from `Build.MANUFACTURER`. +const _dontKillMyAppUrl = 'https://dontkillmyapp.com'; + +/// Banner shown at the top of the Downloads screen when the OS is expected to +/// stop downloads the moment the app is backgrounded. +/// +/// Deliberately conditional on there being something to download: an idle +/// Downloads screen should not nag, and a warning users learn to dismiss is +/// worse than no warning. Collapses to nothing when the verdict is healthy or +/// the platform cannot answer. +class BackgroundDownloadWarningBanner extends StatelessWidget { + const BackgroundDownloadWarningBanner({super.key, required this.hasPendingDownloads, this.service}); + + /// Whether anything is queued or in flight. The restriction only matters + /// while there is a transfer for it to kill. + final bool hasPendingDownloads; + + final BackgroundWorkDiagnosticsService? service; + + BackgroundWorkDiagnosticsService get _service => service ?? BackgroundWorkDiagnosticsService.instance; + + @override + Widget build(BuildContext context) { + if (!hasPendingDownloads || !_service.isSupported) return const SizedBox.shrink(); + + return ListenableBuilder( + listenable: _service, + builder: (context, _) { + final status = _service.status; + if (status.isHealthy) return const SizedBox.shrink(); + + final theme = Theme.of(context); + final scheme = theme.colorScheme; + final blocked = status.isBlocked; + final background = blocked ? scheme.errorContainer : scheme.tertiaryContainer; + final foreground = blocked ? scheme.onErrorContainer : scheme.onTertiaryContainer; + + return Material( + color: background, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), + child: Row( + children: [ + AppIcon(blocked ? Symbols.battery_alert_rounded : Symbols.info_rounded, fill: 1, color: foreground), + const SizedBox(width: 12), + Expanded( + child: Text( + blocked + ? t.downloads.backgroundWarning.bannerBlocked + : t.downloads.backgroundWarning.bannerDegraded, + style: theme.textTheme.bodyMedium?.copyWith(color: foreground, fontWeight: .w500), + ), + ), + const SizedBox(width: 8), + FocusableButton( + onPressed: () => showBackgroundDownloadWarningDialog(context, service: _service), + child: FilledButton.tonal( + style: FilledButton.styleFrom(backgroundColor: foreground, foregroundColor: background), + onPressed: () => showBackgroundDownloadWarningDialog(context, service: _service), + child: Text(t.downloads.backgroundWarning.bannerAction), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +/// Pre-flight gate before a user-initiated download. +/// +/// Returns whether to proceed with queueing. Interrupts at most once per +/// install — the persistent Downloads-screen banner covers repeat offenders, +/// and a dialog on every download would only teach users to dismiss it. +/// Only [BackgroundWorkVerdict.blocked] is worth interrupting for; a Data Saver +/// warning can wait for the banner. +Future confirmBackgroundDownloadRestrictions( + BuildContext context, { + BackgroundWorkDiagnosticsService? service, + Future Function()? notificationPermissionRequester, +}) async { + final diagnostics = service ?? BackgroundWorkDiagnosticsService.instance; + if (!diagnostics.isSupported) return true; + + await (notificationPermissionRequester ?? NotificationPermission.ensure)(); + if (!context.mounted) return false; + + final settings = SettingsService.instanceOrNull; + if (settings?.read(SettingsService.backgroundDownloadWarningAcknowledged) ?? false) return true; + + await diagnostics.refresh(); + if (!diagnostics.status.isBlocked) return true; + if (!context.mounted) return false; + + // Recorded before the dialog resolves: whichever button the user picks, they + // have now been told, and the banner takes over from here. + await settings?.write(SettingsService.backgroundDownloadWarningAcknowledged, true); + if (!context.mounted) return false; + return showBackgroundDownloadWarningDialog(context, preFlight: true, service: diagnostics); +} + +/// Explains the detected restrictions and routes to the Settings screen that +/// fixes them. +/// +/// When [preFlight] is set the dialog is interrupting a download the user just +/// asked for, so it offers "Download anyway" and returns whether to proceed. +/// Otherwise it is informational and always resolves to true. +Future showBackgroundDownloadWarningDialog( + BuildContext context, { + bool preFlight = false, + BackgroundWorkDiagnosticsService? service, + Future Function(Uri uri)? externalUrlLauncher, +}) async { + final diagnostics = service ?? BackgroundWorkDiagnosticsService.instance; + final status = diagnostics.status; + if (status.isHealthy) return true; + + final proceed = await showScopedDialog( + context: context, + builder: (dialogContext) { + final theme = Theme.of(dialogContext); + return AlertDialog( + title: Text( + preFlight + ? t.downloads.backgroundWarning.dialogTitle + : status.isBlocked + ? t.downloads.backgroundWarning.sheetTitle + : t.downloads.backgroundWarning.sheetTitleDegraded, + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + status.isBlocked + ? t.downloads.backgroundWarning.sheetIntro + : t.downloads.backgroundWarning.sheetIntroDegraded, + ), + for (final reason in status.reasons) ...[ + const SizedBox(height: 16), + Row( + crossAxisAlignment: .start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: const AppIcon(Symbols.chevron_right_rounded, fill: 1, size: 18), + ), + const SizedBox(width: 8), + Expanded(child: Text(_describeReason(reason))), + ], + ), + ], + const SizedBox(height: 20), + Text(t.downloads.backgroundWarning.stillNotWorking, style: theme.textTheme.titleSmall), + const SizedBox(height: 4), + Text(t.downloads.backgroundWarning.stillNotWorkingDescription, style: theme.textTheme.bodySmall), + const SizedBox(height: 8), + Align( + alignment: .centerLeft, + child: FocusableButton( + onPressed: () => _openDontKillMyApp(dialogContext, launcher: externalUrlLauncher), + child: TextButton.icon( + onPressed: () => _openDontKillMyApp(dialogContext, launcher: externalUrlLauncher), + icon: const AppIcon(Symbols.open_in_new_rounded, fill: 1, size: 18), + label: const Text('dontkillmyapp.com'), + ), + ), + ), + ], + ), + ), + actions: [ + if (preFlight) + DialogActionButton( + onPressed: () => Navigator.pop(dialogContext, true), + label: t.downloads.backgroundWarning.dialogDownloadAnyway, + ) + else + DialogActionButton(onPressed: () => Navigator.pop(dialogContext, true), label: t.common.close), + DialogActionButton( + autofocus: true, + isPrimary: true, + onPressed: () async { + final navigator = Navigator.of(dialogContext); + final messengerContext = dialogContext; + if (status.reasons.contains(BackgroundWorkReason.oemUnknown)) { + await diagnostics.clearStallEvidence(); + } + final opened = await diagnostics.openSettings(status.remedyTarget); + if (!opened && messengerContext.mounted) { + showErrorSnackBar(messengerContext, t.downloads.backgroundWarning.settingsUnavailable); + } + if (navigator.mounted) navigator.pop(preFlight ? false : true); + }, + label: preFlight + ? t.downloads.backgroundWarning.dialogFixFirst + : t.downloads.backgroundWarning.openSettings, + ), + ], + ); + }, + ); + + return proceed ?? !preFlight; +} + +String _describeReason(BackgroundWorkReason reason) => switch (reason) { + BackgroundWorkReason.backgroundRestricted => t.downloads.backgroundWarning.reasonBackgroundRestricted, + BackgroundWorkReason.standbyRestricted => t.downloads.backgroundWarning.reasonStandbyRestricted, + BackgroundWorkReason.downloadChannelBlocked => t.downloads.backgroundWarning.reasonDownloadChannelBlocked, + BackgroundWorkReason.notificationsDisabled => t.downloads.backgroundWarning.reasonNotificationsDisabled, + BackgroundWorkReason.dataSaver => t.downloads.backgroundWarning.reasonDataSaver, + BackgroundWorkReason.oemUnknown => t.downloads.backgroundWarning.reasonOemUnknown, +}; + +Future _openDontKillMyApp(BuildContext context, {Future Function(Uri uri)? launcher}) async { + var opened = false; + try { + final uri = Uri.parse(_dontKillMyAppUrl); + opened = await (launcher?.call(uri) ?? launchUrl(uri, mode: LaunchMode.externalApplication)); + } catch (_) { + // Reported below using the same user-facing fallback as a false result. + } + if (!opened && context.mounted) { + showErrorSnackBar(context, t.downloads.backgroundWarning.linkUnavailable); + } +} diff --git a/test/i18n/locale_fallback_source_test.dart b/test/i18n/locale_fallback_source_test.dart index 99950460..5c966106 100644 --- a/test/i18n/locale_fallback_source_test.dart +++ b/test/i18n/locale_fallback_source_test.dart @@ -33,10 +33,36 @@ void main() { } } }); + test('background warning requires complete translations in every locale', () { + final sourceDirectory = Directory('lib/i18n'); + final english = _decodeLocale(File('${sourceDirectory.path}/en.i18n.json')); + final englishWarning = _backgroundWarning(english); + final localeFiles = sourceDirectory.listSync().whereType().where( + (file) => file.path.endsWith('.i18n.json') && !file.path.endsWith('en.i18n.json'), + ); + + expect(englishWarning.values, everyElement(isNotEmpty), reason: 'English warning copy must be usable'); + for (final localeFile in localeFiles) { + final localizedWarning = _backgroundWarning(_decodeLocale(localeFile)); + expect( + localizedWarning.keys, + unorderedEquals(englishWarning.keys), + reason: '${localeFile.path} must contain every background warning key', + ); + expect( + localizedWarning.values, + everyElement(isNotEmpty), + reason: '${localeFile.path} must translate every background warning', + ); + } + }); } Map _decodeLocale(File file) => jsonDecode(file.readAsStringSync()) as Map; +Map _backgroundWarning(Map locale) => + (locale['downloads'] as Map)['backgroundWarning'] as Map; + String? _valueAt(Map locale, List path) { Object? value = locale; for (final segment in path) { diff --git a/test/providers/download_provider_test.dart b/test/providers/download_provider_test.dart index 2a1a4629..ba376e4a 100644 --- a/test/providers/download_provider_test.dart +++ b/test/providers/download_provider_test.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart' show ApplyInterceptor, QueryExecutor, QueryInt import 'package:plezy/media/ids.dart'; import 'package:drift/native.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/database/app_database.dart'; import 'package:plezy/database/download_operations.dart'; @@ -18,7 +19,9 @@ import 'package:plezy/services/api_cache.dart'; import 'package:plezy/services/download_storage_service.dart'; import 'package:plezy/services/jellyfin_api_cache.dart'; import 'package:plezy/services/plex_api_cache.dart'; +import 'package:plezy/services/offline_mode_source.dart'; import 'package:plezy/utils/deletion_notifier.dart'; +import 'package:plezy/utils/notification_permission.dart'; import 'package:plezy/utils/watch_state_notifier.dart'; import 'package:plezy/utils/active_client_scope.dart'; import '../test_helpers/media_items.dart'; @@ -191,6 +194,21 @@ Future _putPinnedPlexMetadata( await PlexApiCache.instance.pinForOffline(scope.cacheServerId, id); } +class _FakeOfflineModeSource extends ChangeNotifier implements OfflineModeSource { + _FakeOfflineModeSource(this._isOffline); + + bool _isOffline; + + @override + bool get isOffline => _isOffline; + + void setOffline(bool value) { + if (_isOffline == value) return; + _isOffline = value; + notifyListeners(); + } +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -2713,6 +2731,178 @@ void main() { }); }); + group('DownloadProvider — background download prerequisites', () { + final movie = testMediaItem( + id: '1', + backend: MediaBackend.plex, + kind: MediaKind.movie, + title: 'Queued Movie', + serverId: ServerId('srv'), + ); + + setUp(NotificationPermission.debugReset); + + tearDown(() { + NotificationPermission.debugRequestOverride = null; + NotificationPermission.debugReset(); + }); + + test('queueDownload asks for the notification permission that anchors the transfer', () async { + // Regression guard: music playback used to be the only caller, so a user + // whose first action was a download was never asked, and Android killed + // the transfer the moment the screen went off. + var requests = 0; + NotificationPermission.debugRequestOverride = () async => requests++; + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + // Already downloaded, so the queue short-circuits before touching the + // manager — the permission request is the only thing under test. + p.debugSeedState( + downloads: {'srv:1': const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.completed)}, + metadata: {'srv:1': movie}, + ownedDownloadKeys: const {}, + ); + + await p.queueDownload(movie, _ThrowingClient()); + + expect(requests, 1); + p.dispose(); + }); + + test('the permission is requested once, not on every queued item', () async { + var requests = 0; + NotificationPermission.debugRequestOverride = () async => requests++; + final second = movie.copyWith(id: '2'); + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + p.debugSeedState( + downloads: { + 'srv:1': const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.completed), + 'srv:2': const DownloadProgress(globalKey: 'srv:2', status: DownloadStatus.completed), + }, + metadata: {'srv:1': movie, 'srv:2': second}, + ownedDownloadKeys: const {}, + ); + + await p.queueDownload(movie, _ThrowingClient()); + await p.queueDownload(second, _ThrowingClient()); + + expect(requests, 1); + p.dispose(); + }); + + test('holds the duplicate queue claim while permission is pending', () async { + final requestStarted = Completer(); + final releaseRequest = Completer(); + var requests = 0; + NotificationPermission.debugRequestOverride = () async { + requests++; + requestStarted.complete(); + await releaseRequest.future; + }; + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + p.debugSeedState( + downloads: {'srv:1': const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.completed)}, + metadata: {'srv:1': movie}, + ownedDownloadKeys: const {}, + ); + + final firstQueue = p.queueDownload(movie, _ThrowingClient()); + await requestStarted.future; + + expect(p.isQueueing(movie.globalKey), isTrue); + expect(await p.queueDownload(movie, _ThrowingClient()), 0); + + releaseRequest.complete(); + await firstQueue; + + expect(requests, 1); + p.dispose(); + }); + + test('the activity snapshot counts in-flight work and every downloaded byte', () async { + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + p.debugSeedState( + downloads: { + 'srv:1': const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.downloading, downloadedBytes: 100), + 'srv:2': const DownloadProgress(globalKey: 'srv:2', status: DownloadStatus.queued, downloadedBytes: 0), + 'srv:3': const DownloadProgress(globalKey: 'srv:3', status: DownloadStatus.completed, downloadedBytes: 900), + 'srv:4': const DownloadProgress(globalKey: 'srv:4', status: DownloadStatus.failed, downloadedBytes: 5), + }, + metadata: const {}, + ownedDownloadKeys: const {}, + ); + + final snapshot = p.downloadActivitySnapshot(); + + expect(snapshot.activeTasks, 1, reason: 'only actively downloading tasks arm the stall detector'); + expect(snapshot.completedTasks, 1); + expect(snapshot.failedTasks, 1); + expect(snapshot.downloadedBytes, 1005, reason: 'every download, regardless of profile ownership'); + p.dispose(); + }); + + test('terminal status-only events preserve bytes but a retry resets progress', () async { + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + p.debugSeedState(ownedDownloadKeys: const {'srv:1'}); + + downloadManager.debugEmitProgress( + const DownloadProgress( + globalKey: 'srv:1', + status: DownloadStatus.downloading, + progress: 47, + downloadedBytes: 500, + totalBytes: 1000, + ), + ); + await Future.delayed(Duration.zero); + downloadManager.debugEmitProgress( + const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.completed, progress: 100), + ); + await Future.delayed(Duration.zero); + + expect(p.downloads['srv:1']?.progress, 100); + expect(p.downloads['srv:1']?.downloadedBytes, 500); + expect(p.downloads['srv:1']?.totalBytes, 1000); + + downloadManager.debugEmitProgress(const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.queued)); + await Future.delayed(Duration.zero); + + expect(p.downloads['srv:1']?.progress, 0); + expect(p.downloads['srv:1']?.downloadedBytes, 0); + expect(p.downloads['srv:1']?.totalBytes, 0); + p.dispose(); + }); + + test('activity snapshots record offline-source transitions', () async { + final source = _FakeOfflineModeSource(false); + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + p.setOfflineSource(source); + final online = p.downloadActivitySnapshot(); + + source.setOffline(true); + final offline = p.downloadActivitySnapshot(); + + expect(online.networkAvailable, isTrue); + expect(offline.networkAvailable, isFalse); + expect(offline.networkStateGeneration, greaterThan(online.networkStateGeneration)); + p.dispose(); + source.dispose(); + }); + + test('the activity snapshot tear-off is stable enough to unbind', () async { + final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await p.ensureInitialized(); + + expect(p.downloadActivitySnapshot, equals(p.downloadActivitySnapshot)); + p.dispose(); + }); + }); + group('DownloadProvider — dispose hygiene', () { test('isDisposed flips from false to true on dispose', () async { final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); diff --git a/test/screens/settings/settings_screen_test.dart b/test/screens/settings/settings_screen_test.dart index 4ab7269c..e1da8045 100644 --- a/test/screens/settings/settings_screen_test.dart +++ b/test/screens/settings/settings_screen_test.dart @@ -21,6 +21,7 @@ import 'package:plezy/providers/theme_provider.dart'; import 'package:plezy/providers/trackers_provider.dart'; import 'package:plezy/providers/trakt_account_provider.dart'; import 'package:plezy/screens/settings/settings_screen.dart'; +import 'package:plezy/services/background_work_diagnostics_service.dart'; import 'package:plezy/services/donation_service.dart'; import 'package:plezy/services/download_storage_service.dart'; import 'package:plezy/services/download_manager_service.dart'; @@ -230,6 +231,59 @@ void main() { expect(materialUpdateTile.focusNode, isNotNull); }); + testWidgets('background downloads tile renders and opens the injected blocked status', (tester) async { + const channel = MethodChannel('com.plezy/device.settings-background-test'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { + return switch (call.method) { + 'getBackgroundWorkSignals' => { + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }, + 'openBackgroundSettings' => true, + _ => null, + }; + }); + final diagnostics = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + await diagnostics.refresh(); + final harness = await _pumpSettingsScreen(tester, backgroundWorkDiagnosticsService: diagnostics); + addTearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + await harness.dispose(tester); + diagnostics.dispose(); + }); + + expect(find.text(t.downloads.backgroundWarning.statusTile), findsOneWidget); + expect(find.text(t.downloads.backgroundWarning.statusBlocked), findsOneWidget); + + await tester.tap(find.text(t.downloads.backgroundWarning.statusTile)); + await _pumpUi(tester); + + expect(find.text(t.downloads.backgroundWarning.sheetTitle), findsOneWidget); + }); + + testWidgets('unprobed background diagnostics stay unknown instead of claiming success', (tester) async { + final diagnostics = BackgroundWorkDiagnosticsService.forTesting(); + final harness = await _pumpSettingsScreen(tester, backgroundWorkDiagnosticsService: diagnostics); + addTearDown(() async { + await harness.dispose(tester); + diagnostics.dispose(); + }); + + expect(find.text(t.downloads.backgroundWarning.statusUnknown), findsOneWidget); + expect(find.text(t.downloads.backgroundWarning.statusOk), findsNothing); + }); + + testWidgets('background downloads tile is absent when diagnostics are unsupported', (tester) async { + final diagnostics = BackgroundWorkDiagnosticsService.forTesting(supported: false); + final harness = await _pumpSettingsScreen(tester, backgroundWorkDiagnosticsService: diagnostics); + addTearDown(() async { + await harness.dispose(tester); + diagnostics.dispose(); + }); + + expect(find.text(t.downloads.backgroundWarning.statusTile), findsNothing); + }); + testWidgets('relay dialog rejects invalid bases without persisting them', (tester) async { final harness = await _pumpSettingsScreen(tester); addTearDown(() => harness.dispose(tester)); @@ -534,6 +588,7 @@ Future<_SettingsHarness> _pumpSettingsScreen( Future Function(Directory directory)? writableChecker, Future Function()? settingsExporter, Future Function()? settingsImporter, + BackgroundWorkDiagnosticsService? backgroundWorkDiagnosticsService, }) async { tester.view.physicalSize = const Size(1800, 3200); tester.view.devicePixelRatio = 1; @@ -621,6 +676,7 @@ Future<_SettingsHarness> _pumpSettingsScreen( downloadDirectoryWritableChecker: writableChecker ?? (_) async => true, settingsExporter: settingsExporter, settingsImporter: settingsImporter, + backgroundWorkDiagnosticsService: backgroundWorkDiagnosticsService, ), ), ), diff --git a/test/services/background_work_diagnostics_service_test.dart b/test/services/background_work_diagnostics_service_test.dart new file mode 100644 index 00000000..06f8bc3a --- /dev/null +++ b/test/services/background_work_diagnostics_service_test.dart @@ -0,0 +1,505 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/services/background_work_diagnostics_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DownloadStallDetector', () { + late DownloadStallDetector detector; + final start = DateTime(2026, 7, 25, 12); + + setUp(() { + detector = DownloadStallDetector( + minimumBackgroundGap: const Duration(seconds: 60), + minimumProgressBytes: 64 * 1024, + observationsToConfirm: 2, + ); + }); + + test('needs two consecutive stalls before accusing the OEM', () { + detector.onPaused(_activity(), start); + expect(detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true), isFalse); + expect(detector.isStalled, isFalse); + + detector.onPaused(_activity(), start.add(const Duration(minutes: 6))); + expect(detector.onResumed(_activity(), start.add(const Duration(minutes: 11)), networkAvailable: true), isTrue); + expect(detector.isStalled, isTrue); + }); + + test('confirms only once, so the warning is not re-raised every resume', () { + for (var i = 0; i < 2; i++) { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + } + expect(detector.isStalled, isTrue); + + detector.onPaused(_activity(), start); + expect( + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true), + isFalse, + reason: 'already confirmed', + ); + }); + + test('real background progress clears the counter', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + expect(detector.consecutiveStalls, 1); + + detector.onPaused(_activity(), start.add(const Duration(minutes: 6))); + detector.onResumed( + _activity(downloadedBytes: 1000 + 5 * 1024 * 1024), + start.add(const Duration(minutes: 11)), + networkAvailable: true, + ); + expect(detector.consecutiveStalls, 0); + }); + + test('a completed download below the byte floor clears the counter', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + expect(detector.consecutiveStalls, 1); + + detector.onPaused(_activity(), start.add(const Duration(minutes: 6))); + detector.onResumed( + _activity(activeTasks: 0, completedTasks: 1, downloadedBytes: 21 * 1024), + start.add(const Duration(minutes: 11)), + networkAvailable: true, + ); + + expect(detector.consecutiveStalls, 0); + }); + + test('a completion with omitted byte counters still clears the counter', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + expect(detector.consecutiveStalls, 1); + + detector.onPaused(_activity(downloadedBytes: 500 * 1024 * 1024), start.add(const Duration(minutes: 6))); + detector.onResumed( + _activity(activeTasks: 0, completedTasks: 1, downloadedBytes: 0), + start.add(const Duration(minutes: 11)), + networkAvailable: true, + ); + + expect(detector.consecutiveStalls, 0); + }); + + test('percentage progress clears the counter when byte length is unknown', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + expect(detector.consecutiveStalls, 1); + + detector.onPaused(_activity(progressUnits: 20), start.add(const Duration(minutes: 6))); + detector.onResumed(_activity(progressUnits: 21), start.add(const Duration(minutes: 11)), networkAvailable: true); + + expect(detector.consecutiveStalls, 0); + }); + + test('an idle app is never scored', () { + detector.onPaused(_activity(activeTasks: 0), start); + expect( + detector.onResumed(_activity(activeTasks: 0), start.add(const Duration(hours: 8)), networkAvailable: true), + isFalse, + ); + expect(detector.consecutiveStalls, 0); + }); + + test('a task removed without failing is inconclusive', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + expect(detector.consecutiveStalls, 1); + + detector.onPaused(_activity(), start.add(const Duration(minutes: 6))); + detector.onResumed(_activity(activeTasks: 0), start.add(const Duration(minutes: 11)), networkAvailable: true); + + expect(detector.consecutiveStalls, 1); + }); + + test('a newly failed active task is stall evidence', () { + detector.onPaused(_activity(), start); + detector.onResumed( + _activity(activeTasks: 0, failedTasks: 1), + start.add(const Duration(minutes: 5)), + networkAvailable: true, + ); + + expect(detector.consecutiveStalls, 1); + }); + + test('a short background trip proves nothing', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(seconds: 30)), networkAvailable: true); + + expect(detector.consecutiveStalls, 0); + }); + + test('being offline is not evidence of a restriction', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: false); + + expect(detector.consecutiveStalls, 0); + }); + + test('an offline endpoint is inconclusive even when policy allows downloads', () { + detector.onPaused(_activity(networkAvailable: false), start); + detector.onResumed( + _activity(networkAvailable: false), + start.add(const Duration(minutes: 5)), + networkAvailable: true, + ); + + expect(detector.consecutiveStalls, 0); + }); + + test('a network transition during the background window is inconclusive', () { + detector.onPaused(_activity(networkStateGeneration: 4), start); + detector.onResumed( + _activity(networkStateGeneration: 5), + start.add(const Duration(minutes: 5)), + networkAvailable: true, + ); + + expect(detector.consecutiveStalls, 0); + }); + + test('downloads deleted while backgrounded are inconclusive', () { + detector.onPaused(_activity(activeTasks: 2, downloadedBytes: 900 * 1024 * 1024), start); + detector.onResumed( + _activity(downloadedBytes: 10 * 1024 * 1024), + start.add(const Duration(minutes: 5)), + networkAvailable: true, + ); + + expect(detector.consecutiveStalls, 0); + }); + + test('reset clears both the armed snapshot and the count', () { + detector.onPaused(_activity(), start); + detector.onResumed(_activity(), start.add(const Duration(minutes: 5)), networkAvailable: true); + detector.reset(); + expect(detector.consecutiveStalls, 0); + + expect(detector.onResumed(_activity(), start.add(const Duration(minutes: 10)), networkAvailable: true), isFalse); + }); + }); + + group('BackgroundWorkStatus', () { + test('describe reports unknown until a probe lands', () { + expect(const BackgroundWorkStatus().describe(), 'unknown'); + }); + + test('describe carries verdict, reasons and raw signals for log uploads', () { + const status = BackgroundWorkStatus( + verdict: BackgroundWorkVerdict.blocked, + reasons: [BackgroundWorkReason.backgroundRestricted, BackgroundWorkReason.standbyRestricted], + standbyBucket: 45, + ignoringBatteryOptimizations: false, + probed: true, + ); + + expect(status.describe(), 'blocked background_restricted+standby_restricted bucket:45 batteryWhitelist:false'); + }); + + test('routes notification faults to their valid settings screens', () { + const channel = BackgroundWorkStatus( + verdict: BackgroundWorkVerdict.degraded, + reasons: [BackgroundWorkReason.downloadChannelBlocked], + probed: true, + ); + const appWide = BackgroundWorkStatus( + verdict: BackgroundWorkVerdict.blocked, + reasons: [BackgroundWorkReason.notificationsDisabled], + probed: true, + ); + const battery = BackgroundWorkStatus( + verdict: BackgroundWorkVerdict.blocked, + reasons: [BackgroundWorkReason.backgroundRestricted], + probed: true, + ); + + expect(channel.remedyTarget, BackgroundSettingsTarget.notificationChannel); + expect(appWide.remedyTarget, BackgroundSettingsTarget.appNotifications); + expect(battery.remedyTarget, BackgroundSettingsTarget.appDetails); + }); + }); + + group('BackgroundWorkDiagnosticsService', () { + late List calls; + late Map signals; + const channel = MethodChannel('com.plezy/device.test'); + + BackgroundWorkDiagnosticsService buildService({ + DownloadStallDetector? stallDetector, + Future Function()? networkProbe, + DateTime Function()? clock, + bool supported = true, + }) => BackgroundWorkDiagnosticsService.forTesting( + channel: channel, + stallDetector: stallDetector, + networkProbe: networkProbe, + clock: clock, + supported: supported, + ); + + setUp(() { + calls = []; + signals = {'verdict': 'ok', 'reasons': []}; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { + calls.add(call); + return switch (call.method) { + 'getBackgroundWorkSignals' => signals, + 'openBackgroundSettings' => true, + _ => null, + }; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + + test('a healthy probe stays healthy but records that it ran', () async { + final service = buildService(); + + final status = await service.refresh(); + + expect(status.verdict, BackgroundWorkVerdict.ok); + expect(status.reasons, isEmpty); + expect(status.probed, isTrue); + expect(service.describeSync(), 'ok'); + }); + + test('binding download activity performs the cold-start probe', () async { + signals = { + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }; + final service = buildService(); + + service.bindActivitySource(_activity); + await Future.delayed(Duration.zero); + + expect(calls.where((call) => call.method == 'getBackgroundWorkSignals'), hasLength(1)); + expect(service.status.isBlocked, isTrue); + service.unbindActivitySource(_activity); + service.dispose(); + }); + + test('unsupported devices neither observe nor probe', () async { + final service = buildService(supported: false); + + service.bindActivitySource(_activity); + await Future.delayed(Duration.zero); + + expect(service.isSupported, isFalse); + expect(calls, isEmpty); + expect(service.status.probed, isFalse); + service.unbindActivitySource(_activity); + service.dispose(); + }); + + test('parses a blocked verdict with its reasons in order', () async { + signals = { + 'verdict': 'blocked', + 'reasons': ['background_restricted', 'standby_restricted'], + 'standbyBucket': 45, + 'ignoringBatteryOptimizations': false, + }; + final service = buildService(); + + final status = await service.refresh(); + + expect(status.verdict, BackgroundWorkVerdict.blocked); + expect(status.reasons, [BackgroundWorkReason.backgroundRestricted, BackgroundWorkReason.standbyRestricted]); + expect(status.standbyBucket, 45); + expect(status.primaryReason, BackgroundWorkReason.backgroundRestricted); + }); + + test('a blocked verdict with no recognizable reason is not trusted', () async { + // A newer native build emitting an unknown reason id must not produce a + // warning the UI has nothing to say about. + signals = { + 'verdict': 'blocked', + 'reasons': ['some_future_reason'], + }; + final service = buildService(); + + final status = await service.refresh(); + + expect(status.verdict, BackgroundWorkVerdict.ok); + expect(status.reasons, isEmpty); + }); + + test('refuses an oem_unknown reason arriving over the wire', () async { + signals = { + 'verdict': 'blocked', + 'reasons': ['oem_unknown'], + }; + final service = buildService(); + + final status = await service.refresh(); + + expect(status.reasons, isEmpty); + expect(status.verdict, BackgroundWorkVerdict.ok); + }); + + test('data saver alone is degraded, not blocked', () async { + signals = { + 'verdict': 'degraded', + 'reasons': ['data_saver'], + }; + final service = buildService(); + + final status = await service.refresh(); + + expect(status.verdict, BackgroundWorkVerdict.degraded); + expect(status.isBlocked, isFalse); + expect(status.isHealthy, isFalse); + }); + + test('concurrent refreshes coalesce into a single platform call', () async { + final service = buildService(); + + await Future.wait([service.refresh(), service.refresh(), service.refresh()]); + + expect(calls.where((c) => c.method == 'getBackgroundWorkSignals'), hasLength(1)); + }); + + test('a missing native handler never produces a warning and stops asking', () async { + var attempts = 0; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { + attempts++; + throw MissingPluginException(); + }); + final service = buildService(); + + final first = await service.refresh(); + final second = await service.refresh(); + + expect(first.verdict, BackgroundWorkVerdict.ok); + expect(first.probed, isFalse); + expect(second.probed, isFalse); + expect(attempts, 1); + }); + + test('an in-flight probe completing after disposal is ignored', () async { + final gate = Completer>(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) { + calls.add(call); + return gate.future; + }); + final service = buildService(); + + final refresh = service.refresh(); + await Future.delayed(Duration.zero); + service.dispose(); + gate.complete({ + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }); + + await expectLater(refresh, completes); + expect(service.status.probed, isFalse); + }); + + test('a confirmed stall survives a probe that reports everything is fine', () async { + final detector = DownloadStallDetector(observationsToConfirm: 1, minimumBackgroundGap: Duration.zero); + final service = buildService(stallDetector: detector, networkProbe: () async => true); + detector.onPaused(_activity(downloadedBytes: 0), DateTime(2026)); + detector.onResumed(_activity(downloadedBytes: 0), DateTime(2026, 1, 1, 1), networkAvailable: true); + expect(detector.isStalled, isTrue); + + final status = await service.refresh(); + + expect(status.verdict, BackgroundWorkVerdict.blocked); + expect(status.reasons, [BackgroundWorkReason.oemUnknown]); + }); + + test('clearing stall evidence drops the reason and re-probes', () async { + final detector = DownloadStallDetector(observationsToConfirm: 1, minimumBackgroundGap: Duration.zero); + final service = buildService(stallDetector: detector, networkProbe: () async => true); + detector.onPaused(_activity(downloadedBytes: 0), DateTime(2026)); + detector.onResumed(_activity(downloadedBytes: 0), DateTime(2026, 1, 1, 1), networkAvailable: true); + await service.refresh(); + expect(service.status.isBlocked, isTrue); + + await service.clearStallEvidence(); + + expect(service.status.reasons, isEmpty); + expect(service.status.verdict, BackgroundWorkVerdict.ok); + expect(detector.isStalled, isFalse); + }); + + test('opening settings forwards the target id the native side expects', () async { + final service = buildService(); + + final opened = await service.openSettings(BackgroundSettingsTarget.notificationChannel); + + expect(opened, isTrue); + final call = calls.firstWhere((c) => c.method == 'openBackgroundSettings'); + expect(call.arguments, 'notification_channel'); + }); + + test('lifecycle resume scores the synchronously captured snapshot', () async { + final detector = DownloadStallDetector(observationsToConfirm: 1, minimumBackgroundGap: Duration.zero); + final networkGate = Completer(); + var now = DateTime(2026); + var activity = _activity(downloadedBytes: 1000); + final service = buildService(stallDetector: detector, networkProbe: () => networkGate.future, clock: () => now); + DownloadActivitySnapshot source() => activity; + service.bindActivitySource(source); + await Future.delayed(Duration.zero); + + service.didChangeAppLifecycleState(AppLifecycleState.paused); + now = now.add(const Duration(minutes: 5)); + service.didChangeAppLifecycleState(AppLifecycleState.resumed); + activity = _activity(downloadedBytes: 5 * 1024 * 1024); + networkGate.complete(true); + await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); + + expect(detector.isStalled, isTrue, reason: 'progress after resume must not be credited to the background window'); + expect(service.status.reasons, contains(BackgroundWorkReason.oemUnknown)); + service.unbindActivitySource(source); + service.dispose(); + }); + + test('notifies listeners only when the status actually changes', () async { + signals = { + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }; + final service = buildService(); + var notifications = 0; + service.addListener(() => notifications++); + + await service.refresh(); + await service.refresh(); + + expect(notifications, 1); + }); + }); +} + +DownloadActivitySnapshot _activity({ + int activeTasks = 1, + int completedTasks = 0, + int failedTasks = 0, + int downloadedBytes = 1000, + int progressUnits = 0, + bool? networkAvailable = true, + int networkStateGeneration = 0, +}) => ( + activeTasks: activeTasks, + completedTasks: completedTasks, + failedTasks: failedTasks, + downloadedBytes: downloadedBytes, + progressUnits: progressUnits, + networkAvailable: networkAvailable, + networkStateGeneration: networkStateGeneration, +); diff --git a/test/utils/notification_permission_test.dart b/test/utils/notification_permission_test.dart new file mode 100644 index 00000000..7ea35b45 --- /dev/null +++ b/test/utils/notification_permission_test.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/utils/notification_permission.dart'; + +void main() { + setUp(NotificationPermission.debugReset); + + tearDown(() { + NotificationPermission.debugRequestOverride = null; + NotificationPermission.debugReset(); + }); + + test('concurrent callers await one shared permission request', () async { + final requestStarted = Completer(); + final releaseRequest = Completer(); + var requests = 0; + NotificationPermission.debugRequestOverride = () async { + requests++; + requestStarted.complete(); + await releaseRequest.future; + }; + + var firstCompleted = false; + var secondCompleted = false; + final first = NotificationPermission.ensure().then((_) => firstCompleted = true); + final second = NotificationPermission.ensure().then((_) => secondCompleted = true); + + await requestStarted.future; + expect(requests, 1); + expect(firstCompleted, isFalse); + expect(secondCompleted, isFalse); + + releaseRequest.complete(); + await Future.wait([first, second]); + + expect(firstCompleted, isTrue); + expect(secondCompleted, isTrue); + }); + + test('a failed permission request is swallowed and remains cached', () async { + var requests = 0; + NotificationPermission.debugRequestOverride = () async { + requests++; + throw StateError('permission channel unavailable'); + }; + + await NotificationPermission.ensure(); + await NotificationPermission.ensure(); + + expect(requests, 1); + }); +} diff --git a/test/widgets/background_download_warning_banner_test.dart b/test/widgets/background_download_warning_banner_test.dart new file mode 100644 index 00000000..26420dc0 --- /dev/null +++ b/test/widgets/background_download_warning_banner_test.dart @@ -0,0 +1,295 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/focus/input_mode_tracker.dart'; +import 'package:plezy/i18n/strings.g.dart'; +import 'package:plezy/services/background_work_diagnostics_service.dart'; +import 'package:plezy/services/settings_service.dart'; +import 'package:plezy/widgets/background_download_warning_banner.dart'; + +import '../test_helpers/prefs.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('com.plezy/device.background-warning-test'); + late Map signals; + late List calls; + + setUpAll(() => LocaleSettings.setLocaleSync(AppLocale.en)); + + setUp(() async { + resetSharedPreferencesForTest(); + SettingsService.resetForTesting(); + await SettingsService.getInstance(); + signals = {'verdict': 'ok', 'reasons': []}; + calls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { + calls.add(call); + return switch (call.method) { + 'getBackgroundWorkSignals' => signals, + 'openBackgroundSettings' => true, + _ => null, + }; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + SettingsService.resetForTesting(); + }); + + testWidgets('requests notification permission before probing restrictions', (tester) async { + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + final context = await _pumpShell(tester); + final events = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { + if (call.method == 'getBackgroundWorkSignals') { + events.add('probe'); + return signals; + } + return true; + }); + + final proceed = await confirmBackgroundDownloadRestrictions( + context, + service: service, + notificationPermissionRequester: () async => events.add('permission'), + ); + + expect(proceed, isTrue); + expect(events, ['permission', 'probe']); + }); + + testWidgets('download anyway acknowledges the warning and proceeds', (tester) async { + signals = { + 'verdict': 'blocked', + 'reasons': ['notifications_disabled'], + }; + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + final context = await _pumpShell(tester); + + final result = confirmBackgroundDownloadRestrictions( + context, + service: service, + notificationPermissionRequester: () async {}, + ); + await tester.pumpAndSettle(); + + expect(find.text(t.downloads.backgroundWarning.dialogTitle), findsOneWidget); + await tester.tap(find.text(t.downloads.backgroundWarning.dialogDownloadAnyway)); + await tester.pumpAndSettle(); + + expect(await result, isTrue); + expect(SettingsService.instance.read(SettingsService.backgroundDownloadWarningAcknowledged), isTrue); + }); + + testWidgets('the focused fix action opens settings and cancels enqueue', (tester) async { + signals = { + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }; + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + final context = await _pumpShell(tester); + + final result = confirmBackgroundDownloadRestrictions( + context, + service: service, + notificationPermissionRequester: () async {}, + ); + await tester.pumpAndSettle(); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + expect(await result, isFalse); + expect( + calls.where((call) => call.method == 'openBackgroundSettings').single.arguments, + BackgroundSettingsTarget.appDetails.id, + ); + }); + + testWidgets('notification denial opens app settings without requiring a channel', (tester) async { + signals = { + 'verdict': 'blocked', + 'reasons': ['notifications_disabled'], + }; + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + final context = await _pumpShell(tester); + + final result = confirmBackgroundDownloadRestrictions( + context, + service: service, + notificationPermissionRequester: () async {}, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text(t.downloads.backgroundWarning.dialogFixFirst)); + await tester.pumpAndSettle(); + + expect(await result, isFalse); + expect( + calls.where((call) => call.method == 'openBackgroundSettings').single.arguments, + BackgroundSettingsTarget.appNotifications.id, + ); + }); + + testWidgets('opening the OEM remedy clears stale empirical evidence', (tester) async { + final detector = DownloadStallDetector(observationsToConfirm: 1, minimumBackgroundGap: Duration.zero); + detector.onPaused(_activity(), DateTime(2026)); + detector.onResumed(_activity(), DateTime(2026, 1, 1, 1), networkAvailable: true); + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel, stallDetector: detector); + addTearDown(service.dispose); + await service.refresh(); + final context = await _pumpShell(tester); + + final result = showBackgroundDownloadWarningDialog(context, service: service); + await tester.pumpAndSettle(); + await tester.tap(find.text(t.downloads.backgroundWarning.openSettings)); + await tester.pumpAndSettle(); + + expect(await result, isTrue); + expect(detector.isStalled, isFalse); + expect(service.status.reasons, isEmpty); + }); + + testWidgets('acknowledged warnings still request permission but skip the probe', (tester) async { + await SettingsService.instance.write(SettingsService.backgroundDownloadWarningAcknowledged, true); + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + final context = await _pumpShell(tester); + var permissionRequests = 0; + + final proceed = await confirmBackgroundDownloadRestrictions( + context, + service: service, + notificationPermissionRequester: () async => permissionRequests++, + ); + + expect(proceed, isTrue); + expect(permissionRequests, 1); + expect(calls, isEmpty); + }); + + testWidgets('blocked status renders only while downloads are pending', (tester) async { + signals = { + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }; + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + await service.refresh(); + + await _pumpShell(tester, child: BackgroundDownloadWarningBanner(hasPendingDownloads: true, service: service)); + expect(find.text(t.downloads.backgroundWarning.bannerBlocked), findsOneWidget); + + await _pumpShell(tester, child: BackgroundDownloadWarningBanner(hasPendingDownloads: false, service: service)); + expect(find.text(t.downloads.backgroundWarning.bannerBlocked), findsNothing); + }); + + testWidgets('Data Saver uses degraded banner and dialog copy', (tester) async { + signals = { + 'verdict': 'degraded', + 'reasons': ['data_saver'], + }; + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + await service.refresh(); + + await _pumpShell(tester, child: BackgroundDownloadWarningBanner(hasPendingDownloads: true, service: service)); + expect(find.text(t.downloads.backgroundWarning.bannerDegraded), findsOneWidget); + + await tester.tap(find.text(t.downloads.backgroundWarning.bannerAction)); + await tester.pumpAndSettle(); + + expect( + find.descendant( + of: find.byType(AlertDialog), + matching: find.text(t.downloads.backgroundWarning.sheetTitleDegraded), + ), + findsOneWidget, + ); + expect(find.text(t.downloads.backgroundWarning.sheetTitle), findsNothing); + }); + + testWidgets('an unavailable help link gives user feedback', (tester) async { + signals = { + 'verdict': 'blocked', + 'reasons': ['background_restricted'], + }; + + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel); + addTearDown(service.dispose); + await service.refresh(); + final context = await _pumpShell(tester); + + final result = showBackgroundDownloadWarningDialog( + context, + service: service, + externalUrlLauncher: (_) async => false, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('dontkillmyapp.com')); + await tester.pump(); + + expect(find.text(t.downloads.backgroundWarning.linkUnavailable), findsOneWidget); + + await tester.tap(find.text(t.common.close)); + await tester.pumpAndSettle(); + expect(await result, isTrue); + }); + + testWidgets('unsupported devices skip both warning surfaces', (tester) async { + final service = BackgroundWorkDiagnosticsService.forTesting(channel: channel, supported: false); + addTearDown(service.dispose); + final context = await _pumpShell( + tester, + child: BackgroundDownloadWarningBanner(hasPendingDownloads: true, service: service), + ); + var permissionRequests = 0; + + final proceed = await confirmBackgroundDownloadRestrictions( + context, + service: service, + notificationPermissionRequester: () async => permissionRequests++, + ); + + expect(proceed, isTrue); + expect(permissionRequests, 0); + expect(calls, isEmpty); + expect(find.byType(BackgroundDownloadWarningBanner), findsOneWidget); + expect(find.text(t.downloads.backgroundWarning.bannerBlocked), findsNothing); + }); +} + +DownloadActivitySnapshot _activity() => ( + activeTasks: 1, + completedTasks: 0, + failedTasks: 0, + downloadedBytes: 1000, + progressUnits: 0, + networkAvailable: true, + networkStateGeneration: 0, +); + +Future _pumpShell(WidgetTester tester, {Widget child = const SizedBox.shrink()}) async { + late BuildContext context; + await tester.pumpWidget( + TranslationProvider( + child: InputModeTracker( + child: MaterialApp( + home: Builder( + builder: (builderContext) { + context = builderContext; + return Scaffold(body: child); + }, + ), + ), + ), + ), + ); + await tester.pump(); + return context; +}