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") }}
|
{{ $t("Subscriptions.Subscriptions") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="!hideTrendingVideos"
|
v-if="!hideTrendingVideos"
|
||||||
class="navOption mobileHidden"
|
class="navOption mobileHidden"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
||||||
|
import SubscribedChannels from '../views/SubscribedChannels/SubscribedChannels.vue'
|
||||||
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.vue'
|
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.vue'
|
||||||
import ProfileEdit from '../views/ProfileEdit/ProfileEdit.vue'
|
import ProfileEdit from '../views/ProfileEdit/ProfileEdit.vue'
|
||||||
import Trending from '../views/Trending/Trending.vue'
|
import Trending from '../views/Trending/Trending.vue'
|
||||||
|
@ -66,6 +67,14 @@ const router = new CustomRouter({
|
||||||
},
|
},
|
||||||
component: Subscriptions
|
component: Subscriptions
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/subscribedchannels',
|
||||||
|
meta: {
|
||||||
|
title: 'Channels.Title',
|
||||||
|
icon: 'fa-home'
|
||||||
|
},
|
||||||
|
component: SubscribedChannels
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/profile',
|
path: '/settings/profile',
|
||||||
meta: {
|
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
|
Refresh Subscriptions: Refresh Subscriptions
|
||||||
Load More Videos: Load More Videos
|
Load More Videos: Load More Videos
|
||||||
More: More
|
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: Trending
|
Trending: Trending
|
||||||
Default: Default
|
Default: Default
|
||||||
|
|
Loading…
Reference in New Issue