Moved to provider + type to make it cleaner in future, fix for multiple players
This commit is contained in:
@ -32,7 +32,7 @@ const captions = {
|
||||
}
|
||||
|
||||
// Only Vimeo and HTML5 video supported at this point
|
||||
if (!['video', 'vimeo'].includes(this.type) || (this.type === 'video' && !support.textTracks)) {
|
||||
if (!this.isVideo || this.isYouTube || (this.isVideo && !support.textTracks)) {
|
||||
// Clear menu and hide
|
||||
if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
||||
controls.setCaptionsMenu.call(this);
|
||||
@ -71,7 +71,7 @@ const captions = {
|
||||
// Set the captions language
|
||||
setLanguage() {
|
||||
// Setup HTML5 track rendering
|
||||
if (this.type === 'video') {
|
||||
if (this.isVideo) {
|
||||
captions.getTracks.call(this).forEach(track => {
|
||||
// Remove previous bindings
|
||||
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
|
||||
@ -91,7 +91,7 @@ const captions = {
|
||||
captions.setCue.call(this, currentTrack);
|
||||
}
|
||||
}
|
||||
} else if (this.type === 'vimeo' && this.captions.active) {
|
||||
} else if (this.isVimeo && this.captions.active) {
|
||||
this.embed.enableTextTrack(this.language);
|
||||
}
|
||||
},
|
||||
|
2
src/js/controls.js
vendored
2
src/js/controls.js
vendored
@ -445,7 +445,7 @@ const controls = {
|
||||
}
|
||||
|
||||
// Toggle the pane and tab
|
||||
const toggle = !utils.is.empty(this.options.quality) && this.type === 'youtube';
|
||||
const toggle = !utils.is.empty(this.options.quality) && this.isYouTube;
|
||||
controls.toggleTab.call(this, type, toggle);
|
||||
|
||||
// If we're hiding, nothing more to do
|
||||
|
@ -13,7 +13,7 @@ const defaults = {
|
||||
autoplay: false,
|
||||
|
||||
// Only allow one media playing at once (vimeo only)
|
||||
autopause: false,
|
||||
autopause: true,
|
||||
|
||||
// Default time to skip when rewind/fast forward
|
||||
seekTime: 10,
|
||||
@ -267,6 +267,7 @@ const defaults = {
|
||||
embed: 'plyr__video-embed',
|
||||
control: 'plyr__control',
|
||||
type: 'plyr--{0}',
|
||||
provider: 'plyr--{0}',
|
||||
stopped: 'plyr--stopped',
|
||||
playing: 'plyr--playing',
|
||||
loading: 'plyr--loading',
|
||||
|
@ -92,7 +92,7 @@ const fullscreen = {
|
||||
|
||||
// Setup fullscreen
|
||||
setup() {
|
||||
if (!this.supported.ui || this.type === 'audio' || !this.config.fullscreen.enabled) {
|
||||
if (!this.supported.ui || this.isAudio || !this.config.fullscreen.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -221,7 +221,7 @@ const listeners = {
|
||||
// Handle the media finishing
|
||||
utils.on(this.media, 'ended', () => {
|
||||
// Show poster on end
|
||||
if (this.type === 'video' && this.config.showPosterOnEnd) {
|
||||
if (this.isHTML5 && this.isVideo && this.config.showPosterOnEnd) {
|
||||
// Restart
|
||||
this.restart();
|
||||
|
||||
@ -243,7 +243,7 @@ const listeners = {
|
||||
utils.on(this.media, 'stalled waiting canplay seeked playing', event => ui.checkLoading.call(this, event));
|
||||
|
||||
// Click video
|
||||
if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') {
|
||||
if (this.supported.ui && this.config.clickToPlay && !this.isAudio) {
|
||||
// Re-fetch the wrapper
|
||||
const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`);
|
||||
|
||||
|
@ -23,6 +23,9 @@ const media = {
|
||||
// Add type class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
|
||||
|
||||
// Add provider class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
|
||||
|
||||
// Add video class for embeds
|
||||
// This will require changes if audio embeds are added
|
||||
if (this.isEmbed) {
|
||||
@ -31,7 +34,7 @@ const media = {
|
||||
|
||||
if (this.supported.ui) {
|
||||
// Check for picture-in-picture support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.type === 'video');
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
||||
|
||||
// Check for airplay support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
@ -47,7 +50,7 @@ const media = {
|
||||
}
|
||||
|
||||
// Inject the player wrapper
|
||||
if (['video', 'youtube', 'vimeo'].includes(this.type)) {
|
||||
if (this.isVideo || this.isYouTube || this.isVimeo) {
|
||||
// Create the wrapper div
|
||||
this.elements.wrapper = utils.createElement('div', {
|
||||
class: this.config.classNames.video,
|
||||
@ -58,7 +61,7 @@ const media = {
|
||||
}
|
||||
|
||||
if (this.isEmbed) {
|
||||
switch (this.type) {
|
||||
switch (this.provider) {
|
||||
case 'youtube':
|
||||
youtube.setup.call(this);
|
||||
break;
|
||||
|
@ -4,13 +4,12 @@
|
||||
|
||||
import utils from './../utils';
|
||||
import captions from './../captions';
|
||||
import controls from './../controls';
|
||||
import ui from './../ui';
|
||||
|
||||
const vimeo = {
|
||||
setup() {
|
||||
// Remove old containers
|
||||
const containers = utils.getElements.call(this, `[id^="${this.type}-"]`);
|
||||
const containers = utils.getElements.call(this, `[id^="${this.provider}-"]`);
|
||||
Array.from(containers).forEach(utils.removeElement);
|
||||
|
||||
// Add embed class for responsive
|
||||
@ -20,7 +19,7 @@ const vimeo = {
|
||||
vimeo.setAspectRatio.call(this);
|
||||
|
||||
// Set ID
|
||||
this.media.setAttribute('id', utils.generateId(this.type));
|
||||
this.media.setAttribute('id', utils.generateId(this.provider));
|
||||
|
||||
// Load the API if not already
|
||||
if (!utils.is.object(window.Vimeo)) {
|
||||
|
@ -11,7 +11,7 @@ const youtube = {
|
||||
const videoId = utils.parseYouTubeId(this.embedId);
|
||||
|
||||
// Remove old containers
|
||||
const containers = utils.getElements.call(this, `[id^="${this.type}-"]`);
|
||||
const containers = utils.getElements.call(this, `[id^="${this.provider}-"]`);
|
||||
Array.from(containers).forEach(utils.removeElement);
|
||||
|
||||
// Add embed class for responsive
|
||||
@ -21,7 +21,7 @@ const youtube = {
|
||||
youtube.setAspectRatio.call(this);
|
||||
|
||||
// Set ID
|
||||
this.media.setAttribute('id', utils.generateId(this.type));
|
||||
this.media.setAttribute('id', utils.generateId(this.provider));
|
||||
|
||||
// Setup API
|
||||
if (utils.is.object(window.YT)) {
|
||||
@ -31,6 +31,7 @@ const youtube = {
|
||||
utils.loadScript(this.config.urls.youtube.api);
|
||||
|
||||
// Setup callback for the API
|
||||
// YouTube has it's own system of course...
|
||||
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
|
||||
|
||||
// Add to queue
|
||||
|
101
src/js/plyr.js
101
src/js/plyr.js
@ -5,8 +5,8 @@
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import { providers, types } from './types';
|
||||
import defaults from './defaults';
|
||||
import types from './types';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
@ -40,11 +40,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// jQuery, NodeList or Array passed, use first element
|
||||
if (
|
||||
(window.jQuery && this.media instanceof jQuery) ||
|
||||
utils.is.nodeList(this.media) ||
|
||||
utils.is.array(this.media)
|
||||
) {
|
||||
if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) {
|
||||
// eslint-disable-next-line
|
||||
this.media = this.media[0];
|
||||
}
|
||||
@ -149,7 +145,7 @@ class Plyr {
|
||||
// Embed attributes
|
||||
const attributes = {
|
||||
provider: 'data-plyr-provider',
|
||||
id: 'data-plyr-provider-id',
|
||||
id: 'data-plyr-embed-id',
|
||||
};
|
||||
|
||||
// Different setup based on type
|
||||
@ -157,16 +153,18 @@ class Plyr {
|
||||
// TODO: Handle passing an iframe for true progressive enhancement
|
||||
// case 'iframe':
|
||||
case 'div':
|
||||
this.type = this.media.getAttribute(attributes.provider);
|
||||
this.type = types.video; // Audio will come later for external providers
|
||||
this.provider = this.media.getAttribute(attributes.provider);
|
||||
this.embedId = this.media.getAttribute(attributes.id);
|
||||
|
||||
if (utils.is.empty(this.type)) {
|
||||
this.console.error('Setup failed: embed type missing');
|
||||
if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
|
||||
this.console.error('Setup failed: Invalid provider');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try and get the embed id
|
||||
if (utils.is.empty(this.embedId)) {
|
||||
this.console.error('Setup failed: video id missing');
|
||||
this.console.error('Setup failed: Embed ID or URL missing');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -179,19 +177,24 @@ class Plyr {
|
||||
case 'video':
|
||||
case 'audio':
|
||||
this.type = type;
|
||||
this.provider = providers.html5;
|
||||
|
||||
if (this.media.hasAttribute('crossorigin')) {
|
||||
this.config.crossorigin = true;
|
||||
}
|
||||
|
||||
if (this.media.hasAttribute('autoplay')) {
|
||||
this.config.autoplay = true;
|
||||
}
|
||||
|
||||
if (this.media.hasAttribute('playsinline')) {
|
||||
this.config.inline = true;
|
||||
}
|
||||
|
||||
if (this.media.hasAttribute('muted')) {
|
||||
this.config.muted = true;
|
||||
}
|
||||
|
||||
if (this.media.hasAttribute('loop')) {
|
||||
this.config.loop.active = true;
|
||||
}
|
||||
@ -207,7 +210,7 @@ class Plyr {
|
||||
storage.setup.call(this);
|
||||
|
||||
// Check for support again but with type
|
||||
this.supported = support.check(this.type, this.config.inline);
|
||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
||||
|
||||
// If no support for even API, bail
|
||||
if (!this.supported.api) {
|
||||
@ -253,17 +256,25 @@ class Plyr {
|
||||
// ---------------------------------------
|
||||
|
||||
/**
|
||||
* If the player is HTML5
|
||||
* Types and provider helpers
|
||||
*/
|
||||
get isHTML5() {
|
||||
return types.html5.includes(this.type);
|
||||
return this.provider === providers.html5;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the player is an embed - e.g. YouTube or Vimeo
|
||||
*/
|
||||
get isEmbed() {
|
||||
return types.embed.includes(this.type);
|
||||
return this.isYouTube || this.isVimeo;
|
||||
}
|
||||
get isYouTube() {
|
||||
return this.provider === providers.youtube;
|
||||
}
|
||||
get isVimeo() {
|
||||
return this.provider === providers.vimeo;
|
||||
}
|
||||
get isVideo() {
|
||||
return this.type === types.video;
|
||||
}
|
||||
get isAudio() {
|
||||
return this.type === types.audio;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -518,11 +529,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Get audio tracks
|
||||
return (
|
||||
this.media.mozHasAudio ||
|
||||
Boolean(this.media.webkitAudioDecodedByteCount) ||
|
||||
Boolean(this.media.audioTracks && this.media.audioTracks.length)
|
||||
);
|
||||
return this.media.mozHasAudio || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -683,7 +690,7 @@ class Plyr {
|
||||
* @param {input} - the URL for the new poster image
|
||||
*/
|
||||
set poster(input) {
|
||||
if (!this.isHTML5 || this.type !== 'video') {
|
||||
if (!this.isHTML5 || !this.isVideo) {
|
||||
this.console.warn('Poster can only be set on HTML5 video');
|
||||
return;
|
||||
}
|
||||
@ -697,7 +704,7 @@ class Plyr {
|
||||
* Get the current poster image
|
||||
*/
|
||||
get poster() {
|
||||
if (!this.isHTML5 || this.type !== 'video') {
|
||||
if (!this.isHTML5 || !this.isVideo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -731,9 +738,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// If the method is called without parameter, toggle based on current value
|
||||
const show = utils.is.boolean(input)
|
||||
? input
|
||||
: this.elements.container.className.indexOf(this.config.classNames.captions.active) === -1;
|
||||
const show = utils.is.boolean(input) ? input : this.elements.container.className.indexOf(this.config.classNames.captions.active) === -1;
|
||||
|
||||
// Nothing to change...
|
||||
if (this.captions.enabled === show) {
|
||||
@ -828,11 +833,7 @@ class Plyr {
|
||||
this.fullscreen.active = !this.fullscreen.active;
|
||||
|
||||
// Add class hook
|
||||
utils.toggleClass(
|
||||
this.elements.container,
|
||||
this.config.classNames.fullscreen.fallback,
|
||||
this.fullscreen.active
|
||||
);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.fallback, this.fullscreen.active);
|
||||
|
||||
// Make sure we don't lose scroll position
|
||||
if (this.fullscreen.active) {
|
||||
@ -920,7 +921,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Don't hide if no UI support or it's audio
|
||||
if (!this.supported.ui || this.type === 'audio') {
|
||||
if (!this.supported.ui || this.isAudio) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -980,13 +981,13 @@ class Plyr {
|
||||
// then set the timer to hide the controls
|
||||
if (!show || this.playing) {
|
||||
this.timers.controls = window.setTimeout(() => {
|
||||
console.warn({
|
||||
/* this.console.warn({
|
||||
pressed: this.elements.controls.pressed,
|
||||
hover: this.elements.controls.pressed,
|
||||
playing: this.playing,
|
||||
paused: this.paused,
|
||||
loading: this.loading,
|
||||
});
|
||||
}); */
|
||||
|
||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
||||
@ -1105,8 +1106,18 @@ class Plyr {
|
||||
};
|
||||
|
||||
// Type specific stuff
|
||||
switch (this.type) {
|
||||
case 'youtube':
|
||||
switch (`${this.provider}:${this.type}`) {
|
||||
case 'html5:video':
|
||||
case 'html5:audio':
|
||||
// Restore native video controls
|
||||
ui.toggleNativeControls.call(this, true);
|
||||
|
||||
// Clean up
|
||||
done();
|
||||
|
||||
break;
|
||||
|
||||
case 'youtube:video':
|
||||
// Clear timers
|
||||
window.clearInterval(this.timers.buffering);
|
||||
window.clearInterval(this.timers.playing);
|
||||
@ -1119,7 +1130,7 @@ class Plyr {
|
||||
|
||||
break;
|
||||
|
||||
case 'vimeo':
|
||||
case 'vimeo:video':
|
||||
// Destroy Vimeo API
|
||||
// then clean up (wait, to prevent postmessage errors)
|
||||
this.embed.unload().then(done);
|
||||
@ -1129,16 +1140,6 @@ class Plyr {
|
||||
|
||||
break;
|
||||
|
||||
case 'video':
|
||||
case 'audio':
|
||||
// Restore native video controls
|
||||
ui.toggleNativeControls.call(this, true);
|
||||
|
||||
// Clean up
|
||||
done();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// Plyr source update
|
||||
// ==========================================================================
|
||||
|
||||
import types from './types';
|
||||
import { providers } from './types';
|
||||
import utils from './utils';
|
||||
import media from './media';
|
||||
import ui from './ui';
|
||||
@ -48,35 +48,25 @@ const source = {
|
||||
this.elements.container.removeAttribute('class');
|
||||
}
|
||||
|
||||
// Set the type
|
||||
if ('type' in input) {
|
||||
this.type = input.type;
|
||||
|
||||
// Get child type for video (it might be an embed)
|
||||
if (this.type === 'video') {
|
||||
const firstSource = input.sources[0];
|
||||
|
||||
if ('type' in firstSource && types.embed.includes(firstSource.type)) {
|
||||
this.type = firstSource.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set the type and provider
|
||||
this.type = input.type;
|
||||
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
||||
|
||||
// Check for support
|
||||
this.supported = support.check(this.type, this.config.inline);
|
||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
||||
|
||||
// Create new markup
|
||||
switch (this.type) {
|
||||
case 'video':
|
||||
switch (`${this.provider}:${this.type}`) {
|
||||
case 'html5:video':
|
||||
this.media = utils.createElement('video');
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
case 'html5:audio':
|
||||
this.media = utils.createElement('audio');
|
||||
break;
|
||||
|
||||
case 'youtube':
|
||||
case 'vimeo':
|
||||
case 'youtube:video':
|
||||
case 'vimeo:video':
|
||||
this.media = utils.createElement('div');
|
||||
this.embedId = input.sources[0].src;
|
||||
break;
|
||||
@ -117,7 +107,6 @@ const source = {
|
||||
|
||||
// Restore class hooks
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.supported.ui && this.captions.enabled);
|
||||
|
||||
ui.addStyleHook.call(this);
|
||||
|
||||
// Set new sources for html5
|
||||
|
@ -12,29 +12,29 @@ const support = {
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, inline) {
|
||||
check(type, provider, inline) {
|
||||
let api = false;
|
||||
let ui = false;
|
||||
const browser = utils.getBrowser();
|
||||
const playsInline = browser.isIPhone && inline && support.inline;
|
||||
|
||||
switch (type) {
|
||||
case 'video':
|
||||
switch (`${provider}:${type}`) {
|
||||
case 'html5:video':
|
||||
api = support.video;
|
||||
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
case 'html5:audio':
|
||||
api = support.audio;
|
||||
ui = api && support.rangeInput;
|
||||
break;
|
||||
|
||||
case 'youtube':
|
||||
case 'youtube:video':
|
||||
api = true;
|
||||
ui = support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
break;
|
||||
|
||||
case 'vimeo':
|
||||
case 'vimeo:video':
|
||||
api = true;
|
||||
ui = support.rangeInput && !browser.isIPhone;
|
||||
break;
|
||||
@ -92,12 +92,12 @@ const support = {
|
||||
|
||||
try {
|
||||
// Bail if no checking function
|
||||
if (!utils.is.function(media.canPlayType)) {
|
||||
if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type specific checks
|
||||
if (this.type === 'video') {
|
||||
if (this.isVideo) {
|
||||
switch (type) {
|
||||
case 'video/webm':
|
||||
return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
|
||||
@ -111,7 +111,7 @@ const support = {
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else if (this.type === 'audio') {
|
||||
} else if (this.isAudio) {
|
||||
switch (type) {
|
||||
case 'audio/mpeg':
|
||||
return media.canPlayType('audio/mpeg;').replace(/no/, '');
|
||||
|
@ -1,10 +1,16 @@
|
||||
// ==========================================================================
|
||||
// Plyr supported types
|
||||
// Plyr supported types and providers
|
||||
// ==========================================================================
|
||||
|
||||
const types = {
|
||||
embed: ['youtube', 'vimeo'],
|
||||
html5: ['video', 'audio'],
|
||||
export const providers = {
|
||||
html5: 'html5',
|
||||
youtube: 'youtube',
|
||||
vimeo: 'vimeo',
|
||||
};
|
||||
|
||||
export default types;
|
||||
export const types = {
|
||||
audio: 'audio',
|
||||
video: 'video',
|
||||
};
|
||||
|
||||
export default { providers, types };
|
||||
|
@ -31,7 +31,7 @@ const ui = {
|
||||
|
||||
// Don't setup interface if no support
|
||||
if (!this.supported.ui) {
|
||||
this.console.warn(`Basic support only for ${this.type}`);
|
||||
this.console.warn(`Basic support only for ${this.provider} ${this.type}`);
|
||||
|
||||
// Remove controls
|
||||
utils.removeElement.call(this, 'controls');
|
||||
|
@ -73,24 +73,39 @@ const utils = {
|
||||
|
||||
// Load an external script
|
||||
loadScript(url, callback) {
|
||||
// Check script is not already referenced
|
||||
if (document.querySelectorAll(`script[src="${url}"]`).length) {
|
||||
const current = document.querySelector(`script[src="${url}"]`);
|
||||
|
||||
// Check script is not already referenced, if so wait for load
|
||||
if (current !== null) {
|
||||
current.callbacks = current.callbacks || [];
|
||||
current.callbacks.push(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the element
|
||||
const element = document.createElement('script');
|
||||
element.src = url;
|
||||
|
||||
// Find first script
|
||||
const first = document.getElementsByTagName('script')[0];
|
||||
// Callback queue
|
||||
element.callbacks = element.callbacks || [];
|
||||
element.callbacks.push(callback);
|
||||
|
||||
// Bind callback
|
||||
if (utils.is.function(callback)) {
|
||||
element.addEventListener('load', event => callback.call(null, event), false);
|
||||
element.addEventListener(
|
||||
'load',
|
||||
event => {
|
||||
element.callbacks.forEach(cb => cb.call(null, event));
|
||||
element.callbacks = null;
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Set the URL after binding callback
|
||||
element.src = url;
|
||||
|
||||
// Inject
|
||||
const first = document.getElementsByTagName('script')[0];
|
||||
first.parentNode.insertBefore(element, first);
|
||||
},
|
||||
|
||||
|
Reference in New Issue
Block a user