mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-29 13:20:19 +02:00
feat(YouTube Music): add Enable swipe to dismiss miniplayer
patch
This commit is contained in:
parent
9e3dd001eb
commit
3fe386be81
@ -0,0 +1,240 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.PatchException
|
||||||
|
import app.revanced.patcher.patch.annotation.CompatiblePackage
|
||||||
|
import app.revanced.patcher.patch.annotation.Patch
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.HandleSearchRenderedFingerprint
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.HandleSignInEventFingerprint
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.InteractionLoggingEnumFingerprint
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.MiniPlayerDefaultTextFingerprint
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.MiniPlayerDefaultViewVisibilityFingerprint
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.MusicActivityWidgetFingerprint
|
||||||
|
import app.revanced.patches.music.player.swipetodismiss.fingerprints.SwipeToCloseFingerprint
|
||||||
|
import app.revanced.patches.music.utils.integrations.Constants.PLAYER
|
||||||
|
import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch
|
||||||
|
import app.revanced.patches.music.utils.settings.CategoryType
|
||||||
|
import app.revanced.patches.music.utils.settings.SettingsPatch
|
||||||
|
import app.revanced.util.exception
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.getStringInstructionIndex
|
||||||
|
import app.revanced.util.getTargetIndex
|
||||||
|
import app.revanced.util.getTargetIndexReversed
|
||||||
|
import app.revanced.util.getTargetIndexWithFieldReferenceType
|
||||||
|
import app.revanced.util.getWideLiteralInstructionIndex
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.Reference
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
@Patch(
|
||||||
|
name = "Enable swipe to dismiss miniplayer",
|
||||||
|
description = "Adds an option to swipe down to dismiss the miniplayer.",
|
||||||
|
dependencies = [
|
||||||
|
SettingsPatch::class,
|
||||||
|
SharedResourceIdPatch::class
|
||||||
|
],
|
||||||
|
compatiblePackages = [
|
||||||
|
CompatiblePackage(
|
||||||
|
"com.google.android.apps.youtube.music",
|
||||||
|
[
|
||||||
|
"6.21.52",
|
||||||
|
"6.22.52",
|
||||||
|
"6.23.56",
|
||||||
|
"6.25.53",
|
||||||
|
"6.26.51",
|
||||||
|
"6.27.54",
|
||||||
|
"6.28.53",
|
||||||
|
"6.29.58",
|
||||||
|
"6.31.55",
|
||||||
|
"6.33.52"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Suppress("unused")
|
||||||
|
object SwipeToDismissMiniPlayerPatch : BytecodePatch(
|
||||||
|
setOf(
|
||||||
|
HandleSearchRenderedFingerprint,
|
||||||
|
InteractionLoggingEnumFingerprint,
|
||||||
|
MiniPlayerDefaultTextFingerprint,
|
||||||
|
MiniPlayerDefaultViewVisibilityFingerprint,
|
||||||
|
MusicActivityWidgetFingerprint,
|
||||||
|
SwipeToCloseFingerprint
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
private var widgetIndex by Delegates.notNull<Int>()
|
||||||
|
private lateinit var iGetObjectReference: Reference
|
||||||
|
private lateinit var invokeInterfacePrimaryReference: Reference
|
||||||
|
private lateinit var checkCastReference: Reference
|
||||||
|
private lateinit var sGetObjectReference: Reference
|
||||||
|
private lateinit var newInstanceReference: Reference
|
||||||
|
private lateinit var invokeStaticReference: Reference
|
||||||
|
private lateinit var invokeDirectReference: Reference
|
||||||
|
private lateinit var invokeInterfaceSecondaryReference: Reference
|
||||||
|
|
||||||
|
override fun execute(context: BytecodeContext) {
|
||||||
|
|
||||||
|
if (!SettingsPatch.upward0642) {
|
||||||
|
SwipeToCloseFingerprint.result?.let {
|
||||||
|
it.mutableMethod.apply {
|
||||||
|
val insertIndex = implementation!!.instructions.size - 1
|
||||||
|
val targetRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
insertIndex, """
|
||||||
|
invoke-static {v$targetRegister}, $PLAYER->enableSwipeToDismissMiniPlayer(Z)Z
|
||||||
|
move-result v$targetRegister
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: throw SwipeToCloseFingerprint.exception
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Dismiss mini player by swiping down
|
||||||
|
|
||||||
|
InteractionLoggingEnumFingerprint.result?.let {
|
||||||
|
it.mutableMethod.apply {
|
||||||
|
val stringIndex = getStringInstructionIndex("INTERACTION_LOGGING_GESTURE_TYPE_SWIPE")
|
||||||
|
val sPutObjectIndex = getTargetIndex(stringIndex, Opcode.SPUT_OBJECT)
|
||||||
|
|
||||||
|
sGetObjectReference = getInstruction<ReferenceInstruction>(sPutObjectIndex).reference
|
||||||
|
}
|
||||||
|
} ?: throw InteractionLoggingEnumFingerprint.exception
|
||||||
|
|
||||||
|
MusicActivityWidgetFingerprint.result?.let {
|
||||||
|
it.mutableMethod.apply {
|
||||||
|
widgetIndex = getWideLiteralInstructionIndex(79500)
|
||||||
|
|
||||||
|
iGetObjectReference = getReference(Opcode.IGET_OBJECT, true)
|
||||||
|
invokeInterfacePrimaryReference = getReference(Opcode.INVOKE_INTERFACE, true)
|
||||||
|
checkCastReference = getReference(Opcode.CHECK_CAST, true)
|
||||||
|
newInstanceReference = getReference(Opcode.NEW_INSTANCE, true)
|
||||||
|
invokeStaticReference = getReference(Opcode.INVOKE_STATIC, false)
|
||||||
|
invokeDirectReference = getReference(Opcode.INVOKE_DIRECT, false)
|
||||||
|
invokeInterfaceSecondaryReference = getReference(Opcode.INVOKE_INTERFACE, false)
|
||||||
|
}
|
||||||
|
} ?: throw MusicActivityWidgetFingerprint.exception
|
||||||
|
|
||||||
|
HandleSearchRenderedFingerprint.result?.let { parentResult ->
|
||||||
|
// Resolves fingerprints
|
||||||
|
HandleSignInEventFingerprint.resolve(context, parentResult.classDef)
|
||||||
|
|
||||||
|
HandleSignInEventFingerprint.result?.let {
|
||||||
|
val dismissBehaviorMethod = context.toMethodWalker(it.method)
|
||||||
|
.nextMethod(it.scanResult.patternScanResult!!.startIndex, true)
|
||||||
|
.getMethod() as MutableMethod
|
||||||
|
|
||||||
|
dismissBehaviorMethod.apply {
|
||||||
|
val insertIndex = getTargetIndexWithFieldReferenceType("Ljava/util/concurrent/atomic/AtomicBoolean;")
|
||||||
|
val primaryRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerB
|
||||||
|
val secondaryRegister = primaryRegister + 1
|
||||||
|
val tertiaryRegister = secondaryRegister + 1
|
||||||
|
|
||||||
|
val freeRegister = implementation!!.registerCount - parameters.size - 2
|
||||||
|
|
||||||
|
addInstructionsWithLabels(
|
||||||
|
insertIndex, """
|
||||||
|
invoke-static {}, $PLAYER->enableSwipeToDismissMiniPlayer()Z
|
||||||
|
move-result v$freeRegister
|
||||||
|
if-nez v$freeRegister, :dismiss
|
||||||
|
iget-object v$primaryRegister, v$primaryRegister, $iGetObjectReference
|
||||||
|
invoke-interface {v$primaryRegister}, $invokeInterfacePrimaryReference
|
||||||
|
move-result-object v$primaryRegister
|
||||||
|
check-cast v$primaryRegister, $checkCastReference
|
||||||
|
sget-object v$secondaryRegister, $sGetObjectReference
|
||||||
|
new-instance v$tertiaryRegister, $newInstanceReference
|
||||||
|
const p0, 0x878b
|
||||||
|
invoke-static {p0}, $invokeStaticReference
|
||||||
|
move-result-object p0
|
||||||
|
invoke-direct {v$tertiaryRegister, p0}, $invokeDirectReference
|
||||||
|
const/4 p0, 0x0
|
||||||
|
invoke-interface {v$primaryRegister, v$secondaryRegister, v$tertiaryRegister, p0}, $invokeInterfaceSecondaryReference
|
||||||
|
return-void
|
||||||
|
""", ExternalLabel("dismiss", getInstruction(insertIndex))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: throw HandleSignInEventFingerprint.exception
|
||||||
|
} ?: throw HandleSearchRenderedFingerprint.exception
|
||||||
|
|
||||||
|
// Endregion
|
||||||
|
|
||||||
|
// Hides default text display when the app is cold started
|
||||||
|
|
||||||
|
MiniPlayerDefaultTextFingerprint.result?.let {
|
||||||
|
it.mutableMethod.apply {
|
||||||
|
val insertIndex = it.scanResult.patternScanResult!!.endIndex
|
||||||
|
val insertRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerB
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
insertIndex, """
|
||||||
|
invoke-static {v$insertRegister}, $PLAYER->enableSwipeToDismissMiniPlayer(Ljava/lang/Object;)Ljava/lang/Object;
|
||||||
|
move-result-object v$insertRegister
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: throw MiniPlayerDefaultTextFingerprint.exception
|
||||||
|
|
||||||
|
// Endregion
|
||||||
|
|
||||||
|
// Hides default text display after dismissing the mini player
|
||||||
|
|
||||||
|
MiniPlayerDefaultViewVisibilityFingerprint.result?.let {
|
||||||
|
it.mutableClass.methods.find { method ->
|
||||||
|
method.parameters == listOf("Landroid/view/View;", "I")
|
||||||
|
}?.apply {
|
||||||
|
val bottomSheetBehaviorIndex = implementation!!.instructions.indexOfFirst { instruction ->
|
||||||
|
instruction.opcode == Opcode.INVOKE_VIRTUAL
|
||||||
|
&& instruction.getReference<MethodReference>()?.definingClass == "Lcom/google/android/material/bottomsheet/BottomSheetBehavior;"
|
||||||
|
&& instruction.getReference<MethodReference>()?.parameterTypes?.first() == "Z"
|
||||||
|
}
|
||||||
|
if (bottomSheetBehaviorIndex < 0)
|
||||||
|
throw PatchException("Could not find bottomSheetBehaviorIndex")
|
||||||
|
|
||||||
|
val freeRegister = getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
|
||||||
|
|
||||||
|
addInstructionsWithLabels(
|
||||||
|
bottomSheetBehaviorIndex - 2, """
|
||||||
|
invoke-static {}, $PLAYER->enableSwipeToDismissMiniPlayer()Z
|
||||||
|
move-result v$freeRegister
|
||||||
|
if-nez v$freeRegister, :dismiss
|
||||||
|
""", ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
|
||||||
|
)
|
||||||
|
} ?: throw PatchException("Could not find targetMethod")
|
||||||
|
|
||||||
|
} ?: throw MiniPlayerDefaultViewVisibilityFingerprint.exception
|
||||||
|
|
||||||
|
// Endregion
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsPatch.addMusicPreference(
|
||||||
|
CategoryType.PLAYER,
|
||||||
|
"revanced_enable_swipe_to_dismiss_mini_player",
|
||||||
|
"true"
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableMethod.getReference(
|
||||||
|
opcode: Opcode,
|
||||||
|
reversed: Boolean
|
||||||
|
): Reference {
|
||||||
|
val targetIndex = if (reversed)
|
||||||
|
getTargetIndexReversed(widgetIndex, opcode)
|
||||||
|
else
|
||||||
|
getTargetIndex(widgetIndex, opcode)
|
||||||
|
|
||||||
|
return getInstruction<ReferenceInstruction>(targetIndex).reference
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
|
||||||
|
object HandleSearchRenderedFingerprint : MethodFingerprint(
|
||||||
|
returnType = "V",
|
||||||
|
parameters = listOf("L"),
|
||||||
|
customFingerprint = { methodDef, _ -> methodDef.name == "handleSearchRendered" }
|
||||||
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
object HandleSignInEventFingerprint : MethodFingerprint(
|
||||||
|
returnType = "V",
|
||||||
|
parameters = listOf("L"),
|
||||||
|
opcodes = listOf(
|
||||||
|
Opcode.INVOKE_VIRTUAL,
|
||||||
|
Opcode.RETURN_VOID
|
||||||
|
),
|
||||||
|
customFingerprint = { methodDef, _ -> methodDef.name == "handleSignInEvent" }
|
||||||
|
)
|
@ -0,0 +1,8 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
|
||||||
|
object InteractionLoggingEnumFingerprint : MethodFingerprint(
|
||||||
|
returnType = "V",
|
||||||
|
strings = listOf("INTERACTION_LOGGING_GESTURE_TYPE_SWIPE")
|
||||||
|
)
|
@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.MiniPlayerDefaultText
|
||||||
|
import app.revanced.util.fingerprint.LiteralValueFingerprint
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
object MiniPlayerDefaultTextFingerprint : LiteralValueFingerprint(
|
||||||
|
returnType = "V",
|
||||||
|
parameters = listOf("Ljava/lang/Object;"),
|
||||||
|
opcodes = listOf(
|
||||||
|
Opcode.SGET_OBJECT,
|
||||||
|
Opcode.IF_NE
|
||||||
|
),
|
||||||
|
literalSupplier = { MiniPlayerDefaultText }
|
||||||
|
)
|
@ -0,0 +1,21 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
object MiniPlayerDefaultViewVisibilityFingerprint : MethodFingerprint(
|
||||||
|
returnType = "V",
|
||||||
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||||
|
parameters = listOf("Landroid/view/View;", "F"),
|
||||||
|
opcodes = listOf(
|
||||||
|
Opcode.IGET_OBJECT,
|
||||||
|
Opcode.SUB_FLOAT_2ADDR,
|
||||||
|
Opcode.SGET_OBJECT,
|
||||||
|
Opcode.INVOKE_VIRTUAL
|
||||||
|
),
|
||||||
|
customFingerprint = { methodDef, classDef ->
|
||||||
|
methodDef.name == "a" && classDef.methods.count() == 3
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
import app.revanced.util.containsWideLiteralInstructionIndex
|
||||||
|
|
||||||
|
object MusicActivityWidgetFingerprint : MethodFingerprint(
|
||||||
|
customFingerprint = handler@{ methodDef, _ ->
|
||||||
|
if (!methodDef.definingClass.endsWith("/MusicActivity;"))
|
||||||
|
return@handler false
|
||||||
|
|
||||||
|
methodDef.containsWideLiteralInstructionIndex(79500)
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patches.music.player.swipetodismiss.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.util.fingerprint.LiteralValueFingerprint
|
||||||
|
|
||||||
|
object SwipeToCloseFingerprint : LiteralValueFingerprint(
|
||||||
|
returnType = "Z",
|
||||||
|
parameters = emptyList(),
|
||||||
|
literalSupplier = { 45398432 }
|
||||||
|
)
|
@ -29,6 +29,7 @@ object SharedResourceIdPatch : ResourcePatch() {
|
|||||||
var IsTablet: Long = -1
|
var IsTablet: Long = -1
|
||||||
var LikeDislikeContainer: Long = -1
|
var LikeDislikeContainer: Long = -1
|
||||||
var MenuEntry: Long = -1
|
var MenuEntry: Long = -1
|
||||||
|
var MiniPlayerDefaultText: Long = -1
|
||||||
var MiniPlayerMdxPlaying: Long = -1
|
var MiniPlayerMdxPlaying: Long = -1
|
||||||
var MiniPlayerPlayPauseReplayButton: Long = -1
|
var MiniPlayerPlayPauseReplayButton: Long = -1
|
||||||
var MusicNotifierShelf: Long = -1
|
var MusicNotifierShelf: Long = -1
|
||||||
@ -66,6 +67,7 @@ object SharedResourceIdPatch : ResourcePatch() {
|
|||||||
IsTablet = find(BOOL, "is_tablet")
|
IsTablet = find(BOOL, "is_tablet")
|
||||||
LikeDislikeContainer = find(ID, "like_dislike_container")
|
LikeDislikeContainer = find(ID, "like_dislike_container")
|
||||||
MenuEntry = find(LAYOUT, "menu_entry")
|
MenuEntry = find(LAYOUT, "menu_entry")
|
||||||
|
MiniPlayerDefaultText = find(STRING, "mini_player_default_text")
|
||||||
MiniPlayerMdxPlaying = find(STRING, "mini_player_mdx_playing")
|
MiniPlayerMdxPlaying = find(STRING, "mini_player_mdx_playing")
|
||||||
MiniPlayerPlayPauseReplayButton = find(ID, "mini_player_play_pause_replay_button")
|
MiniPlayerPlayPauseReplayButton = find(ID, "mini_player_play_pause_replay_button")
|
||||||
MusicNotifierShelf = find(LAYOUT, "music_notifier_shelf")
|
MusicNotifierShelf = find(LAYOUT, "music_notifier_shelf")
|
||||||
|
@ -76,6 +76,7 @@ object SettingsPatch : AbstractSettingsResourcePatch(
|
|||||||
val playServicesVersion = node.textContent.toInt()
|
val playServicesVersion = node.textContent.toInt()
|
||||||
|
|
||||||
upward0636 = 240399000 <= playServicesVersion
|
upward0636 = 240399000 <= playServicesVersion
|
||||||
|
upward0642 = 240999000 <= playServicesVersion
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -146,6 +147,7 @@ object SettingsPatch : AbstractSettingsResourcePatch(
|
|||||||
|
|
||||||
lateinit var contexts: ResourceContext
|
lateinit var contexts: ResourceContext
|
||||||
internal var upward0636: Boolean = false
|
internal var upward0636: Boolean = false
|
||||||
|
internal var upward0642: Boolean = false
|
||||||
|
|
||||||
internal fun addMusicPreference(
|
internal fun addMusicPreference(
|
||||||
category: CategoryType,
|
category: CategoryType,
|
||||||
|
@ -67,6 +67,8 @@ Some features may not work properly in the old player layout."</string>
|
|||||||
<string name="revanced_enable_save_playback_speed_title">Enable save playback speed</string>
|
<string name="revanced_enable_save_playback_speed_title">Enable save playback speed</string>
|
||||||
<string name="revanced_enable_save_video_quality_summary">Remembers the last video quality selected.</string>
|
<string name="revanced_enable_save_video_quality_summary">Remembers the last video quality selected.</string>
|
||||||
<string name="revanced_enable_save_video_quality_title">Enable save video quality</string>
|
<string name="revanced_enable_save_video_quality_title">Enable save video quality</string>
|
||||||
|
<string name="revanced_enable_swipe_to_dismiss_mini_player_summary">Enables swipe down to dismiss miniplayer.</string>
|
||||||
|
<string name="revanced_enable_swipe_to_dismiss_mini_player_title">Enable swipe to dismiss miniplayer</string>
|
||||||
<string name="revanced_enable_zen_mode_summary">Changes the player background to light grey to reduce eye strain.</string>
|
<string name="revanced_enable_zen_mode_summary">Changes the player background to light grey to reduce eye strain.</string>
|
||||||
<string name="revanced_enable_zen_mode_title">Enable zen mode</string>
|
<string name="revanced_enable_zen_mode_title">Enable zen mode</string>
|
||||||
<string name="revanced_extended_settings_export_as_file">Export settings to file</string>
|
<string name="revanced_extended_settings_export_as_file">Export settings to file</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user