mirror of
https://github.com/FreeTubeApp/FreeTube.git
synced 2025-12-05 01:10:31 +00:00
Compare commits
24 Commits
44850093cb
...
b91a15277f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b91a15277f | ||
|
|
35d9663d95 | ||
|
|
ec724382de | ||
|
|
e9c48fbb58 | ||
|
|
aa745d5ead | ||
|
|
ee2ff267d1 | ||
|
|
24593d425e | ||
|
|
92f79616d1 | ||
|
|
c092104596 | ||
|
|
ac0a8b0134 | ||
|
|
f1d4991ca6 | ||
|
|
c14e332051 | ||
|
|
e7280db2b9 | ||
|
|
bf340071ad | ||
|
|
ea71f45be5 | ||
|
|
549e3f918a | ||
|
|
3b379dbcb4 | ||
|
|
8695da836b | ||
|
|
a711a9730b | ||
|
|
cb14c8c4ce | ||
|
|
8be984c2fc | ||
|
|
fadaa2457b | ||
|
|
fde2ade504 | ||
|
|
5e243c68ec |
@@ -119,6 +119,7 @@ export default [
|
||||
}],
|
||||
|
||||
'vue/no-unused-emit-declarations': 'error',
|
||||
'vue/prefer-use-template-ref': 'error',
|
||||
|
||||
'jsdoc/check-alignment': 'error',
|
||||
'jsdoc/check-property-names': 'error',
|
||||
|
||||
12
package.json
12
package.json
@@ -65,7 +65,7 @@
|
||||
"electron-context-menu": "^4.1.1",
|
||||
"marked": "^17.0.0",
|
||||
"process": "^0.11.10",
|
||||
"shaka-player": "^4.16.8",
|
||||
"shaka-player": "^4.16.9",
|
||||
"swiper": "^12.0.3",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.1.12",
|
||||
@@ -84,10 +84,10 @@
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
"electron": "^38.4.0",
|
||||
"electron-builder": "^26.1.0",
|
||||
"electron": "^39.2.1",
|
||||
"electron-builder": "^26.2.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-jsdoc": "^61.1.12",
|
||||
"eslint-plugin-jsdoc": "^61.2.1",
|
||||
"eslint-plugin-jsonc": "^2.21.0",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
@@ -97,13 +97,13 @@
|
||||
"html-webpack-plugin": "^5.6.4",
|
||||
"js-yaml": "^4.1.1",
|
||||
"json-minimizer-webpack-plugin": "^5.0.1",
|
||||
"lefthook": "^2.0.3",
|
||||
"lefthook": "^2.0.4",
|
||||
"mini-css-extract-plugin": "^2.9.4",
|
||||
"neostandard": "^0.12.2",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"sass": "^1.93.3",
|
||||
"sass": "^1.94.0",
|
||||
"sass-loader": "^16.0.6",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-sass-guidelines": "^12.1.0",
|
||||
|
||||
@@ -1121,8 +1121,7 @@ function runApp() {
|
||||
newWindow.webContents.ipc.on(IpcChannels.SEARCH_INPUT_HANDLING_READY, searchInputReadyHandler)
|
||||
}
|
||||
|
||||
// Show when loaded
|
||||
newWindow.once('ready-to-show', () => {
|
||||
const showWindow = () => {
|
||||
if (newWindow.isVisible()) {
|
||||
// only open the dev tools if they aren't already open
|
||||
if (process.env.NODE_ENV === 'development' && !newWindow.webContents.isDevToolsOpened()) {
|
||||
@@ -1141,7 +1140,18 @@ function runApp() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
newWindow.webContents.openDevTools({ activate: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The `ready-to-show` event doesn't always fire on wayland.
|
||||
// Use the `did-finish-load` event on the web contents instead as that is similar enough
|
||||
// https://github.com/electron/electron/issues/48859
|
||||
|
||||
if (process.platform === 'linux' && app.commandLine.getSwitchValue('ozone-platform') === 'wayland') {
|
||||
newWindow.webContents.once('did-finish-load', showWindow)
|
||||
} else {
|
||||
// Show when loaded
|
||||
newWindow.once('ready-to-show', showWindow)
|
||||
}
|
||||
|
||||
newWindow.once('close', async () => {
|
||||
if (BrowserWindow.getAllWindows().length !== 1) {
|
||||
@@ -2391,7 +2401,7 @@ function runApp() {
|
||||
},
|
||||
type: 'normal'
|
||||
},
|
||||
!hideTrendingVideos && {
|
||||
(!hideTrendingVideos && (backendFallback || backendPreference === 'local')) && {
|
||||
label: 'Trending',
|
||||
click: (_menuItem, browserWindow, _event) => {
|
||||
navigateTo('/trending', browserWindow)
|
||||
|
||||
@@ -628,7 +628,6 @@ export default defineComponent({
|
||||
transformed = true
|
||||
break
|
||||
case 'subscriptions':
|
||||
case 'trending':
|
||||
case 'history':
|
||||
transformedURL.pathname = `/feed/${pathParts[1]}`
|
||||
transformed = true
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, useTemplateRef } from 'vue'
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
@@ -381,7 +381,7 @@ function search(query) {
|
||||
emit('search', query)
|
||||
}
|
||||
|
||||
const searchBar = ref(null)
|
||||
const searchBar = useTemplateRef('searchBar')
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
|
||||
3
src/renderer/components/DataSettings/DataSettings.css
Normal file
3
src/renderer/components/DataSettings/DataSettings.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.box {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<h4 class="groupTitle">
|
||||
{{ $t('Subscriptions.Subscriptions') }}
|
||||
</h4>
|
||||
<FtFlexBox class="dataSettingsBox">
|
||||
<FtFlexBox class="box">
|
||||
<FtButton
|
||||
:label="$t('Settings.Data Settings.Import Subscriptions')"
|
||||
@click="importSubscriptions"
|
||||
@@ -29,7 +29,7 @@
|
||||
<h4 class="groupTitle">
|
||||
{{ $t('History.History') }}
|
||||
</h4>
|
||||
<FtFlexBox class="dataSettingsBox">
|
||||
<FtFlexBox class="box">
|
||||
<FtButton
|
||||
:label="$t('Settings.Data Settings.Import History')"
|
||||
@click="importWatchHistory"
|
||||
@@ -42,7 +42,7 @@
|
||||
<h4 class="groupTitle">
|
||||
{{ $t('Playlists') }}
|
||||
</h4>
|
||||
<FtFlexBox class="dataSettingsBox">
|
||||
<FtFlexBox class="box">
|
||||
<FtButton
|
||||
:label="$t('Settings.Data Settings.Import Playlists')"
|
||||
@click="importPlaylists"
|
||||
@@ -55,7 +55,7 @@
|
||||
<h4 class="groupTitle">
|
||||
{{ t('Settings.Data Settings.Search history') }}
|
||||
</h4>
|
||||
<FtFlexBox class="dataSettingsBox">
|
||||
<FtFlexBox class="box">
|
||||
<FtButton
|
||||
:label="t('Settings.Data Settings.Import search history')"
|
||||
@click="importSearchHistory"
|
||||
@@ -84,18 +84,18 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from '../composables/use-i18n-polyfill'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import FtButton from './FtButton/FtButton.vue'
|
||||
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'
|
||||
import FtPrompt from './FtPrompt/FtPrompt.vue'
|
||||
import FtSettingsSection from './FtSettingsSection/FtSettingsSection.vue'
|
||||
import FtButton from '../FtButton/FtButton.vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
import FtPrompt from '../FtPrompt/FtPrompt.vue'
|
||||
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
|
||||
|
||||
import store from '../store/index'
|
||||
import store from '../../store/index'
|
||||
|
||||
import { MAIN_PROFILE_ID } from '../../constants'
|
||||
import { calculateColorLuminance, getRandomColor } from '../helpers/colors'
|
||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||
import { calculateColorLuminance, getRandomColor } from '../../helpers/colors'
|
||||
import {
|
||||
deepCopy,
|
||||
escapeHTML,
|
||||
@@ -103,8 +103,8 @@ import {
|
||||
readFileWithPicker,
|
||||
showToast,
|
||||
writeFileWithPicker,
|
||||
} from '../helpers/utils'
|
||||
import { processToBeAddedPlaylistVideo } from '../helpers/playlists'
|
||||
} from '../../helpers/utils'
|
||||
import { processToBeAddedPlaylistVideo } from '../../helpers/playlists'
|
||||
|
||||
const IMPORT_DIRECTORY_ID = 'data-settings-import'
|
||||
const START_IN_DIRECTORY = 'downloads'
|
||||
@@ -1403,3 +1403,5 @@ async function exportYouTubeSearchHistory() {
|
||||
|
||||
// #endregion search history
|
||||
</script>
|
||||
|
||||
<style scoped src="./DataSettings.css" />
|
||||
@@ -96,8 +96,10 @@
|
||||
<div class="switchColumnGrid">
|
||||
<div class="switchColumn">
|
||||
<FtToggleSwitch
|
||||
v-if="SUPPORTS_LOCAL_API"
|
||||
:label="t('Settings.Distraction Free Settings.Hide Trending Videos')"
|
||||
:compact="true"
|
||||
:disabled="disableHideTrendingVideos"
|
||||
:default-value="hideTrendingVideos"
|
||||
@change="updateHideTrendingVideos"
|
||||
/>
|
||||
@@ -298,6 +300,8 @@ import { checkYoutubeChannelId, findChannelTagInfo } from '../../helpers/channel
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const SUPPORTS_LOCAL_API = process.env.SUPPORTS_LOCAL_API
|
||||
|
||||
const channelHiderDisabled = ref(false)
|
||||
|
||||
/** @type {import('vue').ComputedRef<'local' | 'invidious'>} */
|
||||
@@ -368,6 +372,7 @@ function handleHideRecommendedVideos(value) {
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideTrendingVideos = computed(() => store.getters.getHideTrendingVideos)
|
||||
|
||||
const disableHideTrendingVideos = computed(() => backendPreference.value !== 'local' && !backendFallback.value)
|
||||
/**
|
||||
* @param {boolean} value
|
||||
*/
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import autolinker from 'autolinker'
|
||||
import { A11y, Navigation, Pagination } from 'swiper/modules'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, useTemplateRef } from 'vue'
|
||||
|
||||
import FtListVideo from '../ft-list-video/ft-list-video.vue'
|
||||
import FtListPlaylist from '../FtListPlaylist/FtListPlaylist.vue'
|
||||
@@ -305,7 +305,7 @@ function getBestQualityImage(imageArray) {
|
||||
return imageArrayCopy[0]?.url?.replace(/-c-fcrop64=[^-]+/i, '') ?? ''
|
||||
}
|
||||
|
||||
const swiperContainerRef = ref(null)
|
||||
const swiperContainerRef = useTemplateRef('swiperContainerRef')
|
||||
|
||||
if (postType === 'multiImage' && postContent.content.length > 0) {
|
||||
onMounted(() => {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtButton from '../FtButton/FtButton.vue'
|
||||
@@ -83,7 +83,7 @@ const playlistPersistenceDisabled = computed(() => {
|
||||
return playlistName.value === '' || playlistNameBlank.value || playlistWithNameExists.value
|
||||
})
|
||||
|
||||
const playlistNameInput = ref(null)
|
||||
const playlistNameInput = useTemplateRef('playlistNameInput')
|
||||
|
||||
onMounted(() => {
|
||||
// Faster to input required playlist name
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, reactive, ref, shallowRef, useId, watch } from 'vue'
|
||||
import { computed, reactive, ref, shallowRef, useId, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
||||
@@ -195,7 +195,7 @@ const emit = defineEmits(['clear', 'click', 'input', 'remove'])
|
||||
|
||||
const id = useId()
|
||||
|
||||
const inputRef = ref(null)
|
||||
const inputRef = useTemplateRef('inputRef')
|
||||
|
||||
const inputData = ref(props.value)
|
||||
const searchState = reactive({
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { ref, useId } from 'vue'
|
||||
import { useId, useTemplateRef } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtInput from '../FtInput/FtInput.vue'
|
||||
@@ -139,7 +139,7 @@ const { t } = useI18n()
|
||||
|
||||
const id = useId()
|
||||
|
||||
const tagNameInput = ref(null)
|
||||
const tagNameInput = useTemplateRef('tagNameInput')
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
@@ -282,7 +282,7 @@ const anyPlaylistContainsVideosToBeAdded = computed(() => {
|
||||
return playlistIdsContainingVideosToBeAdded.value.size > 0
|
||||
})
|
||||
|
||||
const searchBar = ref(null)
|
||||
const searchBar = useTemplateRef('searchBar')
|
||||
|
||||
watch(allPlaylistsLength, (val, oldVal) => {
|
||||
const allPlaylistIds = new Set()
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, useId } from 'vue'
|
||||
import { computed, nextTick, ref, useId, useTemplateRef } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -143,7 +143,7 @@ function isActiveProfile(profile) {
|
||||
return profile._id === activeProfile.value._id
|
||||
}
|
||||
|
||||
const profileListRef = ref(null)
|
||||
const profileListRef = useTemplateRef('profileListRef')
|
||||
|
||||
function toggleProfileList() {
|
||||
profileListShown.value = !profileListShown.value
|
||||
@@ -178,8 +178,7 @@ function handleProfileListFocusOut() {
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const iconButton = ref(null)
|
||||
const iconButton = useTemplateRef('iconButton')
|
||||
|
||||
function handleProfileListEscape() {
|
||||
iconButton.value?.focus()
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, useId } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, useId, useTemplateRef } from 'vue'
|
||||
|
||||
import store from '../../store/index'
|
||||
|
||||
@@ -103,7 +103,7 @@ const emit = defineEmits(['click'])
|
||||
|
||||
const id = useId()
|
||||
|
||||
const promptCard = ref(null)
|
||||
const promptCard = useTemplateRef('promptCard')
|
||||
|
||||
let promptButtons = []
|
||||
let lastActiveElement = null
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
background-color: var(--card-bg-color);
|
||||
padding-block: 10px;
|
||||
|
||||
> div {
|
||||
> :deep(div) {
|
||||
box-sizing: border-box;
|
||||
padding-block: 0;
|
||||
padding-inline: 20px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
> div:not(:last-child, .ft-flex-box) {
|
||||
> :deep(div:not(:last-child, .ft-flex-box)) {
|
||||
@media only screen and (width <= 800px) {
|
||||
margin-block-end: 20px;
|
||||
}
|
||||
@@ -36,10 +36,6 @@
|
||||
margin-block: 0.5em;
|
||||
}
|
||||
|
||||
.dataSettingsBox {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.groupTitle) {
|
||||
text-align: center;
|
||||
margin-block: 0.5em;
|
||||
@@ -90,10 +86,8 @@
|
||||
|
||||
@media only screen and (width <= 680px) {
|
||||
.settingsSection {
|
||||
> div {
|
||||
:deep(.text.bottom) {
|
||||
inset-inline-start: -85px;
|
||||
}
|
||||
> :deep(div .text.bottom) {
|
||||
inset-inline-start: -85px;
|
||||
}
|
||||
|
||||
:deep(.switch-ctn.containsTooltip) {
|
||||
@@ -103,16 +97,14 @@
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
:not(.select, .selectLabel) {
|
||||
> :deep(.tooltip) {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
inset-inline-end: -25px;
|
||||
inset-block-start: 12px;
|
||||
}
|
||||
:deep(:not(.select, .selectLabel) > .tooltip) {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
inset-inline-end: -25px;
|
||||
inset-block-start: 12px;
|
||||
}
|
||||
|
||||
.settingsFlexStart460px :deep(.tooltip) {
|
||||
:deep(.settingsFlexStart460px .tooltip) {
|
||||
inset-inline-end: 0;
|
||||
inset-block-start: -2px;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { copyToClipboard, openExternalLink } from '../../helpers/utils'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
@@ -165,7 +165,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const includeTimestamp = ref(false)
|
||||
const iconButton = ref(null)
|
||||
const iconButton = useTemplateRef('iconButton')
|
||||
|
||||
const isChannel = computed(() => {
|
||||
return props.shareTargetType === 'Channel'
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, ref, shallowRef, useId } from 'vue'
|
||||
import { computed, ref, shallowRef, useId, useTemplateRef } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtButton from '../FtButton/FtButton.vue'
|
||||
@@ -242,7 +242,7 @@ function handleSubscription(profile) {
|
||||
}
|
||||
}
|
||||
|
||||
const subscribeButton = ref(null)
|
||||
const subscribeButton = useTemplateRef('subscribeButton')
|
||||
|
||||
function handleProfileDropdownFocusOut() {
|
||||
if (subscribeButton.value && !subscribeButton.value.matches(':focus-within')) {
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.containsTooltip .switch-label-text {
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
.switch-label {
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, useTemplateRef } from 'vue'
|
||||
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtInput from '../FtInput/FtInput.vue'
|
||||
@@ -25,7 +25,7 @@ import store from '../../store/index'
|
||||
|
||||
const emit = defineEmits(['unlocked'])
|
||||
|
||||
const password = ref(null)
|
||||
const password = useTemplateRef('password')
|
||||
|
||||
onMounted(() => {
|
||||
password.value.focus()
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -609,7 +609,7 @@ async function savePlaylistInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
const playlistTitleInput = ref(null)
|
||||
const playlistTitleInput = useTemplateRef('playlistTitleInput')
|
||||
|
||||
function enterEditMode() {
|
||||
newTitle.value = props.title
|
||||
@@ -893,7 +893,7 @@ const updateQueryDebounced = debounce((newQuery) => {
|
||||
emit('search-video-query-change', newQuery)
|
||||
}, 500)
|
||||
|
||||
const searchInput = ref(null)
|
||||
const searchInput = useTemplateRef('searchInput')
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</p>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="!hideTrendingVideos"
|
||||
v-if="SUPPORTS_LOCAL_API && !hideTrendingVideos && (backendFallback || backendPreference === 'local')"
|
||||
class="navOption mobileHidden"
|
||||
role="button"
|
||||
to="/trending"
|
||||
@@ -256,6 +256,8 @@ import { KeyboardShortcuts } from '../../../constants'
|
||||
|
||||
const { locale, t } = useI18n()
|
||||
|
||||
const SUPPORTS_LOCAL_API = process.env.SUPPORTS_LOCAL_API
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const isOpen = computed(() => {
|
||||
return store.getters.getIsSideNavOpen
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</p>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="!hideTrendingVideos"
|
||||
v-if=" SUPPORTS_LOCAL_API && trendingVisible"
|
||||
class="navOption"
|
||||
:title="$t('Trending.Trending')"
|
||||
:aria-label="hideLabelsSideBar ? $t('Trending.Trending') : null"
|
||||
@@ -197,18 +197,21 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, ref, onMounted, onBeforeUnmount, useTemplateRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import store from '../../store/index'
|
||||
|
||||
const SUPPORTS_LOCAL_API = process.env.SUPPORTS_LOCAL_API
|
||||
|
||||
const openMoreOptions = ref(false)
|
||||
|
||||
const menuRef = ref(null)
|
||||
const menuRef = useTemplateRef('menuRef')
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideTrendingVideos = computed(() => {
|
||||
return store.getters.getHideTrendingVideos
|
||||
const trendingVisible = computed(() => {
|
||||
return !store.getters.getHideTrendingVideos &&
|
||||
(store.getters.getBackendFallback || store.getters.getBackendPreference === 'local')
|
||||
})
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -369,10 +369,8 @@ function showSearchFilters() {
|
||||
store.dispatch('showSearchFilters')
|
||||
}
|
||||
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const searchContainer = ref(null)
|
||||
/** @type {import('vue').Ref<InstanceType<typeof FtInput> | null>} */
|
||||
const searchInput = ref(null)
|
||||
const searchContainer = useTemplateRef('searchContainer')
|
||||
const searchInput = useTemplateRef('searchInput')
|
||||
|
||||
/** @type {import('vue').ComputedRef<any>} */
|
||||
const searchSettings = computed(() => store.getters.getSearchSettings)
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
@@ -86,8 +86,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['timestamp-event'])
|
||||
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const chaptersWrapper = ref(null)
|
||||
const chaptersWrapper = useTemplateRef('chaptersWrapper')
|
||||
|
||||
let chaptersVisible = false
|
||||
const currentIndex = ref(props.currentChapterIndex)
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<script setup>
|
||||
import autolinker from 'autolinker'
|
||||
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { onMounted, ref, computed, useTemplateRef } from 'vue'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtTimestampCatcher from '../FtTimestampCatcher.vue'
|
||||
|
||||
@@ -67,7 +67,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['timestamp-event'])
|
||||
|
||||
let shownDescription = ''
|
||||
const descriptionContainer = ref()
|
||||
const descriptionContainer = useTemplateRef('descriptionContainer')
|
||||
const showFullDescription = ref(false)
|
||||
const showControls = ref(false)
|
||||
|
||||
|
||||
567
src/renderer/components/WatchVideoInfo/WatchVideoInfo.vue
Normal file
567
src/renderer/components/WatchVideoInfo/WatchVideoInfo.vue
Normal file
@@ -0,0 +1,567 @@
|
||||
<template>
|
||||
<FtCard class="watchVideoInfo">
|
||||
<div>
|
||||
<h1
|
||||
class="videoTitle"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="isUnlisted"
|
||||
class="unlistedBadge"
|
||||
>
|
||||
{{ t('Video.Unlisted') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="videoMetrics">
|
||||
<div class="datePublishedAndViewCount">
|
||||
{{ publishedString }} {{ dateString }}
|
||||
<template
|
||||
v-if="!hideVideoViews"
|
||||
>
|
||||
<span class="seperator">• </span><span class="videoViews">{{ parsedViewCount }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hideVideoLikesAndDislikes"
|
||||
class="likeBarContainer"
|
||||
>
|
||||
<div
|
||||
class="likeSection"
|
||||
>
|
||||
<span class="likeCount"><FontAwesomeIcon :icon="['fas', 'thumbs-up']" /> {{ parsedLikeCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="videoButtons">
|
||||
<div
|
||||
class="profileRow"
|
||||
>
|
||||
<div
|
||||
v-if="!hideUploader"
|
||||
>
|
||||
<RouterLink
|
||||
:to="`/channel/${channelId}`"
|
||||
>
|
||||
<img
|
||||
:src="channelThumbnail"
|
||||
class="channelThumbnail"
|
||||
alt=""
|
||||
>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="!hideUploader"
|
||||
>
|
||||
<RouterLink
|
||||
:to="`/channel/${channelId}`"
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<FtSubscribeButton
|
||||
v-if="!hideUnsubscribeButton"
|
||||
:channel-id="channelId"
|
||||
:channel-name="channelName"
|
||||
:channel-thumbnail="channelThumbnail"
|
||||
:subscription-count-text="subscriptionCountText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="videoOptions">
|
||||
<span class="videoOptionsMobileRow">
|
||||
<FtIconButton
|
||||
v-if="showPlaylists && !isUpcoming"
|
||||
:title="t('User Playlists.Add to Playlist')"
|
||||
:icon="['fas', 'plus']"
|
||||
theme="base"
|
||||
@click="togglePlaylistPrompt"
|
||||
/>
|
||||
<FtIconButton
|
||||
v-if="isQuickBookmarkEnabled"
|
||||
:title="quickBookmarkIconText"
|
||||
:icon="isInQuickBookmarkPlaylist ? ['fas', 'check'] : ['fas', 'bookmark']"
|
||||
class="quickBookmarkVideoIcon"
|
||||
:class="{
|
||||
bookmarked: isInQuickBookmarkPlaylist,
|
||||
}"
|
||||
:theme="quickBookmarkIconTheme"
|
||||
@click="toggleQuickBookmarked"
|
||||
/>
|
||||
<FtIconButton
|
||||
v-if="canSaveWatchedProgress && watchedProgressSavingInSemiAutoMode"
|
||||
:title="t('Video.Save Watched Progress')"
|
||||
:icon="['fas', 'bars-progress']"
|
||||
@click="saveWatchedProgressManually"
|
||||
/>
|
||||
</span>
|
||||
<span class="videoOptionsMobileRow">
|
||||
<FtIconButton
|
||||
v-if="USING_ELECTRON && externalPlayer !== ''"
|
||||
:title="t('Video.External Player.OpenInTemplate', { externalPlayer })"
|
||||
:icon="['fas', 'external-link-alt']"
|
||||
theme="secondary"
|
||||
@click="handleExternalPlayer"
|
||||
/>
|
||||
<FtIconButton
|
||||
v-if="!isUpcoming && downloadLinks.length > 0"
|
||||
ref="downloadButton"
|
||||
:title="t('Video.Download Video')"
|
||||
theme="secondary"
|
||||
:icon="['fas', 'download']"
|
||||
:return-index="true"
|
||||
:dropdown-options="downloadLinks"
|
||||
@click="handleDownload"
|
||||
/>
|
||||
<FtIconButton
|
||||
v-if="!isUpcoming"
|
||||
:title="t('Change Format.Change Media Formats')"
|
||||
theme="secondary"
|
||||
:icon="['fas', 'file-video']"
|
||||
:dropdown-options="formatTypeOptions"
|
||||
@click="changeFormat"
|
||||
/>
|
||||
<FtShareButton
|
||||
v-if="!hideSharingActions"
|
||||
:id="id"
|
||||
:get-timestamp="getTimestamp"
|
||||
:playlist-id="playlistId"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</FtCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||
import FtShareButton from '../FtShareButton/FtShareButton.vue'
|
||||
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
|
||||
|
||||
import store from '../../store'
|
||||
|
||||
import { formatNumber, openExternalLink, showToast } from '../../helpers/utils'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
channelId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
channelName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
channelThumbnail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
published: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
premiereDate: {
|
||||
type: Date,
|
||||
default: undefined
|
||||
},
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
subscriptionCountText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
likeCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
dislikeCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
getTimestamp: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
isLiveContent: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
isUpcoming: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
downloadLinks: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
playlistId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
getPlaylistIndex: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistReverse: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistShuffle: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistLoop: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
lengthSeconds: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
videoThumbnail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
inUserPlaylist: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
isUnlisted: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
canSaveWatchedProgress: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'change-format',
|
||||
'pause-player',
|
||||
'set-info-area-sticky',
|
||||
'scroll-to-info-area',
|
||||
'save-watched-progress',
|
||||
])
|
||||
|
||||
const USING_ELECTRON = process.env.IS_ELECTRON
|
||||
|
||||
const { locale, t } = useI18n()
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideSharingActions = computed(() => store.getters.getHideSharingActions)
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideUnsubscribeButton = computed(() => store.getters.getHideUnsubscribeButton)
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideUploader = computed(() => store.getters.getHideUploader)
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideVideoLikesAndDislikes = computed(() => store.getters.getHideVideoLikesAndDislikes)
|
||||
|
||||
const parsedLikeCount = computed(() => {
|
||||
if (hideVideoLikesAndDislikes.value || props.likeCount === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatNumber(props.likeCount)
|
||||
})
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const hideVideoViews = computed(() => store.getters.getHideVideoViews)
|
||||
|
||||
const parsedViewCount = computed(() => {
|
||||
if (hideVideoViews.value || props.viewCount == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return t('Global.Counts.View Count', { count: formatNumber(props.viewCount) }, props.viewCount)
|
||||
})
|
||||
|
||||
const dateString = computed(() => {
|
||||
const formatter = new Intl.DateTimeFormat([locale.value, 'en'], { dateStyle: 'medium' })
|
||||
const localeDateString = formatter.format(props.published)
|
||||
// replace spaces with no break spaces to make the date act as a single entity while wrapping
|
||||
return localeDateString.replaceAll(' ', '\u00A0')
|
||||
})
|
||||
|
||||
const publishedString = computed(() => {
|
||||
if (props.isLive) {
|
||||
return t('Video.Started streaming on')
|
||||
} else if (props.isLiveContent && !props.isLive) {
|
||||
return t('Video.Streamed on')
|
||||
} else {
|
||||
return t('Video.Published on')
|
||||
}
|
||||
})
|
||||
|
||||
const formatTypeOptions = computed(() => [
|
||||
{
|
||||
label: t('Change Format.Use Dash Formats'),
|
||||
value: 'dash'
|
||||
},
|
||||
{
|
||||
label: t('Change Format.Use Legacy Formats'),
|
||||
value: 'legacy'
|
||||
},
|
||||
{
|
||||
label: t('Change Format.Use Audio Formats'),
|
||||
value: 'audio'
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {'dash' | 'legacy' | 'audio'} value
|
||||
*/
|
||||
function changeFormat(value) {
|
||||
emit('change-format', value)
|
||||
}
|
||||
|
||||
const watchedProgressSavingInSemiAutoMode = computed(() => {
|
||||
return store.getters.getWatchedProgressSavingMode === 'semi-auto'
|
||||
})
|
||||
|
||||
function saveWatchedProgressManually() {
|
||||
emit('save-watched-progress')
|
||||
}
|
||||
|
||||
/** @type {import('vue').ComputedRef<boolean>} */
|
||||
const rememberHistory = computed(() => store.getters.getRememberHistory)
|
||||
|
||||
const historyEntryExists = computed(() => store.getters.getHistoryCacheById[props.id] !== undefined)
|
||||
|
||||
/** @type {import('vue').ComputedRef<string>} */
|
||||
const externalPlayer = computed(() => store.getters.getExternalPlayer)
|
||||
|
||||
/** @type {import('vue').ComputedRef<number>} */
|
||||
const defaultPlayback = computed(() => store.getters.getDefaultPlayback)
|
||||
|
||||
function handleExternalPlayer() {
|
||||
emit('pause-player')
|
||||
|
||||
let payload
|
||||
|
||||
// Only play video in non playlist mode when user playlist detected
|
||||
if (props.inUserPlaylist) {
|
||||
payload = {
|
||||
watchProgress: props.getTimestamp(),
|
||||
playbackRate: defaultPlayback.value,
|
||||
videoId: props.id,
|
||||
videoLength: props.lengthSeconds
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
watchProgress: props.getTimestamp(),
|
||||
playbackRate: defaultPlayback.value,
|
||||
videoId: props.id,
|
||||
videoLength: props.lengthSeconds,
|
||||
playlistId: props.playlistId,
|
||||
playlistIndex: props.getPlaylistIndex(),
|
||||
playlistReverse: props.getPlaylistReverse(),
|
||||
playlistShuffle: props.getPlaylistShuffle(),
|
||||
playlistLoop: props.getPlaylistLoop()
|
||||
}
|
||||
}
|
||||
|
||||
store.dispatch('openInExternalPlayer', payload)
|
||||
|
||||
if (rememberHistory.value) {
|
||||
// Marking as watched
|
||||
const videoData = {
|
||||
videoId: props.id,
|
||||
title: props.title,
|
||||
author: props.channelName,
|
||||
authorId: props.channelId,
|
||||
published: props.published,
|
||||
description: props.description,
|
||||
viewCount: props.viewCount,
|
||||
lengthSeconds: props.lengthSeconds,
|
||||
watchProgress: 0,
|
||||
timeWatched: Date.now(),
|
||||
isLive: false,
|
||||
type: 'video'
|
||||
}
|
||||
|
||||
store.dispatch('updateHistory', videoData)
|
||||
|
||||
if (!historyEntryExists.value) {
|
||||
showToast(t('Video.Video has been marked as watched'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const downloadButton = useTemplateRef('downloadButton')
|
||||
|
||||
/** @type {import('vue').WatchHandle | undefined} */
|
||||
let downloadDropdownWatcher
|
||||
|
||||
onMounted(() => {
|
||||
if (process.env.IS_ELECTRON || 'mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: props.title,
|
||||
artist: props.channelName,
|
||||
artwork: [{
|
||||
src: props.videoThumbnail,
|
||||
sizes: '128x128',
|
||||
type: 'img/png'
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
downloadDropdownWatcher = watch(() => downloadButton.value.dropdownShown, (dropdownShown) => {
|
||||
emit('set-info-area-sticky', !dropdownShown)
|
||||
|
||||
if (dropdownShown && window.innerWidth >= 901) {
|
||||
// adds a slight delay so we know that the dropdown has shown up
|
||||
// and won't mess up our scrolling
|
||||
nextTick(() => {
|
||||
emit('scroll-to-info-area')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (downloadDropdownWatcher) {
|
||||
downloadDropdownWatcher.stop()
|
||||
downloadDropdownWatcher = undefined
|
||||
}
|
||||
})
|
||||
|
||||
/** @type {import('vue').ComputedRef<'download' | 'open'>} */
|
||||
const downloadBehavior = computed(() => store.getters.getDownloadBehavior)
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
function handleDownload(index) {
|
||||
const selectedDownloadLinkOption = props.downloadLinks[index]
|
||||
const mimeTypeUrl = selectedDownloadLinkOption.value.split('||')
|
||||
|
||||
if (!process.env.IS_ELECTRON || downloadBehavior.value === 'open') {
|
||||
openExternalLink(mimeTypeUrl[1])
|
||||
} else {
|
||||
store.dispatch('downloadMedia', {
|
||||
url: mimeTypeUrl[1],
|
||||
title: props.title,
|
||||
mimeType: mimeTypeUrl[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const showPlaylists = computed(() => !store.getters.getHidePlaylists)
|
||||
|
||||
function togglePlaylistPrompt() {
|
||||
const videoData = {
|
||||
videoId: props.id,
|
||||
title: props.title,
|
||||
author: props.channelName,
|
||||
authorId: props.channelId,
|
||||
description: props.description,
|
||||
viewCount: props.viewCount,
|
||||
lengthSeconds: props.lengthSeconds,
|
||||
published: props.published,
|
||||
premiereDate: props.premiereDate
|
||||
}
|
||||
|
||||
store.dispatch('showAddToPlaylistPromptForManyVideos', { videos: [videoData] })
|
||||
}
|
||||
|
||||
const quickBookmarkPlaylist = computed(() => store.getters.getQuickBookmarkPlaylist)
|
||||
|
||||
const isQuickBookmarkEnabled = computed(() => quickBookmarkPlaylist.value != null)
|
||||
|
||||
const isInQuickBookmarkPlaylist = computed(() => {
|
||||
if (!isQuickBookmarkEnabled.value) { return false }
|
||||
|
||||
// Accessing a reactive property has a negligible amount of overhead,
|
||||
// however as we know that some users have playlists that have more than 10k items in them
|
||||
// it adds up quickly. So create a temporary variable outside of the array, so we only have to do it once.
|
||||
// Also the search is retriggered every time any playlist is modified.
|
||||
const id = props.id
|
||||
|
||||
return quickBookmarkPlaylist.value.videos.some((video) => {
|
||||
return video.videoId === id
|
||||
})
|
||||
})
|
||||
|
||||
const quickBookmarkIconText = computed(() => {
|
||||
if (!isQuickBookmarkEnabled.value) { return '' }
|
||||
|
||||
const translationProperties = {
|
||||
playlistName: quickBookmarkPlaylist.value.playlistName,
|
||||
}
|
||||
return isInQuickBookmarkPlaylist.value
|
||||
? t('User Playlists.Remove from Favorites', translationProperties)
|
||||
: t('User Playlists.Add to Favorites', translationProperties)
|
||||
})
|
||||
|
||||
const quickBookmarkIconTheme = computed(() => isInQuickBookmarkPlaylist.value ? 'base favorite' : 'base')
|
||||
|
||||
function toggleQuickBookmarked() {
|
||||
if (!isQuickBookmarkEnabled.value) {
|
||||
// This should be prevented by UI
|
||||
return
|
||||
}
|
||||
|
||||
if (isInQuickBookmarkPlaylist.value) {
|
||||
removeFromQuickBookmarkPlaylist()
|
||||
} else {
|
||||
addToQuickBookmarkPlaylist()
|
||||
}
|
||||
}
|
||||
|
||||
function addToQuickBookmarkPlaylist() {
|
||||
const videoData = {
|
||||
videoId: props.id,
|
||||
title: props.title,
|
||||
author: props.channelName,
|
||||
authorId: props.channelId,
|
||||
lengthSeconds: props.lengthSeconds,
|
||||
published: props.published,
|
||||
premiereDate: props.premiereDate
|
||||
}
|
||||
|
||||
store.dispatch('addVideo', {
|
||||
_id: quickBookmarkPlaylist.value._id,
|
||||
videoData,
|
||||
})
|
||||
|
||||
// TODO: Maybe show playlist name
|
||||
showToast(t('Video.Video has been saved'))
|
||||
}
|
||||
|
||||
function removeFromQuickBookmarkPlaylist() {
|
||||
store.dispatch('removeVideo', {
|
||||
_id: quickBookmarkPlaylist.value._id,
|
||||
// Remove all playlist items with same videoId
|
||||
videoId: props.id,
|
||||
})
|
||||
|
||||
// TODO: Maybe show playlist name
|
||||
showToast(t('Video.Video has been removed from your saved list'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./WatchVideoInfo.css" />
|
||||
@@ -29,7 +29,6 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="comments.length === 0"
|
||||
ref="liveChatMessage"
|
||||
class="messageContainer liveChatMessage"
|
||||
>
|
||||
<p
|
||||
@@ -225,7 +224,7 @@
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import autolinker from 'autolinker'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, shallowReactive } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, shallowReactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { YTNodes } from 'youtubei.js'
|
||||
|
||||
@@ -370,8 +369,7 @@ function startLiveChatLocal() {
|
||||
liveChatInstance.start()
|
||||
}
|
||||
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const commentsRef = ref(null)
|
||||
const commentsRef = useTemplateRef('commentsRef')
|
||||
|
||||
/**
|
||||
* @param {import ('youtubei.js/dist/src/parser/continuations').LiveChatContinuation} initialData
|
||||
|
||||
@@ -92,7 +92,9 @@ export default defineComponent({
|
||||
},
|
||||
defaultPages: function () {
|
||||
let includedPageNames = this.includedDefaultPageNames
|
||||
if (this.hideTrendingVideos) includedPageNames = includedPageNames.filter((pageName) => pageName !== 'trending')
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.hideTrendingVideos || !this.backendFallback || this.backendPreference !== 'local') {
|
||||
includedPageNames = includedPageNames.filter((pageName) => pageName !== 'trending')
|
||||
}
|
||||
if (this.hidePlaylists) includedPageNames = includedPageNames.filter((pageName) => pageName !== 'userPlaylists')
|
||||
if (!(!this.hidePopularVideos && (this.backendFallback || this.backendPreference === 'invidious'))) includedPageNames = includedPageNames.filter((pageName) => pageName !== 'popular')
|
||||
return this.$router.getRoutes().filter((route) => includedPageNames.includes(route.name))
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||
import FtShareButton from '../FtShareButton/FtShareButton.vue'
|
||||
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
|
||||
import { formatNumber, openExternalLink, showToast } from '../../helpers/utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WatchVideoInfo',
|
||||
components: {
|
||||
'ft-card': FtCard,
|
||||
'ft-icon-button': FtIconButton,
|
||||
'ft-share-button': FtShareButton,
|
||||
'ft-subscribe-button': FtSubscribeButton
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
channelId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
channelName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
channelThumbnail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
published: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
premiereDate: {
|
||||
type: Date,
|
||||
default: undefined
|
||||
},
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
subscriptionCountText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
likeCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
dislikeCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
getTimestamp: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
isLiveContent: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
isUpcoming: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
downloadLinks: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
playlistId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
getPlaylistIndex: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistReverse: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistShuffle: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistLoop: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
lengthSeconds: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
videoThumbnail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
inUserPlaylist: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
isUnlisted: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
canSaveWatchedProgress: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'change-format',
|
||||
'pause-player',
|
||||
'set-info-area-sticky',
|
||||
'scroll-to-info-area',
|
||||
'save-watched-progress',
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
usingElectron: process.env.IS_ELECTRON
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hideSharingActions: function() {
|
||||
return this.$store.getters.getHideSharingActions
|
||||
},
|
||||
|
||||
hideUnsubscribeButton: function() {
|
||||
return this.$store.getters.getHideUnsubscribeButton
|
||||
},
|
||||
|
||||
currentLocale: function () {
|
||||
return this.$i18n.locale
|
||||
},
|
||||
|
||||
hideVideoLikesAndDislikes: function () {
|
||||
return this.$store.getters.getHideVideoLikesAndDislikes
|
||||
},
|
||||
|
||||
hideVideoViews: function () {
|
||||
return this.$store.getters.getHideVideoViews
|
||||
},
|
||||
|
||||
hideUploader: function () {
|
||||
return this.$store.getters.getHideUploader
|
||||
},
|
||||
|
||||
showPlaylists: function () {
|
||||
return !this.$store.getters.getHidePlaylists
|
||||
},
|
||||
|
||||
watchedProgressSavingInSemiAutoMode() {
|
||||
return this.$store.getters.getWatchedProgressSavingMode === 'semi-auto'
|
||||
},
|
||||
|
||||
rememberHistory() {
|
||||
return this.$store.getters.getRememberHistory
|
||||
},
|
||||
|
||||
downloadBehavior: function () {
|
||||
return this.$store.getters.getDownloadBehavior
|
||||
},
|
||||
|
||||
formatTypeOptions: function () {
|
||||
return [
|
||||
{
|
||||
label: this.$t('Change Format.Use Dash Formats'),
|
||||
value: 'dash'
|
||||
},
|
||||
{
|
||||
label: this.$t('Change Format.Use Legacy Formats'),
|
||||
value: 'legacy'
|
||||
},
|
||||
{
|
||||
label: this.$t('Change Format.Use Audio Formats'),
|
||||
value: 'audio'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
totalLikeCount: function () {
|
||||
return this.likeCount + this.dislikeCount
|
||||
},
|
||||
|
||||
parsedLikeCount: function () {
|
||||
if (this.hideVideoLikesAndDislikes || this.likeCount === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatNumber(this.likeCount)
|
||||
},
|
||||
|
||||
parsedDislikeCount: function () {
|
||||
if (this.hideVideoLikesAndDislikes || this.dislikeCount === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatNumber(this.dislikeCount)
|
||||
},
|
||||
|
||||
likePercentageRatio: function () {
|
||||
return parseInt(this.likeCount / this.totalLikeCount * 100)
|
||||
},
|
||||
|
||||
parsedViewCount: function () {
|
||||
if (this.hideVideoViews || this.viewCount == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.$t('Global.Counts.View Count', { count: formatNumber(this.viewCount) }, this.viewCount)
|
||||
},
|
||||
|
||||
dateString: function () {
|
||||
const date = new Date(this.published)
|
||||
const localeDateString = new Intl.DateTimeFormat([this.currentLocale, 'en'], { dateStyle: 'medium' }).format(date)
|
||||
// replace spaces with no break spaces to make the date act as a single entity while wrapping
|
||||
return `${localeDateString}`.replaceAll(' ', '\u00A0')
|
||||
},
|
||||
|
||||
publishedString: function () {
|
||||
if (this.isLive) {
|
||||
return this.$t('Video.Started streaming on')
|
||||
} else if (this.isLiveContent && !this.isLive) {
|
||||
return this.$t('Video.Streamed on')
|
||||
} else {
|
||||
return this.$t('Video.Published on')
|
||||
}
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
|
||||
historyEntry: function () {
|
||||
return this.$store.getters.getHistoryCacheById[this.id]
|
||||
},
|
||||
|
||||
historyEntryExists: function () {
|
||||
return typeof this.historyEntry !== 'undefined'
|
||||
},
|
||||
|
||||
defaultPlayback: function () {
|
||||
return this.$store.getters.getDefaultPlayback
|
||||
},
|
||||
|
||||
quickBookmarkPlaylist() {
|
||||
return this.$store.getters.getQuickBookmarkPlaylist
|
||||
},
|
||||
isQuickBookmarkEnabled() {
|
||||
return this.quickBookmarkPlaylist != null
|
||||
},
|
||||
isInQuickBookmarkPlaylist: function () {
|
||||
if (!this.isQuickBookmarkEnabled) { return false }
|
||||
|
||||
// Accessing a reactive property has a negligible amount of overhead,
|
||||
// however as we know that some users have playlists that have more than 10k items in them
|
||||
// it adds up quickly. So create a temporary variable outside of the array, so we only have to do it once.
|
||||
// Also the search is retriggered every time any playlist is modified.
|
||||
const id = this.id
|
||||
|
||||
return this.quickBookmarkPlaylist.videos.some((video) => {
|
||||
return video.videoId === id
|
||||
})
|
||||
},
|
||||
quickBookmarkIconText: function () {
|
||||
if (!this.isQuickBookmarkEnabled) { return false }
|
||||
|
||||
const translationProperties = {
|
||||
playlistName: this.quickBookmarkPlaylist.playlistName,
|
||||
}
|
||||
return this.isInQuickBookmarkPlaylist
|
||||
? this.$t('User Playlists.Remove from Favorites', translationProperties)
|
||||
: this.$t('User Playlists.Add to Favorites', translationProperties)
|
||||
},
|
||||
quickBookmarkIconTheme: function () {
|
||||
return this.isInQuickBookmarkPlaylist ? 'base favorite' : 'base'
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.title,
|
||||
artist: this.channelName,
|
||||
artwork: [
|
||||
{
|
||||
src: this.videoThumbnail,
|
||||
sizes: '128x128',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
this.$watch('$refs.downloadButton.dropdownShown', (dropdownShown) => {
|
||||
this.$emit('set-info-area-sticky', !dropdownShown)
|
||||
|
||||
if (dropdownShown && window.innerWidth >= 901) {
|
||||
// adds a slight delay so we know that the dropdown has shown up
|
||||
// and won't mess up our scrolling
|
||||
nextTick(() => {
|
||||
this.$emit('scroll-to-info-area')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleExternalPlayer: function () {
|
||||
this.$emit('pause-player')
|
||||
|
||||
const payload = {
|
||||
watchProgress: this.getTimestamp(),
|
||||
playbackRate: this.defaultPlayback,
|
||||
videoId: this.id,
|
||||
videoLength: this.lengthSeconds,
|
||||
playlistId: this.playlistId,
|
||||
playlistIndex: this.getPlaylistIndex(),
|
||||
playlistReverse: this.getPlaylistReverse(),
|
||||
playlistShuffle: this.getPlaylistShuffle(),
|
||||
playlistLoop: this.getPlaylistLoop(),
|
||||
}
|
||||
// Only play video in non playlist mode when user playlist detected
|
||||
if (this.inUserPlaylist) {
|
||||
Object.assign(payload, {
|
||||
playlistId: null,
|
||||
playlistIndex: null,
|
||||
playlistReverse: null,
|
||||
playlistShuffle: null,
|
||||
playlistLoop: null,
|
||||
})
|
||||
}
|
||||
|
||||
this.openInExternalPlayer(payload)
|
||||
|
||||
if (this.rememberHistory) {
|
||||
// Marking as watched
|
||||
const videoData = {
|
||||
videoId: this.id,
|
||||
title: this.title,
|
||||
author: this.channelName,
|
||||
authorId: this.channelId,
|
||||
published: this.published,
|
||||
description: this.description,
|
||||
viewCount: this.viewCount,
|
||||
lengthSeconds: this.lengthSeconds,
|
||||
watchProgress: 0,
|
||||
timeWatched: Date.now(),
|
||||
isLive: false,
|
||||
type: 'video'
|
||||
}
|
||||
|
||||
this.updateHistory(videoData)
|
||||
|
||||
if (!this.historyEntryExists) {
|
||||
showToast(this.$t('Video.Video has been marked as watched'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleDownload: function (index) {
|
||||
const selectedDownloadLinkOption = this.downloadLinks[index]
|
||||
const mimeTypeUrl = selectedDownloadLinkOption.value.split('||')
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.downloadBehavior === 'open') {
|
||||
openExternalLink(mimeTypeUrl[1])
|
||||
} else {
|
||||
this.downloadMedia({
|
||||
url: mimeTypeUrl[1],
|
||||
title: this.title,
|
||||
mimeType: mimeTypeUrl[0]
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
grabExtensionFromUrl: function (url) {
|
||||
const regex = /\/(\w*)/i
|
||||
const group = url.match(regex)
|
||||
if (group.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return group[1]
|
||||
},
|
||||
|
||||
togglePlaylistPrompt: function () {
|
||||
const videoData = {
|
||||
videoId: this.id,
|
||||
title: this.title,
|
||||
author: this.channelName,
|
||||
authorId: this.channelId,
|
||||
description: this.description,
|
||||
viewCount: this.viewCount,
|
||||
lengthSeconds: this.lengthSeconds,
|
||||
published: this.published,
|
||||
premiereDate: this.premiereDate,
|
||||
}
|
||||
|
||||
this.showAddToPlaylistPromptForManyVideos({ videos: [videoData] })
|
||||
},
|
||||
|
||||
toggleQuickBookmarked() {
|
||||
if (!this.isQuickBookmarkEnabled) {
|
||||
// This should be prevented by UI
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isInQuickBookmarkPlaylist) {
|
||||
this.removeFromQuickBookmarkPlaylist()
|
||||
} else {
|
||||
this.addToQuickBookmarkPlaylist()
|
||||
}
|
||||
},
|
||||
addToQuickBookmarkPlaylist() {
|
||||
const videoData = {
|
||||
videoId: this.id,
|
||||
title: this.title,
|
||||
author: this.channelName,
|
||||
authorId: this.channelId,
|
||||
lengthSeconds: this.lengthSeconds,
|
||||
published: this.published,
|
||||
premiereDate: this.premiereDate,
|
||||
}
|
||||
|
||||
this.addVideo({
|
||||
_id: this.quickBookmarkPlaylist._id,
|
||||
videoData,
|
||||
})
|
||||
|
||||
// TODO: Maybe show playlist name
|
||||
showToast(this.$t('Video.Video has been saved'))
|
||||
},
|
||||
removeFromQuickBookmarkPlaylist() {
|
||||
this.removeVideo({
|
||||
_id: this.quickBookmarkPlaylist._id,
|
||||
// Remove all playlist items with same videoId
|
||||
videoId: this.id,
|
||||
})
|
||||
|
||||
// TODO: Maybe show playlist name
|
||||
showToast(this.$t('Video.Video has been removed from your saved list'))
|
||||
},
|
||||
|
||||
changeFormat: function(value) {
|
||||
this.$emit('change-format', value)
|
||||
},
|
||||
|
||||
saveWatchedProgressManually() {
|
||||
this.$emit('save-watched-progress')
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'openInExternalPlayer',
|
||||
'downloadMedia',
|
||||
'showAddToPlaylistPromptForManyVideos',
|
||||
'addVideo',
|
||||
'updateHistory',
|
||||
'removeVideo',
|
||||
])
|
||||
}
|
||||
})
|
||||
@@ -1,159 +0,0 @@
|
||||
<template>
|
||||
<ft-card class="watchVideoInfo">
|
||||
<div>
|
||||
<h1
|
||||
class="videoTitle"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="isUnlisted"
|
||||
class="unlistedBadge"
|
||||
>
|
||||
{{ $t('Video.Unlisted') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="videoMetrics">
|
||||
<div class="datePublishedAndViewCount">
|
||||
{{ publishedString }} {{ dateString }}
|
||||
<template
|
||||
v-if="!hideVideoViews"
|
||||
>
|
||||
<span class="seperator">• </span><span class="videoViews">{{ parsedViewCount }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hideVideoLikesAndDislikes"
|
||||
class="likeBarContainer"
|
||||
>
|
||||
<div
|
||||
class="likeSection"
|
||||
>
|
||||
<span class="likeCount"><font-awesome-icon :icon="['fas', 'thumbs-up']" /> {{ parsedLikeCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
// Uncomment if suitable solution for bringing back dislikes is introduced
|
||||
<div
|
||||
v-if="!hideVideoLikesAndDislikes"
|
||||
class="likeBarContainer"
|
||||
>
|
||||
<div
|
||||
class="likeSection"
|
||||
>
|
||||
<div
|
||||
class="likeBar"
|
||||
:style="{ background: `linear-gradient(to right, var(--accent-color) ${likePercentageRatio}%, #9E9E9E ${likePercentageRatio}%` }"
|
||||
/>
|
||||
<div>
|
||||
<span class="likeCount"><font-awesome-icon :icon="['fas', 'thumbs-up']" /> {{ parsedLikeCount }}</span>
|
||||
<span class="dislikeCount"><font-awesome-icon :icon="['fas', 'thumbs-down']" /> {{ parsedDislikeCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<div class="videoButtons">
|
||||
<div
|
||||
class="profileRow"
|
||||
>
|
||||
<div
|
||||
v-if="!hideUploader"
|
||||
>
|
||||
<router-link
|
||||
:to="`/channel/${channelId}`"
|
||||
>
|
||||
<img
|
||||
:src="channelThumbnail"
|
||||
class="channelThumbnail"
|
||||
alt=""
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="!hideUploader"
|
||||
>
|
||||
<router-link
|
||||
:to="`/channel/${channelId}`"
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</router-link>
|
||||
</div>
|
||||
<ft-subscribe-button
|
||||
v-if="!hideUnsubscribeButton"
|
||||
:channel-id="channelId"
|
||||
:channel-name="channelName"
|
||||
:channel-thumbnail="channelThumbnail"
|
||||
:subscription-count-text="subscriptionCountText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="videoOptions">
|
||||
<span class="videoOptionsMobileRow">
|
||||
<ft-icon-button
|
||||
v-if="showPlaylists && !isUpcoming"
|
||||
:title="$t('User Playlists.Add to Playlist')"
|
||||
:icon="['fas', 'plus']"
|
||||
theme="base"
|
||||
@click="togglePlaylistPrompt"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="isQuickBookmarkEnabled"
|
||||
:title="quickBookmarkIconText"
|
||||
:icon="isInQuickBookmarkPlaylist ? ['fas', 'check'] : ['fas', 'bookmark']"
|
||||
class="quickBookmarkVideoIcon"
|
||||
:class="{
|
||||
bookmarked: isInQuickBookmarkPlaylist,
|
||||
}"
|
||||
:theme="quickBookmarkIconTheme"
|
||||
@click="toggleQuickBookmarked"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="canSaveWatchedProgress && watchedProgressSavingInSemiAutoMode"
|
||||
:title="$t('Video.Save Watched Progress')"
|
||||
:icon="['fas', 'bars-progress']"
|
||||
@click="saveWatchedProgressManually"
|
||||
/>
|
||||
</span>
|
||||
<span class="videoOptionsMobileRow">
|
||||
<ft-icon-button
|
||||
v-if="usingElectron && externalPlayer !== ''"
|
||||
:title="$t('Video.External Player.OpenInTemplate', { externalPlayer })"
|
||||
:icon="['fas', 'external-link-alt']"
|
||||
theme="secondary"
|
||||
@click="handleExternalPlayer"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isUpcoming && downloadLinks.length > 0"
|
||||
ref="downloadButton"
|
||||
:title="$t('Video.Download Video')"
|
||||
theme="secondary"
|
||||
:icon="['fas', 'download']"
|
||||
:return-index="true"
|
||||
:dropdown-options="downloadLinks"
|
||||
@click="handleDownload"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isUpcoming"
|
||||
:title="$t('Change Format.Change Media Formats')"
|
||||
theme="secondary"
|
||||
:icon="['fas', 'file-video']"
|
||||
:dropdown-options="formatTypeOptions"
|
||||
@click="changeFormat"
|
||||
/>
|
||||
<ft-share-button
|
||||
v-if="!hideSharingActions"
|
||||
:id="id"
|
||||
:get-timestamp="getTimestamp"
|
||||
:playlist-id="playlistId"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ft-card>
|
||||
</template>
|
||||
|
||||
<script src="./watch-video-info.js" />
|
||||
<style scoped src="./watch-video-info.css" />
|
||||
@@ -529,43 +529,6 @@ export async function getInvidiousPopularFeed() {
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'default' | 'music' | 'gaming' | 'movies'} tab
|
||||
* @param {string} region
|
||||
* @returns {Promise<InvidiousVideoType[] | null>}
|
||||
*/
|
||||
export async function getInvidiousTrending(tab, region) {
|
||||
const params = {
|
||||
resource: 'trending',
|
||||
id: '',
|
||||
params: {
|
||||
region
|
||||
}
|
||||
}
|
||||
|
||||
if (tab !== 'default') {
|
||||
params.params.type = tab.charAt(0).toUpperCase() + tab.substring(1)
|
||||
}
|
||||
|
||||
const response = await invidiousAPICall(params)
|
||||
|
||||
if (!response) {
|
||||
return null
|
||||
}
|
||||
|
||||
const items = response.filter((item) => {
|
||||
return item.type === 'video' || item.type === 'channel' || item.type === 'playlist'
|
||||
})
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.type === 'video') {
|
||||
setPublishedTimestamp(item)
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
* @param {number} page
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClientType, Innertube, Misc, Parser, Platform, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js'
|
||||
import { ClientType, Innertube, Misc, Mixins, Parser, Platform, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js'
|
||||
import Autolinker from 'autolinker'
|
||||
import { SEARCH_CHAR_LIMIT } from '../../../constants'
|
||||
|
||||
@@ -361,42 +361,43 @@ export async function untilEndOfLocalPlayList(playlist, callback, options = { ru
|
||||
|
||||
/**
|
||||
* @param {string} location
|
||||
* @param {'default'|'music'|'gaming'|'movies'} tab
|
||||
* @param {import('youtubei.js').Mixins.TabbedFeed<import('youtubei.js').IBrowseResponse> | null} instance
|
||||
* @param {'gaming' | 'sports' | 'podcasts'} tab
|
||||
*/
|
||||
export async function getLocalTrending(location, tab, instance) {
|
||||
if (instance === null) {
|
||||
const innertube = await createInnertube({ location })
|
||||
instance = await innertube.getTrending()
|
||||
}
|
||||
export async function getLocalTrending(location, tab) {
|
||||
const innertube = await createInnertube({ location })
|
||||
|
||||
// youtubei.js's tab names are localised, so we need to use the index to get tab name that youtubei.js expects
|
||||
const tabIndex = ['default', 'music', 'gaming', 'movies'].indexOf(tab)
|
||||
const resultsInstance = await instance.getTabByName(instance.tabs[tabIndex])
|
||||
let args
|
||||
|
||||
let results
|
||||
|
||||
// the default tab can have duplicate videos so we need to deduplicate them
|
||||
if (tab === 'default') {
|
||||
const alreadySeenIds = []
|
||||
results = []
|
||||
|
||||
resultsInstance.videos.forEach(video => {
|
||||
if (video.type === 'Video' && !alreadySeenIds.includes(video.video_id)) {
|
||||
alreadySeenIds.push(video.video_id)
|
||||
results.push(parseLocalListVideo(video))
|
||||
switch (tab) {
|
||||
case 'gaming':
|
||||
// https://www.youtube.com/gaming/trending
|
||||
args = {
|
||||
browseId: 'UCOpNcN46UbXVtpKMrmU4Abg',
|
||||
params: 'Egh0cmVuZGluZ7gBAJIDAPIGBAoCMgA'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
results = resultsInstance.videos
|
||||
.filter((video) => video.type === 'Video')
|
||||
.map(parseLocalListVideo)
|
||||
break
|
||||
case 'sports':
|
||||
// https://www.youtube.com/channel/UCEgdi0XIXXZ-qJOFPf4JSKw/sportstab?ss=CMMG
|
||||
args = {
|
||||
browseId: 'UCEgdi0XIXXZ-qJOFPf4JSKw',
|
||||
params: 'EglzcG9ydHN0YWK4AQCSAwDyBgQKAjIA'
|
||||
}
|
||||
break
|
||||
case 'podcasts':
|
||||
// https://www.youtube.com/podcasts/popularepisodes
|
||||
args = {
|
||||
browseId: 'FEpodcasts_destination',
|
||||
params: 'qgcCCAM%3D'
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('Unknown trending tab')
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
instance: resultsInstance
|
||||
}
|
||||
const response = await innertube.actions.execute('/browse', args)
|
||||
const feed = new Mixins.Feed(innertube.actions, response)
|
||||
|
||||
return feed.videos.map(video => parseLocalListVideo(video))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1292,27 +1293,12 @@ export function parseLocalPlaylistVideo(video) {
|
||||
|
||||
let viewCount = null
|
||||
|
||||
// the accessiblity label contains the full view count
|
||||
// the video info only contains the short view count
|
||||
if (video_.accessibility_label) {
|
||||
// the `.*\s+` at the start of the regex, ensures we match the last occurence
|
||||
// just in case the video title also contains that pattern
|
||||
const match = video_.accessibility_label.match(/.*\s+([\d,.]+|no)\s+views?/)
|
||||
const viewsText = video_.video_info.runs?.find(run => VIEWS_OR_WATCHING_REGEX.test(run.text))?.text
|
||||
|
||||
if (match) {
|
||||
const count = match[1]
|
||||
|
||||
// as it's rare that a video has no views,
|
||||
// checking the length allows us to avoid running toLowerCase unless we have to
|
||||
if (count.length === 2 && count === 'no') {
|
||||
viewCount = 0
|
||||
} else {
|
||||
const views = extractNumberFromString(count)
|
||||
|
||||
if (!isNaN(views)) {
|
||||
viewCount = views
|
||||
}
|
||||
}
|
||||
if (viewsText) {
|
||||
const views = parseLocalSubscriberCount(viewsText)
|
||||
if (!isNaN(views)) {
|
||||
viewCount = views
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,13 +79,13 @@ import {
|
||||
faLock,
|
||||
faMessage,
|
||||
faMoneyCheckDollar,
|
||||
faMusic,
|
||||
faNetworkWired,
|
||||
faNewspaper,
|
||||
faPalette,
|
||||
faPhotoFilm,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faPodcast,
|
||||
faQuestionCircle,
|
||||
faRandom,
|
||||
faRetweet,
|
||||
@@ -111,6 +111,7 @@ import {
|
||||
faTimesCircle,
|
||||
faTowerBroadcast,
|
||||
faTrash,
|
||||
faTrophy,
|
||||
faUserCheck,
|
||||
faUserLock,
|
||||
faUsers,
|
||||
@@ -203,13 +204,13 @@ library.add(
|
||||
faLock,
|
||||
faMessage,
|
||||
faMoneyCheckDollar,
|
||||
faMusic,
|
||||
faNetworkWired,
|
||||
faNewspaper,
|
||||
faPalette,
|
||||
faPhotoFilm,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faPodcast,
|
||||
faQuestionCircle,
|
||||
faRandom,
|
||||
faRetweet,
|
||||
@@ -235,6 +236,7 @@ library.add(
|
||||
faTimesCircle,
|
||||
faTowerBroadcast,
|
||||
faTrash,
|
||||
faTrophy,
|
||||
faUserCheck,
|
||||
faUserLock,
|
||||
faUsers,
|
||||
|
||||
@@ -42,14 +42,16 @@ const router = createRouter({
|
||||
},
|
||||
component: SubscribedChannels
|
||||
},
|
||||
{
|
||||
path: '/trending',
|
||||
name: 'trending',
|
||||
meta: {
|
||||
title: 'Trending'
|
||||
},
|
||||
component: Trending
|
||||
},
|
||||
...(process.env.SUPPORTS_LOCAL_API
|
||||
? [{
|
||||
path: '/trending',
|
||||
name: 'trending',
|
||||
meta: {
|
||||
title: 'Trending'
|
||||
},
|
||||
component: Trending
|
||||
}]
|
||||
: []),
|
||||
{
|
||||
path: '/popular',
|
||||
name: 'popular',
|
||||
|
||||
@@ -17,10 +17,9 @@ const state = {
|
||||
sessionSearchHistory: [],
|
||||
popularCache: null,
|
||||
trendingCache: {
|
||||
default: null,
|
||||
music: null,
|
||||
gaming: null,
|
||||
movies: null
|
||||
sports: null,
|
||||
podcasts: null
|
||||
},
|
||||
cachedPlaylist: null,
|
||||
deArrowCache: {},
|
||||
@@ -49,10 +48,9 @@ const state = {
|
||||
externalPlayerCmdArguments: {},
|
||||
lastPopularRefreshTimestamp: '',
|
||||
lastTrendingRefreshTimestamp: {
|
||||
default: '',
|
||||
music: '',
|
||||
gaming: '',
|
||||
movies: ''
|
||||
sports: '',
|
||||
podcasts: ''
|
||||
},
|
||||
subscriptionFirstAutoFetchRunData: {
|
||||
videos: false,
|
||||
@@ -656,9 +654,10 @@ const actions = {
|
||||
|
||||
if (feedType === 'playlists' || feedType === 'you' || feedType === 'library') {
|
||||
return { urlType: 'userplaylists' }
|
||||
} else {
|
||||
} else if (process.env.SUPPORTS_LOCAL_API || feedType !== 'trending') {
|
||||
return { urlType: feedType }
|
||||
}
|
||||
// Can fall through if a trending URL is detected in a build without the local API
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -911,7 +910,7 @@ const mutations = {
|
||||
|
||||
/**
|
||||
* @param {typeof state} state
|
||||
* @param {{page: 'default' | 'music' | 'gaming' | 'movies', timestamp: Date}} param1
|
||||
* @param {{page: 'gaming' | 'sports' | 'podcasts', timestamp: Date}} param1
|
||||
*/
|
||||
setLastTrendingRefreshTimestamp (state, { page, timestamp }) {
|
||||
state.lastTrendingRefreshTimestamp[page] = timestamp
|
||||
@@ -923,7 +922,7 @@ const mutations = {
|
||||
|
||||
/**
|
||||
* @param {typeof state} state
|
||||
* @param {'default' | 'music' | 'gaming' | 'movies'} page
|
||||
* @param {'gaming' | 'sports' | 'podcasts'} page
|
||||
*/
|
||||
clearTrendingCache(state, page) {
|
||||
state.trendingCache[page] = null
|
||||
|
||||
@@ -68,6 +68,13 @@ body:not(.hotPink, .pastelPink) {
|
||||
--logo-tertiary-color: #000;
|
||||
}
|
||||
|
||||
/* Text "app" should be visible but retain FreeTube color logo */
|
||||
.system[data-system-theme*='dark'],
|
||||
.dark,
|
||||
.black {
|
||||
--logo-tertiary-color: #fff;
|
||||
}
|
||||
|
||||
/* Given that the Hot Pink theme does not need link underlining due to meeting
|
||||
WCAG 2 Level AA (https://webaim.org/resources/linkcontrastchecker/?fcolor=FFFFFF&bcolor=DE1C85&lcolor=000000),
|
||||
it can be safely elided. This looks quite pleasant on this theme. */
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -270,7 +270,7 @@ if (oldQuery != null && oldQuery !== '') {
|
||||
filterHistory()
|
||||
}
|
||||
|
||||
const searchBar = ref(null)
|
||||
const searchBar = useTemplateRef('searchBar')
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
|
||||
@@ -7,7 +7,7 @@ import ExternalPlayerSettings from '../../components/ExternalPlayerSettings.vue'
|
||||
import SubscriptionSettings from '../../components/SubscriptionSettings/SubscriptionSettings.vue'
|
||||
import DownloadSettings from '../../components/DownloadSettings/DownloadSettings.vue'
|
||||
import PrivacySettings from '../../components/PrivacySettings.vue'
|
||||
import DataSettings from '../../components/DataSettings.vue'
|
||||
import DataSettings from '../../components/DataSettings/DataSettings.vue'
|
||||
import DistractionSettings from '../../components/DistractionSettings/DistractionSettings.vue'
|
||||
import ProxySettings from '../../components/ProxySettings/ProxySettings.vue'
|
||||
import SponsorBlockSettings from '../../components/SponsorBlockSettings.vue'
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onBeforeUnmount, ref, watch, useTemplateRef } from 'vue'
|
||||
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||
@@ -110,8 +110,7 @@ const query = ref('')
|
||||
const subscribedChannels = ref([])
|
||||
const filteredChannels = ref([])
|
||||
|
||||
/** @type {import('vue').Ref<HTMLInputElement | null>} */
|
||||
const searchBarChannels = ref(null)
|
||||
const searchBarChannels = useTemplateRef('searchBarChannels')
|
||||
|
||||
/** @type {import('vue').ComputedRef<object>} */
|
||||
const activeProfile = computed(() => {
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||
@@ -244,14 +244,10 @@ function changeTab(tab) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const videosTab = ref(null)
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const liveTab = ref(null)
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const shortsTab = ref(null)
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const communityTab = ref(null)
|
||||
const videosTab = useTemplateRef('videosTab')
|
||||
const liveTab = useTemplateRef('liveTab')
|
||||
const shortsTab = useTemplateRef('shortsTab')
|
||||
const communityTab = useTemplateRef('communityTab')
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
.trendingInfoTabs {
|
||||
inline-size: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
margin-block: -3px 10px;
|
||||
color: var(--tertiary-text-color);
|
||||
}
|
||||
|
||||
@@ -16,48 +16,6 @@
|
||||
role="tablist"
|
||||
:aria-label="$t('Trending.Trending Tabs')"
|
||||
>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
||||
<div
|
||||
ref="defaultTab"
|
||||
class="tab"
|
||||
role="tab"
|
||||
:aria-selected="currentTab === 'default'"
|
||||
aria-controls="trendingPanel"
|
||||
:tabindex="currentTab === 'default' ? 0 : -1"
|
||||
:class="{ selectedTab: currentTab === 'default' }"
|
||||
@click="changeTab('default')"
|
||||
@keydown.space.enter.prevent="changeTab('default')"
|
||||
@keydown.left="focusTab('movies', $event)"
|
||||
@keydown.right="focusTab('music', $event)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'fire']"
|
||||
class="trendingIcon"
|
||||
fixed-width
|
||||
/>
|
||||
{{ $t("Trending.Default") }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
||||
<div
|
||||
ref="musicTab"
|
||||
class="tab"
|
||||
role="tab"
|
||||
:aria-selected="currentTab === 'music'"
|
||||
aria-controls="trendingPanel"
|
||||
:tabindex="currentTab === 'music' ? 0 : -1"
|
||||
:class="{ selectedTab: currentTab === 'music' }"
|
||||
@click="changeTab('music')"
|
||||
@keydown.space.enter.prevent="changeTab('music')"
|
||||
@keydown.left="focusTab('default', $event)"
|
||||
@keydown.right="focusTab('gaming', $event)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'music']"
|
||||
class="trendingIcon"
|
||||
fixed-width
|
||||
/>
|
||||
{{ $t("Trending.Music") }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
||||
<div
|
||||
ref="gamingTab"
|
||||
@@ -69,8 +27,8 @@
|
||||
:class="{ selectedTab: currentTab === 'gaming' }"
|
||||
@click="changeTab('gaming')"
|
||||
@keydown.space.enter.prevent="changeTab('gaming')"
|
||||
@keydown.left="focusTab('music', $event)"
|
||||
@keydown.right="focusTab('movies', $event)"
|
||||
@keydown.left="focusTab('podcasts', $event)"
|
||||
@keydown.right="focusTab('sports', $event)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'gamepad']"
|
||||
@@ -81,24 +39,45 @@
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
||||
<div
|
||||
ref="moviesTab"
|
||||
ref="sportsTab"
|
||||
class="tab"
|
||||
role="tab"
|
||||
:aria-selected="currentTab === 'movies'"
|
||||
:aria-selected="currentTab === 'sports'"
|
||||
aria-controls="trendingPanel"
|
||||
:tabindex="currentTab === 'movies' ? 0 : -1"
|
||||
:class="{ selectedTab: currentTab === 'movies' }"
|
||||
@click="changeTab('movies')"
|
||||
@keydown.space.enter.prevent="changeTab('movies')"
|
||||
:tabindex="currentTab === 'sports' ? 0 : -1"
|
||||
:class="{ selectedTab: currentTab === 'sports' }"
|
||||
@click="changeTab('sports')"
|
||||
@keydown.space.enter.prevent="changeTab('sports')"
|
||||
@keydown.left="focusTab('gaming', $event)"
|
||||
@keydown.right="focusTab('default', $event)"
|
||||
@keydown.right="focusTab('podcasts', $event)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'film']"
|
||||
:icon="['fas', 'trophy']"
|
||||
class="trendingIcon"
|
||||
fixed-width
|
||||
/>
|
||||
{{ $t("Trending.Movies") }}
|
||||
{{ t("Trending.Sports") }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
||||
<div
|
||||
ref="podcastsTab"
|
||||
class="tab"
|
||||
role="tab"
|
||||
:aria-selected="currentTab === 'podcasts'"
|
||||
aria-controls="trendingPanel"
|
||||
:tabindex="currentTab === 'podcasts' ? 0 : -1"
|
||||
:class="{ selectedTab: currentTab === 'podcasts' }"
|
||||
@click="changeTab('podcasts')"
|
||||
@keydown.space.enter.prevent="changeTab('podcasts')"
|
||||
@keydown.left="focusTab('sports', $event)"
|
||||
@keydown.right="focusTab('gaming', $event)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'podcast']"
|
||||
class="trendingIcon"
|
||||
fixed-width
|
||||
/>
|
||||
{{ t("Channel.Podcasts.Podcasts") }}
|
||||
</div>
|
||||
</FtFlexBox>
|
||||
<div
|
||||
@@ -125,7 +104,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
@@ -138,7 +117,6 @@ import store from '../../store/index'
|
||||
|
||||
import { copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils'
|
||||
import { getLocalTrending } from '../../helpers/api/local'
|
||||
import { getInvidiousTrending } from '../../helpers/api/invidious'
|
||||
import { KeyboardShortcuts } from '../../../constants'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -162,16 +140,16 @@ const region = computed(() => {
|
||||
return store.getters.getRegion.toUpperCase()
|
||||
})
|
||||
|
||||
/** @type {import('vue').ComputedRef<{ default: any[] | null, music: any[] | null, gaming: any[] | null, movies: any[] | null }>} */
|
||||
/** @type {import('vue').ComputedRef<{ gaming: any[] | null, sports: any[] | null, podcasts: any[] | null }>} */
|
||||
const trendingCache = computed(() => {
|
||||
return store.getters.getTrendingCache
|
||||
})
|
||||
|
||||
const isLoading = ref({ default: false, music: false, gaming: false, movies: false })
|
||||
const isLoading = ref({ gaming: false, sports: false, podcasts: false })
|
||||
const shownResults = shallowRef([])
|
||||
|
||||
/** @type {import('vue').Ref<'default' | 'music' | 'gaming' | 'movies'>} */
|
||||
const currentTab = ref('default')
|
||||
/** @type {import('vue').Ref<'gaming' | 'sports' | 'podcasts'>} */
|
||||
const currentTab = ref('gaming')
|
||||
|
||||
const cacheEntry = trendingCache.value[currentTab.value]
|
||||
|
||||
@@ -183,21 +161,12 @@ if (cacheEntry && cacheEntry.length > 0) {
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {import('youtubei.js').Mixins.TabbedFeed<import('youtubei.js').IBrowseResponse> | null} */
|
||||
let trendingInstance = null
|
||||
|
||||
function getTrendingInfo(refresh = false) {
|
||||
if (refresh) {
|
||||
if (process.env.SUPPORTS_LOCAL_API) {
|
||||
trendingInstance = null
|
||||
}
|
||||
|
||||
store.commit('clearTrendingCache', currentTab.value)
|
||||
}
|
||||
|
||||
if (!process.env.SUPPORTS_LOCAL_API || backendPreference.value === 'invidious') {
|
||||
getTrendingInfoInvidious()
|
||||
} else {
|
||||
if (process.env.SUPPORTS_LOCAL_API && (backendFallback.value || backendPreference.value === 'local')) {
|
||||
getTrendingInfoLocal()
|
||||
}
|
||||
|
||||
@@ -208,11 +177,10 @@ async function getTrendingInfoLocal() {
|
||||
isLoading.value[currentTab.value] = true
|
||||
|
||||
try {
|
||||
const { results, instance } = await getLocalTrending(region.value, currentTab.value, trendingInstance)
|
||||
const results = await getLocalTrending(region.value, currentTab.value)
|
||||
|
||||
shownResults.value = results
|
||||
isLoading.value[currentTab.value] = false
|
||||
trendingInstance = instance
|
||||
|
||||
store.commit('setTrendingCache', { value: results, page: currentTab.value })
|
||||
nextTick(() => {
|
||||
@@ -224,56 +192,16 @@ async function getTrendingInfoLocal() {
|
||||
showToast(`${errorMessage}: ${error}`, 10000, () => {
|
||||
copyToClipboard(error)
|
||||
})
|
||||
if (backendPreference.value === 'local' && backendFallback.value) {
|
||||
showToast(t('Falling back to Invidious API'))
|
||||
getTrendingInfoInvidious()
|
||||
} else {
|
||||
isLoading.value[currentTab.value] = false
|
||||
}
|
||||
isLoading.value[currentTab.value] = false
|
||||
}
|
||||
}
|
||||
|
||||
function getTrendingInfoInvidious() {
|
||||
isLoading.value[currentTab.value] = true
|
||||
|
||||
getInvidiousTrending(currentTab.value, region.value).then((items) => {
|
||||
if (!items) {
|
||||
return
|
||||
}
|
||||
|
||||
shownResults.value = items
|
||||
isLoading.value[currentTab.value] = false
|
||||
store.commit('setTrendingCache', { value: items, page: currentTab.value })
|
||||
nextTick(() => {
|
||||
focusTab(currentTab.value)
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = t('Invidious API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
|
||||
if (process.env.SUPPORTS_LOCAL_API && backendPreference.value === 'invidious' && backendFallback.value) {
|
||||
showToast(t('Falling back to Local API'))
|
||||
getTrendingInfoLocal()
|
||||
} else {
|
||||
isLoading.value[currentTab.value] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const defaultTab = ref(null)
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const musicTab = ref(null)
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const gamingTab = ref(null)
|
||||
/** @type {import('vue').Ref<HTMLDivElement | null>} */
|
||||
const moviesTab = ref(null)
|
||||
const gamingTab = useTemplateRef('gamingTab')
|
||||
const sportsTab = useTemplateRef('sportsTab')
|
||||
const podcastsTab = useTemplateRef('podcastsTab')
|
||||
|
||||
/**
|
||||
* @param {'default' | 'music' | 'gaming' | 'movies'} tab
|
||||
* @param {'gaming' | 'sports' | 'podcasts'} tab
|
||||
* @param {KeyboardEvent | undefined} event
|
||||
*/
|
||||
function focusTab(tab, event = undefined) {
|
||||
@@ -285,23 +213,20 @@ function focusTab(tab, event = undefined) {
|
||||
}
|
||||
|
||||
switch (tab) {
|
||||
case 'default':
|
||||
defaultTab.value?.focus()
|
||||
break
|
||||
case 'music':
|
||||
musicTab.value?.focus()
|
||||
break
|
||||
case 'gaming':
|
||||
gamingTab.value?.focus()
|
||||
break
|
||||
case 'movies':
|
||||
moviesTab.value?.focus()
|
||||
case 'sports':
|
||||
sportsTab.value?.focus()
|
||||
break
|
||||
case 'podcasts':
|
||||
podcastsTab.value?.focus()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'default' | 'music' | 'gaming' | 'movies'} tab
|
||||
* @param {'gaming' | 'sports' | 'podcasts'} tab
|
||||
*/
|
||||
function changeTab(tab) {
|
||||
if (tab === currentTab.value) {
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -409,7 +409,7 @@ if (oldQuery != null && oldQuery !== '') {
|
||||
filterPlaylist()
|
||||
}
|
||||
|
||||
const searchBar = ref(null)
|
||||
const searchBar = useTemplateRef('searchBar')
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
|
||||
@@ -4,7 +4,7 @@ import shaka from 'shaka-player'
|
||||
import { Utils, YTNodes } from 'youtubei.js'
|
||||
import FtLoader from '../../components/FtLoader/FtLoader.vue'
|
||||
import FtShakaVideoPlayer from '../../components/ft-shaka-video-player/ft-shaka-video-player.vue'
|
||||
import WatchVideoInfo from '../../components/watch-video-info/watch-video-info.vue'
|
||||
import WatchVideoInfo from '../../components/WatchVideoInfo/WatchVideoInfo.vue'
|
||||
import WatchVideoChapters from '../../components/WatchVideoChapters/WatchVideoChapters.vue'
|
||||
import WatchVideoDescription from '../../components/WatchVideoDescription/WatchVideoDescription.vue'
|
||||
import CommentSection from '../../components/CommentSection/CommentSection.vue'
|
||||
|
||||
@@ -138,10 +138,7 @@ Channels:
|
||||
Unsubscribe Prompt: ''
|
||||
Trending:
|
||||
Trending: ''
|
||||
Default: ''
|
||||
Music: ''
|
||||
Gaming: ''
|
||||
Movies: ''
|
||||
Trending Tabs: ''
|
||||
Most Popular: ''
|
||||
Feed:
|
||||
|
||||
@@ -140,10 +140,7 @@ Channels:
|
||||
Unsubscribe Prompt: ''
|
||||
Trending:
|
||||
Trending: ''
|
||||
Default: ''
|
||||
Music: ''
|
||||
Gaming: ''
|
||||
Movies: ''
|
||||
Trending Tabs: ''
|
||||
Most Popular: ''
|
||||
Feed:
|
||||
|
||||
@@ -106,10 +106,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'المحتوى الرائج'
|
||||
Trending Tabs: علامات التبويب الشائعة
|
||||
Movies: أفلام
|
||||
Gaming: الالعاب
|
||||
Music: الموسيقى
|
||||
Default: الإفتراضي
|
||||
Most Popular: 'الأكثر شعبية'
|
||||
Playlists: 'قوائم التشغيل'
|
||||
User Playlists:
|
||||
|
||||
@@ -148,10 +148,7 @@ Channels:
|
||||
Unsubscribe Prompt: ''
|
||||
Trending:
|
||||
Trending: ''
|
||||
Default: ''
|
||||
Music: ''
|
||||
Gaming: ''
|
||||
Movies: ''
|
||||
Trending Tabs: ''
|
||||
Most Popular: ''
|
||||
Feed:
|
||||
|
||||
@@ -98,10 +98,7 @@ Subscriptions:
|
||||
More: 'Daha Çox'
|
||||
Trending:
|
||||
Trending: 'Trenddədir'
|
||||
Default: 'İlkin'
|
||||
Music: 'Musiqi'
|
||||
Gaming: 'Oyun'
|
||||
Movies: 'Filmlər'
|
||||
Trending Tabs: 'Trend Səhifələr'
|
||||
Most Popular: 'Ən Tanınmış'
|
||||
Playlists: 'Pleylistlər'
|
||||
|
||||
@@ -125,10 +125,7 @@ Channels:
|
||||
Unsubscribe Prompt: 'Сапраўды хочаце адпісацца ад «{channelName}»?'
|
||||
Trending:
|
||||
Trending: 'У трэндзе'
|
||||
Default: 'Па змаўчанні'
|
||||
Music: 'Музыка'
|
||||
Gaming: 'Гульні'
|
||||
Movies: 'Фільмы'
|
||||
Trending Tabs: 'Укладкі Трэндаў'
|
||||
Most Popular: 'Папулярныя'
|
||||
Playlists: 'Плэй-лісты'
|
||||
|
||||
@@ -110,10 +110,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Набиращи популярност'
|
||||
Trending Tabs: Раздели за набиращи популярност
|
||||
Movies: Филми
|
||||
Gaming: Игри
|
||||
Music: Музика
|
||||
Default: По подразбиране
|
||||
Most Popular: 'Най-популярни'
|
||||
Playlists: 'Плейлисти'
|
||||
User Playlists:
|
||||
|
||||
@@ -84,9 +84,6 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'চলছে'
|
||||
Gaming: গেমিং
|
||||
Default: পূর্বনির্ধারিত
|
||||
Music: সঙ্গীত
|
||||
Movies: সিনেমা
|
||||
Trending Tabs: চলমান ভিডিও ট্যাব
|
||||
History:
|
||||
# On History Page
|
||||
|
||||
@@ -141,10 +141,7 @@ Channels:
|
||||
Unsubscribe Prompt: 'Ha sur oc''h e faot deoc''h digoumanantiñ deus "{channelName}" ?'
|
||||
Trending:
|
||||
Trending: 'Diouzh ar cʼhiz'
|
||||
Default: 'Dre ziouer'
|
||||
Music: 'Sonerezh'
|
||||
Gaming: 'C''hoarioù video'
|
||||
Movies: 'Filmoù'
|
||||
Trending Tabs: 'Ivinelloù ar re diouzh ar c''hiz'
|
||||
Most Popular: 'Ar muiañ a verzh ganto'
|
||||
Feed:
|
||||
|
||||
@@ -84,11 +84,8 @@ Subscriptions:
|
||||
Error Channels: Canals amb errors
|
||||
Trending:
|
||||
Trending: 'Tendències'
|
||||
Default: Per defecte
|
||||
Music: Música
|
||||
Gaming: Jocs
|
||||
Trending Tabs: Pestanyes de tendència
|
||||
Movies: Pel·lícules
|
||||
Most Popular: 'Més populars'
|
||||
Playlists: 'Llistes de reproducció'
|
||||
User Playlists:
|
||||
|
||||
@@ -122,10 +122,7 @@ Channels:
|
||||
Unsubscribe Prompt: ''
|
||||
Trending:
|
||||
Trending: 'باو'
|
||||
Default: 'بنەڕەت'
|
||||
Music: 'مۆسیقا'
|
||||
Gaming: 'یاری'
|
||||
Movies: 'فیلم'
|
||||
Trending Tabs: ''
|
||||
Most Popular: 'باوترین'
|
||||
Playlists: 'پێڕستی لێدانەکان'
|
||||
|
||||
@@ -110,10 +110,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Trendy'
|
||||
Trending Tabs: Tabulka trendů
|
||||
Movies: Filmy
|
||||
Gaming: Hry
|
||||
Music: Hudba
|
||||
Default: Výchozí
|
||||
Sports: Sporty
|
||||
Most Popular: 'Populární'
|
||||
Playlists: 'Playlisty'
|
||||
User Playlists:
|
||||
|
||||
@@ -126,10 +126,7 @@ Channels:
|
||||
Unsubscribe Prompt: 'Ydych chi''n siŵr eich bod am ddad-danysgrifio o "{channelName}"?'
|
||||
Trending:
|
||||
Trending: 'Trendio'
|
||||
Default: 'Rhagosodedig'
|
||||
Music: 'Cerddoriaeth'
|
||||
Gaming: 'Gemau'
|
||||
Movies: 'Ffilmiau'
|
||||
Trending Tabs: 'Tabiau Trendio'
|
||||
Most Popular: 'Poblogaidd'
|
||||
Playlists: 'Rhestrau'
|
||||
|
||||
@@ -108,11 +108,8 @@ Subscriptions:
|
||||
All Subscription Tabs Hidden: Alle abonnement-faner er skjult. Gør nogle faner synlige i "{subsection}" under "{settingsSection}" for at se indhold her.
|
||||
Trending:
|
||||
Trending: 'Hot lige nu'
|
||||
Music: Musik
|
||||
Trending Tabs: Hot lige nu-faner
|
||||
Gaming: Gaming
|
||||
Default: Standard
|
||||
Movies: Film
|
||||
Most Popular: 'Mest populære'
|
||||
Playlists: 'Playlister'
|
||||
User Playlists:
|
||||
|
||||
@@ -103,10 +103,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: Trends
|
||||
Trending Tabs: Trends-Tabs
|
||||
Movies: Filme
|
||||
Gaming: Gaming
|
||||
Music: Musik
|
||||
Default: Standard
|
||||
Most Popular: Am beliebtesten
|
||||
Playlists: Playlists
|
||||
User Playlists:
|
||||
|
||||
@@ -105,9 +105,6 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Τάσεις'
|
||||
Gaming: Παιχνίδια
|
||||
Music: Μουσική
|
||||
Movies: Ταινίες
|
||||
Default: Προεπιλογή
|
||||
Trending Tabs: Καρτέλες Τάσεων
|
||||
Most Popular: 'Δημοφιλέστερα'
|
||||
Playlists: 'Λίστες αναπαραγωγής'
|
||||
|
||||
@@ -110,10 +110,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Trending'
|
||||
Trending Tabs: Trending Tabs
|
||||
Movies: Films
|
||||
Gaming: Gaming
|
||||
Music: Music
|
||||
Default: Default
|
||||
Sports: Sports
|
||||
Most Popular: 'Most Popular'
|
||||
Playlists: 'Playlists'
|
||||
User Playlists:
|
||||
|
||||
@@ -143,10 +143,8 @@ Channels:
|
||||
Unsubscribe Prompt: Are you sure you want to unsubscribe from "{channelName}"?
|
||||
Trending:
|
||||
Trending: Trending
|
||||
Default: Default
|
||||
Music: Music
|
||||
Gaming: Gaming
|
||||
Movies: Movies
|
||||
Sports: Sports
|
||||
Trending Tabs: Trending Tabs
|
||||
Most Popular: Most Popular
|
||||
Feed:
|
||||
|
||||
@@ -63,9 +63,6 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Popularaj'
|
||||
Gaming: Ludismo
|
||||
Movies: Filmoj
|
||||
Music: Muziko
|
||||
Default: Defaŭlte
|
||||
Most Popular: 'Plej popularaj'
|
||||
Playlists: 'Ludlistoj'
|
||||
User Playlists:
|
||||
|
||||
@@ -84,10 +84,7 @@ Subscriptions:
|
||||
Error Channels: Canales con errores
|
||||
Trending:
|
||||
Trending: 'Tendencias'
|
||||
Movies: Películas
|
||||
Gaming: Juegos
|
||||
Music: Música
|
||||
Default: Por defecto
|
||||
Trending Tabs: Lo más destacado
|
||||
Most Popular: 'Más popular'
|
||||
Playlists: 'Listas de reproducción'
|
||||
|
||||
@@ -102,11 +102,8 @@ Subscriptions:
|
||||
Load More Posts: Cargar más publicaciones
|
||||
Trending:
|
||||
Trending: 'Tendencias'
|
||||
Default: Predeterminado
|
||||
Music: Música
|
||||
Gaming: Juegos
|
||||
Trending Tabs: Tendencias
|
||||
Movies: Películas
|
||||
Most Popular: 'Más Popular'
|
||||
Playlists: 'Listas de Reproducción'
|
||||
User Playlists:
|
||||
|
||||
@@ -105,11 +105,8 @@ Subscriptions:
|
||||
Empty Posts: Tus canales suscritos no tienen actualmente ninguna publicación.
|
||||
Trending:
|
||||
Trending: 'Tendencias'
|
||||
Default: Predeterminado
|
||||
Trending Tabs: Pestañas de tendencias
|
||||
Movies: Películas
|
||||
Gaming: Videojuegos
|
||||
Music: Música
|
||||
Most Popular: 'Más populares'
|
||||
Playlists: 'Listas de reproducción'
|
||||
User Playlists:
|
||||
|
||||
@@ -110,10 +110,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Populaarsust koguvad videod'
|
||||
Trending Tabs: Populaarsust koguvad kaardid
|
||||
Movies: Filmid
|
||||
Gaming: Arvutimängud
|
||||
Music: Muusika
|
||||
Default: Vaikimisi
|
||||
Sports: Sport
|
||||
Most Popular: 'Populaarseimad'
|
||||
Playlists: 'Esitusloendid'
|
||||
User Playlists:
|
||||
|
||||
@@ -110,10 +110,7 @@ Subscriptions:
|
||||
More: 'Gehiago'
|
||||
Trending:
|
||||
Trending: 'Joerak'
|
||||
Default: Lehenetsia
|
||||
Music: Musika
|
||||
Gaming: Bideo jokoak
|
||||
Movies: Filmak
|
||||
Trending Tabs: Joeren fitxak
|
||||
Most Popular: 'Ikusienak'
|
||||
Playlists: 'Erreprodukzio zerrendak'
|
||||
|
||||
@@ -90,10 +90,7 @@ Subscriptions:
|
||||
More: 'بیشتر'
|
||||
Trending:
|
||||
Trending: 'پربازدید ها'
|
||||
Default: 'حالت عادی'
|
||||
Music: 'موسیقی'
|
||||
Gaming: 'بازی'
|
||||
Movies: 'فیلم ها'
|
||||
Trending Tabs: 'سربرگ پربازدید ها'
|
||||
Most Popular: 'پر طرفدارترین ها'
|
||||
Playlists: 'لیست های پخش'
|
||||
|
||||
@@ -102,10 +102,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Nousussa'
|
||||
Trending Tabs: Nousussa olevat välilehdet
|
||||
Movies: Elokuvat
|
||||
Gaming: Pelaaminen
|
||||
Music: Musiikki
|
||||
Default: Oletus
|
||||
Most Popular: 'Suosituimmat'
|
||||
Playlists: 'Soittolistat'
|
||||
User Playlists:
|
||||
|
||||
@@ -103,10 +103,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Tendance'
|
||||
Trending Tabs: Onglets des Tendances
|
||||
Movies: Films
|
||||
Gaming: Jeux vidéo
|
||||
Music: Musique
|
||||
Default: Par défaut
|
||||
Sports: Sports
|
||||
Most Popular: 'Les plus populaires'
|
||||
Playlists: 'Listes de lecture'
|
||||
User Playlists:
|
||||
|
||||
@@ -85,9 +85,6 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Tendencias'
|
||||
Gaming: Xogos
|
||||
Default: Por defecto
|
||||
Music: Música
|
||||
Movies: Películas
|
||||
Trending Tabs: Pestanas das tendencias
|
||||
Most Popular: 'Máis populares'
|
||||
Playlists: 'Listaxes de reprodución'
|
||||
|
||||
@@ -110,10 +110,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'הסרטונים החמים'
|
||||
Trending Tabs: לשוניות מובילים
|
||||
Movies: סרטים
|
||||
Gaming: משחקים
|
||||
Music: מוזיקה
|
||||
Default: ברירת מחדל
|
||||
Most Popular: 'הכי פופולרי'
|
||||
Playlists: 'רשימות נגינה'
|
||||
User Playlists:
|
||||
|
||||
@@ -96,9 +96,6 @@ Subscriptions:
|
||||
Empty Channels: आपके सब्सक्राइब किए गए चैनल में वर्तमान में कोई वीडियो नहीं है।
|
||||
Trending:
|
||||
Trending: 'रुझान में'
|
||||
Music: संगीत
|
||||
Default: डिफ़ॉल्ट
|
||||
Movies: मूवीज
|
||||
Trending Tabs: ट्रेंडिंग टैब्स
|
||||
Gaming: गेमिंग
|
||||
Most Popular: 'सबसे लोकप्रिय'
|
||||
|
||||
@@ -106,10 +106,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'U trendu'
|
||||
Trending Tabs: Kartice „U trendu”
|
||||
Movies: Filmovi
|
||||
Gaming: Igre
|
||||
Music: Glazba
|
||||
Default: Standardno
|
||||
Most Popular: 'Najpopularniji'
|
||||
Playlists: 'Zbirke'
|
||||
User Playlists:
|
||||
|
||||
@@ -110,10 +110,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Felkapott'
|
||||
Trending Tabs: Felkapott lapok
|
||||
Movies: Filmek
|
||||
Gaming: Játék
|
||||
Music: Zene
|
||||
Default: Alapértelmezett
|
||||
Sports: Sportok
|
||||
Most Popular: 'Legnépszerűbb'
|
||||
Playlists: 'Lejátszási listák'
|
||||
User Playlists:
|
||||
|
||||
@@ -109,10 +109,7 @@ Subscriptions:
|
||||
All Subscription Tabs Hidden: Semua tab langganan disembunyikan. Untuk melihat konten di sini, harap tampilkan beberapa tab di bagian "{subsection}" di "{settingsSection}".
|
||||
Trending:
|
||||
Trending: 'Sedang Tren'
|
||||
Movies: Film
|
||||
Music: Musik
|
||||
Gaming: Bermain game
|
||||
Default: Bawaan
|
||||
Trending Tabs: Tab Trending
|
||||
Most Popular: 'Paling Populer'
|
||||
Playlists: 'Daftar Putar'
|
||||
|
||||
@@ -111,10 +111,8 @@ More: 'Meira'
|
||||
Trending:
|
||||
Trending: 'Í umræðunni'
|
||||
Trending Tabs: Vinsælir flipar
|
||||
Movies: Kvikmyndir
|
||||
Gaming: Leikir
|
||||
Music: Tónlist
|
||||
Default: Sjálfgefið
|
||||
Sports: Íþróttir
|
||||
Most Popular: 'Vinsælast'
|
||||
Playlists: 'Spilunarlistar'
|
||||
User Playlists:
|
||||
@@ -287,6 +285,7 @@ Settings:
|
||||
Everforest Dark Low: Everforest Dökkt-lítið
|
||||
Everforest Light Hard: Everforest Ljóst-skarpt
|
||||
Everforest Light Medium: Everforest Ljóst-miðlungs
|
||||
Catppuccin Latte: Catppuccin Latte
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Aðallitur þema'
|
||||
Red: 'Rautt'
|
||||
@@ -372,6 +371,8 @@ Settings:
|
||||
Everforest Light Aqua: Everforest Ljóssægrænt
|
||||
Everforest Light Blue: Everforest Ljósblátt
|
||||
Everforest Light Purple: Everforest Ljóspurpuralitað
|
||||
Catppuccin Latte Mauve: Catppuccin Latte fjólublár
|
||||
Catppuccin Latte Red: Catppuccin Latte rautt
|
||||
Secondary Color Theme: 'Aukalitur þema'
|
||||
#* Main Color Theme
|
||||
Hide Side Bar Labels: Fela skýringar á hliðarstiku
|
||||
@@ -548,6 +549,12 @@ Settings:
|
||||
Export Playlists For Older FreeTube Versions:
|
||||
Label: Flytja út spilunarlista fyrir eldri útgáfur FreeTube
|
||||
Tooltip: "Þessi valkostur flytur út myndskeið úr öllum spilunarlistum inn í einn spilunarlista sem kallast 'Eftirlæti'.\nHér er skýrt hvernig eigi að flytja út eða inn myndskeið í spilunarlistum fyrir eldri útgáfur FreeTube:\n 1. Flyttu út spilunarlistana þína með þennan valkost virkann.\n2. Eyddu út öllum fyrirliggjandi spilunarlistum hjá þér með valkostinum 'Fjarlægja alla spilunarlista' í stillingum gagnaleyndar.\n3. Ræstu eldri útgáfu FreeTube og flyttu inn spilunarlistana sem þú fluttir út.\""
|
||||
Search history file: Leita í ferilskrá
|
||||
Search history: Leitarferill
|
||||
Import search history: Flytja inn leitarferil
|
||||
Export search history: Flytja út leitarferil
|
||||
All search history has been successfully imported: Allur leitarferill hefur verið fluttur inn
|
||||
All search history has been successfully exported: Allur leitarferill hefur verið fluttur út
|
||||
Proxy Settings:
|
||||
Proxy Settings: 'Milliþjónn (proxy)'
|
||||
Enable Tor / Proxy: 'Virkja Tor / milliþjón'
|
||||
@@ -925,10 +932,13 @@ Comments:
|
||||
Show More Replies: Birta fleiri svör
|
||||
Pinned by: Fest af
|
||||
Member: Meðlimur
|
||||
View {replyCount} replies: Skoða {replyCount} svör
|
||||
View {replyCount} replies: Skoða 1 svar | Skoða {replyCount} svör
|
||||
Hearted: Líkað
|
||||
Subscribed: Áskrifandi
|
||||
There are no comments available for this post: Engar athugasemdir finnast fyrir þessa færslu
|
||||
Hide {replyCount} replies: Fela 1 svar | Fela {replyCount} svör
|
||||
View 1 reply from {channelName}: Skoða 1 svar frá {channelName}
|
||||
View {replyCount} replies from {channelName} and others: Skoða {replyCount} svör frá {channelName} og fleirum
|
||||
Up Next: 'Næst í spilun'
|
||||
|
||||
#Tooltips
|
||||
|
||||
@@ -105,11 +105,9 @@ Subscriptions:
|
||||
Empty Posts: I canali a cui sei iscritto attualmente non hanno post.
|
||||
Trending:
|
||||
Trending: 'Tendenze'
|
||||
Music: Musica
|
||||
Default: Predefinito
|
||||
Trending Tabs: Schede di tendenza
|
||||
Movies: Film
|
||||
Gaming: Videogiochi
|
||||
Sports: Sport
|
||||
Most Popular: 'Più popolari'
|
||||
Playlists: 'Playlist'
|
||||
User Playlists:
|
||||
|
||||
@@ -103,10 +103,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: '急上昇'
|
||||
Trending Tabs: 急上昇のタブ
|
||||
Movies: 映画
|
||||
Gaming: ゲーム
|
||||
Music: 音楽
|
||||
Default: デフォルト
|
||||
Most Popular: '人気'
|
||||
Playlists: '再生リスト'
|
||||
User Playlists:
|
||||
|
||||
@@ -107,10 +107,7 @@ Channels:
|
||||
Unsubscribe Prompt: 'დარწმუნებული ხართ, რომ გსურთ "{channelName}"ის გამოწერის გაუქმება?'
|
||||
Trending:
|
||||
Trending: 'პოპულარული'
|
||||
Default: 'სტანდარტულად'
|
||||
Music: 'მუსიკები'
|
||||
Gaming: 'თამაშები'
|
||||
Movies: 'ფილმები'
|
||||
Trending Tabs: 'პოპულარული'
|
||||
Most Popular: 'ყველაზე პოპულარული'
|
||||
Playlists: 'დასაკრავი სიები'
|
||||
|
||||
@@ -109,10 +109,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: '트렌딩'
|
||||
Trending Tabs: 트렌딩 탭
|
||||
Default: 기본
|
||||
Movies: 영화
|
||||
Gaming: 게임
|
||||
Music: 음악
|
||||
Most Popular: '인기 동영상'
|
||||
Playlists: '재생 목록'
|
||||
User Playlists:
|
||||
|
||||
@@ -106,10 +106,7 @@ Subscriptions:
|
||||
Subscriptions Tabs: تابەکانی بەشداریکردن
|
||||
Trending:
|
||||
Trending: 'زۆر باسکراو'
|
||||
Default: بنەڕەتی
|
||||
Trending Tabs: تابەکانی ترێندنگ
|
||||
Movies: فیلمەکان
|
||||
Music: مووزیک
|
||||
Gaming: یاریکردن
|
||||
Most Popular: 'بەناوبانگترین'
|
||||
Playlists: 'پلەیلیست'
|
||||
|
||||
@@ -93,10 +93,7 @@ More: 'Daugiau'
|
||||
Trending:
|
||||
Trending: 'Dabar populiaru'
|
||||
Trending Tabs: „Dabar populiaru“ kortelės
|
||||
Default: Numatytoji
|
||||
Movies: Filmai
|
||||
Gaming: Žaidimai
|
||||
Music: Muzika
|
||||
Most Popular: 'Populiariausia'
|
||||
Playlists: 'Grojaraščiai'
|
||||
User Playlists:
|
||||
|
||||
@@ -124,10 +124,7 @@ Channels:
|
||||
Unsubscribe Prompt: 'Vai esi drošs, ka vēlies atcelt abonementu no "{channelName}"?'
|
||||
Trending:
|
||||
Trending: 'Pašlaik topā'
|
||||
Default: 'Viss'
|
||||
Music: 'Mūzika'
|
||||
Gaming: 'Spēles'
|
||||
Movies: 'Filmas'
|
||||
Trending Tabs: 'Tendenču cilnes'
|
||||
Most Popular: 'Vispopulārākie'
|
||||
Playlists: 'Atskaņošanas saraksti'
|
||||
|
||||
@@ -140,10 +140,7 @@ Channels:
|
||||
Unsubscribe Prompt: ''
|
||||
Trending:
|
||||
Trending: ''
|
||||
Default: ''
|
||||
Music: ''
|
||||
Gaming: ''
|
||||
Movies: ''
|
||||
Trending Tabs: ''
|
||||
Most Popular: ''
|
||||
Feed:
|
||||
|
||||
@@ -137,10 +137,7 @@ Channels:
|
||||
Unsubscribe Prompt: ''
|
||||
Trending:
|
||||
Trending: ''
|
||||
Default: ''
|
||||
Music: ''
|
||||
Gaming: ''
|
||||
Movies: ''
|
||||
Trending Tabs: ''
|
||||
Most Popular: ''
|
||||
Feed:
|
||||
|
||||
@@ -106,10 +106,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'På vei opp'
|
||||
Trending Tabs: På vei opp-faner
|
||||
Movies: Filmer
|
||||
Gaming: Videospill
|
||||
Music: Musikk
|
||||
Default: Standard
|
||||
Sports: Sport
|
||||
Most Popular: 'Mest populære'
|
||||
Playlists: 'Spillelister'
|
||||
User Playlists:
|
||||
|
||||
@@ -83,10 +83,7 @@ Subscriptions:
|
||||
More: 'थप'
|
||||
Trending:
|
||||
Trending: 'प्रचलित'
|
||||
Default: 'पूर्वनिर्धारित'
|
||||
Music: 'सङ्गीत'
|
||||
Gaming: 'गेमिङ्ग'
|
||||
Movies: 'चलचित्रहरू'
|
||||
Trending Tabs: 'प्रचलित ट्याबहरू'
|
||||
Most Popular: 'सबैभन्दा लोकप्रिय'
|
||||
Playlists: 'प्लेसूचीहरू'
|
||||
|
||||
@@ -106,10 +106,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Trending'
|
||||
Trending Tabs: Trending
|
||||
Movies: Films
|
||||
Gaming: Gaming
|
||||
Music: Muziek
|
||||
Default: Standaard
|
||||
Most Popular: 'Populairste'
|
||||
Playlists: 'Afspeellijsten'
|
||||
User Playlists:
|
||||
|
||||
@@ -88,11 +88,8 @@ Subscriptions:
|
||||
Empty Channels: Kanalane du abonnerer på har ingen videoar for augeblikket.
|
||||
Trending:
|
||||
Trending: 'På veg opp'
|
||||
Music: Musikk
|
||||
Trending Tabs: På veg opp
|
||||
Movies: Filmar
|
||||
Gaming: Dataspel
|
||||
Default: Standard
|
||||
Most Popular: 'Mest populært'
|
||||
Playlists: 'Spelelister'
|
||||
User Playlists:
|
||||
|
||||
@@ -41,10 +41,7 @@ Channels:
|
||||
Channels: 'ଚ୍ୟାନେଲ'
|
||||
Title: 'ଚ୍ୟାନେଲ ତାଲିକା'
|
||||
Trending:
|
||||
Default: 'ଡିଫଲ୍ଟ'
|
||||
Music: 'ସଙ୍ଗୀତ'
|
||||
Gaming: 'ଖେଳ'
|
||||
Movies: 'ଚଳଚ୍ଚିତ୍ର'
|
||||
Playlists: 'ଚାଳନାତାଲିକା'
|
||||
User Playlists:
|
||||
Your Playlists: 'ଆପଣଙ୍କ ଚାଳନାତାଲିକା'
|
||||
|
||||
@@ -103,10 +103,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Na czasie'
|
||||
Trending Tabs: Karty „Na czasie”
|
||||
Default: Domyślna
|
||||
Music: Muzyka
|
||||
Gaming: Gry
|
||||
Movies: Filmy
|
||||
Most Popular: 'Popularne'
|
||||
Playlists: 'Playlisty'
|
||||
User Playlists:
|
||||
|
||||
@@ -103,10 +103,8 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Em alta'
|
||||
Trending Tabs: Guias em destaque
|
||||
Movies: Filmes
|
||||
Gaming: Jogos
|
||||
Music: Música
|
||||
Default: Padrão
|
||||
Sports: Esportes
|
||||
Most Popular: 'Mais populares'
|
||||
Playlists: 'Playlists'
|
||||
User Playlists:
|
||||
|
||||
@@ -110,10 +110,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: Tendências
|
||||
Trending Tabs: Separadores de tendências
|
||||
Movies: Filmes
|
||||
Gaming: Jogos
|
||||
Music: Música
|
||||
Default: Padrão
|
||||
Most Popular: Mais populares
|
||||
Playlists: Listas de reprodução
|
||||
User Playlists:
|
||||
|
||||
@@ -110,10 +110,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Tendências'
|
||||
Trending Tabs: Separadores de tendências
|
||||
Movies: Filmes
|
||||
Gaming: Jogos
|
||||
Music: Músicas
|
||||
Default: Padrão
|
||||
Most Popular: 'Mais populares'
|
||||
Playlists: 'Listas de reprodução'
|
||||
User Playlists:
|
||||
|
||||
@@ -109,10 +109,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'Tendințe'
|
||||
Trending Tabs: File în tendințe
|
||||
Movies: Filme
|
||||
Gaming: Gaming
|
||||
Music: Muzică
|
||||
Default: Implicit
|
||||
Most Popular: 'Cele mai populare'
|
||||
Playlists: 'Liste de redare'
|
||||
User Playlists:
|
||||
|
||||
@@ -103,10 +103,7 @@ Subscriptions:
|
||||
Trending:
|
||||
Trending: 'В тренде'
|
||||
Trending Tabs: Вкладки В тренде
|
||||
Movies: Фильмы
|
||||
Gaming: Игры
|
||||
Music: Музыка
|
||||
Default: По умолчанию
|
||||
Most Popular: 'Самые популярные'
|
||||
Playlists: 'Плейлисты'
|
||||
User Playlists:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user