Feature/channels page (#2129)
* init * sync multiple windows * respect "preferred api" * prompt, update thumbnail * regexp fix * locale * hide search when empty
This commit is contained in:
parent
2340af09e2
commit
94030b6a8d
|
@ -32,6 +32,30 @@
|
|||
{{ $t("Subscriptions.Subscriptions") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="navOption mobileShow"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:title="$t('Channels.Channels')"
|
||||
@click="navigate('subscribedchannels')"
|
||||
>
|
||||
<div
|
||||
class="thumbnailContainer"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="list"
|
||||
class="navIcon"
|
||||
:class="applyNavIconExpand"
|
||||
fixed-width
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="!hideText"
|
||||
class="navLabel"
|
||||
>
|
||||
{{ $t("Channels.Channels") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hideTrendingVideos"
|
||||
class="navOption mobileHidden"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
||||
import SubscribedChannels from '../views/SubscribedChannels/SubscribedChannels.vue'
|
||||
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.vue'
|
||||
import ProfileEdit from '../views/ProfileEdit/ProfileEdit.vue'
|
||||
import Trending from '../views/Trending/Trending.vue'
|
||||
|
@ -66,6 +67,14 @@ const router = new CustomRouter({
|
|||
},
|
||||
component: Subscriptions
|
||||
},
|
||||
{
|
||||
path: '/subscribedchannels',
|
||||
meta: {
|
||||
title: 'Channels.Title',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: SubscribedChannels
|
||||
},
|
||||
{
|
||||
path: '/settings/profile',
|
||||
meta: {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
.card {
|
||||
width: 85%;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--tertiary-text-color);
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.channels {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr) );
|
||||
gap: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.channel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
row-gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.thumbnailContainer {
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.channelThumbnail {
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.channelName {
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.unsubscribeContainer {
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unsubscribeContainer .btn {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 680px) {
|
||||
.card {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.channels {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.channelThumbnail {
|
||||
height: 80px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import Vue from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import FtButton from '../../components/ft-button/ft-button.vue'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||
import FtInput from '../../components/ft-input/ft-input.vue'
|
||||
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
|
||||
import ytch from 'yt-channel-info'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SubscribedChannels',
|
||||
components: {
|
||||
'ft-button': FtButton,
|
||||
'ft-card': FtCard,
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-input': FtInput,
|
||||
'ft-prompt': FtPrompt
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
query: '',
|
||||
subscribedChannels: [],
|
||||
filteredChannels: [],
|
||||
re: {
|
||||
url: /(.+=\w{1})\d+(.+)/,
|
||||
ivToIv: /^.+(ggpht.+)/,
|
||||
ivToYt: /^.+ggpht\/(.+)/,
|
||||
ytToIv: /^.+ggpht\.com\/(.+)/
|
||||
},
|
||||
thumbnailSize: 176,
|
||||
ytBaseURL: 'https://yt3.ggpht.com',
|
||||
showUnsubscribePrompt: false,
|
||||
unsubscribePromptValues: [
|
||||
'yes',
|
||||
'no'
|
||||
],
|
||||
channelToUnsubscribe: null,
|
||||
errorCount: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeProfile: function () {
|
||||
return this.$store.getters.getActiveProfile
|
||||
},
|
||||
|
||||
activeProfileId: function () {
|
||||
return this.activeProfile._id
|
||||
},
|
||||
|
||||
activeSubscriptionList: function () {
|
||||
return this.activeProfile.subscriptions
|
||||
},
|
||||
|
||||
channelList: function () {
|
||||
if (this.query !== '') {
|
||||
return this.filteredChannels
|
||||
} else {
|
||||
return this.subscribedChannels
|
||||
}
|
||||
},
|
||||
|
||||
locale: function () {
|
||||
return this.$store.getters.getCurrentLocale.replace('_', '-')
|
||||
},
|
||||
|
||||
backendPreference: function () {
|
||||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
unsubscribePromptNames: function () {
|
||||
return [
|
||||
this.$t('Yes'),
|
||||
this.$t('No')
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeProfileId: function() {
|
||||
this.query = ''
|
||||
this.getSubscription()
|
||||
},
|
||||
|
||||
activeSubscriptionList: function() {
|
||||
this.getSubscription()
|
||||
this.filterChannels()
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.getSubscription()
|
||||
},
|
||||
methods: {
|
||||
getSubscription: function () {
|
||||
this.subscribedChannels = this.activeSubscriptionList.slice().sort((a, b) => {
|
||||
return a.name.localeCompare(b.name, this.locale)
|
||||
})
|
||||
},
|
||||
|
||||
handleInput: function(input) {
|
||||
this.query = input
|
||||
this.filterChannels()
|
||||
},
|
||||
|
||||
filterChannels: function () {
|
||||
if (this.query === '') {
|
||||
this.filteredChannels = []
|
||||
return
|
||||
}
|
||||
|
||||
const escapedQuery = this.query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const re = new RegExp(escapedQuery, 'i')
|
||||
this.filteredChannels = this.subscribedChannels.filter(channel => {
|
||||
return re.test(channel.name)
|
||||
})
|
||||
},
|
||||
|
||||
handleUnsubscribeButtonClick: function(channel) {
|
||||
this.channelToUnsubscribe = channel
|
||||
this.showUnsubscribePrompt = true
|
||||
},
|
||||
|
||||
handleUnsubscribePromptClick: function(value) {
|
||||
this.showUnsubscribePrompt = false
|
||||
if (value !== 'yes') {
|
||||
this.channelToUnsubscribe = null
|
||||
return
|
||||
}
|
||||
this.unsubscribeChannel()
|
||||
},
|
||||
|
||||
unsubscribeChannel: function () {
|
||||
const currentProfile = JSON.parse(JSON.stringify(this.activeProfile))
|
||||
let index = currentProfile.subscriptions.findIndex(channel => {
|
||||
return channel.id === this.channelToUnsubscribe.id
|
||||
})
|
||||
currentProfile.subscriptions.splice(index, 1)
|
||||
|
||||
this.updateProfile(currentProfile)
|
||||
this.showToast({
|
||||
message: this.$t('Channels.Unsubscribed').replace('$', this.channelToUnsubscribe.name)
|
||||
})
|
||||
|
||||
index = this.subscribedChannels.findIndex(channel => {
|
||||
return channel.id === this.channelToUnsubscribe.id
|
||||
})
|
||||
this.subscribedChannels.splice(index, 1)
|
||||
|
||||
index = this.filteredChannels.findIndex(channel => {
|
||||
return channel.id === this.channelToUnsubscribe.id
|
||||
})
|
||||
if (index !== -1) {
|
||||
this.filteredChannels.splice(index, 1)
|
||||
}
|
||||
|
||||
this.channelToUnsubscribe = null
|
||||
},
|
||||
|
||||
thumbnailURL: function(originalURL) {
|
||||
let newURL = originalURL
|
||||
if (originalURL.indexOf('ggpht.com') > -1) {
|
||||
if (this.backendPreference === 'invidious') { // YT to IV
|
||||
newURL = originalURL.replace(this.re.ytToIv, `${this.currentInvidiousInstance}/ggpht/$1`)
|
||||
}
|
||||
} else {
|
||||
if (this.backendPreference === 'local') { // IV to YT
|
||||
newURL = originalURL.replace(this.re.ivToYt, `${this.ytBaseURL}/$1`)
|
||||
} else { // IV to IV
|
||||
newURL = originalURL.replace(this.re.ivToIv, `${this.currentInvidiousInstance}/$1`)
|
||||
}
|
||||
}
|
||||
|
||||
return newURL.replace(this.re.url, `$1${this.thumbnailSize}$2`)
|
||||
},
|
||||
|
||||
updateThumbnail: function(channel) {
|
||||
this.errorCount += 1
|
||||
if (this.backendPreference === 'local') {
|
||||
// avoid too many concurrent requests
|
||||
setTimeout(() => {
|
||||
ytch.getChannelInfo({ channelId: channel.id }).then(response => {
|
||||
this.updateSubscriptionDetails({
|
||||
channelThumbnailUrl: this.thumbnailURL(response.authorThumbnails[0].url),
|
||||
channelName: channel.name,
|
||||
channelId: channel.id
|
||||
})
|
||||
})
|
||||
}, this.errorCount * 500)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.invidiousGetChannelInfo(channel.id).then(response => {
|
||||
this.updateSubscriptionDetails({
|
||||
channelThumbnailUrl: this.thumbnailURL(response.authorThumbnails[0].url),
|
||||
channelName: channel.name,
|
||||
channelId: channel.id
|
||||
})
|
||||
})
|
||||
}, this.errorCount * 500)
|
||||
}
|
||||
},
|
||||
|
||||
goToChannel: function (id) {
|
||||
this.$router.push({ path: `/channel/${id}` })
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'showToast',
|
||||
'updateProfile',
|
||||
'updateSubscriptionDetails',
|
||||
'invidiousGetChannelInfo'
|
||||
])
|
||||
}
|
||||
})
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div>
|
||||
<ft-card class="card">
|
||||
<h3>{{ $t('Channels.Title') }}</h3>
|
||||
<ft-input
|
||||
v-show="subscribedChannels.length > 0"
|
||||
ref="searchBarChannels"
|
||||
:placeholder="$t('Channels.Search bar placeholder')"
|
||||
:show-clear-text-button="true"
|
||||
:show-action-button="false"
|
||||
:spellcheck="false"
|
||||
@input="handleInput"
|
||||
@clear="query = ''"
|
||||
/>
|
||||
<ft-flex-box
|
||||
v-if="activeSubscriptionList.length === 0"
|
||||
>
|
||||
<p class="message">
|
||||
{{ $t('Channels.Empty') }}
|
||||
</p>
|
||||
</ft-flex-box>
|
||||
<template v-else>
|
||||
<ft-flex-box class="count">
|
||||
{{ $t('Channels.Count').replace('$', channelList.length) }}
|
||||
</ft-flex-box>
|
||||
<ft-flex-box class="channels">
|
||||
<div
|
||||
v-for="channel in channelList"
|
||||
:key="channel.key"
|
||||
class="channel"
|
||||
>
|
||||
<div class="thumbnailContainer">
|
||||
<img
|
||||
class="channelThumbnail"
|
||||
:src="thumbnailURL(channel.thumbnail)"
|
||||
@click="goToChannel(channel.id)"
|
||||
@error.once="updateThumbnail(channel)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="channelName"
|
||||
:title="channel.name"
|
||||
@click="goToChannel(channel.id)"
|
||||
>
|
||||
{{ channel.name }}
|
||||
</div>
|
||||
<div class="unsubscribeContainer">
|
||||
<ft-button
|
||||
:label="$t('Channels.Unsubscribe')"
|
||||
background-color="var(--search-bar-color)"
|
||||
text-color="var(--secondary-text-color)"
|
||||
@click="handleUnsubscribeButtonClick(channel)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ft-flex-box>
|
||||
</template>
|
||||
</ft-card>
|
||||
<ft-prompt
|
||||
v-if="showUnsubscribePrompt"
|
||||
:label="$t('Channels.Unsubscribe Prompt').replace('$', channelToUnsubscribe.name)"
|
||||
:option-names="unsubscribePromptNames"
|
||||
:option-values="unsubscribePromptValues"
|
||||
@click="handleUnsubscribePromptClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./SubscribedChannels.js" />
|
||||
<style scoped src="./SubscribedChannels.css" />
|
|
@ -90,6 +90,15 @@ Subscriptions:
|
|||
Refresh Subscriptions: Refresh Subscriptions
|
||||
Load More Videos: Load More Videos
|
||||
More: More
|
||||
Channels:
|
||||
Channels: Channels
|
||||
Title: Channel List
|
||||
Search bar placeholder: Search Channels
|
||||
Count: $ channel(s) found.
|
||||
Empty: Your channel list is currently empty.
|
||||
Unsubscribe: Unsubscribe
|
||||
Unsubscribed: $ has been removed from your subscriptions
|
||||
Unsubscribe Prompt: Are you sure you want to unsubscribe from "$"?
|
||||
Trending:
|
||||
Trending: Trending
|
||||
Default: Default
|
||||
|
|
Loading…
Reference in New Issue