Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,18 @@
:default-value="hideChapters"
@change="updateHideChapters"
/>
<FtToggleSwitch
:label="t('Settings.Distraction Free Settings.Hide AI Content Label')"
:compact="true"
:default-value="hideAiLabel"
@change="updateHideAiLabel"
/>
<FtToggleSwitch
:label="t('Settings.Distraction Free Settings.Hide How this was made Description')"
:compact="true"
:default-value="hideHowThisWasMade"
@change="updateHideHowThisWasMade"
/>
<FtToggleSwitch
:label="t('Settings.Distraction Free Settings.Hide Video Description')"
:compact="true"
Expand Down Expand Up @@ -492,6 +504,26 @@ function updateHideChapters(value) {
store.dispatch('updateHideChapters', value)
}

/** @type {import('vue').ComputedRef<boolean>} */
const hideAiLabel = computed(() => store.getters.getHideAiLabel)

/**
* @param {boolean} value
*/
function updateHideAiLabel(value) {
store.dispatch('updateHideAiLabel', value)
}

/** @type {import('vue').ComputedRef<boolean>} */
const hideHowThisWasMade = computed(() => store.getters.getHideHowThisWasMade)

/**
* @param {boolean} value
*/
function updateHideHowThisWasMade(value) {
store.dispatch('updateHideHowThisWasMade', value)
}

/** @type {import('vue').ComputedRef<boolean>} */
const hideFeaturedChannels = computed(() => store.getters.getHideFeaturedChannels)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,29 @@
.license {
margin-block: 1em;
}

.aiDisclosureSection {
margin-block-start: 1.5em;
}

.aiDisclosureTitle {
font-size: 20px;
font-weight: 600;
margin-block-end: 12px;
}

.aiDisclosureHeader {
font-weight: 600;
font-size: 15px;
margin-block-end: 4px;
}

.aiDisclosureBody {
font-size: 14px;
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
line-height: 1.4;
opacity: 0.9;
}

.aiDisclosureItem + .aiDisclosureItem {
margin-block-start: 12px;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<FtCard
v-if="shownDescription.length > 0"
v-if="shownDescription.length > 0 || showHowThisWasMade"
:class="{ videoDescription: true, short: !showFullDescription }"
>
<span
Expand All @@ -14,6 +14,7 @@
{{ $t("Description.Expand Description") }}
</span>
<FtTimestampCatcher
v-if="shownDescription.length > 0"
ref="descriptionContainer"
class="description"
:input-html="processedShownDescription"
Expand All @@ -27,6 +28,29 @@
>
{{ license }}
</bdi>
<div
v-if="showHowThisWasMade && showFullDescription"
class="aiDisclosureSection"
>
<div class="aiDisclosureTitle">
{{ contentDisclosures.title || $t("Video.How this was made") }}
</div>
<div
v-for="(item, index) in contentDisclosures.items"
:key="index"
class="aiDisclosureItem"
>
<div class="aiDisclosureHeader">
{{ item.label }}
</div>
<div
v-if="item.description"
class="aiDisclosureBody"
>
{{ item.description }}
</div>
</div>
</div>
<span
v-if="showControls && showFullDescription"
class="descriptionStatus"
Expand All @@ -46,6 +70,7 @@ import autolinker from 'autolinker'
import { onMounted, ref, computed, useTemplateRef } from 'vue'
import FtCard from '../ft-card/ft-card.vue'
import FtTimestampCatcher from '../FtTimestampCatcher.vue'
import store from '../../store'

const props = defineProps({
description: {
Expand All @@ -59,9 +84,18 @@ const props = defineProps({
license: {
type: String,
default: null,
},
contentDisclosures: {
type: Object,
default: null
}
})

/** @type {import('vue').ComputedRef<boolean>} */
const hideHowThisWasMade = computed(() => store.getters.getHideHowThisWasMade)

const showHowThisWasMade = computed(() => (props.contentDisclosures?.items?.length ?? 0) > 0 && !hideHowThisWasMade.value)

const emit = defineEmits(['timestamp-event'])

let shownDescription = ''
Expand Down Expand Up @@ -135,7 +169,10 @@ function collapseDescription() {
*/
function isShortDescription() {
const descriptionElem = descriptionContainer.value?.$el
return descriptionElem?.clientHeight >= descriptionElem?.scrollHeight
if (!descriptionElem) {
return true
Comment thread
Shadorc marked this conversation as resolved.
Outdated
}
return descriptionElem.clientHeight >= descriptionElem.scrollHeight
}

onMounted(() => {
Expand Down
14 changes: 12 additions & 2 deletions src/renderer/components/WatchVideoInfo/WatchVideoInfo.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@
gap: 10px;
}

.unlistedBadge {
.videoBadges {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-block-start: 4px;
}

.unlistedBadge,
.aiBadge {
background-color: var(--secondary-card-bg-color);
border-radius: 5px;
display: inline;
display: inline-flex;
align-items: center;
gap: 5px;
padding-inline: 5px;
padding-block: 2px;
white-space: nowrap;
Expand Down
30 changes: 27 additions & 3 deletions src/renderer/components/WatchVideoInfo/WatchVideoInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,23 @@
{{ title }}
</h1>
<div
v-if="isUnlisted"
class="unlistedBadge"
v-if="isUnlisted || showAiBadge"
class="videoBadges"
>
{{ t('Video.Unlisted') }}
<div
v-if="isUnlisted"
class="unlistedBadge"
>
{{ t('Video.Unlisted') }}
</div>
<div
v-if="showAiBadge"
class="aiBadge"
:title="aiTooltipText"
>
<FontAwesomeIcon :icon="['fas', 'robot']" />
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
<span>{{ t('Video.AI') }}</span>
</div>
</div>
</div>
<div class="videoMetrics">
Expand Down Expand Up @@ -231,6 +244,10 @@ const props = defineProps({
type: Boolean,
required: false
},
contentDisclosures: {
type: Object,
default: null
},
canSaveWatchedProgress: {
type: Boolean,
required: true
Expand All @@ -247,6 +264,13 @@ const USING_ELECTRON = process.env.IS_ELECTRON

const { locale, t } = useI18n()

/** @type {import('vue').ComputedRef<boolean>} */
const hideAiLabel = computed(() => store.getters.getHideAiLabel)

const showAiBadge = computed(() => props.contentDisclosures?.hasAI && !hideAiLabel.value)

const aiTooltipText = computed(() => props.contentDisclosures?.aiItem?.description || props.contentDisclosures?.aiItem?.label || t('Video.AI'))

/** @type {import('vue').ComputedRef<boolean>} */
const hideSharingActions = computed(() => store.getters.getHideSharingActions)

Expand Down
45 changes: 44 additions & 1 deletion src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ export async function getLocalVideoInfo(id) {

if ((info.playability_status.status === 'UNPLAYABLE' && (!hasTrailer || trailerIsAgeRestricted)) ||
info.playability_status.status === 'LOGIN_REQUIRED') {
return { info, poToken: undefined, clientInfo }
return { info, poToken: undefined, clientInfo, contentDisclosures: extractLocalContentDisclosures(info) }
}

if (hasTrailer && info.playability_status.status !== 'OK') {
Expand Down Expand Up @@ -767,6 +767,49 @@ export async function getLocalVideoInfo(id) {
poToken: contentPoToken,
clientInfo,
adEndTimeUnixMs,
contentDisclosures: extractLocalContentDisclosures(info),
}
}

/**
* Extracts content disclosures ('How this was made') from YouTube VideoInfo response
* @param {import('youtubei.js').YT.VideoInfo} info
* @returns {{ title: string, hasAI: boolean, aiItem: { label: string, description: string } | null, items: Array<{ label: string, description: string, isAI: boolean }> } | null}
*/
export function extractLocalContentDisclosures(info) {
Comment thread
caetano-dev marked this conversation as resolved.
if (!info) {
return null
}

const structuredDescPanel = info.page?.[1]?.engagement_panels?.find(
panel => panel.target_id === 'engagement-panel-structured-description'
)

const rawItems = structuredDescPanel?.content?.items?.filter(
item => item.type === 'HowThisWasMadeSectionView' || item.type === 'howThisWasMadeSectionViewModel'
)

if (!rawItems?.length) {
return null
}

const items = rawItems.map(item => {
const label = item.body_header?.text || item.bodyHeader?.content || ''
const rawDescription = item.body_text?.text || item.bodyText?.content || ''
return {
label,
description: rawDescription.replace(/\s*Learn more\.?$/i, '').trim(),
isAI: /\b(ai|artificial intelligence|altered|synthetic)\b/i.test(label)
}
})

const aiItem = items.find(item => item.isAI) || null

return {
title: rawItems[0]?.section_title?.text || rawItems[0]?.sectionTitle?.content || null,
hasAI: !!aiItem,
aiItem,
items
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/renderer/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
faQuestionCircle,
faRandom,
faRetweet,
faRobot,
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
faRss,
faSatelliteDish,
faSave,
Expand Down Expand Up @@ -215,6 +216,7 @@ library.add(
faQuestionCircle,
faRandom,
faRetweet,
faRobot,
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
faRss,
faSatelliteDish,
faSave,
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/store/modules/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ const state = {
unsubscriptionPopupStatus: false,
hideLabelsSideBar: false,
hideChapters: false,
hideAiLabel: false,
hideHowThisWasMade: false,
showDistractionFreeTitles: false,
landingPage: 'subscriptions',
listType: 'grid',
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/views/Watch/Watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export default defineComponent({
/** @type {string[]|null} */
customErrorIcon: null,
videoGenreIsMusic: false,
contentDisclosures: null,
/** @type {Date|null} */
streamingDataExpiryDate: null,
currentPlaybackRate: null,
Expand Down Expand Up @@ -400,6 +401,7 @@ export default defineComponent({
this.videoDescription = ''
this.videoDescriptionHtml = ''
this.license = ''
this.contentDisclosures = null
this.videoViewCount = 0
this.videoLikeCount = 0
this.videoDislikeCount = 0
Expand Down Expand Up @@ -502,7 +504,8 @@ export default defineComponent({

try {
const videoInfo = await getLocalVideoInfo(this.videoId)
const { info: result, poToken, clientInfo, adEndTimeUnixMs } = videoInfo
const { info: result, poToken, clientInfo, adEndTimeUnixMs, contentDisclosures } = videoInfo
this.contentDisclosures = contentDisclosures || null

const playabilityStatus = result.playability_status
this.playabilityStatus = playabilityStatus.status
Expand Down Expand Up @@ -1030,6 +1033,7 @@ export default defineComponent({
this.isFamilyFriendly = result.isFamilyFriendly
this.isPostLiveDvr = !!result.isPostLiveDvr
this.isUnlisted = !result.isListed
this.contentDisclosures = null

this.captions = sortCaptions(result.captions.map(caption => {
return {
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/views/Watch/Watch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
:video-thumbnail="thumbnail"
:in-user-playlist="!!selectedUserPlaylist"
:is-unlisted="isUnlisted"
:content-disclosures="contentDisclosures"
:can-save-watched-progress="canSaveWatchProgress"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
Expand All @@ -172,6 +173,7 @@
:description="videoDescription"
:description-html="videoDescriptionHtml"
:license="license"
:content-disclosures="contentDisclosures"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
@timestamp-event="changeTimestamp"
Expand Down
5 changes: 5 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,8 @@ Settings:
Hide Sharing Actions: Hide Sharing Actions
Hide Videos on Watch: Hide Videos on Watch
Hide Chapters: Hide Chapters
Hide AI Content Label: Hide AI Content Label
Hide How this was made Description: Hide "How this was made" Description
Hide Channels: Hide Videos From Channels
Hide Channels Disabled Message: Some channels were blocked using ID and weren't processed. Feature is blocked while those IDs are updating
Hide Channels Placeholder: Channel ID
Expand Down Expand Up @@ -889,6 +891,9 @@ Video:
Premieres: Premieres
Upcoming: Upcoming
Unlisted: Unlisted
AI: AI
Made with AI: Made with AI
How this was made: How this was made
Live: Live
Live Now: Live Now
Live Chat: Live Chat
Expand Down