Compare commits

...

16 Commits

Author SHA1 Message Date
semantic-release-bot
a38f635514 chore: Release v5.31.0-dev.17 [skip ci]
# [5.31.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.16...v5.31.0-dev.17) (2025-07-11)

### Bug Fixes

* **Spotify - Unlock Premium:** Remove wrongfully hidden non ad browse sections ([#5403](https://github.com/ReVanced/revanced-patches/issues/5403)) ([b3e6c21](b3e6c215cc))

### Features

* **Spotify:** Remove support for old versions ([#5404](https://github.com/ReVanced/revanced-patches/issues/5404)) ([c9cc3d5](c9cc3d5c41))
2025-07-11 15:41:53 +00:00
Nuckyz
b3e6c215cc fix(Spotify - Unlock Premium): Remove wrongfully hidden non ad browse sections (#5403) 2025-07-11 17:38:33 +02:00
Nuckyz
c9cc3d5c41 feat(Spotify): Remove support for old versions (#5404) 2025-07-11 17:37:59 +02:00
semantic-release-bot
536e64565c chore: Release v5.31.0-dev.16 [skip ci]
# [5.31.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.15...v5.31.0-dev.16) (2025-07-11)

### Features

* **Spotify - Spoof client:** Fix issues like songs skipping by spoofing to iOS ([#5388](https://github.com/ReVanced/revanced-patches/issues/5388)) ([65cbf3c](65cbf3c1eb))
* **YouTube:** Disable two-finger tap gesture for skipping chapters ([#5374](https://github.com/ReVanced/revanced-patches/issues/5374)) ([61c1a7a](61c1a7a75a))
2025-07-11 15:37:29 +00:00
Dawid Krajcarz
65cbf3c1eb feat(Spotify - Spoof client): Fix issues like songs skipping by spoofing to iOS (#5388)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-07-11 17:34:02 +02:00
abel1502
61c1a7a75a feat(YouTube): Disable two-finger tap gesture for skipping chapters (#5374)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-07-11 17:32:59 +02:00
Pun Butrach
1e39db06b8 ci: Remove fetch-depth from checkout (#5311) 2025-07-11 17:31:12 +02:00
Pun Butrach
e019f83232 ci: Group all Dependabot update into one PR (#5336) 2025-07-11 17:31:03 +02:00
semantic-release-bot
3b57a5f8c0 chore: Release v5.31.0-dev.15 [skip ci]
# [5.31.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.14...v5.31.0-dev.15) (2025-07-11)

### Bug Fixes

* Handle empty list of announcements ([eafe3df](eafe3dfc45))
2025-07-11 09:31:21 +00:00
oSumAtrIX
eafe3dfc45 fix: Handle empty list of announcements 2025-07-11 11:28:13 +02:00
semantic-release-bot
d56d8d990c chore: Release v5.31.0-dev.14 [skip ci]
# [5.31.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.13...v5.31.0-dev.14) (2025-07-10)

### Bug Fixes

* **Bacon Reader - Spoof client:** Use www instead of ssl API to fix auth related issues  ([#5402](https://github.com/ReVanced/revanced-patches/issues/5402)) ([37a8682](37a8682901))
2025-07-10 18:51:55 +00:00
Chirag Gada
37a8682901 fix(Bacon Reader - Spoof client): Use www instead of ssl API to fix auth related issues (#5402) 2025-07-10 20:49:04 +02:00
semantic-release-bot
11ba7d4e3e chore: Release v5.31.0-dev.13 [skip ci]
# [5.31.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.12...v5.31.0-dev.13) (2025-07-10)

### Bug Fixes

* **YouTube - Slide to seek:** Show tap and hold 2x speed overlay when active ([#5398](https://github.com/ReVanced/revanced-patches/issues/5398)) ([6833d37](6833d37c26))
2025-07-10 13:38:38 +00:00
LisoUseInAIKyrios
6833d37c26 fix(YouTube - Slide to seek): Show tap and hold 2x speed overlay when active (#5398) 2025-07-10 17:35:08 +04:00
github-actions[bot]
e6f72bcb7d chore: Sync translations (#5399) 2025-07-10 17:34:47 +04:00
LisoUseInAIKyrios
e8a227c082 chore: Fix api dump 2025-07-10 15:15:34 +04:00
52 changed files with 752 additions and 1216 deletions

View File

@@ -1,22 +1,26 @@
version: 2
multi-ecosystem-groups:
dependency:
schedule:
interval: "weekly"
target-branch: dev
labels: [ ]
updates:
- package-ecosystem: github-actions
labels: []
multi-ecosystem-group: "dependency"
directory: /
target-branch: dev
schedule:
interval: monthly
patterns:
- "*"
- package-ecosystem: npm
labels: []
multi-ecosystem-group: "dependency"
directory: /
target-branch: dev
schedule:
interval: monthly
patterns:
- "*"
- package-ecosystem: gradle
labels: []
multi-ecosystem-group: "dependency"
directory: /
target-branch: dev
schedule:
interval: monthly
patterns:
- "*"

View File

@@ -13,8 +13,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4

View File

@@ -17,7 +17,6 @@ jobs:
uses: actions/checkout@v4
with:
ref: dev
fetch-depth: 0
clean: true
- name: Pull strings

View File

@@ -15,8 +15,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Preprocess strings
env:

View File

@@ -19,8 +19,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4

View File

@@ -1,3 +1,44 @@
# [5.31.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.16...v5.31.0-dev.17) (2025-07-11)
### Bug Fixes
* **Spotify - Unlock Premium:** Remove wrongfully hidden non ad browse sections ([#5403](https://github.com/ReVanced/revanced-patches/issues/5403)) ([8633544](https://github.com/ReVanced/revanced-patches/commit/8633544decc0814d7a548fbc5576b4bdd1d7eee0))
### Features
* **Spotify:** Remove support for old versions ([#5404](https://github.com/ReVanced/revanced-patches/issues/5404)) ([9d31238](https://github.com/ReVanced/revanced-patches/commit/9d31238803a45e957472760fc40c3862da2cf3f0))
# [5.31.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.15...v5.31.0-dev.16) (2025-07-11)
### Features
* **Spotify - Spoof client:** Fix issues like songs skipping by spoofing to iOS ([#5388](https://github.com/ReVanced/revanced-patches/issues/5388)) ([e36d4c1](https://github.com/ReVanced/revanced-patches/commit/e36d4c1986b58815c7659e6ef44011166873f9c8))
* **YouTube:** Disable two-finger tap gesture for skipping chapters ([#5374](https://github.com/ReVanced/revanced-patches/issues/5374)) ([71db0a2](https://github.com/ReVanced/revanced-patches/commit/71db0a2661b5f76eb5048cdeed83f26fbfdf4fee))
# [5.31.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.14...v5.31.0-dev.15) (2025-07-11)
### Bug Fixes
* Handle empty list of announcements ([de9d720](https://github.com/ReVanced/revanced-patches/commit/de9d7209f4e818a618a7fd9000013ae8ebd728f2))
# [5.31.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.13...v5.31.0-dev.14) (2025-07-10)
### Bug Fixes
* **Bacon Reader - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5402](https://github.com/ReVanced/revanced-patches/issues/5402)) ([72459bb](https://github.com/ReVanced/revanced-patches/commit/72459bb2eaf4691e32822dfdd1db3240e2fe98dd))
# [5.31.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.12...v5.31.0-dev.13) (2025-07-10)
### Bug Fixes
* **YouTube - Slide to seek:** Show tap and hold 2x speed overlay when active ([#5398](https://github.com/ReVanced/revanced-patches/issues/5398)) ([dbc9c5f](https://github.com/ReVanced/revanced-patches/commit/dbc9c5f00c1f5bbb95f8822667cc1ac3c613fa00))
# [5.31.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.11...v5.31.0-dev.12) (2025-07-09)

View File

@@ -7,7 +7,6 @@ dependencies {
compileOnly(project(":extensions:spotify:stub"))
compileOnly(libs.annotation)
implementation(project(":extensions:spotify:utils"))
implementation(libs.nanohttpd)
implementation(libs.protobuf.javalite)
}

View File

@@ -1,9 +1,11 @@
package app.revanced.extension.spotify.layout.hide.createbutton;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.shared.ComponentFilters.*;
import app.revanced.extension.spotify.shared.ComponentFilters.ComponentFilter;
import app.revanced.extension.spotify.shared.ComponentFilters.ResourceIdComponentFilter;
import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFilter;
import java.util.List;
@SuppressWarnings("unused")
public final class HideCreateButtonPatch {
@@ -53,7 +55,9 @@ public final class HideCreateButtonPatch {
return null;
}
}
} catch (Exception ex) {
} catch (Throwable ex) {
// Catch Throwable as calling toString can cause crashes with wrongfully generated code that throws
// NoSuchMethod errors.
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
}

View File

@@ -0,0 +1,115 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import static app.revanced.extension.spotify.misc.fix.Constants.*;
class ClientTokenService {
private static final String IOS_CLIENT_ID = "58bd3c95768941ea9eb4350aaa033eb3";
private static final String IOS_USER_AGENT;
static {
String clientVersion = getClientVersion();
int commitHashIndex = clientVersion.lastIndexOf(".");
String version = clientVersion.substring(
clientVersion.indexOf("-") + 1,
clientVersion.lastIndexOf(".", commitHashIndex - 1)
);
IOS_USER_AGENT = "Spotify/" + version + " iOS/" + getSystemVersion() + " (" + getHardwareMachine() + ")";
}
private static final ConnectivitySdkData.Builder IOS_CONNECTIVITY_SDK_DATA =
ConnectivitySdkData.newBuilder()
.setPlatformSpecificData(PlatformSpecificData.newBuilder()
.setIos(NativeIOSData.newBuilder()
.setHwMachine(getHardwareMachine())
.setSystemVersion(getSystemVersion())
)
);
private static final ClientDataRequest.Builder IOS_CLIENT_DATA_REQUEST =
ClientDataRequest.newBuilder()
.setClientVersion(getClientVersion())
.setClientId(IOS_CLIENT_ID);
private static final ClientTokenRequest.Builder IOS_CLIENT_TOKEN_REQUEST =
ClientTokenRequest.newBuilder()
.setRequestType(ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST);
@NonNull
static ClientTokenRequest newIOSClientTokenRequest(String deviceId) {
Logger.printInfo(() -> "Creating new iOS client token request with device ID: " + deviceId);
return IOS_CLIENT_TOKEN_REQUEST
.setClientData(IOS_CLIENT_DATA_REQUEST
.setConnectivitySdkData(IOS_CONNECTIVITY_SDK_DATA
.setDeviceId(deviceId)
)
)
.build();
}
@Nullable
static ClientTokenResponse getClientTokenResponse(@NonNull ClientTokenRequest request) {
if (request.getRequestType() == ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST) {
Logger.printInfo(() -> "Requesting iOS client token");
String deviceId = request.getClientData().getConnectivitySdkData().getDeviceId();
request = newIOSClientTokenRequest(deviceId);
}
ClientTokenResponse response;
try {
response = requestClientToken(request);
} catch (IOException ex) {
Logger.printException(() -> "Failed to handle request", ex);
return null;
}
return response;
}
@NonNull
private static ClientTokenResponse requestClientToken(@NonNull ClientTokenRequest request) throws IOException {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(CLIENT_TOKEN_API_URL).openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
urlConnection.setRequestProperty("Content-Type", "application/x-protobuf");
urlConnection.setRequestProperty("Accept", "application/x-protobuf");
urlConnection.setRequestProperty("User-Agent", IOS_USER_AGENT);
byte[] requestArray = request.toByteArray();
urlConnection.setFixedLengthStreamingMode(requestArray.length);
urlConnection.getOutputStream().write(requestArray);
try (InputStream inputStream = urlConnection.getInputStream()) {
return ClientTokenResponse.parseFrom(inputStream);
}
}
@Nullable
static ClientTokenResponse serveClientTokenRequest(@NonNull InputStream inputStream) {
ClientTokenRequest request;
try {
request = ClientTokenRequest.parseFrom(inputStream);
} catch (IOException ex) {
Logger.printException(() -> "Failed to parse request from input stream", ex);
return null;
}
Logger.printInfo(() -> "Request of type: " + request.getRequestType());
ClientTokenResponse response = getClientTokenResponse(request);
if (response != null) Logger.printInfo(() -> "Response of type: " + response.getResponseType());
return response;
}
}

View File

@@ -0,0 +1,26 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
class Constants {
static final String CLIENT_TOKEN_API_PATH = "/v1/clienttoken";
static final String CLIENT_TOKEN_API_URL = "https://clienttoken.spotify.com" + CLIENT_TOKEN_API_PATH;
// Modified by a patch. Do not touch.
@NonNull
static String getClientVersion() {
return "";
}
// Modified by a patch. Do not touch.
@NonNull
static String getSystemVersion() {
return "";
}
// Modified by a patch. Do not touch.
@NonNull
static String getHardwareMachine() {
return "";
}
}

View File

@@ -1,158 +0,0 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.login5.v4.proto.Login5.*;
import com.google.protobuf.ByteString;
import com.google.protobuf.MessageLite;
import fi.iki.elonen.NanoHTTPD;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import static app.revanced.extension.spotify.misc.fix.Session.FAILED_TO_RENEW_SESSION;
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
class LoginRequestListener extends NanoHTTPD {
LoginRequestListener(int port) {
super(port);
try {
start();
} catch (IOException ex) {
Logger.printException(() -> "Failed to start login request listener on port " + port, ex);
throw new RuntimeException(ex);
}
}
@NonNull
@Override
public Response serve(IHTTPSession request) {
Logger.printInfo(() -> "Serving request for URI: " + request.getUri());
InputStream requestBodyInputStream = getRequestBodyInputStream(request);
LoginRequest loginRequest;
try {
loginRequest = LoginRequest.parseFrom(requestBodyInputStream);
} catch (IOException ex) {
Logger.printException(() -> "Failed to parse LoginRequest", ex);
return newResponse(INTERNAL_ERROR);
}
MessageLite loginResponse;
// A request may be made concurrently by Spotify,
// however a webview can only handle one request at a time due to singleton cookie manager.
// Therefore, synchronize to ensure that only one webview handles the request at a time.
synchronized (this) {
try {
loginResponse = getLoginResponse(loginRequest);
} catch (Exception ex) {
Logger.printException(() -> "Failed to get login response", ex);
return newResponse(INTERNAL_ERROR);
}
}
return newResponse(Response.Status.OK, loginResponse);
}
private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) {
Session session;
if (!loginRequest.hasStoredCredential()) {
Logger.printInfo(() -> "Received request for initial login");
session = WebApp.currentSession; // Session obtained from WebApp.launchLogin, can be null if still in progress.
} else {
Logger.printInfo(() -> "Received request to restore saved session");
session = Session.read(loginRequest.getStoredCredential().getUsername());
}
return toLoginResponse(session);
}
private static LoginResponse toLoginResponse(@Nullable Session session) {
LoginResponse.Builder builder = LoginResponse.newBuilder();
if (session == null) {
Logger.printException(() -> "Session is null. An initial login may still be in progress, returning try again later error");
builder.setError(LoginError.TRY_AGAIN_LATER);
} else if (session.accessTokenExpired()) {
Logger.printInfo(() -> "Access token expired, renewing session");
WebApp.renewSessionBlocking(session.cookies);
return toLoginResponse(WebApp.currentSession);
} else if (session.username == null) {
Logger.printException(() -> "Session username is null, likely caused by invalid cookies, returning invalid credentials error");
session.delete();
builder.setError(LoginError.INVALID_CREDENTIALS);
} else if (session == FAILED_TO_RENEW_SESSION) {
Logger.printException(() -> "Failed to renew session, likely caused by a timeout, returning try again later error");
builder.setError(LoginError.TRY_AGAIN_LATER);
} else {
session.save();
Logger.printInfo(() -> "Returning session for username: " + session.username);
builder.setOk(LoginOk.newBuilder()
.setUsername(session.username)
.setAccessToken(session.accessToken)
.setStoredCredential(ByteString.fromHex("00")) // Placeholder, as it cannot be null or empty.
.setAccessTokenExpiresIn(session.accessTokenExpiresInSeconds())
.build());
}
return builder.build();
}
@NonNull
private static InputStream limitedInputStream(InputStream inputStream, long contentLength) {
return new FilterInputStream(inputStream) {
private long remaining = contentLength;
@Override
public int read() throws IOException {
if (remaining <= 0) return -1;
int result = super.read();
if (result != -1) remaining--;
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (remaining <= 0) return -1;
len = (int) Math.min(len, remaining);
int result = super.read(b, off, len);
if (result != -1) remaining -= result;
return result;
}
};
}
@NonNull
private static InputStream getRequestBodyInputStream(@NonNull IHTTPSession request) {
long requestContentLength =
Long.parseLong(Objects.requireNonNull(request.getHeaders().get("content-length")));
return limitedInputStream(request.getInputStream(), requestContentLength);
}
@SuppressWarnings("SameParameterValue")
@NonNull
private static Response newResponse(Response.Status status) {
return newResponse(status, null);
}
@NonNull
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
if (messageLite == null) {
return newFixedLengthResponse(status, "application/x-protobuf", null);
}
byte[] messageBytes = messageLite.toByteArray();
InputStream stream = new ByteArrayInputStream(messageBytes);
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
}
}

View File

@@ -0,0 +1,94 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.ClientTokenResponse;
import com.google.protobuf.MessageLite;
import fi.iki.elonen.NanoHTTPD;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import static app.revanced.extension.spotify.misc.fix.ClientTokenService.serveClientTokenRequest;
import static app.revanced.extension.spotify.misc.fix.Constants.CLIENT_TOKEN_API_PATH;
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
class RequestListener extends NanoHTTPD {
RequestListener(int port) {
super(port);
try {
start();
} catch (IOException ex) {
Logger.printException(() -> "Failed to start request listener on port " + port, ex);
throw new RuntimeException(ex);
}
}
@NonNull
@Override
public Response serve(@NonNull IHTTPSession session) {
String uri = session.getUri();
if (!uri.equals(CLIENT_TOKEN_API_PATH)) return INTERNAL_ERROR_RESPONSE;
Logger.printInfo(() -> "Serving request for URI: " + uri);
ClientTokenResponse response = serveClientTokenRequest(getInputStream(session));
if (response != null) return newResponse(Response.Status.OK, response);
Logger.printException(() -> "Failed to serve client token request");
return INTERNAL_ERROR_RESPONSE;
}
@NonNull
private static InputStream newLimitedInputStream(InputStream inputStream, long contentLength) {
return new FilterInputStream(inputStream) {
private long remaining = contentLength;
@Override
public int read() throws IOException {
if (remaining <= 0) return -1;
int result = super.read();
if (result != -1) remaining--;
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (remaining <= 0) return -1;
len = (int) Math.min(len, remaining);
int result = super.read(b, off, len);
if (result != -1) remaining -= result;
return result;
}
};
}
@NonNull
private static InputStream getInputStream(@NonNull IHTTPSession session) {
long requestContentLength = Long.parseLong(Objects.requireNonNull(session.getHeaders().get("content-length")));
return newLimitedInputStream(session.getInputStream(), requestContentLength);
}
private static final Response INTERNAL_ERROR_RESPONSE = newResponse(INTERNAL_ERROR);
@SuppressWarnings("SameParameterValue")
@NonNull
private static Response newResponse(Response.Status status) {
return newResponse(status, null);
}
@NonNull
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
if (messageLite == null) {
return newFixedLengthResponse(status, "application/x-protobuf", null);
}
byte[] messageBytes = messageLite.toByteArray();
InputStream stream = new ByteArrayInputStream(messageBytes);
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
}
}

View File

@@ -1,136 +0,0 @@
package app.revanced.extension.spotify.misc.fix;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import org.json.JSONException;
import org.json.JSONObject;
import static android.content.Context.MODE_PRIVATE;
class Session {
/**
* Username of the account. Null if this session does not have an authenticated user.
*/
@Nullable
final String username;
/**
* Access token for this session.
*/
final String accessToken;
/**
* Session expiration timestamp in milliseconds.
*/
final Long expirationTime;
/**
* Authentication cookies for this session.
*/
final String cookies;
/**
* Session that represents a failed attempt to renew the session.
*/
static final Session FAILED_TO_RENEW_SESSION = new Session("", "", "");
/**
* @param username Username of the account. Empty if this session does not have an authenticated user.
* @param accessToken Access token for this session.
* @param cookies Authentication cookies for this session.
*/
Session(@Nullable String username, String accessToken, String cookies) {
this(username, accessToken, System.currentTimeMillis() + 60 * 60 * 1000, cookies);
}
private Session(@Nullable String username, String accessToken, long expirationTime, String cookies) {
this.username = username;
this.accessToken = accessToken;
this.expirationTime = expirationTime;
this.cookies = cookies;
}
/**
* @return The number of milliseconds until the access token expires.
*/
long accessTokenExpiresInMillis() {
long currentTime = System.currentTimeMillis();
return expirationTime - currentTime;
}
/**
* @return The number of seconds until the access token expires.
*/
int accessTokenExpiresInSeconds() {
return (int) accessTokenExpiresInMillis() / 1000;
}
/**
* @return True if the access token has expired, false otherwise.
*/
boolean accessTokenExpired() {
return accessTokenExpiresInMillis() <= 0;
}
void save() {
Logger.printInfo(() -> "Saving session: " + this);
SharedPreferences.Editor editor = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE).edit();
String json;
try {
json = new JSONObject()
.put("accessToken", accessToken)
.put("expirationTime", expirationTime)
.put("cookies", cookies).toString();
} catch (JSONException ex) {
Logger.printException(() -> "Failed to convert session to stored credential", ex);
return;
}
editor.putString("session_" + username, json);
editor.apply();
}
void delete() {
Logger.printInfo(() -> "Deleting saved session for username: " + username);
SharedPreferences.Editor editor = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE).edit();
editor.remove("session_" + username);
editor.apply();
}
@Nullable
static Session read(String username) {
Logger.printInfo(() -> "Reading saved session for username: " + username);
SharedPreferences sharedPreferences = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE);
String savedJson = sharedPreferences.getString("session_" + username, null);
if (savedJson == null) {
Logger.printInfo(() -> "No session found in shared preferences");
return null;
}
try {
JSONObject json = new JSONObject(savedJson);
String accessToken = json.getString("accessToken");
long expirationTime = json.getLong("expirationTime");
String cookies = json.getString("cookies");
return new Session(username, accessToken, expirationTime, cookies);
} catch (JSONException ex) {
Logger.printException(() -> "Failed to read session from shared preferences", ex);
return null;
}
}
@NonNull
@Override
public String toString() {
return "Session(" +
"username=" + username +
", accessToken=" + accessToken +
", expirationTime=" + expirationTime +
", cookies=" + cookies +
')';
}
}

View File

@@ -1,19 +1,15 @@
package app.revanced.extension.spotify.misc.fix;
import android.view.LayoutInflater;
import android.view.View;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class SpoofClientPatch {
private static LoginRequestListener listener;
private static RequestListener listener;
/**
* Injection point.
* <br>
* Launch login server.
* Injection point. Launch requests listener server.
*/
public static void launchListener(int port) {
public synchronized static void launchListener(int port) {
if (listener != null) {
Logger.printInfo(() -> "Listener already running on port " + port);
return;
@@ -21,34 +17,9 @@ public class SpoofClientPatch {
try {
Logger.printInfo(() -> "Launching listener on port " + port);
listener = new LoginRequestListener(port);
listener = new RequestListener(port);
} catch (Exception ex) {
Logger.printException(() -> "launchListener failure", ex);
}
}
/**
* Injection point.
* <br>
* Launch login web view.
*/
public static void launchLogin(LayoutInflater inflater) {
try {
WebApp.launchLogin(inflater.getContext());
} catch (Exception ex) {
Logger.printException(() -> "launchLogin failure", ex);
}
}
/**
* Injection point.
* <br>
* Set handler to call the native login after the webview login.
*/
public static void setNativeLoginHandler(View startLoginButton) {
WebApp.nativeLoginHandler = (() -> {
startLoginButton.setSoundEffectsEnabled(false);
startLoginButton.performClick();
});
}
}

View File

@@ -1,297 +0,0 @@
package app.revanced.extension.spotify.misc.fix;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.view.Window;
import android.view.WindowInsets;
import android.webkit.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.spotify.UserAgent;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static app.revanced.extension.spotify.misc.fix.Session.FAILED_TO_RENEW_SESSION;
class WebApp {
private static final String OPEN_SPOTIFY_COM = "open.spotify.com";
private static final String OPEN_SPOTIFY_COM_URL = "https://" + OPEN_SPOTIFY_COM;
private static final String OPEN_SPOTIFY_COM_PREFERENCES_URL = OPEN_SPOTIFY_COM_URL + "/preferences";
private static final String ACCOUNTS_SPOTIFY_COM_LOGIN_URL = "https://accounts.spotify.com/login?allow_password=1"
+ "&continue=https%3A%2F%2Fopen.spotify.com%2Fpreferences";
private static final int GET_SESSION_TIMEOUT_SECONDS = 10;
private static final String JAVASCRIPT_INTERFACE_NAME = "androidInterface";
private static final String USER_AGENT = getWebUserAgent();
/**
* A session obtained from the webview after logging in.
*/
@Nullable
static volatile Session currentSession = null;
/**
* Current webview in use. Any use of the object must be done on the main thread.
*/
@SuppressLint("StaticFieldLeak")
private static volatile WebView currentWebView;
interface NativeLoginHandler {
void login();
}
static NativeLoginHandler nativeLoginHandler;
static void launchLogin(Context context) {
final Dialog dialog = newDialog(context);
Utils.runOnBackgroundThread(() -> {
Logger.printInfo(() -> "Launching login");
// A session must be obtained from a login. Repeat until a session is acquired.
boolean isAcquired = false;
do {
CountDownLatch onLoggedInLatch = new CountDownLatch(1);
CountDownLatch getSessionLatch = new CountDownLatch(1);
// Can't use Utils.getContext() here, because autofill won't work.
// See https://stackoverflow.com/a/79182053/11213244.
launchWebView(context, ACCOUNTS_SPOTIFY_COM_LOGIN_URL, new WebViewCallback() {
@Override
void onInitialized(WebView webView) {
super.onInitialized(webView);
dialog.setContentView(webView);
dialog.show();
}
@Override
void onLoggedIn(String cookies) {
onLoggedInLatch.countDown();
}
@Override
void onReceivedSession(Session session) {
super.onReceivedSession(session);
getSessionLatch.countDown();
dialog.dismiss();
try {
nativeLoginHandler.login();
} catch (Exception ex) {
Logger.printException(() -> "nativeLoginHandler failure", ex);
}
}
});
try {
// Wait indefinitely until the user logs in.
onLoggedInLatch.await();
// Wait until the session is received, or timeout.
isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Logger.printException(() -> "Login interrupted", ex);
Thread.currentThread().interrupt();
}
} while (!isAcquired);
});
}
static void renewSessionBlocking(String cookies) {
Logger.printInfo(() -> "Renewing session with cookies: " + cookies);
CountDownLatch getSessionLatch = new CountDownLatch(1);
launchWebView(Utils.getContext(), OPEN_SPOTIFY_COM_PREFERENCES_URL, new WebViewCallback() {
@Override
public void onInitialized(WebView webView) {
setCookies(cookies);
super.onInitialized(webView);
}
public void onReceivedSession(Session session) {
super.onReceivedSession(session);
getSessionLatch.countDown();
}
});
boolean isAcquired = false;
try {
isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Logger.printException(() -> "Session renewal interrupted", ex);
Thread.currentThread().interrupt();
}
if (!isAcquired) {
Logger.printException(() -> "Failed to retrieve session within " + GET_SESSION_TIMEOUT_SECONDS + " seconds");
currentSession = FAILED_TO_RENEW_SESSION;
destructWebView();
}
}
/**
* All methods are called on the main thread.
*/
abstract static class WebViewCallback {
void onInitialized(WebView webView) {
currentWebView = webView;
currentSession = null; // Reset current session.
}
void onLoggedIn(String cookies) {
}
void onReceivedSession(Session session) {
Logger.printInfo(() -> "Received session: " + session);
currentSession = session;
destructWebView();
}
}
@SuppressLint("SetJavaScriptEnabled")
private static void launchWebView(
Context context,
String initialUrl,
WebViewCallback webViewCallback
) {
Utils.runOnMainThreadNowOrLater(() -> {
WebView webView = new WebView(context);
WebSettings settings = webView.getSettings();
settings.setDomStorageEnabled(true);
settings.setJavaScriptEnabled(true);
settings.setUserAgentString(USER_AGENT);
// WebViewClient is always called off the main thread,
// but callback interface methods are called on the main thread.
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (OPEN_SPOTIFY_COM.equals(request.getUrl().getHost())) {
Utils.runOnMainThread(() -> webViewCallback.onLoggedIn(getCurrentCookies()));
}
return super.shouldInterceptRequest(view, request);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Logger.printInfo(() -> "Page started loading: " + url);
if (!url.startsWith(OPEN_SPOTIFY_COM_URL)) {
return;
}
Logger.printInfo(() -> "Evaluating script to get session on url: " + url);
String getSessionScript = "Object.defineProperty(Object.prototype, \"_username\", {" +
" configurable: true," +
" set(username) {" +
" accessToken = this._builder?.accessToken;" +
" if (accessToken) {" +
" " + JAVASCRIPT_INTERFACE_NAME + ".getSession(username, accessToken);" +
" delete Object.prototype._username;" +
" }" +
" " +
" Object.defineProperty(this, \"_username\", {" +
" configurable: true," +
" enumerable: true," +
" writable: true," +
" value: username" +
" })" +
" " +
" }" +
"});" +
"if (new URLSearchParams(window.location.search).get('_authfailed') != null) {" +
" " + JAVASCRIPT_INTERFACE_NAME + ".getSession(null, null);" +
"}";
view.evaluateJavascript(getSessionScript, null);
}
});
webView.addJavascriptInterface(new Object() {
@SuppressWarnings("unused")
@JavascriptInterface
public void getSession(String username, String accessToken) {
Session session = new Session(username, accessToken, getCurrentCookies());
Utils.runOnMainThread(() -> webViewCallback.onReceivedSession(session));
}
}, JAVASCRIPT_INTERFACE_NAME);
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
Logger.printInfo(() -> "Loading URL: " + initialUrl);
webView.loadUrl(initialUrl);
Logger.printInfo(() -> "WebView initialized with user agent: " + USER_AGENT);
webViewCallback.onInitialized(webView);
});
});
}
private static void destructWebView() {
Utils.runOnMainThreadNowOrLater(() -> {
currentWebView.stopLoading();
currentWebView.destroy();
currentWebView = null;
});
}
private static String getWebUserAgent() {
String userAgentString = WebSettings.getDefaultUserAgent(Utils.getContext());
try {
return new UserAgent(userAgentString)
.withCommentReplaced("Android", "Windows NT 10.0; Win64; x64")
.withoutProduct("Mobile")
.toString();
} catch (IllegalArgumentException ex) {
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edge/137.0.0.0";
String fallback = userAgentString;
Logger.printException(() -> "Failed to get user agent, falling back to " + fallback, ex);
}
return userAgentString;
}
@NonNull
private static Dialog newDialog(Context context) {
Dialog dialog = new Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
dialog.setCancelable(false);
// Ensure that the keyboard does not cover the webview content.
Window window = dialog.getWindow();
//noinspection StatementWithEmptyBody
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> {
v.setPadding(0, 0, 0, insets.getInsets(WindowInsets.Type.ime()).bottom);
return WindowInsets.CONSUMED;
});
} else {
// TODO: Implement for lower Android versions.
}
return dialog;
}
private static String getCurrentCookies() {
CookieManager cookieManager = CookieManager.getInstance();
return cookieManager.getCookie(OPEN_SPOTIFY_COM_URL);
}
private static void setCookies(@NonNull String cookies) {
CookieManager cookieManager = CookieManager.getInstance();
String[] cookiesList = cookies.split(";");
for (String cookie : cookiesList) {
cookieManager.setCookie(OPEN_SPOTIFY_COM_URL, cookie);
}
}
}

View File

@@ -0,0 +1,73 @@
syntax = "proto3";
package spotify.clienttoken.data.v0;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.spotify.misc.fix.clienttoken.data.v0";
message ClientTokenRequest {
ClientTokenRequestType request_type = 1;
oneof request {
ClientDataRequest client_data = 2;
}
}
enum ClientTokenRequestType {
REQUEST_UNKNOWN = 0;
REQUEST_CLIENT_DATA_REQUEST = 1;
REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
}
message ClientDataRequest {
string client_version = 1;
string client_id = 2;
oneof data {
ConnectivitySdkData connectivity_sdk_data = 3;
}
}
message ConnectivitySdkData {
PlatformSpecificData platform_specific_data = 1;
string device_id = 2;
}
message PlatformSpecificData {
oneof data {
NativeIOSData ios = 2;
}
}
message NativeIOSData {
int32 user_interface_idiom = 1;
bool target_iphone_simulator = 2;
string hw_machine = 3;
string system_version = 4;
string simulator_model_identifier = 5;
}
message ClientTokenResponse {
ClientTokenResponseType response_type = 1;
oneof response {
GrantedTokenResponse granted_token = 2;
}
}
enum ClientTokenResponseType {
RESPONSE_UNKNOWN = 0;
RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
RESPONSE_CHALLENGES_RESPONSE = 2;
}
message GrantedTokenResponse {
string token = 1;
int32 expires_after_seconds = 2;
int32 refresh_after_seconds = 3;
repeated TokenDomain domains = 4;
}
message TokenDomain {
string domain = 1;
}

View File

@@ -1,43 +0,0 @@
syntax = "proto3";
package spotify.login5.v4;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.spotify.login5.v4.proto";
message StoredCredential {
string username = 1;
bytes data = 2;
}
message LoginRequest {
oneof login_method {
StoredCredential stored_credential = 100;
}
}
message LoginOk {
string username = 1;
string access_token = 2;
bytes stored_credential = 3;
int32 access_token_expires_in = 4;
}
message LoginResponse {
oneof response {
LoginOk ok = 1;
LoginError error = 2;
}
}
enum LoginError {
UNKNOWN_ERROR = 0;
INVALID_CREDENTIALS = 1;
BAD_REQUEST = 2;
UNSUPPORTED_LOGIN_PROTOCOL = 3;
TIMEOUT = 4;
UNKNOWN_IDENTIFIER = 5;
TOO_MANY_ATTEMPTS = 6;
INVALID_PHONENUMBER = 7;
TRY_AGAIN_LATER = 8;
}

View File

@@ -2,7 +2,5 @@ package com.spotify.browsita.v1.resolved;
public final class Section {
public static final int BRAND_ADS_FIELD_NUMBER = 6;
public static final int PROMOTION_V1_FIELD_NUMBER = 3;
public static final int PROMOTION_V3_FIELD_NUMBER = 5;
public int sectionTypeCase_;
}

View File

@@ -1,8 +0,0 @@
package com.spotify.useraccount.v1;
/**
* Used for target 8.6.98.900. Class is still present in newer app targets.
*/
public class AccountAttribute {
public Object value_;
}

View File

@@ -1,19 +0,0 @@
plugins {
java
antlr
}
dependencies {
antlr(libs.antlr4)
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks {
generateGrammarSource {
arguments = listOf("-visitor")
}
}

View File

@@ -1,35 +0,0 @@
grammar UserAgent;
@header { package app.revanced.extension.spotify; }
userAgent
: product (WS product)* EOF
;
product
: name ('/' version)? (WS comment)?
;
name
: STRING
;
version
: STRING ('.' STRING)*
;
comment
: COMMENT
;
COMMENT
: '(' ~ ')'* ')'
;
STRING
: [a-zA-Z0-9]+
;
WS
: [ \r\n]+
;

View File

@@ -1,60 +0,0 @@
package app.revanced.extension.spotify;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.TokenStreamRewriter;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
public class UserAgent {
private final UserAgentParser.UserAgentContext tree;
private final TokenStreamRewriter rewriter;
private final ParseTreeWalker walker;
public UserAgent(String userAgentString) {
CharStream input = CharStreams.fromString(userAgentString);
UserAgentLexer lexer = new UserAgentLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
tree = new UserAgentParser(tokens).userAgent();
walker = new ParseTreeWalker();
rewriter = new TokenStreamRewriter(tokens);
}
public UserAgent withoutProduct(String name) {
walker.walk(new UserAgentBaseListener() {
@Override
public void exitProduct(UserAgentParser.ProductContext ctx) {
if (!ctx.name().getText().contains(name)) return;
int startIndex = ctx.getStart().getTokenIndex();
if (startIndex != 0) startIndex -= 1; // Also remove the preceding whitespace.
int stopIndex = ctx.getStop().getTokenIndex();
rewriter.delete(startIndex, stopIndex);
}
}, tree);
return new UserAgent(rewriter.getText().trim());
}
public UserAgent withCommentReplaced(String containing, String replacement) {
walker.walk(new UserAgentBaseListener() {
@Override
public void exitComment(UserAgentParser.CommentContext ctx) {
if (ctx.getText().contains(containing)) {
rewriter.replace(ctx.getStart(), ctx.getStop(), "(" + replacement + ")");
}
}
}, tree);
return new UserAgent(rewriter.getText());
}
@Override
public String toString() {
return rewriter.getText();
}
}

View File

@@ -0,0 +1,16 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class DisableChapterSkipDoubleTapPatch {
/**
* Injection point.
*
* @return If "should skip to chapter start" flag is set.
*/
public static boolean disableDoubleTapChapters(boolean original) {
return original && !Settings.DISABLE_CHAPTER_SKIP_DOUBLE_TAP.get();
}
}

View File

@@ -59,10 +59,11 @@ public final class AnnouncementsPatch {
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
try {
final var announcementIds = new JSONArray(jsonString);
if (announcementIds.length() == 0) return true;
id = announcementIds.getJSONObject(0).getInt("id");
} catch (Throwable ex) {
Logger.printException(() -> "Failed to parse announcement IDs", ex);
Logger.printException(() -> "Failed to parse announcement ID", ex);
}
// Do not show the announcement, if the last announcement id is the same as the current one.

View File

@@ -10,8 +10,8 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
public class AnnouncementsRoutes {
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4";
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=youtube");
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=youtube");
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=\uD83C\uDF9E\uFE0F YouTube");
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=\uD83C\uDF9E\uFE0F YouTube");
private AnnouncementsRoutes() {
}

View File

@@ -146,6 +146,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE);
public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE);
public static final BooleanSetting DISABLE_AUTO_CAPTIONS = new BooleanSetting("revanced_disable_auto_captions", FALSE, true);
public static final BooleanSetting DISABLE_CHAPTER_SKIP_DOUBLE_TAP = new BooleanSetting("revanced_disable_chapter_skip_double_tap", FALSE);
public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true);
public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE);
public static final EnumSetting<FullscreenMode> EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED);

View File

@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
version = 5.31.0-dev.12
version = 5.31.0-dev.17

View File

@@ -904,6 +904,10 @@ public final class app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPa
public static final fun userAgentClientSpoofPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/shared/misc/string/ReplaceStringPatchKt {
public static final fun replaceStringPatch (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatchKt {
public static final fun getRemoveFileSizeLimitPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1232,6 +1236,10 @@ public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerD
public static final fun getRemoveViewerDiscretionDialogPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatchKt {
public static final fun getDisableChapterSkipDoubleTapPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/interaction/downloads/DownloadsPatchKt {
public static final fun getDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

View File

@@ -4,9 +4,16 @@ import app.revanced.patcher.Fingerprint
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.reddit.customclients.spoofClientPatch
import app.revanced.patches.shared.misc.string.replaceStringPatch
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
val spoofClientPatch = spoofClientPatch(redirectUri = "http://baconreader.com/auth") { clientIdOption ->
dependsOn(
// Redirects from SSL to WWW domain are bugged causing auth problems.
// Manually rewrite the URLs to fix this.
replaceStringPatch("ssl.reddit.com", "www.reddit.com")
)
compatibleWith(
"com.onelouder.baconreader",
"com.onelouder.baconreader.premium",

View File

@@ -6,12 +6,10 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
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
import java.util.logging.Logger
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch;"
@@ -26,13 +24,6 @@ val hideCreateButtonPatch = bytecodePatch(
dependsOn(sharedExtensionPatch)
execute {
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
Logger.getLogger(this::class.java.name).warning(
"Create button does not exist in legacy app target. No changes applied."
)
return@execute
}
val oldNavigationBarAddItemMethod = oldNavigationBarAddItemFingerprint.originalMethodOrNull
// Only throw the fingerprint error when oldNavigationBarAddItemMethod does not exist.
val navigationBarItemSetClassDef = if (oldNavigationBarAddItemMethod == null) {

View File

@@ -7,8 +7,8 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.*
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import org.w3c.dom.Element
@@ -19,12 +19,6 @@ private val customThemeBytecodePatch = bytecodePatch {
dependsOn(sharedExtensionPatch)
execute {
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
// Bytecode changes are not needed for legacy app target.
// Player background color is changed with existing resource patch.
return@execute
}
val colorSpaceUtilsClassDef = colorSpaceUtilsClassFingerprint.originalClassDef
// Hook a util method that converts ARGB to RGBA in the sRGB color space to replace hardcoded accent colors.

View File

@@ -1,23 +0,0 @@
package app.revanced.patches.spotify.lite.ondemand
import com.android.tools.smali.dexlib2.Opcode
import app.revanced.patcher.fingerprint
internal val onDemandFingerprint = fingerprint(fuzzyPatternScanThreshold = 2) {
returns("L")
parameters()
opcodes(
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT,
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT_OBJECT,
Opcode.IF_EQZ,
Opcode.SGET_OBJECT,
Opcode.GOTO,
Opcode.SGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT,
Opcode.IPUT,
Opcode.RETURN_OBJECT,
)
}

View File

@@ -1,21 +1,9 @@
package app.revanced.patches.spotify.lite.ondemand
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.patch.bytecodePatch
@Deprecated("Patch no longer works and will be deleted soon")
@Suppress("unused")
val onDemandPatch = bytecodePatch(
description = "Enables listening to songs on-demand, allowing to play any song from playlists, albums or artists without limitations. This does not remove ads.",
) {
compatibleWith("com.spotify.lite")
execute {
// Spoof a premium account
onDemandFingerprint.method.addInstruction(
onDemandFingerprint.patternMatch!!.endIndex - 1,
"const/4 v0, 0x2",
)
}
}
)

View File

@@ -2,7 +2,6 @@ package app.revanced.patches.spotify.misc
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
@@ -13,25 +12,13 @@ import com.android.tools.smali.dexlib2.iface.reference.TypeReference
context(BytecodePatchContext)
internal val accountAttributeFingerprint get() = fingerprint {
custom { _, classDef ->
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
"Lcom/spotify/useraccount/v1/AccountAttribute;"
} else {
"Lcom/spotify/remoteconfig/internal/AccountAttribute;"
}
}
custom { _, classDef -> classDef.type == "Lcom/spotify/remoteconfig/internal/AccountAttribute;" }
}
context(BytecodePatchContext)
internal val productStateProtoGetMapFingerprint get() = fingerprint {
returns("Ljava/util/Map;")
custom { _, classDef ->
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
"Lcom/spotify/ucs/proto/v0/UcsResponseWrapper${'$'}AccountAttributesResponse;"
} else {
"Lcom/spotify/remoteconfig/internal/ProductStateProto;"
}
}
custom { _, classDef -> classDef.type == "Lcom/spotify/remoteconfig/internal/ProductStateProto;" }
}
internal val buildQueryParametersFingerprint = fingerprint {
@@ -90,14 +77,14 @@ internal val contextFromJsonFingerprint = fingerprint {
)
custom { method, classDef ->
method.name == "fromJson" &&
classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
classDef.type.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
}
}
internal val readPlayerOptionOverridesFingerprint = fingerprint {
custom { method, classDef ->
method.name == "readPlayerOptionOverrides" &&
classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
classDef.type.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
}
}
@@ -119,21 +106,21 @@ internal val abstractProtobufListEnsureIsMutableFingerprint = fingerprint {
internal fun structureGetSectionsFingerprint(className: String) = fingerprint {
custom { method, classDef ->
classDef.endsWith(className) && method.indexOfFirstInstruction {
classDef.type.endsWith(className) && method.indexOfFirstInstruction {
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
} >= 0
}
}
internal val homeSectionFingerprint = fingerprint {
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
custom { _, classDef -> classDef.type.endsWith("homeapi/proto/Section;") }
}
internal val homeStructureGetSectionsFingerprint =
structureGetSectionsFingerprint("homeapi/proto/HomeStructure;")
internal val browseSectionFingerprint = fingerprint {
custom { _, classDef-> classDef.endsWith("browsita/v1/resolved/Section;") }
custom { _, classDef-> classDef.type.endsWith("browsita/v1/resolved/Section;") }
}
internal val browseStructureGetSectionsFingerprint =

View File

@@ -7,37 +7,12 @@ import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val getPackageInfoFingerprint = fingerprint {
strings(
"Failed to get the application signatures"
)
}
internal val loadOrbitLibraryFingerprint = fingerprint {
strings("/liborbit-jni-spotify.so")
}
internal val startupPageLayoutInflateFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Landroid/view/View;")
parameters("Landroid/view/LayoutInflater;", "Landroid/view/ViewGroup;", "Landroid/os/Bundle;")
strings("blueprintContainer", "gradient", "valuePropositionTextView")
}
internal val renderStartLoginScreenFingerprint = fingerprint {
strings("authenticationButtonFactory", "MORE_OPTIONS")
}
internal val renderSecondLoginScreenFingerprint = fingerprint {
strings("authenticationButtonFactory", "intent_login")
}
internal val renderThirdLoginScreenFingerprint = fingerprint {
strings("EMAIL_OR_USERNAME", "listener")
}
internal val thirdLoginScreenLoginOnClickFingerprint = fingerprint {
strings("login", "listener", "none")
internal val extensionFixConstantsFingerprint = fingerprint {
custom { _, classDef -> classDef.type == "Lapp/revanced/extension/spotify/misc/fix/Constants;" }
}
internal val runIntegrityVerificationFingerprint = fingerprint {

View File

@@ -1,19 +1,13 @@
package app.revanced.patches.spotify.misc.fix
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.intOption
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.shared.misc.hex.HexPatchBuilder
import app.revanced.patches.shared.misc.hex.hexPatch
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.util.*
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import app.revanced.util.returnEarly
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/fix/SpoofClientPatch;"
@@ -25,16 +19,40 @@ val spoofClientPatch = bytecodePatch(
val requestListenerPort by intOption(
key = "requestListenerPort",
default = 4345,
title = " Login request listener port",
description = "The port to use for the listener that intercepts and handles login requests. " +
"Port must be between 0 and 65535.",
required = true,
title = "Request listener port",
description = "The port to use for the listener that intercepts and handles spoofed requests. " +
"Port must be between 0 and 65535. " +
"Do not change this option, if you do not know what you are doing.",
validator = {
it!!
!(it < 0 || it > 65535)
}
)
val clientVersion by stringOption(
key = "clientVersion",
default = "iphone-9.0.58.558.g200011c",
title = "Client version",
description = "The client version used for spoofing the client token. " +
"Do not change this option, if you do not know what you are doing."
)
val hardwareMachine by stringOption(
key = "hardwareMachine",
default = "iPhone16,1",
title = "Hardware machine",
description = "The hardware machine used for spoofing the client token. " +
"Do not change this option, if you do not know what you are doing."
)
val systemVersion by stringOption(
key = "systemVersion",
default = "17.7.2",
title = "System version",
description = "The system version used for spoofing the client token. " +
"Do not change this option, if you do not know what you are doing."
)
dependsOn(
sharedExtensionPatch,
hexPatch(ignoreMissingTargetFiles = true, block = fun HexPatchBuilder.() {
@@ -44,10 +62,8 @@ val spoofClientPatch = bytecodePatch(
"x86",
"x86_64"
).forEach { architecture ->
"https://login5.spotify.com/v3/login" to "http://127.0.0.1:$requestListenerPort/v3/login" inFile
"lib/$architecture/liborbit-jni-spotify.so"
"https://login5.spotify.com/v4/login" to "http://127.0.0.1:$requestListenerPort/v4/login" inFile
"https://clienttoken.spotify.com/v1/clienttoken" to
"http://127.0.0.1:$requestListenerPort/v1/clienttoken" inFile
"lib/$architecture/liborbit-jni-spotify.so"
}
})
@@ -56,51 +72,6 @@ val spoofClientPatch = bytecodePatch(
compatibleWith("com.spotify.music")
execute {
// region Spoof package info.
getPackageInfoFingerprint.method.apply {
// region Spoof signature.
val failedToGetSignaturesStringIndex =
getPackageInfoFingerprint.stringMatches!!.first().index
val concatSignaturesIndex = indexOfFirstInstructionReversedOrThrow(
failedToGetSignaturesStringIndex,
Opcode.MOVE_RESULT_OBJECT,
)
val signatureRegister = getInstruction<OneRegisterInstruction>(concatSignaturesIndex).registerA
val expectedSignature = "d6a6dced4a85f24204bf9505ccc1fce114cadb32"
replaceInstruction(concatSignaturesIndex, "const-string v$signatureRegister, \"$expectedSignature\"")
// endregion
// region Spoof installer name.
val expectedInstallerName = "com.android.vending"
findInstructionIndicesReversedOrThrow {
val reference = getReference<MethodReference>()
reference?.name == "getInstallerPackageName" || reference?.name == "getInstallingPackageName"
}.forEach { index ->
val returnObjectIndex = index + 1
val installerPackageNameRegister = getInstruction<OneRegisterInstruction>(
returnObjectIndex
).registerA
addInstruction(
returnObjectIndex + 1,
"const-string v$installerPackageNameRegister, \"$expectedInstallerName\""
)
}
// endregion
}
// endregion
// region Spoof client.
loadOrbitLibraryFingerprint.method.addInstructions(
@@ -111,72 +82,12 @@ val spoofClientPatch = bytecodePatch(
"""
)
startupPageLayoutInflateFingerprint.method.apply {
val openLoginWebViewDescriptor =
"$EXTENSION_CLASS_DESCRIPTOR->launchLogin(Landroid/view/LayoutInflater;)V"
addInstructions(
0,
"invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor"
)
}
renderStartLoginScreenFingerprint.method.apply {
val onEventIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_INTERFACE && getReference<MethodReference>()?.name == "getView"
}
val buttonRegister = getInstruction<OneRegisterInstruction>(onEventIndex + 1).registerA
addInstruction(
onEventIndex + 2,
"invoke-static { v$buttonRegister }, $EXTENSION_CLASS_DESCRIPTOR->setNativeLoginHandler(Landroid/view/View;)V"
)
}
renderSecondLoginScreenFingerprint.method.apply {
val getViewIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_INTERFACE && getReference<MethodReference>()?.name == "getView"
}
val buttonRegister = getInstruction<OneRegisterInstruction>(getViewIndex + 1).registerA
// Early return the render for loop since the first item of the loop is the login button.
addInstructions(
getViewIndex + 2,
"""
invoke-virtual { v$buttonRegister }, Landroid/view/View;->performClick()Z
return-void
"""
)
}
renderThirdLoginScreenFingerprint.method.apply {
val invokeSetListenerIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == "Landroid/view/View;" && reference.name == "setOnClickListener"
}
val buttonRegister = getInstruction<FiveRegisterInstruction>(invokeSetListenerIndex).registerC
addInstruction(
invokeSetListenerIndex + 1,
"invoke-virtual { v$buttonRegister }, Landroid/view/View;->performClick()Z"
)
}
thirdLoginScreenLoginOnClickFingerprint.method.apply {
// Use placeholder credentials to pass the login screen.
val loginActionIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID) - 1
val loginActionInstruction = getInstruction<FiveRegisterInstruction>(loginActionIndex)
addInstructions(
loginActionIndex,
"""
const-string v${loginActionInstruction.registerD}, "placeholder"
const-string v${loginActionInstruction.registerE}, "placeholder"
"""
)
mapOf(
"getClientVersion" to clientVersion!!,
"getSystemVersion" to systemVersion!!,
"getHardwareMachine" to hardwareMachine!!
).forEach { (methodName, value) ->
extensionFixConstantsFingerprint.classDef.methods.single { it.name == methodName }.returnEarly(value)
}
// endregion

View File

@@ -6,7 +6,6 @@ import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
@@ -57,16 +56,10 @@ val changeLyricsProviderPatch = bytecodePatch(
}
execute {
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
Logger.getLogger(this::class.java.name).severe(
"Change lyrics provider patch is not supported for this target version."
)
return@execute
}
val httpClientBuilderMethod = httpClientBuilderFingerprint.originalMethod
// region Create a modified copy of the HTTP client builder method with the custom lyrics provider host.
val patchedHttpClientBuilderMethod = with(httpClientBuilderMethod) {
val invokeBuildUrlIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.returnType == "Lokhttp3/HttpUrl;"
@@ -89,9 +82,11 @@ val changeLyricsProviderPatch = bytecodePatch(
httpClientBuilderFingerprint.classDef.methods.add(this)
}
}
//endregion
// region Replace the call to the HTTP client builder method used exclusively for lyrics by the modified one.
getLyricsHttpClientFingerprint(httpClientBuilderMethod).method.apply {
val getLyricsHttpClientIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>() == httpClientBuilderMethod
@@ -118,6 +113,7 @@ val changeLyricsProviderPatch = bytecodePatch(
)
)
}
//endregion
}
}

View File

@@ -14,7 +14,7 @@ internal val shareCopyUrlFingerprint = fingerprint {
}
}
internal val shareCopyUrlLegacyFingerprint = fingerprint {
internal val oldShareCopyUrlFingerprint = fingerprint {
returns("Ljava/lang/Object;")
parameters("Ljava/lang/Object;")
strings("clipboard", "createNewSession failed")
@@ -38,7 +38,7 @@ internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
}
}
internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
internal val oldFormatAndroidShareSheetUrlFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("Ljava/lang/String;")
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")

View File

@@ -1,11 +1,9 @@
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.sharedExtensionPatch
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
@@ -28,10 +26,10 @@ val sanitizeSharingLinksPatch = bytecodePatch(
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
"sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
shareCopyUrlLegacyFingerprint
} else {
val copyFingerprint = if (shareCopyUrlFingerprint.originalMethodOrNull != null) {
shareCopyUrlFingerprint
} else {
oldShareCopyUrlFingerprint
}
copyFingerprint.method.apply {
@@ -50,15 +48,10 @@ val sanitizeSharingLinksPatch = bytecodePatch(
}
// 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
val methodAccessFlags = formatAndroidShareSheetUrlFingerprint.originalMethod.accessFlags
shareUrlParameter = if (AccessFlags.STATIC.isSet(methodAccessFlags)) {
val shareUrlParameter: String
val shareSheetFingerprint = if (formatAndroidShareSheetUrlFingerprint.originalMethodOrNull != null) {
val methodAccessFlags = formatAndroidShareSheetUrlFingerprint.originalMethod
shareUrlParameter = if (AccessFlags.STATIC.isSet(methodAccessFlags.accessFlags)) {
// In newer implementations the method is static, so p0 is not `this`.
"p1"
} else {
@@ -66,6 +59,11 @@ val sanitizeSharingLinksPatch = bytecodePatch(
// For that reason, add one to the parameter register.
"p2"
}
formatAndroidShareSheetUrlFingerprint
} else {
shareUrlParameter = "p2"
oldFormatAndroidShareSheetUrlFingerprint
}
shareSheetFingerprint.method.addInstructions(

View File

@@ -1,9 +1,7 @@
package app.revanced.patches.spotify.misc.widgets
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.returnEarly
import java.util.logging.Logger
@Suppress("unused")
val fixThirdPartyLaunchersWidgets = bytecodePatch(
@@ -13,14 +11,6 @@ val fixThirdPartyLaunchersWidgets = bytecodePatch(
compatibleWith("com.spotify.music")
execute {
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
// The permission check does not exist in legacy versions.
Logger.getLogger(this::class.java.name).warning(
"Legacy app target does not have any third party launcher restrictions. No changes applied."
)
return@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.

View File

@@ -1,38 +1,15 @@
package app.revanced.patches.spotify.shared
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.BytecodePatchContext
import com.android.tools.smali.dexlib2.AccessFlags
private const val SPOTIFY_MAIN_ACTIVITY = "Lcom/spotify/music/SpotifyMainActivity;"
/**
* Main activity of target 8.6.98.900.
*/
internal const val SPOTIFY_MAIN_ACTIVITY_LEGACY = "Lcom/spotify/music/MainActivity;"
internal val mainActivityOnCreateFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("Landroid/os/Bundle;")
custom { method, classDef ->
method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
method.name == "onCreate" && classDef.type == SPOTIFY_MAIN_ACTIVITY
}
}
private var isLegacyAppTarget: Boolean? = null
/**
* If patching a legacy 8.x target. This may also be set if patching slightly older/newer app targets,
* but the only legacy target of interest is 8.6.98.900 as it's the last version that
* supports Spotify integration on Kenwood/Pioneer car stereos.
*/
context(BytecodePatchContext)
internal val IS_SPOTIFY_LEGACY_APP_TARGET
get(): Boolean {
if (isLegacyAppTarget == null) {
isLegacyAppTarget = mainActivityOnCreateFingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY
}
return isLegacyAppTarget!!
}

View File

@@ -0,0 +1,63 @@
package app.revanced.patches.youtube.interaction.doubletap
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
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
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/DisableChapterSkipDoubleTapPatch;"
@Suppress("unused")
val disableChapterSkipDoubleTapPatch = bytecodePatch(
name = "Disable double tap actions",
description = "Adds an option to disable player double tap gestures.",
) {
dependsOn(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
)
compatibleWith(
"com.google.android.youtube"(
"19.34.42",
"19.43.41",
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)
execute {
addResources("youtube", "interaction.doubletap.disableChapterSkipDoubleTapPatch")
PreferenceScreen.PLAYER.addPreferences(
SwitchPreference("revanced_disable_chapter_skip_double_tap"),
)
// Force isChapterSeek flag to false.
doubleTapInfoGetSeekSourceFingerprint.method.addInstructions(
0,
"""
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->disableDoubleTapChapters(Z)Z
move-result p1
"""
)
doubleTapInfoCtorFingerprint.match(
doubleTapInfoGetSeekSourceFingerprint.classDef
).method.addInstructions(
0,
"""
invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->disableDoubleTapChapters(Z)Z
move-result p3
"""
)
}
}

View File

@@ -0,0 +1,31 @@
package app.revanced.patches.youtube.interaction.doubletap
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val doubleTapInfoGetSeekSourceFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
parameters("Z")
returns("L") // Enum SeekSource, but name obfuscated.
opcodes(
Opcode.IF_EQZ,
Opcode.SGET_OBJECT,
Opcode.RETURN_OBJECT,
Opcode.SGET_OBJECT,
Opcode.RETURN_OBJECT,
)
custom { _, classDef ->
classDef.fields.count() == 4
}
}
internal val doubleTapInfoCtorFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters(
"Landroid/view/MotionEvent;",
"I",
"Z",
"Lj\$/time/Duration;"
)
}

View File

@@ -73,12 +73,9 @@ val enableSlideToSeekPatch = bytecodePatch(
// Disable the double speed seek gesture.
if (is_19_17_or_greater) {
arrayOf(
disableFastForwardGestureFingerprint,
disableFastForwardNoticeFingerprint,
).forEach { fingerprint ->
fingerprint.method.apply {
val targetIndex = fingerprint.patternMatch!!.endIndex
disableFastForwardGestureFingerprint.let {
it.method.apply {
val targetIndex = it.patternMatch!!.endIndex
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(

View File

@@ -3,14 +3,12 @@ package app.revanced.patches.youtube.interaction.seekbar
import app.revanced.patcher.fingerprint
import app.revanced.util.containsLiteralInstruction
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionReversed
import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
internal val swipingUpGestureParentFingerprint = fingerprint {
returns("Z")
@@ -59,25 +57,6 @@ internal val disableFastForwardGestureFingerprint = fingerprint {
}
}
internal val disableFastForwardNoticeFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters()
opcodes(
Opcode.CHECK_CAST,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT,
)
custom { method, _ ->
method.name == "run" && method.indexOfFirstInstruction {
// In later targets the code is found in different methods with different strings.
val string = getReference<StringReference>()?.string
string == "Failed to easy seek haptics vibrate." || string == "search_landing_cache_key"
} >= 0
}
}
internal val onTouchEventHandlerFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.PUBLIC)
returns("Z")

View File

@@ -9,7 +9,6 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.InputType
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.shared.misc.settings.preference.TextPreference
import app.revanced.patches.youtube.interaction.seekbar.disableFastForwardNoticeFingerprint
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.lithoFilterPatch

View File

@@ -1,8 +1,11 @@
package app.revanced.patches.youtube.video.speed.custom
import app.revanced.patcher.fingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.StringReference
internal val speedLimiterFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
@@ -19,3 +22,16 @@ internal val speedLimiterFingerprint = fingerprint {
Opcode.INVOKE_STATIC,
)
}
internal val disableFastForwardNoticeFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters()
custom { method, _ ->
method.name == "run" && method.indexOfFirstInstruction {
// In later targets the code is found in different methods with different strings.
val string = getReference<StringReference>()?.string
string == "Failed to easy seek haptics vibrate." || string == "search_landing_cache_key"
} >= 0
}
}

View File

@@ -168,7 +168,17 @@ Gözlənilməz hallardan xəbərdar olmayacaqsınız."</string>
<string name="revanced_hide_feed_survey_summary_on">Axın sorğuları gizlidir</string>
<string name="revanced_hide_feed_survey_summary_off">Axın sorğuları göstərilir</string>
<string name="revanced_hide_floating_microphone_button_title">Üzən mikrofon düyməsini gizlət</string>
<string name="revanced_hide_floating_microphone_button_summary_on">Axtarışda üzən mikrofon düyməsi gizlidir</string>
<string name="revanced_hide_floating_microphone_button_summary_off">Üzən mikrofon düyməsi axtarışda göstərilir</string>
<string name="revanced_hide_horizontal_shelves_title">Üfüqi hissələri gizlət</string>
<string name="revanced_hide_horizontal_shelves_summary_on">"Üfüqi cərgələr gizlidir, məsələn:
• Son xəbərlər
• İzləməyə davam et
• Daha çox kanal kəşf et
• Ən uyğun
• Alış-veriş
• Yenidən izləyin"</string>
<string name="revanced_hide_horizontal_shelves_summary_off">Üfüqi cərgələr görünür</string>
<string name="revanced_hide_image_shelf_title">Şəkil cərgəsin gizlət</string>
<string name="revanced_hide_image_shelf_summary_on">Şəkil cərgəsi axtarış nəticələrində gizlidir</string>
<string name="revanced_hide_image_shelf_summary_off">Şəkil cərgəsi axtarış nəticələrində görünür</string>
@@ -184,18 +194,27 @@ Gözlənilməz hallardan xəbərdar olmayacaqsınız."</string>
<!-- 'Notify me' should be translated using the same localized wording YouTube displays.
This item appear in the subscription feed for future livestreams or unreleased videos. -->
<string name="revanced_hide_notify_me_button_title">\"Mənə bildir\" düyməsini gizlət</string>
<string name="revanced_hide_notify_me_button_summary_on">Mənə bildir düyməsi gizlidir</string>
<string name="revanced_hide_notify_me_button_summary_off">Mənə bildir düyməsi görünür</string>
<string name="revanced_hide_playables_title">Oynadılan elementləri gizlət</string>
<string name="revanced_hide_playables_summary_on">Oynadılanlar gizlidir</string>
<string name="revanced_hide_playables_summary_off">Oynadılanlar göstərilir</string>
<!-- 'Show more' should be translated with the same localized wording that YouTube displays.
This button usually appears when searching for a YT creator. -->
<string name="revanced_hide_show_more_button_title">\'Daha çox göstər\' düyməsini gizlət</string>
<string name="revanced_hide_show_more_button_summary_on">Daha çox göstər düyməsi axtarış nəticələrində gizlidir</string>
<string name="revanced_hide_show_more_button_summary_off">Daha çox göstər düyməsi axtarış nəticələrində görünür</string>
<string name="revanced_hide_ticket_shelf_title">Bilet bölməsin gizlət</string>
<string name="revanced_hide_ticket_shelf_summary_on">Bilet bölməsi gizlidir</string>
<string name="revanced_hide_ticket_shelf_summary_off">Bilet bölməsi görünür</string>
<!-- 'People also watched' and 'You might also like' should be translated using the same localized wording YouTube displays. -->
<string name="revanced_hide_video_recommendation_labels_title">Video tövsiyə etiketlərini gizlət</string>
<string name="revanced_hide_video_recommendation_labels_summary_on">\'İnsanlar həmçinin izləyiblər\' və \'Bunu da bəyənə bilərsiniz\' etiketləri axtarış nəticələrində gizlədilib</string>
<string name="revanced_hide_video_recommendation_labels_summary_off">\'İnsanlar həmçinin izləyiblər\' və \'Bunu da bəyənə bilərsiniz\' etiketləri axtarış nəticələrində görünür</string>
<!-- https://logos.fandom.com/wiki/YouTube/Yoodles -->
<string name="revanced_hide_doodles_title">YouTube Doodle-ları gizlət</string>
<string name="revanced_hide_doodles_summary_on">YouTube Doodles animasiyası simvolda gizlidir</string>
<string name="revanced_hide_doodles_summary_off">YouTube Doodles animasiyası simvolda görünür</string>
<string name="revanced_hide_doodles_user_dialog_message">"YouTube Doodle-ları hər il bir neçə gün görünür.
Əgər hazırda bölgənizdə Doodle göstərilirsə və bu gizlətmə seçimi aktivdirsə, axtarış cizgisi aşağısındakı filtr sahəsi də gizlədiləcək."</string>
@@ -214,6 +233,8 @@ Gözlənilməz hallardan xəbərdar olmayacaqsınız."</string>
<!-- 'Join' should be translated using the same localized wording YouTube displays.
This appears in the video player for certain videos. -->
<string name="revanced_hide_join_membership_button_title">Qoşul düyməsin gizlət</string>
<string name="revanced_hide_join_membership_button_summary_on">Qoşul düyməsi gizlidir</string>
<string name="revanced_hide_join_membership_button_summary_off">Qoşul düyməsi görünür</string>
<string name="revanced_hide_medical_panels_title">Tibbi lövhələri gizlət</string>
<string name="revanced_hide_medical_panels_summary_on">Tibbi lövhələr gizlidir</string>
<string name="revanced_hide_medical_panels_summary_off">Tibbi lövhələr göstərilir</string>
@@ -371,7 +392,11 @@ Məhdudiyyətlər
</patch>
<patch id="ad.general.hideAdsResourcePatch">
<string name="revanced_hide_creator_store_shelf_title">Yaradıcı mağaza bölümün gizlət</string>
<string name="revanced_hide_creator_store_shelf_summary_on">Yaradıcı alış-veriş cərgəsi video oynadıcı altında gizlidir</string>
<string name="revanced_hide_creator_store_shelf_summary_off">Yaradıcı alış-veriş cərgəsi video oynadıcı altında görünür</string>
<string name="revanced_hide_end_screen_store_banner_title">Son ekran mağaza etiketini gizlət</string>
<string name="revanced_hide_end_screen_store_banner_summary_on">Son ekran alış-veriş etiketi gizlədilib</string>
<string name="revanced_hide_end_screen_store_banner_summary_off">Son ekran alış-veriş etiketi görünür</string>
<string name="revanced_hide_fullscreen_ads_title">Tam ekran reklamlarını gizlət</string>
<string name="revanced_hide_fullscreen_ads_summary_on">"Tam ekran reklamları gizlidir
@@ -392,8 +417,12 @@ Bu xüsusiyyət yalnız köhnə cihazlar üçün mövcuddur"</string>
<string name="revanced_hide_self_sponsor_ads_summary_on">Özünə sponsorluq edilən kartlar gizlidir</string>
<string name="revanced_hide_self_sponsor_ads_summary_off">Özünə sponsorluq edilən kartlar göstərilir</string>
<string name="revanced_hide_shopping_links_title">Alış-veriş linklərini gizlət</string>
<string name="revanced_hide_shopping_links_summary_on">Alış-veriş linkləri video təsvirdə gizlidir</string>
<string name="revanced_hide_shopping_links_summary_off">Alış-veriş linkləri video təsvirdə görünür</string>
<!-- 'View products' should be translated with the same localized wording that YouTube displays. -->
<string name="revanced_hide_view_products_banner_title">“Məhsullara baxın” panelin gizlət</string>
<string name="revanced_hide_view_products_banner_summary_on">Məhsullara baxış etiketi video örtüyündə gizlidir</string>
<string name="revanced_hide_view_products_banner_summary_off">Məhsullara baxış etiketi video örtüyündə görünür</string>
<string name="revanced_hide_web_search_results_title">Veb axtarış nəticələrini gizlət</string>
<string name="revanced_hide_web_search_results_summary_on">Veb axtarış nəticələri gizlədilir</string>
<string name="revanced_hide_web_search_results_summary_off">Veb axtarış nəticələri göstərilir</string>
@@ -707,7 +736,13 @@ Audio trek seçimin göstərmək üçün \"Video axınları saxtalaşdır\"ı iO
<string name="revanced_shorts_player_screen_title">Shorts oynadıcı</string>
<string name="revanced_shorts_player_screen_summary">Shorts oynadıcıda hissəcikləri gizlət və ya göstər</string>
<!-- 'Home' should be translated using the same localized wording YouTube displays for the Home tab. -->
<string name="revanced_hide_shorts_home_title">Shorts-u Ev axınında gizlət</string>
<string name="revanced_hide_shorts_home_summary_on">Ev axını və əlaqəli videolarda gizlidir</string>
<string name="revanced_hide_shorts_home_summary_off">Ev axını və əlaqəli videolarda görünür</string>
<!-- 'Subscriptions' should be translated using the same localized wording YouTube displays for the Subscriptions tab. -->
<string name="revanced_hide_shorts_subscriptions_title">Shorts-u Abunəliklər axınında gizlət</string>
<string name="revanced_hide_shorts_subscriptions_summary_on">Abunəliklər axınında gizlidir</string>
<string name="revanced_hide_shorts_subscriptions_summary_off">Abunəliklər axınında görünür</string>
<string name="revanced_hide_shorts_search_title">Axtarış nəticələrindəki \"Shorts\"u gizlət</string>
<string name="revanced_hide_shorts_search_summary_on">Axtarış nəticələrində gizlidir</string>
<string name="revanced_hide_shorts_search_summary_off">Axtarış nəticələrində görünür</string>
@@ -715,6 +750,8 @@ Audio trek seçimin göstərmək üçün \"Video axınları saxtalaşdır\"ı iO
<string name="revanced_hide_shorts_history_summary_on">Baxış tarixçəsində gizlidir</string>
<string name="revanced_hide_shorts_history_summary_off">Baxış tarixçəsində göstərilib</string>
<string name="revanced_hide_shorts_super_thanks_button_title">Super Təşəkkür Al düyməsini gizlət</string>
<string name="revanced_hide_shorts_super_thanks_button_summary_on">Super Təşəkkürlər Al düyməsi gizlidir</string>
<string name="revanced_hide_shorts_super_thanks_button_summary_off">Super Təşəkkürlər Al düyməsi görünür</string>
<string name="revanced_hide_shorts_effect_button_title">Effekt düyməsini gizlət</string>
<string name="revanced_hide_shorts_effect_button_summary_on">Effekt düyməsi gizlidir</string>
<string name="revanced_hide_shorts_effect_button_summary_off">Effekt düyməsi görünür</string>
@@ -797,7 +834,11 @@ Audio trek seçimin göstərmək üçün \"Video axınları saxtalaşdır\"ı iO
<string name="revanced_hide_shorts_channel_bar_summary_on">Kanal çubuğu gizlidir</string>
<string name="revanced_hide_shorts_channel_bar_summary_off">Kanal çubuğu göstərilir</string>
<string name="revanced_hide_shorts_video_title_title">Video başlığını gizlət</string>
<string name="revanced_hide_shorts_video_title_summary_on">Video başlığı gizlidir</string>
<string name="revanced_hide_shorts_video_title_summary_off">Video başlığı görünür</string>
<string name="revanced_hide_shorts_sound_metadata_label_title">Səs üst məlumat etiketini gizlət</string>
<string name="revanced_hide_shorts_sound_metadata_label_summary_on">Səs üst məlumat etiketi gizlədilib</string>
<string name="revanced_hide_shorts_sound_metadata_label_summary_off">Səs üst məlumat etiketi görünür</string>
<string name="revanced_hide_shorts_full_video_link_label_title">Video keçidi etiketini gizlət</string>
<string name="revanced_hide_shorts_full_video_link_label_summary_on">Video linki etiketi gizlidir</string>
<string name="revanced_hide_shorts_full_video_link_label_summary_off">Video link etiketi göstərilir</string>
@@ -813,6 +854,9 @@ Avtomatik oynatma YouTube ayarlarında dəyişdirilə bilər: Ayarlar → Oxunu
<string name="revanced_end_screen_suggested_video_summary_off">Son ekranda bildirilən video göstərilir</string>
</patch>
<patch id="layout.hide.relatedvideooverlay.hideRelatedVideoOverlayPatch">
<string name="revanced_hide_related_videos_overlay_title">Əlaqəli videolar örtüyünü gizlət</string>
<string name="revanced_hide_related_videos_overlay_summary_on">Əlaqəli videolar yerləşməsi tam ekranda gizlidir</string>
<string name="revanced_hide_related_videos_overlay_summary_off">Əlaqəli videolar yerləşməsi tam ekranda görünür</string>
</patch>
<patch id="layout.hide.time.hideTimestampPatch">
<string name="revanced_hide_timestamp_title">Video vaxt möhürünü gizlət</string>
@@ -1279,8 +1323,10 @@ Bunu aktivləşdirmə, bəzi regionlarda əngəllənib silinən şəkilləri dü
<!-- 'Home' should be translated using the same localized wording YouTube displays for the Home tab. -->
<string name="revanced_alt_thumbnail_home_title">Ev paneli</string>
<!-- 'Subscriptions' should be translated using the same localized wording YouTube displays for the Subscriptions tab. -->
<string name="revanced_alt_thumbnail_subscription_title">Abunəliklər bölməsi</string>
<!-- 'You' should be translated using the same localized wording YouTube displays for the You (Library) tab. -->
<string name="revanced_alt_thumbnail_library_title">\"Siz\" paneli</string>
<string name="revanced_alt_thumbnail_player_title">Oynadıcı pleylistləri &amp; tövsiyələri</string>
<string name="revanced_alt_thumbnail_search_title">Axtarış nəticələri</string>
<string name="revanced_alt_thumbnail_options_entry_1">Orijinal miniatürlər</string>
<string name="revanced_alt_thumbnail_options_entry_2">DeArrow &amp; Orijinal miniatürlər</string>
@@ -1496,6 +1542,7 @@ AVC maksimum 1080p görüntü imkanına malikdir, Opus audio kodlama olmur və v
<string name="revanced_block_video_ads_summary_off">Video reklamlar bloklanmır</string>
</patch>
<patch id="chat.antidelete.showDeletedMessagesPatch">
<string name="revanced_deleted_msg">Məlumat silindi</string>
<string name="revanced_show_deleted_messages_title">Silinən mesajları göstər</string>
<string name="revanced_show_deleted_messages_entry_1">Silinən mesajlar göstərilməsin</string>
<string name="revanced_show_deleted_messages_entry_2">Silinmiş mesajları boz panel arxasında gizlət</string>
@@ -1516,8 +1563,11 @@ AVC maksimum 1080p görüntü imkanına malikdir, Opus audio kodlama olmur və v
<string name="revanced_settings">ReVanced Tənzimləmələri</string>
<string name="revanced_about_title">Haqqında</string>
<string name="revanced_about_summary">ReVanced Haqqında</string>
<string name="revanced_ads_screen_title">Reklam Əngəlləmə</string>
<string name="revanced_ads_screen_summary">Reklam Əngəlləmə tənzimləmələri</string>
<string name="revanced_chat_screen_title">Söhbət</string>
<string name="revanced_chat_screen_summary">Söhbət tənzimləmələri</string>
<string name="revanced_misc_screen_title">Çoxvariantlı</string>
<string name="revanced_misc_screen_summary">Müxtəlif tənzimləmələr</string>
<string name="revanced_general_category_title">Ümumi tənzimləmələr</string>
<string name="revanced_other_category_title">Digər tənzimləmələr</string>

View File

@@ -161,11 +161,19 @@ Et saa ilmoituksia odottamattomista tapahtumista."</string>
<string name="revanced_hide_crowdfunding_box_title">Piilota joukkorahoituslaatikko</string>
<string name="revanced_hide_crowdfunding_box_summary_on">Joukkorahoituslaatikko on piilotettu</string>
<string name="revanced_hide_crowdfunding_box_summary_off">Joukkorahoituslaatikko näytetään</string>
<string name="revanced_hide_expandable_card_title">Piilota laajennettava kortti</string>
<string name="revanced_hide_expandable_card_summary_on">Laajennettava kortti on piilotettu videoiden alla</string>
<string name="revanced_hide_expandable_card_summary_off">Laajennettava kortti näytetään videoiden alla</string>
<string name="revanced_hide_feed_survey_title">Piilota syötteen kyselyt</string>
<string name="revanced_hide_feed_survey_summary_on">Syötteen kyselyt on piilotettu</string>
<string name="revanced_hide_feed_survey_summary_off">Syötteen kyselyt näytetään</string>
<string name="revanced_hide_floating_microphone_button_title">Piilota kelluva mikrofonipainike</string>
<string name="revanced_hide_floating_microphone_button_summary_on">Kelluva mikrofonipainike on piilotettu haussa</string>
<string name="revanced_hide_floating_microphone_button_summary_off">Kelluva mikrofonipainike näytetään haussa</string>
<string name="revanced_hide_horizontal_shelves_title">Piilota vaakasuuntaiset hyllyt</string>
<string name="revanced_hide_image_shelf_title">Piilota kuvahylly</string>
<string name="revanced_hide_image_shelf_summary_on">Kuvahylly on piilotettu hakutuloksissa</string>
<string name="revanced_hide_image_shelf_summary_off">Kuvahylly näytetään hakutuloksissa</string>
<string name="revanced_hide_latest_posts_title">Piilota uusimmat postaukset</string>
<string name="revanced_hide_latest_posts_summary_on">Uusimmat postaukset on piilotettu</string>
<string name="revanced_hide_latest_posts_summary_off">Uusimmat postaukset näytetään</string>
@@ -178,12 +186,16 @@ Et saa ilmoituksia odottamattomista tapahtumista."</string>
<!-- 'Notify me' should be translated using the same localized wording YouTube displays.
This item appear in the subscription feed for future livestreams or unreleased videos. -->
<string name="revanced_hide_notify_me_button_title">Piilota \"Ilmoita minulle\" -painike</string>
<string name="revanced_hide_notify_me_button_summary_on">Ilmoita minulle -painike on piilotettu</string>
<string name="revanced_hide_notify_me_button_summary_off">Ilmoita minulle -painike näytetään</string>
<string name="revanced_hide_playables_title">Piilota Pelattavat</string>
<string name="revanced_hide_playables_summary_on">Pelattavat on piilotettu</string>
<string name="revanced_hide_playables_summary_off">Pelattavat näytetään</string>
<!-- 'Show more' should be translated with the same localized wording that YouTube displays.
This button usually appears when searching for a YT creator. -->
<string name="revanced_hide_show_more_button_title">Piilota \"Näytä lisää\" -painike</string>
<string name="revanced_hide_show_more_button_summary_on">Näytä lisää -painike on piilotettu hakutuloksissa</string>
<string name="revanced_hide_show_more_button_summary_off">Näytä lisää -painike näytetään hakutuloksissa</string>
<string name="revanced_hide_ticket_shelf_title">Piilota lippuhylly</string>
<string name="revanced_hide_ticket_shelf_summary_on">Lippuhylly on piilotettu</string>
<string name="revanced_hide_ticket_shelf_summary_off">Lippuhylly näytetään</string>
@@ -208,9 +220,13 @@ Jos Doodle näkyy tällä hetkellä alueellasi ja tämä piilotusasetus on käyt
<!-- 'Join' should be translated using the same localized wording YouTube displays.
This appears in the video player for certain videos. -->
<string name="revanced_hide_join_membership_button_title">Piilota Liity-painike</string>
<string name="revanced_hide_join_membership_button_summary_on">Liity-painike on piilotettu</string>
<string name="revanced_hide_join_membership_button_summary_off">Liity-painike näytetään</string>
<string name="revanced_hide_medical_panels_title">Piilota lääketieteelliset paneelit</string>
<string name="revanced_hide_medical_panels_summary_on">Lääketieteelliset paneelit on piilotettu</string>
<string name="revanced_hide_medical_panels_summary_off">Lääketieteelliset paneelit näytetään</string>
<string name="revanced_hide_quick_actions_title">Piilota pikatoiminnot</string>
<string name="revanced_hide_related_videos_title">Piilota liittyvät videot</string>
<string name="revanced_hide_subscribers_community_guidelines_title">Piilota tilaajien ohjeet</string>
<string name="revanced_hide_subscribers_community_guidelines_summary_on">Tilaajien yhteisön säännöt on piilotettu</string>
<string name="revanced_hide_subscribers_community_guidelines_summary_off">Tilaajien yhteisön säännöt näytetään</string>
@@ -260,6 +276,7 @@ Jos Doodle näkyy tällä hetkellä alueellasi ja tämä piilotusasetus on käyt
<string name="revanced_hide_filter_bar_feed_in_related_videos_title">Piilota liittyvissä videoissa</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_summary_on">Piilotettu liittyvissä videoissa</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_summary_off">Näytetään liittyvissä videoissa</string>
<string name="revanced_channel_screen_title">Kanavasivu</string>
<!-- 'For You' should be translated using the same localized wording YouTube displays. -->
<string name="revanced_hide_for_you_shelf_title">Piilota \"\'Sinulle\" -hylly</string>
<!-- 'Visit Community' should be translated with the same localized wording that YouTube displays. -->

View File

@@ -33,7 +33,7 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.settings.settingsResourcePatch">
<string name="revanced_settings_submenu_title">設定</string>
<string name="revanced_settings_confirm_user_dialog_title">本当に続行しますか?</string>
<string name="revanced_settings_confirm_user_dialog_title">続行してもよろしいですか?</string>
<string name="revanced_settings_reset">リセット</string>
<string name="revanced_settings_reset_color">色をリセット</string>
<string name="revanced_settings_color_invalid">色の値が無効です</string>
@@ -301,9 +301,9 @@ GmsCore の電池の最適化を無効にしても、バッテリーの使用に
<string name="revanced_hide_for_you_shelf_title">「おすすめ」欄を非表示</string>
<string name="revanced_hide_for_you_shelf_summary_on">「おすすめ」欄は表示されません</string>
<string name="revanced_hide_for_you_shelf_summary_off">「おすすめ」欄は表示されます</string>
<string name="revanced_hide_links_preview_title">リンク プレビューを非表示</string>
<string name="revanced_hide_links_preview_summary_on">リンク プレビューは表示されません</string>
<string name="revanced_hide_links_preview_summary_off">リンク プレビューは表示されます</string>
<string name="revanced_hide_links_preview_title">リンク集のプレビューを非表示</string>
<string name="revanced_hide_links_preview_summary_on">リンク集のプレビューは表示されません</string>
<string name="revanced_hide_links_preview_summary_off">リンク集のプレビューは表示されます</string>
<string name="revanced_hide_members_shelf_title">メンバー欄を非表示</string>
<string name="revanced_hide_members_shelf_summary_on">メンバー欄は表示されません</string>
<string name="revanced_hide_members_shelf_summary_off">メンバー欄は表示されます</string>
@@ -412,9 +412,9 @@ GmsCore の電池の最適化を無効にしても、バッテリーの使用に
<string name="revanced_hide_merchandise_banners_title">商品バナーを非表示</string>
<string name="revanced_hide_merchandise_banners_summary_on">商品バナーは表示されません</string>
<string name="revanced_hide_merchandise_banners_summary_off">商品バナーは表示されます</string>
<string name="revanced_hide_paid_promotion_label_title">「プロモーションを含みます」ボタンを非表示</string>
<string name="revanced_hide_paid_promotion_label_summary_on">プレーヤー画面の「プロモーションを含みます」ボタンは表示されません</string>
<string name="revanced_hide_paid_promotion_label_summary_off">プレーヤー画面の「プロモーションを含みます」ボタンは表示されます</string>
<string name="revanced_hide_paid_promotion_label_title">「プロモーションを含みます」ラベルを非表示</string>
<string name="revanced_hide_paid_promotion_label_summary_on">「プロモーションを含みます」ラベルは表示されません</string>
<string name="revanced_hide_paid_promotion_label_summary_off">「プロモーションを含みます」ラベルは表示されます</string>
<string name="revanced_hide_self_sponsor_ads_title">自己スポンサー カードを非表示</string>
<string name="revanced_hide_self_sponsor_ads_summary_on">自己スポンサー カードは表示されません</string>
<string name="revanced_hide_self_sponsor_ads_summary_off">自己スポンサー カードは表示されます</string>
@@ -422,7 +422,7 @@ GmsCore の電池の最適化を無効にしても、バッテリーの使用に
<string name="revanced_hide_shopping_links_summary_on">動画の概要欄の商品へのリンクは表示されません</string>
<string name="revanced_hide_shopping_links_summary_off">動画の概要欄の商品へのリンクは表示されます</string>
<!-- 'View products' should be translated with the same localized wording that YouTube displays. -->
<string name="revanced_hide_view_products_banner_title">「商品を表示」ボタンを非表示</string>
<string name="revanced_hide_view_products_banner_title">「商品を表示」バナーを非表示</string>
<string name="revanced_hide_view_products_banner_summary_on">動画オーバーレイの「商品を表示」バナーは表示されません</string>
<string name="revanced_hide_view_products_banner_summary_off">動画オーバーレイの「商品を表示」バナーは表示されます</string>
<string name="revanced_hide_web_search_results_title">ウェブ検索結果を非表示</string>

View File

@@ -52,7 +52,7 @@ Second \"item\" text"</string>
<string name="revanced_show_menu_icons_title">Visa ikoner för ReVanced-inställningar</string>
<string name="revanced_show_menu_icons_summary_on">Inställningsikoner visas</string>
<string name="revanced_show_menu_icons_summary_off">Ikoner för inställningar visas inte</string>
<string name="revanced_language_title">Språket för ReVanced</string>
<string name="revanced_language_title">Språk för ReVanced</string>
<string name="revanced_language_user_dialog_message">"Översättningar till vissa språk kan vara ofullständiga eller saknas.
För att översätta till nya språk besök translate.revanced.app"</string>
@@ -95,7 +95,7 @@ Tryck på Fortsätt-knappen och tillåt optimeringsändringar."</string>
<string name="revanced_settings_screen_05_player_title">Spelare</string>
<string name="revanced_settings_screen_07_seekbar_title">Sökreglage</string>
<string name="revanced_settings_screen_08_swipe_controls_title">Svepkontroller</string>
<string name="revanced_settings_screen_11_misc_title">Diverse</string>
<string name="revanced_settings_screen_11_misc_title">Övrigt</string>
<string name="revanced_settings_screen_12_video_title">Video</string>
<string name="revanced_restore_old_settings_menus_title">Återställ gamla inställningsmenyer</string>
<string name="revanced_restore_old_settings_menus_summary_on">Gamla inställningsmenyer visas</string>
@@ -106,8 +106,8 @@ Tryck på Fortsätt-knappen och tillåt optimeringsändringar."</string>
</patch>
<patch id="misc.backgroundplayback.backgroundPlaybackPatch">
<string name="revanced_shorts_disable_background_playback_title">Inaktivera uppspelning av Shorts-videor i bakgrunden</string>
<string name="revanced_shorts_disable_background_playback_summary_on">Uppspelning av Shorts-videor i bakgrunden är inaktiverat</string>
<string name="revanced_shorts_disable_background_playback_summary_off">Uppspelning av Shorts-videor i bakgrunden är aktiverat</string>
<string name="revanced_shorts_disable_background_playback_summary_on">Uppspelning av Shorts-videor i bakgrunden är inaktiverad</string>
<string name="revanced_shorts_disable_background_playback_summary_off">Uppspelning av Shorts-videor i bakgrunden är aktiverad</string>
</patch>
<patch id="misc.debugging.enableDebuggingPatch">
<string name="revanced_debug_screen_title">Felsökning</string>
@@ -126,10 +126,10 @@ Men om du aktiverar detta kommer även vissa användardata, t.ex. din IP-adress,
<string name="revanced_debug_stacktrace_title">Logga stackspårning</string>
<string name="revanced_debug_stacktrace_summary_on">Felsökningsloggar inkluderar stackspårning</string>
<string name="revanced_debug_stacktrace_summary_off">Felsökningsloggar inkluderar inte stackspårning</string>
<string name="revanced_debug_toast_on_error_title">Visa ett meddelande om ett fel uppstår med ReVanced</string>
<string name="revanced_debug_toast_on_error_summary_on">Meddelande visas om fel uppstår</string>
<string name="revanced_debug_toast_on_error_summary_off">Meddelande visas inte om fel uppstår</string>
<string name="revanced_debug_toast_on_error_user_dialog_message">"Om du stänger av felmeddelanden döljs alla ReVanced-felmeddelanden.
<string name="revanced_debug_toast_on_error_title">Visa ett popup-meddelande om det uppstår ett ReVanced-fel</string>
<string name="revanced_debug_toast_on_error_summary_on">Ett popup-meddelande visas om ett fel uppstår</string>
<string name="revanced_debug_toast_on_error_summary_off">Inget popup-meddelande visas om ett fel uppstår</string>
<string name="revanced_debug_toast_on_error_user_dialog_message">"Om du stänger av pupup-felmeddelanden döljs alla ReVanced-felmeddelanden.
Du kommer inte att bli meddelad om oväntade händelser."</string>
<string name="revanced_debug_export_logs_to_clipboard_title">Exportera felsökningsloggar</string>
@@ -144,11 +144,11 @@ Du kommer inte att bli meddelad om oväntade händelser."</string>
</patch>
<patch id="layout.hide.general.hideLayoutComponentsPatch">
<string name="revanced_hide_album_cards_title">Dölj albumkort</string>
<string name="revanced_hide_album_cards_summary_on">Skivkorten är dolda</string>
<string name="revanced_hide_album_cards_summary_off">Albumkort är synliga</string>
<string name="revanced_hide_album_cards_summary_on">Albumkort är dolda</string>
<string name="revanced_hide_album_cards_summary_off">Albumkort visas</string>
<string name="revanced_hide_artist_cards_title">Dölj artistkort</string>
<string name="revanced_hide_artist_cards_summary_on">Konstnärskort är dolda</string>
<string name="revanced_hide_artist_cards_summary_off">Artistkort är synliga</string>
<string name="revanced_hide_artist_cards_summary_on">Artistkort är dolda</string>
<string name="revanced_hide_artist_cards_summary_off">Artistkort visas</string>
<string name="revanced_hide_chips_shelf_title">Dölj chip-hylla</string>
<string name="revanced_hide_chips_shelf_summary_on">Chip-hyllan är dold</string>
<string name="revanced_hide_chips_shelf_summary_off">Chip-hyllan visas</string>
@@ -161,9 +161,9 @@ Du kommer inte att bli meddelad om oväntade händelser."</string>
<string name="revanced_hide_crowdfunding_box_title">Dölj insamlingsrutan</string>
<string name="revanced_hide_crowdfunding_box_summary_on">Insamlingsrutan är dold</string>
<string name="revanced_hide_crowdfunding_box_summary_off">Insamlingsrutan visas</string>
<string name="revanced_hide_expandable_card_title">Dölj utökbart kort</string>
<string name="revanced_hide_expandable_card_summary_on">Utökbart kort under videor är dolt</string>
<string name="revanced_hide_expandable_card_summary_off">Utökbart kort under videor visas</string>
<string name="revanced_hide_expandable_card_title">Dölj expanderbart kort</string>
<string name="revanced_hide_expandable_card_summary_on">Expanderbart kort under videor är dolt</string>
<string name="revanced_hide_expandable_card_summary_off">Expanderbart kort under videor visas</string>
<string name="revanced_hide_feed_survey_title">Dölj enkäter i flödet</string>
<string name="revanced_hide_feed_survey_summary_on">Enkäter i flödet är dolda</string>
<string name="revanced_hide_feed_survey_summary_off">Enkäter i flödet visas</string>
@@ -171,7 +171,7 @@ Du kommer inte att bli meddelad om oväntade händelser."</string>
<string name="revanced_hide_floating_microphone_button_summary_on">Flytande mikrofonknapp i sökning är dold</string>
<string name="revanced_hide_floating_microphone_button_summary_off">Flytande mikrofonknapp i sökning visas</string>
<string name="revanced_hide_horizontal_shelves_title">Dölj horisontella hyllor</string>
<string name="revanced_hide_horizontal_shelves_summary_on">"Hyllor är dolda, till exempel:
<string name="revanced_hide_horizontal_shelves_summary_on">"Horisontella hyllor är dolda, till exempel:
• Senaste nytt
• Fortsätt titta
• Utforska fler kanaler
@@ -196,12 +196,12 @@ Du kommer inte att bli meddelad om oväntade händelser."</string>
<string name="revanced_hide_notify_me_button_title">Dölj knappen \"Meddela mig\"</string>
<string name="revanced_hide_notify_me_button_summary_on">Knappen Meddela mig är dold</string>
<string name="revanced_hide_notify_me_button_summary_off">Knappen Meddela mig visas</string>
<string name="revanced_hide_playables_title">Dölj Playables</string>
<string name="revanced_hide_playables_summary_on">Playables är dolda</string>
<string name="revanced_hide_playables_summary_off">Spelbara är synliga</string>
<string name="revanced_hide_playables_title">Dölj Spelhörna</string>
<string name="revanced_hide_playables_summary_on">Spelhörna är dold</string>
<string name="revanced_hide_playables_summary_off">Spelhörna visas</string>
<!-- 'Show more' should be translated with the same localized wording that YouTube displays.
This button usually appears when searching for a YT creator. -->
<string name="revanced_hide_show_more_button_title">Dölj knappen \"Visa mer\"</string>
<string name="revanced_hide_show_more_button_title">Dölj knappen Visa mer</string>
<string name="revanced_hide_show_more_button_summary_on">Knappen Visa mer i sökresultat är dold</string>
<string name="revanced_hide_show_more_button_summary_off">Knappen Visa mer i sökresultat visas</string>
<string name="revanced_hide_ticket_shelf_title">Dölj biljetthylla</string>
@@ -209,12 +209,12 @@ Du kommer inte att bli meddelad om oväntade händelser."</string>
<string name="revanced_hide_ticket_shelf_summary_off">Biljetthyllan visas</string>
<!-- 'People also watched' and 'You might also like' should be translated using the same localized wording YouTube displays. -->
<string name="revanced_hide_video_recommendation_labels_title">Dölj etiketter för videorekommendationer</string>
<string name="revanced_hide_video_recommendation_labels_summary_on">Etiketter för \"Andra har även tittat på\" och \"Du kanske även gillar\" i sökresultaten är dolda</string>
<string name="revanced_hide_video_recommendation_labels_summary_off">Etiketter för \"Andra har även tittat på\" och \"Du kanske även gillar\" i sökresultaten visas</string>
<string name="revanced_hide_video_recommendation_labels_summary_on">\"Andra har även tittat på\"- och \"Du kanske även gillar\"-etiketterna i sökresultaten är dolda</string>
<string name="revanced_hide_video_recommendation_labels_summary_off">\"Andra har även tittat på\"- och \"Du kanske även gillar\"-etiketterna i sökresultaten visas</string>
<!-- https://logos.fandom.com/wiki/YouTube/Yoodles -->
<string name="revanced_hide_doodles_title">Dölj YouTube-doodles</string>
<string name="revanced_hide_doodles_summary_on">YouTube Doodles-animation på logotypen är dold</string>
<string name="revanced_hide_doodles_summary_off">YouTube Doodles-animering på logotypen visas</string>
<string name="revanced_hide_doodles_summary_on">YouTube-doodles-animation på logotypen är dold</string>
<string name="revanced_hide_doodles_summary_off">YouTube-doodles-animering på logotypen visas</string>
<string name="revanced_hide_doodles_user_dialog_message">"YouTube-doodles visas några dagar varje år.
Om en doodle visas för närvarande i din region och den här döljningsinställningen är på, kommer filterfältet under sökfältet också att döljas."</string>
@@ -278,30 +278,30 @@ Om en doodle visas för närvarande i din region och den här döljningsinställ
<string name="revanced_hide_transcript_section_summary_on">Avsnittet Manuskript är dolt</string>
<string name="revanced_hide_transcript_section_summary_off">Avsnittet Manuskript visas</string>
<string name="revanced_hide_description_components_screen_title">Videobeskrivning</string>
<string name="revanced_hide_description_components_screen_summary">Dölj eller visa videobeskrivningskomponenter</string>
<string name="revanced_hide_description_components_screen_summary">Dölj eller visa komponenter i videobeskrivningen</string>
<string name="revanced_hide_filter_bar_screen_title">Filterfält</string>
<string name="revanced_hide_filter_bar_screen_summary">Dölj eller visa filterfältet i flödena, historiken, sökresultaten och relaterade videor</string>
<string name="revanced_hide_filter_bar_screen_summary">Dölj eller visa filterfältet i flödena, historiken, sökresultaten och liknande videor</string>
<string name="revanced_hide_filter_bar_feed_in_feed_title">Dölj i flöden</string>
<string name="revanced_hide_filter_bar_feed_in_feed_summary_on">Dold i flöden</string>
<string name="revanced_hide_filter_bar_feed_in_feed_summary_on">Dolt i flöden</string>
<string name="revanced_hide_filter_bar_feed_in_feed_summary_off">Visas i flöden</string>
<string name="revanced_hide_filter_bar_feed_in_history_title">Dölj i historik</string>
<string name="revanced_hide_filter_bar_feed_in_history_summary_on">Dold i historik</string>
<string name="revanced_hide_filter_bar_feed_in_history_summary_on">Dolt i historik</string>
<string name="revanced_hide_filter_bar_feed_in_history_summary_off">Visas i historik</string>
<string name="revanced_hide_filter_bar_feed_in_search_title">Dölj i sökresultat</string>
<string name="revanced_hide_filter_bar_feed_in_search_summary_on">Dold i sökresultat</string>
<string name="revanced_hide_filter_bar_feed_in_search_summary_on">Dolt i sökresultat</string>
<string name="revanced_hide_filter_bar_feed_in_search_summary_off">Visas i sökresultat</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_title">Dölj i relaterade videor</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_summary_on">Dold i relaterade videor</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_summary_off">Visas i relaterade videor</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_title">Dölj i liknande videor</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_summary_on">Dolt i liknande videor</string>
<string name="revanced_hide_filter_bar_feed_in_related_videos_summary_off">Visas i liknande videor</string>
<string name="revanced_channel_screen_title">Kanalsida</string>
<string name="revanced_channel_screen_summary">Dölj eller visa komponenter på kanalsidan</string>
<!-- 'For You' should be translated using the same localized wording YouTube displays. -->
<string name="revanced_hide_for_you_shelf_title">Dölj hyllan \"För dig\"</string>
<string name="revanced_hide_for_you_shelf_summary_on">Hyllan \"För dig\" är dold</string>
<string name="revanced_hide_for_you_shelf_summary_off">Hyllan \"För dig\" visas</string>
<string name="revanced_hide_links_preview_title">Dölj förhandsvisning av länkar</string>
<string name="revanced_hide_links_preview_summary_on">Förhandsvisning av länkar är dold</string>
<string name="revanced_hide_links_preview_summary_off">Förhandsvisning av länkar visas</string>
<string name="revanced_hide_for_you_shelf_summary_on">Hyllan För dig är dold</string>
<string name="revanced_hide_for_you_shelf_summary_off">Hyllan För dig visas</string>
<string name="revanced_hide_links_preview_title">Dölj förhandsgranskning av länkar</string>
<string name="revanced_hide_links_preview_summary_on">Förhandsgranskning av länkar är dold</string>
<string name="revanced_hide_links_preview_summary_off">Förhandsgranskning av länkar visas</string>
<string name="revanced_hide_members_shelf_title">Dölj medlemshylla</string>
<string name="revanced_hide_members_shelf_summary_on">Medlemshyllan är dold</string>
<string name="revanced_hide_members_shelf_summary_off">Medlemshyllan visas</string>
@@ -677,7 +677,7 @@ Om du ändrar den här inställningen och det inte får effekt kan du försöka
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
<string name="revanced_hide_player_flyout_audio_track_not_available">"Ljudspårsmenyn är dold
För att visa ljudspårsmenyn, ändra \"Spoof video streams\" till iOS TV"</string>
För att visa ljudspårsmenyn, ändra \"Förfalska videoströmmar\" till iOS TV"</string>
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
<string name="revanced_hide_player_flyout_watch_in_vr_title">Dölj Titta i VR</string>
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Menyn Titta i VR är dold</string>
@@ -972,11 +972,11 @@ Den här funktionen fungerar bäst med en videokvalitet på 720p eller lägre oc
<string name="revanced_sb_enable_auto_hide_skip_segment_button_sum_off">Hoppa över-knappen visas för hela segmentet</string>
<string name="revanced_sb_auto_hide_skip_button_duration">Varaktighet för Hoppa över-knappen</string>
<string name="revanced_sb_auto_hide_skip_button_duration_sum">Hur länge knappen för att hoppa över och hoppa till höjdpunkt ska visas innan den döljs automatiskt</string>
<string name="revanced_sb_general_skiptoast">Visa toastmeddelande för ångra överhoppning</string>
<string name="revanced_sb_general_skiptoast_sum_on">Meddelande visas när ett segment hoppas över automatiskt. Tryck på toastmeddelandet för att ångra överhoppningen</string>
<string name="revanced_sb_general_skiptoast">Visa popup-meddelande för att ångra överhoppning</string>
<string name="revanced_sb_general_skiptoast_sum_on">Popup-meddelande visas när ett segment hoppas över automatiskt. Tryck på popup-meddelandet för att ångra överhoppningen</string>
<string name="revanced_sb_general_skiptoast_sum_off">Toastmeddelande visas inte</string>
<string name="revanced_sb_toast_on_skip_duration">Varaktighet för överhoppningstoastmeddelande</string>
<string name="revanced_sb_toast_on_skip_duration_sum">Hur länge toast-meddelandet för ångra överhoppning ska visas</string>
<string name="revanced_sb_toast_on_skip_duration">Varaktighet för överhoppningsmeddelande</string>
<string name="revanced_sb_toast_on_skip_duration_sum">Hur länge popup-meddelandet för att ångra överhoppning ska visas</string>
<string name="revanced_sb_duration_1s">1 sekund</string>
<string name="revanced_sb_duration_2s">2 sekunder</string>
<string name="revanced_sb_duration_3s">3 sekunder</string>
@@ -1112,7 +1112,7 @@ Redan finns"</string>
<string name="revanced_sb_new_segment_disabled_category">Kategorin är inaktiverad i inställningar. Aktivera kategori för att skicka.</string>
<string name="revanced_sb_new_segment_title">Nytt Sponsorblock-segment</string>
<string name="revanced_sb_new_segment_mark_time_as_question">Ange %s som början eller slutet av ett nytt segment?</string>
<string name="revanced_sb_new_segment_mark_start">Start</string>
<string name="revanced_sb_new_segment_mark_start">Början</string>
<string name="revanced_sb_new_segment_mark_end">Slut</string>
<string name="revanced_sb_new_segment_now">Nu</string>
<string name="revanced_sb_new_segment_time_start">Tid då segmentet börjar på</string>
@@ -1176,7 +1176,7 @@ Billayout
• Flödet ordnas efter ämnen och kanaler"</string>
</patch>
<patch id="layout.spoofappversion.spoofAppVersionPatch">
<string name="revanced_spoof_app_version_title">Spoof app-version</string>
<string name="revanced_spoof_app_version_title">Förfalska appversionen</string>
<string name="revanced_spoof_app_version_summary_on">Version förfalskad</string>
<string name="revanced_spoof_app_version_summary_off">Version inte förfalskad</string>
<string name="revanced_spoof_app_version_user_dialog_message">"Appversionen kommer att förfalskas till en äldre version av YouTube.
@@ -1186,7 +1186,7 @@ Detta kommer att ändra utseendet och funktionerna i appen, men okända bieffekt
Om det senare stängs av rekommenderas det att rensa appens data för att förhindra UI-buggar."</string>
<!-- It is ideal, but not required, if the text here appears is alphabetically after the text used for 'revanced_spoof_app_version_title'.
This is because the 'General layout' menu uses alphabetic sorting, and it functionally works better if the spoof target selector appears below the 'Spoof app version' UI switch. -->
<string name="revanced_spoof_app_version_target_title">Spoof app-versionsmål</string>
<string name="revanced_spoof_app_version_target_title">Mål för Förfalska appversionen</string>
<string name="revanced_spoof_app_version_target_entry_1">19.35.36 - Återställ gamla ikoner i Shorts-spelaren</string>
<string name="revanced_spoof_app_version_target_entry_2">19.01.34 - Återställ gamla navigeringsikoner</string>
</patch>
@@ -1216,8 +1216,8 @@ Om det senare stängs av rekommenderas det att rensa appens data för att förhi
<string name="revanced_change_start_page_entry_virtual_reality">Virtuell verklighet</string>
<string name="revanced_change_start_page_entry_watch_later">Titta senare</string>
<string name="revanced_change_start_page_entry_your_clips">Dina klipp</string>
<string name="revanced_change_start_page_always_title">Ändra alltid startsida</string>
<string name="revanced_change_start_page_always_summary_on">"Startsidan ändras alltid
<string name="revanced_change_start_page_always_title">Byt alltid startsida</string>
<string name="revanced_change_start_page_always_summary_on">"Startsidan byts alltid
Begränsning: Bakåtknappen i verktygsfältet kanske inte fungerar"</string>
<string name="revanced_change_start_page_always_summary_off">Startsidan ändras endast vid appstart</string>
@@ -1313,20 +1313,20 @@ Minispelaren kan dras utanför skärmen till vänster eller höger"</string>
<string name="revanced_header_logo_entry_6">Anpassad</string>
</patch>
<patch id="layout.thumbnails.bypassImageRegionRestrictionsPatch">
<string name="revanced_bypass_image_region_restrictions_title">Begränsningar för förbipassering av bildregionen</string>
<string name="revanced_bypass_image_region_restrictions_summary_on">Använder bildvärd yt4.ggpht.com</string>
<string name="revanced_bypass_image_region_restrictions_title">Kringgå regionsbegränsningar för bilder</string>
<string name="revanced_bypass_image_region_restrictions_summary_on">Använder bildvärden yt4.ggpht.com</string>
<string name="revanced_bypass_image_region_restrictions_summary_off">"Använder ursprunglig bildvärd
Om du aktiverar detta kan det åtgärda saknade bilder som blockeras i vissa regioner"</string>
</patch>
<patch id="layout.thumbnails.alternativeThumbnailsPatch">
<!-- 'Home' should be translated using the same localized wording YouTube displays for the Home tab. -->
<string name="revanced_alt_thumbnail_home_title">Hemflik</string>
<string name="revanced_alt_thumbnail_home_title">Fliken Hem</string>
<!-- 'Subscriptions' should be translated using the same localized wording YouTube displays for the Subscriptions tab. -->
<string name="revanced_alt_thumbnail_subscription_title">Fliken Prenumerationer</string>
<!-- 'You' should be translated using the same localized wording YouTube displays for the You (Library) tab. -->
<string name="revanced_alt_thumbnail_library_title">Du flik</string>
<string name="revanced_alt_thumbnail_player_title">Spelarlistor &amp; rekommendationer för spelare</string>
<string name="revanced_alt_thumbnail_library_title">Fliken Ditt YouTube</string>
<string name="revanced_alt_thumbnail_player_title">Spellistor och rekommendationer i spelaren</string>
<string name="revanced_alt_thumbnail_search_title">Sökresultat</string>
<string name="revanced_alt_thumbnail_options_entry_1">Ursprungliga miniatyrer</string>
<string name="revanced_alt_thumbnail_options_entry_2">DeArrow och ursprungliga miniatyrer</string>
@@ -1341,7 +1341,7 @@ Tryck här för att läsa mer om DeArrow"</string>
<string name="revanced_alt_thumbnail_dearrow_connection_toast_summary_on">Meddelande visas om DeArrow inte är tillgängligt</string>
<string name="revanced_alt_thumbnail_dearrow_connection_toast_summary_off">Meddelande visas inte om DeArrow inte är tillgängligt</string>
<string name="revanced_alt_thumbnail_dearrow_api_url_title">DeArrow API-slutpunkt</string>
<string name="revanced_alt_thumbnail_dearrow_api_url_summary">Slutpunktsadressen för DeArrow-miniatyrbildscachen</string>
<string name="revanced_alt_thumbnail_dearrow_api_url_summary">DeArrow-miniatyrcacheminnets slutpunktsadress</string>
<string name="revanced_alt_thumbnail_stills_about_title">Stillbilder från videor</string>
<string name="revanced_alt_thumbnail_stills_about_summary">Stillbilder tas från början/mitten/slutet av varje video. Dessa bilder är inbyggda i YouTube och inget externt API används</string>
<string name="revanced_alt_thumbnail_stills_fast_title">Använd snabbt stillbilder</string>
@@ -1374,13 +1374,13 @@ Tryck här för att läsa mer om DeArrow"</string>
<string name="revanced_auto_repeat_summary_off">Automatisk upprepning är inaktiverad</string>
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
<string name="revanced_spoof_device_dimensions_title">Enhetsdimensioner för Spoof</string>
<string name="revanced_spoof_device_dimensions_summary_on">"Enhetens dimensioner förfalskas
<string name="revanced_spoof_device_dimensions_title">Förfalska enhetens mått</string>
<string name="revanced_spoof_device_dimensions_summary_on">"Enhetens mått förfalskas
Högre videokvalitet kan låsas upp men du kan uppleva hackig videouppspelning, sämre batteritid och okända bieffekter"</string>
<string name="revanced_spoof_device_dimensions_summary_off">"Enhetens dimensioner förfalskas inte
Högre videokvaliteter kan låsas upp men du kan uppleva hackig videouppspelning, sämre batteritid och okända bieffekter"</string>
<string name="revanced_spoof_device_dimensions_summary_off">"Enhetens mått förfalskas inte
Att aktivera detta kan låsa upp högre videokvalitet"</string>
Om du aktiverar detta kan högre videokvaliteter låsas upp"</string>
<string name="revanced_spoof_device_dimensions_user_dialog_message">Om du aktiverar detta kan det leda till hackig videouppspelning, sämre batteritid och okända biverkningar.</string>
</patch>
<patch id="misc.gms.gmsCoreSupportResourcePatch">
@@ -1396,9 +1396,9 @@ Att aktivera detta kan låsa upp högre videokvalitet"</string>
<string name="revanced_disable_haptic_feedback_precise_seeking_title">Inaktivera haptik för exakt sökning</string>
<string name="revanced_disable_haptic_feedback_precise_seeking_summary_on">Haptik för exakt sökning är inaktiverad</string>
<string name="revanced_disable_haptic_feedback_precise_seeking_summary_off">Haptik för exakt sökning är aktiverad</string>
<string name="revanced_disable_haptic_feedback_seek_undo_title">Inaktivera ångra-sökning haptik</string>
<string name="revanced_disable_haptic_feedback_seek_undo_summary_on">Ångra-sökning haptik är inaktiverad</string>
<string name="revanced_disable_haptic_feedback_seek_undo_summary_off">Ångra-sökning haptik är aktiverad</string>
<string name="revanced_disable_haptic_feedback_seek_undo_title">Inaktivera haptik för att ångra sökning</string>
<string name="revanced_disable_haptic_feedback_seek_undo_summary_on">Haptik för att ångra sökning är inaktiverad</string>
<string name="revanced_disable_haptic_feedback_seek_undo_summary_off">Haptik för att ångra sökning är aktiverad</string>
<string name="revanced_disable_haptic_feedback_zoom_title">Inaktivera zoomhaptik</string>
<string name="revanced_disable_haptic_feedback_zoom_summary_on">Zoomhaptik är inaktiverad</string>
<string name="revanced_disable_haptic_feedback_zoom_summary_off">Zoomhaptik är aktiverad</string>
@@ -1407,9 +1407,9 @@ Att aktivera detta kan låsa upp högre videokvalitet"</string>
<string name="microg_offline_account_login_error">Om du nyligen har ändrat dina inloggningsuppgifter, avinstallera och installera om MicroG.</string>
</patch>
<patch id="misc.links.bypassURLRedirectsPatch">
<string name="revanced_bypass_url_redirects_title">Bypass URL omdirigerar</string>
<string name="revanced_bypass_url_redirects_summary_on">URL-omdirigeringar förbigås</string>
<string name="revanced_bypass_url_redirects_summary_off">URL-omdirigeringar förbigås inte</string>
<string name="revanced_bypass_url_redirects_title">Hoppa över webbadressomdirigeringar</string>
<string name="revanced_bypass_url_redirects_summary_on">Webbadressomdirigeringar hoppas över</string>
<string name="revanced_bypass_url_redirects_summary_off">Webbadressomdirigeringar hoppas inte över</string>
</patch>
<patch id="misc.links.openLinksExternallyPatch">
<string name="revanced_external_browser_title">Öppna länkar i webbläsaren</string>
@@ -1422,7 +1422,7 @@ Att aktivera detta kan låsa upp högre videokvalitet"</string>
<string name="revanced_remove_tracking_query_parameter_summary_off">Spårnings frågeparameter har inte tagits bort från länkar</string>
</patch>
<patch id="video.audio.forceOriginalAudioPatch">
<string name="revanced_force_original_audio_title">Forcera originalljudspråk</string>
<string name="revanced_force_original_audio_title">Tvinga fram ursprungligt ljudspråk</string>
<string name="revanced_force_original_audio_summary_on">Använder originalljudspråk</string>
<string name="revanced_force_original_audio_summary_off">Använder standardljud</string>
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
@@ -1492,16 +1492,16 @@ Att aktivera detta kan låsa upp högre videokvalitet"</string>
<string name="revanced_slide_to_seek_summary_off">Dra för att söka är inaktiverat</string>
</patch>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string name="revanced_spoof_video_streams_screen_title">Spoof videoströmmar</string>
<string name="revanced_spoof_video_streams_screen_summary">Spoof klientens videoströmmar för att förhindra uppspelningsproblem</string>
<string name="revanced_spoof_video_streams_title">Spoof videoströmmar</string>
<string name="revanced_spoof_video_streams_screen_title">Förfalska videoströmmar</string>
<string name="revanced_spoof_video_streams_screen_summary">Förfalska klientens videoströmmar för att förhindra uppspelningsproblem</string>
<string name="revanced_spoof_video_streams_title">Förfalska videoströmmar</string>
<string name="revanced_spoof_video_streams_summary_on">Videoströmmar är förfalskade</string>
<string name="revanced_spoof_video_streams_summary_off">"Videoströmmar förfalskas inte
Videouppspelning kanske inte fungerar"</string>
<string name="revanced_spoof_video_streams_user_dialog_message">Att stänga av den här inställningen kan orsaka videouppspelningsproblem.</string>
<string name="revanced_spoof_video_streams_user_dialog_message">Om du stänger av den här inställningen kan det leda till problem med videouppspelning.</string>
<string name="revanced_spoof_video_streams_client_type_title">Standardklient</string>
<string name="revanced_spoof_video_streams_ios_force_avc_title">Tvinga iOS AVC (H.264)</string>
<string name="revanced_spoof_video_streams_ios_force_avc_title">Tvinga fram iOS AVC (H.264)</string>
<string name="revanced_spoof_video_streams_ios_force_avc_summary_on">Videokodek tvingas till AVC (H.264)</string>
<string name="revanced_spoof_video_streams_ios_force_avc_summary_off">Videokodeken bestäms automatiskt</string>
<string name="revanced_spoof_video_streams_ios_force_avc_user_dialog_message">"Om du aktiverar detta kan det förbättra batteritiden och åtgärda hackig uppspelning.
@@ -1509,14 +1509,14 @@ Videouppspelning kanske inte fungerar"</string>
AVC har en maximal upplösning på 1080p, Opus-ljudkodek är inte tillgängligt och videouppspelning använder mer internetdata än VP9 eller AV1."</string>
<string name="revanced_spoof_video_streams_about_ios_tv_title">Biverkningar av iOS-förfalskning</string>
<string name="revanced_spoof_video_streams_about_ios_tv_summary">"• Filmer eller betalvideor kanske inte spelas upp
• Jämn volym är inte tillgängligt
• Jämn volym är inte tillgänglig
• Videor slutar 1 sekund för tidigt"</string>
<string name="revanced_spoof_video_streams_about_android_title">Biverkningar av Android-förfalskning</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• Ljudspårsmenyn saknas
Stabilt ljud är inte tillgängligt
• Tvinga originalspråk är inte tillgängligt"</string>
<string name="revanced_spoof_video_streams_about_no_av1">• Ingen AV1-videocodec</string>
<string name="revanced_spoof_video_streams_about_kids_videos">Barnvideor kanske inte spelas upp när du är utloggad eller i inkognitoläge</string>
Jämn volym är inte tillgänglig
• Tvinga fram ursprungligt ljud är inte tillgängligt"</string>
<string name="revanced_spoof_video_streams_about_no_av1">• Ingen AV1-videokodek</string>
<string name="revanced_spoof_video_streams_about_kids_videos">Videor för barn kanske inte spelas upp när du är utloggad eller i inkognitoläge</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Visa i Statistik för nördar</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Klienttypen visas i Statistik för nördar</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Klienten är dold i Statistik för nördar</string>

View File

@@ -516,6 +516,11 @@ This feature is only available for older devices"</string>
<string name="revanced_remove_viewer_discretion_dialog_summary_off">Dialog will be shown</string>
<string name="revanced_remove_viewer_discretion_dialog_user_dialog_message">This does not bypass the age restriction. It just accepts it automatically.</string>
</patch>
<patch id="interaction.doubletap.disableChapterSkipDoubleTapPatch">
<string name="revanced_disable_chapter_skip_double_tap_title">Disable double tap chapter skip</string>
<string name="revanced_disable_chapter_skip_double_tap_summary_on">Double tap can never trigger a skip to the next/previous chapter</string>
<string name="revanced_disable_chapter_skip_double_tap_summary_off">Double tap can occasionally trigger a skip to the next/previous chapter</string>
</patch>
<patch id="interaction.downloads.downloadsResourcePatch">
<string name="revanced_external_downloader_screen_title">External downloads</string>
<string name="revanced_external_downloader_screen_summary">Settings for using an external downloader</string>