mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 05:07:41 +02:00
feat: merge patch RVX/anddea (#55)
* feat(YouTube): Add `Custom Shorts action buttons` patch (Replaces `Shorts outline icon` patch) * feat(YouTube): Add `Visual preferences icons` patch * feat(Custom branding icon): Add more icons * feat(Custom branding icon): Add patch options `ChangeHeader`, `ChangeSplashIcon`, `RestoreOldSplashAnimation` * feat(Translations): Add patch options to remove languages and provide own translation / strings.xml * feat(YouTube/Settings): Add search bar in settings * feat(YouTube/Shorts components): Add `Hide disabled comments button` and `Hide live chat header` settings * refactor(YouTube/Settings): Fix typos in strings * feat(YouTube/Settings): clarify patch option descriptions Co-authored-by: KobeW50 <84587632+KobeW50@users.noreply.github.com> * feat(YouTube): clarify patch option Co-authored-by: KobeW50 <84587632+KobeW50@users.noreply.github.com> --------- Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com> Co-authored-by: KobeW50 <84587632+KobeW50@users.noreply.github.com>
This commit is contained in:
@ -2,6 +2,7 @@ package app.revanced.patches.music.layout.branding.icon
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.util.ResourceGroup
|
||||
@ -19,26 +20,79 @@ object CustomBrandingIconPatch : BaseResourcePatch(
|
||||
private const val DEFAULT_ICON_KEY = "Revancify Blue"
|
||||
|
||||
private val availableIcon = mapOf(
|
||||
"AFN Blue" to "afn_blue",
|
||||
"AFN Red" to "afn_red",
|
||||
"MMT" to "mmt",
|
||||
DEFAULT_ICON_KEY to "revancify_blue",
|
||||
"Revancify Red" to "revancify_red"
|
||||
"Revancify Red" to "revancify_red",
|
||||
"YouTube Music" to "youtube_music"
|
||||
)
|
||||
|
||||
private val mipmapIconResourceFileNames = arrayOf(
|
||||
"adaptiveproduct_youtube_music_background_color_108",
|
||||
"adaptiveproduct_youtube_music_foreground_color_108",
|
||||
"ic_launcher_release"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val mipmapDirectories = arrayOf(
|
||||
private val sizeArray = arrayOf(
|
||||
"xxxhdpi",
|
||||
"xxhdpi",
|
||||
"xhdpi",
|
||||
"hdpi",
|
||||
"mdpi"
|
||||
).map { "mipmap-$it" }
|
||||
)
|
||||
|
||||
private var AppIcon by stringPatchOption(
|
||||
private val largeSizeArray = arrayOf(
|
||||
"xlarge-hdpi",
|
||||
"xlarge-mdpi",
|
||||
"large-xhdpi",
|
||||
"large-hdpi",
|
||||
"large-mdpi",
|
||||
"xxhdpi",
|
||||
"xhdpi",
|
||||
"hdpi",
|
||||
"mdpi",
|
||||
)
|
||||
|
||||
private val drawableDirectories = sizeArray.map { "drawable-$it" }
|
||||
|
||||
private val largeDrawableDirectories = largeSizeArray.map { "drawable-$it" }
|
||||
|
||||
private val mipmapDirectories = sizeArray.map { "mipmap-$it" }
|
||||
|
||||
private val headerIconResourceFileNames = arrayOf(
|
||||
"action_bar_logo",
|
||||
"logo_music",
|
||||
"ytm_logo"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val launcherIconResourceFileNames = arrayOf(
|
||||
"adaptiveproduct_youtube_music_background_color_108",
|
||||
"adaptiveproduct_youtube_music_foreground_color_108",
|
||||
"ic_launcher_release"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val splashIconResourceFileNames = arrayOf(
|
||||
// This file only exists in [drawable-hdpi]
|
||||
// Since {@code ResourceUtils#copyResources} checks for null values before copying,
|
||||
// Just adds it to the array.
|
||||
"action_bar_logo_release",
|
||||
"record"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val headerIconResourceGroups = drawableDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *headerIconResourceFileNames
|
||||
)
|
||||
}
|
||||
|
||||
private val launcherIconResourceGroups = mipmapDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *launcherIconResourceFileNames
|
||||
)
|
||||
}
|
||||
|
||||
private val splashIconResourceGroups = largeDrawableDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *splashIconResourceFileNames
|
||||
)
|
||||
}
|
||||
|
||||
private val AppIcon by stringPatchOption(
|
||||
key = "AppIcon",
|
||||
default = DEFAULT_ICON_KEY,
|
||||
values = availableIcon,
|
||||
@ -50,22 +104,35 @@ object CustomBrandingIconPatch : BaseResourcePatch(
|
||||
|
||||
Each of these folders has to have the following files:
|
||||
|
||||
${mipmapIconResourceFileNames.joinToString("\n") { "- $it" }}
|
||||
${launcherIconResourceFileNames.joinToString("\n") { "- $it" }}
|
||||
"""
|
||||
.split("\n")
|
||||
.joinToString("\n") { it.trimIndent() } // Remove the leading whitespace from each line.
|
||||
.trimIndent(), // Remove the leading newline.
|
||||
)
|
||||
|
||||
private val ChangeHeader by booleanPatchOption(
|
||||
key = "ChangeHeader",
|
||||
default = false,
|
||||
title = "Change header",
|
||||
description = "Apply the custom branding icon to the header."
|
||||
)
|
||||
|
||||
private val ChangeSplashIcon by booleanPatchOption(
|
||||
key = "ChangeSplashIcon",
|
||||
default = true,
|
||||
title = "Change splash icons",
|
||||
description = "Apply the custom branding icon to the splash screen."
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
AppIcon?.let { appIcon ->
|
||||
val appIconValue = appIcon.lowercase().replace(" ", "_")
|
||||
val appIconResourcePath = "music/branding/$appIconValue"
|
||||
|
||||
// Check if a custom path is used in the patch options.
|
||||
if (!availableIcon.containsValue(appIconValue)) {
|
||||
mipmapDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *mipmapIconResourceFileNames
|
||||
)
|
||||
}.let { resourceGroups ->
|
||||
launcherIconResourceGroups.let { resourceGroups ->
|
||||
try {
|
||||
val path = File(appIcon)
|
||||
val resourceDirectory = context["res"]
|
||||
@ -82,33 +149,47 @@ object CustomBrandingIconPatch : BaseResourcePatch(
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Exception is thrown if an invalid path is used in the patch option.
|
||||
throw PatchException("Invalid app icon path: $appIcon")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val resourcePath = "music/branding/$appIconValue"
|
||||
|
||||
// change launcher icon.
|
||||
mipmapDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *mipmapIconResourceFileNames
|
||||
)
|
||||
}.let { resourceGroups ->
|
||||
// Change launcher icon.
|
||||
launcherIconResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$resourcePath/launcher", it)
|
||||
context.copyResources("$appIconResourcePath/launcher", it)
|
||||
}
|
||||
}
|
||||
|
||||
// change monochrome icon.
|
||||
// Change monochrome icon.
|
||||
arrayOf(
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
"ic_app_icons_themed_youtube_music.xml"
|
||||
)
|
||||
).forEach { resourceGroup ->
|
||||
context.copyResources("$resourcePath/monochrome", resourceGroup)
|
||||
context.copyResources("$appIconResourcePath/monochrome", resourceGroup)
|
||||
}
|
||||
|
||||
// Change header.
|
||||
if (ChangeHeader == true) {
|
||||
headerIconResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$appIconResourcePath/header", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change splash icon.
|
||||
if (ChangeSplashIcon == true) {
|
||||
splashIconResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$appIconResourcePath/splash", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: throw PatchException("Invalid app icon path.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,24 +20,26 @@ object CustomBrandingNamePatch : BaseResourcePatch(
|
||||
key = "AppNameNotification",
|
||||
default = APP_NAME_LAUNCHER,
|
||||
values = mapOf(
|
||||
"Full name" to APP_NAME_NOTIFICATION,
|
||||
"Short name" to APP_NAME_LAUNCHER
|
||||
"ReVanced Extended Music" to APP_NAME_NOTIFICATION,
|
||||
"RVX Music" to APP_NAME_LAUNCHER,
|
||||
"YouTube Music" to "YouTube Music",
|
||||
"YT Music" to "YT Music",
|
||||
),
|
||||
title = "App name in notification panel",
|
||||
description = "The name of the app as it appears in the notification panel.",
|
||||
required = true
|
||||
)
|
||||
|
||||
private val AppNameLauncher by stringPatchOption(
|
||||
key = "AppNameLauncher",
|
||||
default = APP_NAME_LAUNCHER,
|
||||
values = mapOf(
|
||||
"Full name" to APP_NAME_NOTIFICATION,
|
||||
"Short name" to APP_NAME_LAUNCHER
|
||||
"ReVanced Extended Music" to APP_NAME_NOTIFICATION,
|
||||
"RVX Music" to APP_NAME_LAUNCHER,
|
||||
"YouTube Music" to "YouTube Music",
|
||||
"YT Music" to "YT Music",
|
||||
),
|
||||
title = "App name in launcher",
|
||||
description = "The name of the app as it appears in the launcher.",
|
||||
required = true
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
@ -64,7 +66,7 @@ object CustomBrandingNamePatch : BaseResourcePatch(
|
||||
.appendChild(stringElement)
|
||||
}
|
||||
}
|
||||
} ?: throw PatchException("Invalid app name.")
|
||||
} ?: throw PatchException("Invalid app name.")
|
||||
} ?: throw PatchException("Invalid launcher name.")
|
||||
} ?: throw PatchException("Invalid notification name.")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
package app.revanced.patches.music.layout.translations
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.music.utils.settings.SettingsPatch
|
||||
import app.revanced.patches.shared.translations.APP_LANGUAGES
|
||||
import app.revanced.patches.shared.translations.TranslationsUtils.invoke
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
|
||||
@Suppress("unused")
|
||||
object TranslationsPatch : BaseResourcePatch(
|
||||
name = "Translations YouTube Music",
|
||||
description = "Add translations or remove string resources.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE
|
||||
) {
|
||||
// Array of supported translations, each represented by its language code.
|
||||
private val TRANSLATIONS = arrayOf(
|
||||
"bg-rBG", "bn", "cs-rCZ", "el-rGR", "es-rES", "fr-rFR", "hu-rHU", "id-rID", "in", "it-rIT",
|
||||
"ja-rJP", "ko-rKR", "nl-rNL", "pl-rPL", "pt-rBR", "ro-rRO", "ru-rRU", "tr-rTR", "uk-rUA",
|
||||
"vi-rVN", "zh-rCN", "zh-rTW"
|
||||
)
|
||||
|
||||
private var CustomTranslations by stringPatchOption(
|
||||
key = "CustomTranslations",
|
||||
default = "",
|
||||
title = "Custom translations",
|
||||
description = """
|
||||
The path to the 'strings.xml' file.
|
||||
Please note that applying the 'strings.xml' file will overwrite all existing language translations.
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
private var SelectedTranslations by stringPatchOption(
|
||||
key = "SelectedTranslations",
|
||||
default = TRANSLATIONS.joinToString(", "),
|
||||
title = "Translations to add",
|
||||
description = "A list of translations to be added for the RVX settings, separated by commas."
|
||||
)
|
||||
|
||||
private var SelectedStringResources by stringPatchOption(
|
||||
key = "SelectedStringResources",
|
||||
default = APP_LANGUAGES.joinToString(", "),
|
||||
title = "String resources to keep",
|
||||
description = """
|
||||
A list of string resources to be kept, separated by commas.
|
||||
String resources not in the list will be removed from the app.
|
||||
|
||||
Default string resource, English, is not removed.
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
context.invoke(
|
||||
CustomTranslations, SelectedTranslations, SelectedStringResources,
|
||||
TRANSLATIONS, "music"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package app.revanced.patches.music.misc.translations
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.music.utils.settings.SettingsPatch
|
||||
import app.revanced.patches.shared.translations.TranslationsUtils.copyXml
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
|
||||
@Suppress("unused")
|
||||
object TranslationsPatch : BaseResourcePatch(
|
||||
name = "Translations",
|
||||
description = "Adds Crowdin translations for YouTube Music.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE
|
||||
) {
|
||||
override fun execute(context: ResourceContext) {
|
||||
context.copyXml(
|
||||
"music",
|
||||
arrayOf(
|
||||
"bg-rBG",
|
||||
"bn",
|
||||
"cs-rCZ",
|
||||
"el-rGR",
|
||||
"es-rES",
|
||||
"fr-rFR",
|
||||
"hu-rHU",
|
||||
"id-rID",
|
||||
"in",
|
||||
"it-rIT",
|
||||
"ja-rJP",
|
||||
"ko-rKR",
|
||||
"nl-rNL",
|
||||
"pl-rPL",
|
||||
"pt-rBR",
|
||||
"ro-rRO",
|
||||
"ru-rRU",
|
||||
"tr-rTR",
|
||||
"uk-rUA",
|
||||
"vi-rVN",
|
||||
"zh-rCN",
|
||||
"zh-rTW"
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
@ -26,24 +26,80 @@ object SettingsPatch : BaseResourcePatch(
|
||||
compatiblePackages = COMPATIBLE_PACKAGE,
|
||||
requiresIntegrations = true
|
||||
), Closeable {
|
||||
private val THREAD_COUNT = Runtime.getRuntime().availableProcessors()
|
||||
private val threadPoolExecutor = Executors.newFixedThreadPool(THREAD_COUNT)
|
||||
|
||||
lateinit var contexts: ResourceContext
|
||||
internal var upward0636 = false
|
||||
internal var upward0642 = false
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
|
||||
/**
|
||||
* set resource context
|
||||
*/
|
||||
contexts = context
|
||||
|
||||
val resourceXmlFile = context["res/values/integers.xml"].readBytes()
|
||||
/**
|
||||
* set version info
|
||||
*/
|
||||
setVersionInfo()
|
||||
|
||||
for (threadIndex in 0 until THREAD_COUNT) {
|
||||
/**
|
||||
* copy strings
|
||||
*/
|
||||
context.copyXmlNode("music/settings/host", "values/strings.xml", "resources")
|
||||
|
||||
/**
|
||||
* hide divider
|
||||
*/
|
||||
val styleFile = context["res/values/styles.xml"]
|
||||
|
||||
styleFile.writeText(
|
||||
styleFile.readText()
|
||||
.replace(
|
||||
"allowDividerAbove\">true",
|
||||
"allowDividerAbove\">false"
|
||||
).replace(
|
||||
"allowDividerBelow\">true",
|
||||
"allowDividerBelow\">false"
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Copy arrays
|
||||
*/
|
||||
contexts.copyXmlNode("music/settings/host", "values/arrays.xml", "resources")
|
||||
|
||||
/**
|
||||
* Copy colors
|
||||
*/
|
||||
context.xmlEditor["res/values/colors.xml"].use { editor ->
|
||||
val resourcesNode = editor.file.getElementsByTagName("resources").item(0) as Element
|
||||
|
||||
for (i in 0 until resourcesNode.childNodes.length) {
|
||||
val node = resourcesNode.childNodes.item(i) as? Element ?: continue
|
||||
|
||||
node.textContent = when (node.getAttribute("name")) {
|
||||
"material_deep_teal_500" -> "@android:color/white"
|
||||
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.addRVXSettingsPreference()
|
||||
}
|
||||
|
||||
private fun setVersionInfo() {
|
||||
val threadCount = Runtime.getRuntime().availableProcessors()
|
||||
val threadPoolExecutor = Executors.newFixedThreadPool(threadCount)
|
||||
|
||||
val resourceXmlFile = contexts["res/values/integers.xml"].readBytes()
|
||||
|
||||
for (threadIndex in 0 until threadCount) {
|
||||
threadPoolExecutor.execute thread@{
|
||||
context.xmlEditor[resourceXmlFile.inputStream()].use { editor ->
|
||||
contexts.xmlEditor[resourceXmlFile.inputStream()].use { editor ->
|
||||
val resources = editor.file.documentElement.childNodes
|
||||
val resourcesLength = resources.length
|
||||
val jobSize = resourcesLength / THREAD_COUNT
|
||||
val jobSize = resourcesLength / threadCount
|
||||
|
||||
val batchStart = jobSize * threadIndex
|
||||
val batchEnd = jobSize * (threadIndex + 1)
|
||||
@ -71,47 +127,6 @@ object SettingsPatch : BaseResourcePatch(
|
||||
threadPoolExecutor
|
||||
.also { it.shutdown() }
|
||||
.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS)
|
||||
|
||||
/**
|
||||
* copy strings
|
||||
*/
|
||||
context.copyXmlNode("music/settings/host", "values/strings.xml", "resources")
|
||||
|
||||
/**
|
||||
* hide divider
|
||||
*/
|
||||
val styleFile = context["res/values/styles.xml"]
|
||||
|
||||
styleFile.writeText(
|
||||
styleFile.readText()
|
||||
.replace(
|
||||
"allowDividerAbove\">true",
|
||||
"allowDividerAbove\">false"
|
||||
).replace(
|
||||
"allowDividerBelow\">true",
|
||||
"allowDividerBelow\">false"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Copy colors
|
||||
*/
|
||||
context.xmlEditor["res/values/colors.xml"].use { editor ->
|
||||
val resourcesNode = editor.file.getElementsByTagName("resources").item(0) as Element
|
||||
|
||||
for (i in 0 until resourcesNode.childNodes.length) {
|
||||
val node = resourcesNode.childNodes.item(i) as? Element ?: continue
|
||||
|
||||
node.textContent = when (node.getAttribute("name")) {
|
||||
"material_deep_teal_500" -> "@android:color/white"
|
||||
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.addRVXSettingsPreference()
|
||||
}
|
||||
|
||||
internal fun addSwitchPreference(
|
||||
@ -162,11 +177,6 @@ object SettingsPatch : BaseResourcePatch(
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
/**
|
||||
* Copy arrays
|
||||
*/
|
||||
contexts.copyXmlNode("music/settings/host", "values/arrays.xml", "resources")
|
||||
|
||||
addPreferenceWithIntent(
|
||||
CategoryType.MISC,
|
||||
"revanced_extended_settings_import_export"
|
||||
|
@ -32,7 +32,7 @@ object CustomBrandingNamePatch : BaseResourcePatch(
|
||||
val appName = if (AppName != null) {
|
||||
AppName!!
|
||||
} else {
|
||||
println("WARNING: Invalid name name. Does not apply patches.")
|
||||
println("WARNING: Invalid app name. Does not apply patches.")
|
||||
ORIGINAL_APP_NAME
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ package app.revanced.patches.shared.elements
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Suppress("DEPRECATION", "unused")
|
||||
object StringsElementsUtils {
|
||||
|
||||
internal fun ResourceContext.removeStringsElements(
|
||||
|
@ -1,28 +1,172 @@
|
||||
package app.revanced.patches.shared.translations
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.util.inputStreamFromBundledResource
|
||||
import org.w3c.dom.Node
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object TranslationsUtils {
|
||||
internal fun ResourceContext.copyXml(
|
||||
|
||||
internal fun ResourceContext.invoke(
|
||||
customTranslations: String?,
|
||||
selectedTranslations: String?,
|
||||
selectedStringResources: String?,
|
||||
translationsArray: Array<String>,
|
||||
sourceDirectory: String
|
||||
) {
|
||||
// Check if the custom translation path is valid.
|
||||
customTranslations?.takeIf { it.isNotEmpty() }?.let { customLang ->
|
||||
try {
|
||||
val customLangFile = File(customLang)
|
||||
if (!customLangFile.exists() || !customLangFile.isFile || customLangFile.name != "strings.xml") {
|
||||
throw PatchException("Invalid custom language file: $customLang")
|
||||
}
|
||||
val resourceDirectory = this["res"].resolve("values")
|
||||
val destinationFile = resourceDirectory.resolve("strings.xml")
|
||||
|
||||
updateStringsXml(customLangFile, destinationFile)
|
||||
} catch (e: Exception) {
|
||||
// Exception is thrown if an invalid path is used in the patch option.
|
||||
throw PatchException("Invalid custom translations path: $customLang")
|
||||
}
|
||||
}?: run {
|
||||
// Process selected translations if no custom translation is set.
|
||||
val selectedTranslationsArray =
|
||||
selectedTranslations?.split(",")?.map { it.trim() }?.toTypedArray()
|
||||
?: throw PatchException("Invalid selected languages.")
|
||||
val filteredLanguages = translationsArray.filter { it in selectedTranslationsArray }.toTypedArray()
|
||||
copyXml(sourceDirectory, filteredLanguages)
|
||||
}
|
||||
|
||||
// Process selected string resources.
|
||||
val selectedStringResourcesArray =
|
||||
selectedStringResources?.split(",")?.map { it.trim() }?.toTypedArray()
|
||||
?: throw PatchException("Invalid selected string resources.")
|
||||
val filteredStringResources =
|
||||
APP_LANGUAGES.filter { it in selectedStringResourcesArray }.toTypedArray()
|
||||
val resourceDirectory = this["res"]
|
||||
|
||||
// Remove unselected app languages.
|
||||
APP_LANGUAGES.filter { it !in filteredStringResources }.forEach { language ->
|
||||
resourceDirectory.resolve("values-$language").takeIf { it.exists() && it.isDirectory }
|
||||
?.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to ResourceContext to copy XML translation files.
|
||||
*
|
||||
* @param sourceDirectory The source directory containing the translation files.
|
||||
* @param languageArray The array of language codes to process.
|
||||
*/
|
||||
private fun ResourceContext.copyXml(
|
||||
sourceDirectory: String,
|
||||
languageArray: Array<String>
|
||||
) {
|
||||
languageArray.forEach { language ->
|
||||
val directory = "values-$language-v21"
|
||||
this["res/$directory"].mkdir()
|
||||
inputStreamFromBundledResource(
|
||||
"$sourceDirectory/translations",
|
||||
"$language/strings.xml"
|
||||
)?.let { inputStream ->
|
||||
val directory = "values-$language-v21"
|
||||
val valuesV21Directory = this["res"].resolve(directory)
|
||||
if (!valuesV21Directory.isDirectory) Files.createDirectories(valuesV21Directory.toPath())
|
||||
|
||||
Files.copy(
|
||||
inputStreamFromBundledResource(
|
||||
"$sourceDirectory/translations",
|
||||
"$language/strings.xml"
|
||||
)!!,
|
||||
this["res"].resolve("$directory/strings.xml").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
Files.copy(
|
||||
inputStream,
|
||||
this["res"].resolve("$directory/strings.xml").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the contents of the destination strings.xml file by merging it with the source strings.xml file.
|
||||
*
|
||||
* This function reads both source and destination XML files, compares each <string> element by their
|
||||
* unique "name" attribute, and if a match is found, it replaces the content in the destination file with
|
||||
* the content from the source file.
|
||||
*
|
||||
* @param sourceFile The source strings.xml file containing new string values.
|
||||
* @param destinationFile The destination strings.xml file to be updated with values from the source file.
|
||||
*/
|
||||
private fun updateStringsXml(sourceFile: File, destinationFile: File) {
|
||||
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
||||
|
||||
// Parse the source and destination XML files into Document objects
|
||||
val sourceDoc = documentBuilder.parse(sourceFile)
|
||||
val destinationDoc = documentBuilder.parse(destinationFile)
|
||||
|
||||
val sourceStrings = sourceDoc.getElementsByTagName("string")
|
||||
val destinationStrings = destinationDoc.getElementsByTagName("string")
|
||||
|
||||
// Create a map to store the <string> elements from the source document by their "name" attribute
|
||||
val sourceMap = mutableMapOf<String, Node>()
|
||||
|
||||
// Populate the map with nodes from the source document
|
||||
for (i in 0 until sourceStrings.length) {
|
||||
val node = sourceStrings.item(i)
|
||||
val name = node.attributes.getNamedItem("name").nodeValue
|
||||
sourceMap[name] = node
|
||||
}
|
||||
|
||||
// Update the destination document with values from the source document
|
||||
for (i in 0 until destinationStrings.length) {
|
||||
val node = destinationStrings.item(i)
|
||||
val name = node.attributes.getNamedItem("name").nodeValue
|
||||
if (sourceMap.containsKey(name)) {
|
||||
node.textContent = sourceMap[name]?.textContent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the transformer for writing the updated document back to the file.
|
||||
* The transformer is configured to indent the output XML for better readability.
|
||||
*/
|
||||
val transformerFactory = TransformerFactory.newInstance()
|
||||
val transformer = transformerFactory.newTransformer()
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
|
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
|
||||
|
||||
val domSource = DOMSource(destinationDoc)
|
||||
val streamResult = StreamResult(destinationFile)
|
||||
transformer.transform(domSource, streamResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Array of all possible app languages.
|
||||
val APP_LANGUAGES = arrayOf(
|
||||
"af", "am", "ar", "ar-rXB", "as", "az",
|
||||
"b+es+419", "b+sr+Latn", "be", "bg", "bn", "bs",
|
||||
"ca", "cs",
|
||||
"da", "de",
|
||||
"el", "en-rAU", "en-rCA", "en-rGB", "en-rIN", "en-rXA", "en-rXC", "es", "es-rUS", "et", "eu",
|
||||
"fa", "fi", "fr", "fr-rCA",
|
||||
"gl", "gu",
|
||||
"hi", "hr", "hu", "hy",
|
||||
"id", "in", "is", "it", "iw",
|
||||
"ja",
|
||||
"ka", "kk", "km", "kn", "ko", "ky",
|
||||
"lo", "lt", "lv",
|
||||
"mk", "ml", "mn", "mr", "ms", "my",
|
||||
"nb", "ne", "nl", "no",
|
||||
"or",
|
||||
"pa", "pl", "pt", "pt-rBR", "pt-rPT",
|
||||
"ro", "ru",
|
||||
"si", "sk", "sl", "sq", "sr", "sv", "sw",
|
||||
"ta", "te", "th", "tl", "tr",
|
||||
"uk", "ur", "uz",
|
||||
"vi",
|
||||
"zh", "zh-rCN", "zh-rHK", "zh-rTW", "zu",
|
||||
)
|
||||
|
@ -0,0 +1,84 @@
|
||||
package app.revanced.patches.youtube.layout.actionbuttons
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.youtube.utils.settings.SettingsPatch
|
||||
import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.copyResources
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
|
||||
@Suppress("unused")
|
||||
object ShortsActionButtonsPatch : BaseResourcePatch(
|
||||
name = "Custom Shorts action buttons",
|
||||
description = "Changes, at compile time, the icon of the action buttons of the Shorts player.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE
|
||||
) {
|
||||
private const val DEFAULT_ICON_KEY = "Round"
|
||||
|
||||
private val IconType by stringPatchOption(
|
||||
key = "IconType",
|
||||
default = DEFAULT_ICON_KEY,
|
||||
values = mapOf(
|
||||
"Outline" to "outline",
|
||||
"OutlineCircle" to "outlinecircle",
|
||||
DEFAULT_ICON_KEY to "round"
|
||||
),
|
||||
title = "Shorts icon style ",
|
||||
description = "The style of the icons for the action buttons in the Shorts player."
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
IconType?.let { iconType ->
|
||||
val selectedIconType = iconType.lowercase()
|
||||
|
||||
arrayOf(
|
||||
"xxxhdpi",
|
||||
"xxhdpi",
|
||||
"xhdpi",
|
||||
"hdpi",
|
||||
"mdpi"
|
||||
).forEach { dpi ->
|
||||
context.copyResources(
|
||||
"youtube/shorts/actionbuttons/$selectedIconType",
|
||||
ResourceGroup(
|
||||
"drawable-$dpi",
|
||||
"ic_remix_filled_white_shadowed.webp",
|
||||
"ic_right_comment_shadowed.webp",
|
||||
"ic_right_dislike_off_shadowed.webp",
|
||||
"ic_right_dislike_on_shadowed.webp",
|
||||
"ic_right_like_off_shadowed.webp",
|
||||
"ic_right_like_on_shadowed.webp",
|
||||
"ic_right_share_shadowed.webp",
|
||||
|
||||
// for older versions only
|
||||
"ic_remix_filled_white_24.webp",
|
||||
"ic_right_dislike_on_32c.webp",
|
||||
"ic_right_like_on_32c.webp"
|
||||
),
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
"ic_right_comment_32c.xml",
|
||||
"ic_right_dislike_off_32c.xml",
|
||||
"ic_right_like_off_32c.xml",
|
||||
"ic_right_share_32c.xml"
|
||||
)
|
||||
)
|
||||
|
||||
context.copyResources(
|
||||
"youtube/shorts/actionbuttons/shared",
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
"reel_camera_bold_24dp.xml",
|
||||
"reel_more_vertical_bold_24dp.xml",
|
||||
"reel_search_bold_24dp.xml"
|
||||
)
|
||||
)
|
||||
}
|
||||
} ?: throw PatchException("Invalid icon type.")
|
||||
|
||||
SettingsPatch.updatePatchStatus(this)
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ object AnimatedLikePatch : BaseResourcePatch(
|
||||
* Copy json
|
||||
*/
|
||||
context.copyResources(
|
||||
"youtube/animated",
|
||||
"youtube/shorts/animated",
|
||||
ResourceGroup(
|
||||
"raw",
|
||||
"like_tap_feedback.json"
|
||||
|
@ -2,12 +2,14 @@ package app.revanced.patches.youtube.layout.branding.icon
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon
|
||||
import app.revanced.patches.youtube.utils.settings.SettingsPatch
|
||||
import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.copyResources
|
||||
import app.revanced.util.copyXmlNode
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
@ -17,47 +19,84 @@ object CustomBrandingIconPatch : BaseResourcePatch(
|
||||
name = "Custom branding icon YouTube",
|
||||
description = "Changes the YouTube app icon to the icon specified in options.json.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE
|
||||
compatiblePackages = COMPATIBLE_PACKAGE,
|
||||
) {
|
||||
private const val DEFAULT_ICON_KEY = "Revancify Blue"
|
||||
|
||||
private val availableIcon = mapOf(
|
||||
"AFN Blue" to "afn_blue",
|
||||
"AFN Red" to "afn_red",
|
||||
"MMT" to "mmt",
|
||||
DEFAULT_ICON_KEY to "revancify_blue",
|
||||
"Revancify Red" to "revancify_red"
|
||||
"Revancify Red" to "revancify_red",
|
||||
"YouTube" to "youtube"
|
||||
)
|
||||
|
||||
private val drawableIconResourceFileNames = arrayOf(
|
||||
"product_logo_youtube_color_24",
|
||||
"product_logo_youtube_color_36",
|
||||
"product_logo_youtube_color_144",
|
||||
"product_logo_youtube_color_192"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val drawableDirectories = arrayOf(
|
||||
private val sizeArray = arrayOf(
|
||||
"xxxhdpi",
|
||||
"xxhdpi",
|
||||
"xhdpi",
|
||||
"hdpi",
|
||||
"mdpi"
|
||||
).map { "drawable-$it" }
|
||||
)
|
||||
|
||||
private val mipmapIconResourceFileNames = arrayOf(
|
||||
private val drawableDirectories = sizeArray.map { "drawable-$it" }
|
||||
|
||||
private val mipmapDirectories = sizeArray.map { "mipmap-$it" }
|
||||
|
||||
private val headerIconResourceFileNames = arrayOf(
|
||||
"yt_premium_wordmark_header_dark",
|
||||
"yt_premium_wordmark_header_light",
|
||||
"yt_wordmark_header_dark",
|
||||
"yt_wordmark_header_light"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val launcherIconResourceFileNames = arrayOf(
|
||||
"adaptiveproduct_youtube_background_color_108",
|
||||
"adaptiveproduct_youtube_foreground_color_108",
|
||||
"ic_launcher",
|
||||
"ic_launcher_round"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private val mipmapDirectories = arrayOf(
|
||||
"xxxhdpi",
|
||||
"xxhdpi",
|
||||
"xhdpi",
|
||||
"hdpi",
|
||||
"mdpi"
|
||||
).map { "mipmap-$it" }
|
||||
private val splashIconResourceFileNames = arrayOf(
|
||||
"product_logo_youtube_color_24",
|
||||
"product_logo_youtube_color_36",
|
||||
"product_logo_youtube_color_144",
|
||||
"product_logo_youtube_color_192"
|
||||
).map { "$it.png" }.toTypedArray()
|
||||
|
||||
private var AppIcon by stringPatchOption(
|
||||
private val oldSplashAnimationResourceFileNames = arrayOf(
|
||||
"\$\$avd_anim__1__0",
|
||||
"\$\$avd_anim__1__1",
|
||||
"\$\$avd_anim__2__0",
|
||||
"\$\$avd_anim__2__1",
|
||||
"\$\$avd_anim__3__0",
|
||||
"\$\$avd_anim__3__1",
|
||||
"\$avd_anim__0",
|
||||
"\$avd_anim__1",
|
||||
"\$avd_anim__2",
|
||||
"\$avd_anim__3",
|
||||
"\$avd_anim__4",
|
||||
"avd_anim"
|
||||
).map { "$it.xml" }.toTypedArray()
|
||||
|
||||
private fun List<String>.getResourceGroup(fileNames: Array<String>) = map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *fileNames
|
||||
)
|
||||
}
|
||||
|
||||
private val headerIconResourceGroups = drawableDirectories.getResourceGroup(headerIconResourceFileNames)
|
||||
|
||||
private val launcherIconResourceGroups = mipmapDirectories.getResourceGroup(launcherIconResourceFileNames)
|
||||
|
||||
private val splashIconResourceGroups = drawableDirectories.getResourceGroup(splashIconResourceFileNames)
|
||||
|
||||
private val oldSplashAnimationResourceGroups = listOf("drawable").getResourceGroup(oldSplashAnimationResourceFileNames)
|
||||
|
||||
// region patch option
|
||||
|
||||
val AppIcon by stringPatchOption(
|
||||
key = "AppIcon",
|
||||
default = DEFAULT_ICON_KEY,
|
||||
values = availableIcon,
|
||||
@ -69,22 +108,45 @@ object CustomBrandingIconPatch : BaseResourcePatch(
|
||||
|
||||
Each of these folders has to have the following files:
|
||||
|
||||
${mipmapIconResourceFileNames.joinToString("\n") { "- $it" }}
|
||||
${launcherIconResourceFileNames.joinToString("\n") { "- $it" }}
|
||||
"""
|
||||
.split("\n")
|
||||
.joinToString("\n") { it.trimIndent() } // Remove the leading whitespace from each line.
|
||||
.trimIndent(), // Remove the leading newline.
|
||||
)
|
||||
|
||||
private val ChangeHeader by booleanPatchOption(
|
||||
key = "ChangeHeader",
|
||||
default = false,
|
||||
title = "Change header",
|
||||
description = "Apply the custom branding icon to the header."
|
||||
)
|
||||
|
||||
private val ChangeSplashIcon by booleanPatchOption(
|
||||
key = "ChangeSplashIcon",
|
||||
default = true,
|
||||
title = "Change splash icons",
|
||||
description = "Apply the custom branding icon to the splash screen."
|
||||
)
|
||||
|
||||
private val RestoreOldSplashAnimation by booleanPatchOption(
|
||||
key = "RestoreOldSplashAnimation",
|
||||
default = false,
|
||||
title = "Restore old splash animation",
|
||||
description = "Restores old style splash animation."
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
AppIcon?.let { appIcon ->
|
||||
val appIconValue = appIcon.lowercase().replace(" ", "_")
|
||||
val appIconResourcePath = "youtube/branding/$appIconValue"
|
||||
val stockResourcePath = "youtube/branding/stock"
|
||||
|
||||
// Check if a custom path is used in the patch options.
|
||||
if (!availableIcon.containsValue(appIconValue)) {
|
||||
mipmapDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *mipmapIconResourceFileNames
|
||||
)
|
||||
}.let { resourceGroups ->
|
||||
launcherIconResourceGroups.let { resourceGroups ->
|
||||
try {
|
||||
val path = File(appIcon)
|
||||
val resourceDirectory = context["res"]
|
||||
@ -100,48 +162,63 @@ object CustomBrandingIconPatch : BaseResourcePatch(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context.updatePatchStatusIcon("custom")
|
||||
} catch (_: Exception) {
|
||||
// Exception is thrown if an invalid path is used in the patch option.
|
||||
throw PatchException("Invalid app icon path: $appIcon")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val resourcePath = "youtube/branding/$appIconValue"
|
||||
|
||||
// change launcher icon.
|
||||
mipmapDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *mipmapIconResourceFileNames
|
||||
)
|
||||
}.let { resourceGroups ->
|
||||
// Change launcher icon.
|
||||
launcherIconResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$resourcePath/launcher", it)
|
||||
context.copyResources("$appIconResourcePath/launcher", it)
|
||||
}
|
||||
}
|
||||
|
||||
// change splash icon.
|
||||
drawableDirectories.map { directory ->
|
||||
ResourceGroup(
|
||||
directory, *drawableIconResourceFileNames
|
||||
)
|
||||
}.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$resourcePath/splash", it)
|
||||
}
|
||||
}
|
||||
|
||||
// change monochrome icon.
|
||||
// Change monochrome icon.
|
||||
arrayOf(
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
"adaptive_monochrome_ic_youtube_launcher.xml"
|
||||
)
|
||||
).forEach { resourceGroup ->
|
||||
context.copyResources("$resourcePath/monochrome", resourceGroup)
|
||||
context.copyResources("$appIconResourcePath/monochrome", resourceGroup)
|
||||
}
|
||||
|
||||
// Change header.
|
||||
if (ChangeHeader == true) {
|
||||
headerIconResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$appIconResourcePath/header", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change splash icon.
|
||||
if (ChangeSplashIcon == true) {
|
||||
splashIconResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$appIconResourcePath/splash", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change splash screen.
|
||||
if (RestoreOldSplashAnimation == true) {
|
||||
oldSplashAnimationResourceGroups.let { resourceGroups ->
|
||||
resourceGroups.forEach {
|
||||
context.copyResources("$stockResourcePath/splash", it)
|
||||
context.copyResources("$appIconResourcePath/splash", it)
|
||||
}
|
||||
}
|
||||
|
||||
context.copyXmlNode("$stockResourcePath/splash", "values-v31/styles.xml", "resources")
|
||||
}
|
||||
|
||||
context.updatePatchStatusIcon(appIconValue)
|
||||
}
|
||||
} ?: throw PatchException("Invalid app icon path.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +22,13 @@ object CustomBrandingNamePatch : BaseResourcePatch(
|
||||
key = "AppName",
|
||||
default = APP_NAME,
|
||||
values = mapOf(
|
||||
"Full name" to "ReVanced Extended",
|
||||
"Short name" to APP_NAME
|
||||
"ReVanced Extended" to "ReVanced Extended",
|
||||
"RVX" to APP_NAME,
|
||||
"YouTube RVX" to "YouTube RVX",
|
||||
"YouTube" to "YouTube",
|
||||
),
|
||||
title = "App name",
|
||||
description = "The name of the app.",
|
||||
required = true
|
||||
description = "The name of the app."
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
|
@ -1,42 +1,61 @@
|
||||
package app.revanced.patches.youtube.layout.translations
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patches.shared.translations.TranslationsUtils.copyXml
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.shared.translations.APP_LANGUAGES
|
||||
import app.revanced.patches.shared.translations.TranslationsUtils.invoke
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.youtube.utils.settings.SettingsPatch
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
|
||||
@Suppress("unused")
|
||||
object TranslationsPatch : BaseResourcePatch(
|
||||
name = "Translations",
|
||||
description = "Adds Crowdin translations for YouTube.",
|
||||
name = "Translations YouTube",
|
||||
description = "Add translations or remove string resources.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE
|
||||
) {
|
||||
override fun execute(context: ResourceContext) {
|
||||
// Array of supported translations, each represented by its language code.
|
||||
private val TRANSLATIONS = arrayOf(
|
||||
"ar", "el-rGR", "es-rES", "fr-rFR", "hu-rHU", "it-rIT", "ja-rJP", "ko-rKR", "pl-rPL",
|
||||
"pt-rBR", "ru-rRU", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
)
|
||||
|
||||
context.copyXml(
|
||||
"youtube",
|
||||
arrayOf(
|
||||
"ar",
|
||||
"el-rGR",
|
||||
"es-rES",
|
||||
"fr-rFR",
|
||||
"hu-rHU",
|
||||
"it-rIT",
|
||||
"ja-rJP",
|
||||
"ko-rKR",
|
||||
"pl-rPL",
|
||||
"pt-rBR",
|
||||
"ru-rRU",
|
||||
"tr-rTR",
|
||||
"uk-rUA",
|
||||
"vi-rVN",
|
||||
"zh-rCN",
|
||||
"zh-rTW"
|
||||
)
|
||||
private var CustomTranslations by stringPatchOption(
|
||||
key = "CustomTranslations",
|
||||
default = "",
|
||||
title = "Custom translations",
|
||||
description = """
|
||||
The path to the 'strings.xml' file.
|
||||
Please note that applying the 'strings.xml' file will overwrite all existing translations.
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
private var SelectedTranslations by stringPatchOption(
|
||||
key = "SelectedTranslations",
|
||||
default = TRANSLATIONS.joinToString(", "),
|
||||
title = "Translations to add",
|
||||
description = "A list of translations to be added for the RVX settings, separated by commas."
|
||||
)
|
||||
|
||||
private var SelectedStringResources by stringPatchOption(
|
||||
key = "SelectedStringResources",
|
||||
default = APP_LANGUAGES.joinToString(", "),
|
||||
title = "String resources to keep",
|
||||
description = """
|
||||
A list of string resources to be kept, separated by commas.
|
||||
String resources not in the list will be removed from the app.
|
||||
|
||||
Default string resource, English, is not removed.
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
context.invoke(
|
||||
CustomTranslations, SelectedTranslations, SelectedStringResources,
|
||||
TRANSLATIONS, "youtube"
|
||||
)
|
||||
|
||||
SettingsPatch.updatePatchStatus(this)
|
||||
SettingsPatch.updatePatchStatus("Translations")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,377 @@
|
||||
package app.revanced.patches.youtube.layout.visual
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.youtube.layout.branding.icon.CustomBrandingIconPatch
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.youtube.utils.settings.SettingsPatch
|
||||
import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.copyResources
|
||||
import app.revanced.util.doRecursively
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
import org.w3c.dom.Element
|
||||
|
||||
@Suppress("DEPRECATION", "unused")
|
||||
object VisualPreferencesIconsPatch : BaseResourcePatch(
|
||||
name = "Visual preferences icons",
|
||||
description = "Adds icons to specific preferences in the settings.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE,
|
||||
use = false
|
||||
) {
|
||||
private const val DEFAULT_ICON_KEY = "Extension"
|
||||
|
||||
private val RVXSettingsMenuIcon by stringPatchOption(
|
||||
key = "RVXSettingsMenuIcon",
|
||||
default = DEFAULT_ICON_KEY,
|
||||
values = mapOf(
|
||||
"Custom branding icon" to "custom_branding_icon",
|
||||
DEFAULT_ICON_KEY to "extension",
|
||||
"Gear" to "gear",
|
||||
"ReVanced" to "revanced",
|
||||
"ReVanced Colored" to "revanced_colored",
|
||||
),
|
||||
title = "RVX settings menu icon",
|
||||
description = "Apply different icons for RVX settings menu."
|
||||
)
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
|
||||
// region copy shared resources.
|
||||
|
||||
arrayOf(
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
*preferenceIcon.values.map { "$it.xml" }.toTypedArray()
|
||||
),
|
||||
ResourceGroup(
|
||||
"drawable-xxhdpi",
|
||||
"$emptyIcon.png"
|
||||
),
|
||||
).forEach { resourceGroup ->
|
||||
context.copyResources("youtube/visual/shared", resourceGroup)
|
||||
}
|
||||
|
||||
// endregion.
|
||||
|
||||
// region copy RVX settings menu icon.
|
||||
|
||||
RVXSettingsMenuIcon?.lowercase()?.replace(" ", "_")?.let { selectedIconType ->
|
||||
CustomBrandingIconPatch.AppIcon?.lowercase()?.replace(" ", "_")?.let { appIconValue ->
|
||||
val fallbackIconPath = "youtube/visual/icons/extension"
|
||||
val iconPath = when (selectedIconType) {
|
||||
"custom_branding_icon" -> "youtube/branding/$appIconValue/settings"
|
||||
else -> "youtube/visual/icons/$selectedIconType"
|
||||
}
|
||||
val resourceGroup = ResourceGroup(
|
||||
"drawable",
|
||||
"revanced_extended_settings_key_icon.xml"
|
||||
)
|
||||
|
||||
try {
|
||||
context.copyResources(iconPath, resourceGroup)
|
||||
} catch (_: Exception) {
|
||||
// Ignore if resource copy fails
|
||||
|
||||
// Add a fallback extended icon
|
||||
// It's needed if someone provides custom path to icon(s) folder
|
||||
// but custom branding icons for Extended setting are predefined,
|
||||
// so it won't copy custom branding icon
|
||||
// and will raise an error without fallback icon
|
||||
context.copyResources(fallbackIconPath, resourceGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion.
|
||||
|
||||
// region set visual preferences icon.
|
||||
|
||||
arrayOf(
|
||||
"res/xml/revanced_prefs.xml",
|
||||
"res/xml/settings_fragment.xml"
|
||||
).forEach { xmlFile ->
|
||||
context.xmlEditor[xmlFile].use { editor ->
|
||||
editor.file.doRecursively loop@{ node ->
|
||||
if (node !is Element) return@loop
|
||||
|
||||
node.getAttributeNode("android:key")
|
||||
?.textContent
|
||||
?.removePrefix("@string/")
|
||||
?.let { title ->
|
||||
val drawableName = when (title) {
|
||||
in preferenceKey -> preferenceIcon[title]
|
||||
|
||||
// Add custom RVX settings menu icon
|
||||
in intentKey -> intentIcon[title]
|
||||
in emptyTitles -> emptyIcon
|
||||
else -> null
|
||||
}
|
||||
drawableName?.let {
|
||||
node.setAttribute("android:icon", "@drawable/$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion.
|
||||
|
||||
SettingsPatch.updatePatchStatus(this)
|
||||
}
|
||||
|
||||
// region preference key and icon.
|
||||
|
||||
private val preferenceKey = setOf(
|
||||
// Main settings (sorted as displayed in the settings)
|
||||
"parent_tools_key",
|
||||
"general_key",
|
||||
"account_switcher_key",
|
||||
"data_saving_settings_key",
|
||||
"auto_play_key",
|
||||
"video_quality_settings_key",
|
||||
"offline_key",
|
||||
"pair_with_tv_key",
|
||||
"history_key",
|
||||
"your_data_key",
|
||||
"privacy_key",
|
||||
"premium_early_access_browse_page_key",
|
||||
"subscription_product_setting_key",
|
||||
"billing_and_payment_key",
|
||||
"notification_key",
|
||||
"connected_accounts_browse_page_key",
|
||||
"live_chat_key",
|
||||
"captions_key",
|
||||
"accessibility_settings_key",
|
||||
"about_key",
|
||||
|
||||
// Main RVX settings (sorted as displayed in the settings)
|
||||
"revanced_preference_screen_ads",
|
||||
"revanced_preference_screen_alt_thumbnails",
|
||||
"revanced_preference_screen_feed",
|
||||
"revanced_preference_screen_general",
|
||||
"revanced_preference_screen_player",
|
||||
"revanced_preference_screen_shorts",
|
||||
"revanced_preference_screen_swipe_controls",
|
||||
"revanced_preference_screen_video",
|
||||
"revanced_preference_screen_ryd",
|
||||
"revanced_preference_screen_sb",
|
||||
"revanced_preference_screen_misc",
|
||||
|
||||
// Internal RVX settings (items without prefix are listed first, others are sorted alphabetically)
|
||||
"gms_core_settings",
|
||||
"sb_enable_create_segment",
|
||||
"sb_enable_voting",
|
||||
|
||||
"revanced_alt_thumbnail_home",
|
||||
"revanced_alt_thumbnail_library",
|
||||
"revanced_alt_thumbnail_player",
|
||||
"revanced_alt_thumbnail_search",
|
||||
"revanced_alt_thumbnail_subscriptions",
|
||||
"revanced_change_shorts_repeat_state",
|
||||
"revanced_custom_player_overlay_opacity",
|
||||
"revanced_default_app_settings",
|
||||
"revanced_default_playback_speed",
|
||||
"revanced_default_video_quality_wifi",
|
||||
"revanced_disable_hdr_auto_brightness",
|
||||
"revanced_disable_hdr_video",
|
||||
"revanced_disable_quic_protocol",
|
||||
"revanced_enable_debug_logging",
|
||||
"revanced_enable_default_playback_speed_shorts",
|
||||
"revanced_enable_external_browser",
|
||||
"revanced_enable_old_quality_layout",
|
||||
"revanced_enable_open_links_directly",
|
||||
"revanced_enable_swipe_brightness",
|
||||
"revanced_enable_swipe_haptic_feedback",
|
||||
"revanced_enable_swipe_lowest_value_auto_brightness",
|
||||
"revanced_enable_swipe_press_to_engage",
|
||||
"revanced_enable_swipe_volume",
|
||||
"revanced_enable_watch_panel_gestures",
|
||||
"revanced_hide_clip_button",
|
||||
"revanced_hide_download_button",
|
||||
"revanced_hide_keyword_content_comments",
|
||||
"revanced_hide_keyword_content_home",
|
||||
"revanced_hide_keyword_content_search",
|
||||
"revanced_hide_keyword_content_subscriptions",
|
||||
"revanced_hide_like_dislike_button",
|
||||
"revanced_hide_navigation_create_button",
|
||||
"revanced_hide_navigation_home_button",
|
||||
"revanced_hide_navigation_library_button",
|
||||
"revanced_hide_navigation_notifications_button",
|
||||
"revanced_hide_navigation_shorts_button",
|
||||
"revanced_hide_navigation_subscriptions_button",
|
||||
"revanced_hide_player_autoplay_button",
|
||||
"revanced_hide_player_captions_button",
|
||||
"revanced_hide_player_cast_button",
|
||||
"revanced_hide_player_collapse_button",
|
||||
"revanced_hide_player_flyout_menu_ambient_mode",
|
||||
"revanced_hide_player_flyout_menu_audio_track",
|
||||
"revanced_hide_player_flyout_menu_captions",
|
||||
"revanced_hide_player_flyout_menu_help",
|
||||
"revanced_hide_player_flyout_menu_lock_screen",
|
||||
"revanced_hide_player_flyout_menu_loop_video",
|
||||
"revanced_hide_player_flyout_menu_more_info",
|
||||
"revanced_hide_player_flyout_menu_playback_speed",
|
||||
"revanced_hide_player_flyout_menu_quality_footer",
|
||||
"revanced_hide_player_flyout_menu_report",
|
||||
"revanced_hide_player_flyout_menu_stable_volume",
|
||||
"revanced_hide_player_flyout_menu_stats_for_nerds",
|
||||
"revanced_hide_player_flyout_menu_watch_in_vr",
|
||||
"revanced_hide_player_fullscreen_button",
|
||||
"revanced_hide_player_previous_next_button",
|
||||
"revanced_hide_player_youtube_music_button",
|
||||
"revanced_hide_playlist_button",
|
||||
"revanced_hide_quick_actions_comment_button",
|
||||
"revanced_hide_quick_actions_dislike_button",
|
||||
"revanced_hide_quick_actions_like_button",
|
||||
"revanced_hide_quick_actions_live_chat_button",
|
||||
"revanced_hide_quick_actions_more_button",
|
||||
"revanced_hide_quick_actions_save_to_playlist_button",
|
||||
"revanced_hide_quick_actions_share_button",
|
||||
"revanced_hide_remix_button",
|
||||
"revanced_hide_report_button",
|
||||
"revanced_hide_share_button",
|
||||
"revanced_hide_shorts_comments_button",
|
||||
"revanced_hide_shorts_dislike_button",
|
||||
"revanced_hide_shorts_like_button",
|
||||
"revanced_hide_shorts_navigation_bar",
|
||||
"revanced_hide_shorts_remix_button",
|
||||
"revanced_hide_shorts_share_button",
|
||||
"revanced_hide_shorts_shelf_history",
|
||||
"revanced_hide_shorts_shelf_home_related_videos",
|
||||
"revanced_hide_shorts_shelf_search",
|
||||
"revanced_hide_shorts_shelf_subscriptions",
|
||||
"revanced_hide_shorts_toolbar",
|
||||
"revanced_overlay_button_always_repeat",
|
||||
"revanced_overlay_button_copy_video_url",
|
||||
"revanced_overlay_button_copy_video_url_timestamp",
|
||||
"revanced_overlay_button_external_downloader",
|
||||
"revanced_overlay_button_speed_dialog",
|
||||
"revanced_overlay_button_time_ordered_playlist",
|
||||
"revanced_overlay_button_whitelist",
|
||||
"revanced_preference_screen_account_menu",
|
||||
"revanced_preference_screen_action_buttons",
|
||||
"revanced_preference_screen_ambient_mode",
|
||||
"revanced_preference_screen_category_bar",
|
||||
"revanced_preference_screen_channel_bar",
|
||||
"revanced_preference_screen_channel_profile",
|
||||
"revanced_preference_screen_comments",
|
||||
"revanced_preference_screen_community_posts",
|
||||
"revanced_preference_screen_custom_filter",
|
||||
"revanced_preference_screen_feed_flyout_menu",
|
||||
"revanced_preference_screen_fullscreen",
|
||||
"revanced_preference_screen_haptic_feedback",
|
||||
"revanced_preference_screen_import_export",
|
||||
"revanced_preference_screen_navigation_buttons",
|
||||
"revanced_preference_screen_patch_information",
|
||||
"revanced_preference_screen_player_buttons",
|
||||
"revanced_preference_screen_player_flyout_menu",
|
||||
"revanced_preference_screen_seekbar",
|
||||
"revanced_preference_screen_settings_menu",
|
||||
"revanced_preference_screen_shorts_player",
|
||||
"revanced_preference_screen_spoof_client",
|
||||
"revanced_preference_screen_toolbar",
|
||||
"revanced_preference_screen_video_description",
|
||||
"revanced_preference_screen_video_filter",
|
||||
"revanced_sanitize_sharing_links",
|
||||
"revanced_swipe_gestures_lock_mode",
|
||||
"revanced_swipe_magnitude_threshold",
|
||||
"revanced_swipe_overlay_background_alpha",
|
||||
"revanced_swipe_overlay_rect_size",
|
||||
"revanced_swipe_overlay_text_size",
|
||||
"revanced_swipe_overlay_timeout",
|
||||
"revanced_switch_create_with_notifications_button",
|
||||
"revanced_change_player_flyout_menu_toggle",
|
||||
)
|
||||
|
||||
private val intentKey = setOf(
|
||||
"revanced_extended_settings_key",
|
||||
)
|
||||
|
||||
private val emptyTitles = setOf(
|
||||
"revanced_custom_playback_speeds",
|
||||
"revanced_custom_playback_speed_menu_type",
|
||||
"revanced_default_video_quality_mobile",
|
||||
"revanced_disable_default_playback_speed_live",
|
||||
"revanced_enable_custom_playback_speed",
|
||||
"revanced_external_downloader_package_name",
|
||||
"revanced_hide_shorts_comments_disabled_button",
|
||||
"revanced_hide_player_flyout_menu_captions_footer",
|
||||
"revanced_remember_playback_speed_last_selected",
|
||||
"revanced_remember_video_quality_last_selected",
|
||||
"revanced_restore_old_video_quality_menu",
|
||||
"revanced_enable_debug_buffer_logging",
|
||||
"revanced_whitelist_settings",
|
||||
)
|
||||
|
||||
// A lot of mappings here.
|
||||
// The performance impact should be negligible in this context,
|
||||
// as the operations involved are not computationally intensive.
|
||||
private val preferenceIcon = preferenceKey.associateWith { title ->
|
||||
when (title) {
|
||||
// Main RVX settings
|
||||
"revanced_preference_screen_general" -> "general_key_icon"
|
||||
"revanced_preference_screen_sb" -> "sb_enable_create_segment_icon"
|
||||
|
||||
// Internal RVX settings
|
||||
"revanced_alt_thumbnail_home" -> "revanced_hide_navigation_home_button_icon"
|
||||
"revanced_alt_thumbnail_library" -> "revanced_preference_screen_video_icon"
|
||||
"revanced_alt_thumbnail_player" -> "revanced_preference_screen_player_icon"
|
||||
"revanced_alt_thumbnail_search" -> "revanced_hide_shorts_shelf_search_icon"
|
||||
"revanced_alt_thumbnail_subscriptions" -> "revanced_hide_navigation_subscriptions_button_icon"
|
||||
"revanced_custom_player_overlay_opacity" -> "revanced_swipe_overlay_background_alpha_icon"
|
||||
"revanced_default_app_settings" -> "revanced_preference_screen_settings_menu_icon"
|
||||
"revanced_default_playback_speed" -> "revanced_overlay_button_speed_dialog_icon"
|
||||
"revanced_enable_old_quality_layout" -> "revanced_default_video_quality_wifi_icon"
|
||||
"revanced_enable_watch_panel_gestures" -> "revanced_preference_screen_swipe_controls_icon"
|
||||
"revanced_hide_download_button" -> "revanced_overlay_button_external_downloader_icon"
|
||||
"revanced_hide_keyword_content_comments" -> "revanced_hide_quick_actions_comment_button_icon"
|
||||
"revanced_hide_keyword_content_home" -> "revanced_hide_navigation_home_button_icon"
|
||||
"revanced_hide_keyword_content_search" -> "revanced_hide_shorts_shelf_search_icon"
|
||||
"revanced_hide_keyword_content_subscriptions" -> "revanced_hide_navigation_subscriptions_button_icon"
|
||||
"revanced_hide_like_dislike_button" -> "sb_enable_voting_icon"
|
||||
"revanced_hide_navigation_library_button" -> "revanced_preference_screen_video_icon"
|
||||
"revanced_hide_navigation_notifications_button" -> "notification_key_icon"
|
||||
"revanced_hide_navigation_shorts_button" -> "revanced_preference_screen_shorts_icon"
|
||||
"revanced_hide_player_autoplay_button" -> "revanced_change_player_flyout_menu_toggle_icon"
|
||||
"revanced_hide_player_captions_button" -> "captions_key_icon"
|
||||
"revanced_hide_player_flyout_menu_ambient_mode" -> "revanced_preference_screen_ambient_mode_icon"
|
||||
"revanced_hide_player_flyout_menu_captions" -> "captions_key_icon"
|
||||
"revanced_hide_player_flyout_menu_loop_video" -> "revanced_overlay_button_always_repeat_icon"
|
||||
"revanced_hide_player_flyout_menu_more_info" -> "about_key_icon"
|
||||
"revanced_hide_player_flyout_menu_quality_footer" -> "revanced_default_video_quality_wifi_icon"
|
||||
"revanced_hide_player_flyout_menu_report" -> "revanced_hide_report_button_icon"
|
||||
"revanced_hide_player_fullscreen_button" -> "revanced_preference_screen_fullscreen_icon"
|
||||
"revanced_hide_quick_actions_dislike_button" -> "revanced_preference_screen_ryd_icon"
|
||||
"revanced_hide_quick_actions_live_chat_button" -> "live_chat_key_icon"
|
||||
"revanced_hide_quick_actions_save_to_playlist_button" -> "revanced_hide_playlist_button_icon"
|
||||
"revanced_hide_quick_actions_share_button" -> "revanced_hide_shorts_share_button_icon"
|
||||
"revanced_hide_remix_button" -> "revanced_hide_shorts_remix_button_icon"
|
||||
"revanced_hide_share_button" -> "revanced_hide_shorts_share_button_icon"
|
||||
"revanced_hide_shorts_comments_button" -> "revanced_hide_quick_actions_comment_button_icon"
|
||||
"revanced_hide_shorts_dislike_button" -> "revanced_preference_screen_ryd_icon"
|
||||
"revanced_hide_shorts_like_button" -> "revanced_hide_quick_actions_like_button_icon"
|
||||
"revanced_hide_shorts_navigation_bar" -> "revanced_preference_screen_navigation_buttons_icon"
|
||||
"revanced_hide_shorts_shelf_home_related_videos" -> "revanced_hide_navigation_home_button_icon"
|
||||
"revanced_hide_shorts_shelf_subscriptions" -> "revanced_hide_navigation_subscriptions_button_icon"
|
||||
"revanced_hide_shorts_toolbar" -> "revanced_preference_screen_toolbar_icon"
|
||||
"revanced_preference_screen_account_menu" -> "account_switcher_key_icon"
|
||||
"revanced_preference_screen_channel_bar" -> "account_switcher_key_icon"
|
||||
"revanced_preference_screen_channel_profile" -> "account_switcher_key_icon"
|
||||
"revanced_preference_screen_comments" -> "revanced_hide_quick_actions_comment_button_icon"
|
||||
"revanced_preference_screen_feed_flyout_menu" -> "revanced_preference_screen_player_flyout_menu_icon"
|
||||
"revanced_preference_screen_haptic_feedback" -> "revanced_enable_swipe_haptic_feedback_icon"
|
||||
"revanced_preference_screen_patch_information" -> "about_key_icon"
|
||||
"revanced_preference_screen_shorts_player" -> "revanced_preference_screen_shorts_icon"
|
||||
"revanced_preference_screen_video_filter" -> "revanced_preference_screen_video_icon"
|
||||
"revanced_swipe_gestures_lock_mode" -> "revanced_hide_player_flyout_menu_lock_screen_icon"
|
||||
"revanced_disable_hdr_auto_brightness" -> "revanced_disable_hdr_video_icon"
|
||||
else -> "${title}_icon"
|
||||
}
|
||||
}
|
||||
private val intentIcon = intentKey.associateWith { "${it}_icon" }
|
||||
private const val emptyIcon = "empty_icon"
|
||||
|
||||
// endregion.
|
||||
|
||||
}
|
@ -299,12 +299,14 @@ object ShortsComponentPatch : BaseBytecodePatch(
|
||||
/**
|
||||
* Add settings
|
||||
*/
|
||||
SettingsPatch.addPreference(
|
||||
arrayOf(
|
||||
"PREFERENCE_SCREEN: SHORTS",
|
||||
"SETTINGS: SHORTS_COMPONENTS"
|
||||
)
|
||||
var settingArray = arrayOf(
|
||||
"PREFERENCE_SCREEN: SHORTS",
|
||||
"SETTINGS: SHORTS_COMPONENTS"
|
||||
)
|
||||
if (SettingsPatch.upward1834) {
|
||||
settingArray += "SETTINGS: HIDE_SHORTS_COMMENTS_DISABLED_BUTTON"
|
||||
}
|
||||
SettingsPatch.addPreference(settingArray)
|
||||
|
||||
SettingsPatch.updatePatchStatus(this)
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
package app.revanced.patches.youtube.shorts.outlinebutton
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
import app.revanced.patches.youtube.utils.settings.SettingsPatch
|
||||
import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.copyResources
|
||||
import app.revanced.util.patch.BaseResourcePatch
|
||||
|
||||
@Suppress("unused")
|
||||
object ShortsOutlineButtonPatch : BaseResourcePatch(
|
||||
name = "Shorts outline button",
|
||||
description = "Applies, at compile time, the outline icon to the action buttons in the Shorts player.",
|
||||
dependencies = setOf(SettingsPatch::class),
|
||||
compatiblePackages = COMPATIBLE_PACKAGE
|
||||
) {
|
||||
override fun execute(context: ResourceContext) {
|
||||
|
||||
arrayOf(
|
||||
"xxxhdpi",
|
||||
"xxhdpi",
|
||||
"xhdpi",
|
||||
"hdpi",
|
||||
"mdpi"
|
||||
).forEach { dpi ->
|
||||
context.copyResources(
|
||||
"youtube/shorts/outline",
|
||||
ResourceGroup(
|
||||
"drawable-$dpi",
|
||||
"ic_remix_filled_white_24.webp",
|
||||
"ic_remix_filled_white_shadowed.webp",
|
||||
"ic_right_comment_shadowed.webp",
|
||||
"ic_right_dislike_off_shadowed.webp",
|
||||
"ic_right_dislike_on_32c.webp",
|
||||
"ic_right_dislike_on_shadowed.webp",
|
||||
"ic_right_like_off_shadowed.webp",
|
||||
"ic_right_like_on_32c.webp",
|
||||
"ic_right_like_on_shadowed.webp",
|
||||
"ic_right_share_shadowed.webp"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
arrayOf(
|
||||
// Shorts outline icons for older versions of YouTube
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
"ic_right_comment_32c.xml",
|
||||
"ic_right_dislike_off_32c.xml",
|
||||
"ic_right_like_off_32c.xml",
|
||||
"ic_right_share_32c.xml"
|
||||
)
|
||||
).forEach { resourceGroup ->
|
||||
context.copyResources("youtube/shorts/outline", resourceGroup)
|
||||
}
|
||||
|
||||
SettingsPatch.updatePatchStatus(this)
|
||||
|
||||
}
|
||||
}
|
@ -152,7 +152,7 @@ object SwipeControlsPatch : BaseBytecodePatch(
|
||||
SettingsPatch.addPreference(
|
||||
arrayOf(
|
||||
"PREFERENCE_CATEGORY: SWIPE_CONTROLS_EXPERIMENTAL_FLAGS",
|
||||
"SETTINGS: ENABLE_WATCH_PANEL_GESTEURES"
|
||||
"SETTINGS: ENABLE_WATCH_PANEL_GESTURES"
|
||||
)
|
||||
)
|
||||
} ?: println("WARNING: Failed to resolve WatchPanelGesturesFingerprint")
|
||||
|
@ -100,18 +100,33 @@ object ResourceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun ResourceContext.addPreferenceFragment(key: String) {
|
||||
fun ResourceContext.addPreferenceFragment(key: String, insertKey: String) {
|
||||
val targetClass =
|
||||
"com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity"
|
||||
|
||||
this.xmlEditor[YOUTUBE_SETTINGS_PATH].use { editor ->
|
||||
with(editor.file) {
|
||||
doRecursively loop@{
|
||||
if (it !is Element) return@loop
|
||||
it.getAttributeNode("android:key")?.let { attribute ->
|
||||
if (attribute.textContent == "@string/about_key" && it.getAttributeNode("app:iconSpaceReserved").textContent == "false") {
|
||||
it.insertNode("Preference", it) {
|
||||
setAttribute("android:title", "@string/" + key + "_title")
|
||||
val processedKeys = mutableSetOf<String>() // To track processed keys
|
||||
|
||||
doRecursively loop@{ node ->
|
||||
if (node !is Element) return@loop // Skip if not an element
|
||||
|
||||
val attributeNode = node.getAttributeNode("android:key")
|
||||
?: return@loop // Skip if no key attribute
|
||||
val currentKey = attributeNode.textContent
|
||||
|
||||
// Check if the current key has already been processed
|
||||
if (processedKeys.contains(currentKey)) {
|
||||
return@loop // Skip if already processed
|
||||
} else {
|
||||
processedKeys.add(currentKey) // Add the current key to processedKeys
|
||||
}
|
||||
|
||||
when (currentKey) {
|
||||
insertKey -> {
|
||||
node.insertNode("Preference", node) {
|
||||
setAttribute("android:key", "${key}_key")
|
||||
setAttribute("android:title", "@string/${key}_title")
|
||||
this.appendChild(
|
||||
ownerDocument.createElement("intent").also { intentNode ->
|
||||
intentNode.setAttribute(
|
||||
@ -120,24 +135,18 @@ object ResourceUtils {
|
||||
)
|
||||
intentNode.setAttribute("android:data", key + "_intent")
|
||||
intentNode.setAttribute("android:targetClass", targetClass)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
it.getAttributeNode("app:iconSpaceReserved").textContent = "true"
|
||||
return@loop
|
||||
node.setAttribute("app:iconSpaceReserved", "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doRecursively loop@{
|
||||
if (it !is Element) return@loop
|
||||
|
||||
it.getAttributeNode("app:iconSpaceReserved")?.let { attribute ->
|
||||
if (attribute.textContent == "true") {
|
||||
attribute.textContent = "false"
|
||||
"true" -> {
|
||||
attributeNode.textContent = "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.patches.youtube.utils.settings
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import app.revanced.patches.shared.elements.StringsElementsUtils.removeStringsElements
|
||||
import app.revanced.patches.shared.mapping.ResourceMappingPatch
|
||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||
@ -37,9 +38,48 @@ object SettingsPatch : BaseResourcePatch(
|
||||
compatiblePackages = COMPATIBLE_PACKAGE,
|
||||
requiresIntegrations = true
|
||||
), Closeable {
|
||||
private const val DEFAULT_ELEMENT = "About"
|
||||
private const val DEFAULT_NAME = "ReVanced Extended"
|
||||
|
||||
private val THREAD_COUNT = Runtime.getRuntime().availableProcessors()
|
||||
private val threadPoolExecutor = Executors.newFixedThreadPool(THREAD_COUNT)
|
||||
private val SETTINGS_ELEMENTS_MAP = mapOf(
|
||||
"Parent settings" to "@string/parent_tools_key",
|
||||
"General" to "@string/general_key",
|
||||
"Account" to "@string/account_switcher_key",
|
||||
"Data saving" to "@string/data_saving_settings_key",
|
||||
"Autoplay" to "@string/auto_play_key",
|
||||
"Video quality preferences" to "@string/video_quality_settings_key",
|
||||
"Background" to "@string/offline_key",
|
||||
"Watch on TV" to "@string/pair_with_tv_key",
|
||||
"Manage all history" to "@string/history_key",
|
||||
"Your data in YouTube" to "@string/your_data_key",
|
||||
"Privacy" to "@string/privacy_key",
|
||||
"History & privacy" to "@string/privacy_key",
|
||||
"Try experimental new features" to "@string/premium_early_access_browse_page_key",
|
||||
"Purchases and memberships" to "@string/subscription_product_setting_key",
|
||||
"Billing & payments" to "@string/billing_and_payment_key",
|
||||
"Billing and payments" to "@string/billing_and_payment_key",
|
||||
"Notifications" to "@string/notification_key",
|
||||
"Connected apps" to "@string/connected_accounts_browse_page_key",
|
||||
"Live chat" to "@string/live_chat_key",
|
||||
"Captions" to "@string/captions_key",
|
||||
"Accessibility" to "@string/accessibility_settings_key",
|
||||
DEFAULT_ELEMENT to "@string/about_key"
|
||||
)
|
||||
|
||||
private val InsertPosition by stringPatchOption(
|
||||
key = "InsertPosition",
|
||||
default = DEFAULT_ELEMENT,
|
||||
values = SETTINGS_ELEMENTS_MAP,
|
||||
title = "Insert position",
|
||||
description = "The settings menu name that the RVX settings menu should be above."
|
||||
)
|
||||
|
||||
private val RVXSettingsMenuName by stringPatchOption(
|
||||
key = "RVXSettingsMenuName",
|
||||
default = DEFAULT_NAME,
|
||||
title = "RVX settings menu name",
|
||||
description = "The name of the RVX settings menu."
|
||||
)
|
||||
|
||||
internal lateinit var contexts: ResourceContext
|
||||
internal var upward1831 = false
|
||||
@ -51,48 +91,16 @@ object SettingsPatch : BaseResourcePatch(
|
||||
internal var upward1912 = false
|
||||
|
||||
override fun execute(context: ResourceContext) {
|
||||
|
||||
/**
|
||||
* set resource context
|
||||
*/
|
||||
contexts = context
|
||||
|
||||
val resourceXmlFile = context["res/values/integers.xml"].readBytes()
|
||||
|
||||
for (threadIndex in 0 until THREAD_COUNT) {
|
||||
threadPoolExecutor.execute thread@{
|
||||
context.xmlEditor[resourceXmlFile.inputStream()].use { editor ->
|
||||
val resources = editor.file.documentElement.childNodes
|
||||
val resourcesLength = resources.length
|
||||
val jobSize = resourcesLength / THREAD_COUNT
|
||||
|
||||
val batchStart = jobSize * threadIndex
|
||||
val batchEnd = jobSize * (threadIndex + 1)
|
||||
element@ for (i in batchStart until batchEnd) {
|
||||
if (i >= resourcesLength) return@thread
|
||||
|
||||
val node = resources.item(i)
|
||||
if (node !is Element) continue
|
||||
|
||||
if (node.nodeName != "integer" || !node.getAttribute("name")
|
||||
.startsWith("google_play_services_version")
|
||||
) continue
|
||||
|
||||
val playServicesVersion = node.textContent.toInt()
|
||||
|
||||
upward1831 = 233200000 <= playServicesVersion
|
||||
upward1834 = 233500000 <= playServicesVersion
|
||||
upward1839 = 234000000 <= playServicesVersion
|
||||
upward1842 = 234302000 <= playServicesVersion
|
||||
upward1849 = 235000000 <= playServicesVersion
|
||||
upward1902 = 240204000 < playServicesVersion
|
||||
upward1912 = 241302000 <= playServicesVersion
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threadPoolExecutor
|
||||
.also { it.shutdown() }
|
||||
.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS)
|
||||
/**
|
||||
* set version info
|
||||
*/
|
||||
setVersionInfo()
|
||||
|
||||
/**
|
||||
* remove strings duplicated with RVX resources
|
||||
@ -138,7 +146,16 @@ object SettingsPatch : BaseResourcePatch(
|
||||
/**
|
||||
* initialize ReVanced Extended Settings
|
||||
*/
|
||||
context.addPreferenceFragment("revanced_extended_settings")
|
||||
val elementKey = SETTINGS_ELEMENTS_MAP[InsertPosition]
|
||||
?: InsertPosition
|
||||
?: SETTINGS_ELEMENTS_MAP[DEFAULT_ELEMENT]
|
||||
|
||||
elementKey?.let { insertKey ->
|
||||
context.addPreferenceFragment(
|
||||
"revanced_extended_settings",
|
||||
insertKey
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* remove ReVanced Extended Settings divider
|
||||
@ -164,6 +181,112 @@ object SettingsPatch : BaseResourcePatch(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set revanced-patches version
|
||||
*/
|
||||
val jarManifest = classLoader.getResources("META-INF/MANIFEST.MF")
|
||||
while (jarManifest.hasMoreElements())
|
||||
contexts.updatePatchStatusSettings(
|
||||
"ReVanced Patches",
|
||||
Manifest(jarManifest.nextElement().openStream())
|
||||
.mainAttributes
|
||||
.getValue("Version") + ""
|
||||
)
|
||||
|
||||
/**
|
||||
* set revanced-integrations version
|
||||
*/
|
||||
val versionName = SettingsBytecodePatch.contexts
|
||||
.findClass { it.sourceFile == "BuildConfig.java" }!!
|
||||
.mutableClass
|
||||
.fields
|
||||
.single { it.name == "VERSION_NAME" }
|
||||
.initialValue
|
||||
.toString()
|
||||
.trim()
|
||||
.replace("\"", "")
|
||||
.replace(""", "")
|
||||
|
||||
contexts.updatePatchStatusSettings(
|
||||
"ReVanced Integrations",
|
||||
versionName
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
/**
|
||||
* change RVX settings menu name
|
||||
* since it must be invoked after the Translations patch, it must be the last in the order.
|
||||
*/
|
||||
RVXSettingsMenuName?.let { customName ->
|
||||
if (customName != DEFAULT_NAME) {
|
||||
contexts.removeStringsElements(
|
||||
arrayOf("revanced_extended_settings_title")
|
||||
)
|
||||
contexts.xmlEditor["res/values/strings.xml"].use { editor ->
|
||||
val document = editor.file
|
||||
|
||||
mapOf(
|
||||
"revanced_extended_settings_title" to customName
|
||||
).forEach { (k, v) ->
|
||||
val stringElement = document.createElement("string")
|
||||
|
||||
stringElement.setAttribute("name", k)
|
||||
stringElement.textContent = v
|
||||
|
||||
document.getElementsByTagName("resources").item(0)
|
||||
.appendChild(stringElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: println("WARNING: Invalid RVX settings menu name. RVX settings menu name does not change.")
|
||||
|
||||
}
|
||||
|
||||
private fun setVersionInfo() {
|
||||
val threadCount = Runtime.getRuntime().availableProcessors()
|
||||
val threadPoolExecutor = Executors.newFixedThreadPool(threadCount)
|
||||
|
||||
val resourceXmlFile = contexts["res/values/integers.xml"].readBytes()
|
||||
|
||||
for (threadIndex in 0 until threadCount) {
|
||||
threadPoolExecutor.execute thread@{
|
||||
contexts.xmlEditor[resourceXmlFile.inputStream()].use { editor ->
|
||||
val resources = editor.file.documentElement.childNodes
|
||||
val resourcesLength = resources.length
|
||||
val jobSize = resourcesLength / threadCount
|
||||
|
||||
val batchStart = jobSize * threadIndex
|
||||
val batchEnd = jobSize * (threadIndex + 1)
|
||||
element@ for (i in batchStart until batchEnd) {
|
||||
if (i >= resourcesLength) return@thread
|
||||
|
||||
val node = resources.item(i)
|
||||
if (node !is Element) continue
|
||||
|
||||
if (node.nodeName != "integer" || !node.getAttribute("name")
|
||||
.startsWith("google_play_services_version")
|
||||
) continue
|
||||
|
||||
val playServicesVersion = node.textContent.toInt()
|
||||
|
||||
upward1831 = 233200000 <= playServicesVersion
|
||||
upward1834 = 233500000 <= playServicesVersion
|
||||
upward1839 = 234000000 <= playServicesVersion
|
||||
upward1842 = 234302000 <= playServicesVersion
|
||||
upward1849 = 235000000 <= playServicesVersion
|
||||
upward1902 = 240204000 < playServicesVersion
|
||||
upward1912 = 241302000 <= playServicesVersion
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threadPoolExecutor
|
||||
.also { it.shutdown() }
|
||||
.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
internal fun addPreference(settingArray: Array<String>) {
|
||||
@ -178,39 +301,7 @@ object SettingsPatch : BaseResourcePatch(
|
||||
updatePatchStatus(patch.name!!)
|
||||
}
|
||||
|
||||
internal fun updatePatchStatus(patchTitle: String) {
|
||||
contexts.updatePatchStatus(patchTitle)
|
||||
internal fun updatePatchStatus(patchName: String) {
|
||||
contexts.updatePatchStatus(patchName)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
// region set ReVanced Patches Version
|
||||
|
||||
val jarManifest = classLoader.getResources("META-INF/MANIFEST.MF")
|
||||
while (jarManifest.hasMoreElements())
|
||||
contexts.updatePatchStatusSettings(
|
||||
"ReVanced Patches",
|
||||
Manifest(jarManifest.nextElement().openStream())
|
||||
.mainAttributes
|
||||
.getValue("Version") + ""
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
// region set ReVanced Integrations Version
|
||||
|
||||
val buildConfigMutableClass =
|
||||
SettingsBytecodePatch.contexts.findClass { it.sourceFile == "BuildConfig.java" }!!.mutableClass
|
||||
val versionNameField = buildConfigMutableClass.fields.single { it.name == "VERSION_NAME" }
|
||||
val versionName =
|
||||
versionNameField.initialValue.toString().trim().replace("\"", "").replace(""", "")
|
||||
|
||||
contexts.updatePatchStatusSettings(
|
||||
"ReVanced Integrations",
|
||||
versionName
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,11 +63,14 @@ fun ResourceContext.copyResources(
|
||||
for (resourceGroup in resources) {
|
||||
resourceGroup.resources.forEach { resource ->
|
||||
val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource"
|
||||
Files.copy(
|
||||
inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)!!,
|
||||
targetResourceDirectory.resolve(resourceFile).toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
|
||||
inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)?.let { inputStream ->
|
||||
Files.copy(
|
||||
inputStream,
|
||||
targetResourceDirectory.resolve(resourceFile).toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user