Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommendations #5631

Open
wants to merge 9 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/renderer/components/recommended-tab-ui/recommended-tab-ui.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.card {
inline-size: 85%;
margin-block: 0 60px;
margin-inline: auto;
}

.message {
color: var(--tertiary-text-color);
}

@media only screen and (width <= 680px) {
.card {
inline-size: 90%;
}
}
92 changes: 92 additions & 0 deletions src/renderer/components/recommended-tab-ui/recommended-tab-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { defineComponent } from 'vue'

import FtLoader from '../ft-loader/ft-loader.vue'
import FtButton from '../ft-button/ft-button.vue'
import FtRefreshWidget from '../ft-refresh-widget/ft-refresh-widget.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtElementList from '../FtElementList/FtElementList.vue'
import FtChannelBubble from '../ft-channel-bubble/ft-channel-bubble.vue'
import FtAutoLoadNextPageWrapper from '../ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.vue'

export default defineComponent({
name: 'RecommededTabUI',
components: {
'ft-loader': FtLoader,
'ft-button': FtButton,
'ft-refresh-widget': FtRefreshWidget,
'ft-flex-box': FtFlexBox,
'ft-element-list': FtElementList,
'ft-channel-bubble': FtChannelBubble,
'ft-auto-load-next-page-wrapper': FtAutoLoadNextPageWrapper,
},
props: {
isLoading: {
type: Boolean,
default: false
},
videoList: {
type: Array,
default: () => ([])
},
attemptedFetch: {
type: Boolean,
default: false
},
lastRefreshTimestamp: {
type: String,
required: true
},
title: {
type: String,
required: true
}
},
emits: ['refresh'],
data: function () {
return {
dataLimit: 100,
}
},
computed: {
activeVideoList: function () {
return this.videoList.slice(0, this.dataLimit)
},
searchHistory: function () {
return JSON.parse(localStorage.getItem('search-history') || '[]')
}
},
mounted: function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
},
beforeDestroy: function () {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
/**
* This function `keyboardShortcutHandler` should always be at the bottom of this file
* @param {KeyboardEvent} event the keyboard event
*/
keyboardShortcutHandler: function (event) {
if (event.ctrlKey || document.activeElement.classList.contains('ft-input')) {
return
}
// Avoid handling events due to user holding a key (not released)
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat
if (event.repeat) { return }

switch (event.key) {
case 'r':
case 'R':
case 'F5':
if (!this.isLoading && this.searchHistory.length > 0) {
this.$emit('refresh')
}
break
}
},

refresh: function() {
this.$emit('refresh')
}
}
})
45 changes: 45 additions & 0 deletions src/renderer/components/recommended-tab-ui/recommended-tab-ui.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<div>
<ft-loader
v-if="isLoading"
/>
<ft-flex-box
v-if="!isLoading && activeVideoList.length === 0"
>
<p
v-if="searchHistory.length === 0"
class="message"
>
{{ $t("Your search history is currently empty. Start with some search to see recommendations.") }}

Check warning on line 13 in src/renderer/components/recommended-tab-ui/recommended-tab-ui.vue

View workflow job for this annotation

GitHub Actions / lint

'["Your search history is currently empty. Start with some search to see recommendations."]' does not exist in localization message resources
</p>
</ft-flex-box>
<ft-element-list
v-if="!isLoading && activeVideoList.length > 0"
:data="activeVideoList"
:use-channels-hidden-preference="false"
/>
<ft-auto-load-next-page-wrapper
v-if="!isLoading && videoList.length > dataLimit"
@load-next-page="increaseLimit"
>
<ft-flex-box>
<ft-button
:label="$t('Subscriptions.Load More Videos')"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="increaseLimit"
/>
</ft-flex-box>
</ft-auto-load-next-page-wrapper>

<ft-refresh-widget
:disable-refresh="isLoading || activeVideoList.length === 0"
:last-refresh-timestamp="lastRefreshTimestamp"
:title="title"
@click="refresh"
/>
</div>
</template>

<script src="./recommended-tab-ui.js" />
<style scoped src="./recommended-tab-ui.css" />
123 changes: 123 additions & 0 deletions src/renderer/components/recommended-videos/recommended-videos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { defineComponent } from 'vue'
import { mapActions } from 'vuex'
import RecommendedTabUI from '../recommended-tab-ui/recommended-tab-ui.vue'

import { setPublishedTimestampsInvidious, getRelativeTimeFromDate } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { updateVideoListAfterProcessing } from '../../helpers/subscriptions'

export default defineComponent({
name: 'RecommendedVideos',
components: {
'recommended-tab-ui': RecommendedTabUI
},
data: function () {
return {
isLoading: false,
videoList: [],
attemptedFetch: false,
}
},
computed: {
lastVideoRefreshTimestamp: function () {
return getRelativeTimeFromDate(this.$store.getters.getLastVideoRefreshTimestampByProfile(this.activeProfileId), true)
},

activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileId: function () {
return this.activeProfile._id
},
},
watch: {
activeProfile: async function (_) {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
},
mounted: async function () {
this.isLoading = true

this.loadVideosFromCacheSometimes()
},
methods: {
loadVideosFromCacheSometimes() {
this.updateLastVideoRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: '' })
this.loadRecommendationsFromRemote()
},

loadRecommendationsFromRemote: async function () {
if (localStorage.getItem('search-history') === null) {
this.isLoading = false
this.videoList = []
return
}

const videoList = []
this.isLoading = true
this.attemptedFetch = true

const result = await this.getRecommendedVideos()

videoList.push(...result.videos)
this.updateLastVideoRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: new Date() })

this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
},

getRecommendedVideos: function (failedAttempts = 0) {
return new Promise((resolve, reject) => {
const searchHistory = JSON.parse(localStorage.getItem('search-history')) || []
const numTerms = Math.min(4, searchHistory.length)
const selectedTerms = []

// Select up to 4 random search terms
while (selectedTerms.length < numTerms) {
const index = Math.floor(Math.random() * searchHistory.length)
if (!selectedTerms.includes(searchHistory[index])) {
selectedTerms.push(searchHistory[index])
}
}

const promises = selectedTerms.map(queryTerm => {
const recommendedPayload = {
resource: 'search',
params: {
q: queryTerm + ' sort:date'
}
}

return invidiousAPICall(recommendedPayload).then(videos => {
setPublishedTimestampsInvidious(videos)
return videos
}).catch(err => {
console.error(err)
return []
})
})

Promise.all(promises).then(results => {
const allVideos = results.flat()
let name

if (allVideos.length > 0) {
name = allVideos.find(video => video.type === 'video' && video.author).author
}

resolve({
name,
videos: allVideos
})
}).catch(err => {
reject(err)
})
})
},

...mapActions([
'updateLastVideoRefreshTimestampByProfile'
])
}
})
12 changes: 12 additions & 0 deletions src/renderer/components/recommended-videos/recommended-videos.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<recommended-tab-ui
:is-loading="isLoading"
:video-list="videoList"
:last-refresh-timestamp="lastVideoRefreshTimestamp"
:attempted-fetch="attemptedFetch"
:title="$t('Global.Videos')"
@refresh="loadRecommendationsFromRemote"
/>
</template>

<script src="./recommended-videos.js" />
23 changes: 23 additions & 0 deletions src/renderer/components/side-nav/side-nav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@
{{ $t("Subscriptions.Subscriptions") }}
</p>
</router-link>
<router-link
class="navOption topNavOption mobileShow "
role="button"
to="/recommended"
:title="$t('Recommended.Recommended')"
>
<div
class="thumbnailContainer"
>
<font-awesome-icon
:icon="['fas', 'thumbs-up']"
class="navIcon"
:class="applyNavIconExpand"
fixed-width
/>
</div>
<p
v-if="!hideText"
class="navLabel"
>
{{ $t("Recommended.Recommended") }}
</p>
</router-link>
<router-link
class="navOption mobileHidden"
role="button"
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/components/top-nav/top-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export default defineComponent({
doCreateNewWindow,
searchQueryText: searchQuery,
})

break
}

Expand Down Expand Up @@ -217,6 +218,10 @@ export default defineComponent({

case 'invalid_url':
default: {
const searchHistory = Array.from(new Set(JSON.parse(localStorage.getItem('search-history') || '[]')))
searchHistory.push(queryText)
localStorage.setItem('search-history', JSON.stringify(searchHistory))

openInternalPath({
path: `/search/${encodeURIComponent(queryText)}`,
query: {
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/router/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Vue from 'vue'
import Router from 'vue-router'
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
import Recommended from '../views/Recommended/Recommended.vue'
import SubscribedChannels from '../views/SubscribedChannels/SubscribedChannels.vue'
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.vue'
import Trending from '../views/Trending/Trending.vue'
Expand Down Expand Up @@ -35,6 +36,14 @@ const router = new Router({
},
component: Subscriptions
},
{
path: '/recommended',
name: 'recommended',
meta: {
title: 'Recommended'
},
component: Recommended
},
{
path: '/subscribedchannels',
name: 'subscribedChannels',
Expand Down
Loading