mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Merge pull request #8490 from szaboa:dev-2-8435-ssa-color
PiperOrigin-RevId: 354293679
This commit is contained in:
commit
c9fce083f3
@ -5,6 +5,9 @@
|
|||||||
* Extractors:
|
* Extractors:
|
||||||
* Fix Vorbis private codec data parsing in the Matroska extractor
|
* Fix Vorbis private codec data parsing in the Matroska extractor
|
||||||
([#8496](https://github.com/google/ExoPlayer/issues/8496)).
|
([#8496](https://github.com/google/ExoPlayer/issues/8496)).
|
||||||
|
* Text:
|
||||||
|
* Add support for the SSA `primaryColour` style attribute
|
||||||
|
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
|
||||||
|
|
||||||
### 2.13.0 (not yet released - targeted for 2021-02-TBD)
|
### 2.13.0 (not yet released - targeted for 2021-02-TBD)
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_FRACTION;
|
|||||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
|
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
@ -301,7 +303,18 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||||||
SsaStyle.Overrides styleOverrides,
|
SsaStyle.Overrides styleOverrides,
|
||||||
float screenWidth,
|
float screenWidth,
|
||||||
float screenHeight) {
|
float screenHeight) {
|
||||||
Cue.Builder cue = new Cue.Builder().setText(text);
|
SpannableString spannableText = new SpannableString(text);
|
||||||
|
Cue.Builder cue = new Cue.Builder().setText(spannableText);
|
||||||
|
|
||||||
|
if (style != null) {
|
||||||
|
if (style.primaryColor != null) {
|
||||||
|
spannableText.setSpan(
|
||||||
|
new ForegroundColorSpan(style.primaryColor),
|
||||||
|
/* start= */ 0,
|
||||||
|
/* end= */ spannableText.length(),
|
||||||
|
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SsaStyle.SsaAlignment int alignment;
|
@SsaStyle.SsaAlignment int alignment;
|
||||||
if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {
|
if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {
|
||||||
|
@ -17,16 +17,20 @@
|
|||||||
package com.google.android.exoplayer2.text.ssa;
|
package com.google.android.exoplayer2.text.ssa;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;
|
import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
import android.graphics.PointF;
|
import android.graphics.PointF;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
@ -85,15 +89,18 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
@SsaAlignment public final int alignment;
|
@SsaAlignment public final int alignment;
|
||||||
|
@Nullable @ColorInt public final Integer primaryColor;
|
||||||
|
|
||||||
private SsaStyle(String name, @SsaAlignment int alignment) {
|
private SsaStyle(
|
||||||
|
String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.alignment = alignment;
|
this.alignment = alignment;
|
||||||
|
this.primaryColor = primaryColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static SsaStyle fromStyleLine(String styleLine, Format format) {
|
public static SsaStyle fromStyleLine(String styleLine, Format format) {
|
||||||
Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
|
checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
|
||||||
String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ",");
|
String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ",");
|
||||||
if (styleValues.length != format.length) {
|
if (styleValues.length != format.length) {
|
||||||
Log.w(
|
Log.w(
|
||||||
@ -105,7 +112,9 @@ import java.util.regex.Pattern;
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return new SsaStyle(
|
return new SsaStyle(
|
||||||
styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex]));
|
styleValues[format.nameIndex].trim(),
|
||||||
|
parseAlignment(styleValues[format.alignmentIndex].trim()),
|
||||||
|
parseColor(styleValues[format.primaryColorIndex].trim()));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
|
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
|
||||||
return null;
|
return null;
|
||||||
@ -144,6 +153,44 @@ import java.util.regex.Pattern;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a SSA V4+ color expression.
|
||||||
|
*
|
||||||
|
* <p>A SSA V4+ color can be represented in hex {@code ("&HAABBGGRR")} or in 64-bit decimal format
|
||||||
|
* (byte order AABBGGRR). In both cases the alpha channel's value needs to be inverted because in
|
||||||
|
* SSA the 0xFF alpha value means transparent and 0x00 means opaque which is the opposite from the
|
||||||
|
* Android {@link ColorInt} representation.
|
||||||
|
*
|
||||||
|
* @param ssaColorExpression A SSA V4+ color expression.
|
||||||
|
* @return The parsed color value, or null if parsing failed.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
@ColorInt
|
||||||
|
public static Integer parseColor(String ssaColorExpression) {
|
||||||
|
// We use a long because the value is an unsigned 32-bit number, so can be larger than
|
||||||
|
// Integer.MAX_VALUE.
|
||||||
|
long abgr;
|
||||||
|
try {
|
||||||
|
abgr =
|
||||||
|
ssaColorExpression.startsWith("&H")
|
||||||
|
// Parse color from hex format (&HAABBGGRR).
|
||||||
|
? Long.parseLong(ssaColorExpression.substring(2), /* radix= */ 16)
|
||||||
|
// Parse color from decimal format (bytes order AABBGGRR).
|
||||||
|
: Long.parseLong(ssaColorExpression);
|
||||||
|
// Ensure only the bottom 4 bytes of abgr are set.
|
||||||
|
checkArgument(abgr <= 0xFFFFFFFFL);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.w(TAG, "Failed to parse color expression: '" + ssaColorExpression + "'", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Convert ABGR to ARGB.
|
||||||
|
int a = Ints.checkedCast(((abgr >> 24) & 0xFF) ^ 0xFF); // Flip alpha.
|
||||||
|
int b = Ints.checkedCast((abgr >> 16) & 0xFF);
|
||||||
|
int g = Ints.checkedCast((abgr >> 8) & 0xFF);
|
||||||
|
int r = Ints.checkedCast(abgr & 0xFF);
|
||||||
|
return Color.argb(a, r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a {@code Format:} line from the {@code [V4+ Styles]} section
|
* Represents a {@code Format:} line from the {@code [V4+ Styles]} section
|
||||||
*
|
*
|
||||||
@ -154,11 +201,13 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public final int nameIndex;
|
public final int nameIndex;
|
||||||
public final int alignmentIndex;
|
public final int alignmentIndex;
|
||||||
|
public final int primaryColorIndex;
|
||||||
public final int length;
|
public final int length;
|
||||||
|
|
||||||
private Format(int nameIndex, int alignmentIndex, int length) {
|
private Format(int nameIndex, int alignmentIndex, int primaryColorIndex, int length) {
|
||||||
this.nameIndex = nameIndex;
|
this.nameIndex = nameIndex;
|
||||||
this.alignmentIndex = alignmentIndex;
|
this.alignmentIndex = alignmentIndex;
|
||||||
|
this.primaryColorIndex = primaryColorIndex;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,6 +220,7 @@ import java.util.regex.Pattern;
|
|||||||
public static Format fromFormatLine(String styleFormatLine) {
|
public static Format fromFormatLine(String styleFormatLine) {
|
||||||
int nameIndex = C.INDEX_UNSET;
|
int nameIndex = C.INDEX_UNSET;
|
||||||
int alignmentIndex = C.INDEX_UNSET;
|
int alignmentIndex = C.INDEX_UNSET;
|
||||||
|
int primaryColorIndex = C.INDEX_UNSET;
|
||||||
String[] keys =
|
String[] keys =
|
||||||
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
|
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
|
||||||
for (int i = 0; i < keys.length; i++) {
|
for (int i = 0; i < keys.length; i++) {
|
||||||
@ -181,9 +231,14 @@ import java.util.regex.Pattern;
|
|||||||
case "alignment":
|
case "alignment":
|
||||||
alignmentIndex = i;
|
alignmentIndex = i;
|
||||||
break;
|
break;
|
||||||
|
case "primarycolour":
|
||||||
|
primaryColorIndex = i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null;
|
return nameIndex != C.INDEX_UNSET
|
||||||
|
? new Format(nameIndex, alignmentIndex, primaryColorIndex, keys.length)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,8 +292,7 @@ import java.util.regex.Pattern;
|
|||||||
// Ignore invalid \pos() or \move() function.
|
// Ignore invalid \pos() or \move() function.
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@SsaAlignment
|
@SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents);
|
||||||
int parsedAlignment = parseAlignmentOverride(braceContents);
|
|
||||||
if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) {
|
if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) {
|
||||||
alignment = parsedAlignment;
|
alignment = parsedAlignment;
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,13 @@ package com.google.android.exoplayer2.text.ssa;
|
|||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.testutil.truth.SpannedSubject;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.Subtitle;
|
import com.google.android.exoplayer2.text.Subtitle;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
@ -44,6 +47,7 @@ public final class SsaDecoderTest {
|
|||||||
private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
|
private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
|
||||||
private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning";
|
private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning";
|
||||||
private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres";
|
private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres";
|
||||||
|
private static final String COLORS = "media/ssa/colors";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeEmpty() throws IOException {
|
public void decodeEmpty() throws IOException {
|
||||||
@ -267,6 +271,54 @@ public final class SsaDecoderTest {
|
|||||||
assertTypicalCue3(subtitle, 0);
|
assertTypicalCue3(subtitle, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decodeColors() throws IOException {
|
||||||
|
SsaDecoder decoder = new SsaDecoder();
|
||||||
|
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), COLORS);
|
||||||
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
assertThat(subtitle.getEventTimeCount()).isEqualTo(14);
|
||||||
|
// &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB)
|
||||||
|
Spanned firstCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text;
|
||||||
|
SpannedSubject.assertThat(firstCueText)
|
||||||
|
.hasForegroundColorSpanBetween(0, firstCueText.length())
|
||||||
|
.withColor(Color.RED);
|
||||||
|
// &H0000FFFF (AABBGGRR) -> #FFFFFF00 (AARRGGBB)
|
||||||
|
Spanned secondCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text;
|
||||||
|
SpannedSubject.assertThat(secondCueText)
|
||||||
|
.hasForegroundColorSpanBetween(0, secondCueText.length())
|
||||||
|
.withColor(Color.YELLOW);
|
||||||
|
// &HFF00 (GGRR) -> #FF00FF00 (AARRGGBB)
|
||||||
|
Spanned thirdCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))).text;
|
||||||
|
SpannedSubject.assertThat(thirdCueText)
|
||||||
|
.hasForegroundColorSpanBetween(0, thirdCueText.length())
|
||||||
|
.withColor(Color.GREEN);
|
||||||
|
// &HA00000FF (AABBGGRR) -> #5FFF0000 (AARRGGBB)
|
||||||
|
Spanned fourthCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))).text;
|
||||||
|
SpannedSubject.assertThat(fourthCueText)
|
||||||
|
.hasForegroundColorSpanBetween(0, fourthCueText.length())
|
||||||
|
.withColor(0x5FFF0000);
|
||||||
|
// 16711680 (AABBGGRR) -> &H00FF0000 (AABBGGRR) -> #FF0000FF (AARRGGBB)
|
||||||
|
Spanned fifthCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))).text;
|
||||||
|
SpannedSubject.assertThat(fifthCueText)
|
||||||
|
.hasForegroundColorSpanBetween(0, fifthCueText.length())
|
||||||
|
.withColor(0xFF0000FF);
|
||||||
|
// 2164195328 (AABBGGRR) -> &H80FF0000 (AABBGGRR) -> #7F0000FF (AARRGGBB)
|
||||||
|
Spanned sixthCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))).text;
|
||||||
|
SpannedSubject.assertThat(sixthCueText)
|
||||||
|
.hasForegroundColorSpanBetween(0, sixthCueText.length())
|
||||||
|
.withColor(0x7F0000FF);
|
||||||
|
Spanned seventhCueText =
|
||||||
|
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))).text;
|
||||||
|
SpannedSubject.assertThat(seventhCueText)
|
||||||
|
.hasNoForegroundColorSpanBetween(0, seventhCueText.length());
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
|
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
|
||||||
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
|
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
|
||||||
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
|
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
|
||||||
|
26
testdata/src/test/assets/media/ssa/colors
vendored
Normal file
26
testdata/src/test/assets/media/ssa/colors
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[Script Info]
|
||||||
|
Title: Coloring
|
||||||
|
Script Type: V4.00+
|
||||||
|
PlayResX: 1280
|
||||||
|
PlayResY: 720
|
||||||
|
|
||||||
|
[V4+ Styles]
|
||||||
|
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||||
|
Style: PrimaryColourStyleHexRed ,Roboto,50,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
Style: PrimaryColourStyleHexYellow ,Roboto,50,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
Style: PrimaryColourStyleHexGreen ,Roboto,50,&HFF00 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
Style: PrimaryColourStyleHexAlpha ,Roboto,50,&HA00000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
Style: PrimaryColourStyleDecimal ,Roboto,50,16711680 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
Style: PrimaryColourStyleDecimalAlpha ,Roboto,50,2164195328,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
Style: PrimaryColourStyleInvalid ,Roboto,50,blue ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||||
|
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||||
|
Dialogue: 0,0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,Arnold,0,0,0,,First line in RED (&H000000FF).
|
||||||
|
Dialogue: 0,0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Arnold,0,0,0,,Second line in YELLOW (&H0000FFFF).
|
||||||
|
Dialogue: 0,0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,Third line in GREEN (leading zeros &HFF00).
|
||||||
|
Dialogue: 0,0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Arnold,0,0,0,,Fourth line in RED with alpha (&H400000FF).
|
||||||
|
Dialogue: 0,0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Arnold,0,0,0,,Fifth line in BLUE (16711680).
|
||||||
|
Dialogue: 0,0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha ,Arnold,0,0,0,,Sixth line in BLUE with alpha (2164195328).
|
||||||
|
Dialogue: 0,0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Arnold,0,0,0,,Seventh line with invalid color .
|
Loading…
x
Reference in New Issue
Block a user