Skip to content

Commit 1af3bed

Browse files
Add support for channel's Courses tab
1 parent 23e5c34 commit 1af3bed

File tree

9 files changed

+204
-5
lines changed

9 files changed

+204
-5
lines changed

src/renderer/components/ChannelDetails/ChannelDetails.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,22 @@
171171
{{ $t("Channel.Podcasts.Podcasts").toUpperCase() }}
172172
</div>
173173
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
174+
<div
175+
v-if="visibleTabs.includes('courses')"
176+
id="coursesTab"
177+
class="tab"
178+
role="tab"
179+
:aria-selected="String(currentTab === 'courses')"
180+
aria-controls="coursesPanel"
181+
:tabindex="currentTab === 'courses' ? 0 : -1"
182+
:class="{ selectedTab: currentTab === 'courses' }"
183+
@click="changeTab('courses')"
184+
@keydown.left.right="focusTab('courses', $event)"
185+
@keydown.enter.space.prevent="changeTab('courses')"
186+
>
187+
{{ $t("Channel.Courses.Courses").toUpperCase() }}
188+
</div>
189+
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
174190
<div
175191
v-if="visibleTabs.includes('playlists')"
176192
id="playlistsTab"

src/renderer/components/distraction-settings/distraction-settings.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export default defineComponent({
9393
hideChannelReleases: function () {
9494
return this.$store.getters.getHideChannelReleases
9595
},
96+
hideChannelCourses: function () {
97+
return this.$store.getters.getHideChannelCourses
98+
},
9699
hideChannelCommunity: function () {
97100
return this.$store.getters.getHideChannelCommunity
98101
},
@@ -235,6 +238,7 @@ export default defineComponent({
235238
'updateHideChannelHome',
236239
'updateHideChannelPodcasts',
237240
'updateHideChannelReleases',
241+
'updateHideChannelCourses',
238242
'updateHideSubscriptionsVideos',
239243
'updateHideSubscriptionsShorts',
240244
'updateHideSubscriptionsLive',

src/renderer/components/distraction-settings/distraction-settings.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@
125125
:default-value="hideChannelReleases"
126126
@change="updateHideChannelReleases"
127127
/>
128+
<ft-toggle-switch
129+
:label="$t('Settings.Distraction Free Settings.Hide Channel Courses')"
130+
:compact="true"
131+
:default-value="hideChannelCourses"
132+
@change="updateHideChannelCourses"
133+
/>
128134
</div>
129135
</div>
130136
<h4

src/renderer/helpers/api/invidious.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export async function invidiousGetChannelId(url) {
127127
* description: string,
128128
* descriptionHtml: string,
129129
* allowedRegions: string[],
130-
* tabs: ('home' | 'videos' | 'shorts' | 'streams' | 'podcasts' | 'releases' | 'playlists' | 'community')[],
130+
* tabs: ('home' | 'videos' | 'shorts' | 'streams' | 'podcasts' | 'releases' | 'courses' | 'playlists' | 'community')[],
131131
* latestVideos: InvidiousVideoType[],
132132
* relatedChannels: InvidiousChannelObject[]
133133
* }>}
@@ -240,6 +240,15 @@ export async function getInvidiousChannelPodcasts(channelId, continuation) {
240240
return await getInvidiousChannelTab('podcasts', channelId, continuation)
241241
}
242242

243+
/**
244+
* @param {string} channelId
245+
* @param {string | undefined | null} continuation
246+
*/
247+
export async function getInvidiousChannelCourses(channelId, continuation) {
248+
/** @type {{continuation: string?, playlists: InvidiousPlaylistObject[]}} */
249+
return await getInvidiousChannelTab('courses', channelId, continuation)
250+
}
251+
243252
/**
244253
* @param {string} channelId
245254
* @param {string} query

src/renderer/store/modules/settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ const state = {
186186
hideChannelPlaylists: false,
187187
hideChannelReleases: false,
188188
hideChannelPodcasts: false,
189+
hideChannelCourses: false,
189190
hideChannelShorts: false,
190191
hideChannelSubscriptions: false,
191192
hideCommentLikes: false,

src/renderer/store/modules/utils.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ const actions = {
475475
let urlType = 'unknown'
476476

477477
const channelPattern =
478-
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|shorts|live|streams|podcasts|releases|playlists|about|community|channels))?\/?$/
478+
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|shorts|live|streams|podcasts|releases|courses|playlists|about|community|channels))?\/?$/
479479

480480
const hashtagPattern = /^\/hashtag\/(?<tag>[^#&/?]+)$/
481481

@@ -622,6 +622,9 @@ const actions = {
622622
case 'podcasts':
623623
subPath = 'podcasts'
624624
break
625+
case 'courses':
626+
subPath = 'courses'
627+
break
625628
case 'releases':
626629
subPath = 'releases'
627630
break

src/renderer/views/Channel/Channel.js

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
getInvidiousChannelPlaylists,
2727
getInvidiousChannelPodcasts,
2828
getInvidiousChannelReleases,
29+
getInvidiousChannelCourses,
2930
getInvidiousChannelShorts,
3031
getInvidiousChannelVideos,
3132
invidiousGetChannelId,
@@ -87,6 +88,7 @@ export default defineComponent({
8788
liveContinuationData: null,
8889
releaseContinuationData: null,
8990
podcastContinuationData: null,
91+
coursesContinuationData: null,
9092
playlistContinuationData: null,
9193
searchContinuationData: null,
9294
communityContinuationData: null,
@@ -112,6 +114,7 @@ export default defineComponent({
112114
latestLive: [],
113115
latestReleases: [],
114116
latestPodcasts: [],
117+
latestCourses: [],
115118
latestPlaylists: [],
116119
latestCommunityPosts: [],
117120
searchResults: [],
@@ -134,6 +137,7 @@ export default defineComponent({
134137
'live',
135138
'releases',
136139
'podcasts',
140+
'courses',
137141
'playlists',
138142
'community',
139143
'about'
@@ -144,6 +148,7 @@ export default defineComponent({
144148
'live',
145149
'releases',
146150
'podcasts',
151+
'courses',
147152
'playlists',
148153
'community',
149154
'about'
@@ -234,6 +239,8 @@ export default defineComponent({
234239
return !isNullOrEmpty(this.releaseContinuationData)
235240
case 'podcasts':
236241
return !isNullOrEmpty(this.podcastContinuationData)
242+
case 'courses':
243+
return !isNullOrEmpty(this.coursesContinuationData)
237244
case 'playlists':
238245
return !isNullOrEmpty(this.playlistContinuationData)
239246
case 'community':
@@ -261,6 +268,10 @@ export default defineComponent({
261268
return this.$store.getters.getHideChannelReleases
262269
},
263270

271+
hideChannelCourses: function() {
272+
return this.$store.getters.getHideChannelCourses
273+
},
274+
264275
hideChannelPlaylists: function() {
265276
return this.$store.getters.getHideChannelPlaylists
266277
},
@@ -302,6 +313,10 @@ export default defineComponent({
302313
indexToRemove.push(values.indexOf('releases'))
303314
}
304315

316+
if (this.hideChannelCourses) {
317+
indexToRemove.push(values.indexOf('courses'))
318+
}
319+
305320
if (this.hideChannelHome) {
306321
indexToRemove.push(values.indexOf('home'))
307322
}
@@ -668,6 +683,11 @@ export default defineComponent({
668683
this.getChannelReleasesLocal()
669684
}
670685

686+
if (!this.hideChannelCourses && channel.has_courses) {
687+
tabs.push('courses')
688+
this.getChannelCoursesLocal()
689+
}
690+
671691
if (!this.hideChannelPlaylists) {
672692
if (channel.has_playlists) {
673693
tabs.push('playlists')
@@ -1126,6 +1146,10 @@ export default defineComponent({
11261146
this.channelInvidiousReleases()
11271147
}
11281148

1149+
if (!this.hideChannelCourses && response.tabs.includes('courses')) {
1150+
this.channelInvidiousCourses()
1151+
}
1152+
11291153
if (!this.hideChannelPlaylists && response.tabs.includes('playlists')) {
11301154
this.getPlaylistsInvidious()
11311155
}
@@ -1560,7 +1584,7 @@ export default defineComponent({
15601584

15611585
const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
15621586
this.latestPodcasts = this.latestPodcasts.concat(parsedPodcasts)
1563-
this.releaseContinuationData = continuation.has_continuation ? continuation : null
1587+
this.podcastContinuationData = continuation.has_continuation ? continuation : null
15641588
} catch (err) {
15651589
console.error(err)
15661590
const errorMessage = this.$t('Local API Error (Click to copy)')
@@ -1620,6 +1644,108 @@ export default defineComponent({
16201644
})
16211645
},
16221646

1647+
getChannelCoursesLocal: async function () {
1648+
this.isElementListLoading = true
1649+
const expectedId = this.id
1650+
1651+
try {
1652+
/**
1653+
* @type {import('youtubei.js').YT.Channel}
1654+
*/
1655+
const channel = this.channelInstance
1656+
const coursesTab = await channel.getCourses()
1657+
1658+
if (expectedId !== this.id) {
1659+
return
1660+
}
1661+
1662+
this.latestCourses = coursesTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
1663+
this.coursesContinuationData = coursesTab.has_continuation ? coursesTab : null
1664+
this.isElementListLoading = false
1665+
} catch (err) {
1666+
console.error(err)
1667+
const errorMessage = this.$t('Local API Error (Click to copy)')
1668+
showToast(`${errorMessage}: ${err}`, 10000, () => {
1669+
copyToClipboard(err)
1670+
})
1671+
if (this.backendPreference === 'local' && this.backendFallback) {
1672+
showToast(this.$t('Falling back to Invidious API'))
1673+
this.channelInvidiousCourses()
1674+
} else {
1675+
this.isLoading = false
1676+
}
1677+
}
1678+
},
1679+
1680+
getChannelCoursesLocalMore: async function () {
1681+
try {
1682+
/**
1683+
* @type {import('youtubei.js').YT.ChannelListContinuation}
1684+
*/
1685+
const continuation = await this.coursesContinuationData.getContinuation()
1686+
1687+
const parsedCourses = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
1688+
this.latestCourses = this.latestCourses.concat(parsedCourses)
1689+
this.coursesContinuationData = continuation.has_continuation ? continuation : null
1690+
} catch (err) {
1691+
console.error(err)
1692+
const errorMessage = this.$t('Local API Error (Click to copy)')
1693+
showToast(`${errorMessage}: ${err}`, 10000, () => {
1694+
copyToClipboard(err)
1695+
})
1696+
}
1697+
},
1698+
1699+
channelInvidiousCourses: function() {
1700+
this.isElementListLoading = true
1701+
1702+
getInvidiousChannelCourses(this.id).then((response) => {
1703+
this.coursesContinuationData = response.continuation || null
1704+
this.latestCourses = response.playlists
1705+
this.isElementListLoading = false
1706+
}).catch(async (err) => {
1707+
console.error(err)
1708+
const errorMessage = this.$t('Invidious API Error (Click to copy)')
1709+
showToast(`${errorMessage}: ${err}`, 10000, () => {
1710+
copyToClipboard(err)
1711+
})
1712+
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
1713+
showToast(this.$t('Falling back to Local API'))
1714+
if (!this.channelInstance) {
1715+
this.channelInstance = await getLocalChannel(this.id)
1716+
}
1717+
this.getChannelCoursesLocal()
1718+
} else {
1719+
this.isLoading = false
1720+
}
1721+
})
1722+
},
1723+
1724+
channelInvidiousCoursesMore: function () {
1725+
if (this.coursesContinuationData === null) {
1726+
console.warn('There are no more courses available for this channel')
1727+
return
1728+
}
1729+
1730+
getInvidiousChannelCourses(this.id, this.coursesContinuationData).then((response) => {
1731+
this.coursesContinuationData = response.continuation || null
1732+
this.latestCourses = this.latestCourses.concat(response.playlists)
1733+
this.isElementListLoading = false
1734+
}).catch((err) => {
1735+
console.error(err)
1736+
const errorMessage = this.$t('Invidious API Error (Click to copy)')
1737+
showToast(`${errorMessage}: ${err}`, 10000, () => {
1738+
copyToClipboard(err)
1739+
})
1740+
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
1741+
showToast(this.$t('Falling back to Local API'))
1742+
this.getChannelLocal()
1743+
} else {
1744+
this.isLoading = false
1745+
}
1746+
})
1747+
},
1748+
16231749
getCommunityPostsLocal: async function () {
16241750
const expectedId = this.id
16251751

@@ -1780,10 +1906,25 @@ export default defineComponent({
17801906
}
17811907
break
17821908
case 'releases':
1783-
this.getChannelReleasesLocalMore()
1909+
if (this.apiUsed === 'local') {
1910+
this.getChannelReleasesLocalMore()
1911+
} else {
1912+
this.channelInvidiousReleasesMore()
1913+
}
17841914
break
17851915
case 'podcasts':
1786-
this.getChannelPodcastsLocalMore()
1916+
if (this.apiUsed === 'local') {
1917+
this.getChannelPodcastsLocalMore()
1918+
} else {
1919+
this.channelInvidiousPodcastsMore()
1920+
}
1921+
break
1922+
case 'courses':
1923+
if (this.apiUsed === 'local') {
1924+
this.getChannelCoursesLocalMore()
1925+
} else {
1926+
this.channelInvidiousCoursesMore()
1927+
}
17871928
break
17881929
case 'playlists':
17891930
switch (this.apiUsed) {

src/renderer/views/Channel/Channel.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@
170170
{{ $t("Channel.Releases.This channel does not currently have any releases") }}
171171
</p>
172172
</ft-flex-box>
173+
<ft-element-list
174+
v-if="!hideChannelCourses && currentTab === 'courses'"
175+
id="coursesPanel"
176+
:data="latestCourses"
177+
:use-channels-hidden-preference="false"
178+
role="tabpanel"
179+
aria-labelledby="coursesTab"
180+
/>
181+
<ft-flex-box
182+
v-if="!hideChannelCourses && currentTab === 'courses' && latestCourses.length === 0"
183+
>
184+
<p class="message">
185+
{{ $t("Channel.Courses.This channel does not currently have any courses") }}
186+
</p>
187+
</ft-flex-box>
173188
<ft-element-list
174189
v-if="!hideChannelPlaylists && currentTab === 'playlists'"
175190
id="playlistPanel"

static/locales/en-US.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ Settings:
567567
Hide Channel Shorts: Hide Channel "Shorts" Tab
568568
Hide Channel Podcasts: Hide Channel "Podcasts" Tab
569569
Hide Channel Releases: Hide Channel "Releases" Tab
570+
Hide Channel Courses: Hide Channel "Courses" Tab
570571
Hide Videos and Playlists Containing Text: Hide Videos and Playlists Containing Text
571572
Hide Videos and Playlists Containing Text Placeholder: Word, Word Fragment, or Phrase
572573
Hide Subscriptions Videos: Hide Subscriptions Videos
@@ -800,6 +801,9 @@ Channel:
800801
Releases:
801802
Releases: Releases
802803
This channel does not currently have any releases: This channel does not currently have any releases
804+
Courses:
805+
Courses: Courses
806+
This channel does not currently have any courses: This channel does not currently have any courses
803807
About:
804808
About: About
805809
Channel Description: Channel Description

0 commit comments

Comments
 (0)