mirror of https://github.com/FreeTubeApp/FreeTube
Show when Subscriptions / Trending / Most Popular were last updated (#4380)
* Implement first draft of last subscription refresh timestamp * Update styling to be a top bar * Update styling to be banner-compatible, & increase banner X button size on mobile * Update subscription refresh timestamp to be relative * Implement refresh timestamps for Shorts, Live, and Community tabs * Extract refresh widget to its own component * Add Trending and Popular refresh widgets with timestamps * Fix justifying when no timestamp exists * Move timestamps to utils store * Remove unneeded ref classes and currentLocale computed property * Add page-specific titles for each feed type * Implement showing least recent cache date per profile * Update styling property placement & match top nav box shadow on ft-refresh-widget * Implement showing timestamp for profile only if all channel subscriptions can be found in cache * Disable refresh button instead of removing it or the widget from the DOM * Increase top banner's top margin * Update channel caching calls to provide timestamps * Modify updateCacheByChannel functions to have default timestamp of new Date() * Fix 30-day month relative date calculation scenarios through new optional parameter * Rectify Case 3 (see https://github.com/FreeTubeApp/FreeTube/pull/3668) * Add back missing line in Popular.js
This commit is contained in:
parent
2bc44cd66b
commit
ab3c1b9b29
|
@ -18,10 +18,14 @@
|
|||
|
||||
.banner {
|
||||
inline-size: 85%;
|
||||
margin-block: 20px;
|
||||
margin-block: 40px 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.banner + .banner {
|
||||
margin-block: 20px;
|
||||
}
|
||||
|
||||
.banner-wrapper {
|
||||
margin-block: 0;
|
||||
margin-inline: 10px;
|
||||
|
@ -53,8 +57,8 @@
|
|||
}
|
||||
|
||||
.banner {
|
||||
inline-size: 80%;
|
||||
margin-block-start: 20px;
|
||||
inline-size: 90%;
|
||||
margin-block: 60px 0;
|
||||
}
|
||||
|
||||
.flexBox {
|
||||
|
|
|
@ -16,6 +16,10 @@ export default defineComponent({
|
|||
type: Array,
|
||||
default: () => ['fas', 'ellipsis-v']
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'base'
|
||||
|
@ -88,6 +92,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
handleIconClick: function () {
|
||||
if (this.disabled) { return }
|
||||
if (this.forceDropdown || (this.dropdownOptions.length > 0)) {
|
||||
this.dropdownShown = !this.dropdownShown
|
||||
|
||||
|
@ -104,6 +109,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
handleIconMouseDown: function () {
|
||||
if (this.disabled) { return }
|
||||
if (this.dropdownShown) {
|
||||
this.mouseDownOnIcon = true
|
||||
}
|
||||
|
|
|
@ -79,6 +79,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.iconDropdown {
|
||||
background-color: var(--side-nav-color);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 50%);
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
:icon="icon"
|
||||
:class="{
|
||||
[theme]: true,
|
||||
shadow: useShadow
|
||||
shadow: useShadow,
|
||||
disabled
|
||||
}"
|
||||
:style="{
|
||||
padding: padding + 'px',
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
copyToClipboard,
|
||||
formatDurationAsTimestamp,
|
||||
formatNumber,
|
||||
getRelativeTimeFromDate,
|
||||
openExternalLink,
|
||||
showToast,
|
||||
toDistractionFreeTitle,
|
||||
|
@ -345,6 +346,10 @@ export default defineComponent({
|
|||
return this.historyEntryExists && !this.inHistory
|
||||
},
|
||||
|
||||
currentLocale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
|
@ -462,14 +467,6 @@ export default defineComponent({
|
|||
return query
|
||||
},
|
||||
|
||||
currentLocale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
|
||||
showAddToPlaylistPrompt: function () {
|
||||
return this.$store.getters.getShowAddToPlaylistPrompt
|
||||
},
|
||||
|
||||
useDeArrowTitles: function () {
|
||||
return this.$store.getters.getUseDeArrowTitles
|
||||
},
|
||||
|
@ -668,48 +665,8 @@ export default defineComponent({
|
|||
if (this.inHistory) {
|
||||
this.uploadedTime = new Date(this.data.published).toLocaleDateString([this.currentLocale, 'en'])
|
||||
} else {
|
||||
const now = new Date().getTime()
|
||||
// Convert from ms to second
|
||||
// For easier code interpretation the value is made to be positive
|
||||
let timeDiffFromNow = ((now - this.data.published) / 1000)
|
||||
let timeUnit = 'second'
|
||||
|
||||
if (timeDiffFromNow >= 60) {
|
||||
timeDiffFromNow /= 60
|
||||
timeUnit = 'minute'
|
||||
}
|
||||
|
||||
if (timeUnit === 'minute' && timeDiffFromNow >= 60) {
|
||||
timeDiffFromNow /= 60
|
||||
timeUnit = 'hour'
|
||||
}
|
||||
|
||||
if (timeUnit === 'hour' && timeDiffFromNow >= 24) {
|
||||
timeDiffFromNow /= 24
|
||||
timeUnit = 'day'
|
||||
}
|
||||
|
||||
const timeDiffFromNowDays = timeDiffFromNow
|
||||
|
||||
if (timeUnit === 'day' && timeDiffFromNow >= 7) {
|
||||
timeDiffFromNow /= 7
|
||||
timeUnit = 'week'
|
||||
}
|
||||
|
||||
// Use 30 days per month, just like calculatePublishedDate
|
||||
if (timeUnit === 'week' && timeDiffFromNowDays >= 30) {
|
||||
timeDiffFromNow = timeDiffFromNowDays / 30
|
||||
timeUnit = 'month'
|
||||
}
|
||||
|
||||
if (timeUnit === 'month' && timeDiffFromNow >= 12) {
|
||||
timeDiffFromNow /= 12
|
||||
timeUnit = 'year'
|
||||
}
|
||||
|
||||
// Using `Math.ceil` so that -1.x days ago displayed as 1 day ago
|
||||
// Notice that the value is turned to negative to be displayed as "ago"
|
||||
this.uploadedTime = new Intl.RelativeTimeFormat([this.currentLocale, 'en']).format(Math.ceil(-timeDiffFromNow), timeUnit)
|
||||
this.uploadedTime = getRelativeTimeFromDate(new Date(this.data.published).toDateString(), false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,3 +30,11 @@
|
|||
inset-inline-end: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 680px) {
|
||||
.bannerIcon {
|
||||
inset-block-start: 27%;
|
||||
block-size: 25px;
|
||||
inline-size: 25px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
tabindex="0"
|
||||
:title="$t('Close Banner')"
|
||||
@click.stop="handleClose"
|
||||
@keydown.enter.stop.prevent="handleClose"
|
||||
@keydown.enter.space.stop.prevent="handleClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
.floatingRefreshSection {
|
||||
position: fixed;
|
||||
inset-block-start: 60px;
|
||||
inset-inline-end: 0;
|
||||
box-sizing: border-box;
|
||||
inline-size: calc(100% - 80px);
|
||||
padding-block: 5px;
|
||||
padding-inline: 10px;
|
||||
box-shadow: 0 2px 1px 0 var(--primary-shadow-color);
|
||||
background-color: var(--card-bg-color);
|
||||
border-inline-start: 2px solid var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.floatingRefreshSection:has(.lastRefreshTimestamp + .refreshButton) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.floatingRefreshSection.sideNavOpen {
|
||||
inline-size: calc(100% - 200px);
|
||||
}
|
||||
|
||||
.lastRefreshTimestamp {
|
||||
margin-block: 0;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 680px) {
|
||||
.floatingRefreshSection, .floatingRefreshSection.sideNavOpen {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { defineComponent } from 'vue'
|
||||
|
||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtRefreshWidget',
|
||||
components: {
|
||||
'ft-icon-button': FtIconButton,
|
||||
},
|
||||
props: {
|
||||
disableRefresh: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lastRefreshTimestamp: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSideNavOpen: function () {
|
||||
return this.$store.getters.getIsSideNavOpen
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div
|
||||
class="floatingRefreshSection"
|
||||
:class="{
|
||||
sideNavOpen: isSideNavOpen
|
||||
}"
|
||||
>
|
||||
<p
|
||||
v-if="lastRefreshTimestamp"
|
||||
class="lastRefreshTimestamp"
|
||||
>
|
||||
{{ $t('Feed.Feed Last Updated', { feedName: title, date: lastRefreshTimestamp }) }}
|
||||
</p>
|
||||
<ft-icon-button
|
||||
:disabled="disableRefresh"
|
||||
:icon="['fas', 'sync']"
|
||||
class="refreshButton"
|
||||
:title="$t('Feed.Refresh Feed', { subscriptionName: title })"
|
||||
:size="12"
|
||||
theme="primary"
|
||||
@click="$emit('click')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./ft-refresh-widget.js" />
|
||||
<style scoped src="./ft-refresh-widget.css" />
|
|
@ -2,7 +2,7 @@ import { defineComponent } from 'vue'
|
|||
import { mapActions, mapMutations } from 'vuex'
|
||||
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
|
||||
|
||||
import { calculatePublishedDate, copyToClipboard, showToast } from '../../helpers/utils'
|
||||
import { calculatePublishedDate, copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils'
|
||||
import { getLocalChannelCommunity } from '../../helpers/api/local'
|
||||
import { invidiousGetCommunityPosts } from '../../helpers/api/invidious'
|
||||
|
||||
|
@ -49,6 +49,11 @@ export default defineComponent({
|
|||
})
|
||||
return entries
|
||||
},
|
||||
|
||||
lastCommunityRefreshTimestamp: function () {
|
||||
return getRelativeTimeFromDate(this.$store.getters.getLastCommunityRefreshTimestampByProfile(this.activeProfileId), true)
|
||||
},
|
||||
|
||||
postCacheForAllActiveProfileChannelsPresent() {
|
||||
if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false }
|
||||
if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false }
|
||||
|
@ -69,22 +74,33 @@ export default defineComponent({
|
|||
watch: {
|
||||
activeProfile: async function (_) {
|
||||
this.isLoading = true
|
||||
this.loadpostsFromCacheSometimes()
|
||||
this.loadPostsFromCacheSometimes()
|
||||
},
|
||||
},
|
||||
mounted: async function () {
|
||||
this.isLoading = true
|
||||
|
||||
this.loadpostsFromCacheSometimes()
|
||||
this.loadPostsFromCacheSometimes()
|
||||
},
|
||||
methods: {
|
||||
loadpostsFromCacheSometimes() {
|
||||
loadPostsFromCacheSometimes() {
|
||||
// This method is called on view visible
|
||||
if (this.postCacheForAllActiveProfileChannelsPresent) {
|
||||
this.loadPostsFromCacheForAllActiveProfileChannels()
|
||||
if (this.cacheEntriesForAllActiveProfileChannels.length > 0) {
|
||||
let minTimestamp = null
|
||||
this.cacheEntriesForAllActiveProfileChannels.forEach((cacheEntry) => {
|
||||
if (!minTimestamp || cacheEntry.timestamp.getTime() < minTimestamp.getTime()) {
|
||||
minTimestamp = cacheEntry.timestamp
|
||||
}
|
||||
})
|
||||
this.updateLastCommunityRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: minTimestamp })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// clear timestamp if not all entries are present in the cache
|
||||
this.updateLastCommunityRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: '' })
|
||||
this.maybeLoadPostsForSubscriptionsFromRemote()
|
||||
},
|
||||
|
||||
|
@ -137,7 +153,7 @@ export default defineComponent({
|
|||
|
||||
this.updateSubscriptionPostsCacheByChannel({
|
||||
channelId: channel.id,
|
||||
posts: posts,
|
||||
posts: posts
|
||||
})
|
||||
|
||||
if (posts.length > 0) {
|
||||
|
@ -168,6 +184,7 @@ export default defineComponent({
|
|||
return posts
|
||||
}))).flatMap((o) => o)
|
||||
postList.push(...postListFromRemote)
|
||||
this.updateLastCommunityRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: new Date() })
|
||||
postList.sort((a, b) => {
|
||||
return calculatePublishedDate(b.publishedText) - calculatePublishedDate(a.publishedText)
|
||||
})
|
||||
|
@ -243,6 +260,7 @@ export default defineComponent({
|
|||
'updateShowProgressBar',
|
||||
'batchUpdateSubscriptionDetails',
|
||||
'updateSubscriptionPostsCacheByChannel',
|
||||
'updateLastCommunityRefreshTimestampByProfile'
|
||||
]),
|
||||
|
||||
...mapMutations([
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
:attempted-fetch="attemptedFetch"
|
||||
:is-community="true"
|
||||
:initial-data-limit="20"
|
||||
:last-refresh-timestamp="lastCommunityRefreshTimestamp"
|
||||
:title="$t('Global.Community')"
|
||||
@refresh="loadPostsForSubscriptionsFromRemote"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { defineComponent } from 'vue'
|
|||
import { mapActions, mapMutations } from 'vuex'
|
||||
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
|
||||
|
||||
import { setPublishedTimestampsInvidious, copyToClipboard, showToast } from '../../helpers/utils'
|
||||
import { setPublishedTimestampsInvidious, copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils'
|
||||
import { invidiousAPICall } from '../../helpers/api/invidious'
|
||||
import { getLocalChannelLiveStreams } from '../../helpers/api/local'
|
||||
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
|
||||
|
@ -70,6 +70,10 @@ export default defineComponent({
|
|||
fetchSubscriptionsAutomatically: function() {
|
||||
return this.$store.getters.getFetchSubscriptionsAutomatically
|
||||
},
|
||||
|
||||
lastLiveRefreshTimestamp: function () {
|
||||
return getRelativeTimeFromDate(this.$store.getters.getLastLiveRefreshTimestampByProfile(this.activeProfileId), true)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeProfile: async function (_) {
|
||||
|
@ -87,9 +91,20 @@ export default defineComponent({
|
|||
// This method is called on view visible
|
||||
if (this.videoCacheForAllActiveProfileChannelsPresent) {
|
||||
this.loadVideosFromCacheForAllActiveProfileChannels()
|
||||
if (this.cacheEntriesForAllActiveProfileChannels.length > 0) {
|
||||
let minTimestamp = null
|
||||
this.cacheEntriesForAllActiveProfileChannels.forEach((cacheEntry) => {
|
||||
if (!minTimestamp || cacheEntry.timestamp.getTime() < minTimestamp.getTime()) {
|
||||
minTimestamp = cacheEntry.timestamp
|
||||
}
|
||||
})
|
||||
this.updateLastLiveRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: minTimestamp })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// clear timestamp if not all entries are present in the cache
|
||||
this.updateLastLiveRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: '' })
|
||||
this.maybeLoadVideosForSubscriptionsFromRemote()
|
||||
},
|
||||
|
||||
|
@ -154,7 +169,7 @@ export default defineComponent({
|
|||
this.setProgressBarPercentage(percentageComplete)
|
||||
this.updateSubscriptionLiveCacheByChannel({
|
||||
channelId: channel.id,
|
||||
videos: videos,
|
||||
videos: videos
|
||||
})
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
|
@ -168,6 +183,7 @@ export default defineComponent({
|
|||
return videos
|
||||
}))).flatMap((o) => o)
|
||||
videoList.push(...videoListFromRemote)
|
||||
this.updateLastLiveRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: new Date() })
|
||||
|
||||
this.videoList = updateVideoListAfterProcessing(videoList)
|
||||
this.isLoading = false
|
||||
|
@ -382,6 +398,7 @@ export default defineComponent({
|
|||
'batchUpdateSubscriptionDetails',
|
||||
'updateShowProgressBar',
|
||||
'updateSubscriptionLiveCacheByChannel',
|
||||
'updateLastLiveRefreshTimestampByProfile'
|
||||
]),
|
||||
|
||||
...mapMutations([
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
:video-list="videoList"
|
||||
:error-channels="errorChannels"
|
||||
:attempted-fetch="attemptedFetch"
|
||||
:last-refresh-timestamp="lastLiveRefreshTimestamp"
|
||||
:title="$t('Global.Live')"
|
||||
@refresh="loadVideosForSubscriptionsFromRemote"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mapActions, mapMutations } from 'vuex'
|
|||
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
|
||||
|
||||
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
|
||||
import { copyToClipboard, showToast } from '../../helpers/utils'
|
||||
import { copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SubscriptionsShorts',
|
||||
|
@ -31,6 +31,10 @@ export default defineComponent({
|
|||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
lastShortRefreshTimestamp: function () {
|
||||
return getRelativeTimeFromDate(this.$store.getters.getLastShortRefreshTimestampByProfile(this.activeProfileId), true)
|
||||
},
|
||||
|
||||
activeProfile: function () {
|
||||
return this.$store.getters.getActiveProfile
|
||||
},
|
||||
|
@ -79,11 +83,23 @@ export default defineComponent({
|
|||
methods: {
|
||||
loadVideosFromCacheSometimes() {
|
||||
// This method is called on view visible
|
||||
|
||||
if (this.videoCacheForAllActiveProfileChannelsPresent) {
|
||||
this.loadVideosFromCacheForAllActiveProfileChannels()
|
||||
if (this.cacheEntriesForAllActiveProfileChannels.length > 0) {
|
||||
let minTimestamp = null
|
||||
this.cacheEntriesForAllActiveProfileChannels.forEach((cacheEntry) => {
|
||||
if (!minTimestamp || cacheEntry.timestamp.getTime() < minTimestamp.getTime()) {
|
||||
minTimestamp = cacheEntry.timestamp
|
||||
}
|
||||
})
|
||||
this.updateLastShortRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: minTimestamp })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// clear timestamp if not all entries are present in the cache
|
||||
this.updateLastShortRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: '' })
|
||||
this.maybeLoadVideosForSubscriptionsFromRemote()
|
||||
},
|
||||
|
||||
|
@ -131,7 +147,7 @@ export default defineComponent({
|
|||
this.setProgressBarPercentage(percentageComplete)
|
||||
this.updateSubscriptionShortsCacheByChannel({
|
||||
channelId: channel.id,
|
||||
videos: videos,
|
||||
videos: videos
|
||||
})
|
||||
|
||||
if (name) {
|
||||
|
@ -144,6 +160,7 @@ export default defineComponent({
|
|||
return videos
|
||||
}))).flatMap((o) => o)
|
||||
videoList.push(...videoListFromRemote)
|
||||
this.updateLastShortRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: new Date() })
|
||||
|
||||
this.videoList = updateVideoListAfterProcessing(videoList)
|
||||
this.isLoading = false
|
||||
|
@ -254,6 +271,7 @@ export default defineComponent({
|
|||
'batchUpdateSubscriptionDetails',
|
||||
'updateShowProgressBar',
|
||||
'updateSubscriptionShortsCacheByChannel',
|
||||
'updateLastShortRefreshTimestampByProfile'
|
||||
]),
|
||||
|
||||
...mapMutations([
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
:video-list="videoList"
|
||||
:error-channels="errorChannels"
|
||||
:attempted-fetch="attemptedFetch"
|
||||
:last-refresh-timestamp="lastShortRefreshTimestamp"
|
||||
:title="$t('Global.Shorts')"
|
||||
@refresh="loadVideosForSubscriptionsFromRemote"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -8,18 +8,6 @@
|
|||
color: var(--tertiary-text-color);
|
||||
}
|
||||
|
||||
.floatingTopButton {
|
||||
position: fixed;
|
||||
inset-block-start: 70px;
|
||||
inset-inline-end: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 350px) {
|
||||
.floatingTopButton {
|
||||
position: absolute
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 680px) {
|
||||
.card {
|
||||
inline-size: 90%;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { defineComponent } from 'vue'
|
|||
import FtLoader from '../ft-loader/ft-loader.vue'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtButton from '../ft-button/ft-button.vue'
|
||||
import FtIconButton from '../ft-icon-button/ft-icon-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 '../ft-element-list/ft-element-list.vue'
|
||||
import FtChannelBubble from '../ft-channel-bubble/ft-channel-bubble.vue'
|
||||
|
@ -15,7 +15,7 @@ export default defineComponent({
|
|||
'ft-loader': FtLoader,
|
||||
'ft-card': FtCard,
|
||||
'ft-button': FtButton,
|
||||
'ft-icon-button': FtIconButton,
|
||||
'ft-refresh-widget': FtRefreshWidget,
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-element-list': FtElementList,
|
||||
'ft-channel-bubble': FtChannelBubble,
|
||||
|
@ -45,6 +45,14 @@ export default defineComponent({
|
|||
initialDataLimit: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
lastRefreshTimestamp: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['refresh'],
|
||||
|
@ -72,7 +80,7 @@ export default defineComponent({
|
|||
|
||||
fetchSubscriptionsAutomatically: function() {
|
||||
return this.$store.getters.getFetchSubscriptionsAutomatically
|
||||
},
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
const dataLimit = sessionStorage.getItem('subscriptionLimit')
|
||||
|
|
|
@ -58,13 +58,10 @@
|
|||
/>
|
||||
</ft-flex-box>
|
||||
</ft-auto-load-next-page-wrapper>
|
||||
<ft-icon-button
|
||||
v-if="!isLoading && activeSubscriptionList.length > 0"
|
||||
:icon="['fas', 'sync']"
|
||||
class="floatingTopButton"
|
||||
:title="$t('Subscriptions.Refresh Subscriptions')"
|
||||
:size="12"
|
||||
theme="primary"
|
||||
<ft-refresh-widget
|
||||
:disable-refresh="isLoading || activeSubscriptionList.length === 0"
|
||||
:last-refresh-timestamp="lastRefreshTimestamp"
|
||||
:title="title"
|
||||
@click="$emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { defineComponent } from 'vue'
|
|||
import { mapActions, mapMutations } from 'vuex'
|
||||
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
|
||||
|
||||
import { setPublishedTimestampsInvidious, copyToClipboard, showToast } from '../../helpers/utils'
|
||||
import { setPublishedTimestampsInvidious, copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils'
|
||||
import { invidiousAPICall } from '../../helpers/api/invidious'
|
||||
import { getLocalChannelVideos } from '../../helpers/api/local'
|
||||
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
|
||||
|
@ -33,6 +33,14 @@ export default defineComponent({
|
|||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
currentLocale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
|
||||
lastVideoRefreshTimestamp: function () {
|
||||
return getRelativeTimeFromDate(this.$store.getters.getLastVideoRefreshTimestampByProfile(this.activeProfileId), true)
|
||||
},
|
||||
|
||||
useRssFeeds: function () {
|
||||
return this.$store.getters.getUseRssFeeds
|
||||
},
|
||||
|
@ -87,9 +95,20 @@ export default defineComponent({
|
|||
// This method is called on view visible
|
||||
if (this.videoCacheForAllActiveProfileChannelsPresent) {
|
||||
this.loadVideosFromCacheForAllActiveProfileChannels()
|
||||
if (this.cacheEntriesForAllActiveProfileChannels.length > 0) {
|
||||
let minTimestamp = null
|
||||
this.cacheEntriesForAllActiveProfileChannels.forEach((cacheEntry) => {
|
||||
if (!minTimestamp || cacheEntry.timestamp.getTime() < minTimestamp.getTime()) {
|
||||
minTimestamp = cacheEntry.timestamp
|
||||
}
|
||||
})
|
||||
this.updateLastVideoRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: minTimestamp })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// clear timestamp if not all entries are present in the cache
|
||||
this.updateLastVideoRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: '' })
|
||||
this.maybeLoadVideosForSubscriptionsFromRemote()
|
||||
},
|
||||
|
||||
|
@ -154,7 +173,7 @@ export default defineComponent({
|
|||
this.setProgressBarPercentage(percentageComplete)
|
||||
this.updateSubscriptionVideosCacheByChannel({
|
||||
channelId: channel.id,
|
||||
videos: videos,
|
||||
videos: videos
|
||||
})
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
|
@ -168,6 +187,7 @@ export default defineComponent({
|
|||
return videos
|
||||
}))).flatMap((o) => o)
|
||||
videoList.push(...videoListFromRemote)
|
||||
this.updateLastVideoRefreshTimestampByProfile({ profileId: this.activeProfileId, timestamp: new Date() })
|
||||
|
||||
this.videoList = updateVideoListAfterProcessing(videoList)
|
||||
this.isLoading = false
|
||||
|
@ -380,6 +400,7 @@ export default defineComponent({
|
|||
'batchUpdateSubscriptionDetails',
|
||||
'updateShowProgressBar',
|
||||
'updateSubscriptionVideosCacheByChannel',
|
||||
'updateLastVideoRefreshTimestampByProfile'
|
||||
]),
|
||||
|
||||
...mapMutations([
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
:is-loading="isLoading"
|
||||
:video-list="videoList"
|
||||
:error-channels="errorChannels"
|
||||
:last-refresh-timestamp="lastVideoRefreshTimestamp"
|
||||
:attempted-fetch="attemptedFetch"
|
||||
:title="$t('Global.Videos')"
|
||||
@refresh="loadVideosForSubscriptionsFromRemote"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -10,6 +10,11 @@ import router from '../router/index'
|
|||
export const CHANNEL_HANDLE_REGEX = /^@[\w.-]{3,30}$/
|
||||
|
||||
const PUBLISHED_TEXT_REGEX = /(\d+)\s?([a-z]+)/i
|
||||
|
||||
function currentLocale () {
|
||||
return i18n.locale.replace('_', '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} publishedText
|
||||
* @param {boolean} isLive
|
||||
|
@ -52,6 +57,7 @@ export function calculatePublishedDate(publishedText, isLive = false, isUpcoming
|
|||
} else if (timeFrame.startsWith('week') || timeFrame === 'w') {
|
||||
timeSpan = timeAmount * 604800000
|
||||
} else if (timeFrame.startsWith('month') || timeFrame === 'mo') {
|
||||
// 30 day month being used
|
||||
timeSpan = timeAmount * 2592000000
|
||||
} else if (timeFrame.startsWith('year') || timeFrame === 'y') {
|
||||
timeSpan = timeAmount * 31556952000
|
||||
|
@ -715,6 +721,57 @@ export function getTodayDateStrLocalTimezone() {
|
|||
return timeNowStr.split('T')[0]
|
||||
}
|
||||
|
||||
export function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDayMonths = true) {
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const now = new Date().getTime()
|
||||
// Convert from ms to second
|
||||
// For easier code interpretation the value is made to be positive
|
||||
// `comparisonDate` is sometimes a string
|
||||
const comparisonDate = Date.parse(date)
|
||||
let timeDiffFromNow = ((now - comparisonDate) / 1000)
|
||||
let timeUnit = 'second'
|
||||
|
||||
if (timeDiffFromNow < 60 && hideSeconds) {
|
||||
return i18n.t('Moments Ago')
|
||||
}
|
||||
|
||||
if (timeDiffFromNow >= 60) {
|
||||
timeDiffFromNow /= 60
|
||||
timeUnit = 'minute'
|
||||
}
|
||||
|
||||
if (timeUnit === 'minute' && timeDiffFromNow >= 60) {
|
||||
timeDiffFromNow /= 60
|
||||
timeUnit = 'hour'
|
||||
}
|
||||
|
||||
if (timeUnit === 'hour' && timeDiffFromNow >= 24) {
|
||||
timeDiffFromNow /= 24
|
||||
timeUnit = 'day'
|
||||
}
|
||||
|
||||
/* Different months might have a different number of days.
|
||||
In some contexts, to ensure the display is fine, we use 31.
|
||||
In other contexts, like when working with calculatePublishedDate, we use 30. */
|
||||
const daysInMonth = useThirtyDayMonths ? 30 : 31
|
||||
if (timeUnit === 'day' && timeDiffFromNow >= daysInMonth) {
|
||||
timeDiffFromNow /= daysInMonth
|
||||
timeUnit = 'month'
|
||||
}
|
||||
|
||||
if (timeUnit === 'month' && timeDiffFromNow >= 12) {
|
||||
timeDiffFromNow /= 12
|
||||
timeUnit = 'year'
|
||||
}
|
||||
|
||||
// Using `Math.ceil` so that -1.x days ago displayed as 1 day ago
|
||||
// Notice that the value is turned to negative to be displayed as "ago"
|
||||
return new Intl.RelativeTimeFormat([currentLocale(), 'en']).format(Math.ceil(-timeDiffFromNow), timeUnit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML tags to avoid XSS
|
||||
* @param {string} untrusted
|
||||
|
|
|
@ -69,19 +69,21 @@ const actions = {
|
|||
}
|
||||
|
||||
const mutations = {
|
||||
updateVideoCacheByChannel(state, { channelId, videos }) {
|
||||
updateVideoCacheByChannel(state, { channelId, videos, timestamp = new Date() }) {
|
||||
const existingObject = state.videoCache[channelId]
|
||||
const newObject = existingObject ?? { videos: null }
|
||||
if (videos != null) { newObject.videos = videos }
|
||||
newObject.timestamp = timestamp
|
||||
state.videoCache[channelId] = newObject
|
||||
},
|
||||
clearVideoCache(state) {
|
||||
state.videoCache = {}
|
||||
},
|
||||
updateShortsCacheByChannel(state, { channelId, videos }) {
|
||||
updateShortsCacheByChannel(state, { channelId, videos, timestamp = new Date() }) {
|
||||
const existingObject = state.shortsCache[channelId]
|
||||
const newObject = existingObject ?? { videos: null }
|
||||
if (videos != null) { newObject.videos = videos }
|
||||
newObject.timestamp = timestamp
|
||||
state.shortsCache[channelId] = newObject
|
||||
},
|
||||
updateShortsCacheWithChannelPageShorts(state, { channelId, videos }) {
|
||||
|
@ -112,19 +114,21 @@ const mutations = {
|
|||
clearShortsCache(state) {
|
||||
state.shortsCache = {}
|
||||
},
|
||||
updateLiveCacheByChannel(state, { channelId, videos }) {
|
||||
updateLiveCacheByChannel(state, { channelId, videos, timestamp = new Date() }) {
|
||||
const existingObject = state.liveCache[channelId]
|
||||
const newObject = existingObject ?? { videos: null }
|
||||
if (videos != null) { newObject.videos = videos }
|
||||
newObject.timestamp = timestamp
|
||||
state.liveCache[channelId] = newObject
|
||||
},
|
||||
clearLiveCache(state) {
|
||||
state.liveCache = {}
|
||||
},
|
||||
updatePostsCacheByChannel(state, { channelId, posts }) {
|
||||
updatePostsCacheByChannel(state, { channelId, posts, timestamp = new Date() }) {
|
||||
const existingObject = state.postsCache[channelId]
|
||||
const newObject = existingObject ?? { posts: null }
|
||||
if (posts != null) { newObject.posts = posts }
|
||||
newObject.timestamp = timestamp
|
||||
state.postsCache[channelId] = newObject
|
||||
},
|
||||
clearPostsCache(state) {
|
||||
|
|
|
@ -48,7 +48,13 @@ const state = {
|
|||
},
|
||||
externalPlayerNames: [],
|
||||
externalPlayerValues: [],
|
||||
externalPlayerCmdArguments: {}
|
||||
externalPlayerCmdArguments: {},
|
||||
lastVideoRefreshTimestampByProfile: {},
|
||||
lastShortRefreshTimestampByProfile: {},
|
||||
lastLiveRefreshTimestampByProfile: {},
|
||||
lastCommunityRefreshTimestampByProfile: {},
|
||||
lastPopularRefreshTimestamp: '',
|
||||
lastTrendingRefreshTimestamp: '',
|
||||
}
|
||||
|
||||
const getters = {
|
||||
|
@ -138,6 +144,30 @@ const getters = {
|
|||
|
||||
getExternalPlayerCmdArguments () {
|
||||
return state.externalPlayerCmdArguments
|
||||
},
|
||||
|
||||
getLastTrendingRefreshTimestamp() {
|
||||
return state.lastTrendingRefreshTimestamp
|
||||
},
|
||||
|
||||
getLastPopularRefreshTimestamp() {
|
||||
return state.lastPopularRefreshTimestamp
|
||||
},
|
||||
|
||||
getLastCommunityRefreshTimestampByProfile: (state) => (profileId) => {
|
||||
return state.lastCommunityRefreshTimestampByProfile[profileId]
|
||||
},
|
||||
|
||||
getLastShortRefreshTimestampByProfile: (state) => (profileId) => {
|
||||
return state.lastShortRefreshTimestampByProfile[profileId]
|
||||
},
|
||||
|
||||
getLastLiveRefreshTimestampByProfile: (state) => (profileId) => {
|
||||
return state.lastLiveRefreshTimestampByProfile[profileId]
|
||||
},
|
||||
|
||||
getLastVideoRefreshTimestampByProfile: (state) => (profileId) => {
|
||||
return state.lastVideoRefreshTimestampByProfile[profileId]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -732,6 +762,22 @@ const actions = {
|
|||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send(IpcChannels.OPEN_IN_EXTERNAL_PLAYER, { executable, args })
|
||||
}
|
||||
},
|
||||
|
||||
updateLastCommunityRefreshTimestampByProfile ({ commit }, payload) {
|
||||
commit('updateLastCommunityRefreshTimestampByProfile', payload)
|
||||
},
|
||||
|
||||
updateLastShortRefreshTimestampByProfile ({ commit }, payload) {
|
||||
commit('updateLastShortRefreshTimestampByProfile', payload)
|
||||
},
|
||||
|
||||
updateLastLiveRefreshTimestampByProfile ({ commit }, payload) {
|
||||
commit('updateLastLiveRefreshTimestampByProfile', payload)
|
||||
},
|
||||
|
||||
updateLastVideoRefreshTimestampByProfile ({ commit }, payload) {
|
||||
commit('updateLastVideoRefreshTimestampByProfile', payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -824,6 +870,30 @@ const mutations = {
|
|||
state.trendingCache[page] = value
|
||||
},
|
||||
|
||||
setLastTrendingRefreshTimestamp (state, timestamp) {
|
||||
state.lastTrendingRefreshTimestamp = timestamp
|
||||
},
|
||||
|
||||
setLastPopularRefreshTimestamp (state, timestamp) {
|
||||
state.lastPopularRefreshTimestamp = timestamp
|
||||
},
|
||||
|
||||
updateLastCommunityRefreshTimestampByProfile (state, { profileId, timestamp }) {
|
||||
vueSet(state.lastCommunityRefreshTimestampByProfile, profileId, timestamp)
|
||||
},
|
||||
|
||||
updateLastShortRefreshTimestampByProfile (state, { profileId, timestamp }) {
|
||||
vueSet(state.lastShortRefreshTimestampByProfile, profileId, timestamp)
|
||||
},
|
||||
|
||||
updateLastLiveRefreshTimestampByProfile (state, { profileId, timestamp }) {
|
||||
vueSet(state.lastLiveRefreshTimestampByProfile, profileId, timestamp)
|
||||
},
|
||||
|
||||
updateLastVideoRefreshTimestampByProfile (state, { profileId, timestamp }) {
|
||||
vueSet(state.lastVideoRefreshTimestampByProfile, profileId, timestamp)
|
||||
},
|
||||
|
||||
clearTrendingCache(state) {
|
||||
state.trendingCache = {
|
||||
default: null,
|
||||
|
|
|
@ -4,18 +4,6 @@
|
|||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.floatingTopButton {
|
||||
position: fixed;
|
||||
inset-block-start: 70px;
|
||||
inset-inline-end: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 350px) {
|
||||
.floatingTopButton {
|
||||
position: absolute
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 680px) {
|
||||
.card {
|
||||
inline-size: 90%;
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import { mapMutations } from 'vuex'
|
||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
||||
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
||||
import FtRefreshWidget from '../../components/ft-refresh-widget/ft-refresh-widget.vue'
|
||||
|
||||
import { invidiousAPICall } from '../../helpers/api/invidious'
|
||||
import { copyToClipboard, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
|
||||
import { copyToClipboard, getRelativeTimeFromDate, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Popular',
|
||||
|
@ -13,7 +15,8 @@ export default defineComponent({
|
|||
'ft-loader': FtLoader,
|
||||
'ft-card': FtCard,
|
||||
'ft-element-list': FtElementList,
|
||||
'ft-icon-button': FtIconButton
|
||||
'ft-icon-button': FtIconButton,
|
||||
'ft-refresh-widget': FtRefreshWidget,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
@ -22,6 +25,9 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
lastPopularRefreshTimestamp: function () {
|
||||
return getRelativeTimeFromDate(this.$store.getters.getLastPopularRefreshTimestamp, true)
|
||||
},
|
||||
popularCache: function () {
|
||||
return this.$store.getters.getPopularCache
|
||||
}
|
||||
|
@ -64,6 +70,7 @@ export default defineComponent({
|
|||
return item.type === 'video' || item.type === 'shortVideo' || item.type === 'channel' || item.type === 'playlist'
|
||||
})
|
||||
setPublishedTimestampsInvidious(items.filter(item => item.type === 'video' || item.type === 'shortVideo'))
|
||||
this.setLastPopularRefreshTimestamp(new Date())
|
||||
|
||||
this.shownResults = items
|
||||
|
||||
|
@ -92,6 +99,10 @@ export default defineComponent({
|
|||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
...mapMutations([
|
||||
'setLastPopularRefreshTimestamp'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -13,12 +13,10 @@
|
|||
:data="shownResults"
|
||||
/>
|
||||
</ft-card>
|
||||
<ft-icon-button
|
||||
v-if="!isLoading"
|
||||
:icon="['fas', 'sync']"
|
||||
class="floatingTopButton"
|
||||
:size="12"
|
||||
theme="primary"
|
||||
<ft-refresh-widget
|
||||
:disable-refresh="isLoading"
|
||||
:last-refresh-timestamp="lastPopularRefreshTimestamp"
|
||||
:title="$t('Most Popular')"
|
||||
@click="fetchPopularInfo"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,6 @@
|
|||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.floatingTopButton {
|
||||
position: fixed;
|
||||
inset-block-start: 70px;
|
||||
inset-inline-end: 10px;
|
||||
}
|
||||
|
||||
.trendingInfoTabs {
|
||||
inline-size: 100%;
|
||||
display: grid;
|
||||
|
@ -38,12 +32,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 350px) {
|
||||
.floatingTopButton {
|
||||
position: absolute
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 680px) {
|
||||
.card {
|
||||
inline-size: 90%;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import { mapActions, mapMutations } from 'vuex'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
||||
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||
import FtRefreshWidget from '../../components/ft-refresh-widget/ft-refresh-widget.vue'
|
||||
|
||||
import { copyToClipboard, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
|
||||
import { copyToClipboard, getRelativeTimeFromDate, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
|
||||
import { getLocalTrending } from '../../helpers/api/local'
|
||||
import { invidiousAPICall } from '../../helpers/api/invidious'
|
||||
|
||||
|
@ -17,7 +18,8 @@ export default defineComponent({
|
|||
'ft-loader': FtLoader,
|
||||
'ft-element-list': FtElementList,
|
||||
'ft-icon-button': FtIconButton,
|
||||
'ft-flex-box': FtFlexBox
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-refresh-widget': FtRefreshWidget,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
@ -34,6 +36,9 @@ export default defineComponent({
|
|||
backendFallback: function () {
|
||||
return this.$store.getters.getBackendFallback
|
||||
},
|
||||
lastTrendingRefreshTimestamp: function () {
|
||||
return getRelativeTimeFromDate(this.$store.getters.getLastTrendingRefreshTimestamp, true)
|
||||
},
|
||||
region: function () {
|
||||
return this.$store.getters.getRegion.toUpperCase()
|
||||
},
|
||||
|
@ -90,6 +95,8 @@ export default defineComponent({
|
|||
} else {
|
||||
this.getTrendingInfoLocal()
|
||||
}
|
||||
|
||||
this.setLastTrendingRefreshTimestamp(new Date())
|
||||
},
|
||||
|
||||
getTrendingInfoLocal: async function () {
|
||||
|
@ -195,6 +202,10 @@ export default defineComponent({
|
|||
|
||||
...mapActions([
|
||||
'showOutlines'
|
||||
]),
|
||||
|
||||
...mapMutations([
|
||||
'setLastTrendingRefreshTimestamp'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -85,12 +85,10 @@
|
|||
:data="shownResults"
|
||||
/>
|
||||
</ft-card>
|
||||
<ft-icon-button
|
||||
v-if="!isLoading"
|
||||
:icon="['fas', 'sync']"
|
||||
class="floatingTopButton"
|
||||
:size="12"
|
||||
theme="primary"
|
||||
<ft-refresh-widget
|
||||
:disable-refresh="isLoading"
|
||||
:last-refresh-timestamp="lastTrendingRefreshTimestamp"
|
||||
:title="$t('Trending.Trending')"
|
||||
@click="getTrendingInfo(true)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -109,7 +109,6 @@ Subscriptions:
|
|||
Empty Channels: Your subscribed channels currently does not have any videos.
|
||||
'Getting Subscriptions. Please wait.': Getting Subscriptions. Please wait.
|
||||
Empty Posts: Your subscribed channels currently do not have any posts.
|
||||
Refresh Subscriptions: Refresh Subscriptions
|
||||
Load More Videos: Load More Videos
|
||||
Load More Posts: Load More Posts
|
||||
Subscriptions Tabs: Subscriptions Tabs
|
||||
|
@ -132,6 +131,9 @@ Trending:
|
|||
Movies: Movies
|
||||
Trending Tabs: Trending Tabs
|
||||
Most Popular: Most Popular
|
||||
Feed:
|
||||
Feed Last Updated: '{feedName} feed last updated: {date}'
|
||||
Refresh Feed: Refresh {subscriptionName}
|
||||
Playlists: Playlists
|
||||
User Playlists:
|
||||
Your Playlists: Your Playlists
|
||||
|
@ -1061,6 +1063,7 @@ Hashtag:
|
|||
Hashtag: Hashtag
|
||||
This hashtag does not currently have any videos: This hashtag does not currently
|
||||
have any videos
|
||||
Moments Ago: moments ago
|
||||
Yes: Yes
|
||||
No: No
|
||||
Ok: Ok
|
||||
|
|
Loading…
Reference in New Issue