mirror of
https://github.com/revanced/revanced-patches
synced 2024-11-19 11:39:25 +01:00
feat: sponsorblock
patch (#101)
* chore(release): 2.5.1-dev.1 [skip ci] ## [2.5.1-dev.1](https://github.com/revanced/revanced-patches/compare/v2.5.0...v2.5.1-dev.1) (2022-06-30) ### Bug Fixes * freezing panels when watching video in fullscreen ([#89](https://github.com/revanced/revanced-patches/issues/89)) ([f5d4f6c
](f5d4f6c341
)) * invalid version in compatibility annotation ([#90](https://github.com/revanced/revanced-patches/issues/90)) ([df43547
](df435475cd
)) * feat: migrate to breaking changes of patcher * chore(release): 2.6.0-dev.1 [skip ci] # [2.6.0-dev.1](https://github.com/revanced/revanced-patches/compare/v2.5.1-dev.1...v2.6.0-dev.1) (2022-07-02) ### Features * migrate to breaking changes of patcher ([a116852
](a11685263f
)) * refactor: add package for fingerprints * feat: partial `sponsorblock` patch * feat: add `Name` annotation to `sponsorblock-resource-patch` * fix: `sponsorblock-resource-patch` * refactor: remove unused resources * refactor: remove `locale-config-fix` dependency * fix: broken fingerprints * feat: general fixes on `sponsorblock` patch * feat: more fixes on `sponsorblock` patch * feat: update `sponsorblock` patch to 17.26.35 * style: use better wording and fix spelling mistakes * fix: finish `sponsorblock` patch Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
This commit is contained in:
parent
8f06ea9115
commit
36af4cc14f
@ -0,0 +1,13 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.annotations
|
||||
|
||||
import app.revanced.patcher.annotation.Compatibility
|
||||
import app.revanced.patcher.annotation.Package
|
||||
|
||||
@Compatibility(
|
||||
[Package(
|
||||
"com.google.android.youtube", arrayOf("17.22.36", "17.26.35", "17.27.39")
|
||||
)]
|
||||
)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class SponsorBlockCompatibility
|
@ -0,0 +1,41 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import org.jf.dexlib2.AccessFlags
|
||||
import org.jf.dexlib2.Opcode
|
||||
|
||||
@Name("append-time-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Liet;", "e"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object AppendTimeFingerprint : MethodFingerprint(
|
||||
"V",
|
||||
AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||
listOf("L", "L", "L"),
|
||||
listOf(
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT_OBJECT,
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.CHECK_CAST,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT_OBJECT,
|
||||
Opcode.INVOKE_STATIC,
|
||||
Opcode.MOVE_RESULT,
|
||||
Opcode.IF_NEZ,
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.CHECK_CAST,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.RETURN_VOID
|
||||
)
|
||||
)
|
@ -0,0 +1,21 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
|
||||
@Name("create-video-player-seekbar-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Lfcm;", "onDraw"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object CreateVideoPlayerSeekbarFingerprint : MethodFingerprint(
|
||||
"V", null, null,
|
||||
null,
|
||||
listOf("timed_markers_width")
|
||||
)
|
@ -0,0 +1,24 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import org.jf.dexlib2.util.MethodUtil
|
||||
|
||||
@Name("next-gen-watch-layout-fingerprint")
|
||||
@MatchingMethod(
|
||||
"LNextGenWatchLayout;", "<init>"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object NextGenWatchLayoutFingerprint : MethodFingerprint(
|
||||
"V", // constructors return void, in favour of speed of matching, this fingerprint has been added
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
customFingerprint = { methodDef -> MethodUtil.isConstructor(methodDef) && methodDef.parameterTypes.size == 3 && methodDef.definingClass.endsWith("NextGenWatchLayout;") }
|
||||
)
|
@ -0,0 +1,22 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import org.jf.dexlib2.Opcode
|
||||
|
||||
@Name("player-controller-set-time-reference-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Lxqm;", "<init>"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object PlayerControllerSetTimeReferenceFingerprint : MethodFingerprint(
|
||||
null, null, null,
|
||||
listOf(Opcode.INVOKE_DIRECT_RANGE, Opcode.IGET_OBJECT),
|
||||
listOf("Media progress reported outside media playback: ")
|
||||
)
|
@ -0,0 +1,23 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
|
||||
@Name("player-init-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Laajv;", "aG"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object PlayerInitFingerprint : MethodFingerprint(
|
||||
null, null, null,
|
||||
null,
|
||||
strings = listOf(
|
||||
"playVideo called on player response with no videoStreamingData."
|
||||
),
|
||||
)
|
@ -0,0 +1,22 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
|
||||
@Name("player-overlays-layout-init-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Lihh;", "u"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object PlayerOverlaysLayoutInitFingerprint : MethodFingerprint(
|
||||
null, null, null,
|
||||
null,
|
||||
null,
|
||||
{ methodDef -> methodDef.returnType.endsWith("YouTubePlayerOverlaysLayout;") }
|
||||
)
|
@ -0,0 +1,37 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import org.jf.dexlib2.iface.reference.MethodReference
|
||||
|
||||
@Name("rectangle-field-invalidator-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Lfcm;", "kY"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object RectangleFieldInvalidatorFingerprint : MethodFingerprint(
|
||||
"V",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
custom@{ methodDef ->
|
||||
val instructions = methodDef.implementation?.instructions!!
|
||||
val instructionCount = instructions.count()
|
||||
|
||||
// the method has definitely more than 5 instructions
|
||||
if (instructionCount < 5) return@custom false
|
||||
|
||||
val referenceInstruction = instructions.elementAt(instructionCount - 2) // the second to last instruction
|
||||
val reference = ((referenceInstruction as? ReferenceInstruction)?.reference as? MethodReference)
|
||||
|
||||
reference?.parameterTypes?.size == 1 && reference.name == "invalidate" // the reference is the invalidate(..) method
|
||||
}
|
||||
)
|
@ -0,0 +1,23 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
|
||||
@Name("seek-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Laajv;", "af"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object SeekFingerprint : MethodFingerprint(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
listOf("Attempting to seek during an ad")
|
||||
)
|
@ -0,0 +1,19 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
|
||||
@Name("show-player-controls-fingerprint")
|
||||
@MatchingMethod(
|
||||
"LYouTubeControlsOverlay;", "ac"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object ShowPlayerControlsFingerprint : MethodFingerprint(
|
||||
"V", null, listOf("Z","Z"), null, null
|
||||
)
|
@ -0,0 +1,34 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import org.jf.dexlib2.Opcode
|
||||
|
||||
@Name("video-length-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Lyfh;", "z"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object VideoLengthFingerprint : MethodFingerprint(
|
||||
null, null, null,
|
||||
listOf(
|
||||
Opcode.MOVE_RESULT_WIDE,
|
||||
Opcode.CMP_LONG,
|
||||
Opcode.IF_LEZ,
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.CHECK_CAST,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT_WIDE,
|
||||
Opcode.GOTO,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT_WIDE,
|
||||
Opcode.CONST_4,
|
||||
Opcode.INVOKE_VIRTUAL
|
||||
)
|
||||
)
|
@ -0,0 +1,20 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
|
||||
@Name("video-time-fingerprint")
|
||||
@MatchingMethod(
|
||||
"Lwyh;", "<init>"
|
||||
)
|
||||
@DirectPatternScanMethod
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
object VideoTimeFingerprint : MethodFingerprint(
|
||||
null, null, null, null,
|
||||
listOf("MedialibPlayerTimeInfo{currentPositionMillis=")
|
||||
)
|
@ -0,0 +1,335 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.patch
|
||||
|
||||
import app.revanced.patcher.annotation.Description
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.data.impl.BytecodeData
|
||||
import app.revanced.patcher.data.impl.toMethodWalker
|
||||
import app.revanced.patcher.extensions.addInstruction
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.extensions.replaceInstruction
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils.resolve
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.patch.annotations.Dependencies
|
||||
import app.revanced.patcher.patch.annotations.Patch
|
||||
import app.revanced.patcher.patch.impl.BytecodePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints.*
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.resource.patch.SponsorBlockResourcePatch
|
||||
import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch
|
||||
import app.revanced.patches.youtube.misc.mapping.patch.ResourceIdMappingProviderResourcePatch
|
||||
import app.revanced.patches.youtube.misc.videoid.patch.VideoIdPatch
|
||||
import org.jf.dexlib2.AccessFlags
|
||||
import org.jf.dexlib2.Opcode
|
||||
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||
import org.jf.dexlib2.iface.instruction.*
|
||||
import org.jf.dexlib2.iface.instruction.formats.Instruction35c
|
||||
import org.jf.dexlib2.iface.reference.FieldReference
|
||||
import org.jf.dexlib2.iface.reference.MethodReference
|
||||
import org.jf.dexlib2.iface.reference.StringReference
|
||||
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||
import org.jf.dexlib2.immutable.ImmutableMethodParameter
|
||||
import org.jf.dexlib2.util.MethodUtil
|
||||
|
||||
@Patch
|
||||
@Dependencies(
|
||||
dependencies = [IntegrationsPatch::class, ResourceIdMappingProviderResourcePatch::class, SponsorBlockResourcePatch::class, VideoIdPatch::class]
|
||||
)
|
||||
@Name("sponsorblock")
|
||||
@Description("Integrate SponsorBlock.")
|
||||
@SponsorBlockCompatibility
|
||||
@Version("0.0.1")
|
||||
class SponsorBlockBytecodePatch : BytecodePatch(
|
||||
listOf(
|
||||
PlayerControllerSetTimeReferenceFingerprint,
|
||||
CreateVideoPlayerSeekbarFingerprint,
|
||||
VideoTimeFingerprint,
|
||||
NextGenWatchLayoutFingerprint,
|
||||
AppendTimeFingerprint,
|
||||
PlayerInitFingerprint,
|
||||
PlayerOverlaysLayoutInitFingerprint
|
||||
)
|
||||
) {
|
||||
override fun execute(data: BytecodeData): PatchResult {/*
|
||||
Set current video time
|
||||
*/
|
||||
val referenceResult = PlayerControllerSetTimeReferenceFingerprint.result!!
|
||||
val playerControllerSetTimeMethod =
|
||||
data.toMethodWalker(referenceResult.method).nextMethod(referenceResult.patternScanResult!!.startIndex, true)
|
||||
.getMethod() as MutableMethod
|
||||
playerControllerSetTimeMethod.addInstruction(
|
||||
2,
|
||||
"invoke-static {p1, p2}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setCurrentVideoTime(J)V"
|
||||
)
|
||||
|
||||
/*
|
||||
Set current video time high precision
|
||||
*/
|
||||
val constructorFingerprint =
|
||||
object : MethodFingerprint("V", null, listOf("J", "J", "J", "J", "I", "L"), null) {}
|
||||
constructorFingerprint.resolve(data, VideoTimeFingerprint.result!!.classDef)
|
||||
|
||||
val constructor = constructorFingerprint.result!!.mutableMethod
|
||||
constructor.addInstruction(
|
||||
0,
|
||||
"invoke-static {p1, p2}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setCurrentVideoTimeHighPrecision(J)V"
|
||||
)
|
||||
|
||||
/*
|
||||
Set current video id
|
||||
*/
|
||||
VideoIdPatch.injectCall("Lapp/revanced/integrations/sponsorblock/PlayerController;->setCurrentVideoId(Ljava/lang/String;)V")
|
||||
|
||||
/*
|
||||
Seekbar drawing
|
||||
*/
|
||||
val seekbarSignatureResult = CreateVideoPlayerSeekbarFingerprint.result!!
|
||||
val seekbarMethod = seekbarSignatureResult.mutableMethod
|
||||
val seekbarMethodInstructions = seekbarMethod.implementation!!.instructions
|
||||
|
||||
/*
|
||||
Get the instance of the seekbar rectangle
|
||||
*/
|
||||
seekbarMethod.addInstruction(
|
||||
1,
|
||||
"invoke-static {v0}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarRect(Ljava/lang/Object;)V"
|
||||
)
|
||||
|
||||
for ((index, instruction) in seekbarMethodInstructions.withIndex()) {
|
||||
if (instruction.opcode != Opcode.INVOKE_STATIC) continue
|
||||
|
||||
val invokeInstruction = instruction as Instruction35c
|
||||
if ((invokeInstruction.reference as MethodReference).name != "round") continue
|
||||
|
||||
val insertIndex = index + 2
|
||||
|
||||
// set the thickness of the segment
|
||||
seekbarMethod.addInstruction(
|
||||
insertIndex,
|
||||
"invoke-static {v${invokeInstruction.registerC}}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarThickness(I)V"
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
/*
|
||||
Set rectangle absolute left and right positions
|
||||
*/
|
||||
val drawRectangleInstructions = seekbarMethodInstructions.filter {
|
||||
it is ReferenceInstruction && (it.reference as? MethodReference)?.name == "drawRect" && it is FiveRegisterInstruction
|
||||
}.map { // TODO: improve code
|
||||
seekbarMethodInstructions.indexOf(it) to (it as FiveRegisterInstruction).registerD
|
||||
}
|
||||
|
||||
val (indexRight, rectangleRightRegister) = drawRectangleInstructions[0]
|
||||
val (indexLeft, rectangleLeftRegister) = drawRectangleInstructions[3]
|
||||
|
||||
// order of operation is important here due to the code above which has to be improved
|
||||
// the reason for that is that we get the index, add instructions and then the offset would be wrong
|
||||
seekbarMethod.addInstruction(
|
||||
indexLeft + 1,
|
||||
"invoke-static {v$rectangleLeftRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarAbsoluteLeft(Landroid/graphics/Rect;)V"
|
||||
)
|
||||
seekbarMethod.addInstruction(
|
||||
indexRight + 1,
|
||||
"invoke-static {v$rectangleRightRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarAbsoluteRight(Landroid/graphics/Rect;)V"
|
||||
)
|
||||
|
||||
/*
|
||||
Draw segment
|
||||
*/
|
||||
val drawSegmentInstructionInsertIndex = (seekbarMethodInstructions.size - 1 - 2)
|
||||
val (canvasInstance, centerY) = (seekbarMethodInstructions[drawSegmentInstructionInsertIndex] as FiveRegisterInstruction).let {
|
||||
it.registerC to it.registerE
|
||||
}
|
||||
seekbarMethod.addInstruction(
|
||||
drawSegmentInstructionInsertIndex - 1,
|
||||
"invoke-static {v$canvasInstance, v$centerY}, Lapp/revanced/integrations/sponsorblock/PlayerController;->drawSponsorTimeBars(Landroid/graphics/Canvas;F)V"
|
||||
)
|
||||
|
||||
/*
|
||||
Set video length
|
||||
*/
|
||||
VideoLengthFingerprint.resolve(data, seekbarSignatureResult.classDef)
|
||||
val videoLengthMethodResult = VideoLengthFingerprint.result!!
|
||||
val videoLengthMethod = videoLengthMethodResult.mutableMethod
|
||||
val videoLengthMethodInstructions = videoLengthMethod.implementation!!.instructions
|
||||
|
||||
val videoLengthRegister =
|
||||
(videoLengthMethodInstructions[videoLengthMethodResult.patternScanResult!!.endIndex - 2] as OneRegisterInstruction).registerA
|
||||
val dummyRegisterForLong =
|
||||
videoLengthRegister + 1 // this is required for long values since they are 64 bit wide
|
||||
videoLengthMethod.addInstruction(
|
||||
videoLengthMethodResult.patternScanResult!!.endIndex,
|
||||
"invoke-static {v$videoLengthRegister, v$dummyRegisterForLong}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setVideoLength(J)V"
|
||||
)
|
||||
|
||||
/*
|
||||
Voting & Shield button
|
||||
*/
|
||||
ShowPlayerControlsFingerprint.resolve(data, data.classes.find { it.type.endsWith("YouTubeControlsOverlay;") }!!)
|
||||
val controlsMethodResult = ShowPlayerControlsFingerprint.result!!
|
||||
|
||||
val controlsLayoutStubResourceId =
|
||||
ResourceIdMappingProviderResourcePatch.resourceMappings.single { it.type == "id" && it.name == "controls_layout_stub" }.id
|
||||
val zoomOverlayResourceId =
|
||||
ResourceIdMappingProviderResourcePatch.resourceMappings.single { it.type == "id" && it.name == "video_zoom_overlay_stub" }.id
|
||||
|
||||
methods@ for (method in controlsMethodResult.mutableClass.methods) {
|
||||
val instructions = method.implementation?.instructions!!
|
||||
instructions@ for ((index, instruction) in instructions.withIndex()) {
|
||||
// search for method which inflates the controls layout view
|
||||
if (instruction.opcode != Opcode.CONST) continue@instructions
|
||||
|
||||
when ((instruction as NarrowLiteralInstruction).wideLiteral) {
|
||||
controlsLayoutStubResourceId -> {
|
||||
// replace the view with the YouTubeControlsOverlay
|
||||
val moveResultInstructionIndex = index + 5
|
||||
val inflatedViewRegister =
|
||||
(instructions[moveResultInstructionIndex] as OneRegisterInstruction).registerA
|
||||
// initialize with the player overlay object
|
||||
method.addInstructions(
|
||||
moveResultInstructionIndex + 1, // insert right after moving the view to the register and use that register
|
||||
"""
|
||||
invoke-static {v$inflatedViewRegister}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->initialize(Ljava/lang/Object;)V
|
||||
invoke-static {v$inflatedViewRegister}, Lapp/revanced/integrations/sponsorblock/VotingButton;->initialize(Ljava/lang/Object;)V
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
zoomOverlayResourceId -> {
|
||||
val invertVisibilityMethod =
|
||||
data.toMethodWalker(method).nextMethod(index - 6, true).getMethod() as MutableMethod
|
||||
// change visibility of the buttons
|
||||
invertVisibilityMethod.addInstructions(
|
||||
0, """
|
||||
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->changeVisibilityNegatedImmediate(Z)V
|
||||
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/VotingButton;->changeVisibilityNegatedImmediate(Z)V
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// change visibility of the buttons
|
||||
controlsMethodResult.mutableMethod.addInstructions(
|
||||
0, """
|
||||
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->changeVisibility(Z)V
|
||||
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/VotingButton;->changeVisibility(Z)V
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
// set SegmentHelperLayout.context to the player layout instance
|
||||
val instanceRegister = 0
|
||||
NextGenWatchLayoutFingerprint.result!!.mutableMethod.addInstruction(
|
||||
3, // after super call
|
||||
"invoke-static/range {p$instanceRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->addSkipSponsorView15(Landroid/view/View;)V"
|
||||
)
|
||||
|
||||
// append the new time to the player layout
|
||||
val appendTimeFingerprintResult = AppendTimeFingerprint.result!!
|
||||
val appendTimePatternScanStartIndex = appendTimeFingerprintResult.patternScanResult!!.startIndex
|
||||
val targetRegister =
|
||||
(appendTimeFingerprintResult.method.implementation!!.instructions.elementAt(appendTimePatternScanStartIndex + 1) as OneRegisterInstruction).registerA
|
||||
|
||||
appendTimeFingerprintResult.mutableMethod.addInstructions(
|
||||
appendTimePatternScanStartIndex + 2, """
|
||||
invoke-static {v$targetRegister}, Lapp/revanced/integrations/sponsorblock/SponsorBlockUtils;->appendTimeWithoutSegments(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$targetRegister
|
||||
"""
|
||||
)
|
||||
|
||||
// initialize the player controller
|
||||
val initFingerprintResult = PlayerInitFingerprint.result!!
|
||||
val initInstanceRegister = 0
|
||||
initFingerprintResult.mutableClass.methods.first { MethodUtil.isConstructor(it) }.addInstruction(
|
||||
4, // after super class invoke
|
||||
"invoke-static {v$initInstanceRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->onCreate(Ljava/lang/Object;)V"
|
||||
)
|
||||
|
||||
// initialize the sponsorblock view
|
||||
PlayerOverlaysLayoutInitFingerprint.result!!.mutableMethod.addInstruction(
|
||||
6, // after inflating the view
|
||||
"invoke-static {p0}, Lapp/revanced/integrations/sponsorblock/player/ui/SponsorBlockView;->initialize(Ljava/lang/Object;)V"
|
||||
)
|
||||
|
||||
// lastly create hooks for the player controller
|
||||
|
||||
// get original seek method
|
||||
SeekFingerprint.resolve(data, initFingerprintResult.classDef)
|
||||
val seekFingerprintResultMethod = SeekFingerprint.result!!.method
|
||||
// get enum type for the seek helper method
|
||||
val seekSourceEnumType = seekFingerprintResultMethod.parameterTypes[1].toString()
|
||||
|
||||
// create helper method
|
||||
val seekHelperMethod = ImmutableMethod(
|
||||
seekFingerprintResultMethod.definingClass,
|
||||
"seekHelper",
|
||||
listOf(ImmutableMethodParameter("J", null, "time")),
|
||||
"Z",
|
||||
AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||
null, null,
|
||||
MutableMethodImplementation(4)
|
||||
).toMutable()
|
||||
|
||||
// insert helper method instructions
|
||||
seekHelperMethod.addInstructions(
|
||||
0,
|
||||
"""
|
||||
sget-object v0, $seekSourceEnumType->a:$seekSourceEnumType
|
||||
invoke-virtual {p0, p1, p2, v0}, ${seekFingerprintResultMethod.definingClass}->${seekFingerprintResultMethod.name}(J$seekSourceEnumType)Z
|
||||
move-result p1
|
||||
return p1
|
||||
"""
|
||||
)
|
||||
|
||||
// add the helper method to the original class
|
||||
initFingerprintResult.mutableClass.methods.add(seekHelperMethod)
|
||||
|
||||
// get rectangle field name
|
||||
RectangleFieldInvalidatorFingerprint.resolve(data, seekbarSignatureResult.classDef)
|
||||
val rectangleFieldInvalidatorInstructions =
|
||||
RectangleFieldInvalidatorFingerprint.result!!.method.implementation!!.instructions
|
||||
val rectangleFieldName =
|
||||
((rectangleFieldInvalidatorInstructions.elementAt(rectangleFieldInvalidatorInstructions.count() - 3) as ReferenceInstruction).reference as FieldReference).name
|
||||
|
||||
// get the player controller class from the integrations
|
||||
val playerControllerMethods =
|
||||
data.proxy(data.classes.first { it.type.endsWith("PlayerController;") }).resolve().methods
|
||||
|
||||
// get the method which contain the "replaceMe" strings
|
||||
val replaceMeMethods =
|
||||
playerControllerMethods.filter { it.name == "onCreate" || it.name == "setSponsorBarRect" }
|
||||
|
||||
fun MutableMethod.replaceStringInstruction(index: Int, instruction: Instruction, with: String) {
|
||||
val register = (instruction as OneRegisterInstruction).registerA
|
||||
this.replaceInstruction(
|
||||
index, "const-string v$register, \"$with\""
|
||||
)
|
||||
}
|
||||
|
||||
// replace the "replaceMeWith*" strings
|
||||
for (method in replaceMeMethods) {
|
||||
for ((index, it) in method.implementation!!.instructions.withIndex()) {
|
||||
if (it.opcode.ordinal != Opcode.CONST_STRING.ordinal) continue
|
||||
|
||||
when (((it as ReferenceInstruction).reference as StringReference).string) {
|
||||
"replaceMeWithsetSponsorBarRect" ->
|
||||
method.replaceStringInstruction(index, it, rectangleFieldName)
|
||||
|
||||
"replaceMeWithsetMillisecondMethod" ->
|
||||
method.replaceStringInstruction(index, it, "seekHelper")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: isSBChannelWhitelisting implementation
|
||||
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package app.revanced.patches.youtube.layout.sponsorblock.resource.patch
|
||||
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.data.impl.DomFileEditor
|
||||
import app.revanced.patcher.data.impl.ResourceData
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.patch.annotations.Dependencies
|
||||
import app.revanced.patcher.patch.impl.ResourcePatch
|
||||
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
|
||||
import app.revanced.patches.youtube.misc.manifest.patch.FixLocaleConfigErrorPatch
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
|
||||
@Name("sponsorblock-resource-patch")
|
||||
@SponsorBlockCompatibility
|
||||
@Dependencies([FixLocaleConfigErrorPatch::class])
|
||||
@Version("0.0.1")
|
||||
class SponsorBlockResourcePatch : ResourcePatch() {
|
||||
override fun execute(data: ResourceData): PatchResult {
|
||||
val classLoader = this.javaClass.classLoader
|
||||
|
||||
/*
|
||||
merge SponsorBlock strings to main strings
|
||||
*/
|
||||
val stringsResourcePath = "values/strings.xml"
|
||||
val stringsResourceInputStream = classLoader.getResourceAsStream("sponsorblock/$stringsResourcePath")!!
|
||||
|
||||
// copy nodes from the resources node to the real resource node
|
||||
"resources".copyXmlNode(
|
||||
data.xmlEditor[stringsResourceInputStream, OutputStream.nullOutputStream()],
|
||||
data.xmlEditor["res/$stringsResourcePath"]
|
||||
).close() // close afterwards
|
||||
|
||||
/*
|
||||
merge SponsorBlock drawables to main drawables
|
||||
*/
|
||||
val drawables = "drawable" to arrayOf(
|
||||
"ic_sb_adjust",
|
||||
"ic_sb_compare",
|
||||
"ic_sb_edit",
|
||||
"ic_sb_logo",
|
||||
"ic_sb_publish",
|
||||
"ic_sb_voting"
|
||||
)
|
||||
|
||||
val layouts = "layout" to arrayOf(
|
||||
"inline_sponsor_overlay", "new_segment", "skip_sponsor_button"
|
||||
)
|
||||
|
||||
// collect resources
|
||||
val xmlResources = arrayOf(drawables, layouts)
|
||||
|
||||
// write resources
|
||||
xmlResources.forEach { (path, resourceNames) ->
|
||||
resourceNames.forEach { name ->
|
||||
val relativePath = "$path/$name.xml"
|
||||
|
||||
Files.copy(
|
||||
classLoader.getResourceAsStream("sponsorblock/$relativePath")!!,
|
||||
data["res"].resolve(relativePath).toPath()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
merge xml nodes from the host to their real xml files
|
||||
*/
|
||||
|
||||
// collect all host resources
|
||||
val hostingXmlResources = mapOf("layout" to arrayOf("youtube_controls_layout"))
|
||||
|
||||
// copy nodes from host resources to their real xml files
|
||||
hostingXmlResources.forEach { (path, resources) ->
|
||||
resources.forEach { resource ->
|
||||
val hostingResourceStream = classLoader.getResourceAsStream("sponsorblock/host/$path/$resource.xml")!!
|
||||
|
||||
val targetXmlEditor = data.xmlEditor["res/$path/$resource.xml"]
|
||||
"RelativeLayout".copyXmlNode(
|
||||
data.xmlEditor[hostingResourceStream, OutputStream.nullOutputStream()],
|
||||
targetXmlEditor
|
||||
).also {
|
||||
val children = targetXmlEditor.file.getElementsByTagName("RelativeLayout").item(0).childNodes
|
||||
|
||||
// Replace the startOf with the voting button view so that the button does not overlap
|
||||
for (i in 1 until children.length) {
|
||||
val view = children.item(i)
|
||||
|
||||
// Replace the attribute for a specific node only
|
||||
if (!view.attributes.getNamedItem("android:id").nodeValue.endsWith("live_chat_overlay_button")) continue
|
||||
|
||||
// voting button id from the voting button view from the youtube_controls_layout.xml host file
|
||||
val votingButtonId = "@+id/voting_button"
|
||||
|
||||
view.attributes.getNamedItem("android:layout_toStartOf").nodeValue = votingButtonId
|
||||
|
||||
break
|
||||
}
|
||||
}.close() // close afterwards
|
||||
}
|
||||
}
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the specified node of the source [DomFileEditor] to the target [DomFileEditor].
|
||||
* @param source the source [DomFileEditor].
|
||||
* @param target the target [DomFileEditor]-
|
||||
*/
|
||||
private fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable {
|
||||
val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes
|
||||
|
||||
val destinationResourceFile = target.file
|
||||
val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0)
|
||||
|
||||
for (index in 0 until hostNodes.length) {
|
||||
val node = hostNodes.item(index).cloneNode(true)
|
||||
destinationResourceFile.adoptNode(node)
|
||||
destinationNode.appendChild(node)
|
||||
}
|
||||
|
||||
return AutoCloseable {
|
||||
source.close()
|
||||
target.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -5,14 +5,16 @@ import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.data.impl.BytecodeData
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.patch.annotations.Dependencies
|
||||
import app.revanced.patcher.patch.impl.BytecodePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch
|
||||
import app.revanced.patches.youtube.misc.videoid.annotation.VideoIdCompatibility
|
||||
import app.revanced.patches.youtube.misc.videoid.fingerprint.VideoIdFingerprint
|
||||
import org.jf.dexlib2.iface.instruction.formats.Instruction11x
|
||||
import org.jf.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
@Name("video-id-hook")
|
||||
@Description("hook to detect when the video id changes")
|
||||
@ -25,12 +27,21 @@ class VideoIdPatch : BytecodePatch(
|
||||
)
|
||||
) {
|
||||
override fun execute(data: BytecodeData): PatchResult {
|
||||
result = VideoIdFingerprint.result!!
|
||||
|
||||
insertMethod = result.mutableMethod
|
||||
videoIdRegister =
|
||||
(insertMethod.implementation!!.instructions[result.patternScanResult!!.endIndex + 1] as OneRegisterInstruction).registerA
|
||||
|
||||
injectCall("Lapp/revanced/integrations/videoplayer/VideoInformation;->setCurrentVideoId(Ljava/lang/String;)V")
|
||||
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var result: MethodFingerprintResult
|
||||
private var videoIdRegister: Int = 0
|
||||
private lateinit var insertMethod: MutableMethod
|
||||
private var offset = 2
|
||||
|
||||
/**
|
||||
@ -40,12 +51,7 @@ class VideoIdPatch : BytecodePatch(
|
||||
fun injectCall(
|
||||
methodDescriptor: String
|
||||
) {
|
||||
val result = VideoIdFingerprint.result!!
|
||||
|
||||
val method = result.mutableMethod
|
||||
val videoIdRegister =
|
||||
(method.implementation!!.instructions[result.patternScanResult!!.endIndex + 1] as Instruction11x).registerA
|
||||
method.addInstructions(
|
||||
insertMethod.addInstructions(
|
||||
result.patternScanResult!!.endIndex + offset, // after the move-result-object
|
||||
"invoke-static {v$videoIdRegister}, $methodDescriptor"
|
||||
)
|
||||
|
10
src/main/resources/sponsorblock/drawable/ic_sb_adjust.xml
Normal file
10
src/main/resources/sponsorblock/drawable/ic_sb_adjust.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10 10,-4.49 10,-10S17.51,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15,12c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3z" />
|
||||
</vector>
|
10
src/main/resources/sponsorblock/drawable/ic_sb_compare.xml
Normal file
10
src/main/resources/sponsorblock/drawable/ic_sb_compare.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h5v2h2L12,1h-2v2zM10,18L5,18l5,-6v6zM19,3h-5v2h5v13l-5,-6v9h5c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
10
src/main/resources/sponsorblock/drawable/ic_sb_edit.xml
Normal file
10
src/main/resources/sponsorblock/drawable/ic_sb_edit.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
19
src/main/resources/sponsorblock/drawable/ic_sb_logo.xml
Normal file
19
src/main/resources/sponsorblock/drawable/ic_sb_logo.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX=".8"
|
||||
android:scaleY=".8">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
|
||||
</group>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM19,11c0,4.52 -2.98,8.69 -7,9.93 -4.02,-1.24 -7,-5.41 -7,-9.93L5,6.3l7,-3.11 7,3.11L19,14.17z" />
|
||||
</vector>
|
10
src/main/resources/sponsorblock/drawable/ic_sb_publish.xml
Normal file
10
src/main/resources/sponsorblock/drawable/ic_sb_publish.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M5,5c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L6,4c-0.55,0 -1,0.45 -1,1zM7.41,14L9,14v5c0,0.55 0.45,1 1,1h4c0.55,0 1,-0.45 1,-1v-5h1.59c0.89,0 1.34,-1.08 0.71,-1.71L12.71,7.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l-4.59,4.59c-0.63,0.63 -0.19,1.71 0.7,1.71z" />
|
||||
</vector>
|
10
src/main/resources/sponsorblock/drawable/ic_sb_voting.xml
Normal file
10
src/main/resources/sponsorblock/drawable/ic_sb_voting.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,6c0,-0.55 -0.45,-1 -1,-1L5.82,5l0.66,-3.18 0.02,-0.23c0,-0.31 -0.13,-0.59 -0.33,-0.8L5.38,0 0.44,4.94C0.17,5.21 0,5.59 0,6v6.5c0,0.83 0.67,1.5 1.5,1.5h6.75c0.62,0 1.15,-0.38 1.38,-0.91l2.26,-5.29c0.07,-0.17 0.11,-0.36 0.11,-0.55L12,6zM22.5,10h-6.75c-0.62,0 -1.15,0.38 -1.38,0.91l-2.26,5.29c-0.07,0.17 -0.11,0.36 -0.11,0.55L12,18c0,0.55 0.45,1 1,1h5.18l-0.66,3.18 -0.02,0.24c0,0.31 0.13,0.59 0.33,0.8l0.79,0.78 4.94,-4.94c0.27,-0.27 0.44,-0.65 0.44,-1.06v-6.5c0,-0.83 -0.67,-1.5 -1.5,-1.5z" />
|
||||
</vector>
|
@ -0,0 +1,4 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<com.google.android.libraries.youtube.common.ui.TouchImageView android:id="@+id/sponsorblock_button" android:padding="@dimen/controls_overlay_action_button_padding" android:layout_width="@dimen/controls_overlay_action_button_size" android:layout_height="@dimen/controls_overlay_action_button_size" android:layout_marginTop="2dp" android:src="@drawable/ic_sb_logo" android:layout_alignParentTop="true" android:layout_alignWithParentIfMissing="true" android:layout_marginEnd="4dp" android:layout_toStartOf="@+id/player_additional_view_container" style="@style/YouTubePlayerButton"/>
|
||||
<com.google.android.libraries.youtube.common.ui.TouchImageView android:id="@+id/voting_button" android:padding="@dimen/controls_overlay_action_button_padding" android:layout_width="@dimen/controls_overlay_action_button_size" android:layout_height="@dimen/controls_overlay_action_button_size" android:layout_marginTop="2dp" android:src="@drawable/ic_sb_voting" android:layout_alignParentTop="true" android:layout_alignWithParentIfMissing="true" android:layout_marginEnd="4dp" android:layout_toStartOf="@+id/sponsorblock_button" style="@style/YouTubePlayerButton"/>
|
||||
</RelativeLayout>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<app.revanced.integrations.sponsorblock.player.ui.NewSegmentLayout android:id="@+id/new_segment_view" android:focusable="true" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/brand_interaction_default_bottom_margin" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true"/>
|
||||
<app.revanced.integrations.sponsorblock.player.ui.SkipSponsorButton android:id="@+id/skip_sponsor_button" android:focusable="true" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/inline_controls_bottom_bar_height" android:layout_alignParentRight="true" android:layout_alignParentBottom="true"/>
|
||||
</merge>
|
107
src/main/resources/sponsorblock/layout/new_segment.xml
Normal file
107
src/main/resources/sponsorblock/layout/new_segment.xml
Normal file
@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:yt="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:gravity="start|center"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/new_segment_container"
|
||||
android:background="#66000000"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36.0dip">
|
||||
|
||||
<ImageButton
|
||||
android:layout_gravity="start|center"
|
||||
android:id="@+id/new_segment_rewind"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingTop="3.0dip"
|
||||
android:paddingBottom="3.0dip"
|
||||
android:layout_width="45.0dip"
|
||||
android:layout_height="36.0dip"
|
||||
android:src="@drawable/player_fast_rewind"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="1.0"
|
||||
android:paddingStart="10.0dip"
|
||||
android:paddingEnd="5.0dip" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_gravity="start|center"
|
||||
android:id="@+id/new_segment_forward"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingTop="3.0dip"
|
||||
android:paddingBottom="3.0dip"
|
||||
android:layout_width="45.0dip"
|
||||
android:layout_height="36.0dip"
|
||||
android:src="@drawable/player_fast_forward"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="1.0"
|
||||
android:paddingStart="5.0dip"
|
||||
android:paddingEnd="5.0dip" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_gravity="start|center"
|
||||
android:id="@+id/new_segment_adjust"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingTop="3.0dip"
|
||||
android:paddingBottom="3.0dip"
|
||||
android:layout_width="45.0dip"
|
||||
android:layout_height="36.0dip"
|
||||
android:src="@drawable/ic_sb_adjust"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="1.0"
|
||||
android:paddingStart="5.0dip"
|
||||
android:paddingEnd="10.0dip" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36.0dip">
|
||||
|
||||
<ImageButton
|
||||
android:layout_gravity="start|center"
|
||||
android:id="@+id/new_segment_compare"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingTop="3.0dip"
|
||||
android:paddingBottom="3.0dip"
|
||||
android:layout_width="45.0dip"
|
||||
android:layout_height="36.0dip"
|
||||
android:src="@drawable/ic_sb_compare"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="1.0"
|
||||
android:paddingStart="10.0dip"
|
||||
android:paddingEnd="5.0dip" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_gravity="start|center"
|
||||
android:id="@+id/new_segment_edit"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingTop="3.0dip"
|
||||
android:paddingBottom="3.0dip"
|
||||
android:layout_width="45.0dip"
|
||||
android:layout_height="36.0dip"
|
||||
android:src="@drawable/ic_sb_edit"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="1.0"
|
||||
android:paddingStart="5.0dip"
|
||||
android:paddingEnd="5.0dip" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_gravity="start|center"
|
||||
android:id="@+id/new_segment_publish"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingTop="3.0dip"
|
||||
android:paddingBottom="3.0dip"
|
||||
android:layout_width="45.0dip"
|
||||
android:layout_height="36.0dip"
|
||||
android:src="@drawable/ic_sb_publish"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="1.0"
|
||||
android:paddingStart="5.0dip"
|
||||
android:paddingEnd="10.0dip" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</merge>
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:yt="http://schemas.android.com/apk/res-auto">
|
||||
<LinearLayout android:layout_gravity="center_vertical" android:orientation="horizontal" android:id="@+id/skip_sponsor_button_container" android:padding="8dp" android:layout_width="wrap_content" android:layout_height="32dp" android:contentDescription="@string/skip_sponsor">
|
||||
<com.google.android.libraries.youtube.common.ui.YouTubeTextView android:textSize="@dimen/extra_small_font_size" android:textColor="@color/skip_ad_button_foreground_color" android:layout_gravity="center_vertical" android:id="@+id/skip_sponsor_button_text" android:paddingRight="@dimen/ad_overlay_ad_text_padding" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/skip_sponsor" android:singleLine="true" android:includeFontPadding="false" yt:robotoFont="light"/>
|
||||
<ImageView android:layout_gravity="center_vertical" android:id="@+id/skip_sponsor_button_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/quantum_ic_skip_next_white_24" android:contentDescription="@null" android:alpha="0.8"/>
|
||||
</LinearLayout>
|
||||
</merge>
|
182
src/main/resources/sponsorblock/values/strings.xml
Normal file
182
src/main/resources/sponsorblock/values/strings.xml
Normal file
@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="enable_sb">Enable SponsorBlock</string>
|
||||
<string name="enable_sb_sum">SponsorBlock is a crowd-sourced system for skipping annoying parts in YouTube videos</string>
|
||||
<string name="enable_segmadding">Enable new segment adding</string>
|
||||
<string name="enable_segmadding_sum">Switch this on to enable experimental segment adding (has button visibility issues)</string>
|
||||
<string name="diff_segments">What to do with different segments</string>
|
||||
<string name="general">General</string>
|
||||
<string name="general_skiptoast">Show a toast when skipping segment automatically</string>
|
||||
<string name="general_skiptoast_sum">Click to see an example toast</string>
|
||||
<string name="general_skipcount">Skip count tracking</string>
|
||||
<string name="general_skipcount_sum">This lets SponsorBlock leaderboard system know how much time people have saved. The extension sends a message to the server each time you skip a segment</string>
|
||||
<string name="general_adjusting">Adjusting new segment step</string>
|
||||
<string name="general_adjusting_sum">This is the number of milliseconds you can move when you use the time adjustment buttons while adding new segment</string>
|
||||
<string name="general_min_duration">Minimum segment duration</string>
|
||||
<string name="general_min_duration_sum">Segments shorter than the set value (in seconds) will not be skipped or shown in the player</string>
|
||||
<string name="general_uuid">Your unique user id</string>
|
||||
<string name="general_uuid_sum">This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you</string>
|
||||
<string name="settings_ie">Import/Export settings</string>
|
||||
<string name="settings_ie_sum">This is your entire configuration that is applicable in the desktop extension in JSON. This includes your userID, so be sure to share this wisely.</string>
|
||||
<string name="general_api_url">Change API URL</string>
|
||||
<string name="general_api_url_sum">The address SponsorBlock uses to make calls to the server. <b>Don\'t change this unless you know what you\'re doing.</b></string>
|
||||
<string name="settings_import_successful">Settings were successfully imported</string>
|
||||
<string name="settings_import_failed">Failed to import settings</string>
|
||||
<string name="settings_export_failed">Failed to export settings</string>
|
||||
<string name="general_cache">Cache segments locally</string>
|
||||
<string name="general_cache_sum">Frequently watched videos (eg. music videos) may store segments in app cache to make skipping segments faster</string>
|
||||
<string name="general_cache_clear">Clear SponsorBlock segments cache</string>
|
||||
<string name="segments_sponsor">Sponsor</string>
|
||||
<string name="segments_sponsor_sum">Paid promotion, paid referrals and direct advertisements</string>
|
||||
<string name="segments_intermission">Intermission/Intro Animation</string>
|
||||
<string name="segments_intermission_sum">An interval without actual content. Could be a pause, static frame, repeating animation</string>
|
||||
<string name="segments_endcards">Endcards/Credits</string>
|
||||
<string name="segments_endcards_sum">Credits or when the YouTube endcards appear. Not for spoken conclusions</string>
|
||||
<string name="segments_subscribe">Interaction Reminder (Subscribe)</string>
|
||||
<string name="segments_subscribe_sum">When there is a short reminder to like, subscribe or follow them in the middle of content</string>
|
||||
<string name="segments_selfpromo">Unpaid/Self Promotion</string>
|
||||
<string name="segments_selfpromo_sum">When there is unpaid or self promotion. This includes specific sections about merchandise, donations, or information about who they collaborated with</string>
|
||||
<string name="segments_nomusic">Music: Non-Music Section</string>
|
||||
<string name="segments_nomusic_sum">Only for use in music videos. This includes introductions or outros in music videos</string>
|
||||
<string name="segments_filler">Filler Tangent/Jokes</string>
|
||||
<string name="segments_filler_sum">Tangential scenes added only for filler or humor that are not required to understand the main content of the video. This should not include context or background details</string>
|
||||
<string name="skipped_segment">Skipped a sponsor segment</string>
|
||||
<string name="skipped_sponsor">Skipped sponsor</string>
|
||||
<string name="skipped_intermission">Skipped intro</string>
|
||||
<string name="skipped_endcard">Skipped outro</string>
|
||||
<string name="skipped_subscribe">Skipped annoying reminder</string>
|
||||
<string name="skipped_selfpromo">Skipped self promotion</string>
|
||||
<string name="skipped_nomusic">Skipped a non-music section</string>
|
||||
<string name="skipped_preview">Skipped preview</string>
|
||||
<string name="skipped_filler">Skipped filler</string>
|
||||
<string name="skipped_unsubmitted">Skipped unsubmitted segment</string>
|
||||
<string name="skip_automatically">Skip automatically</string>
|
||||
<string name="skip_showbutton">Show a skip button</string>
|
||||
<string name="skip_ignore">Don\'t do anything</string>
|
||||
<string name="skip_sponsor">Skip segment</string>
|
||||
<string name="about">About</string>
|
||||
<string name="about_api">This app uses the API from SponsorBlock</string>
|
||||
<string name="about_api_sum">Tap to learn more, and see downloads for other platforms at: sponsor.ajay.app</string>
|
||||
<string name="about_madeby">Integration made by JakubWeg</string>
|
||||
<string name="tap_skip">Tap to skip</string>
|
||||
|
||||
<string name="submit_failed_unknown_error" formatted="false">Unable to submit segments: Status: %d %s</string>
|
||||
<string name="submit_failed_rate_limit">Can\'t submit the segment.\nRate Limited (Too many from the same user or IP)</string>
|
||||
<string name="submit_failed_forbidden" formatted="false">Can\'t submit the segment.\n\n%s</string>
|
||||
<string name="submit_failed_duplicate">Can\'t submit the segment.\nAlready exists</string>
|
||||
<string name="submit_succeeded">Segment submitted successfully</string>
|
||||
<string name="submit_started">Submitting segment…</string>
|
||||
|
||||
<string name="vote_failed_unknown_error" formatted="false">Unable to vote for segment: Status: %d %s</string>
|
||||
<string name="vote_failed_rate_limit">Can\'t vote for segment.\nRate Limited (Too many from the same user or IP)</string>
|
||||
<string name="vote_failed_forbidden" formatted="false">Can\'t vote for segment.\n\n%s</string>
|
||||
<string name="vote_succeeded">Voted successfully</string>
|
||||
<string name="vote_started">Voting for segment…</string>
|
||||
<string name="vote_upvote">Upvote</string>
|
||||
<string name="vote_downvote">Downvote</string>
|
||||
<string name="vote_category">Change category</string>
|
||||
<string name="vote_no_segments">There are no segments to vote for</string>
|
||||
<string name="enable_voting">Enable voting</string>
|
||||
<string name="enable_voting_sum">Switch this on to enable voting.</string>
|
||||
|
||||
<string name="new_segment_choose_category">Choose the segment category</string>
|
||||
<string name="new_segment_disabled_category">You\'ve disabled this category in the settings, enable it to be able to submit</string>
|
||||
<string name="new_segment_title">New SponsorBlock segment</string>
|
||||
<string name="new_segment_mark_time_as_question" formatted="false">Set %02d:%02d:%04d as the start or end of a new segment?</string>
|
||||
<string name="new_segment_mark_start">start</string>
|
||||
<string name="new_segment_mark_end">end</string>
|
||||
<string name="new_segment_now">now</string>
|
||||
<string name="new_segment_time_start">Time the segment begins at</string>
|
||||
<string name="new_segment_time_end">Time the segment ends at</string>
|
||||
<string name="new_segment_time_start_set">Beginning of segment set</string>
|
||||
<string name="new_segment_time_end_set">End of segment set</string>
|
||||
<string name="new_segment_confirm_title">Are the times correct?</string>
|
||||
<string name="new_segment_confirm_content" formatted="false">The segment lasts from %02d:%02d to %02d:%02d (%d minutes %02d seconds)\nIs it ready to submit?</string>
|
||||
<string name="new_segment_mark_locations_first">Mark two locations on the time bar first</string>
|
||||
<string name="new_segment_edit_by_hand_title">Edit timing of segment manually</string>
|
||||
<string name="new_segment_edit_by_hand_content">Do you want to edit the timing for the start or end of the segment?</string>
|
||||
<string name="new_segment_edit_by_hand_saved">Done</string>
|
||||
<string name="new_segment_edit_by_hand_parse_error">Invalid time given</string>
|
||||
|
||||
<string name="sb_guidelines_preference_title">View guidelines</string>
|
||||
<string name="sb_guidelines_preference_sum">Guidelines contain tips and rules about submitting segments</string>
|
||||
<string name="sb_guidelines_popup_title">There are guidelines</string>
|
||||
<string name="sb_guidelines_popup_content">It\'s recommended to read the SponsorBlock guidelines before submitting any segment</string>
|
||||
<string name="sb_guidelines_popup_already_read">Already read</string>
|
||||
<string name="sb_guidelines_popup_open">Show me</string>
|
||||
|
||||
<string name="sb_settings">SponsorBlock settings</string>
|
||||
<string name="sb_summary">Uses the sponsor.ajay.app API</string>
|
||||
|
||||
<string name="microg_notification_settings">Notification settings</string>
|
||||
<string name="microg_notification_settings_summary">"1. Google device registration and Cloud Messaging need to be enabled for notifications.
|
||||
2. ReVanced needs to be shown as registered under Cloud Messaging.
|
||||
3. Current State in Cloud Messaging must be Connected."</string>
|
||||
<string name="microg_settings">MicroG settings</string>
|
||||
<string name="revanced_settings">ReVanced settings</string>
|
||||
|
||||
<string name="revanced_seekbar_tapping">Seekbar Tapping</string>
|
||||
<string name="revanced_seekbar_tapping_off">Seekbar Tapping (video progress bar) is disabled</string>
|
||||
<string name="revanced_seekbar_tapping_on">Seekbar Tapping (video progress bar) is enabled</string>
|
||||
<string name="pref_subtitles_scale_normal">Normal</string>
|
||||
|
||||
<string name="litho_shorts_shelf">Shorts Shelf</string>
|
||||
<string name="litho_shorts_shelf_off">Shorts Shelf removal is turned off</string>
|
||||
<string name="litho_shorts_shelf_on">Shorts Shelf removal is turned on</string>
|
||||
|
||||
<string name="revanced_create_button_summary_off">Create Button has default visibility</string>
|
||||
<string name="revanced_create_button_summary_on">Create Button is forcefully disabled</string>
|
||||
<string name="revanced_create_button_title">Create Button</string>
|
||||
|
||||
<string name="litho_community_guidelines">Community Guidelines</string>
|
||||
<string name="litho_community_guidelines_off">Community Guidelines removal is turned off</string>
|
||||
<string name="litho_community_guidelines_on">Community Guidelines removal is turned on</string>
|
||||
|
||||
<string name="revanced_copy_video_url_summary_off">Copy Link Button is hidden from the player overlay</string>
|
||||
<string name="revanced_copy_video_url_summary_on">Copy Link Button is shown in the player overlay</string>
|
||||
<string name="revanced_copy_video_url_timestamp_summary_off">Copy Link Button With Timestamp is hidden from the player overlay</string>
|
||||
<string name="revanced_copy_video_url_timestamp_summary_on">Copy Link Button With Timestamp is shown in the player overlay</string>
|
||||
<string name="revanced_copy_video_url_timestamp_title">Copy Link Button With Timestamp</string>
|
||||
<string name="revanced_copy_video_url_title">Copy Link Button</string>
|
||||
|
||||
<string name="revanced_old_style_quality_settings_title">Quality Settings style</string>
|
||||
<string name="revanced_old_style_quality_settings_summary_off">Using default style video quality settings</string>
|
||||
<string name="revanced_old_style_quality_settings_summary_on">Using old style video quality settings</string>
|
||||
|
||||
<string name="general_time_without_sb">Show time without segments</string>
|
||||
<string name="general_time_without_sb_sum">This time appears in brackets next to the current time. This shows the total video duration minus any segments.</string>
|
||||
<string name="general_whitelisting">Channel whitelisting</string>
|
||||
<string name="general_whitelisting_sum">Use the Segments button under the player to whitelist a channel</string>
|
||||
<string name="general_browser_button">Enable SB Browser button</string>
|
||||
<string name="general_browser_button_sum">Clicking this button under the player will open sb.ltn.fi where you can see all the segments on the video.</string>
|
||||
<string name="segments_preview">Preview/Recap</string>
|
||||
<string name="segments_preview_sum">Recap of previous episodes, or a preview of what\'s coming up later in the current video or future videos in the same series. Intended for edited together clips that do not provide additional information.</string>
|
||||
<string name="stats">Stats</string>
|
||||
<string name="stats_loading">Loading..</string>
|
||||
<string name="stats_sb_disabled">SponsorBlock is disabled</string>
|
||||
<string name="stats_username" formatted="false">Your username: <b>%s</b></string>
|
||||
<string name="stats_username_change">Click to change your username</string>
|
||||
<string name="stats_username_change_unknown_error" formatted="false">Unable to change username: Status: %d %s</string>
|
||||
<string name="stats_username_changed">Username successfully changed</string>
|
||||
<string name="stats_submissions">Submissions: <b>%s</b></string>
|
||||
<string name="stats_saved">You\'ve saved people from <b>%s</b> segments.</string>
|
||||
<string name="stats_saved_sum">That\'s <b>%s</b> of their lives. Click to see the leaderboard</string>
|
||||
<string name="stats_self_saved">You\'ve skipped <b>%s</b> segments.</string>
|
||||
<string name="stats_self_saved_sum">That\'s <b>%s</b>.</string>
|
||||
<string name="minutes">minutes</string>
|
||||
<string name="color_change">Are you looking for changing colors?</string>
|
||||
<string name="color_change_sum">You can now change a category\'s color by clicking on it above.</string>
|
||||
<string name="color_choose_category">Choose the category</string>
|
||||
<string name="color_changed">Color changed</string>
|
||||
<string name="color_reset">Color reset</string>
|
||||
<string name="color_invalid">Invalid hex code</string>
|
||||
<string name="change">Change</string>
|
||||
<string name="reset">Reset</string>
|
||||
|
||||
<string name="action_segments">Segments</string>
|
||||
<string name="action_browser">SB Browser</string>
|
||||
|
||||
<string name="api_url_changed">API URL changed</string>
|
||||
<string name="api_url_reset">API URL reset</string>
|
||||
<string name="api_url_invalid">Provided API URL is invalid</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user