diff --git a/.github/workflows/sync_crowdin.yml b/.github/workflows/sync_crowdin.yml new file mode 100644 index 0000000000..e0d0912b72 --- /dev/null +++ b/.github/workflows/sync_crowdin.yml @@ -0,0 +1,39 @@ +name: Sync Crowdin + +on: + workflow_dispatch: + schedule: + - cron: 0 * 1 * * + push: + paths: + - /src/main/resources/addresources/values/strings.xml + +jobs: + sync: + name: Sync translations + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Sync translations + uses: crowdin/github-action@v1 + with: + config: crowdin.yml + upload_sources: true + upload_translations: false + download_translations: true + localization_branch_name: feat/translations + create_pull_request: true + pull_request_title: "chore: Sync translations" + pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)" + pull_request_base_branch_name: "dev" + commit_message: "chore: Sync translations" + github_user_name: revanced-bot + github_user_email: github@revanced.app + env: + GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a4078bad06..9afb840949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [4.5.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.5.0-dev.1...v4.5.0-dev.2) (2024-03-30) + + +### Features + +* **YouTube - GmsCore:** Require ignoring battery optimizations ([#2952](https://github.com/ReVanced/revanced-patches/issues/2952)) ([c0bef25](https://github.com/ReVanced/revanced-patches/commit/c0bef255909ca884838675ca6f7ac5b0e2e21730)) + +# [4.5.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.4.0...v4.5.0-dev.1) (2024-03-29) + + +### Features + +* **YouTube - Alternative thumbnails:** Selectively enable for home / subscription / search ([#2926](https://github.com/ReVanced/revanced-patches/issues/2926)) ([8549e1b](https://github.com/ReVanced/revanced-patches/commit/8549e1ba58ad1e1608f5e3ceacd31eeb94578949)) + # [4.4.0](https://github.com/ReVanced/revanced-patches/compare/v4.3.0...v4.4.0) (2024-03-27) diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000000..4ac3cb98b3 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,8 @@ +project_id_env: "CROWDIN_PROJECT_ID" +api_token_env: "CROWDIN_PERSONAL_TOKEN" + +preserve_hierarchy: false +files: + - source: src/main/resources/addresources/values/strings.xml + translation: src/main/resources/addresources/values-%android_code%/strings.xml + skip_untranslated_strings: true diff --git a/gradle.properties b/gradle.properties index fa4f042dc7..9bb54304c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 4.4.0 +version = 4.5.0-dev.2 diff --git a/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt index 65d9053960..060d3447c7 100644 --- a/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt +++ b/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt @@ -34,5 +34,5 @@ object GmsCoreSupportPatch : BaseGmsCoreSupportPatch( PrimeMethodFingerprint, ), ) { - override val gmsCoreVendor by gmsCoreVendorGroupIdOption + override val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption } diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch.kt b/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch.kt index f7bfcb6efd..9d2544b7e8 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch.kt +++ b/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch.kt @@ -2,7 +2,7 @@ package app.revanced.patches.shared.misc.gms import app.revanced.patcher.PatchClass import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstructions import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.fingerprint.MethodFingerprint @@ -12,7 +12,7 @@ import app.revanced.patches.shared.misc.gms.BaseGmsCoreSupportPatch.Constants.AC import app.revanced.patches.shared.misc.gms.BaseGmsCoreSupportPatch.Constants.AUTHORITIES import app.revanced.patches.shared.misc.gms.BaseGmsCoreSupportPatch.Constants.PERMISSIONS import app.revanced.patches.shared.misc.gms.fingerprints.GmsCoreSupportFingerprint -import app.revanced.patches.shared.misc.gms.fingerprints.GmsCoreSupportFingerprint.GET_GMS_CORE_VENDOR_METHOD_NAME +import app.revanced.patches.shared.misc.gms.fingerprints.GmsCoreSupportFingerprint.GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME import app.revanced.util.exception import app.revanced.util.getReference import app.revanced.util.returnEarly @@ -32,7 +32,7 @@ import com.android.tools.smali.dexlib2.util.MethodUtil * @param toPackageName The package name to fall back to if no custom package name is specified in patch options. * @param primeMethodFingerprint The fingerprint of the "prime" method that needs to be patched. * @param earlyReturnFingerprints The fingerprints of methods that need to be returned early. - * @param mainActivityOnCreateFingerprint The fingerprint of the main activity's onCreate method. + * @param mainActivityOnCreateFingerprint The fingerprint of the main activity onCreate method. * @param integrationsPatchDependency The patch responsible for the integrations. * @param gmsCoreSupportResourcePatch The corresponding resource patch that is used to patch the resources. * @param dependencies Additional dependencies of this patch. @@ -60,7 +60,10 @@ abstract class BaseGmsCoreSupportPatch( integrationsPatchDependency, ) + dependencies, compatiblePackages = compatiblePackages, - fingerprints = setOf(GmsCoreSupportFingerprint, mainActivityOnCreateFingerprint) + fingerprints, + fingerprints = setOf( + GmsCoreSupportFingerprint, + mainActivityOnCreateFingerprint, + ) + fingerprints, requiresIntegrations = true, ) { init { @@ -68,7 +71,7 @@ abstract class BaseGmsCoreSupportPatch( gmsCoreSupportResourcePatch.options.values.forEach(options::register) } - internal abstract val gmsCoreVendor: String? + internal abstract val gmsCoreVendorGroupId: String? override fun execute(context: BytecodeContext) { val packageName = ChangePackageNamePatch.setOrGetFallbackPackageName(toPackageName) @@ -93,16 +96,17 @@ abstract class BaseGmsCoreSupportPatch( // Return these methods early to prevent the app from crashing. earlyReturnFingerprints.toList().returnEarly() - // Check the availability of GmsCore. - mainActivityOnCreateFingerprint.result?.mutableMethod?.addInstruction( - 1, // Hack to not disturb other patches (such as the integrations patch). - "invoke-static {}, Lapp/revanced/integrations/shared/GmsCoreSupport;->checkAvailability()V", + // Verify GmsCore is installed and whitelisted for power optimizations and background usage. + mainActivityOnCreateFingerprint.result?.mutableMethod?.addInstructions( + 1, // Hack to not disturb other patches (such as the YTMusic integrations patch). + "invoke-static/range { p0 .. p0 }, Lapp/revanced/integrations/shared/GmsCoreSupport;->" + + "checkGmsCore(Landroid/content/Context;)V", ) ?: throw mainActivityOnCreateFingerprint.exception // Change the vendor of GmsCore in ReVanced Integrations. GmsCoreSupportFingerprint.result?.mutableClass?.methods - ?.single { it.name == GET_GMS_CORE_VENDOR_METHOD_NAME } - ?.replaceInstruction(0, "const-string v0, \"$gmsCoreVendor\"") + ?.single { it.name == GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME } + ?.replaceInstruction(0, "const-string v0, \"$gmsCoreVendorGroupId\"") ?: throw GmsCoreSupportFingerprint.exception } @@ -146,10 +150,10 @@ abstract class BaseGmsCoreSupportPatch( in PERMISSIONS, in ACTIONS, in AUTHORITIES, - -> referencedString.replace("com.google", gmsCoreVendor!!) + -> referencedString.replace("com.google", gmsCoreVendorGroupId!!) // No vendor prefix for whatever reason... - "subscribedfeeds" -> "$gmsCoreVendor.subscribedfeeds" + "subscribedfeeds" -> "$gmsCoreVendorGroupId.subscribedfeeds" else -> null } @@ -162,7 +166,7 @@ abstract class BaseGmsCoreSupportPatch( if (str.startsWith(uriPrefix)) { return str.replace( uriPrefix, - "content://${authority.replace("com.google", gmsCoreVendor!!)}", + "content://${authority.replace("com.google", gmsCoreVendorGroupId!!)}", ) } } @@ -170,7 +174,7 @@ abstract class BaseGmsCoreSupportPatch( // gms also has a 'subscribedfeeds' authority, check for that one too val subFeedsUriPrefix = "content://subscribedfeeds" if (str.startsWith(subFeedsUriPrefix)) { - return str.replace(subFeedsUriPrefix, "content://$gmsCoreVendor.subscribedfeeds") + return str.replace(subFeedsUriPrefix, "content://$gmsCoreVendorGroupId.subscribedfeeds") } } diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch.kt b/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch.kt index eef039f740..f77a6d3616 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch.kt +++ b/src/main/kotlin/app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch.kt @@ -121,7 +121,6 @@ abstract class BaseGmsCoreSupportResourcePatch( } private companion object { - private const val VANCED_VENDOR = "com.mgoogle" private const val PACKAGE_NAME_REGEX_PATTERN = "^[a-z]\\w*(\\.[a-z]\\w*)+\$" } } diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/gms/fingerprints/GmsCoreSupportFingerprint.kt b/src/main/kotlin/app/revanced/patches/shared/misc/gms/fingerprints/GmsCoreSupportFingerprint.kt index 79be107f90..52cef8fd6b 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/gms/fingerprints/GmsCoreSupportFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/shared/misc/gms/fingerprints/GmsCoreSupportFingerprint.kt @@ -5,7 +5,7 @@ import app.revanced.patcher.fingerprint.MethodFingerprint internal object GmsCoreSupportFingerprint : MethodFingerprint( customFingerprint = { _, classDef -> classDef.type.endsWith("GmsCoreSupport;") - } + }, ) { - const val GET_GMS_CORE_VENDOR_METHOD_NAME = "getGmsCoreVendor" + const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId" } diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt index e9f2c920be..6147a3d546 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -14,7 +14,7 @@ import app.revanced.util.resultOrThrow @Patch( name = "Downloads", - description = "Adds support to download videos with an external downloader app" + + description = "Adds support to download videos with an external downloader app " + "using the in-app download button or a video player action button.", dependencies = [ DownloadsResourcePatch::class, diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt index 4daa6248fa..2f624435b1 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt @@ -22,6 +22,8 @@ import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.reques import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.request.callback.OnResponseStartedFingerprint import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.request.callback.OnSucceededFingerprint import app.revanced.patches.youtube.misc.integrations.IntegrationsPatch +import app.revanced.patches.youtube.misc.navigation.NavigationBarHookPatch +import app.revanced.patches.youtube.misc.playertype.PlayerTypeHookPatch import app.revanced.patches.youtube.misc.settings.SettingsPatch import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.AccessFlags @@ -38,6 +40,8 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethod IntegrationsPatch::class, SettingsPatch::class, AddResourcesPatch::class, + NavigationBarHookPatch::class, + PlayerTypeHookPatch::class ], compatiblePackages = [ CompatiblePackage( @@ -127,25 +131,45 @@ object AlternativeThumbnailsPatch : BytecodePatch( override fun execute(context: BytecodeContext) { AddResourcesPatch(this::class) + val entries = "revanced_alt_thumbnail_options_entries" + val values = "revanced_alt_thumbnail_options_entry_values" SettingsPatch.PreferenceScreen.ALTERNATIVE_THUMBNAILS.addPreferences( - NonInteractivePreference( - "revanced_alt_thumbnail_about", - null, // Summary is dynamically updated based on the current settings. - tag = "app.revanced.integrations.youtube.settings.preference.AlternativeThumbnailsStatusPreference", + ListPreference("revanced_alt_thumbnail_home", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values + ), + ListPreference("revanced_alt_thumbnail_subscription", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values + ), + ListPreference("revanced_alt_thumbnail_library", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values + ), + ListPreference("revanced_alt_thumbnail_player", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values + ), + ListPreference("revanced_alt_thumbnail_search", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values ), - SwitchPreference("revanced_alt_thumbnail_dearrow"), - SwitchPreference("revanced_alt_thumbnail_dearrow_connection_toast"), - TextPreference("revanced_alt_thumbnail_dearrow_api_url"), NonInteractivePreference( "revanced_alt_thumbnail_dearrow_about", // Custom about preference with link to the DeArrow website. tag = "app.revanced.integrations.youtube.settings.preference.AlternativeThumbnailsAboutDeArrowPreference", selectable = true, ), - SwitchPreference("revanced_alt_thumbnail_stills"), - ListPreference("revanced_alt_thumbnail_stills_time", summaryKey = null), - SwitchPreference("revanced_alt_thumbnail_stills_fast"), + SwitchPreference("revanced_alt_thumbnail_dearrow_connection_toast"), + TextPreference("revanced_alt_thumbnail_dearrow_api_url"), NonInteractivePreference("revanced_alt_thumbnail_stills_about"), + SwitchPreference("revanced_alt_thumbnail_stills_fast"), + ListPreference("revanced_alt_thumbnail_stills_time", summaryKey = null) ) fun MethodFingerprint.alsoResolve(fingerprint: MethodFingerprint) = diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt index f7b43a2b60..434ddcaa21 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt @@ -2,16 +2,14 @@ package app.revanced.patches.youtube.misc.announcements import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstructions import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.annotation.CompatiblePackage import app.revanced.patcher.patch.annotation.Patch import app.revanced.patches.all.misc.resources.AddResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.settings.SettingsPatch -import app.revanced.patches.youtube.shared.fingerprints.MainActivityFingerprint -import app.revanced.util.exception -import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patches.youtube.shared.fingerprints.MainActivityOnCreateFingerprint +import app.revanced.util.resultOrThrow @Patch( name = "Announcements", @@ -21,7 +19,7 @@ import com.android.tools.smali.dexlib2.Opcode ) @Suppress("unused") object AnnouncementsPatch : BytecodePatch( - setOf(MainActivityFingerprint) + setOf(MainActivityOnCreateFingerprint) ) { private const val INTEGRATIONS_CLASS_DESCRIPTOR = "Lapp/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch;" @@ -33,16 +31,11 @@ object AnnouncementsPatch : BytecodePatch( SwitchPreference("revanced_announcements") ) - val onCreateMethod = MainActivityFingerprint.result?.let { - it.mutableClass.methods.find { method -> method.name == "onCreate" } - } ?: throw MainActivityFingerprint.exception - - val superCallIndex = onCreateMethod.getInstructions().indexOfFirst { it.opcode == Opcode.INVOKE_SUPER_RANGE } - - onCreateMethod.addInstructions( - superCallIndex + 1, - "invoke-static { v1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V" + MainActivityOnCreateFingerprint.resultOrThrow().mutableMethod.addInstructions( + // Insert index must be great than the insert index used by GmsCoreSupport, + // as both patch the same method and GmsCore check should be first. + 1, + "invoke-static/range { p0 .. p0 }, $INTEGRATIONS_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V" ) - } } diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt index ff29023516..6547f0ec32 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt @@ -9,7 +9,7 @@ import app.revanced.patches.youtube.misc.gms.Constants.YOUTUBE_PACKAGE_NAME import app.revanced.patches.youtube.misc.gms.GmsCoreSupportResourcePatch.gmsCoreVendorGroupIdOption import app.revanced.patches.youtube.misc.gms.fingerprints.* import app.revanced.patches.youtube.misc.integrations.IntegrationsPatch -import app.revanced.patches.youtube.shared.fingerprints.HomeActivityFingerprint +import app.revanced.patches.youtube.shared.fingerprints.MainActivityOnCreateFingerprint @Suppress("unused") object GmsCoreSupportPatch : BaseGmsCoreSupportPatch( @@ -23,7 +23,7 @@ object GmsCoreSupportPatch : BaseGmsCoreSupportPatch( CastDynamiteModuleV2Fingerprint, CastContextFetchFingerprint, ), - mainActivityOnCreateFingerprint = HomeActivityFingerprint, + mainActivityOnCreateFingerprint = MainActivityOnCreateFingerprint, integrationsPatchDependency = IntegrationsPatch::class, dependencies = setOf( HideCastButtonPatch::class, @@ -57,5 +57,5 @@ object GmsCoreSupportPatch : BaseGmsCoreSupportPatch( PrimeMethodFingerprint, ), ) { - override val gmsCoreVendor by gmsCoreVendorGroupIdOption + override val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption } diff --git a/src/main/kotlin/app/revanced/patches/youtube/shared/fingerprints/MainActivityOnCreateFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/shared/fingerprints/MainActivityOnCreateFingerprint.kt new file mode 100644 index 0000000000..fa63c0fdeb --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/shared/fingerprints/MainActivityOnCreateFingerprint.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.youtube.shared.fingerprints + +import app.revanced.patcher.fingerprint.MethodFingerprint + +internal object MainActivityOnCreateFingerprint : MethodFingerprint( + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + customFingerprint = { methodDef, classDef -> + methodDef.name == "onCreate" && + (classDef.type.endsWith("MainActivity;") + // Old versions of YouTube called this class "WatchWhileActivity" instead. + || classDef.type.endsWith("WatchWhileActivity;")) + } +) \ No newline at end of file diff --git a/src/main/resources/addresources/values/arrays.xml b/src/main/resources/addresources/values/arrays.xml index 064dcd05b3..5fad2d8841 100644 --- a/src/main/resources/addresources/values/arrays.xml +++ b/src/main/resources/addresources/values/arrays.xml @@ -44,15 +44,28 @@ + + @string/revanced_alt_thumbnail_options_entry_1 + @string/revanced_alt_thumbnail_options_entry_2 + @string/revanced_alt_thumbnail_options_entry_3 + @string/revanced_alt_thumbnail_options_entry_4 + + + + ORIGINAL + DEARROW + DEARROW_STILL_IMAGES + STILL_IMAGES + @string/revanced_alt_thumbnail_stills_time_entry_1 @string/revanced_alt_thumbnail_stills_time_entry_2 @string/revanced_alt_thumbnail_stills_time_entry_3 - 1 - 2 - 3 + BEGINNING + MIDDLE + END diff --git a/src/main/resources/addresources/values/strings.xml b/src/main/resources/addresources/values/strings.xml index b799eef24b..b587547558 100644 --- a/src/main/resources/addresources/values/strings.xml +++ b/src/main/resources/addresources/values/strings.xml @@ -13,8 +13,12 @@ Import failed: %s - GmsCore is not installed. Please install. - GmsCore is failing to run. Please follow the \"Don\'t kill my app\" guide for GmsCore. + GmsCore is not installed. Install it. + Follow the \"Don\'t kill my app\" guide for GmsCore. + Action needed + GmsCore is not whitelisted from battery optimization.\n\nFollow the \"Don\'t kill my app\" guide for GmsCore. + GmsCore does not have permission to run in the background.\n\nFollow the \"Don\'t kill my app\" guide for GmsCore. + Open website @@ -847,33 +851,31 @@ Invalid seekbar color value. Using default value. - Thumbnails in use - Enable DeArrow thumbnails - Using DeArrow thumbnails - Not using DeArrow thumbnails + Home tab + Subscription tab + You tab + Player playlists, recommendations + Search results + Original thumbnails + DeArrow & Original thumbnails + DeArrow & Still captures + Still captures + DeArrow + DeArrow provides crowd-sourced thumbnails for YouTube videos. These thumbnails are often more relevant than those provided by YouTube\n\nIf enabled, video URLs will be sent to the API server and no other data is sent. If a video does not have DeArrow thumbnails, then the original or still captures are shown\n\nTap here to learn more about DeArrow Show a toast if API is not available Toast is shown if DeArrow is not available Toast is not shown if DeArrow is not available DeArrow API endpoint - The URL of the DeArrow thumbnail cache endpoint. Do not change this unless you know what you\'re doing - About DeArrow - DeArrow provides crowd-sourced thumbnails for YouTube videos. These thumbnails are often more relevant than those provided by YouTube. If enabled, video URLs will be sent to the API server and no other data is sent\n\nTap here to learn more about DeArrow - Enable still video captures - Using YouTube still video captures - Not using YouTube still video captures - Video time to take the still from - Beginning of video - Middle of video - End of video + The URL of the DeArrow thumbnail cache endpoint + Still video captures + Still captures are taken from the beginning/middle/end of each video. These images are built into YouTube and no external API is used Use fast still captures Using medium quality still captures. Thumbnails will load faster, but live streams, unreleased, or very old videos may show blank thumbnails Using high quality still captures - About still video captures - Still captures are taken from the beginning/middle/end of each video. These images are built into YouTube and no external API is used - Showing original YouTube thumbnails - Showing still video captures - Showing DeArrow thumbnails. If a video has no DeArrow thumbnails then the original YouTube thumbnails are shown - Showing DeArrow thumbnails. If a video has no DeArrow thumbnails then still video captures are shown + Video time to take still captures from + Beginning of video + Middle of video + End of video DeArrow temporarily not available (status code: %s) DeArrow temporarily not available @@ -883,6 +885,7 @@ Announcements are not shown on startup Show announcements on startup Failed connecting to announcements provider + Dismiss Enable auto-repeat