diff --git a/package.json b/package.json index 5fc1d46dc..90bb41fea 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderer/components/ft-community-post/ft-community-post.js b/src/renderer/components/ft-community-post/ft-community-post.js new file mode 100644 index 000000000..1620ce453 --- /dev/null +++ b/src/renderer/components/ft-community-post/ft-community-post.js @@ -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 ?? '' + } + } +}) diff --git a/src/renderer/components/ft-community-post/ft-community-post.scss b/src/renderer/components/ft-community-post/ft-community-post.scss new file mode 100644 index 000000000..d9dcf438b --- /dev/null +++ b/src/renderer/components/ft-community-post/ft-community-post.scss @@ -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; +} diff --git a/src/renderer/components/ft-community-post/ft-community-post.vue b/src/renderer/components/ft-community-post/ft-community-post.vue new file mode 100644 index 000000000..e8e88b4bb --- /dev/null +++ b/src/renderer/components/ft-community-post/ft-community-post.vue @@ -0,0 +1,123 @@ + + + + + + {{ author }} + + + {{ publishedText }} + + + + + + + + + + + + + + + {{ postContent.totalVotes }} + + + + + + + {{ poll.text }} + + + + + + + + + {{ voteCount }} + + + {{ commentCount }} + + + + + + + diff --git a/src/renderer/components/ft-community-post/slider-style.css b/src/renderer/components/ft-community-post/slider-style.css new file mode 100644 index 000000000..b21fc85a7 --- /dev/null +++ b/src/renderer/components/ft-community-post/slider-style.css @@ -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; +} diff --git a/src/renderer/components/ft-element-list/ft-element-list.js b/src/renderer/components/ft-element-list/ft-element-list.js index 2ed0f51da..bc509ed8a 100644 --- a/src/renderer/components/ft-element-list/ft-element-list.js +++ b/src/renderer/components/ft-element-list/ft-element-list.js @@ -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 + } } }) diff --git a/src/renderer/components/ft-element-list/ft-element-list.vue b/src/renderer/components/ft-element-list/ft-element-list.vue index adb2dbb62..82203b6f4 100644 --- a/src/renderer/components/ft-element-list/ft-element-list.vue +++ b/src/renderer/components/ft-element-list/ft-element-list.vue @@ -1,6 +1,6 @@ diff --git a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js index f448346f6..fae88932f 100644 --- a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js +++ b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js @@ -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: { diff --git a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue index 5fc381fa5..16a018970 100644 --- a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue +++ b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue @@ -16,13 +16,18 @@ :data="data" /> + diff --git a/src/renderer/components/ft-list-playlist/ft-list-playlist.vue b/src/renderer/components/ft-list-playlist/ft-list-playlist.vue index 418165176..18906c84a 100644 --- a/src/renderer/components/ft-list-playlist/ft-list-playlist.vue +++ b/src/renderer/components/ft-list-playlist/ft-list-playlist.vue @@ -12,6 +12,7 @@ :to="`/playlist/${playlistId}`" > diff --git a/src/renderer/helpers/api/invidious.js b/src/renderer/helpers/api/invidious.js index 784d764d3..59c7d0c6d 100644 --- a/src/renderer/helpers/api/invidious.js +++ b/src/renderer/helpers/api/invidious.js @@ -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) +} diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index fb9ad9667..b313ee553 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -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') + } +} diff --git a/src/renderer/helpers/utils.js b/src/renderer/helpers/utils.js index 36b1f0127..85650e656 100644 --- a/src/renderer/helpers/utils.js +++ b/src/renderer/helpers/utils.js @@ -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 diff --git a/src/renderer/main.js b/src/renderer/main.js index 2aedf2dc6..06a7999dd 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -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, diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index 61b8cd516..f11429050 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -429,6 +429,8 @@ const actions = { subPath = 'about' break case 'community': + subPath = 'community' + break default: subPath = 'videos' break diff --git a/src/renderer/views/Channel/Channel.css b/src/renderer/views/Channel/Channel.css index ffaa72ab5..4b805f85f 100644 --- a/src/renderer/views/Channel/Channel.css +++ b/src/renderer/views/Channel/Channel.css @@ -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; diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js index 22d4c1222..38e7c4ba9 100644 --- a/src/renderer/views/Channel/Channel.js +++ b/src/renderer/views/Channel/Channel.js @@ -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) } }, diff --git a/src/renderer/views/Channel/Channel.vue b/src/renderer/views/Channel/Channel.vue index fc8d402b7..9ac8380f0 100644 --- a/src/renderer/views/Channel/Channel.vue +++ b/src/renderer/views/Channel/Channel.vue @@ -113,6 +113,19 @@ > {{ $t("Channel.Playlists.Playlists").toUpperCase() }} + + {{ $t("Channel.Community.Community").toUpperCase() }} + + + + + {{ $t("Channel.Community.This channel currently does not have any posts") }} + +
+ {{ author }} +
+ {{ publishedText }} +
+ {{ $t("Channel.Community.This channel currently does not have any posts") }} +