-
Notifications
You must be signed in to change notification settings - Fork 898
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate WatchVideoRecommendations, WatchVideoDescription and WatchVid…
…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
Showing
13 changed files
with
342 additions
and
343 deletions.
There are no files selected for viewing
File renamed without changes.
191 changes: 191 additions & 0 deletions
191
src/renderer/components/WatchVideoChapters/WatchVideoChapters.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" /> |
File renamed without changes.
87 changes: 87 additions & 0 deletions
87
src/renderer/components/WatchVideoDescription/WatchVideoDescription.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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('&', '&') | ||
.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" /> |
File renamed without changes.
61 changes: 61 additions & 0 deletions
61
src/renderer/components/WatchVideoRecommendations/WatchVideoRecommendations.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" /> |
Oops, something went wrong.