Improve text linkifier function parameters
This commit is contained in:
parent
218f25c171
commit
eef418a757
4 changed files with 74 additions and 94 deletions
|
@ -16,9 +16,9 @@ import org.schabi.newpipe.BuildConfig
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
class AboutActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
@ -132,24 +131,19 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
|
|
||||||
private void loadDescriptionContent() {
|
private void loadDescriptionContent() {
|
||||||
final Description description = streamInfo.getDescription();
|
final Description description = streamInfo.getDescription();
|
||||||
final String contentUrl = streamInfo.getUrl();
|
|
||||||
final StreamingService service = streamInfo.getService();
|
|
||||||
|
|
||||||
switch (description.getType()) {
|
switch (description.getType()) {
|
||||||
case Description.HTML:
|
case Description.HTML:
|
||||||
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
|
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(
|
||||||
description.getContent(), binding.detailDescriptionView,
|
binding.detailDescriptionView, description.getContent(),
|
||||||
service, contentUrl, HtmlCompat.FROM_HTML_MODE_LEGACY);
|
HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo);
|
||||||
break;
|
break;
|
||||||
case Description.MARKDOWN:
|
case Description.MARKDOWN:
|
||||||
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
|
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(
|
||||||
description.getContent(), binding.detailDescriptionView,
|
binding.detailDescriptionView, description.getContent(), streamInfo);
|
||||||
service, contentUrl);
|
|
||||||
break;
|
break;
|
||||||
case Description.PLAIN_TEXT: default:
|
case Description.PLAIN_TEXT: default:
|
||||||
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
|
descriptionDisposable = TextLinkifier.createLinksFromPlainText(
|
||||||
description.getContent(), binding.detailDescriptionView,
|
binding.detailDescriptionView, description.getContent(), streamInfo);
|
||||||
service, contentUrl);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,8 +198,7 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (linkifyContent) {
|
if (linkifyContent) {
|
||||||
TextLinkifier.createLinksFromPlainText(requireContext(),
|
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null);
|
||||||
content, itemBinding.metadataContentView, null, null);
|
|
||||||
} else {
|
} else {
|
||||||
itemBinding.metadataContentView.setText(content);
|
itemBinding.metadataContentView.setText(content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,9 +311,9 @@ public final class ExtractorHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
metaInfoSeparator.setVisibility(View.VISIBLE);
|
metaInfoSeparator.setVisibility(View.VISIBLE);
|
||||||
return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(),
|
return TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView,
|
||||||
metaInfoTextView, null, null,
|
stringBuilder.toString(), HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING,
|
||||||
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,10 @@ import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
@ -42,85 +43,74 @@ public final class TextLinkifier {
|
||||||
* Create web links for contents with an HTML description.
|
* Create web links for contents with an HTML description.
|
||||||
* <p>
|
* <p>
|
||||||
* This will call
|
* This will call
|
||||||
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView,
|
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
|
||||||
* StreamingService, String)}
|
|
||||||
* after having linked the URLs with {@link HtmlCompat#fromHtml(String, int)}.
|
* after having linked the URLs with {@link HtmlCompat#fromHtml(String, int)}.
|
||||||
*
|
*
|
||||||
* @param context the context to use
|
* @param textView the TextView to set the htmlBlock linked
|
||||||
* @param htmlBlock the htmlBlock to be linked
|
* @param htmlBlock the htmlBlock to be linked
|
||||||
* @param textView the TextView to set the htmlBlock linked
|
* @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)}
|
||||||
* @param streamingService the {@link StreamingService} of the content
|
* will be called
|
||||||
* @param contentUrl the URL of the content
|
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
|
||||||
* @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)}
|
* the specific time, and hashtags to search for the term in the correct
|
||||||
* will be called
|
* service
|
||||||
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Disposable createLinksFromHtmlBlock(final Context context,
|
public static Disposable createLinksFromHtmlBlock(@NonNull final TextView textView,
|
||||||
final String htmlBlock,
|
final String htmlBlock,
|
||||||
final TextView textView,
|
final int htmlCompatFlag,
|
||||||
final StreamingService streamingService,
|
@Nullable final Info relatedInfo) {
|
||||||
final String contentUrl,
|
return changeIntentsOfDescriptionLinks(
|
||||||
final int htmlCompatFlag) {
|
textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfo);
|
||||||
return changeIntentsOfDescriptionLinks(context,
|
|
||||||
HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), textView, streamingService,
|
|
||||||
contentUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create web links for contents with a plain text description.
|
* Create web links for contents with a plain text description.
|
||||||
* <p>
|
* <p>
|
||||||
* This will call
|
* This will call
|
||||||
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView,
|
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
|
||||||
* StreamingService, String)}
|
|
||||||
* after having linked the URLs with {@link TextView#setAutoLinkMask(int)} and
|
* after having linked the URLs with {@link TextView#setAutoLinkMask(int)} and
|
||||||
* {@link TextView#setText(CharSequence, TextView.BufferType)}.
|
* {@link TextView#setText(CharSequence, TextView.BufferType)}.
|
||||||
*
|
*
|
||||||
* @param context the context to use
|
* @param textView the TextView to set the plain text block linked
|
||||||
* @param plainTextBlock the block of plain text to be linked
|
* @param plainTextBlock the block of plain text to be linked
|
||||||
* @param textView the TextView to set the plain text block linked
|
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
|
||||||
* @param streamingService the {@link StreamingService} of the content
|
* the specific time, and hashtags to search for the term in the correct
|
||||||
* @param contentUrl the URL of the content
|
* service
|
||||||
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Disposable createLinksFromPlainText(final Context context,
|
public static Disposable createLinksFromPlainText(@NonNull final TextView textView,
|
||||||
final String plainTextBlock,
|
final String plainTextBlock,
|
||||||
@NonNull final TextView textView,
|
@Nullable final Info relatedInfo) {
|
||||||
final StreamingService streamingService,
|
|
||||||
final String contentUrl) {
|
|
||||||
textView.setAutoLinkMask(Linkify.WEB_URLS);
|
textView.setAutoLinkMask(Linkify.WEB_URLS);
|
||||||
textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
|
textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
|
||||||
return changeIntentsOfDescriptionLinks(context, textView.getText(), textView,
|
return changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo);
|
||||||
streamingService, contentUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create web links for contents with a markdown description.
|
* Create web links for contents with a markdown description.
|
||||||
* <p>
|
* <p>
|
||||||
* This will call
|
* This will call
|
||||||
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView,
|
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
|
||||||
* StreamingService, String)}
|
|
||||||
* after creating an {@link Markwon} object and using
|
* after creating an {@link Markwon} object and using
|
||||||
* {@link Markwon#setMarkdown(TextView, String)}.
|
* {@link Markwon#setMarkdown(TextView, String)}.
|
||||||
*
|
*
|
||||||
* @param context the context to use
|
* @param textView the TextView to set the plain text block linked
|
||||||
* @param markdownBlock the block of markdown text to be linked
|
* @param markdownBlock the block of markdown text to be linked
|
||||||
* @param textView the TextView to set the plain text block linked
|
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
|
||||||
* @param streamingService the {@link StreamingService} of the content
|
* the specific time, and hashtags to search for the term in the correct
|
||||||
* @param contentUrl the URL of the content
|
* service
|
||||||
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Disposable createLinksFromMarkdownText(final Context context,
|
public static Disposable createLinksFromMarkdownText(@NonNull final TextView textView,
|
||||||
final String markdownBlock,
|
final String markdownBlock,
|
||||||
final TextView textView,
|
@Nullable final Info relatedInfo) {
|
||||||
final StreamingService streamingService,
|
final Markwon markwon = Markwon.builder(textView.getContext())
|
||||||
final String contentUrl) {
|
.usePlugin(LinkifyPlugin.create()).build();
|
||||||
final Markwon markwon = Markwon.builder(context).usePlugin(LinkifyPlugin.create()).build();
|
|
||||||
markwon.setMarkdown(textView, markdownBlock);
|
markwon.setMarkdown(textView, markdownBlock);
|
||||||
return changeIntentsOfDescriptionLinks(context, textView.getText(), textView,
|
return changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo);
|
||||||
streamingService, contentUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -134,12 +124,12 @@ public final class TextLinkifier {
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param spannableDescription the SpannableStringBuilder with the text of the
|
* @param spannableDescription the SpannableStringBuilder with the text of the
|
||||||
* content description
|
* content description
|
||||||
* @param streamingService the {@link StreamingService} of the content
|
* @param relatedInfo used to search for the term in the correct service
|
||||||
*/
|
*/
|
||||||
private static void addClickListenersOnHashtags(final Context context,
|
private static void addClickListenersOnHashtags(final Context context,
|
||||||
@NonNull final SpannableStringBuilder
|
@NonNull final SpannableStringBuilder
|
||||||
spannableDescription,
|
spannableDescription,
|
||||||
final StreamingService streamingService) {
|
final Info relatedInfo) {
|
||||||
final String descriptionText = spannableDescription.toString();
|
final String descriptionText = spannableDescription.toString();
|
||||||
final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText);
|
final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText);
|
||||||
|
|
||||||
|
@ -155,7 +145,7 @@ public final class TextLinkifier {
|
||||||
spannableDescription.setSpan(new ClickableSpan() {
|
spannableDescription.setSpan(new ClickableSpan() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(@NonNull final View view) {
|
public void onClick(@NonNull final View view) {
|
||||||
NavigationHelper.openSearch(context, streamingService.getServiceId(),
|
NavigationHelper.openSearch(context, relatedInfo.getServiceId(),
|
||||||
parsedHashtag);
|
parsedHashtag);
|
||||||
}
|
}
|
||||||
}, hashtagStart, hashtagEnd, 0);
|
}, hashtagStart, hashtagEnd, 0);
|
||||||
|
@ -173,14 +163,12 @@ public final class TextLinkifier {
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param spannableDescription the SpannableStringBuilder with the text of the
|
* @param spannableDescription the SpannableStringBuilder with the text of the
|
||||||
* content description
|
* content description
|
||||||
* @param contentUrl the URL of the content
|
* @param relatedInfo what to open in the popup player when timestamps are clicked
|
||||||
* @param streamingService the {@link StreamingService} of the content
|
|
||||||
*/
|
*/
|
||||||
private static void addClickListenersOnTimestamps(final Context context,
|
private static void addClickListenersOnTimestamps(final Context context,
|
||||||
@NonNull final SpannableStringBuilder
|
@NonNull final SpannableStringBuilder
|
||||||
spannableDescription,
|
spannableDescription,
|
||||||
final String contentUrl,
|
final Info relatedInfo) {
|
||||||
final StreamingService streamingService) {
|
|
||||||
final String descriptionText = spannableDescription.toString();
|
final String descriptionText = spannableDescription.toString();
|
||||||
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
|
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
|
||||||
|
|
||||||
|
@ -189,14 +177,14 @@ public final class TextLinkifier {
|
||||||
final int timestampEnd = timestampsMatches.end(3);
|
final int timestampEnd = timestampsMatches.end(3);
|
||||||
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
|
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
|
||||||
final String[] timestampParts = parsedTimestamp.split(":");
|
final String[] timestampParts = parsedTimestamp.split(":");
|
||||||
final int time;
|
|
||||||
|
|
||||||
|
final int seconds;
|
||||||
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
|
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
|
||||||
time = Integer.parseInt(timestampParts[0]) * 3600 // hours
|
seconds = Integer.parseInt(timestampParts[0]) * 3600 // hours
|
||||||
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
|
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
|
||||||
+ Integer.parseInt(timestampParts[2]); // seconds
|
+ Integer.parseInt(timestampParts[2]); // seconds
|
||||||
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
|
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
|
||||||
time = Integer.parseInt(timestampParts[0]) * 60 // minutes
|
seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
|
||||||
+ Integer.parseInt(timestampParts[1]); // seconds
|
+ Integer.parseInt(timestampParts[1]); // seconds
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
|
@ -205,8 +193,8 @@ public final class TextLinkifier {
|
||||||
spannableDescription.setSpan(new ClickableSpan() {
|
spannableDescription.setSpan(new ClickableSpan() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(@NonNull final View view) {
|
public void onClick(@NonNull final View view) {
|
||||||
playOnPopup(new CompositeDisposable(), context, contentUrl, streamingService,
|
playOnPopup(new CompositeDisposable(), context, relatedInfo.getUrl(),
|
||||||
time);
|
relatedInfo.getService(), seconds);
|
||||||
}
|
}
|
||||||
}, timestampStart, timestampEnd, 0);
|
}, timestampStart, timestampEnd, 0);
|
||||||
}
|
}
|
||||||
|
@ -221,28 +209,28 @@ public final class TextLinkifier {
|
||||||
* with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
|
* with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
|
||||||
* This method will also add click listeners on timestamps in this description, which will play
|
* This method will also add click listeners on timestamps in this description, which will play
|
||||||
* the content in the popup player at the time indicated in the timestamp, by using
|
* the content in the popup player at the time indicated in the timestamp, by using
|
||||||
* {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, String,
|
* {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, Info)}
|
||||||
* StreamingService)} method and click listeners on hashtags, which will open a search
|
* method and click listeners on hashtags, by using
|
||||||
* on the current service with the hashtag.
|
* {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, Info)},
|
||||||
|
* which will open a search on the current service with the hashtag.
|
||||||
* <p>
|
* <p>
|
||||||
* This method is required in order to intercept links and e.g. show a confirmation dialog
|
* This method is required in order to intercept links and e.g. show a confirmation dialog
|
||||||
* before opening a web link.
|
* before opening a web link.
|
||||||
*
|
*
|
||||||
* @param context the context to use
|
* @param textView the TextView in which the converted CharSequence will be applied
|
||||||
* @param chars the CharSequence to be parsed
|
* @param chars the CharSequence to be parsed
|
||||||
* @param textView the TextView in which the converted CharSequence will be applied
|
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
|
||||||
* @param streamingService the {@link StreamingService} of the content
|
* the specific time, and hashtags to search for the term in the correct
|
||||||
* @param contentUrl the URL of the content
|
* service
|
||||||
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private static Disposable changeIntentsOfDescriptionLinks(final Context context,
|
private static Disposable changeIntentsOfDescriptionLinks(final TextView textView,
|
||||||
final CharSequence chars,
|
final CharSequence chars,
|
||||||
final TextView textView,
|
@Nullable final Info relatedInfo) {
|
||||||
final StreamingService
|
|
||||||
streamingService,
|
|
||||||
final String contentUrl) {
|
|
||||||
return Single.fromCallable(() -> {
|
return Single.fromCallable(() -> {
|
||||||
|
final Context context = textView.getContext();
|
||||||
|
|
||||||
// add custom click actions on web links
|
// add custom click actions on web links
|
||||||
final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars);
|
final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars);
|
||||||
final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class);
|
final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class);
|
||||||
|
@ -264,11 +252,10 @@ public final class TextLinkifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
// add click actions on plain text timestamps only for description of contents,
|
// add click actions on plain text timestamps only for description of contents,
|
||||||
// unneeded for metainfo TextViews
|
// unneeded for meta-info or other TextViews
|
||||||
if (contentUrl != null || streamingService != null) {
|
if (relatedInfo != null) {
|
||||||
addClickListenersOnTimestamps(context, textBlockLinked, contentUrl,
|
addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo);
|
||||||
streamingService);
|
addClickListenersOnHashtags(context, textBlockLinked, relatedInfo);
|
||||||
addClickListenersOnHashtags(context, textBlockLinked, streamingService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return textBlockLinked;
|
return textBlockLinked;
|
||||||
|
|
Loading…
Reference in a new issue