feat(YouTube - Change header): Add in-app setting to change the app header (#5346)

This commit is contained in:
LisoUseInAIKyrios
2025-07-05 12:02:58 +04:00
committed by GitHub
parent 04caa66662
commit 4e742075f3
46 changed files with 282 additions and 138 deletions

View File

@@ -0,0 +1,50 @@
package app.revanced.extension.youtube.patches;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ChangeHeaderPatch {
public enum HeaderLogo {
DEFAULT(null),
REGULAR("ytWordmarkHeader"),
PREMIUM("ytPremiumWordmarkHeader"),
REVANCED("revanced_header_logo"),
REVANCED_MINIMAL("revanced_header_logo_minimal"),
CUSTOM("custom_header");
@Nullable
private final String resourceName;
HeaderLogo(@Nullable String resourceName) {
this.resourceName = resourceName;
}
/**
* @return The attribute id of this header logo, or NULL if the logo should not be replaced.
*/
@Nullable
private Integer getAttributeId() {
if (resourceName == null) {
return null;
}
return Utils.getResourceIdentifier(resourceName, "attr");
}
}
@Nullable
private static final Integer headerLogoResource = Settings.HEADER_LOGO.get().getAttributeId();
/**
* Injection point.
*/
public static int getHeaderAttributeId(int original) {
return Objects.requireNonNullElse(headerLogoResource, original);
}
}

View File

@@ -8,6 +8,7 @@ import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.settings.Setting.parentsAll;
import static app.revanced.extension.shared.settings.Setting.parentsAny;
import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor;
import static app.revanced.extension.youtube.patches.ChangeHeaderPatch.HeaderLogo;
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability;
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage;
import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode;
@@ -238,7 +239,8 @@ public class Settings extends BaseSettings {
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
public static final EnumSetting<SplashScreenAnimationStyle> SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true);
public static final EnumSetting<SplashScreenAnimationStyle> SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("revanced_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true);
public static final EnumSetting<HeaderLogo> HEADER_LOGO = new EnumSetting<>("revanced_header_logo", HeaderLogo.DEFAULT, true);
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
"revanced_remove_viewer_discretion_dialog_user_dialog_message");

View File

@@ -1,43 +1,83 @@
package app.revanced.patches.youtube.layout.branding.header
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patcher.util.Document
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.mapping.get
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyResources
import app.revanced.util.findElementByAttributeValueOrThrow
import app.revanced.util.forEachLiteralValueInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import java.io.File
private const val HEADER_FILE_NAME = "yt_wordmark_header"
private const val PREMIUM_HEADER_FILE_NAME = "yt_premium_wordmark_header"
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/ChangeHeaderPatch;"
private const val HEADER_OPTION = "header*"
private const val PREMIUM_HEADER_OPTION = "premium*header"
private const val REVANCED_HEADER_OPTION = "revanced*"
private const val REVANCED_BORDERLESS_HEADER_OPTION = "revanced*borderless"
private val changeHeaderBytecodePatch = bytecodePatch {
dependsOn(resourceMappingPatch)
execute {
arrayOf(
"ytWordmarkHeader",
"ytPremiumWordmarkHeader"
).forEach { resourceName ->
val resourceId = resourceMappings["attr", resourceName]
forEachLiteralValueInstruction(resourceId) { literalIndex ->
val register = getInstruction<OneRegisterInstruction>(literalIndex).registerA
addInstructions(
literalIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getHeaderAttributeId(I)I
move-result v$register
"""
)
}
}
}
}
private val targetResourceDirectoryNames = mapOf(
"xxxhdpi" to "512px x 192px",
"xxhdpi" to "387px x 144px",
"xhdpi" to "258px x 96px",
"hdpi" to "194px x 72px",
"mdpi" to "129px x 48px",
).map { (dpi, dim) ->
"drawable-$dpi" to dim
}.toMap()
"mdpi" to "129px x 48px"
).mapKeys { (dpi, _) -> "drawable-$dpi" }
private val variants = arrayOf("light", "dark")
/**
* Header logos built into this patch.
*/
private val logoResourceNames = arrayOf(
"revanced_header_logo_minimal",
"revanced_header_logo",
)
/**
* Custom header resource/file name.
*/
private const val CUSTOM_HEADER_RESOURCE_NAME = "custom_header"
@Suppress("unused")
val changeHeaderPatch = resourcePatch(
name = "Change header",
description = "Applies a custom header in the top left corner within the app. Defaults to the ReVanced header.",
use = false,
description = "Adds an option to change the header logo in the top left corner of the app.",
) {
dependsOn(versionCheckPatch)
dependsOn(addResourcesPatch, changeHeaderBytecodePatch)
compatibleWith(
"com.google.android.youtube"(
@@ -50,85 +90,46 @@ val changeHeaderPatch = resourcePatch(
)
)
val header by stringOption(
key = "header",
default = REVANCED_BORDERLESS_HEADER_OPTION,
values = mapOf(
"YouTube" to HEADER_OPTION,
"YouTube Premium" to PREMIUM_HEADER_OPTION,
"ReVanced" to REVANCED_HEADER_OPTION,
"ReVanced (borderless logo)" to REVANCED_BORDERLESS_HEADER_OPTION,
),
title = "Header",
val custom by stringOption(
key = "custom",
title = "Custom header logo",
description = """
The header to apply to the app.
If a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device:
Folder with images to use as a custom header logo.
The folder must contain one or more of the following folders, depending on the DPI of the device:
${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }}
Each of the folders must contain all of the following files:
${variants.joinToString("\n") { variant -> "- ${HEADER_FILE_NAME}_$variant.png" }}
${variants.joinToString("\n") { variant -> "- ${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png" }}
The image dimensions must be as follows:
${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")}
""".trimIndentMultiline(),
required = true,
""".trimIndentMultiline()
)
execute {
// The directories to copy the header to.
val targetResourceDirectories = targetResourceDirectoryNames.keys.mapNotNull {
get("res").resolve(it).takeIf(File::exists)
}
// The files to replace in the target directories.
val targetResourceFiles = targetResourceDirectoryNames.keys.map { directoryName ->
ResourceGroup(
directoryName,
*variants.map { variant -> "${HEADER_FILE_NAME}_$variant.png" }.toTypedArray(),
)
}
addResources("youtube", "layout.branding.changeHeaderPatch")
/**
* A function that overwrites both header variants in the target resource directories.
*/
fun overwriteFromTo(from: String, to: String) {
targetResourceDirectories.forEach { directory ->
variants.forEach { variant ->
val fromPath = directory.resolve("${from}_$variant.png")
val toPath = directory.resolve("${to}_$variant.png")
fun getLightDarkFileNames(vararg resourceNames: String): Array<String> =
variants.flatMap { variant ->
resourceNames.map { resource -> "${resource}_$variant.png" }
}.toTypedArray()
fromPath.copyTo(toPath, true)
}
}
}
val logoResourceFileNames = getLightDarkFileNames(*logoResourceNames)
copyResources(
"change-header",
ResourceGroup("drawable-hdpi", *logoResourceFileNames),
ResourceGroup("drawable-mdpi", *logoResourceFileNames),
ResourceGroup("drawable-xhdpi", *logoResourceFileNames),
ResourceGroup("drawable-xxhdpi", *logoResourceFileNames),
ResourceGroup("drawable-xxxhdpi", *logoResourceFileNames),
)
// Functions to overwrite the header to the different variants.
fun toPremium() { overwriteFromTo(PREMIUM_HEADER_FILE_NAME, HEADER_FILE_NAME) }
fun toHeader() { overwriteFromTo(HEADER_FILE_NAME, PREMIUM_HEADER_FILE_NAME) }
fun toReVanced() {
// Copy the ReVanced header to the resource directories.
targetResourceFiles.forEach { copyResources("change-header/revanced", it) }
if (custom != null) {
val sourceFolders = File(custom!!).listFiles { file -> file.isDirectory }
?: throw PatchException("The provided path is not a directory: $custom")
// Overwrite the premium with the custom header as well.
toHeader()
}
fun toReVancedBorderless() {
// Copy the ReVanced borderless header to the resource directories.
targetResourceFiles.forEach {
copyResources(
"change-header/revanced-borderless",
it
)
}
// Overwrite the premium with the custom header as well.
toHeader()
}
fun toCustom() {
val sourceFolders = File(header!!).listFiles { file -> file.isDirectory }
?: throw PatchException("The provided path is not a directory: $header")
val customResourceFileNames = getLightDarkFileNames(CUSTOM_HEADER_RESOURCE_NAME)
var copiedFiles = false
@@ -137,62 +138,87 @@ val changeHeaderPatch = resourcePatch(
val targetDpiFolder = get("res").resolve(dpiSourceFolder.name)
if (!targetDpiFolder.exists()) return@forEach
val imgSourceFiles = dpiSourceFolder.listFiles { file -> file.isFile }!!
imgSourceFiles.forEach { imgSourceFile ->
val customFiles = dpiSourceFolder.listFiles { file ->
file.isFile && file.name in customResourceFileNames
}!!
if (customFiles.size > 0 && customFiles.size != variants.size) {
throw PatchException("Both light/dark mode images " +
"must be specified but only found: " + customFiles.map { it.name })
}
customFiles.forEach { imgSourceFile ->
val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name)
imgSourceFile.copyTo(imgTargetFile, true)
imgSourceFile.copyTo(imgTargetFile)
copiedFiles = true
}
}
if (!copiedFiles) {
throw PatchException("No header files were copied from the provided path: $header.")
throw PatchException("No custom header images found in the provided path: $custom")
}
}
// Logo is replaced using an attribute reference.
document("res/values/attrs.xml").use { document ->
val resources = document.childNodes.item(0)
fun addAttributeReference(logoName: String) {
val item = document.createElement("attr")
item.setAttribute("format", "reference")
item.setAttribute("name", logoName)
resources.appendChild(item)
}
// Overwrite the premium with the custom header as well.
toHeader()
logoResourceNames.forEach { logoName ->
addAttributeReference(logoName)
}
if (custom != null) {
addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME)
}
}
when (header) {
HEADER_OPTION -> toHeader()
PREMIUM_HEADER_OPTION -> toPremium()
REVANCED_HEADER_OPTION -> toReVanced()
REVANCED_BORDERLESS_HEADER_OPTION -> toReVancedBorderless()
else -> toCustom()
}
// Add custom drawables to all styles that use the regular and premium logo.
document("res/values/styles.xml").use { document ->
arrayOf(
"Base.Theme.YouTube.Light" to "light",
"Base.Theme.YouTube.Dark" to "dark",
"CairoLightThemeRingo2Updates" to "light",
"CairoDarkThemeRingo2Updates" to "dark"
).forEach { (style, mode) ->
val styleElement = document.childNodes.findElementByAttributeValueOrThrow(
"name", style
)
// Fix 19.25+ A/B layout with different header icons:
// yt_ringo2_wordmark_header, yt_ringo2_premium_wordmark_header
//
// These images are webp and not png, so overwriting them is not so simple.
// Instead change styles.xml to use the old drawable resources.
if (is_19_25_or_greater) {
document("res/values/styles.xml").use { document ->
val documentChildNodes = document.childNodes
fun addDrawableElement(document: Document, logoName: String, mode: String) {
val item = document.createElement("item")
item.setAttribute("name", logoName)
item.textContent = "@drawable/${logoName}_$mode"
styleElement.appendChild(item)
}
arrayOf(
"CairoLightThemeRingo2Updates" to variants[0],
"CairoDarkThemeRingo2Updates" to variants[1]
).forEach { (styleName, theme) ->
val styleNodes = documentChildNodes.findElementByAttributeValueOrThrow(
"name",
styleName,
).childNodes
logoResourceNames.forEach { logoName ->
addDrawableElement(document, logoName, mode)
}
val drawable = "@drawable/${HEADER_FILE_NAME}_${theme}"
arrayOf(
"ytWordmarkHeader",
"ytPremiumWordmarkHeader"
).forEach { itemName ->
styleNodes.findElementByAttributeValueOrThrow(
"name",
itemName,
).textContent = drawable
}
if (custom != null) {
addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode)
}
}
}
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
if (custom == null) {
ListPreference("revanced_header_logo")
} else {
ListPreference(
key = "revanced_header_logo",
entriesKey = "revanced_header_logo_custom_entries",
entryValuesKey = "revanced_header_logo_custom_entry_values"
)
}
)
}
}

View File

@@ -222,7 +222,7 @@ val themePatch = bytecodePatch(
if (is_19_47_or_greater) {
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
ListPreference("splash_screen_animation_style")
ListPreference("revanced_splash_screen_animation_style")
)
}

View File

@@ -355,7 +355,7 @@ fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Float): Int {
* @see indexOfFirstLiteralInstructionOrThrow
*/
fun Method.indexOfFirstLiteralInstruction(literal: Double) =
indexOfFirstLiteralInstruction(literal.toRawBits().toLong())
indexOfFirstLiteralInstruction(literal.toRawBits())
/**
* Find the index of the first literal instruction with the given double value,
@@ -421,7 +421,7 @@ fun Method.indexOfFirstLiteralInstructionReversedOrThrow(literal: Float): Int {
* @see indexOfFirstLiteralInstructionOrThrow
*/
fun Method.indexOfFirstLiteralInstructionReversed(literal: Double) =
indexOfFirstLiteralInstructionReversed(literal.toRawBits().toLong())
indexOfFirstLiteralInstructionReversed(literal.toRawBits())
/**
* Find the index of the last wide literal instruction with the given double value,
@@ -715,24 +715,50 @@ internal fun MutableMethod.insertLiteralOverride(literal: Long, override: Boolea
}
/**
* Called for _all_ instructions with the given literal value.
* Called for _all_ methods with the given literal value.
* Method indices are iterated from last to first.
*/
fun BytecodePatchContext.forEachLiteralValueInstruction(
literal: Long,
block: MutableMethod.(literalInstructionIndex: Int) -> Unit,
block: MutableMethod.(matchingIndex: Int) -> Unit,
) {
val matchingIndexes = ArrayList<Int>()
classes.forEach { classDef ->
classDef.methods.forEach { method ->
method.implementation?.instructions?.forEachIndexed { index, instruction ->
if (instruction.opcode == CONST &&
(instruction as WideLiteralInstruction).wideLiteral == literal
) {
method.implementation?.instructions?.let { instructions ->
matchingIndexes.clear()
instructions.forEachIndexed { index, instruction ->
if ((instruction as? WideLiteralInstruction)?.wideLiteral == literal) {
matchingIndexes.add(index)
}
}
if (matchingIndexes.isNotEmpty()) {
val mutableMethod = proxy(classDef).mutableClass.findMutableMethodOf(method)
block.invoke(mutableMethod, index)
// FIXME: Until patcher V22 is merged, this workaround is needed
// because if multiple patches modify the same class
// then after modifying the method indexes of immutable classes
// are no longer correct.
matchingIndexes.clear()
mutableMethod.instructions.forEachIndexed { index, instruction ->
if ((instruction as? WideLiteralInstruction)?.wideLiteral == literal) {
matchingIndexes.add(index)
}
}
if (matchingIndexes.isEmpty()) return@forEach
// FIXME Remove code above after V22 merge.
matchingIndexes.asReversed().forEach { index ->
block.invoke(mutableMethod, index)
}
}
}
}
}
}
private const val RETURN_TYPE_MISMATCH = "Mismatch between override type and Method return type"

View File

@@ -190,16 +190,15 @@
(YouTube closes the animation as soon as the feed is loaded),
only the 60fps 1 second styles are exposed in the settings.
Imported settings data can still be manually edited to force the other styles. -->
<string-array name="splash_screen_animation_style_entries">
<item>@string/splash_screen_animation_style_entry_1</item>
<item>@string/splash_screen_animation_style_entry_2</item>
<string-array name="revanced_splash_screen_animation_style_entries">
<item>@string/revanced_splash_screen_animation_style_entry_1</item>
<item>@string/revanced_splash_screen_animation_style_entry_2</item>
</string-array>
<string-array name="splash_screen_animation_style_entry_values">
<string-array name="revanced_splash_screen_animation_style_entry_values">
<item>FPS_60_ONE_SECOND</item>
<item>FPS_60_BLACK_AND_WHITE</item>
</string-array>
</patch>
<patch id="layout.player.fullscreen.exitFullscreenPatch">
<string-array name="revanced_exit_fullscreen_entries">
<item>@string/revanced_exit_fullscreen_entry_1</item>
@@ -270,6 +269,38 @@
<item>MODERN_3</item>
</string-array>
</patch>
<patch id="layout.branding.changeHeaderPatch">
<string-array name="revanced_header_logo_entries">
<item>@string/revanced_header_logo_entry_1</item>
<item>@string/revanced_header_logo_entry_2</item>
<item>@string/revanced_header_logo_entry_3</item>
<item>@string/revanced_header_logo_entry_4</item>
<item>@string/revanced_header_logo_entry_5</item>
</string-array>
<string-array name="revanced_header_logo_entry_values">
<item>DEFAULT</item>
<item>REGULAR</item>
<item>PREMIUM</item>
<item>REVANCED</item>
<item>REVANCED_MINIMAL</item>
</string-array>
<string-array name="revanced_header_logo_custom_entries">
<item>@string/revanced_header_logo_entry_1</item>
<item>@string/revanced_header_logo_entry_2</item>
<item>@string/revanced_header_logo_entry_3</item>
<item>@string/revanced_header_logo_entry_4</item>
<item>@string/revanced_header_logo_entry_5</item>
<item>@string/revanced_header_logo_entry_6</item>
</string-array>
<string-array name="revanced_header_logo_custom_entry_values">
<item>DEFAULT</item>
<item>REGULAR</item>
<item>PREMIUM</item>
<item>REVANCED</item>
<item>REVANCED_MINIMAL</item>
<item>CUSTOM</item>
</string-array>
</patch>
<patch id="layout.startpage.changeStartPagePatch">
<string-array name="revanced_change_start_page_entries">
<item>@string/revanced_change_start_page_entry_default</item>

View File

@@ -1369,9 +1369,9 @@ Swipe to expand or close"</string>
<string name="revanced_gradient_loading_screen_title">Enable gradient loading screen</string>
<string name="revanced_gradient_loading_screen_summary_on">Loading screen will have a gradient background</string>
<string name="revanced_gradient_loading_screen_summary_off">Loading screen will have a solid background</string>
<string name="splash_screen_animation_style_title">Splash screen style</string>
<string name="splash_screen_animation_style_entry_1">Color</string>
<string name="splash_screen_animation_style_entry_2">Black and white</string>
<string name="revanced_splash_screen_animation_style_title">Splash screen style</string>
<string name="revanced_splash_screen_animation_style_entry_1">Color</string>
<string name="revanced_splash_screen_animation_style_entry_2">Black and white</string>
<string name="revanced_seekbar_custom_color_title">Enable custom seekbar color</string>
<string name="revanced_seekbar_custom_color_summary_on">Custom seekbar color is shown</string>
<string name="revanced_seekbar_custom_color_summary_off">Original seekbar color is shown</string>
@@ -1381,6 +1381,15 @@ Swipe to expand or close"</string>
<string name="revanced_seekbar_custom_color_accent_summary">The accent color of the seekbar</string>
<string name="revanced_seekbar_custom_color_invalid">Invalid seekbar color value</string>
</patch>
<patch id="layout.branding.changeHeaderPatch">
<string name="revanced_header_logo_title">Header logo</string>
<string name="revanced_header_logo_entry_1">Default</string>
<string name="revanced_header_logo_entry_2">Regular</string>
<string name="revanced_header_logo_entry_3">Premium</string>
<string name="revanced_header_logo_entry_4">ReVanced</string>
<string name="revanced_header_logo_entry_5">ReVanced minimal</string>
<string name="revanced_header_logo_entry_6">Custom</string>
</patch>
<patch id="layout.thumbnails.bypassImageRegionRestrictionsPatch">
<string name="revanced_bypass_image_region_restrictions_title">Bypass image region restrictions</string>
<string name="revanced_bypass_image_region_restrictions_summary_on">Using image host yt4.ggpht.com</string>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB