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:
bob1520 2022-07-08 12:40:10 +09:00 committed by GitHub
parent 2340af09e2
commit 94030b6a8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 406 additions and 0 deletions

View File

@ -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"

View File

@ -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: {

View File

@ -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;
}
}

View File

@ -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'
])
}
})

View File

@ -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" />

View File

@ -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