Compare commits
15 Commits
feat/unive
...
feat/music
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc540ddab2 | ||
|
|
0900686564 | ||
|
|
470feb6967 | ||
|
|
6a44e75203 | ||
|
|
0af156f189 | ||
|
|
fe4d855dc7 | ||
|
|
030093e913 | ||
|
|
7df20df2de | ||
|
|
8281cf6a3e | ||
|
|
3ce5ceeef7 | ||
|
|
4e682b7a3a | ||
|
|
124698688d | ||
|
|
38a4bad5b8 | ||
|
|
c2da147046 | ||
|
|
6359f725bf |
42
CHANGELOG.md
@@ -1,3 +1,45 @@
|
||||
## [5.2.4-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.2...v5.2.4-dev.3) (2024-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Spoof video streams:** Enable opus codec by updating iOS client version ([#4063](https://github.com/ReVanced/revanced-patches/issues/4063)) ([0af156f](https://github.com/ReVanced/revanced-patches/commit/0af156f18972c5f089af4bb69824968d2a47d18f))
|
||||
|
||||
## [5.2.4-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.1...v5.2.4-dev.2) (2024-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Sync for Reddit:** Fix patches by using correct extension name ([030093e](https://github.com/ReVanced/revanced-patches/commit/030093e913aab3fab43935eedbaeba0f6c0491bb))
|
||||
|
||||
## [5.2.4-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.3...v5.2.4-dev.1) (2024-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Twitter:** Merge correct extension by depending on correct extension patch ([8281cf6](https://github.com/ReVanced/revanced-patches/commit/8281cf6a3eead8cc25a277371e0b0ab2be982497))
|
||||
|
||||
## [5.2.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3) (2024-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b))
|
||||
|
||||
## [5.2.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3-dev.1) (2024-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b))
|
||||
|
||||
## [5.2.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2) (2024-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([4017185](https://github.com/ReVanced/revanced-patches/commit/4017185e760c0569e6644b94bbe66a84fa245b4b))
|
||||
|
||||
## [5.2.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2-dev.1) (2024-12-05)
|
||||
|
||||
|
||||
|
||||
4
extensions/music/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
1
extensions/music/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.revanced.extension.music.announcements;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import app.revanced.extension.shared.announcements.BaseAnnouncementsPatch;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AnnouncementsPatch extends BaseAnnouncementsPatch {
|
||||
private static final AnnouncementsPatch INSTANCE = new AnnouncementsPatch();
|
||||
|
||||
private AnnouncementsPatch() {
|
||||
super("music");
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void showAnnouncement(final Activity context) {
|
||||
INSTANCE._showAnnouncement(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package app.revanced.extension.shared.announcements;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.announcements.requests.AnnouncementsRoutes;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
public abstract class BaseAnnouncementsPatch {
|
||||
private final AnnouncementsRoutes announcementsRoutes;
|
||||
|
||||
public BaseAnnouncementsPatch(String tag) {
|
||||
this.announcementsRoutes = new AnnouncementsRoutes(tag);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private boolean isLatestAlready() throws IOException {
|
||||
HttpURLConnection connection =
|
||||
announcementsRoutes.getAnnouncementsConnectionFromRoute(announcementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS);
|
||||
|
||||
Logger.printDebug(() -> "Get latest announcement IDs route connection url: " + connection.getURL());
|
||||
|
||||
try {
|
||||
// Do not show the announcement if the request failed.
|
||||
if (connection.getResponseCode() != 200) {
|
||||
if (BaseSettings.ANNOUNCEMENT_LAST_ID.isSetToDefault())
|
||||
return true;
|
||||
|
||||
BaseSettings.ANNOUNCEMENT_LAST_ID.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_announcements_connection_failed"));
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Logger.printException(() -> "Could not connect to announcements provider", ex);
|
||||
return true;
|
||||
}
|
||||
|
||||
var jsonString = Requester.parseStringAndDisconnect(connection);
|
||||
|
||||
// Parse the ID. Fall-back to raw string if it fails.
|
||||
int id = BaseSettings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||
try {
|
||||
final var announcementIds = new JSONArray(jsonString);
|
||||
id = announcementIds.getJSONObject(0).getInt("id");
|
||||
|
||||
} catch (Throwable ex) {
|
||||
Logger.printException(() -> "Failed to parse announcement IDs", ex);
|
||||
}
|
||||
|
||||
// Do not show the announcement, if the last announcement id is the same as the current one.
|
||||
return BaseSettings.ANNOUNCEMENT_LAST_ID.get() == id;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public void _showAnnouncement(final Activity context) {
|
||||
if (!BaseSettings.ANNOUNCEMENTS.get()) return;
|
||||
|
||||
// Check if there is internet connection
|
||||
if (!Utils.isNetworkConnected()) return;
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
if (isLatestAlready()) return;
|
||||
|
||||
HttpURLConnection connection = announcementsRoutes
|
||||
.getAnnouncementsConnectionFromRoute(announcementsRoutes.GET_LATEST_ANNOUNCEMENTS);
|
||||
|
||||
Logger.printDebug(() -> "Get latest announcements route connection url: " + connection.getURL());
|
||||
|
||||
var jsonString = Requester.parseStringAndDisconnect(connection);
|
||||
|
||||
// Parse the announcement. Fall-back to raw string if it fails.
|
||||
int id = BaseSettings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||
String title;
|
||||
String message;
|
||||
LocalDateTime archivedAt = LocalDateTime.MAX;
|
||||
Level level = Level.INFO;
|
||||
try {
|
||||
final var announcement = new JSONArray(jsonString).getJSONObject(0);
|
||||
|
||||
id = announcement.getInt("id");
|
||||
title = announcement.getString("title");
|
||||
message = announcement.getString("content");
|
||||
if (!announcement.isNull("archived_at")) {
|
||||
archivedAt = LocalDateTime.parse(announcement.getString("archived_at"));
|
||||
}
|
||||
if (!announcement.isNull("level")) {
|
||||
level = Level.fromInt(announcement.getInt("level"));
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Logger.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
|
||||
|
||||
title = "Announcement";
|
||||
message = jsonString;
|
||||
}
|
||||
|
||||
// If the announcement is archived, do not show it.
|
||||
if (archivedAt.isBefore(LocalDateTime.now())) {
|
||||
BaseSettings.ANNOUNCEMENT_LAST_ID.save(id);
|
||||
return;
|
||||
}
|
||||
|
||||
int finalId = id;
|
||||
final var finalTitle = title;
|
||||
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
|
||||
final Level finalLevel = level;
|
||||
|
||||
Utils.runOnMainThread(() -> {
|
||||
// Show the announcement.
|
||||
var alert = new AlertDialog.Builder(context)
|
||||
.setTitle(finalTitle)
|
||||
.setMessage(finalMessage)
|
||||
.setIcon(finalLevel.icon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
BaseSettings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
|
||||
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
|
||||
// Make links clickable.
|
||||
((TextView) dialog.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
});
|
||||
} catch (Exception e) {
|
||||
final var message = "Failed to get announcement";
|
||||
|
||||
Logger.printException(() -> message, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Use better icons.
|
||||
private enum Level {
|
||||
INFO(android.R.drawable.ic_dialog_info),
|
||||
WARNING(android.R.drawable.ic_dialog_alert),
|
||||
SEVERE(android.R.drawable.ic_dialog_alert);
|
||||
|
||||
public final int icon;
|
||||
|
||||
Level(int icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public static Level fromInt(int value) {
|
||||
return values()[Math.min(value, values().length - 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.extension.shared.announcements.requests;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
public class AnnouncementsRoutes {
|
||||
public final Route GET_LATEST_ANNOUNCEMENTS;
|
||||
public final Route GET_LATEST_ANNOUNCEMENT_IDS;
|
||||
|
||||
public AnnouncementsRoutes(String tag) {
|
||||
this.GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=" + tag);
|
||||
this.GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/ids?tag=" + tag);
|
||||
}
|
||||
|
||||
public HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException {
|
||||
return Requester.getConnectionFromRoute("https://api.revanced.app/v4", route, params);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,7 @@ public class BaseSettings {
|
||||
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
|
||||
|
||||
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
|
||||
|
||||
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
|
||||
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
|
||||
}
|
||||
|
||||
@@ -1,172 +1,20 @@
|
||||
package app.revanced.extension.youtube.patches.announcements;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
|
||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.announcements.BaseAnnouncementsPatch;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class AnnouncementsPatch {
|
||||
public class AnnouncementsPatch extends BaseAnnouncementsPatch {
|
||||
private static final AnnouncementsPatch INSTANCE = new AnnouncementsPatch();
|
||||
|
||||
private AnnouncementsPatch() {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private static boolean isLatestAlready() throws IOException {
|
||||
HttpURLConnection connection =
|
||||
AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT_IDS);
|
||||
|
||||
Logger.printDebug(() -> "Get latest announcement IDs route connection url: " + connection.getURL());
|
||||
|
||||
try {
|
||||
// Do not show the announcement if the request failed.
|
||||
if (connection.getResponseCode() != 200) {
|
||||
if (Settings.ANNOUNCEMENT_LAST_ID.isSetToDefault())
|
||||
return true;
|
||||
|
||||
Settings.ANNOUNCEMENT_LAST_ID.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_announcements_connection_failed"));
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Logger.printException(() -> "Could not connect to announcements provider", ex);
|
||||
return true;
|
||||
}
|
||||
|
||||
var jsonString = Requester.parseStringAndDisconnect(connection);
|
||||
|
||||
// Parse the ID. Fall-back to raw string if it fails.
|
||||
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||
try {
|
||||
final var announcementIds = new JSONArray(jsonString);
|
||||
id = announcementIds.getJSONObject(0).getInt("id");
|
||||
|
||||
} catch (Throwable ex) {
|
||||
Logger.printException(() -> "Failed to parse announcement IDs", ex);
|
||||
}
|
||||
|
||||
// Do not show the announcement, if the last announcement id is the same as the current one.
|
||||
return Settings.ANNOUNCEMENT_LAST_ID.get() == id;
|
||||
super("youtube");
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void showAnnouncement(final Activity context) {
|
||||
if (!Settings.ANNOUNCEMENTS.get()) return;
|
||||
|
||||
// Check if there is internet connection
|
||||
if (!Utils.isNetworkConnected()) return;
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
if (isLatestAlready()) return;
|
||||
|
||||
HttpURLConnection connection = AnnouncementsRoutes
|
||||
.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENTS);
|
||||
|
||||
Logger.printDebug(() -> "Get latest announcements route connection url: " + connection.getURL());
|
||||
|
||||
var jsonString = Requester.parseStringAndDisconnect(connection);
|
||||
|
||||
// Parse the announcement. Fall-back to raw string if it fails.
|
||||
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||
String title;
|
||||
String message;
|
||||
LocalDateTime archivedAt = LocalDateTime.MAX;
|
||||
Level level = Level.INFO;
|
||||
try {
|
||||
final var announcement = new JSONArray(jsonString).getJSONObject(0);
|
||||
|
||||
id = announcement.getInt("id");
|
||||
title = announcement.getString("title");
|
||||
message = announcement.getString("content");
|
||||
if (!announcement.isNull("archived_at")) {
|
||||
archivedAt = LocalDateTime.parse(announcement.getString("archived_at"));
|
||||
}
|
||||
if (!announcement.isNull("level")) {
|
||||
level = Level.fromInt(announcement.getInt("level"));
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Logger.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
|
||||
|
||||
title = "Announcement";
|
||||
message = jsonString;
|
||||
}
|
||||
|
||||
// If the announcement is archived, do not show it.
|
||||
if (archivedAt.isBefore(LocalDateTime.now())) {
|
||||
Settings.ANNOUNCEMENT_LAST_ID.save(id);
|
||||
return;
|
||||
}
|
||||
|
||||
int finalId = id;
|
||||
final var finalTitle = title;
|
||||
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
|
||||
final Level finalLevel = level;
|
||||
|
||||
Utils.runOnMainThread(() -> {
|
||||
// Show the announcement.
|
||||
var alert = new AlertDialog.Builder(context)
|
||||
.setTitle(finalTitle)
|
||||
.setMessage(finalMessage)
|
||||
.setIcon(finalLevel.icon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
|
||||
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
|
||||
// Make links clickable.
|
||||
((TextView) dialog.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
});
|
||||
} catch (Exception e) {
|
||||
final var message = "Failed to get announcement";
|
||||
|
||||
Logger.printException(() -> message, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Use better icons.
|
||||
private enum Level {
|
||||
INFO(android.R.drawable.ic_dialog_info),
|
||||
WARNING(android.R.drawable.ic_dialog_alert),
|
||||
SEVERE(android.R.drawable.ic_dialog_alert);
|
||||
|
||||
public final int icon;
|
||||
|
||||
Level(int icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public static Level fromInt(int value) {
|
||||
return values()[Math.min(value, values().length - 1)];
|
||||
}
|
||||
INSTANCE._showAnnouncement(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches.announcements.requests;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
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");
|
||||
|
||||
private AnnouncementsRoutes() {
|
||||
}
|
||||
|
||||
public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException {
|
||||
return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
public enum ClientType {
|
||||
// Specific purpose for age restricted, or private videos, because the iOS client is not logged in.
|
||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||
ANDROID_VR(28,
|
||||
"Quest 3",
|
||||
"12",
|
||||
@@ -19,7 +20,6 @@ public enum ClientType {
|
||||
true
|
||||
),
|
||||
// Specific for kids videos.
|
||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||
IOS(5,
|
||||
// iPhone 15 supports AV1 hardware decoding.
|
||||
// Only use if this Android device also has hardware decoding.
|
||||
@@ -31,12 +31,12 @@ public enum ClientType {
|
||||
? "17.5.1.21F90"
|
||||
: "13.7.17H35",
|
||||
allowVP9()
|
||||
? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
|
||||
: "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
|
||||
? "com.google.ios.youtube/19.47.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
|
||||
: "com.google.ios.youtube/19.47.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
|
||||
null,
|
||||
// Version number should be a valid iOS release.
|
||||
// https://www.ipa4fun.com/history/185230
|
||||
"19.10.7",
|
||||
"19.47.7",
|
||||
"IOS",
|
||||
false
|
||||
);
|
||||
|
||||
@@ -271,12 +271,10 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
|
||||
"revanced_spoof_device_dimensions_user_dialog_message");
|
||||
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
|
||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true,"revanced_spoof_video_streams_user_dialog_message");
|
||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
|
||||
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofVideoStreamsPatch.ForceiOSAVCAvailability());
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
|
||||
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
||||
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
||||
org.gradle.parallel = true
|
||||
android.useAndroidX = true
|
||||
kotlin.code.style = official
|
||||
version = 5.2.2-dev.1
|
||||
version = 5.2.4-dev.3
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
public final class app/revanced/patches/all/layout/branding/IconPatchKt {
|
||||
public static final fun getChangeIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatchKt {
|
||||
public static final fun getExportAllActivitiesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/announcements/AnnouncementsPatchKt {
|
||||
public static final fun announcementsPatch (Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static synthetic fun announcementsPatch$default (Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatchKt {
|
||||
public static final fun baseSpoofBuildInfoPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -308,6 +309,10 @@ public final class app/revanced/patches/music/misc/androidauto/BypassCertificate
|
||||
public static final fun getBypassCertificateChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/music/misc/announcements/AnnouncementsPatchKt {
|
||||
public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatchKt {
|
||||
public static final fun getBackgroundPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package app.revanced.patches.all.layout.branding
|
||||
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.util.getNode
|
||||
import app.revanced.util.inputStreamFromBundledResource
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
private const val FULL_ICON = 0
|
||||
private const val ROUND_ICON = 1
|
||||
private const val BACKGROUND_ICON = 2
|
||||
private const val FOREGROUND_ICON = 3
|
||||
private const val MONOCHROME_ICON = 4
|
||||
|
||||
val changeIconPatch = resourcePatch(
|
||||
name = "Change icon",
|
||||
description = "Changes the app icon to a custom icon. By default, the ReVanced icon is used.",
|
||||
use = false,
|
||||
) {
|
||||
val revancedIconOptionValue = emptyList<String>() // Empty list == ReVanced icon.
|
||||
|
||||
val pixelDensities = setOf(
|
||||
"mdpi",
|
||||
"hdpi",
|
||||
"xhdpi",
|
||||
"xxhdpi",
|
||||
"xxxhdpi",
|
||||
)
|
||||
|
||||
val iconOptions = pixelDensities.associateWith { pixelDensity ->
|
||||
stringsOption(
|
||||
key = "${pixelDensity}Icons",
|
||||
default = revancedIconOptionValue,
|
||||
values = mapOf("ReVanced logo" to revancedIconOptionValue),
|
||||
title = "Icons (Pixel density: $pixelDensity)",
|
||||
description = buildString {
|
||||
appendLine("Provide paths to the following icons for pixel density $pixelDensity (PNG, JPG, WEBP, or vector drawable XML):")
|
||||
appendLine("1. Launcher icon (required)")
|
||||
appendLine("2. Round icon (optional, Android 7+)")
|
||||
appendLine("\nYou can use adaptive icons (Android 8+) by providing the following additional icons:")
|
||||
appendLine("\n3. Background icon (optional)")
|
||||
appendLine("4. Foreground icon (optional)")
|
||||
appendLine("5. Monochrome icon (optional, Android 13+")
|
||||
appendLine("\nIcons must be provided in the same order as listed above. Missing optional icons can be skipped by leaving the field empty.")
|
||||
appendLine("\nYou can create custom icon sets at https://icon.kitchen.")
|
||||
},
|
||||
required = true,
|
||||
)
|
||||
}
|
||||
|
||||
execute {
|
||||
val firstPixelDensity = pixelDensities.first()
|
||||
|
||||
fun patchIcon(
|
||||
getIcon: (String, Int) -> String?,
|
||||
readIcon: (String) -> InputStream,
|
||||
) {
|
||||
// Any density, as the user should provide the icons for all densities.
|
||||
|
||||
// region Change the app icon in the AndroidManifest.xml file.
|
||||
|
||||
// If a round icon is provided, set the android:roundIcon attribute.
|
||||
document("AndroidManifest.xml").use {
|
||||
it.getNode("application").attributes.apply {
|
||||
getNamedItem("android:icon").textContent = "@mipmap/ic_launcher"
|
||||
|
||||
val roundIcon = getIcon(firstPixelDensity, ROUND_ICON)
|
||||
if (roundIcon?.isNotEmpty() == true) {
|
||||
val roundIconAttribute = getNamedItem("android:roundIcon")
|
||||
?: setNamedItem(it.createAttribute("android:roundIcon"))
|
||||
roundIconAttribute.textContent = "@mipmap/ic_launcher_round"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Change the app icon for each pixel density.
|
||||
|
||||
val hasAdaptiveIcon = getIcon(firstPixelDensity, BACKGROUND_ICON)
|
||||
|
||||
if (hasAdaptiveIcon?.isNotEmpty() == true) {
|
||||
val monochromeIconXmlString = if (getIcon(firstPixelDensity, MONOCHROME_ICON)?.isNotEmpty() == true) {
|
||||
"<monochrome android:drawable=\"@drawable/ic_launcher_monochrome\"/>"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// If an adaptive icon is provided, add the adaptive icon XML file to the res/mipmap-anydpi directory.
|
||||
get("res/mipmap-anydpi/ic_launcher.xml").writeText(
|
||||
"""
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
$monochromeIconXmlString
|
||||
</adaptive-icon>
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
pixelDensities.forEach { pixelDensity ->
|
||||
val icon = getIcon(pixelDensity, FULL_ICON)!!
|
||||
// Safe call (?.) is used because the user may just provide the full icon and skip the other optional icons.
|
||||
val roundIcon = getIcon(pixelDensity, ROUND_ICON)
|
||||
val backgroundIcon = getIcon(pixelDensity, BACKGROUND_ICON)
|
||||
val foregroundIcon = getIcon(pixelDensity, FOREGROUND_ICON)
|
||||
val monochromeIcon = getIcon(pixelDensity, MONOCHROME_ICON)
|
||||
|
||||
infix fun String?.to(target: String) {
|
||||
if (isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
Files.copy(
|
||||
readIcon(this),
|
||||
get("res/$target").toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
}
|
||||
|
||||
// Copy the icons to the mipmap directory.
|
||||
icon to "mipmap-$pixelDensity/ic_launcher.png"
|
||||
roundIcon to "mipmap-$pixelDensity/ic_launcher_round.png"
|
||||
backgroundIcon to "mipmap-$pixelDensity/ic_launcher_background.png"
|
||||
foregroundIcon to "mipmap-$pixelDensity/ic_launcher_foreground.png"
|
||||
monochromeIcon to "drawable-$pixelDensity/ic_launcher_monochrome.png"
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
if (iconOptions[firstPixelDensity]!!.value === revancedIconOptionValue) {
|
||||
patchIcon({ pixelDensity, iconIndex ->
|
||||
when (iconIndex) {
|
||||
FULL_ICON -> "mipmap-$pixelDensity/revanced-icon"
|
||||
ROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-round"
|
||||
BACKGROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-background"
|
||||
FOREGROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-foreground"
|
||||
MONOCHROME_ICON -> "drawable-$pixelDensity/revanced-icon-monochrome"
|
||||
else -> throw IllegalArgumentException("Invalid icon index: $iconIndex")
|
||||
}
|
||||
}) { icon ->
|
||||
inputStreamFromBundledResource("change-icon", "$icon.png")!!
|
||||
}
|
||||
} else {
|
||||
patchIcon({ pixelDensity, iconIndex ->
|
||||
iconOptions[pixelDensity]?.value?.get(iconIndex)
|
||||
}) { icon ->
|
||||
get(icon).inputStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.revanced.patches.all.misc.announcements
|
||||
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
|
||||
fun announcementsPatch(
|
||||
mainActivityOnCreateFingerprint: Fingerprint,
|
||||
extensionPatch: Patch<*>,
|
||||
extensionClassDescriptor: String,
|
||||
block: BytecodePatchBuilder.() -> Unit = {},
|
||||
executeBlock: BytecodePatchContext.() -> Unit = {},
|
||||
) = bytecodePatch(
|
||||
name = "Announcements",
|
||||
description = "Shows announcements from ReVanced on app startup.",
|
||||
) {
|
||||
block()
|
||||
|
||||
dependsOn(
|
||||
extensionPatch,
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
execute {
|
||||
addResources("shared", "misc.announcements.announcementsPatch")
|
||||
|
||||
mainActivityOnCreateFingerprint.method.addInstructions(
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 }, $extensionClassDescriptor->showAnnouncement(Landroid/app/Activity;)V",
|
||||
)
|
||||
|
||||
executeBlock()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.revanced.patches.music.layout.compactheader
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val constructCategoryBarFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||
@@ -15,6 +15,6 @@ internal val constructCategoryBarFingerprint = fingerprint {
|
||||
Opcode.MOVE_RESULT_OBJECT,
|
||||
Opcode.IPUT_OBJECT,
|
||||
Opcode.CONST,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.INVOKE_VIRTUAL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.revanced.patches.music.misc.announcements
|
||||
|
||||
import app.revanced.patches.all.misc.announcements.announcementsPatch
|
||||
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.shared.musicActivityOnCreateFingerprint
|
||||
|
||||
val announcementsPatch = announcementsPatch(
|
||||
musicActivityOnCreateFingerprint,
|
||||
sharedExtensionPatch,
|
||||
"Lapp/revanced/extension/music/announcements/AnnouncementsPatch;",
|
||||
{
|
||||
compatibleWith("com.google.android.apps.music")
|
||||
},
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import app.revanced.patcher.patch.Option
|
||||
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
|
||||
import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME
|
||||
import app.revanced.patches.music.shared.musicActivityOnCreateFingerprint
|
||||
import app.revanced.patches.shared.castContextFetchFingerprint
|
||||
import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch
|
||||
import app.revanced.patches.shared.primeMethodFingerprint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.patches.music.misc.gms
|
||||
package app.revanced.patches.music.shared
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
@@ -3,4 +3,4 @@ package app.revanced.patches.reddit.customclients.sync.syncforreddit.extension
|
||||
import app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.hooks.initHook
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
|
||||
val sharedExtensionPatch = sharedExtensionPatch("sync", initHook)
|
||||
val sharedExtensionPatch = sharedExtensionPatch("syncforreddit", initHook)
|
||||
|
||||
@@ -5,7 +5,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.reddit.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.twitter.misc.extension.sharedExtensionPatch
|
||||
import java.io.InvalidClassException
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,51 +1,27 @@
|
||||
package app.revanced.patches.youtube.misc.announcements
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.all.misc.announcements.announcementsPatch
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/announcements/AnnouncementsPatch;"
|
||||
val announcementsPatch = announcementsPatch(
|
||||
mainActivityOnCreateFingerprint,
|
||||
sharedExtensionPatch,
|
||||
"Lapp/revanced/extension/youtube/patches/announcements/AnnouncementsPatch;",
|
||||
{
|
||||
dependsOn(settingsPatch)
|
||||
|
||||
val announcementsPatch = bytecodePatch(
|
||||
name = "Announcements",
|
||||
description = "Adds an option to show announcements from ReVanced on app startup.",
|
||||
) {
|
||||
dependsOn(
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
"18.38.44",
|
||||
"18.49.37",
|
||||
"19.16.39",
|
||||
"19.25.37",
|
||||
"19.34.42",
|
||||
"19.43.41",
|
||||
"19.45.38",
|
||||
"19.46.42",
|
||||
),
|
||||
)
|
||||
|
||||
execute {
|
||||
compatibleWith("com.google.android.youtube")
|
||||
},
|
||||
{
|
||||
addResources("youtube", "misc.announcements.announcementsPatch")
|
||||
|
||||
PreferenceScreen.MISC.addPreferences(
|
||||
SwitchPreference("revanced_announcements"),
|
||||
)
|
||||
|
||||
mainActivityOnCreateFingerprint.method.addInstructions(
|
||||
// Insert index must be greater than the insert index used by GmsCoreSupport,
|
||||
// as both patch the same method and GmsCore check should be first.
|
||||
1,
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -23,14 +23,16 @@ fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(i
|
||||
* Returns a sequence for all child nodes.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence<Element>
|
||||
fun Node.childElementsSequence() =
|
||||
this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence<Element>
|
||||
|
||||
/**
|
||||
* Performs the given [action] on each child element.
|
||||
*/
|
||||
inline fun Node.forEachChildElement(action: (Element) -> Unit) = childElementsSequence().forEach {
|
||||
action(it)
|
||||
}
|
||||
inline fun Node.forEachChildElement(action: (Element) -> Unit) =
|
||||
childElementsSequence().forEach {
|
||||
action(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverse the DOM tree starting from the given root node.
|
||||
@@ -139,8 +141,7 @@ internal fun Node.addResource(
|
||||
appendChild(resource.serialize(ownerDocument, resourceCallback))
|
||||
}
|
||||
|
||||
internal fun org.w3c.dom.Document.getNode(tagName: String) = getElementsByTagName(tagName).item(0)
|
||||
internal fun Node.getNode(tagName: String) = childNodes.asSequence().find { it.nodeName == tagName }
|
||||
internal fun org.w3c.dom.Document.getNode(tagName: String) = this.getElementsByTagName(tagName).item(0)
|
||||
|
||||
internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? {
|
||||
for (i in 0 until length) {
|
||||
@@ -163,7 +164,8 @@ internal fun NodeList.findElementByAttributeValue(attributeName: String, value:
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value")
|
||||
internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) =
|
||||
findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value")
|
||||
|
||||
internal fun Element.copyAttributesFrom(oldContainer: Element) {
|
||||
// Copy attributes from the old element to the new element
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |