mirror of https://github.com/FreeTubeApp/FreeTube
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:
parent
1b69215855
commit
4ef2f709ee
|
@ -74,8 +74,9 @@
|
||||||
"vue-i18n": "^8.28.2",
|
"vue-i18n": "^8.28.2",
|
||||||
"vue-observe-visibility": "^1.0.0",
|
"vue-observe-visibility": "^1.0.0",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
|
"vue-tiny-slider": "^0.1.39",
|
||||||
"vuex": "^3.6.2",
|
"vuex": "^3.6.2",
|
||||||
"youtubei.js": "^3.1.0"
|
"youtubei.js": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.0",
|
"@babel/core": "^7.21.0",
|
||||||
|
|
|
@ -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 ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -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;
|
||||||
|
}
|
|
@ -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" />
|
|
@ -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;
|
||||||
|
}
|
|
@ -13,14 +13,29 @@ export default defineComponent({
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
display: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
showVideoWithLastViewedPlaylist: {
|
showVideoWithLastViewedPlaylist: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
displayValue: this.display
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
listType: function () {
|
listType: function () {
|
||||||
return this.$store.getters.getListType
|
return this.$store.getters.getListType
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
if (this.display === '') {
|
||||||
|
this.displayValue = this.listType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<ft-auto-grid
|
<ft-auto-grid
|
||||||
:grid="listType !== 'list'"
|
:grid="displayValue !== 'list'"
|
||||||
>
|
>
|
||||||
<ft-list-lazy-wrapper
|
<ft-list-lazy-wrapper
|
||||||
v-for="(result, index) in data"
|
v-for="(result, index) in data"
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
appearance="result"
|
appearance="result"
|
||||||
:data="result"
|
:data="result"
|
||||||
:first-screen="index < 16"
|
:first-screen="index < 16"
|
||||||
:layout="listType"
|
:layout="displayValue"
|
||||||
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
|
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
|
||||||
/>
|
/>
|
||||||
</ft-auto-grid>
|
</ft-auto-grid>
|
||||||
|
|
|
@ -2,13 +2,15 @@ import { defineComponent } from 'vue'
|
||||||
import FtListVideo from '../ft-list-video/ft-list-video.vue'
|
import FtListVideo from '../ft-list-video/ft-list-video.vue'
|
||||||
import FtListChannel from '../ft-list-channel/ft-list-channel.vue'
|
import FtListChannel from '../ft-list-channel/ft-list-channel.vue'
|
||||||
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
|
import FtListPlaylist from '../ft-list-playlist/ft-list-playlist.vue'
|
||||||
|
import FtCommunityPost from '../ft-community-post/ft-community-post.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'FtListLazyWrapper',
|
name: 'FtListLazyWrapper',
|
||||||
components: {
|
components: {
|
||||||
'ft-list-video': FtListVideo,
|
'ft-list-video': FtListVideo,
|
||||||
'ft-list-channel': FtListChannel,
|
'ft-list-channel': FtListChannel,
|
||||||
'ft-list-playlist': FtListPlaylist
|
'ft-list-playlist': FtListPlaylist,
|
||||||
|
'ft-community-post': FtCommunityPost
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -16,13 +16,18 @@
|
||||||
:data="data"
|
:data="data"
|
||||||
/>
|
/>
|
||||||
<ft-list-video
|
<ft-list-video
|
||||||
v-if="(data.type === 'video' || data.type === 'shortVideo') && visible"
|
v-else-if="(data.type === 'video' || data.type === 'shortVideo') && visible"
|
||||||
:appearance="appearance"
|
:appearance="appearance"
|
||||||
:data="data"
|
:data="data"
|
||||||
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
|
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
|
||||||
/>
|
/>
|
||||||
<ft-list-playlist
|
<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"
|
:appearance="appearance"
|
||||||
:data="data"
|
:data="data"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
:to="`/playlist/${playlistId}`"
|
:to="`/playlist/${playlistId}`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
:src="thumbnail"
|
:src="thumbnail"
|
||||||
class="thumbnailImage"
|
class="thumbnailImage"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import store from '../../store/index'
|
import store from '../../store/index'
|
||||||
import { stripHTML, toLocalePublicationString } from '../utils'
|
import { isNullOrEmpty, stripHTML, toLocalePublicationString } from '../utils'
|
||||||
import autolinker from 'autolinker'
|
import autolinker from 'autolinker'
|
||||||
|
|
||||||
function getCurrentInstance() {
|
function getCurrentInstance() {
|
||||||
return store.getters.getCurrentInvidiousInstance
|
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) => {
|
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)
|
fetch(requestUrl)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
|
@ -109,8 +108,10 @@ export function youtubeImageUrlToInvidious(url, currentInstance = null) {
|
||||||
if (url.startsWith('//')) {
|
if (url.startsWith('//')) {
|
||||||
url = 'https:' + url
|
url = 'https:' + url
|
||||||
}
|
}
|
||||||
|
const newUrl = `${currentInstance}/ggpht`
|
||||||
return url.replace('https://yt3.ggpht.com', `${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) {
|
export function invidiousImageUrlToInvidious(url, currentInstance = null) {
|
||||||
|
@ -137,3 +138,91 @@ function parseInvidiousCommentData(response) {
|
||||||
return comment
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -655,3 +655,68 @@ export function parseLocalSubscriberCount(text) {
|
||||||
|
|
||||||
return subscribers
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -620,6 +620,15 @@ export function formatNumber(number, options = undefined) {
|
||||||
return Intl.NumberFormat([i18n.locale.replace('_', '-'), 'en'], options).format(number)
|
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() {
|
export function getTodayDateStrLocalTimezone() {
|
||||||
const timeNow = new Date()
|
const timeNow = new Date()
|
||||||
// `Date#getTimezoneOffset` returns the difference, in minutes
|
// `Date#getTimezoneOffset` returns the difference, in minutes
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCircleUser,
|
faCircleUser,
|
||||||
faClone,
|
faClone,
|
||||||
|
faComment,
|
||||||
faCommentDots,
|
faCommentDots,
|
||||||
faCopy,
|
faCopy,
|
||||||
faDownload,
|
faDownload,
|
||||||
|
@ -50,6 +51,7 @@ import {
|
||||||
faStepBackward,
|
faStepBackward,
|
||||||
faStepForward,
|
faStepForward,
|
||||||
faSync,
|
faSync,
|
||||||
|
faThumbsDown,
|
||||||
faThumbsUp,
|
faThumbsUp,
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faTimes,
|
faTimes,
|
||||||
|
@ -80,6 +82,7 @@ library.add(
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCircleUser,
|
faCircleUser,
|
||||||
faClone,
|
faClone,
|
||||||
|
faComment,
|
||||||
faCommentDots,
|
faCommentDots,
|
||||||
faCopy,
|
faCopy,
|
||||||
faDownload,
|
faDownload,
|
||||||
|
@ -114,6 +117,7 @@ library.add(
|
||||||
faStepBackward,
|
faStepBackward,
|
||||||
faStepForward,
|
faStepForward,
|
||||||
faSync,
|
faSync,
|
||||||
|
faThumbsDown,
|
||||||
faThumbsUp,
|
faThumbsUp,
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faTimes,
|
faTimes,
|
||||||
|
|
|
@ -429,6 +429,8 @@ const actions = {
|
||||||
subPath = 'about'
|
subPath = 'about'
|
||||||
break
|
break
|
||||||
case 'community':
|
case 'community':
|
||||||
|
subPath = 'community'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
subPath = 'videos'
|
subPath = 'videos'
|
||||||
break
|
break
|
||||||
|
|
|
@ -121,8 +121,7 @@
|
||||||
border-bottom: 3px solid var(--tertiary-text-color);
|
border-bottom: 3px solid var(--tertiary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedTab,
|
.selectedTab {
|
||||||
.selectedTab:hover {
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
border-bottom: 3px solid var(--primary-color);
|
border-bottom: 3px solid var(--primary-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -212,6 +211,25 @@
|
||||||
margin: 0;
|
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) {
|
@media only screen and (max-width: 800px) {
|
||||||
.channelInfoTabs {
|
.channelInfoTabs {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
@ -13,18 +13,20 @@ import FtShareButton from '../../components/ft-share-button/ft-share-button.vue'
|
||||||
|
|
||||||
import autolinker from 'autolinker'
|
import autolinker from 'autolinker'
|
||||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
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 packageDetails from '../../../../package.json'
|
||||||
import {
|
import {
|
||||||
invidiousAPICall,
|
invidiousAPICall,
|
||||||
invidiousGetChannelId,
|
invidiousGetChannelId,
|
||||||
invidiousGetChannelInfo,
|
invidiousGetChannelInfo,
|
||||||
|
invidiousGetCommunityPosts,
|
||||||
youtubeImageUrlToInvidious
|
youtubeImageUrlToInvidious
|
||||||
} from '../../helpers/api/invidious'
|
} from '../../helpers/api/invidious'
|
||||||
import {
|
import {
|
||||||
getLocalChannel,
|
getLocalChannel,
|
||||||
getLocalChannelId,
|
getLocalChannelId,
|
||||||
parseLocalChannelVideos,
|
parseLocalChannelVideos,
|
||||||
|
parseLocalCommunityPost,
|
||||||
parseLocalListPlaylist,
|
parseLocalListPlaylist,
|
||||||
parseLocalListVideo,
|
parseLocalListVideo,
|
||||||
parseLocalSubscriberCount
|
parseLocalSubscriberCount
|
||||||
|
@ -59,6 +61,7 @@ export default defineComponent({
|
||||||
videoContinuationData: null,
|
videoContinuationData: null,
|
||||||
playlistContinuationData: null,
|
playlistContinuationData: null,
|
||||||
searchContinuationData: null,
|
searchContinuationData: null,
|
||||||
|
communityContinuationData: null,
|
||||||
description: '',
|
description: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
views: 0,
|
views: 0,
|
||||||
|
@ -70,6 +73,7 @@ export default defineComponent({
|
||||||
relatedChannels: [],
|
relatedChannels: [],
|
||||||
latestVideos: [],
|
latestVideos: [],
|
||||||
latestPlaylists: [],
|
latestPlaylists: [],
|
||||||
|
latestCommunityPosts: [],
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
shownElementList: [],
|
shownElementList: [],
|
||||||
apiUsed: '',
|
apiUsed: '',
|
||||||
|
@ -88,6 +92,7 @@ export default defineComponent({
|
||||||
tabInfoValues: [
|
tabInfoValues: [
|
||||||
'videos',
|
'videos',
|
||||||
'playlists',
|
'playlists',
|
||||||
|
'community',
|
||||||
'about'
|
'about'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -179,20 +184,13 @@ export default defineComponent({
|
||||||
showFetchMoreButton: function () {
|
showFetchMoreButton: function () {
|
||||||
switch (this.currentTab) {
|
switch (this.currentTab) {
|
||||||
case 'videos':
|
case 'videos':
|
||||||
if (this.videoContinuationData !== null) {
|
return !isNullOrEmpty(this.videoContinuationData)
|
||||||
return true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'playlists':
|
case 'playlists':
|
||||||
if (this.playlistContinuationData !== null) {
|
return !isNullOrEmpty(this.playlistContinuationData)
|
||||||
return true
|
case 'community':
|
||||||
}
|
return !isNullOrEmpty(this.communityContinuationData)
|
||||||
break
|
|
||||||
case 'search':
|
case 'search':
|
||||||
if (this.searchContinuationData !== null) {
|
return !isNullOrEmpty(this.searchContinuationData)
|
||||||
return true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -236,6 +234,7 @@ export default defineComponent({
|
||||||
this.videoContinuationData = null
|
this.videoContinuationData = null
|
||||||
this.playlistContinuationData = null
|
this.playlistContinuationData = null
|
||||||
this.searchContinuationData = null
|
this.searchContinuationData = null
|
||||||
|
this.communityContinuationData = null
|
||||||
this.showSearchBar = true
|
this.showSearchBar = true
|
||||||
|
|
||||||
if (this.id === '@@@') {
|
if (this.id === '@@@') {
|
||||||
|
@ -509,6 +508,10 @@ export default defineComponent({
|
||||||
this.getChannelPlaylistsLocal()
|
this.getChannelPlaylistsLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channel.has_community) {
|
||||||
|
this.getCommunityPostsLocal()
|
||||||
|
}
|
||||||
|
|
||||||
this.showSearchBar = channel.has_search
|
this.showSearchBar = channel.has_search
|
||||||
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
|
@ -671,6 +674,10 @@ export default defineComponent({
|
||||||
this.getPlaylistsInvidious()
|
this.getPlaylistsInvidious()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.tabs.includes('community')) {
|
||||||
|
this.getCommunityPostsInvidious()
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.setErrorMessage(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 () {
|
handleSubscription: function () {
|
||||||
const currentProfile = JSON.parse(JSON.stringify(this.activeProfile))
|
const currentProfile = JSON.parse(JSON.stringify(this.activeProfile))
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
|
@ -976,6 +1045,19 @@ export default defineComponent({
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,19 @@
|
||||||
>
|
>
|
||||||
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
|
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
|
||||||
</div>
|
</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
|
<div
|
||||||
id="aboutTab"
|
id="aboutTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
|
@ -302,6 +315,21 @@
|
||||||
{{ $t("Channel.Playlists.This channel does not currently have any playlists") }}
|
{{ $t("Channel.Playlists.This channel does not currently have any playlists") }}
|
||||||
</p>
|
</p>
|
||||||
</ft-flex-box>
|
</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
|
<ft-element-list
|
||||||
v-show="currentTab === 'search'"
|
v-show="currentTab === 'search'"
|
||||||
:data="searchResults"
|
:data="searchResults"
|
||||||
|
|
|
@ -162,7 +162,8 @@ export default defineComponent({
|
||||||
|
|
||||||
thumbnailURL: function(originalURL) {
|
thumbnailURL: function(originalURL) {
|
||||||
let newURL = 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
|
if (this.backendPreference === 'invidious') { // YT to IV
|
||||||
newURL = youtubeImageUrlToInvidious(originalURL, this.currentInvidiousInstance)
|
newURL = youtubeImageUrlToInvidious(originalURL, this.currentInvidiousInstance)
|
||||||
}
|
}
|
||||||
|
|
|
@ -546,6 +546,9 @@ Channel:
|
||||||
Joined: Joined
|
Joined: Joined
|
||||||
Location: Location
|
Location: Location
|
||||||
Featured Channels: Featured Channels
|
Featured Channels: Featured Channels
|
||||||
|
Community:
|
||||||
|
Community: Community
|
||||||
|
This channel currently does not have any posts: This channel currently does not have any posts
|
||||||
Video:
|
Video:
|
||||||
Mark As Watched: Mark As Watched
|
Mark As Watched: Mark As Watched
|
||||||
Remove From History: Remove From History
|
Remove From History: Remove From History
|
||||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -5860,7 +5860,7 @@ lodash.uniq@^4.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
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"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
@ -8322,6 +8322,11 @@ timers-browserify@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
process "~0.11.0"
|
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:
|
tmp-promise@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"
|
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"
|
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
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:
|
vue@^2.7.14:
|
||||||
version "2.7.14"
|
version "2.7.14"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"
|
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"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
youtubei.js@^3.1.0:
|
youtubei.js@^3.1.1:
|
||||||
version "3.1.0"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-3.1.0.tgz#134169fc45aa4cdfc6f28b2071a38baac834c50b"
|
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-3.1.1.tgz#459cedbf49f9e037233513e0b49d8e27aeb88074"
|
||||||
integrity sha512-eVklZqdg2DRon40srC2uMw8z67Bv3qT3vgfiTO9crqRVV2phirGXq0RM6vxmovW3lDIJR0jK67M8j69OvK1BkA==
|
integrity sha512-MapNwEbZD0xG8ZEvjYeqOhrgsSiof+5Ra4nPtLQwFvFD3q+wmKZW2sCepVo+PwRyhurj2BShIdNznqmguuqpTQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
jintr "^0.4.1"
|
jintr "^0.4.1"
|
||||||
linkedom "^0.14.12"
|
linkedom "^0.14.12"
|
||||||
|
|
Loading…
Reference in New Issue