Skip to content

Commit

Permalink
Migrate WatchVideoRecommendations, WatchVideoDescription and WatchVid…
Browse files Browse the repository at this point in the history
…eoChapters to the composition API (#5556)

* Migrate WatchVideoRecommendations to the composition API

* Migrate WatchVideoDescription to the composition API

* Migrate WatchVideoChapters to the composition API
  • Loading branch information
absidue authored Aug 13, 2024
1 parent 27d7ba6 commit 122db20
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 343 deletions.
191 changes: 191 additions & 0 deletions src/renderer/components/WatchVideoChapters/WatchVideoChapters.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<template>
<FtCard class="videoChapters">
<h3
class="chaptersTitle"
tabindex="0"
:aria-label="showChapters
? $t('Chapters.Chapters list visible, current chapter: {chapterName}', { chapterName: currentTitle })
: $t('Chapters.Chapters list hidden, current chapter: {chapterName}', { chapterName: currentTitle })
"
:aria-pressed="showChapters"
@click="toggleShowChapters"
@keydown.space.stop.prevent="toggleShowChapters"
@keydown.enter.stop.prevent="toggleShowChapters"
>
{{ $t("Chapters.Chapters") }}

<span class="currentChapter">
• {{ currentTitle }}
</span>

<FontAwesomeIcon
class="chaptersChevron"
:icon="['fas', 'chevron-right']"
:rotation="showChapters ? 90 : null"
:class="{ open: showChapters }"
/>
</h3>
<div
v-show="showChapters"
ref="chaptersWrapper"
v-observe-visibility="observeVisibilityOptions"
class="chaptersWrapper"
:class="{ compact }"
@keydown.arrow-up.stop.prevent="navigateChapters('up')"
@keydown.arrow-down.stop.prevent="navigateChapters('down')"
>
<div
v-for="(chapter, index) in chapters"
:key="index"
:ref="index === currentIndex ? 'currentChaptersItem' : null"
class="chapter"
role="button"
tabindex="0"
:aria-selected="index === currentIndex"
:class="{ current: index === currentIndex }"
@click="changeChapter(index)"
@keydown.space.stop.prevent="changeChapter(index)"
@keydown.enter.stop.prevent="changeChapter(index)"
>
<!-- Setting the aspect ratio avoids layout shifts when the images load -->
<img
v-if="!compact"
alt=""
aria-hidden="true"
class="chapterThumbnail"
loading="lazy"
:src="chapter.thumbnail.url"
:style="{ aspectRatio: chapter.thumbnail.width / chapter.thumbnail.height }"
>
<div class="chapterTimestamp">
{{ chapter.timestamp }}
</div>
<p class="chapterTitle">
{{ chapter.title }}
</p>
</div>
</div>
</FtCard>
</template>

<script setup>
import { computed, nextTick, ref, watch } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import FtCard from '../ft-card/ft-card.vue'
const props = defineProps({
chapters: {
type: Array,
required: true
},
currentChapterIndex: {
type: Number,
required: true
}
})
const emit = defineEmits(['timestamp-event'])
/** @type {import('vue').Ref<HTMLDivElement | null>} */
const chaptersWrapper = ref(null)
/** @type {import('vue').Ref<HTMLDivElement[]>} */
const currentChaptersItem = ref([])
const showChapters = ref(false)
const currentIndex = ref(props.currentChapterIndex)
watch(() => props.currentChapterIndex, (value) => {
if (currentIndex.value !== value) {
currentIndex.value = value
}
})
const currentChapter = computed(() => {
return props.chapters[currentIndex.value]
})
const currentTitle = computed(() => {
return currentChapter.value.title
})
const compact = computed(() => {
return !props.chapters[0].thumbnail
})
const observeVisibilityOptions = computed(() => {
return {
callback: (isVisible, _entry) => {
// This is also fired when **hidden**
// No point doing anything if not visible
if (!isVisible) { return }
// Only auto scroll when expanded
if (!showChapters.value) { return }
scrollToCurrentChapter()
},
intersection: {
// Only when it intersects with N% above bottom
rootMargin: '0% 0% 0% 0%',
},
// Callback responsible for scolling to current chapter multiple times
once: false,
}
})
/**
* @param {number} index
*/
function changeChapter(index) {
currentIndex.value = index
emit('timestamp-event', props.chapters[index].startSeconds)
window.scrollTo(0, 0)
}
/**
* @param {'up' | 'down'} direction
*/
function navigateChapters(direction) {
const chapterElements = Array.from(chaptersWrapper.value.children)
const focusedIndex = chapterElements.indexOf(document.activeElement)
let newIndex = focusedIndex
if (direction === 'up') {
if (focusedIndex === 0) {
newIndex = chapterElements.length - 1
} else {
newIndex--
}
} else {
if (focusedIndex === chapterElements.length - 1) {
newIndex = 0
} else {
newIndex++
}
}
chapterElements[newIndex].focus()
}
function toggleShowChapters() {
showChapters.value = !showChapters.value
if (showChapters.value) {
scrollToCurrentChapter()
}
}
function scrollToCurrentChapter() {
const container = chaptersWrapper.value
const currentItem = currentChaptersItem.value[0]
// Must wait until rendering done after value change
nextTick(() => {
if (container != null && currentItem != null) {
container.scrollTop = currentItem.offsetTop - container.offsetTop
}
})
}
</script>

<style scoped src="./WatchVideoChapters.css" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<template>
<FtCard
v-if="shownDescription.length > 0"
class="videoDescription"
>
<FtTimestampCatcher
class="description"
:input-html="shownDescription"
@timestamp-event="onTimestamp"
/>
</FtCard>
</template>

<script setup>
import autolinker from 'autolinker'
import FtCard from '../ft-card/ft-card.vue'
import FtTimestampCatcher from '../ft-timestamp-catcher/ft-timestamp-catcher.vue'
const props = defineProps({
description: {
type: String,
required: true
},
descriptionHtml: {
type: String,
default: ''
}
})
const emit = defineEmits(['timestamp-event'])
let shownDescription = ''
if (props.descriptionHtml !== '') {
const parsed = parseDescriptionHtml(props.descriptionHtml)
// the invidious API returns emtpy html elements when the description is empty
// so we need to parse it to see if there is any meaningful text in the html
// or if it's just empty html elements e.g. `<p></p>`
const testDiv = document.createElement('div')
testDiv.innerHTML = parsed
if (!/^\s*$/.test(testDiv.innerText)) {
shownDescription = parsed
}
} else {
if (!/^\s*$/.test(props.description)) {
shownDescription = autolinker.link(props.description)
}
}
/**
* @param {number} timestamp
*/
function onTimestamp(timestamp) {
emit('timestamp-event', timestamp)
}
/**
* @param {string} descriptionText
*/
function parseDescriptionHtml(descriptionText) {
return descriptionText
.replaceAll('target="_blank"', '')
.replaceAll(/\/redirect.+?(?=q=)/g, '')
.replaceAll('q=', '')
.replaceAll(/rel="nofollow\snoopener"/g, '')
.replaceAll(/class=.+?(?=")./g, '')
.replaceAll(/id=.+?(?=")./g, '')
.replaceAll(/data-target-new-window=.+?(?=")./g, '')
.replaceAll(/data-url=.+?(?=")./g, '')
.replaceAll(/data-sessionlink=.+?(?=")./g, '')
.replaceAll('&amp;', '&')
.replaceAll('%3A', ':')
.replaceAll('%2F', '/')
.replaceAll(/&v.+?(?=")/g, '')
.replaceAll(/&redirect-token.+?(?=")/g, '')
.replaceAll(/&redir_token.+?(?=")/g, '')
.replaceAll('href="/', 'href="https://www.youtube.com/')
.replaceAll('href="/hashtag/', 'href="https://wwww.youtube.com/hashtag/')
.replaceAll('yt.www.watch.player.seekTo', 'changeDuration')
}
</script>

<style scoped src="./WatchVideoDescription.css" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<FtCard
class="relative watchVideoRecommendations"
>
<div class="VideoRecommendationsTopBar">
<h3>
{{ $t("Up Next") }}
</h3>
<FtToggleSwitch
v-if="showAutoplay"
class="autoPlayToggle"
:label="$t('Video.Autoplay')"
:compact="true"
:default-value="playNextVideo"
@change="updatePlayNextVideo"
/>
</div>
<FtListVideoLazy
v-for="(video, index) in data"
:key="index"
:data="video"
appearance="recommendation"
force-list-type="list"
:use-channels-hidden-preference="true"
/>
</FtCard>
</template>

<script setup>
import { computed } from 'vue'
import FtCard from '../ft-card/ft-card.vue'
import FtListVideoLazy from '../ft-list-video-lazy/ft-list-video-lazy.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
import store from '../../store/index'
defineProps({
data: {
type: Array,
required: true
},
showAutoplay: {
type: Boolean,
default: false
}
})
const playNextVideo = computed(() => {
return store.getters.getPlayNextVideo
})
/**
* @param {boolean} value
*/
function updatePlayNextVideo(value) {
store.dispatch('updatePlayNextVideo', value)
}
</script>

<style scoped src="./WatchVideoRecommendations.css" />
Loading

0 comments on commit 122db20

Please sign in to comment.