Skip to content

Commit dfb5407

Browse files
authored
feat(YouTube - Loop video): Add player button to change loop video state (#5961)
1 parent 6d5f6ec commit dfb5407

File tree

14 files changed

+299
-89
lines changed

14 files changed

+299
-89
lines changed

extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AutoRepeatPatch.java

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package app.revanced.extension.youtube.patches;
2+
3+
import app.revanced.extension.youtube.settings.Settings;
4+
5+
@SuppressWarnings("unused")
6+
public class LoopVideoPatch {
7+
/**
8+
* Injection point
9+
*/
10+
public static boolean shouldLoopVideo() {
11+
return Settings.LOOP_VIDEO.get();
12+
}
13+
}

extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ public class Settings extends BaseSettings {
341341
// Miscellaneous
342342
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
343343
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
344-
public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
344+
public static final BooleanSetting LOOP_VIDEO = new BooleanSetting("revanced_loop_video", FALSE);
345+
public static final BooleanSetting LOOP_VIDEO_BUTTON = new BooleanSetting("revanced_loop_video_button", FALSE);
345346
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
346347
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
347348
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
@@ -444,28 +445,30 @@ public class Settings extends BaseSettings {
444445
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFFFF", false, false);
445446

446447
// Deprecated migrations
448+
private static final BooleanSetting DEPRECATED_AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
447449
private static final BooleanSetting DEPRECATED_HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true);
448450
private static final BooleanSetting DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE);
449451
private static final IntegerSetting DEPRECATED_SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127);
450-
private static final StringSetting DEPRECATED_SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033");
452+
private static final StringSetting DEPRECATED_SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033");
451453
private static final BooleanSetting DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_disable_suggested_video_end_screen", FALSE);
452454
private static final BooleanSetting DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
453455
private static final BooleanSetting DEPRECATED_AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE);
454456

455-
public static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false);
456-
public static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false);
457-
public static final FloatSetting DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f, false, false);
458-
public static final FloatSetting DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f, false, false);
459-
public static final FloatSetting DEPRECATED_SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f, false, false);
460-
public static final FloatSetting DEPRECATED_SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f, false, false);
461-
public static final FloatSetting DEPRECATED_SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f, false, false);
462-
public static final FloatSetting DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f, false, false);
463-
public static final FloatSetting DEPRECATED_SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f, false, false);
464-
public static final FloatSetting DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f, false, false);
457+
private static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false);
458+
private static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false);
459+
private static final FloatSetting DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f, false, false);
460+
private static final FloatSetting DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f, false, false);
461+
private static final FloatSetting DEPRECATED_SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f, false, false);
462+
private static final FloatSetting DEPRECATED_SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f, false, false);
463+
private static final FloatSetting DEPRECATED_SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f, false, false);
464+
private static final FloatSetting DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f, false, false);
465+
private static final FloatSetting DEPRECATED_SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f, false, false);
466+
private static final FloatSetting DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f, false, false);
465467

466468
static {
467469
// region Migration
468470

471+
migrateOldSettingToNew(DEPRECATED_AUTO_REPEAT, LOOP_VIDEO);
469472
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
470473
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
471474
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package app.revanced.extension.youtube.videoplayer;
2+
3+
import static app.revanced.extension.shared.StringRef.str;
4+
5+
import android.view.View;
6+
import androidx.annotation.Nullable;
7+
import app.revanced.extension.shared.Logger;
8+
import app.revanced.extension.shared.Utils;
9+
import app.revanced.extension.youtube.settings.Settings;
10+
11+
@SuppressWarnings("unused")
12+
public class LoopVideoButton {
13+
@Nullable
14+
private static PlayerControlButton instance;
15+
16+
private static final int LOOP_VIDEO_ON = Utils.getResourceIdentifierOrThrow(
17+
"revanced_loop_video_button_on", "drawable");
18+
private static final int LOOP_VIDEO_OFF = Utils.getResourceIdentifierOrThrow(
19+
"revanced_loop_video_button_off", "drawable");
20+
21+
/**
22+
* Injection point.
23+
*/
24+
public static void initializeButton(View controlsView) {
25+
try {
26+
instance = new PlayerControlButton(
27+
controlsView,
28+
"revanced_loop_video_button",
29+
null,
30+
Settings.LOOP_VIDEO_BUTTON::get,
31+
v -> updateButtonAppearance(),
32+
null
33+
);
34+
} catch (Exception ex) {
35+
Logger.printException(() -> "initializeButton failure", ex);
36+
}
37+
}
38+
39+
/**
40+
* injection point.
41+
*/
42+
public static void setVisibilityNegatedImmediate() {
43+
if (instance != null) instance.setVisibilityNegatedImmediate();
44+
}
45+
46+
/**
47+
* injection point.
48+
*/
49+
public static void setVisibilityImmediate(boolean visible) {
50+
if (instance != null) instance.setVisibilityImmediate(visible);
51+
}
52+
53+
/**
54+
* injection point.
55+
*/
56+
public static void setVisibility(boolean visible, boolean animated) {
57+
if (instance != null) instance.setVisibility(visible, animated);
58+
}
59+
60+
/**
61+
* Updates the button's appearance.
62+
*/
63+
private static void updateButtonAppearance() {
64+
if (instance == null) return;
65+
66+
try {
67+
Utils.verifyOnMainThread();
68+
69+
final boolean currentState = Settings.LOOP_VIDEO.get();
70+
final boolean newState = !currentState;
71+
Settings.LOOP_VIDEO.save(newState);
72+
73+
instance.setIcon(newState
74+
? LOOP_VIDEO_ON
75+
: LOOP_VIDEO_OFF);
76+
Utils.showToastShort(str(newState
77+
? "revanced_loop_video_button_toast_on"
78+
: "revanced_loop_video_button_toast_off"));
79+
} catch (Exception ex) {
80+
Logger.printException(() -> "updateButtonAppearance failure", ex);
81+
}
82+
}
83+
}

patches/api/patches.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,10 @@ public final class app/revanced/patches/youtube/misc/litho/filter/LithoFilterPat
16101610
public static final fun getLithoFilterPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
16111611
}
16121612

1613+
public final class app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatchKt {
1614+
public static final fun getLoopVideoPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
1615+
}
1616+
16131617
public final class app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatchKt {
16141618
public static field hookNavigationButtonCreated Lkotlin/jvm/functions/Function1;
16151619
public static final fun getHookNavigationButtonCreated ()Lkotlin/jvm/functions/Function1;

patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import app.revanced.patches.youtube.misc.playercontrols.playerControlsPatch
99
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
1010
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
1111
import app.revanced.patches.youtube.misc.settings.settingsPatch
12-
import app.revanced.patches.youtube.shared.autoRepeatFingerprint
13-
import app.revanced.patches.youtube.shared.autoRepeatParentFingerprint
12+
import app.revanced.patches.youtube.shared.loopVideoFingerprint
13+
import app.revanced.patches.youtube.shared.loopVideoParentFingerprint
1414
import app.revanced.util.addInstructionsAtControlFlowLabel
1515

1616
@Suppress("unused")
@@ -50,7 +50,7 @@ internal val exitFullscreenPatch = bytecodePatch(
5050
ListPreference("revanced_exit_fullscreen")
5151
)
5252

53-
autoRepeatFingerprint.match(autoRepeatParentFingerprint.originalClassDef).method.apply {
53+
loopVideoFingerprint.match(loopVideoParentFingerprint.originalClassDef).method.apply {
5454
addInstructionsAtControlFlowLabel(
5555
implementation!!.instructions.lastIndex,
5656
"invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->endOfVideoReached()V",
Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,9 @@
11
package app.revanced.patches.youtube.misc.autorepeat
22

3-
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
4-
import app.revanced.patcher.extensions.InstructionExtensions.instructions
5-
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
63
import app.revanced.patcher.patch.bytecodePatch
7-
import app.revanced.patches.all.misc.resources.addResources
8-
import app.revanced.patches.all.misc.resources.addResourcesPatch
9-
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
10-
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
11-
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
12-
import app.revanced.patches.youtube.shared.autoRepeatFingerprint
13-
import app.revanced.patches.youtube.shared.autoRepeatParentFingerprint
4+
import app.revanced.patches.youtube.misc.loopvideo.loopVideoPatch
145

15-
// TODO: Rename this patch to AlwaysRepeatPatch (as well as strings and references in the extension).
16-
val autoRepeatPatch = bytecodePatch(
17-
name = "Always repeat",
18-
description = "Adds an option to always repeat videos when they end.",
19-
) {
20-
dependsOn(
21-
sharedExtensionPatch,
22-
addResourcesPatch,
23-
)
24-
25-
compatibleWith(
26-
"com.google.android.youtube"(
27-
"19.34.42",
28-
"19.43.41",
29-
"20.07.39",
30-
"20.13.41",
31-
"20.14.43",
32-
)
33-
)
34-
35-
execute {
36-
addResources("youtube", "misc.autorepeat.autoRepeatPatch")
37-
38-
PreferenceScreen.MISC.addPreferences(
39-
SwitchPreference("revanced_auto_repeat"),
40-
)
41-
42-
autoRepeatFingerprint.match(autoRepeatParentFingerprint.originalClassDef).method.apply {
43-
val playMethod = autoRepeatParentFingerprint.method
44-
val index = instructions.lastIndex
45-
46-
// Remove return-void.
47-
removeInstruction(index)
48-
// Add own instructions there.
49-
addInstructionsWithLabels(
50-
index,
51-
"""
52-
invoke-static {}, Lapp/revanced/extension/youtube/patches/AutoRepeatPatch;->shouldAutoRepeat()Z
53-
move-result v0
54-
if-eqz v0, :noautorepeat
55-
invoke-virtual { p0 }, $playMethod
56-
:noautorepeat
57-
return-void
58-
""",
59-
)
60-
}
61-
}
6+
@Deprecated("Patch was renamed", ReplaceWith("looVideoPatch"))
7+
val autoRepeatPatch = bytecodePatch {
8+
dependsOn(loopVideoPatch)
629
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package app.revanced.patches.youtube.misc.loopvideo
2+
3+
import app.revanced.patcher.patch.bytecodePatch
4+
import app.revanced.patches.all.misc.resources.addResources
5+
import app.revanced.patches.all.misc.resources.addResourcesPatch
6+
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
7+
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
8+
import app.revanced.patches.youtube.misc.loopvideo.button.loopVideoButtonPatch
9+
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
10+
import app.revanced.patches.youtube.shared.loopVideoFingerprint
11+
import app.revanced.patches.youtube.shared.loopVideoParentFingerprint
12+
import app.revanced.util.addInstructionsAtControlFlowLabel
13+
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
14+
import com.android.tools.smali.dexlib2.Opcode
15+
16+
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/LoopVideoPatch;"
17+
18+
val loopVideoPatch = bytecodePatch(
19+
name = "Loop video",
20+
description = "Adds an option to loop videos and display loop video button in the video player.",
21+
) {
22+
dependsOn(
23+
sharedExtensionPatch,
24+
addResourcesPatch,
25+
loopVideoButtonPatch
26+
)
27+
28+
compatibleWith(
29+
"com.google.android.youtube"(
30+
"19.34.42",
31+
"19.43.41",
32+
"20.07.39",
33+
"20.13.41",
34+
"20.14.43",
35+
)
36+
)
37+
38+
execute {
39+
addResources("youtube", "misc.loopvideo.loopVideoPatch")
40+
41+
PreferenceScreen.MISC.addPreferences(
42+
SwitchPreference("revanced_loop_video"),
43+
)
44+
45+
loopVideoFingerprint.match(loopVideoParentFingerprint.originalClassDef).method.apply {
46+
val playMethod = loopVideoParentFingerprint.method
47+
val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.RETURN_VOID)
48+
49+
addInstructionsAtControlFlowLabel(
50+
insertIndex,
51+
"""
52+
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldLoopVideo()Z
53+
move-result v0
54+
if-eqz v0, :do_not_loop
55+
invoke-virtual { p0 }, $playMethod
56+
:do_not_loop
57+
nop
58+
"""
59+
)
60+
}
61+
}
62+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package app.revanced.patches.youtube.misc.loopvideo.button
2+
3+
import app.revanced.patcher.patch.bytecodePatch
4+
import app.revanced.patcher.patch.resourcePatch
5+
import app.revanced.patches.all.misc.resources.addResources
6+
import app.revanced.patches.all.misc.resources.addResourcesPatch
7+
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
8+
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
9+
import app.revanced.patches.youtube.misc.playercontrols.*
10+
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
11+
import app.revanced.patches.youtube.misc.settings.settingsPatch
12+
import app.revanced.util.ResourceGroup
13+
import app.revanced.util.copyResources
14+
15+
private val loopVideoButtonResourcePatch = resourcePatch {
16+
dependsOn(playerControlsResourcePatch)
17+
18+
execute {
19+
copyResources(
20+
"loopvideobutton",
21+
ResourceGroup(
22+
"drawable",
23+
"revanced_loop_video_button_on.xml",
24+
"revanced_loop_video_button_off.xml"
25+
)
26+
)
27+
28+
addBottomControl("loopvideobutton")
29+
}
30+
}
31+
32+
private const val LOOP_VIDEO_BUTTON_CLASS_DESCRIPTOR =
33+
"Lapp/revanced/extension/youtube/videoplayer/LoopVideoButton;"
34+
35+
internal val loopVideoButtonPatch = bytecodePatch(
36+
description = "Adds the option to display loop video button in the video player.",
37+
) {
38+
dependsOn(
39+
sharedExtensionPatch,
40+
settingsPatch,
41+
addResourcesPatch,
42+
loopVideoButtonResourcePatch,
43+
playerControlsPatch,
44+
)
45+
46+
execute {
47+
addResources("youtube", "misc.loopvideo.button.loopVideoButtonPatch")
48+
49+
PreferenceScreen.PLAYER.addPreferences(
50+
SwitchPreference("revanced_loop_video_button"),
51+
)
52+
53+
// Initialize the button using standard approach.
54+
initializeBottomControl(LOOP_VIDEO_BUTTON_CLASS_DESCRIPTOR)
55+
injectVisibilityCheckCall(LOOP_VIDEO_BUTTON_CLASS_DESCRIPTOR)
56+
}
57+
}

0 commit comments

Comments
 (0)