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:
Aaron Veil
2024-06-08 18:04:23 +03:00
committed by GitHub
parent ecd6fbdf5b
commit 09ed1b965d
816 changed files with 36893 additions and 499 deletions

View File

@ -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.")
}
}
}

View File

@ -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.")
}
}

View File

@ -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"
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"

View File

@ -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
}

View File

@ -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(

View File

@ -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",
)

View File

@ -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)
}
}

View File

@ -21,7 +21,7 @@ object AnimatedLikePatch : BaseResourcePatch(
* Copy json
*/
context.copyResources(
"youtube/animated",
"youtube/shorts/animated",
ResourceGroup(
"raw",
"like_tap_feedback.json"

View File

@ -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.")
}
}
}

View File

@ -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) {

View File

@ -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")
}
}

View File

@ -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.
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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"
}
}
}
}
}
}
}
}

View File

@ -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("&quot;", "")
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("&quot;", "")
contexts.updatePatchStatusSettings(
"ReVanced Integrations",
versionName
)
// endregion
}
}
}

View File

@ -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,
)
}
}
}
}