1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-06 03:50:26 +02:00

Improve NSFW warning in player

This commit is contained in:
Chocobozzz 2025-07-09 15:32:57 +02:00
parent f2556d80e3
commit ee96cf3a19
No known key found for this signature in database
GPG key ID: 583A612D890159BE
10 changed files with 249 additions and 138 deletions

View file

@ -13,6 +13,7 @@ my-select-checkbox {
.playlists { .playlists {
.pt-badge { .pt-badge {
max-width: 200px; max-width: 200px;
vertical-align: bottom;
@include ellipsis; @include ellipsis;
} }

View file

@ -78,6 +78,9 @@
--header-fg: #{pvar(--fg)}; --header-fg: #{pvar(--fg)};
--header-bg: #{pvar(--bg)}; --header-bg: #{pvar(--bg)};
--player-overlay-fg: #{pvar(--fg-400)};
--player-overlay-bg: #{pvar(--bg-secondary-400)};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
--tmp-header-height: #{$header-height}; --tmp-header-height: #{$header-height};

View file

@ -204,7 +204,9 @@ $variables: (
--alert-primary-bg: var(--alert-primary-bg), --alert-primary-bg: var(--alert-primary-bg),
--alert-primary-border-color: var(--alert-primary-border-color), --alert-primary-border-color: var(--alert-primary-border-color),
--embed-fg: var(--embed-fg), --embed-fg: var(--embed-fg),
--embed-big-play-bg: var(--embed-big-play-bg) --embed-big-play-bg: var(--embed-big-play-bg),
--player-overlay-fg: var(--player-overlay-fg),
--player-overlay-bg: var(--player-overlay-bg)
); );
// SASS type check our CSS variables // SASS type check our CSS variables

View file

@ -20,7 +20,7 @@ import './shared/control-bar/theater-button'
import './shared/control-bar/time-tooltip' import './shared/control-bar/time-tooltip'
import './shared/dock/peertube-dock-component' import './shared/dock/peertube-dock-component'
import './shared/dock/peertube-dock-plugin' import './shared/dock/peertube-dock-plugin'
import './shared/nsfw/peertube-nsfw-component' import './shared/nsfw/peertube-nsfw-info-component'
import './shared/nsfw/peertube-nsfw-plugin' import './shared/nsfw/peertube-nsfw-plugin'
import './shared/hotkeys/peertube-hotkeys-plugin' import './shared/hotkeys/peertube-hotkeys-plugin'
import './shared/metrics/metrics-plugin' import './shared/metrics/metrics-plugin'

View file

@ -1,49 +1,54 @@
@use 'sass:math'; @use "sass:math";
@use '_variables' as *; @use "_variables" as *;
@use '_mixins' as *; @use "_mixins" as *;
@use '_icons' as *; @use "_icons" as *;
@use './_player-variables' as *; @use "./_player-variables" as *;
.video-js.vjs-peertube-skin { .video-js.vjs-peertube-skin {
--container-margin-x: 20px; --nsfw-info-margin-x: 20px;
--container-margin-y: 20px; --nsfw-info-margin-y: 20px;
--nsfw-font-size: 13px;
&.vjs-size-570 { &.vjs-size-570 {
--container-margin-x: 10px; --nsfw-info-margin-x: 10px;
--container-margin-y: 10px; --nsfw-info-margin-y: 10px;
}
.nsfw-container {
font-size: 14px;
position: absolute;
top: var(--container-margin-y);
right: var(--container-margin-x);
width: 100%;
width: fit-content;
background-color: pvar(--bg-secondary-500);
color: pvar(--fg-400);
max-width: calc(40% - 2 * var(--container-margin-x));
max-height: calc(100% - 2 * var(--container-margin-y));
padding: 1rem;;
border-radius: 4px;
overflow: auto;
.nsfw-title {
font-size: 1.25rem;
font-weight: $font-bold;
margin-bottom: 0.5rem;
}
button,
.nsfw-more-flags,
.nsfw-more-summary {
margin-top: 0.75rem;
font-size: 13px;
} }
button { button {
cursor: pointer;
}
.nsfw-info,
.nsfw-details {
width: 100%;
width: fit-content;
background-color: pvar(--player-overlay-bg);
color: pvar(--player-overlay-fg);
max-width: calc(40% - 2 * var(--nsfw-info-margin-x));
padding: 1rem;
}
// ---------------------------------------------------------------------------
// NSFW info
// ---------------------------------------------------------------------------
.nsfw-info {
position: absolute;
top: var(--nsfw-info-margin-y);
right: var(--nsfw-info-margin-x);
max-height: calc(100% - 2 * var(--nsfw-info-margin-y));
font-size: var(--nsfw-font-size);
border-radius: 4px;
overflow: auto;
strong {
display: block;
}
button {
margin-top: 0.75rem;
padding: 0; padding: 0;
color: pvar(--fg-450);
text-decoration: underline; text-decoration: underline;
&:hover { &:hover {
@ -55,20 +60,63 @@
@include margin-left(5px); @include margin-left(5px);
} }
} }
.nsfw-more-content {
strong {
display: block;
margin-bottom: 5px;
}
}
} }
&.peertube-dock { &.peertube-dock {
.nsfw-container { .nsfw-info {
top: unset; top: unset;
bottom: var(--container-margin-y); bottom: var(--nsfw-info-margin-y);
max-width: 90%; max-width: 90%;
} }
} }
// ---------------------------------------------------------------------------
// NSFW details
// ---------------------------------------------------------------------------
.nsfw-details-container {
background-color: rgba(0, 0, 0, 0.6);
position: absolute;
top: 0;
right: 0;
z-index: 1000;
width: 100%;
height: 100%;
}
.nsfw-details {
margin-left: auto;
height: 100%;
.nsfw-details-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.5;
height: 100%;
}
.nsfw-details-flags + .nsfw-details-summary {
margin-top: 1rem;
}
.nsfw-details-close {
mask-image: url("./svg/x.svg");
-webkit-mask-image: url("./svg/x.svg");
mask-size: cover;
-webkit-mask-size: cover;
width: 20px;
height: 20px;
background-color: pvar(--player-overlay-fg);
display: block;
@include margin-left(auto);
&:hover {
opacity: 0.8;
}
}
}
} }

View file

@ -1,88 +0,0 @@
import { NSFWFlag } from '@peertube/peertube-models'
import videojs from 'video.js'
import { type PeerTubeNSFWPluginOptions } from './peertube-nsfw-plugin'
const Component = videojs.getComponent('Component')
class PeerTubeNSFWComponent extends Component {
declare options_: videojs.ComponentOptions & PeerTubeNSFWPluginOptions
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeNSFWPluginOptions) {
super(player, options)
}
createEl () {
const el = super.createEl('div', { className: 'nsfw-container' })
const title = super.createEl('div', { className: 'nsfw-title' })
title.textContent = this.player().localize('Sensitive content')
const content = super.createEl('div', { className: 'nsfw-content' })
content.textContent = this.player().localize('This video contains sensitive content.')
el.appendChild(title)
el.appendChild(content)
if (this.options_.flags || this.options_.summary) {
const moreButton = super.createEl(
'button',
{ textContent: this.player().localize('Learn more') },
{ type: 'button' }
) as HTMLButtonElement
el.appendChild(moreButton)
moreButton.addEventListener('click', () => {
this.appendMoreContent()
moreButton.style.display = 'none'
})
}
return el
}
private appendMoreContent () {
const moreContentEl = super.createEl('div', { className: 'nsfw-more-content' })
if (this.options_.flags) {
const moreContentFlags = super.createEl('div', { className: 'nsfw-more-flags' })
moreContentFlags.appendChild(super.createEl('strong', { textContent: this.player().localize('Content warning') }))
moreContentFlags.appendChild(super.createEl('div', { textContent: this.buildFlagStrings().join(' - ') }))
moreContentEl.appendChild(moreContentFlags)
}
if (this.options_.summary) {
const moreContentSummary = super.createEl('div', { className: 'nsfw-more-summary' })
moreContentSummary.appendChild(super.createEl('strong', { textContent: `Author note` }))
moreContentSummary.appendChild(super.createEl('div', { textContent: this.options_.summary }))
moreContentEl.appendChild(moreContentSummary)
}
this.el().appendChild(moreContentEl)
}
private buildFlagStrings () {
const flags = this.options_.flags
const flagStrings: string[] = []
if ((flags & NSFWFlag.VIOLENT) === NSFWFlag.VIOLENT) {
flagStrings.push(this.player().localize(`Violence`))
}
if ((flags & NSFWFlag.EXPLICIT_SEX) === NSFWFlag.EXPLICIT_SEX) {
flagStrings.push(this.player().localize(`Explicit Sex`))
}
return flagStrings
}
}
videojs.registerComponent('PeerTubeNSFWComponent', PeerTubeNSFWComponent)
export {
PeerTubeNSFWComponent
}

View file

@ -0,0 +1,82 @@
import { NSFWFlag } from '@peertube/peertube-models'
import videojs from 'video.js'
import { type PeerTubeNSFWPluginOptions } from './peertube-nsfw-plugin'
const Component = videojs.getComponent('Component')
class PeerTubeNSFWDetailsComponent extends Component {
declare options_: videojs.ComponentOptions & PeerTubeNSFWPluginOptions
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeNSFWPluginOptions) {
super(player, options)
}
createEl () {
// Overlay
const el = super.createEl('div', { className: 'nsfw-details-container' })
const nsfwDetails = super.createEl('div', { className: 'nsfw-details' })
el.appendChild(nsfwDetails)
const closeButton = super.createEl('button', {
className: 'nsfw-details-close',
title: this.player().localize('Close details'),
type: 'button'
}) as HTMLButtonElement
closeButton.addEventListener('click', () => {
this.trigger('hideDetails')
})
nsfwDetails.appendChild(closeButton)
const content = super.createEl('div', { className: 'nsfw-details-content' })
nsfwDetails.appendChild(content)
if (this.options_.flags) {
const flags = super.createEl('div', { className: 'nsfw-details-flags' })
const label = super.createEl('strong', { textContent: this.player().localize('This video contains sensitive content, including:') })
flags.appendChild(label)
flags.appendChild(super.createEl('div', { textContent: this.buildFlagStrings().join(' - ') }))
content.appendChild(flags)
}
if (this.options_.summary) {
const summary = super.createEl('div', { className: 'nsfw-details-summary' })
const label = super.createEl('strong', { textContent: this.player().localize('Uploader note:') })
summary.appendChild(label)
summary.appendChild(super.createEl('div', { textContent: this.options_.summary }))
content.appendChild(summary)
}
return el
}
private buildFlagStrings () {
const flags = this.options_.flags
const flagStrings: string[] = []
if ((flags & NSFWFlag.VIOLENT) === NSFWFlag.VIOLENT) {
flagStrings.push(this.player().localize(`Potentially violent content`))
}
if ((flags & NSFWFlag.EXPLICIT_SEX) === NSFWFlag.EXPLICIT_SEX) {
flagStrings.push(this.player().localize(`Potentially sexually explicit content`))
}
return flagStrings
}
}
videojs.registerComponent('PeerTubeNSFWDetailsComponent', PeerTubeNSFWDetailsComponent)
export {
PeerTubeNSFWDetailsComponent
}

View file

@ -0,0 +1,43 @@
import videojs from 'video.js'
import { type PeerTubeNSFWPluginOptions } from './peertube-nsfw-plugin'
const Component = videojs.getComponent('Component')
class PeerTubeNSFWInfoComponent extends Component {
declare options_: videojs.ComponentOptions & PeerTubeNSFWPluginOptions
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeNSFWPluginOptions) {
super(player, options)
}
createEl () {
const el = super.createEl('div', { className: 'nsfw-info' })
const content = super.createEl('strong')
content.textContent = this.player().localize('This video contains sensitive content.')
el.appendChild(content)
if (this.options_.flags || this.options_.summary) {
const moreButton = super.createEl(
'button',
{ textContent: this.player().localize('Learn more') },
{ type: 'button' }
) as HTMLButtonElement
el.appendChild(moreButton)
moreButton.addEventListener('click', () => {
this.trigger('showDetails')
})
}
return el
}
}
videojs.registerComponent('PeerTubeNSFWInfoComponent', PeerTubeNSFWInfoComponent)
export {
PeerTubeNSFWInfoComponent
}

View file

@ -1,5 +1,6 @@
import videojs from 'video.js' import videojs from 'video.js'
import { PeerTubeNSFWComponent } from './peertube-nsfw-component' import { PeerTubeNSFWInfoComponent } from './peertube-nsfw-info-component'
import { PeerTubeNSFWDetailsComponent } from './peertube-nsfw-details-component'
const Plugin = videojs.getPlugin('plugin') const Plugin = videojs.getPlugin('plugin')
@ -9,7 +10,8 @@ export type PeerTubeNSFWPluginOptions = {
} }
class PeerTubeNSFWPlugin extends Plugin { class PeerTubeNSFWPlugin extends Plugin {
declare private nsfwComponent: PeerTubeNSFWComponent declare private nsfwInfoComponent: PeerTubeNSFWInfoComponent
declare private nsfwDetailsComponent: PeerTubeNSFWDetailsComponent
constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeNSFWPluginOptions) { constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeNSFWPluginOptions) {
super(player, options) super(player, options)
@ -17,18 +19,33 @@ class PeerTubeNSFWPlugin extends Plugin {
player.ready(() => { player.ready(() => {
player.addClass('peertube-nsfw') player.addClass('peertube-nsfw')
this.nsfwComponent = new PeerTubeNSFWComponent(player, options) this.nsfwInfoComponent = new PeerTubeNSFWInfoComponent(player, options)
player.addChild(this.nsfwComponent) player.addChild(this.nsfwInfoComponent)
this.nsfwDetailsComponent = new PeerTubeNSFWDetailsComponent(player, options)
this.nsfwDetailsComponent.hide()
player.addChild(this.nsfwDetailsComponent)
this.nsfwInfoComponent.on('showDetails', () => {
this.nsfwDetailsComponent.show()
this.nsfwInfoComponent.hide()
})
this.nsfwDetailsComponent.on('hideDetails', () => {
this.nsfwInfoComponent.show()
this.nsfwDetailsComponent.hide()
})
}) })
player.one('play', () => { player.one('play', () => {
this.nsfwComponent.hide() this.nsfwInfoComponent.hide()
}) })
} }
dispose () { dispose () {
this.nsfwComponent?.dispose() this.nsfwInfoComponent?.dispose()
this.player.removeChild(this.nsfwComponent) this.player.removeChild(this.nsfwInfoComponent)
this.player.removeChild(this.nsfwDetailsComponent)
this.player.removeClass('peertube-nsfw') this.player.removeClass('peertube-nsfw')
super.dispose() super.dispose()

View file

@ -87,13 +87,16 @@ const playerKeys = {
'Audio only': 'Audio only', 'Audio only': 'Audio only',
'Sensitive content': 'Sensitive content', 'Sensitive content': 'Sensitive content',
'This video contains sensitive content.': 'This video contains sensitive content.', 'This video contains sensitive content.': 'This video contains sensitive content.',
'This video contains sensitive content, including:': 'This video contains sensitive content, including:',
'Learn more': 'Learn more', 'Learn more': 'Learn more',
'Content warning': 'Content warning', 'Content warning': 'Content warning',
'Violence': 'Violence', 'Violence': 'Violence',
'Shocking Content': 'Shocking Content', 'Shocking Content': 'Shocking Content',
'Explicit Sex': 'Explicit Sex', 'Explicit Sex': 'Explicit Sex',
'Upload speed:': 'Upload speed:', 'Upload speed:': 'Upload speed:',
'Download speed:': 'Download speed:' 'Download speed:': 'Download speed:',
'Uploader note:': 'Uploader note:',
'Close': 'Close'
} }
Object.assign(playerKeys, videojs) Object.assign(playerKeys, videojs)