Merge remote-tracking branch 'upstream/main'
This commit is contained in:
164
CHANGELOG.md
164
CHANGELOG.md
@ -1,3 +1,167 @@
|
|||||||
|
# [5.24.0](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0) (2025-05-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5))
|
||||||
|
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d))
|
||||||
|
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf))
|
||||||
|
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560))
|
||||||
|
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a))
|
||||||
|
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2))
|
||||||
|
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c))
|
||||||
|
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be))
|
||||||
|
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5))
|
||||||
|
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058))
|
||||||
|
|
||||||
|
# [5.24.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.8...v5.24.0-dev.9) (2025-05-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b))
|
||||||
|
|
||||||
|
# [5.24.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.7...v5.24.0-dev.8) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf))
|
||||||
|
|
||||||
|
# [5.24.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.6...v5.24.0-dev.7) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be))
|
||||||
|
|
||||||
|
# [5.24.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.5...v5.24.0-dev.6) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c))
|
||||||
|
|
||||||
|
# [5.24.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.4...v5.24.0-dev.5) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058))
|
||||||
|
|
||||||
|
# [5.24.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.3...v5.24.0-dev.4) (2025-05-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2))
|
||||||
|
|
||||||
|
# [5.24.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.2...v5.24.0-dev.3) (2025-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d))
|
||||||
|
|
||||||
|
# [5.24.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.1...v5.24.0-dev.2) (2025-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560))
|
||||||
|
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5))
|
||||||
|
|
||||||
|
# [5.24.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0-dev.1) (2025-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a))
|
||||||
|
|
||||||
|
# [5.23.0](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0) (2025-05-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa))
|
||||||
|
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
|
||||||
|
* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0))
|
||||||
|
* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad))
|
||||||
|
* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1))
|
||||||
|
* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575))
|
||||||
|
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10))
|
||||||
|
* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c))
|
||||||
|
|
||||||
|
# [5.23.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.6...v5.23.0-dev.7) (2025-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
|
||||||
|
|
||||||
|
# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa))
|
||||||
|
|
||||||
|
# [5.23.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.4...v5.23.0-dev.5) (2025-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575))
|
||||||
|
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10))
|
||||||
|
|
||||||
|
# [5.23.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.3...v5.23.0-dev.4) (2025-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c))
|
||||||
|
|
||||||
|
# [5.23.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.2...v5.23.0-dev.3) (2025-05-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a))
|
||||||
|
|
||||||
|
# [5.23.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.1...v5.23.0-dev.2) (2025-05-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad))
|
||||||
|
|
||||||
|
# [5.23.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0-dev.1) (2025-05-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1))
|
||||||
|
|
||||||
# [5.22.0](https://github.com/ReVanced/revanced-patches/compare/v5.21.0...v5.22.0) (2025-05-01)
|
# [5.22.0](https://github.com/ReVanced/revanced-patches/compare/v5.21.0...v5.22.0) (2025-05-01)
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ public class HideAdsPatch {
|
|||||||
|
|
||||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
// Filter HeaderBlock with known ads until next HeaderBlock.
|
||||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
if (currentBlock instanceof HeaderBlock headerBlock) {
|
||||||
StyledText headerText = headerBlock.component20();
|
StyledText headerText = headerBlock.getTitle();
|
||||||
if (headerText != null) {
|
if (headerText != null) {
|
||||||
skipFullHeader = false;
|
skipFullHeader = false;
|
||||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
||||||
|
@ -3,8 +3,7 @@ package nl.nu.performance.api.client.objects;
|
|||||||
import nl.nu.performance.api.client.interfaces.Block;
|
import nl.nu.performance.api.client.interfaces.Block;
|
||||||
|
|
||||||
public class HeaderBlock extends Block {
|
public class HeaderBlock extends Block {
|
||||||
// returns title
|
public final StyledText getTitle() {
|
||||||
public final StyledText component20() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
throw new UnsupportedOperationException("Stub");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
extensions/primevideo/build.gradle.kts
Normal file
4
extensions/primevideo/build.gradle.kts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:primevideo:stub"))
|
||||||
|
}
|
1
extensions/primevideo/src/main/AndroidManifest.xml
Normal file
1
extensions/primevideo/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@ -0,0 +1,36 @@
|
|||||||
|
package app.revanced.extension.primevideo.ads;
|
||||||
|
|
||||||
|
import com.amazon.avod.fsm.SimpleTrigger;
|
||||||
|
import com.amazon.avod.media.ads.AdBreak;
|
||||||
|
import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
|
||||||
|
import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
|
||||||
|
import com.amazon.avod.media.playback.VideoPlayer;
|
||||||
|
import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class SkipAdsPatch {
|
||||||
|
public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
|
||||||
|
try {
|
||||||
|
AdBreak adBreak = trigger.getBreak();
|
||||||
|
|
||||||
|
// There are two scenarios when entering the original method:
|
||||||
|
// 1. Player naturally entered an ad break while watching a video.
|
||||||
|
// 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
|
||||||
|
// user is forced to watch an ad before continuing.
|
||||||
|
//
|
||||||
|
// Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
|
||||||
|
// target. Otherwise, just calculate when the ad break should end and skip to there.
|
||||||
|
if (trigger.getSeekStartPosition() != null)
|
||||||
|
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
|
||||||
|
else
|
||||||
|
player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
|
||||||
|
|
||||||
|
// Send "end of ads" trigger to state machine so everything doesn't get whacky.
|
||||||
|
state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Failed skipping ads", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
extensions/primevideo/stub/build.gradle.kts
Normal file
17
extensions/primevideo/stub/build.gradle.kts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id(libs.plugins.android.library.get().pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
1
extensions/primevideo/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/primevideo/stub/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.amazon.avod.fsm;
|
||||||
|
|
||||||
|
public final class SimpleTrigger<T> implements Trigger<T> {
|
||||||
|
public SimpleTrigger(T triggerType) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.fsm;
|
||||||
|
|
||||||
|
public abstract class StateBase<S, T> {
|
||||||
|
// This method orginally has protected access (modified in patch code).
|
||||||
|
public void doTrigger(Trigger<T> trigger) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.fsm;
|
||||||
|
|
||||||
|
public interface Trigger<T> {
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.media;
|
||||||
|
|
||||||
|
public final class TimeSpan {
|
||||||
|
public long getTotalMilliseconds() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.media.ads;
|
||||||
|
|
||||||
|
import com.amazon.avod.media.TimeSpan;
|
||||||
|
|
||||||
|
public interface AdBreak {
|
||||||
|
TimeSpan getDurationExcludingAux();
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
public abstract class AdBreakState extends AdEnabledPlaybackState {
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
import com.amazon.avod.media.ads.AdBreak;
|
||||||
|
import com.amazon.avod.media.TimeSpan;
|
||||||
|
|
||||||
|
public class AdBreakTrigger {
|
||||||
|
public AdBreak getBreak() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan getSeekTarget() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan getSeekStartPosition() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
import com.amazon.avod.fsm.StateBase;
|
||||||
|
import com.amazon.avod.media.playback.state.PlayerStateType;
|
||||||
|
import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
|
||||||
|
|
||||||
|
public class AdEnabledPlaybackState extends StateBase<PlayerStateType, PlayerTriggerType> {
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
public enum AdEnabledPlayerTriggerType {
|
||||||
|
NO_MORE_ADS_SKIP_TRANSITION
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
public class ServerInsertedAdBreakState extends AdBreakState {
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.media.playback;
|
||||||
|
|
||||||
|
public interface VideoPlayer {
|
||||||
|
long getCurrentPosition();
|
||||||
|
|
||||||
|
void seekTo(long positionMs);
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.playback.state;
|
||||||
|
|
||||||
|
public interface PlayerStateType {
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.playback.state.trigger;
|
||||||
|
|
||||||
|
public interface PlayerTriggerType {
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -15,10 +16,16 @@ import android.os.Build;
|
|||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
|
import app.revanced.extension.shared.requests.Route;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
@ -29,10 +36,24 @@ public class GmsCoreSupport {
|
|||||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
private static final Uri GMS_CORE_PROVIDER
|
||||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||||
private static final String DONT_KILL_MY_APP_LINK
|
private static final String DONT_KILL_MY_APP_URL
|
||||||
= "https://dontkillmyapp.com";
|
= "https://dontkillmyapp.com/";
|
||||||
|
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
||||||
|
= new Route(GET, "/api/v2/{manufacturer}.json");
|
||||||
|
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
||||||
|
= "?app=MicroG";
|
||||||
|
private static final String BUILD_MANUFACTURER
|
||||||
|
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a manufacturer specific page exists on DontKillMyApp.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
private static void open(String queryOrLink) {
|
private static void open(String queryOrLink) {
|
||||||
|
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||||
|
|
||||||
Intent intent;
|
Intent intent;
|
||||||
try {
|
try {
|
||||||
// Check if queryOrLink is a valid URL.
|
// Check if queryOrLink is a valid URL.
|
||||||
@ -86,7 +107,7 @@ public class GmsCoreSupport {
|
|||||||
|
|
||||||
// Do not exit. If the app exits before launch completes (and without
|
// Do not exit. If the app exits before launch completes (and without
|
||||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||||
// no toast will be shown and the app will continually be relaunched
|
// no toast will be shown and the app will continually relaunch
|
||||||
// with the appearance of a hung app.
|
// with the appearance of a hung app.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,11 +143,12 @@ public class GmsCoreSupport {
|
|||||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||||
|
checkIfDontKillMyAppSupportsManufacturer();
|
||||||
|
|
||||||
showBatteryOptimizationDialog(context,
|
showBatteryOptimizationDialog(context,
|
||||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||||
"gms_core_dialog_open_website_text",
|
"gms_core_dialog_open_website_text",
|
||||||
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
(dialog, id) -> openDontKillMyApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@ -141,6 +163,48 @@ public class GmsCoreSupport {
|
|||||||
activity.startActivityForResult(intent, 0);
|
activity.startActivityForResult(intent, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void checkIfDontKillMyAppSupportsManufacturer() {
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
try {
|
||||||
|
final long start = System.currentTimeMillis();
|
||||||
|
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||||
|
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
||||||
|
connection.setConnectTimeout(5000);
|
||||||
|
connection.setReadTimeout(5000);
|
||||||
|
|
||||||
|
final boolean supported = connection.getResponseCode() == 200;
|
||||||
|
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
||||||
|
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
||||||
|
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
||||||
|
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
||||||
|
+ BUILD_MANUFACTURER, ex);
|
||||||
|
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openDontKillMyApp() {
|
||||||
|
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
|
String manufacturerPageToOpen;
|
||||||
|
if (manufacturerSupported == null) {
|
||||||
|
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
||||||
|
// and the user spends less than 1 second reading what's on screen.
|
||||||
|
// Instead of waiting for the fetch (which may timeout),
|
||||||
|
// open the website without a vendor.
|
||||||
|
manufacturerPageToOpen = "";
|
||||||
|
} else if (manufacturerSupported) {
|
||||||
|
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
||||||
|
} else {
|
||||||
|
// No manufacturer specific page exists. Open the general page.
|
||||||
|
manufacturerPageToOpen = "general";
|
||||||
|
}
|
||||||
|
|
||||||
|
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||||
*/
|
*/
|
||||||
|
@ -371,7 +371,7 @@ public class Utils {
|
|||||||
if (language != AppLanguage.DEFAULT) {
|
if (language != AppLanguage.DEFAULT) {
|
||||||
// Create a new context with the desired language.
|
// Create a new context with the desired language.
|
||||||
Logger.printDebug(() -> "Using app language: " + language);
|
Logger.printDebug(() -> "Using app language: " + language);
|
||||||
Configuration config = appContext.getResources().getConfiguration();
|
Configuration config = new Configuration(appContext.getResources().getConfiguration());
|
||||||
config.setLocale(language.getLocale());
|
config.setLocale(language.getLocale());
|
||||||
context = appContext.createConfigurationContext(config);
|
context = appContext.createConfigurationContext(config);
|
||||||
}
|
}
|
||||||
@ -391,16 +391,47 @@ public class Utils {
|
|||||||
private static Boolean isRightToLeftTextLayout;
|
private static Boolean isRightToLeftTextLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc).
|
||||||
|
* If this should match any ReVanced language override then instead use
|
||||||
|
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
|
* This is the default locale of the device, which may differ if
|
||||||
|
* {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language.
|
||||||
*/
|
*/
|
||||||
public static boolean isRightToLeftTextLayout() {
|
public static boolean isRightToLeftLocale() {
|
||||||
if (isRightToLeftTextLayout == null) {
|
if (isRightToLeftTextLayout == null) {
|
||||||
String displayLanguage = Locale.getDefault().getDisplayLanguage();
|
isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault());
|
||||||
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
|
||||||
}
|
}
|
||||||
return isRightToLeftTextLayout;
|
return isRightToLeftTextLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc).
|
||||||
|
*/
|
||||||
|
public static boolean isRightToLeftLocale(Locale locale) {
|
||||||
|
String displayLanguage = locale.getDisplayLanguage();
|
||||||
|
return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A UTF8 string containing a left-to-right or right-to-left
|
||||||
|
* character of the device locale. If this should match any ReVanced language
|
||||||
|
* override then instead use {@link #getTextDirectionString(Locale)} with
|
||||||
|
* {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
|
*/
|
||||||
|
public static String getTextDirectionString() {
|
||||||
|
return getTextDirectionString(isRightToLeftLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTextDirectionString(Locale locale) {
|
||||||
|
return getTextDirectionString(isRightToLeftLocale(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTextDirectionString(boolean isRightToLeft) {
|
||||||
|
return isRightToLeft
|
||||||
|
? "\u200F" // u200F = right to left character.
|
||||||
|
: "\u200E"; // u200E = left to right character.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the text contains at least 1 number character,
|
* @return if the text contains at least 1 number character,
|
||||||
* including any unicode numbers such as Arabic.
|
* including any unicode numbers such as Arabic.
|
||||||
@ -692,9 +723,10 @@ public class Utils {
|
|||||||
/**
|
/**
|
||||||
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
||||||
*/
|
*/
|
||||||
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
|
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
||||||
if (original == null) return "";
|
if (original == null) return "";
|
||||||
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
|
return punctuationPattern.matcher(original).replaceAll("")
|
||||||
|
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -726,7 +758,7 @@ public class Utils {
|
|||||||
final String sortValue;
|
final String sortValue;
|
||||||
switch (preferenceSort) {
|
switch (preferenceSort) {
|
||||||
case BY_TITLE:
|
case BY_TITLE:
|
||||||
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
|
sortValue = removePunctuationToLowercase(preference.getTitle());
|
||||||
break;
|
break;
|
||||||
case BY_KEY:
|
case BY_KEY:
|
||||||
sortValue = preference.getKey();
|
sortValue = preference.getKey();
|
||||||
@ -810,4 +842,12 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
return getResourceColor(colorString);
|
return getResourceColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int clamp(int value, int lower, int upper) {
|
||||||
|
return Math.max(lower, Math.min(value, upper));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float clamp(float value, float lower, float upper) {
|
||||||
|
return Math.max(lower, Math.min(value, upper));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,9 +89,11 @@ public enum AppLanguage {
|
|||||||
ZU;
|
ZU;
|
||||||
|
|
||||||
private final String language;
|
private final String language;
|
||||||
|
private final Locale locale;
|
||||||
|
|
||||||
AppLanguage() {
|
AppLanguage() {
|
||||||
language = name().toLowerCase(Locale.US);
|
language = name().toLowerCase(Locale.US);
|
||||||
|
locale = Locale.forLanguageTag(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,6 +114,6 @@ public enum AppLanguage {
|
|||||||
return Locale.getDefault();
|
return Locale.getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Locale.forLanguageTag(language);
|
return locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,10 @@ public class NoTitlePreferenceCategory extends PreferenceCategory {
|
|||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
public NoTitlePreferenceCategory(Context context) {
|
public NoTitlePreferenceCategory(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.ListPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreferenceList that sorts itself.
|
||||||
|
* By default the first entry is preserved in its original position,
|
||||||
|
* and all other entries are sorted alphabetically.
|
||||||
|
*
|
||||||
|
* Ideally the 'keep first entries to preserve' is an xml parameter,
|
||||||
|
* but currently that's not so simple since Extensions code cannot use
|
||||||
|
* generated code from the Patches repo (which is required for custom xml parameters).
|
||||||
|
*
|
||||||
|
* If any class wants to use a different getFirstEntriesToPreserve value,
|
||||||
|
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SortedListPreference extends ListPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the current list entries.
|
||||||
|
*
|
||||||
|
* @param firstEntriesToPreserve The number of entries to preserve in their original position.
|
||||||
|
*/
|
||||||
|
public void sortEntryAndValues(int firstEntriesToPreserve) {
|
||||||
|
CharSequence[] entries = getEntries();
|
||||||
|
CharSequence[] entryValues = getEntryValues();
|
||||||
|
if (entries == null || entryValues == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int entrySize = entries.length;
|
||||||
|
if (entrySize != entryValues.length) {
|
||||||
|
// Xml array declaration has a missing/extra entry.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
||||||
|
SortedMap<String, Pair<CharSequence, CharSequence>> lastEntries = new TreeMap<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < entrySize; i++) {
|
||||||
|
Pair<CharSequence, CharSequence> pair = new Pair<>(entries[i], entryValues[i]);
|
||||||
|
if (i < firstEntriesToPreserve) {
|
||||||
|
firstEntries.add(pair);
|
||||||
|
} else {
|
||||||
|
lastEntries.put(Utils.removePunctuationToLowercase(pair.first), pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
||||||
|
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (Pair<CharSequence, CharSequence> pair : firstEntries) {
|
||||||
|
sortedEntries[i] = pair.first;
|
||||||
|
sortedEntryValues[i] = pair.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<CharSequence, CharSequence> pair : lastEntries.values()) {
|
||||||
|
sortedEntries[i] = pair.first;
|
||||||
|
sortedEntryValues[i] = pair.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setEntries(sortedEntries);
|
||||||
|
super.setEntryValues(sortedEntryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getFirstEntriesToPreserve() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
}
|
@ -130,6 +130,7 @@ public final class UnlockPremiumPatch {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point. Remove ads sections from home.
|
* Injection point. Remove ads sections from home.
|
||||||
|
* Depends on patching protobuffer list remove method.
|
||||||
*/
|
*/
|
||||||
public static void removeHomeSections(List<Section> sections) {
|
public static void removeHomeSections(List<Section> sections) {
|
||||||
try {
|
try {
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package app.revanced.extension.spotify.misc.privacy;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class SanitizeSharingLinksPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters that are considered undesirable and should be stripped away.
|
||||||
|
*/
|
||||||
|
private static final List<String> SHARE_PARAMETERS_TO_REMOVE = List.of(
|
||||||
|
"si", // Share tracking parameter.
|
||||||
|
"utm_source" // Share source, such as "copy-link".
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String sanitizeUrl(String url) {
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
Uri.Builder builder = uri.buildUpon().clearQuery();
|
||||||
|
|
||||||
|
for (String paramName : uri.getQueryParameterNames()) {
|
||||||
|
if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) {
|
||||||
|
for (String value : uri.getQueryParameters(paramName)) {
|
||||||
|
builder.appendQueryParameter(paramName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build().toString();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "sanitizeUrl failure", ex);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,18 @@
|
|||||||
package app.revanced.extension.youtube;
|
package app.revanced.extension.youtube;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.clamp;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
import android.text.TextPaint;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
@ -121,4 +129,43 @@ public class ThemeHelper {
|
|||||||
window.setNavigationBarContrastEnforced(true);
|
window.setNavigationBarContrastEnforced(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
|
||||||
|
* <p>
|
||||||
|
* If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
|
||||||
|
* If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
|
||||||
|
* The alpha channel remains unchanged.
|
||||||
|
*
|
||||||
|
* @param color The input color to adjust, in ARGB format.
|
||||||
|
* @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
|
||||||
|
* or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
|
||||||
|
* @return The adjusted color in ARGB format.
|
||||||
|
*/
|
||||||
|
public static int adjustColorBrightness(int color, float factor) {
|
||||||
|
final int alpha = Color.alpha(color);
|
||||||
|
int red = Color.red(color);
|
||||||
|
int green = Color.green(color);
|
||||||
|
int blue = Color.blue(color);
|
||||||
|
|
||||||
|
if (factor > 1.0f) {
|
||||||
|
// Lighten: Interpolate toward white (255)
|
||||||
|
final float t = 1.0f - (1.0f / factor); // Interpolation parameter
|
||||||
|
red = Math.round(red + (255 - red) * t);
|
||||||
|
green = Math.round(green + (255 - green) * t);
|
||||||
|
blue = Math.round(blue + (255 - blue) * t);
|
||||||
|
} else {
|
||||||
|
// Darken or no change: Scale toward black
|
||||||
|
red = (int) (red * factor);
|
||||||
|
green = (int) (green * factor);
|
||||||
|
blue = (int) (blue * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure values are within [0, 255]
|
||||||
|
red = clamp(red, 0, 255);
|
||||||
|
green = clamp(green, 0, 255);
|
||||||
|
blue = clamp(blue, 0, 255);
|
||||||
|
|
||||||
|
return Color.argb(alpha, red, green, blue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class HideRelatedVideoOverlayPatch {
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean hideRelatedVideoOverlay() {
|
||||||
|
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,6 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@ -69,13 +68,6 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static volatile String lastPrefetchedVideoId;
|
private static volatile String lastPrefetchedVideoId;
|
||||||
|
|
||||||
public static void onRYDStatusChange(boolean rydEnabled) {
|
|
||||||
ReturnYouTubeDislikeApi.resetRateLimits();
|
|
||||||
// Must remove all values to protect against using stale data
|
|
||||||
// if the user enables RYD while a video is on screen.
|
|
||||||
clearData();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void clearData() {
|
private static void clearData() {
|
||||||
currentVideoData = null;
|
currentVideoData = null;
|
||||||
lastLithoShortsVideoData = null;
|
lastLithoShortsVideoData = null;
|
||||||
@ -274,7 +266,7 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
Logger.printDebug(() -> "Adding rolling number TextView changes");
|
Logger.printDebug(() -> "Adding rolling number TextView changes");
|
||||||
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
||||||
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||||
if (Utils.isRightToLeftTextLayout()) {
|
if (Utils.isRightToLeftLocale()) {
|
||||||
view.setCompoundDrawables(null, null, separator, null);
|
view.setCompoundDrawables(null, null, separator, null);
|
||||||
} else {
|
} else {
|
||||||
view.setCompoundDrawables(separator, null, null, null);
|
view.setCompoundDrawables(separator, null, null, null);
|
||||||
|
@ -36,7 +36,7 @@ public final class WideSearchbarPatch {
|
|||||||
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||||
8, Resources.getSystem().getDisplayMetrics());
|
8, Resources.getSystem().getDisplayMetrics());
|
||||||
|
|
||||||
if (Utils.isRightToLeftTextLayout()) {
|
if (Utils.isRightToLeftLocale()) {
|
||||||
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
||||||
} else {
|
} else {
|
||||||
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
|
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
|
||||||
|
@ -177,10 +177,7 @@ public final class AdsFilter extends Filter {
|
|||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == playerShoppingShelf) {
|
if (matchedGroup == playerShoppingShelf) {
|
||||||
if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) {
|
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for the index because of likelihood of false positives.
|
// Check for the index because of likelihood of false positives.
|
||||||
@ -198,13 +195,10 @@ public final class AdsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == channelProfile) {
|
if (matchedGroup == channelProfile) {
|
||||||
if (visitStoreButton.check(protobufBufferArray).isFiltered()) {
|
return visitStoreButton.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,29 +99,23 @@ final class ButtonsFilter extends Filter {
|
|||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == likeSubscribeGlow) {
|
if (matchedGroup == likeSubscribeGlow) {
|
||||||
if ((path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
||||||
&& path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
|
&& path.contains(ANIMATED_VECTOR_TYPE_PATH);
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current matched group is the action bar group,
|
// If the current matched group is the action bar group,
|
||||||
// in case every filter group is enabled, hide the action bar.
|
// in case every filter group is enabled, hide the action bar.
|
||||||
if (matchedGroup == actionBarGroup) {
|
if (matchedGroup == actionBarGroup) {
|
||||||
if (!isEveryFilterGroupEnabled()) {
|
return isEveryFilterGroupEnabled();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (matchedGroup == bufferFilterPathGroup) {
|
|
||||||
// Make sure the current path is the right one
|
|
||||||
// to avoid false positives.
|
|
||||||
if (!path.startsWith(VIDEO_ACTION_BAR_PATH)) return false;
|
|
||||||
|
|
||||||
// In case the group list has no match, return false.
|
|
||||||
if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
if (matchedGroup == bufferFilterPathGroup) {
|
||||||
|
// Make sure the current path is the right one
|
||||||
|
// to avoid false positives.
|
||||||
|
return path.startsWith(VIDEO_ACTION_BAR_PATH)
|
||||||
|
&& bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,22 +88,15 @@ final class CommentsFilter extends Filter {
|
|||||||
if (matchedGroup == commentComposer) {
|
if (matchedGroup == commentComposer) {
|
||||||
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
|
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
|
||||||
// also hidden because the buffer is exactly the same and there's no way selectively hide.
|
// also hidden because the buffer is exactly the same and there's no way selectively hide.
|
||||||
if (contentIndex == 0
|
return contentIndex == 0
|
||||||
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
|
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
|
||||||
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered()) {
|
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == filterChipBar) {
|
if (matchedGroup == filterChipBar) {
|
||||||
if (aiCommentsSummary.check(protobufBufferArray).isFiltered()) {
|
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,9 +153,11 @@ final class CustomFilter extends Filter {
|
|||||||
if (custom.startsWith && contentIndex != 0) {
|
if (custom.startsWith && contentIndex != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
|
|
||||||
return false;
|
if (custom.bufferSearch == null) {
|
||||||
|
return true; // No buffer filter, only path filtering.
|
||||||
}
|
}
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
|
return custom.bufferSearch.matches(protobufBufferArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,6 +28,11 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
"cell_expandable_metadata.eml"
|
"cell_expandable_metadata.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final StringFilterGroup askSection = new StringFilterGroup(
|
||||||
|
Settings.HIDE_ASK_SECTION,
|
||||||
|
"youchat_entrypoint.eml"
|
||||||
|
);
|
||||||
|
|
||||||
final StringFilterGroup attributesSection = new StringFilterGroup(
|
final StringFilterGroup attributesSection = new StringFilterGroup(
|
||||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||||
"gaming_section",
|
"gaming_section",
|
||||||
@ -73,6 +78,7 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
aiGeneratedVideoSummarySection,
|
aiGeneratedVideoSummarySection,
|
||||||
|
askSection,
|
||||||
attributesSection,
|
attributesSection,
|
||||||
infoCardsSection,
|
infoCardsSection,
|
||||||
howThisWasMadeSection,
|
howThisWasMadeSection,
|
||||||
@ -88,13 +94,9 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
if (exceptions.matches(path)) return false;
|
if (exceptions.matches(path)) return false;
|
||||||
|
|
||||||
if (matchedGroup == macroMarkersCarousel) {
|
if (matchedGroup == macroMarkersCarousel) {
|
||||||
if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) {
|
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters litho based components.
|
* Filters litho based components.
|
||||||
*
|
*
|
||||||
@ -62,10 +59,7 @@ abstract class Filter {
|
|||||||
* Called after an enabled filter has been matched.
|
* Called after an enabled filter has been matched.
|
||||||
* Default implementation is to always filter the matched component and log the action.
|
* Default implementation is to always filter the matched component and log the action.
|
||||||
* Subclasses can perform additional or different checks if needed.
|
* Subclasses can perform additional or different checks if needed.
|
||||||
* <p>
|
*
|
||||||
* If the content is to be filtered, subclasses should always
|
|
||||||
* call this method (and never return a plain 'true').
|
|
||||||
* That way the logs will always show when a component was filtered and which filter hide it.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Method is called off the main thread.
|
* Method is called off the main thread.
|
||||||
*
|
*
|
||||||
@ -76,14 +70,6 @@ abstract class Filter {
|
|||||||
*/
|
*/
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (BaseSettings.DEBUG.get()) {
|
|
||||||
String filterSimpleName = getClass().getSimpleName();
|
|
||||||
if (contentType == FilterContentType.IDENTIFIER) {
|
|
||||||
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
|
|
||||||
} else {
|
|
||||||
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -576,7 +576,7 @@ final class KeywordContentFilter extends Filter {
|
|||||||
MutableReference<String> matchRef = new MutableReference<>();
|
MutableReference<String> matchRef = new MutableReference<>();
|
||||||
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
||||||
updateStats(true, matchRef.value);
|
updateStats(true, matchRef.value);
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStats(false, null);
|
updateStats(false, null);
|
||||||
|
@ -34,12 +34,11 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
private final StringFilterGroup notifyMe;
|
private final StringFilterGroup notifyMe;
|
||||||
private final StringFilterGroup singleItemInformationPanel;
|
private final StringFilterGroup singleItemInformationPanel;
|
||||||
private final StringFilterGroup expandableMetadata;
|
private final StringFilterGroup expandableMetadata;
|
||||||
private final ByteArrayFilterGroup searchResultRecommendations;
|
|
||||||
private final StringFilterGroup searchResultVideo;
|
|
||||||
private final StringFilterGroup compactChannelBarInner;
|
private final StringFilterGroup compactChannelBarInner;
|
||||||
private final StringFilterGroup compactChannelBarInnerButton;
|
private final StringFilterGroup compactChannelBarInnerButton;
|
||||||
private final ByteArrayFilterGroup joinMembershipButton;
|
private final ByteArrayFilterGroup joinMembershipButton;
|
||||||
private final StringFilterGroup horizontalShelves;
|
private final StringFilterGroup horizontalShelves;
|
||||||
|
private final ByteArrayFilterGroup ticketShelf;
|
||||||
|
|
||||||
public LayoutComponentsFilter() {
|
public LayoutComponentsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
@ -233,14 +232,9 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"mixed_content_shelf"
|
"mixed_content_shelf"
|
||||||
);
|
);
|
||||||
|
|
||||||
searchResultVideo = new StringFilterGroup(
|
final var searchResultRecommendationLabels = new StringFilterGroup(
|
||||||
Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
|
Settings.HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS,
|
||||||
"search_video_with_context.eml"
|
"endorsement_header_footer.eml"
|
||||||
);
|
|
||||||
|
|
||||||
searchResultRecommendations = new ByteArrayFilterGroup(
|
|
||||||
Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
|
|
||||||
"endorsement_header_footer"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
horizontalShelves = new StringFilterGroup(
|
horizontalShelves = new StringFilterGroup(
|
||||||
@ -251,6 +245,11 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"horizontal_tile_shelf.eml"
|
"horizontal_tile_shelf.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ticketShelf = new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_TICKET_SHELF,
|
||||||
|
"ticket"
|
||||||
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
expandableMetadata,
|
expandableMetadata,
|
||||||
inFeedSurvey,
|
inFeedSurvey,
|
||||||
@ -258,7 +257,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
compactChannelBar,
|
compactChannelBar,
|
||||||
communityPosts,
|
communityPosts,
|
||||||
paidPromotion,
|
paidPromotion,
|
||||||
searchResultVideo,
|
searchResultRecommendationLabels,
|
||||||
latestPosts,
|
latestPosts,
|
||||||
channelWatermark,
|
channelWatermark,
|
||||||
communityGuidelines,
|
communityGuidelines,
|
||||||
@ -293,50 +292,29 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
// From 2025, the medical information panel is no longer shown in the search results.
|
// From 2025, the medical information panel is no longer shown in the search results.
|
||||||
// Therefore, this identifier does not filter when the search bar is activated.
|
// Therefore, this identifier does not filter when the search bar is activated.
|
||||||
if (matchedGroup == singleItemInformationPanel) {
|
if (matchedGroup == singleItemInformationPanel) {
|
||||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive()) {
|
return PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedGroup == searchResultVideo) {
|
|
||||||
if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The groups are excluded from the filter due to the exceptions list below.
|
// The groups are excluded from the filter due to the exceptions list below.
|
||||||
// Filter them separately here.
|
// Filter them separately here.
|
||||||
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
|
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) {
|
||||||
{
|
return true;
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||||
|
|
||||||
if (matchedGroup == compactChannelBarInner) {
|
if (matchedGroup == compactChannelBarInner) {
|
||||||
if (compactChannelBarInnerButton.check(path).isFiltered()) {
|
return compactChannelBarInnerButton.check(path).isFiltered()
|
||||||
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
||||||
// it's safe to assume that the button is the only thing that should be hidden.
|
// it's safe to assume that the button is the only thing that should be hidden.
|
||||||
if (joinMembershipButton.check(protobufBufferArray).isFiltered()) {
|
&& joinMembershipButton.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == horizontalShelves) {
|
if (matchedGroup == horizontalShelves) {
|
||||||
if (contentIndex == 0 && hideShelves()) {
|
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,6 +7,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.youtube.StringTrieSearch;
|
import app.revanced.extension.youtube.StringTrieSearch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@ -87,6 +88,10 @@ public final class LithoFilterPatch {
|
|||||||
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
||||||
*/
|
*/
|
||||||
private static final ThreadLocal<ByteBuffer> bufferThreadLocal = new ThreadLocal<>();
|
private static final ThreadLocal<ByteBuffer> bufferThreadLocal = new ThreadLocal<>();
|
||||||
|
/**
|
||||||
|
* Results of calling {@link #filter(String, StringBuilder)}.
|
||||||
|
*/
|
||||||
|
private static final ThreadLocal<Boolean> filterResult = new ThreadLocal<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (Filter filter : filters) {
|
for (Filter filter : filters) {
|
||||||
@ -110,12 +115,29 @@ public final class LithoFilterPatch {
|
|||||||
if (!group.includeInSearch()) {
|
if (!group.includeInSearch()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String pattern : group.filters) {
|
for (String pattern : group.filters) {
|
||||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
String filterSimpleName = filter.getClass().getSimpleName();
|
||||||
|
|
||||||
|
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
|
||||||
|
matchedLength, callbackParameter) -> {
|
||||||
if (!group.isEnabled()) return false;
|
if (!group.isEnabled()) return false;
|
||||||
|
|
||||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||||
return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
|
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
||||||
group, type, matchedStartIndex);
|
parameters.path, parameters.protoBuffer, group, type, matchedStartIndex);
|
||||||
|
|
||||||
|
if (isFiltered && BaseSettings.DEBUG.get()) {
|
||||||
|
if (type == Filter.FilterContentType.IDENTIFIER) {
|
||||||
|
Logger.printDebug(() -> "Filtered " + filterSimpleName
|
||||||
|
+ " identifier: " + parameters.identifier);
|
||||||
|
} else {
|
||||||
|
Logger.printDebug(() -> "Filtered " + filterSimpleName
|
||||||
|
+ " path: " + parameters.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFiltered;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -140,11 +162,22 @@ public final class LithoFilterPatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean shouldFilter() {
|
||||||
|
Boolean shouldFilter = filterResult.get();
|
||||||
|
return shouldFilter != null && shouldFilter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
|
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
public static void filter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||||
public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
|
filterResult.set(handleFiltering(lithoIdentifier, pathBuilder));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean handleFiltering(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||||
try {
|
try {
|
||||||
if (pathBuilder.length() == 0) {
|
if (pathBuilder.length() == 0) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -99,7 +99,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == videoQualityMenuFooter) {
|
if (matchedGroup == videoQualityMenuFooter) {
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentIndex != 0) {
|
if (contentIndex != 0) {
|
||||||
@ -111,11 +111,6 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
|
return flyoutFilterGroupList.check(protobufBufferArray).isFiltered();
|
||||||
// Super class handles logging.
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,27 +278,18 @@ public final class ShortsFilter extends Filter {
|
|||||||
if (contentType == FilterContentType.PATH) {
|
if (contentType == FilterContentType.PATH) {
|
||||||
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
|
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
|
||||||
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
|
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
|
||||||
if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
|
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == shortsCompactFeedVideoPath) {
|
if (matchedGroup == shortsCompactFeedVideoPath) {
|
||||||
if (shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
|
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video action buttons (comment, share, remix) have the same path.
|
// Video action buttons (comment, share, remix) have the same path.
|
||||||
// Like and dislike are separate path filters and don't require buffer searching.
|
// Like and dislike are separate path filters and don't require buffer searching.
|
||||||
if (matchedGroup == shortsActionBar) {
|
if (matchedGroup == shortsActionBar) {
|
||||||
if (actionButton.check(path).isFiltered()
|
return actionButton.check(path).isFiltered()
|
||||||
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
|
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == suggestedAction) {
|
if (matchedGroup == suggestedAction) {
|
||||||
@ -306,28 +297,23 @@ public final class ShortsFilter extends Filter {
|
|||||||
// This has a secondary effect of hiding all new un-identified actions
|
// This has a secondary effect of hiding all new un-identified actions
|
||||||
// under the assumption that the user wants all suggestions hidden.
|
// under the assumption that the user wants all suggestions hidden.
|
||||||
if (isEverySuggestedActionFilterEnabled()) {
|
if (isEverySuggestedActionFilterEnabled()) {
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
|
return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
return true;
|
||||||
// Feed/search identifier components.
|
|
||||||
if (matchedGroup == shelfHeader) {
|
|
||||||
// Because the header is used in watch history and possibly other places, check for the index,
|
|
||||||
// which is 0 when the shelf header is used for Shorts.
|
|
||||||
if (contentIndex != 0) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldHideShortsFeedItems()) return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Super class handles logging.
|
// Feed/search identifier components.
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
if (matchedGroup == shelfHeader) {
|
||||||
|
// Because the header is used in watch history and possibly other places, check for the index,
|
||||||
|
// which is 0 when the shelf header is used for Shorts.
|
||||||
|
if (contentIndex != 0) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldHideShortsFeedItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldHideShortsFeedItems() {
|
private static boolean shouldHideShortsFeedItems() {
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
package app.revanced.extension.youtube.patches.playback.speed;
|
package app.revanced.extension.youtube.patches.playback.speed;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
@ -21,8 +17,6 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class CustomPlaybackSpeedPatch {
|
public class CustomPlaybackSpeedPatch {
|
||||||
|
|
||||||
private static final float PLAYBACK_SPEED_AUTO = Settings.PLAYBACK_SPEED_DEFAULT.defaultValue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
||||||
* <p>
|
* <p>
|
||||||
@ -47,11 +41,6 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
*/
|
*/
|
||||||
private static long lastTimeOldPlaybackMenuInvoked;
|
private static long lastTimeOldPlaybackMenuInvoked;
|
||||||
|
|
||||||
/**
|
|
||||||
* PreferenceList entries and values, of all available playback speeds.
|
|
||||||
*/
|
|
||||||
private static String[] preferenceListEntries, preferenceListEntryValues;
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
||||||
|
|
||||||
@ -117,33 +106,6 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a settings preference list with the available playback speeds.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public static void initializeListPreference(ListPreference preference) {
|
|
||||||
if (preferenceListEntries == null) {
|
|
||||||
final int numberOfEntries = customPlaybackSpeeds.length + 1;
|
|
||||||
preferenceListEntries = new String[numberOfEntries];
|
|
||||||
preferenceListEntryValues = new String[numberOfEntries];
|
|
||||||
|
|
||||||
// Auto speed (same behavior as unpatched).
|
|
||||||
preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
|
|
||||||
preferenceListEntryValues[0] = String.valueOf(PLAYBACK_SPEED_AUTO);
|
|
||||||
|
|
||||||
int i = 1;
|
|
||||||
for (float speed : customPlaybackSpeeds) {
|
|
||||||
String speedString = String.valueOf(speed);
|
|
||||||
preferenceListEntries[i] = speedString + "x";
|
|
||||||
preferenceListEntryValues[i] = speedString;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setEntries(preferenceListEntries);
|
|
||||||
preference.setEntryValues(preferenceListEntryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches.theme;
|
package app.revanced.extension.youtube.patches.theme;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.clamp;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@ -378,14 +379,4 @@ public final class SeekbarColorPatch {
|
|||||||
return originalColor;
|
return originalColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
|
||||||
private static int clamp(int value, int lower, int upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
|
||||||
private static float clamp(float value, float lower, float upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -235,7 +235,7 @@ public class ReturnYouTubeDislike {
|
|||||||
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
|
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
|
||||||
|
|
||||||
if (!compactLayout) {
|
if (!compactLayout) {
|
||||||
String leftSeparatorString = getTextDirectionString();
|
String leftSeparatorString = Utils.getTextDirectionString();
|
||||||
final Spannable leftSeparatorSpan;
|
final Spannable leftSeparatorSpan;
|
||||||
if (isRollingNumber) {
|
if (isRollingNumber) {
|
||||||
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
||||||
@ -279,12 +279,6 @@ public class ReturnYouTubeDislike {
|
|||||||
return new SpannableString(builder);
|
return new SpannableString(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull String getTextDirectionString() {
|
|
||||||
return Utils.isRightToLeftTextLayout()
|
|
||||||
? "\u200F" // u200F = right to left character
|
|
||||||
: "\u200E"; // u200E = left to right character
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If the text is likely for a previously created likes/dislikes segmented span.
|
* @return If the text is likely for a previously created likes/dislikes segmented span.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.extension.youtube.returnyoutubedislike.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows tapping the RYD about preference to open the website.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ReturnYouTubeDislikeAboutPreference extends UrlLinkPreference {
|
||||||
|
{
|
||||||
|
externalUrl = "https://returnyoutubedislike.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
package app.revanced.extension.youtube.returnyoutubedislike.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||||
|
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends PreferenceCategory {
|
||||||
|
|
||||||
|
private static final boolean SHOW_RYD_DEBUG_STATS = BaseSettings.DEBUG.get();
|
||||||
|
|
||||||
|
private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
|
||||||
|
if (value == 0) {
|
||||||
|
return str(summaryStringZeroKey);
|
||||||
|
}
|
||||||
|
return str(summaryStringOneOrMoreKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createMillisecondStringFromNumber(long number) {
|
||||||
|
return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateView(ViewGroup parent) {
|
||||||
|
if (!SHOW_RYD_DEBUG_STATS) {
|
||||||
|
// Use an empty view to hide without removing.
|
||||||
|
return new View(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateView(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAttachedToActivity() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToActivity();
|
||||||
|
if (!SHOW_RYD_DEBUG_STATS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Updating stats preferences");
|
||||||
|
removeAll();
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeAverage_title",
|
||||||
|
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeMin_title",
|
||||||
|
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeMax_title",
|
||||||
|
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())
|
||||||
|
);
|
||||||
|
|
||||||
|
String fetchCallTimeWaitingLastSummary;
|
||||||
|
final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
|
||||||
|
if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
|
||||||
|
fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
|
||||||
|
} else {
|
||||||
|
fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
|
||||||
|
}
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeLast_title",
|
||||||
|
fetchCallTimeWaitingLastSummary
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallCount_title",
|
||||||
|
createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
|
||||||
|
"revanced_ryd_statistics_getFetchCallCount_zero_summary",
|
||||||
|
"revanced_ryd_statistics_getFetchCallCount_non_zero_summary"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallNumberOfFailures_title",
|
||||||
|
createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
|
||||||
|
"revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
|
||||||
|
"revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title",
|
||||||
|
createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
|
||||||
|
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
|
||||||
|
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onAttachedToActivity failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStatisticPreference(String titleKey, String SummaryText) {
|
||||||
|
Preference statisticPreference = new Preference(getContext());
|
||||||
|
statisticPreference.setSelectable(false);
|
||||||
|
statisticPreference.setTitle(str(titleKey));
|
||||||
|
statisticPreference.setSummary(SummaryText);
|
||||||
|
addPreference(statisticPreference);
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,6 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
@ -21,8 +19,6 @@ import app.revanced.extension.youtube.ThemeHelper;
|
|||||||
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
||||||
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
||||||
import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment;
|
|
||||||
import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks LicenseActivity.
|
* Hooks LicenseActivity.
|
||||||
@ -88,28 +84,15 @@ public class LicenseActivityHook {
|
|||||||
licenseActivity.setContentView(getResourceIdentifier(
|
licenseActivity.setContentView(getResourceIdentifier(
|
||||||
"revanced_settings_with_toolbar", "layout"));
|
"revanced_settings_with_toolbar", "layout"));
|
||||||
|
|
||||||
PreferenceFragment fragment;
|
// Sanity check.
|
||||||
String toolbarTitleResourceName;
|
String dataString = licenseActivity.getIntent().getDataString();
|
||||||
String dataString = Objects.requireNonNull(licenseActivity.getIntent().getDataString());
|
if (!"revanced_settings_intent".equals(dataString)) {
|
||||||
switch (dataString) {
|
Logger.printException(() -> "Unknown intent: " + dataString);
|
||||||
case "revanced_sb_settings_intent":
|
return;
|
||||||
toolbarTitleResourceName = "revanced_sb_settings_title";
|
|
||||||
fragment = new SponsorBlockPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case "revanced_ryd_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_ryd_settings_title";
|
|
||||||
fragment = new ReturnYouTubeDislikePreferenceFragment();
|
|
||||||
break;
|
|
||||||
case "revanced_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_settings_title";
|
|
||||||
fragment = new ReVancedPreferenceFragment();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Logger.printException(() -> "Unknown setting: " + dataString);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createToolbar(licenseActivity, toolbarTitleResourceName);
|
PreferenceFragment fragment = new ReVancedPreferenceFragment();
|
||||||
|
createToolbar(licenseActivity, fragment);
|
||||||
|
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
licenseActivity.getFragmentManager()
|
licenseActivity.getFragmentManager()
|
||||||
@ -122,7 +105,7 @@ public class LicenseActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
private static void createToolbar(Activity activity, String toolbarTitleResourceName) {
|
private static void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||||
// Replace dummy placeholder toolbar.
|
// Replace dummy placeholder toolbar.
|
||||||
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||||
ViewGroup toolBarParent = activity.findViewById(
|
ViewGroup toolBarParent = activity.findViewById(
|
||||||
@ -134,8 +117,7 @@ public class LicenseActivityHook {
|
|||||||
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
||||||
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
|
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
|
||||||
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
|
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
|
||||||
toolbar.setNavigationOnClickListener(view -> activity.onBackPressed());
|
toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
|
||||||
toolbar.setTitle(getResourceIdentifier(toolbarTitleResourceName, "string"));
|
|
||||||
|
|
||||||
final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
|
final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
|
||||||
Utils.getContext().getResources().getDisplayMetrics());
|
Utils.getContext().getResources().getDisplayMetrics());
|
||||||
@ -148,6 +130,11 @@ public class LicenseActivityHook {
|
|||||||
}
|
}
|
||||||
setToolbarLayoutParams(toolbar);
|
setToolbarLayoutParams(toolbar);
|
||||||
|
|
||||||
|
// Add Search Icon and EditText for ReVancedPreferenceFragment only.
|
||||||
|
if (fragment instanceof ReVancedPreferenceFragment) {
|
||||||
|
SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
|
||||||
|
}
|
||||||
|
|
||||||
toolBarParent.addView(toolbar, 0);
|
toolBarParent.addView(toolbar, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,381 @@
|
|||||||
|
package app.revanced.extension.youtube.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.AutoCompleteTextView;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.SearchView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
import app.revanced.extension.youtube.ThemeHelper;
|
||||||
|
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for managing the search view in ReVanced settings.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecated", "DiscouragedApi"})
|
||||||
|
public class SearchViewController {
|
||||||
|
private static final int MAX_HISTORY_SIZE = 5;
|
||||||
|
|
||||||
|
private final SearchView searchView;
|
||||||
|
private final FrameLayout searchContainer;
|
||||||
|
private final Toolbar toolbar;
|
||||||
|
private final Activity activity;
|
||||||
|
private boolean isSearchActive;
|
||||||
|
private final CharSequence originalTitle;
|
||||||
|
private final Deque<String> searchHistory;
|
||||||
|
private final AutoCompleteTextView autoCompleteTextView;
|
||||||
|
private final boolean showSettingsSearchHistory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a background drawable for the SearchView with rounded corners.
|
||||||
|
*/
|
||||||
|
private static GradientDrawable createBackgroundDrawable(Context context) {
|
||||||
|
GradientDrawable background = new GradientDrawable();
|
||||||
|
background.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
|
||||||
|
int baseColor = ThemeHelper.getBackgroundColor();
|
||||||
|
int adjustedColor = ThemeHelper.isDarkTheme()
|
||||||
|
? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme.
|
||||||
|
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||||
|
background.setColor(adjustedColor);
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a background drawable for suggestion items with rounded corners.
|
||||||
|
*/
|
||||||
|
private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
|
||||||
|
GradientDrawable background = new GradientDrawable();
|
||||||
|
background.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
background.setCornerRadius(8 * context.getResources().getDisplayMetrics().density); // 8dp corner radius.
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds search view components to the activity.
|
||||||
|
*/
|
||||||
|
public static void addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||||
|
new SearchViewController(activity, toolbar, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.toolbar = toolbar;
|
||||||
|
this.originalTitle = toolbar.getTitle();
|
||||||
|
this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
|
||||||
|
this.searchHistory = new LinkedList<>();
|
||||||
|
StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
|
||||||
|
if (showSettingsSearchHistory) {
|
||||||
|
String entries = searchEntries.get();
|
||||||
|
if (!entries.isBlank()) {
|
||||||
|
searchHistory.addAll(Arrays.asList(entries.split("\n")));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear old saved history if the user turns off the feature.
|
||||||
|
searchEntries.resetToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve SearchView and container from XML.
|
||||||
|
searchView = activity.findViewById(getResourceIdentifier(
|
||||||
|
"revanced_search_view", "id"));
|
||||||
|
searchContainer = activity.findViewById(getResourceIdentifier(
|
||||||
|
"revanced_search_view_container", "id"));
|
||||||
|
|
||||||
|
// Initialize AutoCompleteTextView.
|
||||||
|
autoCompleteTextView = searchView.findViewById(
|
||||||
|
searchView.getContext().getResources().getIdentifier(
|
||||||
|
"android:id/search_src_text", null, null));
|
||||||
|
|
||||||
|
// Set background and query hint.
|
||||||
|
searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
|
||||||
|
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
||||||
|
|
||||||
|
// Configure RTL support based on app language.
|
||||||
|
AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
|
if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
|
||||||
|
searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
|
||||||
|
searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up search history suggestions.
|
||||||
|
if (showSettingsSearchHistory) {
|
||||||
|
setupSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up query text listener.
|
||||||
|
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
try {
|
||||||
|
String queryTrimmed = query.trim();
|
||||||
|
if (!queryTrimmed.isEmpty()) {
|
||||||
|
saveSearchQuery(queryTrimmed);
|
||||||
|
}
|
||||||
|
// Hide suggestions on submit.
|
||||||
|
if (showSettingsSearchHistory && autoCompleteTextView != null) {
|
||||||
|
autoCompleteTextView.dismissDropDown();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onQueryTextSubmit failure", ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "Search query: " + newText);
|
||||||
|
fragment.filterPreferences(newText);
|
||||||
|
// Prevent suggestions from showing during text input.
|
||||||
|
if (showSettingsSearchHistory && autoCompleteTextView != null) {
|
||||||
|
if (!newText.isEmpty()) {
|
||||||
|
autoCompleteTextView.dismissDropDown();
|
||||||
|
autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions.
|
||||||
|
} else {
|
||||||
|
autoCompleteTextView.setThreshold(1); // Re-enable for empty input.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onQueryTextChange failure", ex);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set menu and search icon.
|
||||||
|
final int actionSearchId = getResourceIdentifier("action_search", "id");
|
||||||
|
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
|
||||||
|
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
|
||||||
|
searchItem.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||||
|
? "yt_outline_search_white_24"
|
||||||
|
: "yt_outline_search_black_24",
|
||||||
|
"drawable")).setTooltipText(null);
|
||||||
|
|
||||||
|
// Set menu item click listener.
|
||||||
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
|
try {
|
||||||
|
if (item.getItemId() == actionSearchId) {
|
||||||
|
if (!isSearchActive) {
|
||||||
|
openSearch();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "menu click failure", ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set navigation click listener.
|
||||||
|
toolbar.setNavigationOnClickListener(view -> {
|
||||||
|
try {
|
||||||
|
if (isSearchActive) {
|
||||||
|
closeSearch();
|
||||||
|
} else {
|
||||||
|
activity.onBackPressed();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "navigation click failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the search history suggestions for the SearchView with custom adapter.
|
||||||
|
*/
|
||||||
|
private void setupSearchHistory() {
|
||||||
|
if (autoCompleteTextView != null) {
|
||||||
|
SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory));
|
||||||
|
autoCompleteTextView.setAdapter(adapter);
|
||||||
|
autoCompleteTextView.setThreshold(1); // Initial threshold for empty input.
|
||||||
|
autoCompleteTextView.setLongClickable(true);
|
||||||
|
|
||||||
|
// Show suggestions only when search bar is active and query is empty.
|
||||||
|
autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) {
|
||||||
|
autoCompleteTextView.showDropDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a search query to the search history.
|
||||||
|
* @param query The search query to save.
|
||||||
|
*/
|
||||||
|
private void saveSearchQuery(String query) {
|
||||||
|
if (!showSettingsSearchHistory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchHistory.remove(query); // Remove if already exists to update position.
|
||||||
|
searchHistory.addFirst(query); // Add to the most recent.
|
||||||
|
|
||||||
|
// Remove extra old entries.
|
||||||
|
while (searchHistory.size() > MAX_HISTORY_SIZE) {
|
||||||
|
String last = searchHistory.removeLast();
|
||||||
|
Logger.printDebug(() -> "Removing search history query: " + last);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSearchHistory();
|
||||||
|
|
||||||
|
updateSearchHistoryAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a search query from the search history.
|
||||||
|
* @param query The search query to remove.
|
||||||
|
*/
|
||||||
|
private void removeSearchQuery(String query) {
|
||||||
|
searchHistory.remove(query);
|
||||||
|
|
||||||
|
saveSearchHistory();
|
||||||
|
|
||||||
|
updateSearchHistoryAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the search history to the shared preferences.
|
||||||
|
*/
|
||||||
|
private void saveSearchHistory() {
|
||||||
|
Logger.printDebug(() -> "Saving search history: " + searchHistory);
|
||||||
|
|
||||||
|
Settings.SETTINGS_SEARCH_ENTRIES.save(
|
||||||
|
String.join("\n", searchHistory)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the search history adapter with the latest history.
|
||||||
|
*/
|
||||||
|
private void updateSearchHistoryAdapter() {
|
||||||
|
if (autoCompleteTextView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter();
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.clear();
|
||||||
|
adapter.addAll(searchHistory);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the search view and shows the keyboard.
|
||||||
|
*/
|
||||||
|
private void openSearch() {
|
||||||
|
isSearchActive = true;
|
||||||
|
toolbar.getMenu().findItem(getResourceIdentifier(
|
||||||
|
"action_search", "id")).setVisible(false);
|
||||||
|
toolbar.setTitle("");
|
||||||
|
searchContainer.setVisibility(View.VISIBLE);
|
||||||
|
searchView.requestFocus();
|
||||||
|
|
||||||
|
// Show keyboard.
|
||||||
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
|
||||||
|
// Show suggestions with a slight delay.
|
||||||
|
if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
|
||||||
|
searchView.postDelayed(() -> {
|
||||||
|
if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
|
||||||
|
autoCompleteTextView.showDropDown();
|
||||||
|
}
|
||||||
|
}, 100); // 100ms delay to ensure focus is stable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the search view and hides the keyboard.
|
||||||
|
*/
|
||||||
|
private void closeSearch() {
|
||||||
|
isSearchActive = false;
|
||||||
|
toolbar.getMenu().findItem(getResourceIdentifier(
|
||||||
|
"action_search", "id"))
|
||||||
|
.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||||
|
? "yt_outline_search_white_24"
|
||||||
|
: "yt_outline_search_black_24",
|
||||||
|
"drawable")
|
||||||
|
).setVisible(true);
|
||||||
|
toolbar.setTitle(originalTitle);
|
||||||
|
searchContainer.setVisibility(View.GONE);
|
||||||
|
searchView.setQuery("", false);
|
||||||
|
|
||||||
|
// Hide keyboard.
|
||||||
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom ArrayAdapter for search history.
|
||||||
|
*/
|
||||||
|
private class SearchHistoryAdapter extends ArrayAdapter<String> {
|
||||||
|
public SearchHistoryAdapter(Context context, List<String> history) {
|
||||||
|
super(context, 0, history);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
|
||||||
|
"revanced_search_suggestion_item", "layout"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply rounded corners programmatically.
|
||||||
|
convertView.setBackground(createSuggestionBackgroundDrawable(getContext()));
|
||||||
|
String query = getItem(position);
|
||||||
|
|
||||||
|
// Set query text.
|
||||||
|
TextView textView = convertView.findViewById(getResourceIdentifier(
|
||||||
|
"suggestion_text", "id"));
|
||||||
|
if (textView != null) {
|
||||||
|
textView.setText(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set click listener for inserting query into SearchView.
|
||||||
|
convertView.setOnClickListener(v -> {
|
||||||
|
searchView.setQuery(query, true); // Insert selected query and submit.
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set long click listener for deletion confirmation.
|
||||||
|
convertView.setOnLongClickListener(v -> {
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(query)
|
||||||
|
.setMessage(str("revanced_settings_search_remove_message"))
|
||||||
|
.setPositiveButton(android.R.string.ok,
|
||||||
|
(dialog, which) -> removeSearchQuery(query))
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,8 @@ import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehavi
|
|||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@ -103,8 +105,9 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
|
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
|
||||||
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
|
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
|
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
|
||||||
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATIONS = new BooleanSetting("revanced_hide_search_result_recommendations", TRUE);
|
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_search_result_recommendation_labels", TRUE);
|
||||||
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
|
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
|
||||||
|
public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", FALSE);
|
||||||
// Alternative thumbnails
|
// Alternative thumbnails
|
||||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
||||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
|
||||||
@ -139,6 +142,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
|
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
|
||||||
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
|
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
|
||||||
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
|
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
|
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
|
||||||
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
|
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
|
||||||
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
|
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
|
||||||
@ -182,6 +186,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
||||||
// Description
|
// Description
|
||||||
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
||||||
|
public static final BooleanSetting HIDE_ASK_SECTION = new BooleanSetting("revanced_hide_ask_section", FALSE);
|
||||||
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
|
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
|
||||||
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE);
|
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE);
|
||||||
public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE);
|
public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE);
|
||||||
@ -217,6 +222,8 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
// General layout
|
// General layout
|
||||||
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
|
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
|
||||||
|
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
|
||||||
|
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true);
|
||||||
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
||||||
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
||||||
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
||||||
@ -340,19 +347,17 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS));
|
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS));
|
||||||
|
|
||||||
// ReturnYoutubeDislike
|
// ReturnYoutubeDislike
|
||||||
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
|
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
|
||||||
public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "", false, false);
|
public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false);
|
||||||
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("revanced_ryd_shorts", TRUE, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE, true, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE, true, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", TRUE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", TRUE, true, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
|
||||||
|
|
||||||
// SponsorBlock
|
// SponsorBlock
|
||||||
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
||||||
/**
|
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
|
||||||
* Do not use directly, instead use {@link SponsorBlockSettings}
|
|
||||||
*/
|
|
||||||
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
|
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
|
||||||
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
||||||
@ -416,12 +421,10 @@ public class Settings extends BaseSettings {
|
|||||||
// region Migration
|
// region Migration
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
|
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
|
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
|
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU);
|
migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU);
|
||||||
|
migrateOldSettingToNew(DEPRECATED_AUTO_CAPTIONS, DISABLE_AUTO_CAPTIONS);
|
||||||
|
|
||||||
// Migrate renamed enum.
|
// Migrate renamed enum.
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
@ -464,10 +467,15 @@ public class Settings extends BaseSettings {
|
|||||||
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DEPRECATED_AUTO_CAPTIONS.isSetToDefault()) {
|
// RYD requires manually migrating old settings since the lack of
|
||||||
DISABLE_AUTO_CAPTIONS.save(true);
|
// a "revanced_" on the old setting causes duplicate key exceptions during export.
|
||||||
DEPRECATED_AUTO_CAPTIONS.resetToDefault();
|
SharedPrefCategory revancedPrefs = Setting.preferences;
|
||||||
}
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_USER_ID, "ryd_user_id");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ENABLED, "ryd_enabled");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_DISLIKE_PERCENTAGE, "ryd_dislike_percentage");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_COMPACT_LAYOUT, "ryd_compact_layout");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -478,4 +486,3 @@ public class Settings extends BaseSettings {
|
|||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings("unused")
|
||||||
public class AlternativeThumbnailsAboutDeArrowPreference extends Preference {
|
public class AlternativeThumbnailsAboutDeArrowPreference extends UrlLinkPreference {
|
||||||
{
|
{
|
||||||
setOnPreferenceClickListener(pref -> {
|
externalUrl = "https://dearrow.ajay.app";
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://dearrow.ajay.app"));
|
|
||||||
pref.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.sf;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.ListPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom video speeds used by {@link CustomPlaybackSpeedPatch}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public final class CustomVideoSpeedListPreference extends ListPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a settings preference list with the available playback speeds.
|
||||||
|
*/
|
||||||
|
private void initializeEntryValues() {
|
||||||
|
float[] customPlaybackSpeeds = CustomPlaybackSpeedPatch.customPlaybackSpeeds;
|
||||||
|
final int numberOfEntries = customPlaybackSpeeds.length + 1;
|
||||||
|
String[] preferenceListEntries = new String[numberOfEntries];
|
||||||
|
String[] preferenceListEntryValues = new String[numberOfEntries];
|
||||||
|
|
||||||
|
// Auto speed (same behavior as unpatched).
|
||||||
|
preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
|
||||||
|
preferenceListEntryValues[0] = String.valueOf(Settings.PLAYBACK_SPEED_DEFAULT.defaultValue);
|
||||||
|
|
||||||
|
int i = 1;
|
||||||
|
for (float speed : customPlaybackSpeeds) {
|
||||||
|
String speedString = String.valueOf(speed);
|
||||||
|
preferenceListEntries[i] = speedString + "x";
|
||||||
|
preferenceListEntryValues[i] = speedString;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntries(preferenceListEntries);
|
||||||
|
setEntryValues(preferenceListEntryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
initializeEntryValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
@ -19,12 +19,15 @@ public class HtmlPreference extends Preference {
|
|||||||
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HtmlPreference(Context context, AttributeSet attrs) {
|
public HtmlPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HtmlPreference(Context context) {
|
public HtmlPreference(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -9,34 +10,66 @@ import android.graphics.drawable.Drawable;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.util.Pair;
|
import android.preference.SwitchPreference;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.EnumSetting;
|
|
||||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
||||||
|
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
||||||
import app.revanced.extension.youtube.ThemeHelper;
|
import app.revanced.extension.youtube.ThemeHelper;
|
||||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
|
||||||
import app.revanced.extension.youtube.settings.LicenseActivityHook;
|
import app.revanced.extension.youtube.settings.LicenseActivityHook;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference fragment for ReVanced settings.
|
* Preference fragment for ReVanced settings.
|
||||||
*
|
|
||||||
* @noinspection deprecation
|
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main PreferenceScreen used to display the current set of preferences.
|
||||||
|
* This screen is manipulated during initialization and filtering to show or hide preferences.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen preferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A copy of the original PreferenceScreen created during initialization.
|
||||||
|
* Used to restore the preference structure to its initial state after filtering or other modifications.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen originalPreferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for searching preferences. A Collection of all preferences including nested preferences.
|
||||||
|
* Root preferences are excluded (no need to search what's on the root screen),
|
||||||
|
* but their sub preferences are included.
|
||||||
|
*/
|
||||||
|
private final List<AbstractPreferenceSearchData<?>> allPreferences = new ArrayList<>();
|
||||||
|
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
public static Drawable getBackButtonDrawable() {
|
public static Drawable getBackButtonDrawable() {
|
||||||
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||||
@ -47,85 +80,140 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts a preference list by menu entries, but preserves the first value as the first entry.
|
* Initializes the preference fragment, copying the original screen to allow full restoration.
|
||||||
*
|
|
||||||
* @noinspection SameParameterValue
|
|
||||||
*/
|
*/
|
||||||
private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) {
|
|
||||||
CharSequence[] entries = listPreference.getEntries();
|
|
||||||
CharSequence[] entryValues = listPreference.getEntryValues();
|
|
||||||
final int entrySize = entries.length;
|
|
||||||
|
|
||||||
if (entrySize != entryValues.length) {
|
|
||||||
// Xml array declaration has a missing/extra entry.
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Pair<String, String>> firstPairs = new ArrayList<>(firstEntriesToPreserve);
|
|
||||||
List<Pair<String, String>> pairsToSort = new ArrayList<>(entrySize);
|
|
||||||
|
|
||||||
for (int i = 0; i < entrySize; i++) {
|
|
||||||
Pair<String, String> pair = new Pair<>(entries[i].toString(), entryValues[i].toString());
|
|
||||||
if (i < firstEntriesToPreserve) {
|
|
||||||
firstPairs.add(pair);
|
|
||||||
} else {
|
|
||||||
pairsToSort.add(pair);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pairsToSort.sort((pair1, pair2)
|
|
||||||
-> pair1.first.compareToIgnoreCase(pair2.first));
|
|
||||||
|
|
||||||
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
|
||||||
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
for (Pair<String, String> pair : firstPairs) {
|
|
||||||
sortedEntries[i] = pair.first;
|
|
||||||
sortedEntryValues[i] = pair.second;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Pair<String, String> pair : pairsToSort) {
|
|
||||||
sortedEntries[i] = pair.first;
|
|
||||||
sortedEntryValues[i] = pair.second;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
listPreference.setEntries(sortedEntries);
|
|
||||||
listPreference.setEntryValues(sortedEntryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize() {
|
protected void initialize() {
|
||||||
super.initialize();
|
super.initialize();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setPreferenceScreenToolbar(getPreferenceScreen());
|
preferenceScreen = getPreferenceScreen();
|
||||||
|
Utils.sortPreferenceGroups(preferenceScreen);
|
||||||
|
|
||||||
// If the preference was included, then initialize it based on the available playback speed.
|
// Store the original structure for restoration after filtering.
|
||||||
Preference preference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key);
|
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
|
||||||
if (preference instanceof ListPreference playbackPreference) {
|
for (int i = 0, count = preferenceScreen.getPreferenceCount(); i < count; i++) {
|
||||||
CustomPlaybackSpeedPatch.initializeListPreference(playbackPreference);
|
originalPreferenceScreen.addPreference(preferenceScreen.getPreference(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
|
setPreferenceScreenToolbar(preferenceScreen);
|
||||||
sortPreferenceListMenu(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE);
|
|
||||||
sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sortPreferenceListMenu(EnumSetting<?> setting) {
|
/**
|
||||||
Preference preference = findPreference(setting.key);
|
* Called when the fragment starts, ensuring all preferences are collected after initialization.
|
||||||
if (preference instanceof ListPreference languagePreference) {
|
*/
|
||||||
sortListPreferenceByValues(languagePreference, 1);
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
try {
|
||||||
|
if (allPreferences.isEmpty()) {
|
||||||
|
// Must collect preferences on start and not in initialize since
|
||||||
|
// legacy SB settings are not loaded yet.
|
||||||
|
Logger.printDebug(() -> "Collecting preferences to search");
|
||||||
|
|
||||||
|
// Do not show root menu preferences in search results.
|
||||||
|
// Instead search for everything that's not shown when search is not active.
|
||||||
|
collectPreferences(preferenceScreen, 1, 0);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onStart failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively collects all preferences from the screen or group.
|
||||||
|
* @param includeDepth Menu depth to start including preferences.
|
||||||
|
* A value of 0 adds all preferences.
|
||||||
|
*/
|
||||||
|
private void collectPreferences(PreferenceGroup group, int includeDepth, int currentDepth) {
|
||||||
|
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
||||||
|
Preference preference = group.getPreference(i);
|
||||||
|
if (includeDepth <= currentDepth && !(preference instanceof PreferenceCategory)
|
||||||
|
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
|
||||||
|
|
||||||
|
AbstractPreferenceSearchData<?> data;
|
||||||
|
if (preference instanceof SwitchPreference switchPref) {
|
||||||
|
data = new SwitchPreferenceSearchData(switchPref);
|
||||||
|
} else if (preference instanceof ListPreference listPref) {
|
||||||
|
data = new ListPreferenceSearchData(listPref);
|
||||||
|
} else {
|
||||||
|
data = new PreferenceSearchData(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
allPreferences.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference instanceof PreferenceGroup subGroup) {
|
||||||
|
collectPreferences(subGroup, includeDepth, currentDepth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the preferences using the given query string and applies highlighting.
|
||||||
|
*/
|
||||||
|
public void filterPreferences(String query) {
|
||||||
|
preferenceScreen.removeAll();
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(query)) {
|
||||||
|
// Restore original preferences and their titles/summaries/entries.
|
||||||
|
for (int i = 0, count = originalPreferenceScreen.getPreferenceCount(); i < count; i++) {
|
||||||
|
preferenceScreen.addPreference(originalPreferenceScreen.getPreference(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (AbstractPreferenceSearchData<?> data : allPreferences) {
|
||||||
|
data.clearHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation path -> Category
|
||||||
|
Map<String, PreferenceCategory> categoryMap = new HashMap<>();
|
||||||
|
String queryLower = Utils.removePunctuationToLowercase(query);
|
||||||
|
|
||||||
|
Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)),
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
for (AbstractPreferenceSearchData<?> data : allPreferences) {
|
||||||
|
if (data.matchesSearchQuery(queryLower)) {
|
||||||
|
data.applyHighlighting(queryLower, queryPattern);
|
||||||
|
|
||||||
|
String navigationPath = data.navigationPath;
|
||||||
|
PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> {
|
||||||
|
PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext());
|
||||||
|
newGroup.setTitle(navigationPath);
|
||||||
|
preferenceScreen.addPreference(newGroup);
|
||||||
|
return newGroup;
|
||||||
|
});
|
||||||
|
group.addPreference(data.preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show 'No results found' if search results are empty.
|
||||||
|
if (categoryMap.isEmpty()) {
|
||||||
|
Preference noResultsPreference = new Preference(preferenceScreen.getContext());
|
||||||
|
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
||||||
|
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
||||||
|
noResultsPreference.setSelectable(false);
|
||||||
|
// Set icon for the placeholder preference.
|
||||||
|
noResultsPreference.setLayoutResource(getResourceIdentifier(
|
||||||
|
"revanced_preference_with_icon_no_search_result", "layout"));
|
||||||
|
noResultsPreference.setIcon(getResourceIdentifier(
|
||||||
|
ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24",
|
||||||
|
"drawable"));
|
||||||
|
preferenceScreen.addPreference(noResultsPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets toolbar for all nested preference screens.
|
||||||
|
*/
|
||||||
private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
|
private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
|
||||||
for (int i = 0, preferenceCount = parentScreen.getPreferenceCount(); i < preferenceCount; i++) {
|
for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) {
|
||||||
Preference childPreference = parentScreen.getPreference(i);
|
Preference childPreference = parentScreen.getPreference(i);
|
||||||
if (childPreference instanceof PreferenceScreen) {
|
if (childPreference instanceof PreferenceScreen) {
|
||||||
// Recursively set sub preferences.
|
// Recursively set sub preferences.
|
||||||
@ -156,6 +244,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
toolbar.setTitle(childScreen.getTitle());
|
toolbar.setTitle(childScreen.getTitle());
|
||||||
toolbar.setNavigationIcon(getBackButtonDrawable());
|
toolbar.setNavigationIcon(getBackButtonDrawable());
|
||||||
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
||||||
|
|
||||||
final int margin = (int) TypedValue.applyDimension(
|
final int margin = (int) TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
|
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
|
||||||
);
|
);
|
||||||
@ -177,3 +266,277 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class AbstractPreferenceSearchData<T extends Preference> {
|
||||||
|
/**
|
||||||
|
* @return The navigation path for the given preference, such as "Player > Action buttons".
|
||||||
|
*/
|
||||||
|
private static String getPreferenceNavigationString(Preference preference) {
|
||||||
|
Deque<CharSequence> pathElements = new ArrayDeque<>();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
preference = preference.getParent();
|
||||||
|
|
||||||
|
if (preference == null) {
|
||||||
|
if (pathElements.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
|
||||||
|
return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(preference instanceof NoTitlePreferenceCategory)
|
||||||
|
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
|
||||||
|
CharSequence title = preference.getTitle();
|
||||||
|
if (title != null && title.length() > 0) {
|
||||||
|
pathElements.addFirst(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights the search query in the given text by applying color span.
|
||||||
|
* @param text The original text to process.
|
||||||
|
* @param queryPattern The search query to highlight.
|
||||||
|
* @return The text with highlighted query matches as a SpannableStringBuilder.
|
||||||
|
*/
|
||||||
|
static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
||||||
|
if (TextUtils.isEmpty(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int baseColor = ThemeHelper.getBackgroundColor();
|
||||||
|
final int adjustedColor = ThemeHelper.isDarkTheme()
|
||||||
|
? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
|
||||||
|
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||||
|
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
||||||
|
|
||||||
|
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
||||||
|
Matcher matcher = queryPattern.matcher(text);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
spannable.setSpan(
|
||||||
|
highlightSpan,
|
||||||
|
matcher.start(),
|
||||||
|
matcher.end(),
|
||||||
|
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
|
final T preference;
|
||||||
|
final String key;
|
||||||
|
final String navigationPath;
|
||||||
|
boolean highlightingApplied;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
CharSequence originalTitle;
|
||||||
|
@Nullable
|
||||||
|
String searchTitle;
|
||||||
|
|
||||||
|
AbstractPreferenceSearchData(T pref) {
|
||||||
|
preference = pref;
|
||||||
|
key = Utils.removePunctuationToLowercase(pref.getKey());
|
||||||
|
navigationPath = getPreferenceNavigationString(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
// Must clear, otherwise old highlighting is still applied.
|
||||||
|
clearHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence title = preference.getTitle();
|
||||||
|
if (originalTitle != title) { // Check using reference equality.
|
||||||
|
originalTitle = title;
|
||||||
|
searchTitle = Utils.removePunctuationToLowercase(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
return key.contains(query)
|
||||||
|
|| searchTitle != null && searchTitle.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
preference.setTitle(highlightSearchQuery(originalTitle, queryPattern));
|
||||||
|
highlightingApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setTitle(originalTitle);
|
||||||
|
highlightingApplied = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular preference type that only uses the base preference summary.
|
||||||
|
* Should only be used if a more specific data class does not exist.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class PreferenceSearchData extends AbstractPreferenceSearchData<Preference> {
|
||||||
|
@Nullable
|
||||||
|
CharSequence originalSummary;
|
||||||
|
@Nullable
|
||||||
|
String searchSummary;
|
||||||
|
|
||||||
|
PreferenceSearchData(Preference pref) {
|
||||||
|
super(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
super.updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
CharSequence summary = preference.getSummary();
|
||||||
|
if (originalSummary != summary) {
|
||||||
|
originalSummary = summary;
|
||||||
|
searchSummary = Utils.removePunctuationToLowercase(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
return super.matchesSearchQuery(query)
|
||||||
|
|| searchSummary != null && searchSummary.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
super.applyHighlighting(query, queryPattern);
|
||||||
|
|
||||||
|
preference.setSummary(highlightSearchQuery(originalSummary, queryPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setSummary(originalSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch preference type that uses summaryOn and summaryOff.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class SwitchPreferenceSearchData extends AbstractPreferenceSearchData<SwitchPreference> {
|
||||||
|
@Nullable
|
||||||
|
CharSequence originalSummaryOn, originalSummaryOff;
|
||||||
|
@Nullable
|
||||||
|
String searchSummaryOn, searchSummaryOff;
|
||||||
|
|
||||||
|
SwitchPreferenceSearchData(SwitchPreference pref) {
|
||||||
|
super(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
super.updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
CharSequence summaryOn = preference.getSummaryOn();
|
||||||
|
if (originalSummaryOn != summaryOn) {
|
||||||
|
originalSummaryOn = summaryOn;
|
||||||
|
searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence summaryOff = preference.getSummaryOff();
|
||||||
|
if (originalSummaryOff != summaryOff) {
|
||||||
|
originalSummaryOff = summaryOff;
|
||||||
|
searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
return super.matchesSearchQuery(query)
|
||||||
|
|| searchSummaryOn != null && searchSummaryOn.contains(query)
|
||||||
|
|| searchSummaryOff != null && searchSummaryOff.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
super.applyHighlighting(query, queryPattern);
|
||||||
|
|
||||||
|
preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern));
|
||||||
|
preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setSummaryOn(originalSummaryOn);
|
||||||
|
preference.setSummaryOff(originalSummaryOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List preference type that uses entries.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class ListPreferenceSearchData extends AbstractPreferenceSearchData<ListPreference> {
|
||||||
|
@Nullable
|
||||||
|
CharSequence[] originalEntries;
|
||||||
|
@Nullable
|
||||||
|
String searchEntries;
|
||||||
|
|
||||||
|
ListPreferenceSearchData(ListPreference pref) {
|
||||||
|
super(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
super.updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
CharSequence[] entries = preference.getEntries();
|
||||||
|
if (originalEntries != entries) {
|
||||||
|
originalEntries = entries;
|
||||||
|
searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
return super.matchesSearchQuery(query)
|
||||||
|
|| searchEntries != null && searchEntries.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
super.applyHighlighting(query, queryPattern);
|
||||||
|
|
||||||
|
if (originalEntries != null) {
|
||||||
|
final int length = originalEntries.length;
|
||||||
|
CharSequence[] highlightedEntries = new CharSequence[length];
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
|
||||||
|
|
||||||
|
// Cannot highlight the summary text, because ListPreference uses
|
||||||
|
// the toString() of the summary CharSequence which strips away all formatting.
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setEntries(highlightedEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setEntries(originalEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,257 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceCategory;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch;
|
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
/** @noinspection deprecation*/
|
|
||||||
public class ReturnYouTubeDislikePreferenceFragment extends PreferenceFragment {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If dislikes are shown on Shorts.
|
|
||||||
*/
|
|
||||||
private SwitchPreference shortsPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If dislikes are shown as percentage.
|
|
||||||
*/
|
|
||||||
private SwitchPreference percentagePreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If segmented like/dislike button uses smaller compact layout.
|
|
||||||
*/
|
|
||||||
private SwitchPreference compactLayoutPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If hidden likes are replaced with an estimated value.
|
|
||||||
*/
|
|
||||||
private SwitchPreference estimatedLikesPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If segmented like/dislike button uses smaller compact layout.
|
|
||||||
*/
|
|
||||||
private SwitchPreference toastOnRYDNotAvailable;
|
|
||||||
|
|
||||||
private void updateUIState() {
|
|
||||||
shortsPreference.setEnabled(Settings.RYD_SHORTS.isAvailable());
|
|
||||||
percentagePreference.setEnabled(Settings.RYD_DISLIKE_PERCENTAGE.isAvailable());
|
|
||||||
compactLayoutPreference.setEnabled(Settings.RYD_COMPACT_LAYOUT.isAvailable());
|
|
||||||
estimatedLikesPreference.setEnabled(Settings.RYD_ESTIMATED_LIKE.isAvailable());
|
|
||||||
toastOnRYDNotAvailable.setEnabled(Settings.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
try {
|
|
||||||
Activity context = getActivity();
|
|
||||||
PreferenceManager manager = getPreferenceManager();
|
|
||||||
manager.setSharedPreferencesName(Setting.preferences.name);
|
|
||||||
PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context);
|
|
||||||
setPreferenceScreen(preferenceScreen);
|
|
||||||
|
|
||||||
SwitchPreference enabledPreference = new SwitchPreference(context);
|
|
||||||
enabledPreference.setChecked(Settings.RYD_ENABLED.get());
|
|
||||||
enabledPreference.setTitle(str("revanced_ryd_enable_title"));
|
|
||||||
enabledPreference.setSummaryOn(str("revanced_ryd_enable_summary_on"));
|
|
||||||
enabledPreference.setSummaryOff(str("revanced_ryd_enable_summary_off"));
|
|
||||||
enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
final Boolean rydIsEnabled = (Boolean) newValue;
|
|
||||||
Settings.RYD_ENABLED.save(rydIsEnabled);
|
|
||||||
ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled);
|
|
||||||
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(enabledPreference);
|
|
||||||
|
|
||||||
shortsPreference = new SwitchPreference(context);
|
|
||||||
shortsPreference.setChecked(Settings.RYD_SHORTS.get());
|
|
||||||
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
|
|
||||||
String shortsSummary = str("revanced_ryd_shorts_summary_on_disclaimer");
|
|
||||||
shortsPreference.setSummaryOn(shortsSummary);
|
|
||||||
shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
|
|
||||||
shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_SHORTS.save((Boolean) newValue);
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(shortsPreference);
|
|
||||||
|
|
||||||
percentagePreference = new SwitchPreference(context);
|
|
||||||
percentagePreference.setChecked(Settings.RYD_DISLIKE_PERCENTAGE.get());
|
|
||||||
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
|
|
||||||
percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on"));
|
|
||||||
percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off"));
|
|
||||||
percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_DISLIKE_PERCENTAGE.save((Boolean) newValue);
|
|
||||||
ReturnYouTubeDislike.clearAllUICaches();
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(percentagePreference);
|
|
||||||
|
|
||||||
compactLayoutPreference = new SwitchPreference(context);
|
|
||||||
compactLayoutPreference.setChecked(Settings.RYD_COMPACT_LAYOUT.get());
|
|
||||||
compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title"));
|
|
||||||
compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on"));
|
|
||||||
compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off"));
|
|
||||||
compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_COMPACT_LAYOUT.save((Boolean) newValue);
|
|
||||||
ReturnYouTubeDislike.clearAllUICaches();
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(compactLayoutPreference);
|
|
||||||
|
|
||||||
estimatedLikesPreference = new SwitchPreference(context);
|
|
||||||
estimatedLikesPreference.setChecked(Settings.RYD_ESTIMATED_LIKE.get());
|
|
||||||
estimatedLikesPreference.setTitle(str("revanced_ryd_estimated_like_title"));
|
|
||||||
estimatedLikesPreference.setSummaryOn(str("revanced_ryd_estimated_like_summary_on"));
|
|
||||||
estimatedLikesPreference.setSummaryOff(str("revanced_ryd_estimated_like_summary_off"));
|
|
||||||
estimatedLikesPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_ESTIMATED_LIKE.save((Boolean) newValue);
|
|
||||||
ReturnYouTubeDislike.clearAllUICaches();
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(estimatedLikesPreference);
|
|
||||||
|
|
||||||
toastOnRYDNotAvailable = new SwitchPreference(context);
|
|
||||||
toastOnRYDNotAvailable.setChecked(Settings.RYD_TOAST_ON_CONNECTION_ERROR.get());
|
|
||||||
toastOnRYDNotAvailable.setTitle(str("revanced_ryd_toast_on_connection_error_title"));
|
|
||||||
toastOnRYDNotAvailable.setSummaryOn(str("revanced_ryd_toast_on_connection_error_summary_on"));
|
|
||||||
toastOnRYDNotAvailable.setSummaryOff(str("revanced_ryd_toast_on_connection_error_summary_off"));
|
|
||||||
toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(toastOnRYDNotAvailable);
|
|
||||||
|
|
||||||
updateUIState();
|
|
||||||
|
|
||||||
|
|
||||||
// About category
|
|
||||||
|
|
||||||
PreferenceCategory aboutCategory = new PreferenceCategory(context);
|
|
||||||
aboutCategory.setTitle(str("revanced_ryd_about"));
|
|
||||||
preferenceScreen.addPreference(aboutCategory);
|
|
||||||
|
|
||||||
// ReturnYouTubeDislike Website
|
|
||||||
|
|
||||||
Preference aboutWebsitePreference = new Preference(context);
|
|
||||||
aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title"));
|
|
||||||
aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary"));
|
|
||||||
aboutWebsitePreference.setOnPreferenceClickListener(pref -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://returnyoutubedislike.com"));
|
|
||||||
pref.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
aboutCategory.addPreference(aboutWebsitePreference);
|
|
||||||
|
|
||||||
// RYD API connection statistics
|
|
||||||
|
|
||||||
if (BaseSettings.DEBUG.get()) {
|
|
||||||
PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding
|
|
||||||
preferenceScreen.addPreference(emptyCategory);
|
|
||||||
|
|
||||||
PreferenceCategory statisticsCategory = new PreferenceCategory(context);
|
|
||||||
statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title"));
|
|
||||||
preferenceScreen.addPreference(statisticsCategory);
|
|
||||||
|
|
||||||
Preference statisticPreference;
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title"));
|
|
||||||
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage()));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title"));
|
|
||||||
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin()));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title"));
|
|
||||||
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax()));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
String fetchCallTimeWaitingLastSummary;
|
|
||||||
final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
|
|
||||||
if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
|
|
||||||
fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
|
|
||||||
} else {
|
|
||||||
fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
|
|
||||||
}
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title"));
|
|
||||||
statisticPreference.setSummary(fetchCallTimeWaitingLastSummary);
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title"));
|
|
||||||
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
|
|
||||||
"revanced_ryd_statistics_getFetchCallCount_zero_summary",
|
|
||||||
"revanced_ryd_statistics_getFetchCallCount_non_zero_summary"));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title"));
|
|
||||||
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
|
|
||||||
"revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
|
|
||||||
"revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title"));
|
|
||||||
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
|
|
||||||
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
|
|
||||||
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onCreate failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
|
|
||||||
if (value == 0) {
|
|
||||||
return str(summaryStringZeroKey);
|
|
||||||
}
|
|
||||||
return String.format(str(summaryStringOneOrMoreKey), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createMillisecondStringFromNumber(long number) {
|
|
||||||
return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,629 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.*;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController;
|
|
||||||
|
|
||||||
import static android.text.Html.fromHtml;
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class SponsorBlockPreferenceFragment extends PreferenceFragment {
|
|
||||||
|
|
||||||
private SwitchPreference sbEnabled;
|
|
||||||
private SwitchPreference addNewSegment;
|
|
||||||
private SwitchPreference votingEnabled;
|
|
||||||
private SwitchPreference autoHideSkipSegmentButton;
|
|
||||||
private SwitchPreference compactSkipButton;
|
|
||||||
private SwitchPreference squareLayout;
|
|
||||||
private SwitchPreference showSkipToast;
|
|
||||||
private SwitchPreference trackSkips;
|
|
||||||
private SwitchPreference showTimeWithoutSegments;
|
|
||||||
private SwitchPreference toastOnConnectionError;
|
|
||||||
|
|
||||||
private ResettableEditTextPreference newSegmentStep;
|
|
||||||
private ResettableEditTextPreference minSegmentDuration;
|
|
||||||
private EditTextPreference privateUserId;
|
|
||||||
private EditTextPreference importExport;
|
|
||||||
private Preference apiUrl;
|
|
||||||
|
|
||||||
private PreferenceCategory statsCategory;
|
|
||||||
private PreferenceCategory segmentCategory;
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
try {
|
|
||||||
final boolean enabled = Settings.SB_ENABLED.get();
|
|
||||||
if (!enabled) {
|
|
||||||
SponsorBlockViewController.hideAll();
|
|
||||||
SegmentPlaybackController.setCurrentVideoId(null);
|
|
||||||
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
|
||||||
SponsorBlockViewController.hideNewSegmentLayout();
|
|
||||||
}
|
|
||||||
// Voting and add new segment buttons automatically show/hide themselves.
|
|
||||||
|
|
||||||
SponsorBlockViewController.updateLayout();
|
|
||||||
|
|
||||||
sbEnabled.setChecked(enabled);
|
|
||||||
|
|
||||||
addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
|
|
||||||
addNewSegment.setEnabled(enabled);
|
|
||||||
|
|
||||||
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
|
||||||
votingEnabled.setEnabled(enabled);
|
|
||||||
|
|
||||||
autoHideSkipSegmentButton.setEnabled(enabled);
|
|
||||||
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
|
||||||
|
|
||||||
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
|
||||||
compactSkipButton.setEnabled(enabled);
|
|
||||||
|
|
||||||
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
|
||||||
squareLayout.setEnabled(enabled);
|
|
||||||
|
|
||||||
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
|
||||||
showSkipToast.setEnabled(enabled);
|
|
||||||
|
|
||||||
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
|
||||||
toastOnConnectionError.setEnabled(enabled);
|
|
||||||
|
|
||||||
trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
|
|
||||||
trackSkips.setEnabled(enabled);
|
|
||||||
|
|
||||||
showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
|
|
||||||
showTimeWithoutSegments.setEnabled(enabled);
|
|
||||||
|
|
||||||
newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
|
|
||||||
newSegmentStep.setEnabled(enabled);
|
|
||||||
|
|
||||||
minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
|
|
||||||
minSegmentDuration.setEnabled(enabled);
|
|
||||||
|
|
||||||
privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
|
|
||||||
privateUserId.setEnabled(enabled);
|
|
||||||
|
|
||||||
// If the user has a private user id, then include a subtext that mentions not to share it.
|
|
||||||
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
|
||||||
? str("revanced_sb_settings_ie_sum_warning")
|
|
||||||
: str("revanced_sb_settings_ie_sum");
|
|
||||||
importExport.setSummary(importExportSummary);
|
|
||||||
|
|
||||||
apiUrl.setEnabled(enabled);
|
|
||||||
importExport.setEnabled(enabled);
|
|
||||||
segmentCategory.setEnabled(enabled);
|
|
||||||
statsCategory.setEnabled(enabled);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "update settings UI failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
try {
|
|
||||||
Activity context = getActivity();
|
|
||||||
PreferenceManager manager = getPreferenceManager();
|
|
||||||
manager.setSharedPreferencesName(Setting.preferences.name);
|
|
||||||
PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context);
|
|
||||||
setPreferenceScreen(preferenceScreen);
|
|
||||||
|
|
||||||
SponsorBlockSettings.initialize();
|
|
||||||
|
|
||||||
sbEnabled = new SwitchPreference(context);
|
|
||||||
sbEnabled.setTitle(str("revanced_sb_enable_sb"));
|
|
||||||
sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
|
|
||||||
preferenceScreen.addPreference(sbEnabled);
|
|
||||||
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_ENABLED.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
addAppearanceCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
segmentCategory = new PreferenceCategory(context);
|
|
||||||
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
|
||||||
preferenceScreen.addPreference(segmentCategory);
|
|
||||||
updateSegmentCategories();
|
|
||||||
|
|
||||||
addCreateSegmentCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
addGeneralCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
statsCategory = new PreferenceCategory(context);
|
|
||||||
statsCategory.setTitle(str("revanced_sb_stats"));
|
|
||||||
preferenceScreen.addPreference(statsCategory);
|
|
||||||
fetchAndDisplayStats();
|
|
||||||
|
|
||||||
addAboutCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen);
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onCreate failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAppearanceCategory(Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_appearance_category"));
|
|
||||||
|
|
||||||
votingEnabled = new SwitchPreference(context);
|
|
||||||
votingEnabled.setTitle(str("revanced_sb_enable_voting"));
|
|
||||||
votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
|
|
||||||
votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
|
|
||||||
category.addPreference(votingEnabled);
|
|
||||||
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
autoHideSkipSegmentButton = new SwitchPreference(context);
|
|
||||||
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
|
||||||
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
|
||||||
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
|
||||||
category.addPreference(autoHideSkipSegmentButton);
|
|
||||||
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
compactSkipButton = new SwitchPreference(context);
|
|
||||||
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
|
||||||
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
|
||||||
compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
|
|
||||||
category.addPreference(compactSkipButton);
|
|
||||||
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
squareLayout = new SwitchPreference(context);
|
|
||||||
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
|
||||||
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
|
||||||
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
|
||||||
category.addPreference(squareLayout);
|
|
||||||
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
showSkipToast = new SwitchPreference(context);
|
|
||||||
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
|
||||||
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
|
||||||
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
|
||||||
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(showSkipToast);
|
|
||||||
|
|
||||||
showTimeWithoutSegments = new SwitchPreference(context);
|
|
||||||
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
|
||||||
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
|
||||||
showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
|
|
||||||
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(showTimeWithoutSegments);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addCreateSegmentCategory(Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_create_segment_category"));
|
|
||||||
|
|
||||||
addNewSegment = new SwitchPreference(context);
|
|
||||||
addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
|
|
||||||
addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
|
|
||||||
addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
|
|
||||||
category.addPreference(addNewSegment);
|
|
||||||
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
|
||||||
Boolean newValue = (Boolean) o;
|
|
||||||
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
|
||||||
new AlertDialog.Builder(preference1.getContext())
|
|
||||||
.setTitle(str("revanced_sb_guidelines_popup_title"))
|
|
||||||
.setMessage(str("revanced_sb_guidelines_popup_content"))
|
|
||||||
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
|
|
||||||
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
|
|
||||||
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
|
|
||||||
.setCancelable(false)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
newSegmentStep = new ResettableEditTextPreference(context);
|
|
||||||
newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
|
|
||||||
newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
|
|
||||||
newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
|
|
||||||
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
|
||||||
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
try {
|
|
||||||
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
|
||||||
if (newAdjustmentValue != 0) {
|
|
||||||
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Logger.printInfo(() -> "Invalid new segment step", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
|
||||||
updateUI();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
category.addPreference(newSegmentStep);
|
|
||||||
|
|
||||||
Preference guidelinePreferences = new Preference(context);
|
|
||||||
guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title"));
|
|
||||||
guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum"));
|
|
||||||
guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
openGuidelines();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(guidelinePreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_general"));
|
|
||||||
|
|
||||||
toastOnConnectionError = new SwitchPreference(context);
|
|
||||||
toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
|
|
||||||
toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
|
|
||||||
toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
|
|
||||||
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(toastOnConnectionError);
|
|
||||||
|
|
||||||
trackSkips = new SwitchPreference(context);
|
|
||||||
trackSkips.setTitle(str("revanced_sb_general_skipcount"));
|
|
||||||
trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
|
|
||||||
trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
|
|
||||||
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(trackSkips);
|
|
||||||
|
|
||||||
minSegmentDuration = new ResettableEditTextPreference(context);
|
|
||||||
minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
|
|
||||||
minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
|
|
||||||
minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
|
|
||||||
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
|
||||||
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
try {
|
|
||||||
Float minTimeDuration = Float.valueOf(newValue.toString());
|
|
||||||
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
|
||||||
return true;
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
|
||||||
updateUI();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
category.addPreference(minSegmentDuration);
|
|
||||||
|
|
||||||
privateUserId = new EditTextPreference(context) {
|
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
|
||||||
Utils.setEditTextDialogTheme(builder);
|
|
||||||
|
|
||||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
privateUserId.setTitle(str("revanced_sb_general_uuid"));
|
|
||||||
privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
|
|
||||||
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
String newUUID = newValue.toString();
|
|
||||||
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
|
||||||
updateUI();
|
|
||||||
fetchAndDisplayStats();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(privateUserId);
|
|
||||||
|
|
||||||
apiUrl = new Preference(context);
|
|
||||||
apiUrl.setTitle(str("revanced_sb_general_api_url"));
|
|
||||||
apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
|
|
||||||
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
EditText editText = new EditText(context);
|
|
||||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
|
||||||
editText.setText(Settings.SB_API_URL.get());
|
|
||||||
|
|
||||||
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
|
||||||
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
|
||||||
Settings.SB_API_URL.resetToDefault();
|
|
||||||
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
|
||||||
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
String serverAddress = editText.getText().toString();
|
|
||||||
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
|
||||||
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
|
||||||
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
|
||||||
Settings.SB_API_URL.save(serverAddress);
|
|
||||||
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(apiUrl.getTitle())
|
|
||||||
.setView(editText)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
|
|
||||||
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
|
||||||
.show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(apiUrl);
|
|
||||||
|
|
||||||
importExport = new EditTextPreference(context) {
|
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
|
||||||
Utils.setEditTextDialogTheme(builder);
|
|
||||||
|
|
||||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
importExport.setTitle(str("revanced_sb_settings_ie"));
|
|
||||||
// Summary is set in updateUI()
|
|
||||||
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
|
|
||||||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
||||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
|
||||||
importExport.getEditText().setAutofillHints((String) null);
|
|
||||||
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
|
|
||||||
importExport.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
SponsorBlockSettings.importDesktopSettings((String) newValue);
|
|
||||||
updateSegmentCategories();
|
|
||||||
fetchAndDisplayStats();
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(importExport);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSegmentCategories() {
|
|
||||||
try {
|
|
||||||
segmentCategory.removeAll();
|
|
||||||
|
|
||||||
Activity activity = getActivity();
|
|
||||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
|
||||||
segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category));
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "updateSegmentCategories failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAboutCategory(Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_about"));
|
|
||||||
|
|
||||||
{
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
category.addPreference(preference);
|
|
||||||
preference.setTitle(str("revanced_sb_about_api"));
|
|
||||||
preference.setSummary(str("revanced_sb_about_api_sum"));
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://sponsor.ajay.app"));
|
|
||||||
preference1.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openGuidelines() {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
|
||||||
getActivity().startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchAndDisplayStats() {
|
|
||||||
try {
|
|
||||||
statsCategory.removeAll();
|
|
||||||
if (!SponsorBlockSettings.userHasSBPrivateId()) {
|
|
||||||
// User has never voted or created any segments. No stats to show.
|
|
||||||
addLocalUserStats();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Preference loadingPlaceholderPreference = new Preference(this.getActivity());
|
|
||||||
loadingPlaceholderPreference.setEnabled(false);
|
|
||||||
statsCategory.addPreference(loadingPlaceholderPreference);
|
|
||||||
if (Settings.SB_ENABLED.get()) {
|
|
||||||
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading"));
|
|
||||||
Utils.runOnBackgroundThread(() -> {
|
|
||||||
UserStats stats = SBRequester.retrieveUserStats();
|
|
||||||
Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements
|
|
||||||
addUserStats(loadingPlaceholderPreference, stats);
|
|
||||||
addLocalUserStats();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled"));
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "fetchAndDisplayStats failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) {
|
|
||||||
Utils.verifyOnMainThread();
|
|
||||||
try {
|
|
||||||
if (stats == null) {
|
|
||||||
loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
statsCategory.removeAll();
|
|
||||||
Context context = statsCategory.getContext();
|
|
||||||
|
|
||||||
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
|
||||||
// If user has not created any segments, there's no reason to set a username.
|
|
||||||
EditTextPreference preference = new ResettableEditTextPreference(context);
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
String userName = stats.userName;
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
|
|
||||||
preference.setSummary(str("revanced_sb_stats_username_change"));
|
|
||||||
preference.setText(userName);
|
|
||||||
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
|
||||||
Utils.runOnBackgroundThread(() -> {
|
|
||||||
String newUserName = (String) value;
|
|
||||||
String errorMessage = SBRequester.setUsername(newUserName);
|
|
||||||
Utils.runOnMainThread(() -> {
|
|
||||||
if (errorMessage == null) {
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
|
|
||||||
preference.setText(newUserName);
|
|
||||||
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
|
||||||
} else {
|
|
||||||
preference.setText(userName); // revert to previous
|
|
||||||
SponsorBlockUtils.showErrorDialog(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// number of segment submissions (does not include ignored segments)
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
|
|
||||||
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
|
||||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
|
||||||
preference.setSelectable(false);
|
|
||||||
} else {
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
|
|
||||||
preference1.getContext().startActivity(i);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// "user reputation". Usually not useful, since it appears most users have zero reputation.
|
|
||||||
// But if there is a reputation, then show it here
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
|
|
||||||
preference.setSelectable(false);
|
|
||||||
if (stats.reputation != 0) {
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// time saved for other users
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
|
|
||||||
String stats_saved;
|
|
||||||
String stats_saved_sum;
|
|
||||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
|
||||||
stats_saved = str("revanced_sb_stats_saved_zero");
|
|
||||||
stats_saved_sum = str("revanced_sb_stats_saved_sum_zero");
|
|
||||||
} else {
|
|
||||||
stats_saved = str("revanced_sb_stats_saved",
|
|
||||||
SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount));
|
|
||||||
stats_saved_sum = str("revanced_sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
|
||||||
}
|
|
||||||
preference.setTitle(fromHtml(stats_saved));
|
|
||||||
preference.setSummary(fromHtml(stats_saved_sum));
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
|
||||||
preference1.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "addUserStats failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addLocalUserStats() {
|
|
||||||
// time the user saved by using SB
|
|
||||||
Preference preference = new Preference(statsCategory.getContext());
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
|
|
||||||
Runnable updateStatsSelfSaved = () -> {
|
|
||||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
|
|
||||||
String formattedSaved = SponsorBlockUtils.getTimeSavedString(Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
|
||||||
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
|
|
||||||
};
|
|
||||||
updateStatsSelfSaved.run();
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
new AlertDialog.Builder(preference1.getContext())
|
|
||||||
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
|
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
|
||||||
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
|
||||||
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
|
||||||
updateStatsSelfSaved.run();
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.no, null).show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,44 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple preference that opens a url when clicked.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class UrlLinkPreference extends Preference {
|
||||||
|
|
||||||
|
protected String externalUrl;
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
if (externalUrl == null) {
|
||||||
|
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse(externalUrl));
|
||||||
|
pref.getContext().startActivity(i);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public UrlLinkPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public UrlLinkPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ public class SponsorBlockSettings {
|
|||||||
@Override
|
@Override
|
||||||
public void settingsImported(@Nullable Context context) {
|
public void settingsImported(@Nullable Context context) {
|
||||||
SegmentCategory.loadAllCategoriesFromSettings();
|
SegmentCategory.loadAllCategoriesFromSettings();
|
||||||
|
SponsorBlockPreferenceGroup.settingsImported = true;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void settingsExported(@Nullable Context context) {
|
public void settingsExported(@Nullable Context context) {
|
||||||
|
@ -53,9 +53,9 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
setEntryValues(isHighlightCategory
|
setEntryValues(isHighlightCategory
|
||||||
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
||||||
: CategoryBehaviour.getBehaviorKeyValues());
|
: CategoryBehaviour.getBehaviorKeyValues());
|
||||||
setSummary(category.description.toString());
|
super.setSummary(category.description.toString());
|
||||||
|
|
||||||
updateTitleFromCategory();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -202,7 +202,7 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
||||||
try {
|
try {
|
||||||
category.resetColorAndOpacity();
|
category.resetColorAndOpacity();
|
||||||
updateTitleFromCategory();
|
updateUI();
|
||||||
Utils.showToastShort(str("revanced_sb_color_reset"));
|
Utils.showToastShort(str("revanced_sb_color_reset"));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "setNeutralButton failure", ex);
|
Logger.printException(() -> "setNeutralButton failure", ex);
|
||||||
@ -240,7 +240,7 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTitleFromCategory();
|
updateUI();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onDialogClosed failure", ex);
|
Logger.printException(() -> "onDialogClosed failure", ex);
|
||||||
@ -251,7 +251,7 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
|
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTitleFromCategory() {
|
public void updateUI() {
|
||||||
categoryColor = category.getColorNoOpacity();
|
categoryColor = category.getColorNoOpacity();
|
||||||
categoryOpacity = category.getOpacity();
|
categoryOpacity = category.getOpacity();
|
||||||
applyOpacityToCategoryColor();
|
applyOpacityToCategoryColor();
|
||||||
@ -268,4 +268,13 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
private void updateOpacityText() {
|
private void updateOpacityText() {
|
||||||
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
|
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummary(CharSequence summary) {
|
||||||
|
// Ignore calls to set the summary.
|
||||||
|
// Summary is always the description of the category.
|
||||||
|
//
|
||||||
|
// This is required otherwise the ReVanced preference fragment
|
||||||
|
// sets all ListPreference summaries to show the current selection.
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,13 +5,19 @@ import androidx.annotation.NonNull;
|
|||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SponsorBlock user stats
|
* SponsorBlock user stats
|
||||||
*/
|
*/
|
||||||
public class UserStats {
|
public class UserStats {
|
||||||
@NonNull
|
/**
|
||||||
|
* How long to cache user stats objects.
|
||||||
|
*/
|
||||||
|
private static final long STATS_EXPIRATION_MILLISECONDS = 60 * 60 * 1000; // 60 minutes.
|
||||||
|
|
||||||
|
private final String privateUserId;
|
||||||
public final String publicUserId;
|
public final String publicUserId;
|
||||||
@NonNull
|
|
||||||
public final String userName;
|
public final String userName;
|
||||||
/**
|
/**
|
||||||
* "User reputation". Unclear how SB determines this value.
|
* "User reputation". Unclear how SB determines this value.
|
||||||
@ -26,7 +32,13 @@ public class UserStats {
|
|||||||
public final int viewCount;
|
public final int viewCount;
|
||||||
public final double minutesSaved;
|
public final double minutesSaved;
|
||||||
|
|
||||||
public UserStats(@NonNull JSONObject json) throws JSONException {
|
/**
|
||||||
|
* When this stat was fetched.
|
||||||
|
*/
|
||||||
|
public final long fetchTime;
|
||||||
|
|
||||||
|
public UserStats(String privateSbId, @NonNull JSONObject json) throws JSONException {
|
||||||
|
privateUserId = privateSbId;
|
||||||
publicUserId = json.getString("userID");
|
publicUserId = json.getString("userID");
|
||||||
userName = json.getString("userName");
|
userName = json.getString("userName");
|
||||||
reputation = (float)json.getDouble("reputation");
|
reputation = (float)json.getDouble("reputation");
|
||||||
@ -35,11 +47,23 @@ public class UserStats {
|
|||||||
totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount;
|
totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount;
|
||||||
viewCount = json.getInt("viewCount");
|
viewCount = json.getInt("viewCount");
|
||||||
minutesSaved = json.getDouble("minutesSaved");
|
minutesSaved = json.getDouble("minutesSaved");
|
||||||
|
fetchTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
if (STATS_EXPIRATION_MILLISECONDS < System.currentTimeMillis() - fetchTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User changed their SB private user id.
|
||||||
|
return !SponsorBlockSettings.userHasSBPrivateId()
|
||||||
|
|| !SponsorBlockSettings.getSBPrivateUserID().equals(privateUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
// Do not include private user id in toString().
|
||||||
return "UserStats{"
|
return "UserStats{"
|
||||||
+ "publicUserId='" + publicUserId + '\''
|
+ "publicUserId='" + publicUserId + '\''
|
||||||
+ ", userName='" + userName + '\''
|
+ ", userName='" + userName + '\''
|
||||||
|
@ -47,6 +47,9 @@ public class SBRequester {
|
|||||||
*/
|
*/
|
||||||
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
|
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static volatile UserStats lastFetchedStats;
|
||||||
|
|
||||||
private SBRequester() {
|
private SBRequester() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +184,8 @@ public class SBRequester {
|
|||||||
Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage()));
|
Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage()));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "failed to submit segments", ex); // Should never happen.
|
Logger.printException(() -> "failed to submit segments", ex); // Should never happen.
|
||||||
|
} finally {
|
||||||
|
lastFetchedStats = null; // Fetch updated stats if needed.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,9 +257,17 @@ public class SBRequester {
|
|||||||
public static UserStats retrieveUserStats() {
|
public static UserStats retrieveUserStats() {
|
||||||
Utils.verifyOffMainThread();
|
Utils.verifyOffMainThread();
|
||||||
try {
|
try {
|
||||||
UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID()));
|
UserStats stats = lastFetchedStats;
|
||||||
Logger.printDebug(() -> "user stats: " + stats);
|
if (stats != null && !stats.isExpired()) {
|
||||||
return stats;
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
String privateUserID = SponsorBlockSettings.getSBPrivateUserID();
|
||||||
|
UserStats fetchedStats = new UserStats(privateUserID,
|
||||||
|
getJSONObject(SBRoutes.GET_USER_STATS, privateUserID));
|
||||||
|
Logger.printDebug(() -> "user stats: " + fetchedStats);
|
||||||
|
lastFetchedStats = fetchedStats;
|
||||||
|
return fetchedStats;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
|
Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class SponsorBlockAboutPreference extends UrlLinkPreference {
|
||||||
|
{
|
||||||
|
externalUrl = "https://sponsor.ajay.app";
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public SponsorBlockAboutPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public SponsorBlockAboutPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,471 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.*;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lots of old code that could be converted to a half dozen custom preferences,
|
||||||
|
* but instead it's wrapped in this group container and all logic is handled here.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReVanced settings were recently imported and the UI needs to be updated.
|
||||||
|
*/
|
||||||
|
public static boolean settingsImported;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the preferences have been created and added to this group.
|
||||||
|
*/
|
||||||
|
private boolean preferencesInitialized;
|
||||||
|
|
||||||
|
private SwitchPreference sbEnabled;
|
||||||
|
private SwitchPreference addNewSegment;
|
||||||
|
private SwitchPreference votingEnabled;
|
||||||
|
private SwitchPreference autoHideSkipSegmentButton;
|
||||||
|
private SwitchPreference compactSkipButton;
|
||||||
|
private SwitchPreference squareLayout;
|
||||||
|
private SwitchPreference showSkipToast;
|
||||||
|
private SwitchPreference trackSkips;
|
||||||
|
private SwitchPreference showTimeWithoutSegments;
|
||||||
|
private SwitchPreference toastOnConnectionError;
|
||||||
|
|
||||||
|
private ResettableEditTextPreference newSegmentStep;
|
||||||
|
private ResettableEditTextPreference minSegmentDuration;
|
||||||
|
private EditTextPreference privateUserId;
|
||||||
|
private EditTextPreference importExport;
|
||||||
|
private Preference apiUrl;
|
||||||
|
|
||||||
|
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
||||||
|
private PreferenceCategory segmentCategory;
|
||||||
|
|
||||||
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressLint("MissingSuperCall")
|
||||||
|
protected View onCreateView(ViewGroup parent) {
|
||||||
|
// Title is not shown.
|
||||||
|
return new View(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI() {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "updateUI");
|
||||||
|
|
||||||
|
final boolean enabled = Settings.SB_ENABLED.get();
|
||||||
|
if (!enabled) {
|
||||||
|
SponsorBlockViewController.hideAll();
|
||||||
|
SegmentPlaybackController.setCurrentVideoId(null);
|
||||||
|
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
||||||
|
SponsorBlockViewController.hideNewSegmentLayout();
|
||||||
|
}
|
||||||
|
// Voting and add new segment buttons automatically show/hide themselves.
|
||||||
|
|
||||||
|
SponsorBlockViewController.updateLayout();
|
||||||
|
|
||||||
|
sbEnabled.setChecked(enabled);
|
||||||
|
|
||||||
|
addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
|
||||||
|
addNewSegment.setEnabled(enabled);
|
||||||
|
|
||||||
|
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
||||||
|
votingEnabled.setEnabled(enabled);
|
||||||
|
|
||||||
|
autoHideSkipSegmentButton.setEnabled(enabled);
|
||||||
|
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
||||||
|
|
||||||
|
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
||||||
|
compactSkipButton.setEnabled(enabled);
|
||||||
|
|
||||||
|
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
||||||
|
squareLayout.setEnabled(enabled);
|
||||||
|
|
||||||
|
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
||||||
|
showSkipToast.setEnabled(enabled);
|
||||||
|
|
||||||
|
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
||||||
|
toastOnConnectionError.setEnabled(enabled);
|
||||||
|
|
||||||
|
trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
|
||||||
|
trackSkips.setEnabled(enabled);
|
||||||
|
|
||||||
|
showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
|
||||||
|
showTimeWithoutSegments.setEnabled(enabled);
|
||||||
|
|
||||||
|
newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
|
||||||
|
newSegmentStep.setEnabled(enabled);
|
||||||
|
|
||||||
|
minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
|
||||||
|
minSegmentDuration.setEnabled(enabled);
|
||||||
|
|
||||||
|
privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
|
||||||
|
privateUserId.setEnabled(enabled);
|
||||||
|
|
||||||
|
// If the user has a private user id, then include a subtext that mentions not to share it.
|
||||||
|
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
||||||
|
? str("revanced_sb_settings_ie_sum_warning")
|
||||||
|
: str("revanced_sb_settings_ie_sum");
|
||||||
|
importExport.setSummary(importExportSummary);
|
||||||
|
|
||||||
|
apiUrl.setEnabled(enabled);
|
||||||
|
importExport.setEnabled(enabled);
|
||||||
|
segmentCategory.setEnabled(enabled);
|
||||||
|
|
||||||
|
for (SegmentCategoryListPreference category : segmentCategories) {
|
||||||
|
category.updateUI();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "updateUI failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAttachedToActivity() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToActivity();
|
||||||
|
|
||||||
|
if (preferencesInitialized) {
|
||||||
|
if (settingsImported) {
|
||||||
|
settingsImported = false;
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preferencesInitialized = true;
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Creating settings preferences");
|
||||||
|
Context context = getContext();
|
||||||
|
SponsorBlockSettings.initialize();
|
||||||
|
|
||||||
|
sbEnabled = new SwitchPreference(context);
|
||||||
|
sbEnabled.setTitle(str("revanced_sb_enable_sb"));
|
||||||
|
sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
|
||||||
|
addPreference(sbEnabled);
|
||||||
|
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_ENABLED.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
PreferenceCategory appearanceCategory = new PreferenceCategory(context);
|
||||||
|
appearanceCategory.setTitle(str("revanced_sb_appearance_category"));
|
||||||
|
addPreference(appearanceCategory);
|
||||||
|
|
||||||
|
votingEnabled = new SwitchPreference(context);
|
||||||
|
votingEnabled.setTitle(str("revanced_sb_enable_voting"));
|
||||||
|
votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
|
||||||
|
votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
|
||||||
|
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(votingEnabled);
|
||||||
|
|
||||||
|
autoHideSkipSegmentButton = new SwitchPreference(context);
|
||||||
|
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
||||||
|
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
||||||
|
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
||||||
|
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
||||||
|
|
||||||
|
compactSkipButton = new SwitchPreference(context);
|
||||||
|
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
||||||
|
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
||||||
|
compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
|
||||||
|
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(compactSkipButton);
|
||||||
|
|
||||||
|
squareLayout = new SwitchPreference(context);
|
||||||
|
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
||||||
|
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
||||||
|
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
||||||
|
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(squareLayout);
|
||||||
|
|
||||||
|
showSkipToast = new SwitchPreference(context);
|
||||||
|
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
||||||
|
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
||||||
|
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
||||||
|
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(showSkipToast);
|
||||||
|
|
||||||
|
showTimeWithoutSegments = new SwitchPreference(context);
|
||||||
|
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
||||||
|
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
||||||
|
showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
|
||||||
|
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(showTimeWithoutSegments);
|
||||||
|
|
||||||
|
segmentCategory = new PreferenceCategory(context);
|
||||||
|
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
||||||
|
addPreference(segmentCategory);
|
||||||
|
|
||||||
|
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||||
|
SegmentCategoryListPreference categoryPreference = new SegmentCategoryListPreference(context, category);
|
||||||
|
segmentCategories.add(categoryPreference);
|
||||||
|
segmentCategory.addPreference(categoryPreference);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceCategory createSegmentCategory = new PreferenceCategory(context);
|
||||||
|
createSegmentCategory.setTitle(str("revanced_sb_create_segment_category"));
|
||||||
|
addPreference(createSegmentCategory);
|
||||||
|
|
||||||
|
addNewSegment = new SwitchPreference(context);
|
||||||
|
addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
|
||||||
|
addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
|
||||||
|
addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
|
||||||
|
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
||||||
|
Boolean newValue = (Boolean) o;
|
||||||
|
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
||||||
|
new AlertDialog.Builder(preference1.getContext())
|
||||||
|
.setTitle(str("revanced_sb_guidelines_popup_title"))
|
||||||
|
.setMessage(str("revanced_sb_guidelines_popup_content"))
|
||||||
|
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
|
||||||
|
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
|
||||||
|
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
createSegmentCategory.addPreference(addNewSegment);
|
||||||
|
|
||||||
|
newSegmentStep = new ResettableEditTextPreference(context);
|
||||||
|
newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
|
||||||
|
newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
|
||||||
|
newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
|
||||||
|
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
try {
|
||||||
|
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
||||||
|
if (newAdjustmentValue != 0) {
|
||||||
|
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
Logger.printInfo(() -> "Invalid new segment step", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
||||||
|
updateUI();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
createSegmentCategory.addPreference(newSegmentStep);
|
||||||
|
|
||||||
|
Preference guidelinePreferences = new Preference(context);
|
||||||
|
guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title"));
|
||||||
|
guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum"));
|
||||||
|
guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
openGuidelines();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
createSegmentCategory.addPreference(guidelinePreferences);
|
||||||
|
|
||||||
|
PreferenceCategory generalCategory = new PreferenceCategory(context);
|
||||||
|
generalCategory.setTitle(str("revanced_sb_general"));
|
||||||
|
addPreference(generalCategory);
|
||||||
|
|
||||||
|
toastOnConnectionError = new SwitchPreference(context);
|
||||||
|
toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
|
||||||
|
toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
|
||||||
|
toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
|
||||||
|
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(toastOnConnectionError);
|
||||||
|
|
||||||
|
trackSkips = new SwitchPreference(context);
|
||||||
|
trackSkips.setTitle(str("revanced_sb_general_skipcount"));
|
||||||
|
trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
|
||||||
|
trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
|
||||||
|
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(trackSkips);
|
||||||
|
|
||||||
|
minSegmentDuration = new ResettableEditTextPreference(context);
|
||||||
|
minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
|
||||||
|
minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
|
||||||
|
minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
|
||||||
|
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||||
|
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
try {
|
||||||
|
Float minTimeDuration = Float.valueOf(newValue.toString());
|
||||||
|
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
||||||
|
return true;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
||||||
|
updateUI();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(minSegmentDuration);
|
||||||
|
|
||||||
|
privateUserId = new EditTextPreference(context) {
|
||||||
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
|
Utils.setEditTextDialogTheme(builder);
|
||||||
|
|
||||||
|
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||||
|
Utils.setClipboard(getEditText().getText().toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
privateUserId.setTitle(str("revanced_sb_general_uuid"));
|
||||||
|
privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
|
||||||
|
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
String newUUID = newValue.toString();
|
||||||
|
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
||||||
|
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(privateUserId);
|
||||||
|
|
||||||
|
apiUrl = new Preference(context);
|
||||||
|
apiUrl.setTitle(str("revanced_sb_general_api_url"));
|
||||||
|
apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
|
||||||
|
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
EditText editText = new EditText(context);
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||||
|
editText.setText(Settings.SB_API_URL.get());
|
||||||
|
|
||||||
|
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
||||||
|
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
||||||
|
Settings.SB_API_URL.resetToDefault();
|
||||||
|
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
||||||
|
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
||||||
|
String serverAddress = editText.getText().toString();
|
||||||
|
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
||||||
|
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
||||||
|
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
||||||
|
Settings.SB_API_URL.save(serverAddress);
|
||||||
|
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(apiUrl.getTitle())
|
||||||
|
.setView(editText)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
|
||||||
|
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(apiUrl);
|
||||||
|
|
||||||
|
importExport = new EditTextPreference(context) {
|
||||||
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
|
Utils.setEditTextDialogTheme(builder);
|
||||||
|
|
||||||
|
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||||
|
Utils.setClipboard(getEditText().getText().toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
importExport.setTitle(str("revanced_sb_settings_ie"));
|
||||||
|
// Summary is set in updateUI()
|
||||||
|
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
|
||||||
|
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
importExport.getEditText().setAutofillHints((String) null);
|
||||||
|
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
|
||||||
|
importExport.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
SponsorBlockSettings.importDesktopSettings((String) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(importExport);
|
||||||
|
|
||||||
|
Utils.setPreferenceTitlesToMultiLineIfNeeded(this);
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onAttachedToActivity failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openGuidelines() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import static android.text.Html.fromHtml;
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.EditTextPreference;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User skip stats.
|
||||||
|
*
|
||||||
|
* None of the preferences here show up in search results because
|
||||||
|
* a category cannot be added to another category for the search results.
|
||||||
|
* Additionally the stats must load remotely on a background thread which means the
|
||||||
|
* preferences are not available to collect for search when the settings first load.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
||||||
|
|
||||||
|
public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAttachedToActivity() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToActivity();
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Updating SB stats UI");
|
||||||
|
final boolean enabled = Settings.SB_ENABLED.get();
|
||||||
|
setEnabled(enabled);
|
||||||
|
removeAll();
|
||||||
|
|
||||||
|
if (!SponsorBlockSettings.userHasSBPrivateId()) {
|
||||||
|
// User has never voted or created any segments. Only local stats exist.
|
||||||
|
addLocalUserStats();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference loadingPlaceholderPreference = new Preference(getContext());
|
||||||
|
loadingPlaceholderPreference.setEnabled(false);
|
||||||
|
addPreference(loadingPlaceholderPreference);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading"));
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
UserStats stats = SBRequester.retrieveUserStats();
|
||||||
|
Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements
|
||||||
|
addUserStats(loadingPlaceholderPreference, stats);
|
||||||
|
addLocalUserStats();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled"));
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onAttachedToActivity failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addUserStats(Preference loadingPlaceholder, @Nullable UserStats stats) {
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
try {
|
||||||
|
if (stats == null) {
|
||||||
|
loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeAll();
|
||||||
|
Context context = getContext();
|
||||||
|
|
||||||
|
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
||||||
|
// If user has not created any segments, there's no reason to set a username.
|
||||||
|
String userName = stats.userName;
|
||||||
|
EditTextPreference preference = new ResettableEditTextPreference(context);
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
|
||||||
|
preference.setSummary(str("revanced_sb_stats_username_change"));
|
||||||
|
preference.setText(userName);
|
||||||
|
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
String newUserName = (String) value;
|
||||||
|
String errorMessage = SBRequester.setUsername(newUserName);
|
||||||
|
Utils.runOnMainThread(() -> {
|
||||||
|
if (errorMessage == null) {
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
|
||||||
|
preference.setText(newUserName);
|
||||||
|
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
||||||
|
} else {
|
||||||
|
preference.setText(userName); // revert to previous
|
||||||
|
SponsorBlockUtils.showErrorDialog(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Number of segment submissions (does not include ignored segments).
|
||||||
|
Preference preference = new Preference(context);
|
||||||
|
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
|
||||||
|
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
||||||
|
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||||
|
preference.setSelectable(false);
|
||||||
|
} else {
|
||||||
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
|
||||||
|
preference1.getContext().startActivity(i);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// "user reputation". Usually not useful since it appears most users have zero reputation.
|
||||||
|
// But if there is a reputation then show it here.
|
||||||
|
Preference preference = new Preference(context);
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
|
||||||
|
preference.setSelectable(false);
|
||||||
|
if (stats.reputation != 0) {
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Time saved for other users.
|
||||||
|
Preference preference = new Preference(context);
|
||||||
|
|
||||||
|
String stats_saved;
|
||||||
|
String stats_saved_sum;
|
||||||
|
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||||
|
stats_saved = str("revanced_sb_stats_saved_zero");
|
||||||
|
stats_saved_sum = str("revanced_sb_stats_saved_sum_zero");
|
||||||
|
} else {
|
||||||
|
stats_saved = str("revanced_sb_stats_saved",
|
||||||
|
SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount));
|
||||||
|
stats_saved_sum = str("revanced_sb_stats_saved_sum",
|
||||||
|
SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
||||||
|
}
|
||||||
|
preference.setTitle(fromHtml(stats_saved));
|
||||||
|
preference.setSummary(fromHtml(stats_saved_sum));
|
||||||
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
||||||
|
preference1.getContext().startActivity(i);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "addUserStats failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLocalUserStats() {
|
||||||
|
// Time the user saved by using SB.
|
||||||
|
Preference preference = new Preference(getContext());
|
||||||
|
Runnable updateStatsSelfSaved = () -> {
|
||||||
|
String formatted = SponsorBlockUtils.getNumberOfSkipsString(
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
|
||||||
|
|
||||||
|
String formattedSaved = SponsorBlockUtils.getTimeSavedString(
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
||||||
|
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
|
||||||
|
};
|
||||||
|
updateStatsSelfSaved.run();
|
||||||
|
|
||||||
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
new AlertDialog.Builder(preference1.getContext())
|
||||||
|
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
|
||||||
|
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
||||||
|
updateStatsSelfSaved.run();
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.no, null).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
}
|
@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
android.useAndroidX = true
|
android.useAndroidX = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 5.22.0
|
version = 5.24.0
|
||||||
|
@ -240,6 +240,10 @@ public final class app/revanced/patches/instagram/ads/HideAdsPatchKt {
|
|||||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/instagram/misc/signature/SignatureCheckPatchKt {
|
||||||
|
public static final fun getSignatureCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt {
|
public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt {
|
||||||
public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@ -449,6 +453,14 @@ public final class app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatc
|
|||||||
public static final fun getFixCrashPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getFixCrashPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/pandora/ads/DisableAudioAdsPatchKt {
|
||||||
|
public static final fun getDisableAudioAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatchKt {
|
||||||
|
public static final fun getEnableUnlimitedSkipsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatchKt {
|
public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatchKt {
|
||||||
public static final fun getGetDeviceIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getGetDeviceIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@ -481,6 +493,14 @@ public final class app/revanced/patches/pixiv/ads/HideAdsPatchKt {
|
|||||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/primevideo/ads/SkipAdsPatchKt {
|
||||||
|
public static final fun getSkipAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatchKt {
|
||||||
|
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
|
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
|
||||||
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
}
|
}
|
||||||
@ -921,6 +941,14 @@ public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt {
|
|||||||
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
|
||||||
|
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgetsKt {
|
||||||
|
public static final fun getFixThirdPartyLaunchersWidgets ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt {
|
public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt {
|
||||||
public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@ -1265,6 +1293,10 @@ public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupan
|
|||||||
public static final fun getHidePlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getHidePlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatchKt {
|
||||||
|
public static final fun getHideRelatedVideoOverlayPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatchKt {
|
public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatchKt {
|
||||||
public static final fun getDisableRollingNumberAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getDisableRollingNumberAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@ -1509,8 +1541,10 @@ public final class app/revanced/patches/youtube/misc/settings/PreferenceScreen :
|
|||||||
public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
|
public final fun getRETURN_YOUTUBE_DISLIKE ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
|
public final fun getSPONSORBLOCK ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
}
|
}
|
||||||
@ -1603,6 +1637,7 @@ public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPat
|
|||||||
|
|
||||||
public final class app/revanced/util/BytecodeUtilsKt {
|
public final class app/revanced/util/BytecodeUtilsKt {
|
||||||
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
|
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
|
||||||
|
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;[Lapp/revanced/patcher/util/smali/ExternalLabel;)V
|
||||||
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z
|
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z
|
||||||
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z
|
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z
|
||||||
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z
|
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z
|
||||||
|
@ -9,6 +9,5 @@ internal val adInjectorFingerprint = fingerprint {
|
|||||||
parameters("L", "L")
|
parameters("L", "L")
|
||||||
strings(
|
strings(
|
||||||
"SponsoredContentController.insertItem",
|
"SponsoredContentController.insertItem",
|
||||||
"SponsoredContentController::Delivery",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.patches.instagram.misc.signature
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.indexOfFirstInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
|
internal val isValidSignatureClassFingerprint = fingerprint {
|
||||||
|
strings("The provider for uri '", "' is not trusted: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val isValidSignatureMethodFingerprint = fingerprint {
|
||||||
|
parameters("L", "Z")
|
||||||
|
returns("Z")
|
||||||
|
custom { method, _ ->
|
||||||
|
method.indexOfFirstInstruction {
|
||||||
|
getReference<MethodReference>()?.name == "keySet"
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.patches.instagram.misc.signature
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.util.returnEarly
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val signatureCheckPatch = bytecodePatch(
|
||||||
|
name = "Disable signature check",
|
||||||
|
description = "Disables the signature check that causes the app to crash on startup."
|
||||||
|
) {
|
||||||
|
compatibleWith("com.instagram.android"("378.0.0.52.68"))
|
||||||
|
|
||||||
|
execute {
|
||||||
|
isValidSignatureMethodFingerprint
|
||||||
|
.match(isValidSignatureClassFingerprint.classDef)
|
||||||
|
.method
|
||||||
|
.returnEarly(true)
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
|||||||
val disableMandatoryLoginPatch = bytecodePatch(
|
val disableMandatoryLoginPatch = bytecodePatch(
|
||||||
name = "Disable mandatory login",
|
name = "Disable mandatory login",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.adobe.lrmobile")
|
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
isLoggedInFingerprint.method.apply {
|
isLoggedInFingerprint.method.apply {
|
||||||
|
@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
|||||||
val unlockPremiumPatch = bytecodePatch(
|
val unlockPremiumPatch = bytecodePatch(
|
||||||
name = "Unlock premium",
|
name = "Unlock premium",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.adobe.lrmobile")
|
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
// Set hasPremium = true.
|
// Set hasPremium = true.
|
||||||
|
@ -4,10 +4,10 @@ import app.revanced.patcher.fingerprint
|
|||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
internal val jwUtilCreateAdvertisementFingerprint = fingerprint {
|
internal val jwPlayerConfigFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
|
accessFlags(AccessFlags.PUBLIC)
|
||||||
custom { methodDef, classDef ->
|
custom { methodDef, classDef ->
|
||||||
classDef.type == "Lnl/sanomamedia/android/nu/video/util/JWUtil;" && methodDef.name == "createAdvertising"
|
classDef.type == "Lcom/jwplayer/pub/api/configuration/PlayerConfig${'$'}Builder;" && methodDef.name == "advertisingConfig"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,11 @@ package app.revanced.patches.nunl.ads
|
|||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@ -11,23 +14,15 @@ val hideAdsPatch = bytecodePatch(
|
|||||||
name = "Hide ads",
|
name = "Hide ads",
|
||||||
description = "Hide ads and sponsored articles in list pages and remove pre-roll ads on videos.",
|
description = "Hide ads and sponsored articles in list pages and remove pre-roll ads on videos.",
|
||||||
) {
|
) {
|
||||||
compatibleWith("nl.sanomamedia.android.nu"("11.0.0", "11.0.1", "11.1.0"))
|
compatibleWith("nl.sanomamedia.android.nu"("11.3.0"))
|
||||||
|
|
||||||
dependsOn(sharedExtensionPatch("nunl", mainActivityOnCreateHook))
|
dependsOn(sharedExtensionPatch("nunl", mainActivityOnCreateHook))
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
// Disable video pre-roll ads.
|
// Disable video pre-roll ads.
|
||||||
// Whenever the app tries to create an ad via JWUtils.createAdvertising, don't actually tell the underlying JWPlayer library to do so => JWPlayer will not display ads.
|
// Whenever the app tries to define the advertising config for JWPlayer, don't set the advertising config and directly return.
|
||||||
jwUtilCreateAdvertisementFingerprint.method.addInstructions(
|
val iputInstructionIndex = jwPlayerConfigFingerprint.method.indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT)
|
||||||
0,
|
jwPlayerConfigFingerprint.method.removeInstructions(iputInstructionIndex, 1)
|
||||||
"""
|
|
||||||
new-instance v0, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;
|
|
||||||
invoke-direct { v0 }, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;-><init>()V
|
|
||||||
invoke-virtual { v0 }, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;->build()Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig;
|
|
||||||
move-result-object v0
|
|
||||||
return-object v0
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Filter injected content from API calls out of lists.
|
// Filter injected content from API calls out of lists.
|
||||||
arrayOf(screenMapperFingerprint, nextPageRepositoryImplFingerprint).forEach {
|
arrayOf(screenMapperFingerprint, nextPageRepositoryImplFingerprint).forEach {
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.patches.pandora.ads
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.pandora.shared.constructUserDataFingerprint
|
||||||
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val disableAudioAdsPatch = bytecodePatch(
|
||||||
|
name = "Disable audio ads",
|
||||||
|
) {
|
||||||
|
compatibleWith("com.pandora.android")
|
||||||
|
|
||||||
|
execute {
|
||||||
|
constructUserDataFingerprint.method.apply {
|
||||||
|
// First match is "hasAudioAds".
|
||||||
|
val hasAudioAdsStringIndex = constructUserDataFingerprint.stringMatches!!.first().index
|
||||||
|
val moveResultIndex = indexOfFirstInstructionOrThrow(hasAudioAdsStringIndex, Opcode.MOVE_RESULT)
|
||||||
|
val hasAudioAdsRegister = getInstruction<OneRegisterInstruction>(moveResultIndex).registerA
|
||||||
|
|
||||||
|
addInstruction(
|
||||||
|
moveResultIndex + 1,
|
||||||
|
"const/4 v$hasAudioAdsRegister, 0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package app.revanced.patches.pandora.misc
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.pandora.shared.constructUserDataFingerprint
|
||||||
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val enableUnlimitedSkipsPatch = bytecodePatch(
|
||||||
|
name = "Enable unlimited skips",
|
||||||
|
) {
|
||||||
|
compatibleWith("com.pandora.android")
|
||||||
|
|
||||||
|
execute {
|
||||||
|
constructUserDataFingerprint.method.apply {
|
||||||
|
// Last match is "skipLimitBehavior".
|
||||||
|
val skipLimitBehaviorStringIndex = constructUserDataFingerprint.stringMatches!!.last().index
|
||||||
|
val moveResultObjectIndex =
|
||||||
|
indexOfFirstInstructionOrThrow(skipLimitBehaviorStringIndex, Opcode.MOVE_RESULT_OBJECT)
|
||||||
|
val skipLimitBehaviorRegister = getInstruction<OneRegisterInstruction>(moveResultObjectIndex).registerA
|
||||||
|
|
||||||
|
addInstruction(
|
||||||
|
moveResultObjectIndex + 1,
|
||||||
|
"const-string v$skipLimitBehaviorRegister, \"unlimited\""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package app.revanced.patches.pandora.shared
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
|
||||||
|
internal val constructUserDataFingerprint = fingerprint {
|
||||||
|
strings("hasAudioAds", "skipLimitBehavior")
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.patches.primevideo.ads
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
internal val enterServerInsertedAdBreakStateFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PUBLIC)
|
||||||
|
parameters("Lcom/amazon/avod/fsm/Trigger;")
|
||||||
|
returns("V")
|
||||||
|
opcodes(
|
||||||
|
Opcode.INVOKE_VIRTUAL,
|
||||||
|
Opcode.MOVE_RESULT_OBJECT,
|
||||||
|
Opcode.CONST_4,
|
||||||
|
Opcode.CONST_4
|
||||||
|
)
|
||||||
|
custom { method, classDef ->
|
||||||
|
method.name == "enter" && classDef.type == "Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val doTriggerFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PROTECTED)
|
||||||
|
returns("V")
|
||||||
|
opcodes(
|
||||||
|
Opcode.IGET_OBJECT,
|
||||||
|
Opcode.INVOKE_INTERFACE,
|
||||||
|
Opcode.RETURN_VOID
|
||||||
|
)
|
||||||
|
custom { method, classDef ->
|
||||||
|
method.name == "doTrigger" && classDef.type == "Lcom/amazon/avod/fsm/StateBase;"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package app.revanced.patches.primevideo.ads
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val skipAdsPatch = bytecodePatch(
|
||||||
|
name = "Skip ads",
|
||||||
|
description = "Automatically skips video stream ads.",
|
||||||
|
) {
|
||||||
|
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
|
||||||
|
|
||||||
|
dependsOn(sharedExtensionPatch)
|
||||||
|
|
||||||
|
// Skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
|
||||||
|
// ad break. Instead, force the video player to seek over the entire break and reset the state machine.
|
||||||
|
execute {
|
||||||
|
// Force doTrigger() access to public so we can call it from our extension.
|
||||||
|
doTriggerFingerprint.method.accessFlags = AccessFlags.PUBLIC.value;
|
||||||
|
|
||||||
|
val getPlayerIndex = enterServerInsertedAdBreakStateFingerprint.patternMatch!!.startIndex
|
||||||
|
enterServerInsertedAdBreakStateFingerprint.method.apply {
|
||||||
|
// Get register that stores VideoPlayer:
|
||||||
|
// invoke-virtual ->getPrimaryPlayer()
|
||||||
|
// move-result-object { playerRegister }
|
||||||
|
val playerRegister = getInstruction<OneRegisterInstruction>(getPlayerIndex + 1).registerA
|
||||||
|
|
||||||
|
// Reuse the params from the original method:
|
||||||
|
// p0 = ServerInsertedAdBreakState
|
||||||
|
// p1 = AdBreakTrigger
|
||||||
|
addInstructions(
|
||||||
|
getPlayerIndex + 2,
|
||||||
|
"""
|
||||||
|
invoke-static { p0, p1, v$playerRegister }, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->enterServerInsertedAdBreakState(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V
|
||||||
|
return-void
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
|||||||
|
package app.revanced.patches.primevideo.misc.extension
|
||||||
|
|
||||||
|
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||||
|
|
||||||
|
val sharedExtensionPatch = sharedExtensionPatch("primevideo", applicationInitHook)
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patches.primevideo.misc.extension
|
||||||
|
|
||||||
|
import app.revanced.patches.shared.misc.extension.extensionHook
|
||||||
|
|
||||||
|
internal val applicationInitHook = extensionHook {
|
||||||
|
custom { method, classDef ->
|
||||||
|
method.name == "onCreate" && classDef.endsWith("/SplashScreenActivity;")
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,12 @@ package app.revanced.patches.spotify.misc
|
|||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
import app.revanced.patcher.fingerprint
|
||||||
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.indexOfFirstInstruction
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||||
|
|
||||||
internal val accountAttributeFingerprint = fingerprint {
|
internal val accountAttributeFingerprint = fingerprint {
|
||||||
custom { _, classDef ->
|
custom { _, classDef ->
|
||||||
@ -15,7 +19,7 @@ internal val accountAttributeFingerprint = fingerprint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val productStateProtoFingerprint = fingerprint {
|
internal val productStateProtoGetMapFingerprint = fingerprint {
|
||||||
returns("Ljava/util/Map;")
|
returns("Ljava/util/Map;")
|
||||||
custom { _, classDef ->
|
custom { _, classDef ->
|
||||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
@ -56,16 +60,40 @@ internal val readPlayerOptionOverridesFingerprint = fingerprint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val homeSectionFingerprint = fingerprint {
|
|
||||||
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val protobufListsFingerprint = fingerprint {
|
internal val protobufListsFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||||
custom { method, _ -> method.name == "emptyProtobufList" }
|
custom { method, _ -> method.name == "emptyProtobufList" }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val homeStructureFingerprint = fingerprint {
|
internal val protobufListRemoveFingerprint = fingerprint {
|
||||||
opcodes(Opcode.IGET_OBJECT, Opcode.RETURN_OBJECT)
|
custom { method, _ -> method.name == "remove" }
|
||||||
custom { _, classDef -> classDef.endsWith("homeapi/proto/HomeStructure;") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val homeSectionFingerprint = fingerprint {
|
||||||
|
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val homeStructureGetSectionsFingerprint = fingerprint {
|
||||||
|
custom { method, classDef ->
|
||||||
|
classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
|
||||||
|
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
|
||||||
|
returns("Ljava/lang/Object;")
|
||||||
|
parameters("Ljava/lang/Object;")
|
||||||
|
custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction {
|
||||||
|
opcode == Opcode.NEW_INSTANCE && getReference<TypeReference>()?.type?.endsWith(className) == true
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME = "FetchMessageRequest;"
|
||||||
|
internal val pendragonJsonFetchMessageRequestFingerprint =
|
||||||
|
reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME)
|
||||||
|
|
||||||
|
internal const val PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME = "FetchMessageListRequest;"
|
||||||
|
internal val pendragonProtoFetchMessageListRequestFingerprint =
|
||||||
|
reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME)
|
||||||
|
@ -4,22 +4,25 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
|||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
import app.revanced.patcher.fingerprint
|
import app.revanced.patcher.patch.PatchException
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.spotify.misc.check.checkEnvironmentPatch
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.util.getReference
|
import app.revanced.util.getReference
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import app.revanced.util.toPublicAccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
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.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
|
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
|
||||||
@ -42,14 +45,18 @@ val unlockPremiumPatch = bytecodePatch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
// Make _value accessible so that it can be overridden in the extension.
|
fun MutableClass.publicizeField(fieldName: String) {
|
||||||
accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply {
|
fields.first { it.name == fieldName }.apply {
|
||||||
// Add public flag and remove private.
|
// Add public and remove private flag.
|
||||||
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
|
accessFlags = accessFlags.toPublicAccessFlags()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make _value accessible so that it can be overridden in the extension.
|
||||||
|
accountAttributeFingerprint.classDef.publicizeField("value_")
|
||||||
|
|
||||||
// Override the attributes map in the getter method.
|
// Override the attributes map in the getter method.
|
||||||
productStateProtoFingerprint.method.apply {
|
productStateProtoGetMapFingerprint.method.apply {
|
||||||
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
|
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
|
||||||
val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA
|
val attributesMapRegister = getInstruction<TwoRegisterInstruction>(getAttributesMapIndex).registerA
|
||||||
|
|
||||||
@ -62,19 +69,20 @@ val unlockPremiumPatch = bytecodePatch(
|
|||||||
|
|
||||||
|
|
||||||
// Add the query parameter trackRows to show popular tracks in the artist page.
|
// Add the query parameter trackRows to show popular tracks in the artist page.
|
||||||
buildQueryParametersFingerprint.apply {
|
buildQueryParametersFingerprint.method.apply {
|
||||||
val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow(
|
val addQueryParameterConditionIndex = indexOfFirstInstructionReversedOrThrow(
|
||||||
stringMatches!!.first().index, Opcode.IF_EQZ
|
buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ
|
||||||
)
|
)
|
||||||
|
|
||||||
method.replaceInstruction(addQueryParameterConditionIndex, "nop")
|
replaceInstruction(addQueryParameterConditionIndex, "nop")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
return@execute Logger.getLogger(this::class.java.name).warning(
|
Logger.getLogger(this::class.java.name).warning(
|
||||||
"Patching a legacy Spotify version. Patch functionality may be limited."
|
"Patching a legacy Spotify version. Patch functionality may be limited."
|
||||||
)
|
)
|
||||||
|
return@execute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -105,48 +113,39 @@ val unlockPremiumPatch = bytecodePatch(
|
|||||||
val shufflingContextCallIndex = indexOfFirstInstructionOrThrow {
|
val shufflingContextCallIndex = indexOfFirstInstructionOrThrow {
|
||||||
getReference<MethodReference>()?.name == "shufflingContext"
|
getReference<MethodReference>()?.name == "shufflingContext"
|
||||||
}
|
}
|
||||||
|
val boolRegister = getInstruction<FiveRegisterInstruction>(shufflingContextCallIndex).registerD
|
||||||
|
|
||||||
val registerBool = getInstruction<FiveRegisterInstruction>(shufflingContextCallIndex).registerD
|
|
||||||
addInstruction(
|
addInstruction(
|
||||||
shufflingContextCallIndex,
|
shufflingContextCallIndex,
|
||||||
"sget-object v$registerBool, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
|
"sget-object v$boolRegister, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Disable the "Spotify Premium" upsell experiment in context menus.
|
// Disable the "Spotify Premium" upsell experiment in context menus.
|
||||||
contextMenuExperimentsFingerprint.apply {
|
contextMenuExperimentsFingerprint.method.apply {
|
||||||
val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow(
|
val moveIsEnabledIndex = indexOfFirstInstructionOrThrow(
|
||||||
stringMatches!!.first().index, Opcode.MOVE_RESULT
|
contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT
|
||||||
)
|
)
|
||||||
val isUpsellEnabledRegister = method.getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
|
val isUpsellEnabledRegister = getInstruction<OneRegisterInstruction>(moveIsEnabledIndex).registerA
|
||||||
|
|
||||||
method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
|
replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Make featureTypeCase_ accessible so we can check the home section type in the extension.
|
val protobufListClassDef = with(protobufListsFingerprint.originalMethod) {
|
||||||
homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply {
|
|
||||||
// Add public flag and remove private.
|
|
||||||
accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
|
|
||||||
}
|
|
||||||
|
|
||||||
val protobufListClassName = with(protobufListsFingerprint.originalMethod) {
|
|
||||||
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
|
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
|
||||||
getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass
|
// Find the protobuffer list class using the definingClass which contains the empty list static value.
|
||||||
}
|
val classType = getInstruction(emptyProtobufListGetIndex).getReference<FieldReference>()!!.definingClass
|
||||||
|
|
||||||
val protobufListRemoveFingerprint = fingerprint {
|
classes.find { it.type == classType } ?: throw PatchException("Could not find protobuffer list class.")
|
||||||
custom { method, classDef ->
|
|
||||||
method.name == "remove" && classDef.type == protobufListClassName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to allow mutation of the list so the home ads sections can be removed.
|
// Need to allow mutation of the list so the home ads sections can be removed.
|
||||||
// Protobuffer list has an 'isMutable' boolean parameter that sets the mutability.
|
// Protobuffer list has an 'isMutable' boolean parameter that sets the mutability.
|
||||||
// Forcing that always on breaks unrelated code in strange ways.
|
// Forcing that always on breaks unrelated code in strange ways.
|
||||||
// Instead, remove the method call that checks if the list is unmodifiable.
|
// Instead, remove the method call that checks if the list is unmodifiable.
|
||||||
protobufListRemoveFingerprint.method.apply {
|
protobufListRemoveFingerprint.match(protobufListClassDef).method.apply {
|
||||||
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
|
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
|
||||||
val reference = getReference<MethodReference>()
|
val reference = getReference<MethodReference>()
|
||||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||||
@ -157,8 +156,12 @@ val unlockPremiumPatch = bytecodePatch(
|
|||||||
removeInstruction(invokeThrowUnmodifiableIndex)
|
removeInstruction(invokeThrowUnmodifiableIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Make featureTypeCase_ accessible so we can check the home section type in the extension.
|
||||||
|
homeSectionFingerprint.classDef.publicizeField("featureTypeCase_")
|
||||||
|
|
||||||
// Remove ads sections from home.
|
// Remove ads sections from home.
|
||||||
homeStructureFingerprint.method.apply {
|
homeStructureGetSectionsFingerprint.method.apply {
|
||||||
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
|
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
|
||||||
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA
|
val sectionsRegister = getInstruction<TwoRegisterInstruction>(getSectionsIndex).registerA
|
||||||
|
|
||||||
@ -168,5 +171,56 @@ val unlockPremiumPatch = bytecodePatch(
|
|||||||
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
|
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Replace a fetch request that returns and maps Singles with their static onErrorReturn value.
|
||||||
|
fun MutableMethod.replaceFetchRequestSingleWithError(requestClassName: String) {
|
||||||
|
// The index of where the request class is being instantiated.
|
||||||
|
val requestInstantiationIndex = indexOfFirstInstructionOrThrow {
|
||||||
|
getReference<TypeReference>()?.type?.endsWith(requestClassName) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The index of where the onErrorReturn method is called with the error static value.
|
||||||
|
val onErrorReturnCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
|
||||||
|
getReference<MethodReference>()?.name == "onErrorReturn"
|
||||||
|
}
|
||||||
|
val onErrorReturnCallInstruction = getInstruction<FiveRegisterInstruction>(onErrorReturnCallIndex)
|
||||||
|
|
||||||
|
// The error static value register.
|
||||||
|
val onErrorReturnValueRegister = onErrorReturnCallInstruction.registerD
|
||||||
|
|
||||||
|
// The index where the error static value starts being constructed.
|
||||||
|
// Because the Singles are mapped, the error static value starts being constructed right after the first
|
||||||
|
// move-result-object of the map call, before the onErrorReturn method call.
|
||||||
|
val onErrorReturnValueConstructionIndex =
|
||||||
|
indexOfFirstInstructionReversedOrThrow(onErrorReturnCallIndex, Opcode.MOVE_RESULT_OBJECT) + 1
|
||||||
|
|
||||||
|
val singleClassName = onErrorReturnCallInstruction.getReference<MethodReference>()!!.definingClass
|
||||||
|
// The index where the request is firstly called, before its result is mapped to other values.
|
||||||
|
val requestCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
|
||||||
|
getReference<MethodReference>()?.returnType == singleClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a new single with the error static value and return it.
|
||||||
|
addInstructions(
|
||||||
|
onErrorReturnCallIndex,
|
||||||
|
"invoke-static { v$onErrorReturnValueRegister }, " +
|
||||||
|
"$singleClassName->just(Ljava/lang/Object;)$singleClassName\n" +
|
||||||
|
"move-result-object v$onErrorReturnValueRegister\n" +
|
||||||
|
"return-object v$onErrorReturnValueRegister"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove every instruction from the request call to right before the error static value construction.
|
||||||
|
val removeCount = onErrorReturnValueConstructionIndex - requestCallIndex
|
||||||
|
removeInstructions(requestCallIndex, removeCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove pendragon (pop up ads) requests and return the errors instead.
|
||||||
|
pendragonJsonFetchMessageRequestFingerprint.method.replaceFetchRequestSingleWithError(
|
||||||
|
PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME
|
||||||
|
)
|
||||||
|
pendragonProtoFetchMessageListRequestFingerprint.method.replaceFetchRequestSingleWithError(
|
||||||
|
PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package app.revanced.patches.spotify.misc.privacy
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import app.revanced.util.literal
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
|
||||||
|
internal val shareCopyUrlFingerprint = fingerprint {
|
||||||
|
returns("Ljava/lang/Object;")
|
||||||
|
parameters("Ljava/lang/Object;")
|
||||||
|
strings("clipboard", "Spotify Link")
|
||||||
|
custom { method, _ ->
|
||||||
|
method.name == "invokeSuspend"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val shareCopyUrlLegacyFingerprint = fingerprint {
|
||||||
|
returns("Ljava/lang/Object;")
|
||||||
|
parameters("Ljava/lang/Object;")
|
||||||
|
strings("clipboard", "createNewSession failed")
|
||||||
|
custom { method, _ ->
|
||||||
|
method.name == "apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||||
|
returns("Ljava/lang/String;")
|
||||||
|
parameters("L", "Ljava/lang/String;")
|
||||||
|
literal {
|
||||||
|
'\n'.code.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PUBLIC)
|
||||||
|
returns("Ljava/lang/String;")
|
||||||
|
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
|
||||||
|
literal {
|
||||||
|
'\n'.code.toLong()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package app.revanced.patches.spotify.misc.privacy
|
||||||
|
|
||||||
|
import app.revanced.patcher.Fingerprint
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
|
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
|
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
|
"Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val sanitizeSharingLinksPatch = bytecodePatch(
|
||||||
|
name = "Sanitize sharing links",
|
||||||
|
description = "Removes the tracking query parameters from links before they are shared.",
|
||||||
|
) {
|
||||||
|
compatibleWith("com.spotify.music")
|
||||||
|
|
||||||
|
dependsOn(sharedExtensionPatch)
|
||||||
|
|
||||||
|
execute {
|
||||||
|
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||||
|
"sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
|
||||||
|
|
||||||
|
val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
|
shareCopyUrlLegacyFingerprint
|
||||||
|
} else {
|
||||||
|
shareCopyUrlFingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
copyFingerprint.method.apply {
|
||||||
|
val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
|
||||||
|
getReference<MethodReference>()?.name == "newPlainText"
|
||||||
|
}
|
||||||
|
val register = getInstruction<FiveRegisterInstruction>(newPlainTextInvokeIndex).registerD
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
newPlainTextInvokeIndex,
|
||||||
|
"""
|
||||||
|
invoke-static { v$register }, $extensionMethodDescriptor
|
||||||
|
move-result-object v$register
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
|
||||||
|
val shareUrlParameter : String
|
||||||
|
val shareSheetFingerprint : Fingerprint
|
||||||
|
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||||
|
shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
|
||||||
|
shareUrlParameter = "p2"
|
||||||
|
} else {
|
||||||
|
shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
|
||||||
|
shareUrlParameter = "p1"
|
||||||
|
}
|
||||||
|
|
||||||
|
shareSheetFingerprint.method.addInstructions(
|
||||||
|
0,
|
||||||
|
"""
|
||||||
|
invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
|
||||||
|
move-result-object $shareUrlParameter
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.patches.spotify.misc.widgets
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import app.revanced.util.indexOfFirstInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
internal val canBindAppWidgetPermissionFingerprint = fingerprint {
|
||||||
|
strings("android.permission.BIND_APPWIDGET")
|
||||||
|
opcodes(Opcode.AND_INT_LIT8)
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.patches.spotify.misc.widgets
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.util.returnEarly
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val fixThirdPartyLaunchersWidgets = bytecodePatch(
|
||||||
|
name = "Fix third party launchers widgets",
|
||||||
|
description = "Fixes Spotify widgets not working in third party launchers, like Nova Launcher.",
|
||||||
|
) {
|
||||||
|
compatibleWith("com.spotify.music")
|
||||||
|
|
||||||
|
execute {
|
||||||
|
// Only system app launchers are granted the BIND_APPWIDGET permission.
|
||||||
|
// Override the method that checks for it to always return true, as this permission is not actually required
|
||||||
|
// for the widgets to work.
|
||||||
|
canBindAppWidgetPermissionFingerprint.method.returnEarly(true)
|
||||||
|
}
|
||||||
|
}
|
@ -143,6 +143,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
|||||||
key = "revanced_hide_description_components_screen",
|
key = "revanced_hide_description_components_screen",
|
||||||
preferences = setOf(
|
preferences = setOf(
|
||||||
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
|
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
|
||||||
|
SwitchPreference("revanced_hide_ask_section"),
|
||||||
SwitchPreference("revanced_hide_attributes_section"),
|
SwitchPreference("revanced_hide_attributes_section"),
|
||||||
SwitchPreference("revanced_hide_chapters_section"),
|
SwitchPreference("revanced_hide_chapters_section"),
|
||||||
SwitchPreference("revanced_hide_info_cards_section"),
|
SwitchPreference("revanced_hide_info_cards_section"),
|
||||||
@ -222,8 +223,9 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
|||||||
SwitchPreference("revanced_hide_movies_section"),
|
SwitchPreference("revanced_hide_movies_section"),
|
||||||
SwitchPreference("revanced_hide_notify_me_button"),
|
SwitchPreference("revanced_hide_notify_me_button"),
|
||||||
SwitchPreference("revanced_hide_playables"),
|
SwitchPreference("revanced_hide_playables"),
|
||||||
SwitchPreference("revanced_hide_search_result_recommendations"),
|
SwitchPreference("revanced_hide_search_result_recommendation_labels"),
|
||||||
SwitchPreference("revanced_hide_show_more_button"),
|
SwitchPreference("revanced_hide_show_more_button"),
|
||||||
|
SwitchPreference("revanced_hide_ticket_shelf"),
|
||||||
SwitchPreference("revanced_hide_doodles"),
|
SwitchPreference("revanced_hide_doodles"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patches.youtube.layout.hide.relatedvideooverlay
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import app.revanced.util.literal
|
||||||
|
|
||||||
|
internal val relatedEndScreenResultsParentFingerprint = fingerprint {
|
||||||
|
returns("V")
|
||||||
|
literal{ appRelatedEndScreenResults }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val relatedEndScreenResultsFingerprint = fingerprint {
|
||||||
|
returns("V")
|
||||||
|
parameters(
|
||||||
|
"I",
|
||||||
|
"Z",
|
||||||
|
"I",
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package app.revanced.patches.youtube.layout.hide.relatedvideooverlay
|
||||||
|
|
||||||
|
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.resourcePatch
|
||||||
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
|
import app.revanced.patches.shared.misc.mapping.get
|
||||||
|
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
|
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
|
|
||||||
|
internal var appRelatedEndScreenResults = -1L
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val hideRelatedVideoOverlayResourcePatch = resourcePatch {
|
||||||
|
dependsOn(
|
||||||
|
resourceMappingPatch,
|
||||||
|
)
|
||||||
|
|
||||||
|
execute {
|
||||||
|
appRelatedEndScreenResults = resourceMappings[
|
||||||
|
"layout",
|
||||||
|
"app_related_endscreen_results",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
|
"Lapp/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch;"
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val hideRelatedVideoOverlayPatch = bytecodePatch(
|
||||||
|
name = "Hide related video overlay",
|
||||||
|
description = "Adds an option to hide the related video overlay shown when swiping up in fullscreen.",
|
||||||
|
) {
|
||||||
|
dependsOn(
|
||||||
|
settingsPatch,
|
||||||
|
sharedExtensionPatch,
|
||||||
|
addResourcesPatch,
|
||||||
|
hideRelatedVideoOverlayResourcePatch,
|
||||||
|
)
|
||||||
|
|
||||||
|
compatibleWith(
|
||||||
|
"com.google.android.youtube"(
|
||||||
|
"19.16.39",
|
||||||
|
"19.25.37",
|
||||||
|
"19.34.42",
|
||||||
|
"19.43.41",
|
||||||
|
"19.47.53",
|
||||||
|
"20.07.39",
|
||||||
|
"20.12.46",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
execute {
|
||||||
|
addResources("youtube", "layout.hide.relatedvideooverlay.hideRelatedVideoOverlayPatch")
|
||||||
|
|
||||||
|
PreferenceScreen.PLAYER.addPreferences(
|
||||||
|
SwitchPreference("revanced_hide_related_video_overlay")
|
||||||
|
)
|
||||||
|
|
||||||
|
relatedEndScreenResultsFingerprint.match(
|
||||||
|
relatedEndScreenResultsParentFingerprint.originalClassDef
|
||||||
|
).method.apply {
|
||||||
|
addInstructionsWithLabels(
|
||||||
|
0,
|
||||||
|
"""
|
||||||
|
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideRelatedVideoOverlay()Z
|
||||||
|
move-result v0
|
||||||
|
if-eqz v0, :show
|
||||||
|
return-void
|
||||||
|
""",
|
||||||
|
ExternalLabel("show", getInstruction(0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,18 +5,6 @@ import app.revanced.util.literal
|
|||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
internal val conversionContextFingerprint = fingerprint {
|
|
||||||
returns("Ljava/lang/String;")
|
|
||||||
parameters()
|
|
||||||
strings(
|
|
||||||
", widthConstraint=",
|
|
||||||
", heightConstraint=",
|
|
||||||
", templateLoggerFactory=",
|
|
||||||
", rootDisposableContainer=",
|
|
||||||
"ConversionContext{containerInternal=",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val dislikeFingerprint = fingerprint {
|
internal val dislikeFingerprint = fingerprint {
|
||||||
returns("V")
|
returns("V")
|
||||||
strings("like/dislike")
|
strings("like/dislike")
|
||||||
|
@ -6,7 +6,10 @@ import app.revanced.patcher.patch.PatchException
|
|||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
|
import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
@ -15,14 +18,18 @@ import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater
|
|||||||
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_20_10_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_20_10_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
||||||
import app.revanced.patches.youtube.misc.settings.addSettingPreference
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
import app.revanced.patches.youtube.misc.settings.newIntent
|
|
||||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
|
import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
|
||||||
import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint
|
import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint
|
||||||
import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId
|
import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId
|
||||||
import app.revanced.patches.youtube.video.videoid.hookVideoId
|
import app.revanced.patches.youtube.video.videoid.hookVideoId
|
||||||
import app.revanced.patches.youtube.video.videoid.videoIdPatch
|
import app.revanced.patches.youtube.video.videoid.videoIdPatch
|
||||||
import app.revanced.util.*
|
import app.revanced.util.addInstructionsAtControlFlowLabel
|
||||||
|
import app.revanced.util.findFreeRegister
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import app.revanced.util.returnLate
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
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.OneRegisterInstruction
|
||||||
@ -67,15 +74,24 @@ val returnYouTubeDislikePatch = bytecodePatch(
|
|||||||
execute {
|
execute {
|
||||||
addResources("youtube", "layout.returnyoutubedislike.returnYouTubeDislikePatch")
|
addResources("youtube", "layout.returnyoutubedislike.returnYouTubeDislikePatch")
|
||||||
|
|
||||||
addSettingPreference(
|
PreferenceScreen.RETURN_YOUTUBE_DISLIKE.addPreferences(
|
||||||
IntentPreference(
|
SwitchPreference("revanced_ryd_enabled"),
|
||||||
key = "revanced_settings_screen_09",
|
SwitchPreference("revanced_ryd_shorts"),
|
||||||
titleKey = "revanced_ryd_settings_title",
|
SwitchPreference("revanced_ryd_dislike_percentage"),
|
||||||
summaryKey = null,
|
SwitchPreference("revanced_ryd_compact_layout"),
|
||||||
icon = "@drawable/revanced_settings_screen_09_ryd",
|
SwitchPreference("revanced_ryd_estimated_like"),
|
||||||
layout = "@layout/preference_with_icon",
|
SwitchPreference("revanced_ryd_toast_on_connection_error"),
|
||||||
intent = newIntent("revanced_ryd_settings_intent"),
|
NonInteractivePreference(
|
||||||
|
key = "revanced_ryd_attribution",
|
||||||
|
tag = "app.revanced.extension.youtube.returnyoutubedislike.ui.ReturnYouTubeDislikeAboutPreference",
|
||||||
|
selectable = true,
|
||||||
),
|
),
|
||||||
|
PreferenceCategory(
|
||||||
|
key = "revanced_ryd_statistics_category",
|
||||||
|
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
||||||
|
preferences = emptySet(), // Preferences are added by custom class at runtime.
|
||||||
|
tag = "app.revanced.extension.youtube.returnyoutubedislike.ui.ReturnYouTubeDislikeDebugStatsPreferenceCategory"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// region Inject newVideoLoaded event handler to update dislikes when a new video is loaded.
|
// region Inject newVideoLoaded event handler to update dislikes when a new video is loaded.
|
||||||
@ -113,11 +129,11 @@ val returnYouTubeDislikePatch = bytecodePatch(
|
|||||||
// This hook handles all situations, as it's where the created Spans are stored and later reused.
|
// This hook handles all situations, as it's where the created Spans are stored and later reused.
|
||||||
// Find the field name of the conversion context.
|
// Find the field name of the conversion context.
|
||||||
val conversionContextField = textComponentConstructorFingerprint.originalClassDef.fields.find {
|
val conversionContextField = textComponentConstructorFingerprint.originalClassDef.fields.find {
|
||||||
it.type == conversionContextFingerprint.originalClassDef.type
|
it.type == conversionContextFingerprintToString.originalClassDef.type
|
||||||
} ?: throw PatchException("Could not find conversion context field")
|
} ?: throw PatchException("Could not find conversion context field")
|
||||||
|
|
||||||
textComponentLookupFingerprint.match(textComponentConstructorFingerprint.originalClassDef)
|
textComponentLookupFingerprint.match(textComponentConstructorFingerprint.originalClassDef)
|
||||||
textComponentLookupFingerprint.method.apply {
|
.method.apply {
|
||||||
// Find the instruction for creating the text data object.
|
// Find the instruction for creating the text data object.
|
||||||
val textDataClassType = textComponentDataFingerprint.originalClassDef.type
|
val textDataClassType = textComponentDataFingerprint.originalClassDef.type
|
||||||
|
|
||||||
@ -160,12 +176,12 @@ val returnYouTubeDislikePatch = bytecodePatch(
|
|||||||
addInstructionsAtControlFlowLabel(
|
addInstructionsAtControlFlowLabel(
|
||||||
insertIndex,
|
insertIndex,
|
||||||
"""
|
"""
|
||||||
# Copy conversion context
|
# Copy conversion context
|
||||||
move-object/from16 v$tempRegister, p0
|
move-object/from16 v$tempRegister, p0
|
||||||
iget-object v$tempRegister, v$tempRegister, $conversionContextField
|
iget-object v$tempRegister, v$tempRegister, $conversionContextField
|
||||||
invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
|
invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
|
||||||
move-result-object v$charSequenceRegister
|
move-result-object v$charSequenceRegister
|
||||||
""",
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,11 +217,9 @@ val returnYouTubeDislikePatch = bytecodePatch(
|
|||||||
val charSequenceFieldReference =
|
val charSequenceFieldReference =
|
||||||
getInstruction<ReferenceInstruction>(dislikesIndex).reference
|
getInstruction<ReferenceInstruction>(dislikesIndex).reference
|
||||||
|
|
||||||
val registerCount = implementation!!.registerCount
|
val conversionContextRegister = implementation!!.registerCount - parameters.size + 1
|
||||||
|
|
||||||
// This register is being overwritten, so it is free to use.
|
val freeRegister = findFreeRegister(insertIndex, charSequenceInstanceRegister, conversionContextRegister)
|
||||||
val freeRegister = registerCount - 1
|
|
||||||
val conversionContextRegister = registerCount - parameters.size + 1
|
|
||||||
|
|
||||||
addInstructions(
|
addInstructions(
|
||||||
insertIndex,
|
insertIndex,
|
||||||
|
@ -12,12 +12,13 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
|||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
|
import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.playercontrols.*
|
import app.revanced.patches.youtube.misc.playercontrols.*
|
||||||
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
|
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
|
||||||
import app.revanced.patches.youtube.misc.settings.addSettingPreference
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
import app.revanced.patches.youtube.misc.settings.newIntent
|
|
||||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
import app.revanced.patches.youtube.shared.*
|
import app.revanced.patches.youtube.shared.*
|
||||||
import app.revanced.patches.youtube.video.information.onCreateHook
|
import app.revanced.patches.youtube.video.information.onCreateHook
|
||||||
@ -43,15 +44,32 @@ private val sponsorBlockResourcePatch = resourcePatch {
|
|||||||
execute {
|
execute {
|
||||||
addResources("youtube", "layout.sponsorblock.sponsorBlockResourcePatch")
|
addResources("youtube", "layout.sponsorblock.sponsorBlockResourcePatch")
|
||||||
|
|
||||||
addSettingPreference(
|
PreferenceScreen.SPONSORBLOCK.addPreferences(
|
||||||
IntentPreference(
|
// SB setting is old code with lots of custom preferences and updating behavior.
|
||||||
key = "revanced_settings_screen_10",
|
// Added as a preference group and not a fragment so the preferences are searchable.
|
||||||
titleKey = "revanced_sb_settings_title",
|
PreferenceCategory(
|
||||||
summaryKey = null,
|
key = "revanced_settings_screen_10_sponsorblock",
|
||||||
icon = "@drawable/revanced_settings_screen_10_sb",
|
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
||||||
layout = "@layout/preference_with_icon",
|
preferences = emptySet(), // Preferences are added by custom class at runtime.
|
||||||
intent = newIntent("revanced_sb_settings_intent"),
|
tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup"
|
||||||
),
|
),
|
||||||
|
PreferenceCategory(
|
||||||
|
key = "revanced_sb_stats",
|
||||||
|
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
||||||
|
preferences = emptySet(), // Preferences are added by custom class at runtime.
|
||||||
|
tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockStatsPreferenceCategory"
|
||||||
|
),
|
||||||
|
PreferenceCategory(
|
||||||
|
key = "revanced_sb_about",
|
||||||
|
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
||||||
|
preferences = setOf(
|
||||||
|
NonInteractivePreference(
|
||||||
|
key = "revanced_sb_about_api",
|
||||||
|
tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockAboutPreference",
|
||||||
|
selectable = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
arrayOf(
|
arrayOf(
|
||||||
|
@ -54,6 +54,7 @@ val changeStartPagePatch = bytecodePatch(
|
|||||||
ListPreference(
|
ListPreference(
|
||||||
key = "revanced_change_start_page",
|
key = "revanced_change_start_page",
|
||||||
summaryKey = null,
|
summaryKey = null,
|
||||||
|
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
|
||||||
),
|
),
|
||||||
SwitchPreference("revanced_change_start_page_always")
|
SwitchPreference("revanced_change_start_page_always")
|
||||||
)
|
)
|
||||||
|
@ -5,10 +5,6 @@ import app.revanced.util.literal
|
|||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
/**
|
|
||||||
* In 19.17 and earlier, this resolves to the same method as [readComponentIdentifierFingerprint].
|
|
||||||
* In 19.18+ this resolves to a different method.
|
|
||||||
*/
|
|
||||||
internal val componentContextParserFingerprint = fingerprint {
|
internal val componentContextParserFingerprint = fingerprint {
|
||||||
strings(
|
strings(
|
||||||
"TreeNode result must be set.",
|
"TreeNode result must be set.",
|
||||||
@ -17,11 +13,21 @@ internal val componentContextParserFingerprint = fingerprint {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves to the class found in [componentContextParserFingerprint].
|
||||||
|
* When patching 19.16 this fingerprint matches the same method as [componentContextParserFingerprint].
|
||||||
|
*/
|
||||||
|
internal val componentContextSubParserFingerprint = fingerprint {
|
||||||
|
strings(
|
||||||
|
"Number of bits must be positive"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
internal val lithoFilterFingerprint = fingerprint {
|
internal val lithoFilterFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
||||||
returns("V")
|
returns("V")
|
||||||
custom { _, classDef ->
|
custom { _, classDef ->
|
||||||
classDef.endsWith("LithoFilterPatch;")
|
classDef.endsWith("/LithoFilterPatch;")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,14 +43,6 @@ internal val protobufBufferReferenceFingerprint = fingerprint {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* In 19.17 and earlier, this resolves to the same method as [componentContextParserFingerprint].
|
|
||||||
* In 19.18+ this resolves to a different method.
|
|
||||||
*/
|
|
||||||
internal val readComponentIdentifierFingerprint = fingerprint {
|
|
||||||
strings("Number of bits must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val emptyComponentFingerprint = fingerprint {
|
internal val emptyComponentFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
|
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
|
||||||
parameters()
|
parameters()
|
||||||
|
@ -4,25 +4,25 @@ package app.revanced.patches.youtube.misc.litho.filter
|
|||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
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.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patcher.util.smali.ExternalLabel
|
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_18_or_greater
|
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
||||||
|
import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
|
||||||
|
import app.revanced.util.addInstructionsAtControlFlowLabel
|
||||||
import app.revanced.util.findFreeRegister
|
import app.revanced.util.findFreeRegister
|
||||||
|
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
||||||
import app.revanced.util.getReference
|
import app.revanced.util.getReference
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
@ -53,42 +53,33 @@ val lithoFilterPatch = bytecodePatch(
|
|||||||
* The buffer is a large byte array that represents the component tree.
|
* The buffer is a large byte array that represents the component tree.
|
||||||
* This byte array is searched for strings that indicate the current component.
|
* This byte array is searched for strings that indicate the current component.
|
||||||
*
|
*
|
||||||
* The following pseudocode shows how the patch works:
|
* All modifications done here must allow all the original code to still execute
|
||||||
|
* even when filtering, otherwise memory leaks or poor app performance may occur.
|
||||||
|
*
|
||||||
|
* The following pseudocode shows how this patch works:
|
||||||
*
|
*
|
||||||
* class SomeOtherClass {
|
* class SomeOtherClass {
|
||||||
* // Called before ComponentContextParser.parseBytesToComponentContext method.
|
* // Called before ComponentContextParser.parseComponent() method.
|
||||||
* public void someOtherMethod(ByteBuffer byteBuffer) {
|
* public void someOtherMethod(ByteBuffer byteBuffer) {
|
||||||
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
|
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
|
||||||
* ...
|
* ...
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* When patching 19.17 and earlier:
|
|
||||||
*
|
|
||||||
* class ComponentContextParser {
|
* class ComponentContextParser {
|
||||||
* public ComponentContext ReadComponentIdentifierFingerprint(...) {
|
* public Component parseComponent() {
|
||||||
* ...
|
* ...
|
||||||
* if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
|
*
|
||||||
|
* // Checks if the component should be filtered.
|
||||||
|
* // Sets a thread local with the filtering result.
|
||||||
|
* extensionClass.filter(identifier, pathBuilder); // Inserted by this patch.
|
||||||
|
*
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* if (extensionClass.shouldFilter()) { // Inserted by this patch.
|
||||||
* return emptyComponent;
|
* return emptyComponent;
|
||||||
* ...
|
* }
|
||||||
* }
|
* return originalUnpatchedComponent; // Original code.
|
||||||
* }
|
|
||||||
*
|
|
||||||
* When patching 19.18 and later:
|
|
||||||
*
|
|
||||||
* class ComponentContextParser {
|
|
||||||
* public ComponentContext parseBytesToComponentContext(...) {
|
|
||||||
* ...
|
|
||||||
* if (ReadComponentIdentifierFingerprint() == null); // Inserted by this patch.
|
|
||||||
* return emptyComponent;
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* public ComponentIdentifierObj readComponentIdentifier(...) {
|
|
||||||
* ...
|
|
||||||
* if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
|
|
||||||
* return null;
|
|
||||||
* ...
|
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
@ -103,7 +94,7 @@ val lithoFilterPatch = bytecodePatch(
|
|||||||
2,
|
2,
|
||||||
"""
|
"""
|
||||||
new-instance v1, $classDescriptor
|
new-instance v1, $classDescriptor
|
||||||
invoke-direct {v1}, $classDescriptor-><init>()V
|
invoke-direct { v1 }, $classDescriptor-><init>()V
|
||||||
const/16 v2, ${filterCount++}
|
const/16 v2, ${filterCount++}
|
||||||
aput-object v1, v0, v2
|
aput-object v1, v0, v2
|
||||||
""",
|
""",
|
||||||
@ -115,110 +106,105 @@ val lithoFilterPatch = bytecodePatch(
|
|||||||
|
|
||||||
protobufBufferReferenceFingerprint.method.addInstruction(
|
protobufBufferReferenceFingerprint.method.addInstruction(
|
||||||
0,
|
0,
|
||||||
" invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
|
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
|
||||||
)
|
)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Hook the method that parses bytes into a ComponentContext.
|
// region Hook the method that parses bytes into a ComponentContext.
|
||||||
|
|
||||||
val readComponentMethod = readComponentIdentifierFingerprint.originalMethod
|
// Allow the method to run to completion, and override the
|
||||||
// Get the only static method in the class.
|
// return value with an empty component if it should be filtered.
|
||||||
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.first { method ->
|
// It is important to allow the original code to always run to completion,
|
||||||
AccessFlags.STATIC.isSet(method.accessFlags)
|
// otherwise memory leaks and poor app performance can occur.
|
||||||
}
|
//
|
||||||
// Only one field.
|
// The extension filtering result needs to be saved off somewhere, but cannot
|
||||||
val emptyComponentField = classBy { classDef ->
|
// save to a class field since the target class is called by multiple threads.
|
||||||
builderMethodDescriptor.returnType == classDef.type
|
// It would be great if there was a way to change the register count of the
|
||||||
}!!.immutableClass.fields.single()
|
// method implementation and save the result to a high register to later use
|
||||||
|
// in the method, but there is no simple way to do that.
|
||||||
// Returns an empty component instead of the original component.
|
// Instead save the extension filter result to a thread local and check the
|
||||||
fun createReturnEmptyComponentInstructions(register: Int): String =
|
// filtering result at each method return index.
|
||||||
"""
|
// String field for the litho identifier.
|
||||||
move-object/from16 v$register, p1
|
|
||||||
invoke-static { v$register }, $builderMethodDescriptor
|
|
||||||
move-result-object v$register
|
|
||||||
iget-object v$register, v$register, $emptyComponentField
|
|
||||||
return-object v$register
|
|
||||||
"""
|
|
||||||
|
|
||||||
componentContextParserFingerprint.method.apply {
|
componentContextParserFingerprint.method.apply {
|
||||||
// 19.18 and later require patching 2 methods instead of one.
|
val conversionContextClass = conversionContextFingerprintToString.originalClassDef
|
||||||
// Otherwise the modifications done here are the same for all targets.
|
|
||||||
if (is_19_18_or_greater) {
|
val conversionContextIdentifierField = componentContextSubParserFingerprint.match(
|
||||||
// Get the method name of the ReadComponentIdentifierFingerprint call.
|
componentContextParserFingerprint.originalClassDef
|
||||||
val readComponentMethodCallIndex = indexOfFirstInstructionOrThrow {
|
).let {
|
||||||
val reference = getReference<MethodReference>()
|
// Identifier field is loaded just before the string declaration.
|
||||||
reference?.definingClass == readComponentMethod.definingClass &&
|
val index = it.method.indexOfFirstInstructionReversedOrThrow(
|
||||||
reference.name == readComponentMethod.name
|
it.stringMatches!!.first().index
|
||||||
|
) {
|
||||||
|
val reference = getReference<FieldReference>()
|
||||||
|
reference?.definingClass == conversionContextClass.type
|
||||||
|
&& reference.type == "Ljava/lang/String;"
|
||||||
}
|
}
|
||||||
|
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()
|
||||||
// Result of read component, and also a free register.
|
|
||||||
val register = getInstruction<OneRegisterInstruction>(readComponentMethodCallIndex + 1).registerA
|
|
||||||
|
|
||||||
// Insert after 'move-result-object'
|
|
||||||
val insertHookIndex = readComponentMethodCallIndex + 2
|
|
||||||
|
|
||||||
// Return an EmptyComponent instead of the original component if the filterState method returns true.
|
|
||||||
addInstructionsWithLabels(
|
|
||||||
insertHookIndex,
|
|
||||||
"""
|
|
||||||
if-nez v$register, :unfiltered
|
|
||||||
|
|
||||||
# Component was filtered in ReadComponentIdentifierFingerprint hook
|
|
||||||
${createReturnEmptyComponentInstructions(register)}
|
|
||||||
""",
|
|
||||||
ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
// StringBuilder field for the litho path.
|
||||||
|
val conversionContextPathBuilderField = conversionContextClass.fields
|
||||||
|
.single { field -> field.type == "Ljava/lang/StringBuilder;" }
|
||||||
|
|
||||||
// region Read component then store the result.
|
val conversionContextResultIndex = indexOfFirstInstructionOrThrow {
|
||||||
|
val reference = getReference<MethodReference>()
|
||||||
|
reference?.returnType == conversionContextClass.type
|
||||||
|
} + 1
|
||||||
|
|
||||||
readComponentIdentifierFingerprint.method.apply {
|
val conversionContextResultRegister = getInstruction<OneRegisterInstruction>(
|
||||||
val insertHookIndex = indexOfFirstInstructionOrThrow {
|
conversionContextResultIndex
|
||||||
opcode == Opcode.IPUT_OBJECT &&
|
|
||||||
getReference<FieldReference>()?.type == "Ljava/lang/StringBuilder;"
|
|
||||||
}
|
|
||||||
val stringBuilderRegister = getInstruction<TwoRegisterInstruction>(insertHookIndex).registerA
|
|
||||||
|
|
||||||
// Identifier is saved to a field just before the string builder.
|
|
||||||
val identifierRegister = getInstruction<TwoRegisterInstruction>(
|
|
||||||
indexOfFirstInstructionReversedOrThrow(insertHookIndex) {
|
|
||||||
opcode == Opcode.IPUT_OBJECT &&
|
|
||||||
getReference<FieldReference>()?.type == "Ljava/lang/String;"
|
|
||||||
},
|
|
||||||
).registerA
|
).registerA
|
||||||
|
|
||||||
val freeRegister = findFreeRegister(insertHookIndex, identifierRegister, stringBuilderRegister)
|
val identifierRegister = findFreeRegister(
|
||||||
val invokeFilterInstructions = """
|
conversionContextResultIndex, conversionContextResultRegister
|
||||||
invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
|
|
||||||
move-result v$freeRegister
|
|
||||||
if-eqz v$freeRegister, :unfiltered
|
|
||||||
"""
|
|
||||||
|
|
||||||
addInstructionsWithLabels(
|
|
||||||
insertHookIndex,
|
|
||||||
if (is_19_18_or_greater) {
|
|
||||||
"""
|
|
||||||
$invokeFilterInstructions
|
|
||||||
|
|
||||||
# Return null, and the ComponentContextParserFingerprint hook
|
|
||||||
# handles returning an empty component.
|
|
||||||
const/4 v$freeRegister, 0x0
|
|
||||||
return-object v$freeRegister
|
|
||||||
"""
|
|
||||||
} else {
|
|
||||||
"""
|
|
||||||
$invokeFilterInstructions
|
|
||||||
|
|
||||||
${createReturnEmptyComponentInstructions(freeRegister)}
|
|
||||||
"""
|
|
||||||
},
|
|
||||||
ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
|
|
||||||
)
|
)
|
||||||
|
val stringBuilderRegister = findFreeRegister(
|
||||||
|
conversionContextResultIndex, conversionContextResultRegister, identifierRegister
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if the component should be filtered, and save the result to a thread local.
|
||||||
|
addInstructionsAtControlFlowLabel(
|
||||||
|
conversionContextResultIndex + 1,
|
||||||
|
"""
|
||||||
|
iget-object v$identifierRegister, v$conversionContextResultRegister, $conversionContextIdentifierField
|
||||||
|
iget-object v$stringBuilderRegister, v$conversionContextResultRegister, $conversionContextPathBuilderField
|
||||||
|
invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)V
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the only static method in the class.
|
||||||
|
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
|
||||||
|
method -> AccessFlags.STATIC.isSet(method.accessFlags)
|
||||||
|
}
|
||||||
|
// Only one field.
|
||||||
|
val emptyComponentField = classBy { classDef ->
|
||||||
|
classDef.type == builderMethodDescriptor.returnType
|
||||||
|
}!!.immutableClass.fields.single()
|
||||||
|
|
||||||
|
// Check at each return value if the component is filtered,
|
||||||
|
// and return an empty component if filtering is needed.
|
||||||
|
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { returnIndex ->
|
||||||
|
val freeRegister = findFreeRegister(returnIndex)
|
||||||
|
|
||||||
|
addInstructionsAtControlFlowLabel(
|
||||||
|
returnIndex,
|
||||||
|
"""
|
||||||
|
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter()Z
|
||||||
|
move-result v$freeRegister
|
||||||
|
if-eqz v$freeRegister, :unfiltered
|
||||||
|
|
||||||
|
move-object/from16 v$freeRegister, p1
|
||||||
|
invoke-static { v$freeRegister }, $builderMethodDescriptor
|
||||||
|
move-result-object v$freeRegister
|
||||||
|
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
|
||||||
|
return-object v$freeRegister
|
||||||
|
|
||||||
|
:unfiltered
|
||||||
|
nop
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -74,6 +74,7 @@ private val settingsResourcePatch = resourcePatch {
|
|||||||
|
|
||||||
arrayOf(
|
arrayOf(
|
||||||
ResourceGroup("drawable",
|
ResourceGroup("drawable",
|
||||||
|
"revanced_settings_cursor.xml",
|
||||||
"revanced_settings_icon.xml",
|
"revanced_settings_icon.xml",
|
||||||
"revanced_settings_screen_00_about.xml",
|
"revanced_settings_screen_00_about.xml",
|
||||||
"revanced_settings_screen_01_ads.xml",
|
"revanced_settings_screen_01_ads.xml",
|
||||||
@ -84,12 +85,16 @@ private val settingsResourcePatch = resourcePatch {
|
|||||||
"revanced_settings_screen_06_shorts.xml",
|
"revanced_settings_screen_06_shorts.xml",
|
||||||
"revanced_settings_screen_07_seekbar.xml",
|
"revanced_settings_screen_07_seekbar.xml",
|
||||||
"revanced_settings_screen_08_swipe_controls.xml",
|
"revanced_settings_screen_08_swipe_controls.xml",
|
||||||
"revanced_settings_screen_09_ryd.xml",
|
"revanced_settings_screen_09_return_youtube_dislike.xml",
|
||||||
"revanced_settings_screen_10_sb.xml",
|
"revanced_settings_screen_10_sponsorblock.xml",
|
||||||
"revanced_settings_screen_11_misc.xml",
|
"revanced_settings_screen_11_misc.xml",
|
||||||
"revanced_settings_screen_12_video.xml",
|
"revanced_settings_screen_12_video.xml",
|
||||||
),
|
),
|
||||||
ResourceGroup("layout", "revanced_settings_with_toolbar.xml"),
|
ResourceGroup("layout",
|
||||||
|
"revanced_preference_with_icon_no_search_result.xml",
|
||||||
|
"revanced_search_suggestion_item.xml",
|
||||||
|
"revanced_settings_with_toolbar.xml"),
|
||||||
|
ResourceGroup("menu", "revanced_search_menu.xml")
|
||||||
).forEach { resourceGroup ->
|
).forEach { resourceGroup ->
|
||||||
copyResources("settings", resourceGroup)
|
copyResources("settings", resourceGroup)
|
||||||
}
|
}
|
||||||
@ -188,6 +193,7 @@ val settingsPatch = bytecodePatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
|
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
|
||||||
|
SwitchPreference("revanced_settings_search_history"),
|
||||||
SwitchPreference("revanced_show_menu_icons")
|
SwitchPreference("revanced_show_menu_icons")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -201,7 +207,8 @@ val settingsPatch = bytecodePatch(
|
|||||||
),
|
),
|
||||||
ListPreference(
|
ListPreference(
|
||||||
key = "revanced_language",
|
key = "revanced_language",
|
||||||
summaryKey = null
|
summaryKey = null,
|
||||||
|
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -347,10 +354,20 @@ object PreferenceScreen : BasePreferenceScreen() {
|
|||||||
layout = "@layout/preference_with_icon",
|
layout = "@layout/preference_with_icon",
|
||||||
sorting = Sorting.UNSORTED,
|
sorting = Sorting.UNSORTED,
|
||||||
)
|
)
|
||||||
|
val RETURN_YOUTUBE_DISLIKE = Screen(
|
||||||
// RYD and SB are items 9 and 10.
|
key = "revanced_settings_screen_09_return_youtube_dislike",
|
||||||
// Menus are added in their own patch because they use an Intent and not a Screen.
|
summaryKey = null,
|
||||||
|
icon = "@drawable/revanced_settings_screen_09_return_youtube_dislike",
|
||||||
|
layout = "@layout/preference_with_icon",
|
||||||
|
sorting = Sorting.UNSORTED,
|
||||||
|
)
|
||||||
|
val SPONSORBLOCK = Screen(
|
||||||
|
key = "revanced_settings_screen_10_sponsorblock",
|
||||||
|
summaryKey = null,
|
||||||
|
icon = "@drawable/revanced_settings_screen_10_sponsorblock",
|
||||||
|
layout = "@layout/preference_with_icon",
|
||||||
|
sorting = Sorting.UNSORTED,
|
||||||
|
)
|
||||||
val MISC = Screen(
|
val MISC = Screen(
|
||||||
key = "revanced_settings_screen_11_misc",
|
key = "revanced_settings_screen_11_misc",
|
||||||
summaryKey = null,
|
summaryKey = null,
|
||||||
|
@ -62,7 +62,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch({
|
|||||||
summaryKey = null,
|
summaryKey = null,
|
||||||
// Language strings are declared in Setting patch.
|
// Language strings are declared in Setting patch.
|
||||||
entriesKey = "revanced_language_entries",
|
entriesKey = "revanced_language_entries",
|
||||||
entryValuesKey = "revanced_language_entry_values"
|
entryValuesKey = "revanced_language_entry_values",
|
||||||
|
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
|
||||||
),
|
),
|
||||||
SwitchPreference("revanced_spoof_video_streams_ios_force_avc"),
|
SwitchPreference("revanced_spoof_video_streams_ios_force_avc"),
|
||||||
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
|
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
|
||||||
|
@ -4,6 +4,21 @@ import app.revanced.patcher.fingerprint
|
|||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
internal val conversionContextFingerprintToString = fingerprint {
|
||||||
|
parameters()
|
||||||
|
strings(
|
||||||
|
"ConversionContext{containerInternal=",
|
||||||
|
", widthConstraint=",
|
||||||
|
", heightConstraint=",
|
||||||
|
", templateLoggerFactory=",
|
||||||
|
", rootDisposableContainer=",
|
||||||
|
", identifierProperty="
|
||||||
|
)
|
||||||
|
custom { method, _ ->
|
||||||
|
method.name == "toString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal val autoRepeatFingerprint = fingerprint {
|
internal val autoRepeatFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||||
returns("V")
|
returns("V")
|
||||||
|
@ -38,6 +38,7 @@ internal val rememberPlaybackSpeedPatch = bytecodePatch {
|
|||||||
// Entries and values are set by the extension code based on the actual speeds available.
|
// Entries and values are set by the extension code based on the actual speeds available.
|
||||||
entriesKey = null,
|
entriesKey = null,
|
||||||
entryValuesKey = null,
|
entryValuesKey = null,
|
||||||
|
tag = "app.revanced.extension.youtube.settings.preference.CustomVideoSpeedListPreference"
|
||||||
),
|
),
|
||||||
SwitchPreference("revanced_remember_playback_speed_last_selected")
|
SwitchPreference("revanced_remember_playback_speed_last_selected")
|
||||||
)
|
)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user