mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
Merge pull request #3411 from Isira-Seneviratne/Pip_compat
Add picture-in-picture helper classes.
This commit is contained in:
commit
29a855d415
@ -0,0 +1,22 @@
|
||||
package com.github.libretube.compat
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
|
||||
object PictureInPictureCompat {
|
||||
fun isInPictureInPictureMode(activity: Activity): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.isInPictureInPictureMode
|
||||
}
|
||||
|
||||
fun setPictureInPictureParams(activity: Activity, params: PictureInPictureParamsCompat) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.setPictureInPictureParams(params.toPictureInPictureParams())
|
||||
}
|
||||
}
|
||||
|
||||
fun enterPictureInPictureMode(activity: Activity, params: PictureInPictureParamsCompat) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.enterPictureInPictureMode(params.toPictureInPictureParams())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package com.github.libretube.compat
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.RemoteActionCompat
|
||||
import com.google.android.exoplayer2.video.VideoSize
|
||||
|
||||
class PictureInPictureParamsCompat private constructor(
|
||||
private val autoEnterEnabled: Boolean,
|
||||
private val seamlessResizeEnabled: Boolean,
|
||||
private val closeAction: RemoteActionCompat?,
|
||||
private val actions: List<RemoteActionCompat>,
|
||||
private val sourceRectHint: Rect?,
|
||||
private val title: CharSequence?,
|
||||
private val subtitle: CharSequence?,
|
||||
private val aspectRatio: Rational?,
|
||||
private val expandedAspectRatio: Rational?
|
||||
) {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun toPictureInPictureParams(): PictureInPictureParams {
|
||||
val pipParams = PictureInPictureParams.Builder()
|
||||
.setSourceRectHint(sourceRectHint)
|
||||
.setActions(actions.map { it.toRemoteAction() })
|
||||
.setAspectRatio(aspectRatio)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
pipParams.setAutoEnterEnabled(autoEnterEnabled)
|
||||
.setSeamlessResizeEnabled(seamlessResizeEnabled)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pipParams.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setCloseAction(closeAction?.toRemoteAction())
|
||||
.setExpandedAspectRatio(expandedAspectRatio)
|
||||
}
|
||||
|
||||
return pipParams.build()
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private var autoEnterEnabled = false
|
||||
private var seamlessResizeEnabled = true
|
||||
private var closeAction: RemoteActionCompat? = null
|
||||
private var actions: List<RemoteActionCompat> = emptyList()
|
||||
private var sourceRectHint: Rect? = null
|
||||
private var title: CharSequence? = null
|
||||
private var subtitle: CharSequence? = null
|
||||
private var aspectRatio: Rational? = null
|
||||
private var expandedAspectRatio: Rational? = null
|
||||
|
||||
fun setAutoEnterEnabled(autoEnterEnabled: Boolean) = apply {
|
||||
this.autoEnterEnabled = autoEnterEnabled
|
||||
}
|
||||
|
||||
fun setSeamlessResizeEnabled(seamlessResizeEnabled: Boolean) = apply {
|
||||
this.seamlessResizeEnabled = seamlessResizeEnabled
|
||||
}
|
||||
|
||||
fun setCloseAction(action: RemoteActionCompat?) = apply {
|
||||
this.closeAction = action
|
||||
}
|
||||
|
||||
fun setActions(actions: List<RemoteActionCompat>) = apply {
|
||||
this.actions = actions
|
||||
}
|
||||
|
||||
fun setSourceRectHint(sourceRectHint: Rect?) = apply {
|
||||
this.sourceRectHint = sourceRectHint
|
||||
}
|
||||
|
||||
fun setTitle(title: CharSequence?) = apply {
|
||||
this.title = title
|
||||
}
|
||||
|
||||
fun setSubtitle(subtitle: CharSequence?) = apply {
|
||||
this.subtitle = subtitle
|
||||
}
|
||||
|
||||
fun setAspectRatio(aspectRatio: Rational?) = apply {
|
||||
this.aspectRatio = aspectRatio
|
||||
}
|
||||
|
||||
// Additional function replacing the project's extension function for the platform builder.
|
||||
fun setAspectRatio(videoSize: VideoSize): Builder {
|
||||
val ratio = (videoSize.width.toFloat() / videoSize.height)
|
||||
val rational = when {
|
||||
ratio.isNaN() -> Rational(4, 3)
|
||||
ratio <= 0.418410 -> Rational(41841, 100000)
|
||||
ratio >= 2.390000 -> Rational(239, 100)
|
||||
else -> Rational(videoSize.width, videoSize.height)
|
||||
}
|
||||
return setAspectRatio(rational)
|
||||
}
|
||||
|
||||
fun setExpandedAspectRatio(expandedAspectRatio: Rational?) = apply {
|
||||
this.expandedAspectRatio = expandedAspectRatio
|
||||
}
|
||||
|
||||
fun build(): PictureInPictureParamsCompat {
|
||||
return PictureInPictureParamsCompat(
|
||||
autoEnterEnabled,
|
||||
seamlessResizeEnabled,
|
||||
closeAction,
|
||||
actions,
|
||||
sourceRectHint,
|
||||
title,
|
||||
subtitle,
|
||||
aspectRatio,
|
||||
expandedAspectRatio
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,19 +2,18 @@ package com.github.libretube.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.RemoteAction
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.view.accessibility.CaptioningManager
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.RemoteActionCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.compat.PendingIntentCompat
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.enums.AudioQuality
|
||||
import com.github.libretube.enums.PlayerEvent
|
||||
@ -378,26 +377,24 @@ object PlayerHelper {
|
||||
return context.packageName + "." + ACTION_MEDIA_CONTROL
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getPendingIntent(activity: Activity, code: Int): PendingIntent {
|
||||
return PendingIntent.getBroadcast(
|
||||
return PendingIntentCompat.getBroadcast(
|
||||
activity,
|
||||
code,
|
||||
Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun getRemoteAction(
|
||||
activity: Activity,
|
||||
id: Int,
|
||||
@StringRes title: Int,
|
||||
event: PlayerEvent
|
||||
): RemoteAction {
|
||||
): RemoteActionCompat {
|
||||
val text = activity.getString(title)
|
||||
return RemoteAction(
|
||||
Icon.createWithResource(activity, id),
|
||||
return RemoteActionCompat(
|
||||
IconCompat.createWithResource(activity, id),
|
||||
text,
|
||||
text,
|
||||
getPendingIntent(activity, event.value)
|
||||
@ -407,8 +404,7 @@ object PlayerHelper {
|
||||
/**
|
||||
* Create controls to use in the PiP window
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList<RemoteAction> {
|
||||
fun getPiPModeActions(activity: Activity, isPlaying: Boolean): List<RemoteActionCompat> {
|
||||
val audioModeAction = getRemoteAction(
|
||||
activity,
|
||||
R.drawable.ic_headphones,
|
||||
@ -443,12 +439,10 @@ object PlayerHelper {
|
||||
R.string.forward,
|
||||
PlayerEvent.Forward
|
||||
)
|
||||
return if (
|
||||
!isOfflinePlayer && alternativePiPControls
|
||||
) {
|
||||
arrayListOf(audioModeAction, playPauseAction, skipNextAction)
|
||||
return if (alternativePiPControls) {
|
||||
listOf(audioModeAction, playPauseAction, skipNextAction)
|
||||
} else {
|
||||
arrayListOf(rewindAction, playPauseAction, forwardAction)
|
||||
listOf(rewindAction, playPauseAction, forwardAction)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
package com.github.libretube.ui.activities
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.compat.PictureInPictureCompat
|
||||
import com.github.libretube.compat.PictureInPictureParamsCompat
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.ActivityOfflinePlayerBinding
|
||||
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
||||
@ -21,7 +21,6 @@ import com.github.libretube.helpers.PlayerHelper
|
||||
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
|
||||
import com.github.libretube.helpers.WindowHelper
|
||||
import com.github.libretube.ui.base.BaseActivity
|
||||
import com.github.libretube.ui.extensions.setAspectRatio
|
||||
import com.github.libretube.ui.models.PlayerViewModel
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
@ -197,18 +196,14 @@ class OfflinePlayerActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
if (!PlayerHelper.pipEnabled) return
|
||||
|
||||
if (player.playbackState == PlaybackState.STATE_PAUSED) return
|
||||
|
||||
enterPictureInPictureMode(
|
||||
PictureInPictureParams.Builder()
|
||||
.setActions(emptyList())
|
||||
.setAspectRatio(player.videoSize.width, player.videoSize.height)
|
||||
.build()
|
||||
)
|
||||
if (PlayerHelper.pipEnabled && player.playbackState != PlaybackState.STATE_PAUSED) {
|
||||
PictureInPictureCompat.enterPictureInPictureMode(
|
||||
this,
|
||||
PictureInPictureParamsCompat.Builder()
|
||||
.setAspectRatio(player.videoSize)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
super.onUserLeaveHint()
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
package com.github.libretube.ui.extensions
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun PictureInPictureParams.Builder.setAspectRatio(
|
||||
width: Int,
|
||||
height: Int
|
||||
): PictureInPictureParams.Builder {
|
||||
val ratio = (width.toFloat() / height).let {
|
||||
when {
|
||||
it.isNaN() -> Rational(4, 3)
|
||||
it <= 0.418410 -> Rational(41841, 100000)
|
||||
it >= 2.390000 -> Rational(239, 100)
|
||||
else -> Rational(width, height)
|
||||
}
|
||||
}
|
||||
return setAspectRatio(ratio)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package com.github.libretube.ui.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -11,7 +10,6 @@ import android.content.res.Configuration
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@ -25,7 +23,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
@ -49,6 +46,8 @@ import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.compat.PictureInPictureCompat
|
||||
import com.github.libretube.compat.PictureInPictureParamsCompat
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.databinding.FragmentPlayerBinding
|
||||
@ -82,7 +81,6 @@ import com.github.libretube.ui.dialogs.AddToPlaylistDialog
|
||||
import com.github.libretube.ui.dialogs.DownloadDialog
|
||||
import com.github.libretube.ui.dialogs.ShareDialog
|
||||
import com.github.libretube.ui.dialogs.StatsDialog
|
||||
import com.github.libretube.ui.extensions.setAspectRatio
|
||||
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
||||
import com.github.libretube.ui.listeners.SeekbarPreviewListener
|
||||
@ -346,9 +344,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
binding.playerMotionLayout.progress = 1.toFloat()
|
||||
binding.playerMotionLayout.transitionToStart()
|
||||
|
||||
if (usePiP()) activity?.setPictureInPictureParams(getPipParams())
|
||||
if (PlayerHelper.pipEnabled) {
|
||||
PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams)
|
||||
}
|
||||
|
||||
if (SDK_INT < Build.VERSION_CODES.O) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
binding.relPlayerPip.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@ -620,11 +620,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
private fun disableAutoPiP() {
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
return
|
||||
}
|
||||
activity?.setPictureInPictureParams(
|
||||
PictureInPictureParams.Builder().setAutoEnterEnabled(false).build()
|
||||
// autoEnterEnabled is false by default
|
||||
PictureInPictureCompat.setPictureInPictureParams(
|
||||
requireActivity(),
|
||||
PictureInPictureParamsCompat.Builder().build()
|
||||
)
|
||||
}
|
||||
|
||||
@ -729,7 +728,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
|
||||
if (binding.playerMotionLayout.progress != 1.0f) {
|
||||
// show controllers when not in picture in picture mode
|
||||
if (!(usePiP() && activity?.isInPictureInPictureMode!!)) {
|
||||
val inPipMode = PlayerHelper.pipEnabled &&
|
||||
PictureInPictureCompat.isInPictureInPictureMode(requireActivity())
|
||||
if (!inPipMode) {
|
||||
binding.player.useController = true
|
||||
}
|
||||
}
|
||||
@ -745,13 +746,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether PiP is supported and enabled
|
||||
*/
|
||||
private fun usePiP(): Boolean {
|
||||
return SDK_INT >= Build.VERSION_CODES.O && PlayerHelper.pipEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch the segments for SponsorBlock
|
||||
*/
|
||||
@ -913,7 +907,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
// Listener for play and pause icon change
|
||||
exoPlayer.addListener(object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (usePiP()) activity?.setPictureInPictureParams(getPipParams())
|
||||
if (PlayerHelper.pipEnabled) {
|
||||
PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams)
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
// Stop [BackgroundMode] service if it is running.
|
||||
@ -964,17 +960,22 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
val activity = requireActivity()
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
// media actually playing
|
||||
transitioning = false
|
||||
// update the PiP params to use the correct aspect ratio
|
||||
if (usePiP()) activity?.setPictureInPictureParams(getPipParams())
|
||||
if (PlayerHelper.pipEnabled) {
|
||||
PictureInPictureCompat.setPictureInPictureParams(activity, pipParams)
|
||||
}
|
||||
}
|
||||
|
||||
// listen for the stop button in the notification
|
||||
if (playbackState == PlaybackState.STATE_STOPPED && usePiP()) {
|
||||
if (playbackState == PlaybackState.STATE_STOPPED && PlayerHelper.pipEnabled &&
|
||||
PictureInPictureCompat.isInPictureInPictureMode(activity)
|
||||
) {
|
||||
// finish PiP by finishing the activity
|
||||
if (activity?.isInPictureInPictureMode!!) activity?.finish()
|
||||
activity.finish()
|
||||
}
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
}
|
||||
@ -1005,12 +1006,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
binding.relPlayerPip.setOnClickListener {
|
||||
if (SDK_INT < Build.VERSION_CODES.O) return@setOnClickListener
|
||||
try {
|
||||
activity?.enterPictureInPictureMode(getPipParams())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
|
||||
}
|
||||
initializeRelatedVideos(streams.relatedStreams.filter { !it.title.isNullOrBlank() })
|
||||
// set video description
|
||||
@ -1502,25 +1498,25 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
fun onUserLeaveHint() {
|
||||
if (usePiP() && shouldStartPiP()) {
|
||||
activity?.enterPictureInPictureMode(getPipParams())
|
||||
if (PlayerHelper.pipEnabled && shouldStartPiP()) {
|
||||
PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
|
||||
return
|
||||
}
|
||||
if (PlayerHelper.pauseOnQuit) exoPlayer.pause()
|
||||
if (PlayerHelper.pauseOnQuit) {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getPipParams(): PictureInPictureParams = PictureInPictureParams.Builder()
|
||||
.setActions(PlayerHelper.getPiPModeActions(requireActivity(), exoPlayer.isPlaying))
|
||||
.apply {
|
||||
if (SDK_INT >= Build.VERSION_CODES.S && PlayerHelper.pipEnabled) {
|
||||
setAutoEnterEnabled(true)
|
||||
private val pipParams
|
||||
get() = PictureInPictureParamsCompat.Builder()
|
||||
.setActions(PlayerHelper.getPiPModeActions(requireActivity(), exoPlayer.isPlaying))
|
||||
.setAutoEnterEnabled(PlayerHelper.pipEnabled)
|
||||
.apply {
|
||||
if (exoPlayer.isPlaying) {
|
||||
setAspectRatio(exoPlayer.videoSize)
|
||||
}
|
||||
}
|
||||
if (exoPlayer.isPlaying) {
|
||||
setAspectRatio(exoPlayer.videoSize.width, exoPlayer.videoSize.height)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
.build()
|
||||
|
||||
private fun setupSeekbarPreview() {
|
||||
playerBinding.seekbarPreview.visibility = View.GONE
|
||||
@ -1534,7 +1530,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
private fun shouldStartPiP(): Boolean {
|
||||
if (!PlayerHelper.pipEnabled || SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (!PlayerHelper.pipEnabled || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1559,10 +1555,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
if (!PlayerHelper.autoRotationEnabled) return
|
||||
|
||||
// If in PiP mode, orientation is given as landscape.
|
||||
if (SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true) return
|
||||
if (!PlayerHelper.autoRotationEnabled ||
|
||||
// If in PiP mode, orientation is given as landscape.
|
||||
PictureInPictureCompat.isInPictureInPictureMode(requireActivity())
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
when (newConfig.orientation) {
|
||||
// go to fullscreen mode
|
||||
|
Loading…
Reference in New Issue
Block a user