Channel community page (#1568)

* Comunity page strings, Communtiy tab, Community initial API call
Added:
1) Community page strings - the first few strings are now available
2) Community tab - A clickable tab is now displayed on channel pages
3) Community initial API call - on loading the page, the initial access

* Comunity page strings, Communtiy tab, Community initial API call
Added:
1) Community page strings - the first few strings are now available
2) Community tab - A clickable tab is now displayed on channel pages
3) Community initial API call - on loading the page, the initial access

* Data returning added

* Comunity page strings, Communtiy tab, Community initial API call
Added:
1) Community page strings - the first few strings are now available
2) Community tab - A clickable tab is now displayed on channel pages
3) Community initial API call - on loading the page, the initial access

* Data returning added

* Images are now displayed in the community tab

* Comunity page strings, Communtiy tab, Community initial API call
Added:
1) Community page strings - the first few strings are now available
2) Community tab - A clickable tab is now displayed on channel pages
3) Community initial API call - on loading the page, the initial access

* Data returning added

* Images are now displayed in the community tab

* Added primitive video display

* Current changes

* Added preston's change with the ftcard and started on some layout basics

* Created Community Post Component and added fetch more button + functionality

* Fixed problem with videothumbnails not loading and adjusted their height to 100% in the ft-list sass file

* Added poll and ft-list-video to the community page

* Added author name placeholder (missing in module), the published date, the likes and dislikes as well as comment counts to posts. Additionally scaling of images was added

* Added basis for community page playlists

* Finalized a setup for playlists when wide enough

* Fix for missing key in custom list

* Added publish date translation

* Add empty alt tags

Co-authored-by: Jason <84899178+jasonhenriquez@users.noreply.github.com>

* fix accessibility issue

Co-authored-by: Jason <84899178+jasonhenriquez@users.noreply.github.com>

* change: ununique ids to classes

* add missing alt tag

* Redirect channel/id/community to the channel's community tab

* update yt-channel-info

* update to 3.0.1

* Update yarn.lock

* add basic multiImage support

* use tiny-slider for multiImage community posts

* update getChannelCommunityPostsMore

* Update yarn.lock

* fix yarn lock

* swap community and about tab

* Update yarn.lock

* Fix missing comma

* Removed trailing spaces

* Clearing all community post data when changing to another channel

* Restructuring of how the post cards are added, Empty page text,
ft-element-list props customization
1) Now the community page uses the same setup of ft-element-list as the
other pages on the channel.
2) If no posts are available, now it displays a message saying so
3) The ft-element-list component's display style can now be forced into
a certain display mode (list/grid) with the new prop. It will overwrite
the corresponding default value for list display

* Fixed display text path

* Fix lint"

* Adjusted css to fit to new layout

* Final touches community page to tidy up the console

* fix icons, fix linter

* fix hiding showmore button for community page

* fix showToast calls

* change all this.showToast to showToaast

* reinstall tinyslider

* use helpers

* small fixes

* fix: getting continuation of community posts

* remove unused code

* improve slider style import

* fix hiding 'ShowMore' button

* fix weird typo in css

* add invidous community tab support

* remove console testing code

* Apply suggestions from code review

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* implement suggestions, improve thumbnail replacement

* use flip horizontal

* readd invidious fallback code, remove author name workaround

* replace another google domain when using invidious

* suppport invidious multiImage posts

* Use youtube.js for community posts

* add invidious polls, remove support for fetching more

* reorder icons alpabetically

* re-allow loading more when using localapi

* fix styling of multiImage, hide NA text

* fix loading playlist

* fix spacing of items

* fix issue with direct url to community tab

* make review recommendations

Co-Authored-By: absidue <48293849+absidue@users.noreply.github.com>

* fix displaying selected tab, get best quality image

---------

Co-authored-by: Preston <freetubeapp@protonmail.com>
Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
Co-authored-by: Jason <84899178+jasonhenriquez@users.noreply.github.com>
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
This commit is contained in:
Luca Hohmann 2023-03-04 09:56:04 +01:00 committed by GitHub
parent 1b69215855
commit 4ef2f709ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 782 additions and 34 deletions

View File

@ -74,8 +74,9 @@
"vue-i18n": "^8.28.2",
"vue-observe-visibility": "^1.0.0",
"vue-router": "^3.6.5",
"vue-tiny-slider": "^0.1.39",
"vuex": "^3.6.2",
"youtubei.js": "^3.1.0"
"youtubei.js": "^3.1.1"
},
"devDependencies": {
"@babel/core": "^7.21.0",

View File

@ -0,0 +1,127 @@
import Vue from 'vue'
import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
import autolinker from 'autolinker'
import VueTinySlider from 'vue-tiny-slider'
import {
toLocalePublicationString
} from '../../helpers/utils'
import { youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
import 'tiny-slider/dist/tiny-slider.css'
export default Vue.extend({
name: 'FtCommunityPost',
components: {
'ft-list-playlist': FtListPlaylist,
'ft-list-video': FtListVideo,
'tiny-slider': VueTinySlider
},
props: {
data: {
type: Object,
required: true
},
playlistId: {
type: String,
default: null
},
forceListType: {
type: String,
default: null
},
appearance: {
type: String,
required: true
}
},
data: function () {
return {
postText: '',
postId: '',
authorThumbnails: null,
publishedText: '',
voteCount: '',
postContent: '',
commentCount: '',
isLoading: true,
author: ''
}
},
computed: {
tinySliderOptions: function() {
return {
items: 1,
arrowKeys: false,
controls: false,
autoplay: false,
slideBy: 'page',
navPosition: 'bottom'
}
},
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
this.parseVideoData()
},
methods: {
parseVideoData: function () {
if ('backstagePostThreadRenderer' in this.data) {
this.postText = 'Shared post'
this.type = 'text'
let authorThumbnails = ['', 'https://yt3.ggpht.com/ytc/AAUvwnjm-0qglHJkAHqLFsCQQO97G7cCNDuDLldsrn25Lg=s88-c-k-c0x00ffffff-no-rj']
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
authorThumbnails = authorThumbnails.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
})
}
this.authorThumbnails = authorThumbnails
return
}
this.postText = autolinker.link(this.data.postText)
let authorThumbnails = this.data.authorThumbnails
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
authorThumbnails = authorThumbnails.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
})
} else {
authorThumbnails = authorThumbnails.map(thumbnail => {
if (thumbnail.url.startsWith('//')) {
thumbnail.url = 'https:' + thumbnail.url
}
return thumbnail
})
}
this.authorThumbnails = authorThumbnails
this.postContent = this.data.postContent
this.postId = this.data.postId
this.publishedText = toLocalePublicationString({
publishText: this.data.publishedText,
isLive: this.isLive,
isUpcoming: this.isUpcoming,
isRSS: this.data.isRSS
})
this.voteCount = this.data.voteCount
this.commentCount = this.data.commentCount
this.type = (this.data.postContent !== null && this.data.postContent !== undefined) ? this.data.postContent.type : 'text'
this.author = this.data.author
this.isLoading = false
},
getBestQualityImage(imageArray) {
const imageArrayCopy = Array.from(imageArray)
imageArrayCopy.sort((a, b) => {
return Number.parseInt(b.width) - Number.parseInt(a.width)
})
return imageArrayCopy.at(0)?.url ?? ''
}
}
})

View File

@ -0,0 +1,149 @@
/* stylelint-disable property-no-vendor-prefix */
@use '../../scss-partials/_ft-list-item';
.outside {
margin: auto;
width: 40%;
}
.circle {
background-color: transparent;
border-radius: 50%;
border-style: solid;
border-width: 2px;
display: block;
float: left;
height: 10px;
left: 5px;
position: relative;
top: 8px;
width: 10px;
}
.poll-text {
border-radius: 5px;
border-style: solid;
border-width: 2px;
padding: 5px 25px;
}
.poll-option {
padding-bottom: 10px;
}
.communityImage {
height: 100%;
width: 100%;
}
.communityThumbnail {
-webkit-border-radius: 50%;
border-radius: 50%;
height: 55px;
margin-right: 5px;
width: 55px;
}
.author-div {
display: flex;
.authorName {
font-size: 15px;
font-weight: bold;
margin: 5px 6px 0 5px;
}
.publishedText {
font-size: 15px;
margin: 5px 6px 0 5px;
}
}
.bottomSection {
color: var(--tertiary-text-color);
display: block;
flex-direction: column;
font-size: 15px;
margin-top: 4px;
max-width: 210px;
text-align: left;
@media screen and (max-width: 680px) {
margin-left: 0;
text-align: left;
}
.likeBar {
border-radius: 4px;
height: 8px;
margin-bottom: 4px;
}
.likeCount {
margin-left: 5px;
margin-right: 6px;
}
.dislikeCount {
margin-right: 10px;
}
}
.playlistWrapper {
display: flex;
.videoThumbnail {
display: flex;
margin-bottom: auto;
margin-top: auto;
position: relative;
width: fit-content;
.thumbnailImage {
display: block;
height: auto;
max-width: 100%;
width: auto;
}
}
.playlistText {
margin-left: 10px;
width: 50%;
word-wrap: break-word;
.playlistAuthor {
font-size: small;
.playlistVideoCount {
font-size: smaller;
}
}
.playlistTitle {
color: var(--primary-text-color);
}
.playlistPreviewVideos {
color: var(--primary-text-color);
display: flex;
font-size: small;
padding-top: 10px;
text-decoration-line: none;
width: 100%;
}
.playlistPreviewVideoTitle {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
}
.ft-list-item.grid {
min-height: 0 !important;
padding-bottom: 20px;
}

View File

@ -0,0 +1,123 @@
<template>
<div
v-if="!isLoading"
class="ft-list-post ft-list-item outside"
:appearance="appearance"
:class="{ list: listType === 'list', grid: listType === 'grid' }"
>
<div
class="author-div"
>
<img
v-if="authorThumbnails.length > 0"
:src="getBestQualityImage(authorThumbnails)"
class="communityThumbnail"
alt=""
>
<p
class="authorName"
>
{{ author }}
</p>
<p
class="publishedText"
>
{{ publishedText }}
</p>
</div>
<p v-html="postText" />
<tiny-slider
v-if="type === 'multiImage' && postContent.content.length > 0"
v-bind="tinySliderOptions"
class="slider"
>
<img
v-for="(img, index) in postContent.content"
:key="index"
:src="getBestQualityImage(img)"
class="communityImage tns-lazy-img"
alt=""
>
</tiny-slider>
<div
v-if="type === 'image' && postContent.content.length > 0"
>
<img
:src="getBestQualityImage(postContent.content)"
class="communityImage"
alt=""
>
</div>
<div
v-if="type === 'video'"
>
<ft-list-video
:data="data.postContent.content"
appearance=""
/>
</div>
<div
v-if="type === 'poll'"
>
<div
class="poll-count"
>
{{ postContent.totalVotes }}
</div>
<div
v-for="(poll, index) in postContent.content"
:key="index"
>
<div
class="poll-option"
>
<span
class="circle"
/>
<div
class="poll-text"
>
<!-- <img
v-if="poll.image != null && poll.image.length >0"
:src="getBestQualityImage(poll.image)"
class="poll-image"
alt=""
> -->
{{ poll.text }}
</div>
</div>
</div>
</div>
<div
v-if="type === 'playlist'"
class="playlistWrapper"
>
<ft-list-playlist
:data="postContent.content"
:appearance="appearance"
/>
</div>
<div
class="bottomSection"
>
<span class="likeCount"><font-awesome-icon
class="thumbs-up-icon"
:icon="['fas', 'thumbs-up']"
/> {{ voteCount }}</span>
<span class="dislikeCount"><font-awesome-icon
class="thumbs-down-icon"
:icon="['fas', 'thumbs-down']"
flip="horizontal"
/></span>
<span class="commentCount">
<font-awesome-icon
class="comment-count-icon"
:icon="['fas', 'comment']"
/> {{ commentCount }}</span>
</div>
</div>
</template>
<script src="./ft-community-post.js" />
<style src="./ft-community-post.scss" lang="scss" />
<style src="./slider-style.css" lang="css" />

View File

@ -0,0 +1,11 @@
.tns-nav .tns-nav-active {
background-color: #999;
}
.tns-nav button {
background-color: #ddd;
border-radius: 50%;
height: 1.5em;
padding: 0;
width: 1.5em;
}

View File

@ -13,14 +13,29 @@ export default defineComponent({
type: Array,
required: true
},
display: {
type: String,
required: false,
default: ''
},
showVideoWithLastViewedPlaylist: {
type: Boolean,
default: false
},
}
},
data: function () {
return {
displayValue: this.display
}
},
computed: {
listType: function () {
return this.$store.getters.getListType
}
},
mounted: function () {
if (this.display === '') {
this.displayValue = this.listType
}
}
})

View File

@ -1,6 +1,6 @@
<template>
<ft-auto-grid
:grid="listType !== 'list'"
:grid="displayValue !== 'list'"
>
<ft-list-lazy-wrapper
v-for="(result, index) in data"
@ -8,7 +8,7 @@
appearance="result"
:data="result"
:first-screen="index < 16"
:layout="listType"
:layout="displayValue"
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
/>
</ft-auto-grid>

View File

@ -2,13 +2,15 @@ import { defineComponent } from 'vue'
import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListChannel from '../ft-list-channel/ft-list-channel.vue'
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
import FtCommunityPost from '../ft-community-post/ft-community-post.vue'
export default defineComponent({
name: 'FtListLazyWrapper',
components: {
'ft-list-video': FtListVideo,
'ft-list-channel': FtListChannel,
'ft-list-playlist': FtListPlaylist
'ft-list-playlist': FtListPlaylist,
'ft-community-post': FtCommunityPost
},
props: {
data: {

View File

@ -16,13 +16,18 @@
:data="data"
/>
<ft-list-video
v-if="(data.type === 'video' || data.type === 'shortVideo') && visible"
v-else-if="(data.type === 'video' || data.type === 'shortVideo') && visible"
:appearance="appearance"
:data="data"
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
/>
<ft-list-playlist
v-if="data.type === 'playlist' && visible"
v-else-if="data.type === 'playlist' && visible"
:appearance="appearance"
:data="data"
/>
<ft-community-post
v-else-if="data.type === 'community' && visible"
:appearance="appearance"
:data="data"
/>

View File

@ -12,6 +12,7 @@
:to="`/playlist/${playlistId}`"
>
<img
alt=""
:src="thumbnail"
class="thumbnailImage"
>

View File

@ -1,15 +1,14 @@
import store from '../../store/index'
import { stripHTML, toLocalePublicationString } from '../utils'
import { isNullOrEmpty, stripHTML, toLocalePublicationString } from '../utils'
import autolinker from 'autolinker'
function getCurrentInstance() {
return store.getters.getCurrentInvidiousInstance
}
export function invidiousAPICall({ resource, id = '', params = {}, doLogError = true }) {
export function invidiousAPICall({ resource, id = '', params = {}, doLogError = true, subResource = '' }) {
return new Promise((resolve, reject) => {
const requestUrl = getCurrentInstance() + '/api/v1/' + resource + '/' + id + '?' + new URLSearchParams(params).toString()
const requestUrl = getCurrentInstance() + '/api/v1/' + resource + '/' + id + (!isNullOrEmpty(subResource) ? `/${subResource}` : '') + '?' + new URLSearchParams(params).toString()
fetch(requestUrl)
.then((response) => response.json())
.then((json) => {
@ -109,8 +108,10 @@ export function youtubeImageUrlToInvidious(url, currentInstance = null) {
if (url.startsWith('//')) {
url = 'https:' + url
}
return url.replace('https://yt3.ggpht.com', `${currentInstance}/ggpht`)
const newUrl = `${currentInstance}/ggpht`
return url.replace('https://yt3.ggpht.com', newUrl)
.replace('https://yt3.googleusercontent.com', newUrl)
.replace(/https:\/\/i\d*\.ytimg\.com/, newUrl)
}
export function invidiousImageUrlToInvidious(url, currentInstance = null) {
@ -137,3 +138,91 @@ function parseInvidiousCommentData(response) {
return comment
})
}
export async function invidiousGetCommunityPosts(channelId) {
const payload = {
resource: 'channels',
id: channelId,
subResource: 'community'
}
const response = await invidiousAPICall(payload)
response.comments = response.comments.map(communityPost => parseInvidiousCommunityData(communityPost))
return response.comments
}
function parseInvidiousCommunityData(data) {
return {
postText: data.contentHtml,
postId: data.commentId,
authorThumbnails: data.authorThumbnails.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
}),
publishedText: data.publishedText,
voteCount: data.likeCount,
postContent: parseInvidiousCommunityAttachments(data.attachment),
commentCount: data?.replyCount ?? 0, // https://github.com/iv-org/invidious/pull/3635/
author: data.author,
type: 'community'
}
}
function parseInvidiousCommunityAttachments(data) {
if (!data) {
return null
}
if (data.type === 'image') {
return {
type: data.type,
content: data.imageThumbnails.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
})
}
}
if (data.type === 'video') {
data.videoThumbnails = data.videoThumbnails.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
})
return {
type: data.type,
content: data
}
}
if (data.type === 'multiImage') {
const content = data.images.map(imageThumbnails => {
return imageThumbnails.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
})
})
return {
type: 'multiImage',
content: content
}
}
// https://github.com/iv-org/invidious/pull/3635/files
if (data.type === 'poll') {
return {
type: 'poll',
totalVotes: data.totalVotes ?? 0,
content: data.choices.map(choice => {
return {
text: choice.text,
image: choice.image.map(thumbnail => {
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
return thumbnail
})
}
})
}
}
console.error('New Invidious Community Post Type: ' + data.type)
}

View File

@ -655,3 +655,68 @@ export function parseLocalSubscriberCount(text) {
return subscribers
}
/**
* Parse community posts
* @param {import('youtubei.js/dist/src/parser/classes/BackstagePost').default} post
*/
export function parseLocalCommunityPost(post) {
let replyCount = post.action_buttons.reply_button?.text ?? null
if (replyCount !== null) {
replyCount = parseLocalSubscriberCount(post?.action_buttons.reply_button.text)
}
return {
postText: post.content.text === 'N/A' ? '' : post.content.text,
postId: post.id,
authorThumbnails: post.author.thumbnails,
publishedText: post.published.text,
voteCount: post.vote_count,
postContent: parseLocalAttachment(post.attachment),
commentCount: replyCount,
author: post.author.name,
type: 'community'
}
}
function parseLocalAttachment(attachment) {
if (!attachment) {
return null
}
// image post
if (attachment.type === 'BackstageImage') {
return {
type: 'image',
content: attachment.image
}
} else if (attachment.type === 'Video') {
return {
type: 'video',
content: parseLocalListVideo(attachment)
}
} else if (attachment.type === 'Playlist') {
return {
type: 'playlist',
content: parseLocalListPlaylist(attachment)
}
} else if (attachment.type === 'PostMultiImage') {
return {
type: 'multiImage',
content: attachment.images.map(thumbnail => thumbnail.image)
}
} else if (attachment.type === 'Poll') {
return {
type: 'poll',
totalVotes: attachment.total_votes ?? 0,
content: attachment.choices.map(choice => {
return {
text: choice.text.text,
image: choice.image
}
})
}
} else {
console.error(attachment)
console.error('unknown type')
}
}

View File

@ -620,6 +620,15 @@ export function formatNumber(number, options = undefined) {
return Intl.NumberFormat([i18n.locale.replace('_', '-'), 'en'], options).format(number)
}
/**
* This will return true if a string is null, undefined or empty.
* @param {string} _string the string to process
* @returns {bool} whether the string is empty or not
*/
export function isNullOrEmpty(_string) {
return _string == null || _string === ''
}
export function getTodayDateStrLocalTimezone() {
const timeNow = new Date()
// `Date#getTimezoneOffset` returns the difference, in minutes

View File

@ -16,6 +16,7 @@ import {
faChevronRight,
faCircleUser,
faClone,
faComment,
faCommentDots,
faCopy,
faDownload,
@ -50,6 +51,7 @@ import {
faStepBackward,
faStepForward,
faSync,
faThumbsDown,
faThumbsUp,
faThumbtack,
faTimes,
@ -80,6 +82,7 @@ library.add(
faChevronRight,
faCircleUser,
faClone,
faComment,
faCommentDots,
faCopy,
faDownload,
@ -114,6 +117,7 @@ library.add(
faStepBackward,
faStepForward,
faSync,
faThumbsDown,
faThumbsUp,
faThumbtack,
faTimes,

View File

@ -429,6 +429,8 @@ const actions = {
subPath = 'about'
break
case 'community':
subPath = 'community'
break
default:
subPath = 'videos'
break

View File

@ -121,8 +121,7 @@
border-bottom: 3px solid var(--tertiary-text-color);
}
.selectedTab,
.selectedTab:hover {
.selectedTab {
color: var(--primary-text-color);
border-bottom: 3px solid var(--primary-color);
font-weight: bold;
@ -212,6 +211,25 @@
margin: 0;
}
.communityThumbnail {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-border-radius: 200px;
border-radius: 200px;
height: 12%;
width: 12%;
}
.ft-community-image {
display: block;
margin-left: auto;
margin-right: auto;
}
.community-post-container {
padding-left: 30%;
padding-right: 30%;
}
@media only screen and (max-width: 800px) {
.channelInfoTabs {
height: auto;

View File

@ -13,18 +13,20 @@ import FtShareButton from '../../components/ft-share-button/ft-share-button.vue'
import autolinker from 'autolinker'
import { MAIN_PROFILE_ID } from '../../../constants'
import { copyToClipboard, extractNumberFromString, formatNumber, showToast } from '../../helpers/utils'
import { copyToClipboard, extractNumberFromString, formatNumber, isNullOrEmpty, showToast } from '../../helpers/utils'
import packageDetails from '../../../../package.json'
import {
invidiousAPICall,
invidiousGetChannelId,
invidiousGetChannelInfo,
invidiousGetCommunityPosts,
youtubeImageUrlToInvidious
} from '../../helpers/api/invidious'
import {
getLocalChannel,
getLocalChannelId,
parseLocalChannelVideos,
parseLocalCommunityPost,
parseLocalListPlaylist,
parseLocalListVideo,
parseLocalSubscriberCount
@ -59,6 +61,7 @@ export default defineComponent({
videoContinuationData: null,
playlistContinuationData: null,
searchContinuationData: null,
communityContinuationData: null,
description: '',
tags: [],
views: 0,
@ -70,6 +73,7 @@ export default defineComponent({
relatedChannels: [],
latestVideos: [],
latestPlaylists: [],
latestCommunityPosts: [],
searchResults: [],
shownElementList: [],
apiUsed: '',
@ -88,6 +92,7 @@ export default defineComponent({
tabInfoValues: [
'videos',
'playlists',
'community',
'about'
]
}
@ -179,20 +184,13 @@ export default defineComponent({
showFetchMoreButton: function () {
switch (this.currentTab) {
case 'videos':
if (this.videoContinuationData !== null) {
return true
}
break
return !isNullOrEmpty(this.videoContinuationData)
case 'playlists':
if (this.playlistContinuationData !== null) {
return true
}
break
return !isNullOrEmpty(this.playlistContinuationData)
case 'community':
return !isNullOrEmpty(this.communityContinuationData)
case 'search':
if (this.searchContinuationData !== null) {
return true
}
break
return !isNullOrEmpty(this.searchContinuationData)
}
return false
@ -236,6 +234,7 @@ export default defineComponent({
this.videoContinuationData = null
this.playlistContinuationData = null
this.searchContinuationData = null
this.communityContinuationData = null
this.showSearchBar = true
if (this.id === '@@@') {
@ -509,6 +508,10 @@ export default defineComponent({
this.getChannelPlaylistsLocal()
}
if (channel.has_community) {
this.getCommunityPostsLocal()
}
this.showSearchBar = channel.has_search
this.isLoading = false
@ -671,6 +674,10 @@ export default defineComponent({
this.getPlaylistsInvidious()
}
if (response.tabs.includes('community')) {
this.getCommunityPostsInvidious()
}
this.isLoading = false
}).catch((err) => {
this.setErrorMessage(err)
@ -866,6 +873,68 @@ export default defineComponent({
})
},
getCommunityPostsLocal: async function () {
const expectedId = this.id
try {
/**
* @type {import('youtubei.js/dist/src/parser/youtube/Channel').default}
*/
const channel = this.channelInstance
const communityTab = await channel.getCommunity()
if (expectedId !== this.id) {
return
}
this.latestCommunityPosts = communityTab.posts.map(parseLocalCommunityPost)
this.communityContinuationData = communityTab.has_continuation ? communityTab : null
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (this.backendPreference === 'local' && this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
this.getCommunityPostsInvidious()
} else {
this.isLoading = false
}
}
},
getCommunityPostsLocalMore: async function () {
try {
/**
* @type {import('youtubei.js/dist/src/parser/youtube/Channel').ChannelListContinuation}
*/
const continuation = await this.communityContinuationData.getContinuation()
this.latestCommunityPosts = this.latestCommunityPosts.concat(continuation.posts.map(parseLocalCommunityPost))
this.communityContinuationData = continuation.has_continuation ? continuation : null
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
}
},
getCommunityPostsInvidious: function() {
invidiousGetCommunityPosts(this.id).then(posts => {
this.latestCommunityPosts = posts
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
this.getCommunityPostsLocal()
}
})
},
handleSubscription: function () {
const currentProfile = JSON.parse(JSON.stringify(this.activeProfile))
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
@ -976,6 +1045,19 @@ export default defineComponent({
break
}
break
case 'community':
switch (this.apiUsed) {
case 'local':
this.getCommunityPostsLocalMore()
break
case 'invidious':
// not supported by invidious yet...
// this.getCommunityPostsInvidiousMore()
break
}
break
default:
console.error(this.currentTab)
}
},

View File

@ -113,6 +113,19 @@
>
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
</div>
<div
id="communityTab"
class="tab"
role="tab"
aria-selected="false"
aria-controls="communityPanel"
tabindex="-1"
:class="(currentTab==='community')?'selectedTab':''"
@click="changeTab('community')"
@keydown.left.right.enter.space="changeTab('community', $event)"
>
{{ $t("Channel.Community.Community").toUpperCase() }}
</div>
<div
id="aboutTab"
class="tab"
@ -302,6 +315,21 @@
{{ $t("Channel.Playlists.This channel does not currently have any playlists") }}
</p>
</ft-flex-box>
<ft-element-list
v-show="currentTab === 'community'"
id="communityPanel"
:data="latestCommunityPosts"
role="tabpanel"
aria-labelledby="communityTab"
display="list"
/>
<ft-flex-box
v-if="currentTab === 'community' && latestCommunityPosts.length === 0"
>
<p class="message">
{{ $t("Channel.Community.This channel currently does not have any posts") }}
</p>
</ft-flex-box>
<ft-element-list
v-show="currentTab === 'search'"
:data="searchResults"

View File

@ -162,7 +162,8 @@ export default defineComponent({
thumbnailURL: function(originalURL) {
let newURL = originalURL
if (new URL(originalURL).hostname === 'yt3.ggpht.com') {
const hostname = new URL(originalURL).hostname
if (hostname === 'yt3.ggpht.com' || hostname === 'yt3.googleusercontent.com') {
if (this.backendPreference === 'invidious') { // YT to IV
newURL = youtubeImageUrlToInvidious(originalURL, this.currentInvidiousInstance)
}

View File

@ -546,6 +546,9 @@ Channel:
Joined: Joined
Location: Location
Featured Channels: Featured Channels
Community:
Community: Community
This channel currently does not have any posts: This channel currently does not have any posts
Video:
Mark As Watched: Mark As Watched
Remove From History: Remove From History

View File

@ -5860,7 +5860,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -8322,6 +8322,11 @@ timers-browserify@^1.0.1:
dependencies:
process "~0.11.0"
tiny-slider@^2.9.2:
version "2.9.4"
resolved "https://registry.yarnpkg.com/tiny-slider/-/tiny-slider-2.9.4.tgz#dd5cbf3065f1688ade8383ea6342aefcba22ccc4"
integrity sha512-LAs2kldWcY+BqCKw4kxd4CMx2RhWrHyEePEsymlOIISTlOVkjfK40sSD7ay73eKXBLg/UkluAZpcfCstimHXew==
tmp-promise@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"
@ -8825,6 +8830,14 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-tiny-slider@^0.1.39:
version "0.1.39"
resolved "https://registry.yarnpkg.com/vue-tiny-slider/-/vue-tiny-slider-0.1.39.tgz#9301eada256fa12725b050767e1e67a287b3e3ef"
integrity sha512-dLOuMI6YyIBabXPZTQ0LL2jhOqZuwsCD7ztPEoE1ejFQ9GNxyRxwkRsIwUtVnq5SCTzQAhCYlgoibyMGoDHReA==
dependencies:
lodash "^4.17.11"
tiny-slider "^2.9.2"
vue@^2.7.14:
version "2.7.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"
@ -9152,10 +9165,10 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
youtubei.js@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-3.1.0.tgz#134169fc45aa4cdfc6f28b2071a38baac834c50b"
integrity sha512-eVklZqdg2DRon40srC2uMw8z67Bv3qT3vgfiTO9crqRVV2phirGXq0RM6vxmovW3lDIJR0jK67M8j69OvK1BkA==
youtubei.js@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-3.1.1.tgz#459cedbf49f9e037233513e0b49d8e27aeb88074"
integrity sha512-MapNwEbZD0xG8ZEvjYeqOhrgsSiof+5Ra4nPtLQwFvFD3q+wmKZW2sCepVo+PwRyhurj2BShIdNznqmguuqpTQ==
dependencies:
jintr "^0.4.1"
linkedom "^0.14.12"