mirror of https://github.com/FreeTubeApp/FreeTube
Merge branch 'development' into piped-support
This commit is contained in:
commit
ff715e9c88
|
@ -112,6 +112,8 @@ module.exports = {
|
|||
ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube']
|
||||
}
|
||||
],
|
||||
'vue/require-explicit-emits': 'error',
|
||||
'vue/no-unused-emit-declarations': 'error',
|
||||
},
|
||||
settings: {
|
||||
'vue-i18n': {
|
||||
|
|
|
@ -27,3 +27,5 @@ jobs:
|
|||
- run: yarn run lint
|
||||
# let's verify that webpack is able to package the project
|
||||
- run: yarn run pack
|
||||
# verify that webpack is able to package the project using the web config
|
||||
- run: yarn run pack:web
|
||||
|
|
|
@ -111,13 +111,14 @@ function startRenderer(callback) {
|
|||
|
||||
const server = new WebpackDevServer({
|
||||
static: {
|
||||
directory: path.join(process.cwd(), 'static'),
|
||||
directory: path.resolve(__dirname, '..', 'static'),
|
||||
watch: {
|
||||
ignored: [
|
||||
/(dashFiles|storyboards)\/*/,
|
||||
'/**/.DS_Store',
|
||||
]
|
||||
}
|
||||
},
|
||||
publicPath: '/static'
|
||||
},
|
||||
port
|
||||
}, compiler)
|
||||
|
|
|
@ -111,8 +111,8 @@ const config = {
|
|||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: isDevMode,
|
||||
__filename: isDevMode
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
plugins: [
|
||||
processLocalesPlugin,
|
||||
|
@ -151,6 +151,8 @@ const config = {
|
|||
alias: {
|
||||
vue$: 'vue/dist/vue.runtime.esm.js',
|
||||
|
||||
'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$': path.resolve(__dirname, '../src/datastores/handlers/electron.js'),
|
||||
|
||||
'youtubei.js$': 'youtubei.js/web',
|
||||
|
||||
// video.js's mpd-parser uses @xmldom/xmldom so that it can support both node and web browsers
|
||||
|
|
|
@ -108,8 +108,8 @@ const config = {
|
|||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: true,
|
||||
__filename: isDevMode,
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
|
@ -160,6 +160,8 @@ const config = {
|
|||
alias: {
|
||||
vue$: 'vue/dist/vue.runtime.esm.js',
|
||||
|
||||
'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$': path.resolve(__dirname, '../src/datastores/handlers/web.js'),
|
||||
|
||||
// video.js's mpd-parser uses @xmldom/xmldom so that it can support both node and web browsers
|
||||
// As FreeTube only runs in electron and web browsers, we can use the native DOMParser class, instead of the "polyfill"
|
||||
// https://caniuse.com/mdn-api_domparser
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
"target": 2.7
|
||||
},
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"DB_HANDLERS_ELECTRON_RENDERER_OR_WEB": [
|
||||
"src/datastores/handlers/electron",
|
||||
"src/datastores/handlers/web"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import db from '../index'
|
||||
import * as db from '../index'
|
||||
|
||||
class Settings {
|
||||
static find() {
|
||||
|
@ -192,13 +192,11 @@ function compactAllDatastores() {
|
|||
])
|
||||
}
|
||||
|
||||
const baseHandlers = {
|
||||
settings: Settings,
|
||||
history: History,
|
||||
profiles: Profiles,
|
||||
playlists: Playlists,
|
||||
export {
|
||||
Settings as settings,
|
||||
History as history,
|
||||
Profiles as profiles,
|
||||
Playlists as playlists,
|
||||
|
||||
compactAllDatastores,
|
||||
}
|
||||
|
||||
export default baseHandlers
|
||||
|
|
|
@ -205,11 +205,9 @@ class Playlists {
|
|||
}
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
settings: Settings,
|
||||
history: History,
|
||||
profiles: Profiles,
|
||||
playlists: Playlists
|
||||
export {
|
||||
Settings as settings,
|
||||
History as history,
|
||||
Profiles as profiles,
|
||||
Playlists as playlists
|
||||
}
|
||||
|
||||
export default handlers
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
let handlers
|
||||
if (process.env.IS_ELECTRON) {
|
||||
handlers = require('./electron').default
|
||||
} else {
|
||||
handlers = require('./web').default
|
||||
}
|
||||
|
||||
const DBSettingHandlers = handlers.settings
|
||||
const DBHistoryHandlers = handlers.history
|
||||
const DBProfileHandlers = handlers.profiles
|
||||
const DBPlaylistHandlers = handlers.playlists
|
||||
|
||||
export {
|
||||
DBSettingHandlers,
|
||||
DBHistoryHandlers,
|
||||
DBProfileHandlers,
|
||||
DBPlaylistHandlers
|
||||
}
|
||||
settings as DBSettingHandlers,
|
||||
history as DBHistoryHandlers,
|
||||
profiles as DBProfileHandlers,
|
||||
playlists as DBPlaylistHandlers
|
||||
} from 'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import baseHandlers from './base'
|
||||
import * as baseHandlers from './base'
|
||||
|
||||
// TODO: Syncing
|
||||
// Syncing on the web would involve a different implementation
|
||||
|
@ -118,11 +118,9 @@ class Playlists {
|
|||
}
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
settings: Settings,
|
||||
history: History,
|
||||
profiles: Profiles,
|
||||
playlists: Playlists
|
||||
export {
|
||||
Settings as settings,
|
||||
History as history,
|
||||
Profiles as profiles,
|
||||
Playlists as playlists
|
||||
}
|
||||
|
||||
export default handlers
|
||||
|
|
|
@ -22,10 +22,7 @@ if (process.env.IS_ELECTRON_MAIN) {
|
|||
dbPath = (dbName) => `${dbName}.db`
|
||||
}
|
||||
|
||||
const db = {}
|
||||
db.settings = new Datastore({ filename: dbPath('settings'), autoload: true })
|
||||
db.profiles = new Datastore({ filename: dbPath('profiles'), autoload: true })
|
||||
db.playlists = new Datastore({ filename: dbPath('playlists'), autoload: true })
|
||||
db.history = new Datastore({ filename: dbPath('history'), autoload: true })
|
||||
|
||||
export default db
|
||||
export const settings = new Datastore({ filename: dbPath('settings'), autoload: true })
|
||||
export const profiles = new Datastore({ filename: dbPath('profiles'), autoload: true })
|
||||
export const playlists = new Datastore({ filename: dbPath('playlists'), autoload: true })
|
||||
export const history = new Datastore({ filename: dbPath('history'), autoload: true })
|
||||
|
|
|
@ -7,7 +7,7 @@ import path from 'path'
|
|||
import cp from 'child_process'
|
||||
|
||||
import { IpcChannels, DBActions, SyncEvents } from '../constants'
|
||||
import baseHandlers from '../datastores/handlers/base'
|
||||
import * as baseHandlers from '../datastores/handlers/base'
|
||||
import { extractExpiryTimestamp, ImageCache } from './ImageCache'
|
||||
import { existsSync } from 'fs'
|
||||
import asyncFs from 'fs/promises'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -287,10 +287,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
checkExternalPlayer: async function () {
|
||||
const payload = {
|
||||
externalPlayer: this.externalPlayer
|
||||
}
|
||||
this.getExternalPlayerCmdArgumentsData(payload)
|
||||
this.getExternalPlayerCmdArgumentsData()
|
||||
},
|
||||
|
||||
handleUpdateBannerClick: function (response) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { defineComponent } from 'vue'
|
|||
|
||||
export default defineComponent({
|
||||
name: 'FtAutoLoadNextPageWrapper',
|
||||
emits: ['load-next-page'],
|
||||
computed: {
|
||||
generalAutoLoadMorePaginatedItemsEnabled() {
|
||||
return this.$store.getters.getGeneralAutoLoadMorePaginatedItemsEnabled
|
||||
|
|
|
@ -20,6 +20,7 @@ export default defineComponent({
|
|||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
data: function () {
|
||||
return {
|
||||
selected: false
|
||||
|
|
|
@ -16,6 +16,10 @@ export default defineComponent({
|
|||
type: Array,
|
||||
default: () => ['fas', 'ellipsis-v']
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'base'
|
||||
|
@ -61,6 +65,7 @@ export default defineComponent({
|
|||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
data: function () {
|
||||
return {
|
||||
dropdownShown: false,
|
||||
|
@ -87,6 +92,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
handleIconClick: function () {
|
||||
if (this.disabled) { return }
|
||||
if (this.forceDropdown || (this.dropdownOptions.length > 0)) {
|
||||
this.dropdownShown = !this.dropdownShown
|
||||
|
||||
|
@ -103,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',
|
||||
|
|
|
@ -53,6 +53,7 @@ export default defineComponent({
|
|||
default: (_) => ({ preferredName: '', icon: '' }),
|
||||
}
|
||||
},
|
||||
emits: ['already-exists', 'change', 'error-find-tag-info', 'invalid-name'],
|
||||
methods: {
|
||||
updateTags: async function (text, _e) {
|
||||
if (this.areChannelTags) {
|
||||
|
|
|
@ -64,6 +64,7 @@ export default defineComponent({
|
|||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['clear', 'click', 'input'],
|
||||
data: function () {
|
||||
let actionIcon = ['fas', 'search']
|
||||
if (this.forceActionButtonIconName !== null) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
copyToClipboard,
|
||||
formatDurationAsTimestamp,
|
||||
formatNumber,
|
||||
getRelativeTimeFromDate,
|
||||
openExternalLink,
|
||||
showToast,
|
||||
toDistractionFreeTitle,
|
||||
|
@ -84,6 +85,7 @@ export default defineComponent({
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['pause-player'],
|
||||
data: function () {
|
||||
return {
|
||||
id: '',
|
||||
|
@ -365,6 +367,10 @@ export default defineComponent({
|
|||
return this.historyEntryExists && !this.inHistory
|
||||
},
|
||||
|
||||
currentLocale: function () {
|
||||
return this.$i18n.locale.replace('_', '-')
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
|
@ -482,14 +488,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
|
||||
},
|
||||
|
@ -688,48 +686,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export default defineComponent({
|
|||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
handleClick: function (response) {
|
||||
this.$emit('click', response)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -25,6 +25,7 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['selected'],
|
||||
data: function () {
|
||||
return {
|
||||
title: '',
|
||||
|
|
|
@ -32,6 +32,7 @@ export default defineComponent({
|
|||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['new-profile-created', 'profile-deleted'],
|
||||
data: function () {
|
||||
return {
|
||||
showDeletePrompt: false,
|
||||
|
|
|
@ -38,6 +38,7 @@ export default defineComponent({
|
|||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
data: function () {
|
||||
return {
|
||||
promptButtons: [],
|
||||
|
|
|
@ -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" />
|
|
@ -8,6 +8,7 @@ export default defineComponent({
|
|||
'ft-flex-box': FtFlexBox,
|
||||
'ft-radio-button': FtRadioButton
|
||||
},
|
||||
emits: ['filterValueUpdated'],
|
||||
data: function () {
|
||||
return {
|
||||
sortByValues: [
|
||||
|
|
|
@ -8,6 +8,7 @@ export default defineComponent({
|
|||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['timestamp-event'],
|
||||
methods: {
|
||||
catchTimestampClick: function (event) {
|
||||
this.$emit('timestamp-event', event.detail)
|
||||
|
|
|
@ -117,6 +117,7 @@ export default defineComponent({
|
|||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['ended', 'error', 'ready', 'store-caption-list', 'timeupdate', 'toggle-theatre-mode'],
|
||||
data: function () {
|
||||
return {
|
||||
powerSaveBlocker: null,
|
||||
|
|
|
@ -97,6 +97,7 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['enter-edit-mode', 'exit-edit-mode', 'search-video-query-change'],
|
||||
data: function () {
|
||||
return {
|
||||
searchVideoMode: false,
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
@ -53,6 +53,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 }
|
||||
|
@ -73,22 +78,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()
|
||||
},
|
||||
|
||||
|
@ -141,7 +157,7 @@ export default defineComponent({
|
|||
|
||||
this.updateSubscriptionPostsCacheByChannel({
|
||||
channelId: channel.id,
|
||||
posts: posts,
|
||||
posts: posts
|
||||
})
|
||||
|
||||
if (posts.length > 0) {
|
||||
|
@ -172,6 +188,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)
|
||||
})
|
||||
|
@ -247,6 +264,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'
|
||||
|
@ -74,6 +74,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 (_) {
|
||||
|
@ -91,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.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()
|
||||
},
|
||||
|
||||
|
@ -158,7 +173,7 @@ export default defineComponent({
|
|||
this.setProgressBarPercentage(percentageComplete)
|
||||
this.updateSubscriptionLiveCacheByChannel({
|
||||
channelId: channel.id,
|
||||
videos: videos,
|
||||
videos: videos
|
||||
})
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
|
@ -172,6 +187,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
|
||||
|
@ -386,6 +402,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',
|
||||
|
@ -35,6 +35,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
|
||||
},
|
||||
|
@ -83,11 +87,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()
|
||||
},
|
||||
|
||||
|
@ -135,7 +151,7 @@ export default defineComponent({
|
|||
this.setProgressBarPercentage(percentageComplete)
|
||||
this.updateSubscriptionShortsCacheByChannel({
|
||||
channelId: channel.id,
|
||||
videos: videos,
|
||||
videos: videos
|
||||
})
|
||||
|
||||
if (name) {
|
||||
|
@ -148,6 +164,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
|
||||
|
@ -258,6 +275,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,8 +45,17 @@ export default defineComponent({
|
|||
initialDataLimit: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
lastRefreshTimestamp: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['refresh'],
|
||||
data: function () {
|
||||
return {
|
||||
dataLimit: 100,
|
||||
|
@ -71,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'
|
||||
|
@ -37,6 +37,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
|
||||
},
|
||||
|
@ -91,9 +99,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()
|
||||
},
|
||||
|
||||
|
@ -158,7 +177,7 @@ export default defineComponent({
|
|||
this.setProgressBarPercentage(percentageComplete)
|
||||
this.updateSubscriptionVideosCacheByChannel({
|
||||
channelId: channel.id,
|
||||
videos: videos,
|
||||
videos: videos
|
||||
})
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
|
@ -172,6 +191,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
|
||||
|
@ -384,6 +404,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>
|
||||
|
|
|
@ -16,6 +16,7 @@ export default defineComponent({
|
|||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['timestamp-event'],
|
||||
data: function () {
|
||||
return {
|
||||
showChapters: false,
|
||||
|
|
|
@ -37,6 +37,7 @@ export default defineComponent({
|
|||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['timestamp-event'],
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
|
|
|
@ -19,6 +19,7 @@ export default defineComponent({
|
|||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['timestamp-event'],
|
||||
data: function () {
|
||||
return {
|
||||
shownDescription: ''
|
||||
|
|
|
@ -104,6 +104,7 @@ export default defineComponent({
|
|||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['set-info-area-sticky', 'scroll-to-info-area', 'pause-player'],
|
||||
computed: {
|
||||
hideSharingActions: function() {
|
||||
return this.$store.getters.getHideSharingActions
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -36,14 +36,14 @@ export async function loadLocale(locale) {
|
|||
|
||||
// locales are only compressed in our production Electron builds
|
||||
if (process.env.IS_ELECTRON && process.env.NODE_ENV !== 'development') {
|
||||
const { readFile } = require('fs/promises')
|
||||
const { promisify } = require('util')
|
||||
const { brotliDecompress } = require('zlib')
|
||||
const brotliDecompressAsync = promisify(brotliDecompress)
|
||||
try {
|
||||
// decompress brotli compressed json file and then load it
|
||||
// eslint-disable-next-line n/no-path-concat
|
||||
const compressed = await readFile(`${__dirname}/static/locales/${locale}.json.br`)
|
||||
const url = createWebURL(`/static/locales/${locale}.json.br`)
|
||||
const compressed = await (await fetch(url)).arrayBuffer()
|
||||
|
||||
const decompressed = await brotliDecompressAsync(compressed)
|
||||
const data = JSON.parse(decompressed.toString())
|
||||
i18n.setLocaleMessage(locale, data)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import fs from 'fs/promises'
|
||||
import { createWebURL, fetchWithTimeout } from '../../helpers/utils'
|
||||
|
||||
const state = {
|
||||
|
@ -42,14 +41,11 @@ const actions = {
|
|||
|
||||
// If the invidious instance fetch isn't returning anything interpretable
|
||||
if (instances.length === 0) {
|
||||
// Fallback: read from static file
|
||||
const fileName = 'invidious-instances.json'
|
||||
/* eslint-disable-next-line n/no-path-concat */
|
||||
const fileLocation = process.env.NODE_ENV === 'development' ? './static/' : `${__dirname}/static/`
|
||||
const filePath = `${fileLocation}${fileName}`
|
||||
console.warn('reading static file for invidious instances')
|
||||
const fileData = process.env.IS_ELECTRON ? await fs.readFile(filePath, 'utf8') : await (await fetch(createWebURL(filePath))).text()
|
||||
instances = JSON.parse(fileData).filter(e => {
|
||||
const url = createWebURL('/static/invidious-instances.json')
|
||||
|
||||
const fileData = await (await fetch(url)).json()
|
||||
instances = fileData.filter(e => {
|
||||
return process.env.SUPPORTS_LOCAL_API || e.cors
|
||||
}).map(e => {
|
||||
return e.url
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,11 +384,10 @@ const actions = {
|
|||
|
||||
async getRegionData ({ commit }, { locale }) {
|
||||
const localePathExists = process.env.GEOLOCATION_NAMES.includes(locale)
|
||||
// Exclude __dirname from path if not in electron
|
||||
const fileLocation = `${process.env.IS_ELECTRON ? process.env.NODE_ENV === 'development' ? '.' : __dirname : ''}/static/geolocations/`
|
||||
|
||||
const pathName = `${fileLocation}${localePathExists ? locale : 'en-US'}.json`
|
||||
const countries = process.env.IS_ELECTRON ? JSON.parse(await fs.readFile(pathName)) : await (await fetch(createWebURL(pathName))).json()
|
||||
const url = createWebURL(`/static/geolocations/${localePathExists ? locale : 'en-US'}.json`)
|
||||
|
||||
const countries = await (await fetch(url)).json()
|
||||
|
||||
const regionNames = countries.map((entry) => { return entry.name })
|
||||
const regionValues = countries.map((entry) => { return entry.code })
|
||||
|
@ -590,16 +619,9 @@ const actions = {
|
|||
commit('setSessionSearchHistory', [])
|
||||
},
|
||||
|
||||
async getExternalPlayerCmdArgumentsData ({ commit }, payload) {
|
||||
const fileName = 'external-player-map.json'
|
||||
/* eslint-disable-next-line n/no-path-concat */
|
||||
const fileLocation = process.env.NODE_ENV === 'development' ? './static/' : `${__dirname}/static/`
|
||||
|
||||
const fileData = await fs.readFile(`${fileLocation}${fileName}`)
|
||||
|
||||
const externalPlayerMap = JSON.parse(fileData).map((entry) => {
|
||||
return { name: entry.name, value: entry.value, cmdArguments: entry.cmdArguments }
|
||||
})
|
||||
async getExternalPlayerCmdArgumentsData ({ commit }) {
|
||||
const url = createWebURL('/static/external-player-map.json')
|
||||
const externalPlayerMap = await (await fetch(url)).json()
|
||||
// Sort external players alphabetically & case-insensitive, keep default entry at the top
|
||||
const playerNone = externalPlayerMap.shift()
|
||||
externalPlayerMap.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }))
|
||||
|
@ -732,6 +754,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 +862,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'
|
||||
import { getPipedTrending } from '../../helpers/api/piped'
|
||||
|
@ -18,7 +19,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 {
|
||||
|
@ -39,6 +41,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()
|
||||
},
|
||||
|
@ -99,6 +104,8 @@ export default defineComponent({
|
|||
} else {
|
||||
this.getTrendingInfoLocal()
|
||||
}
|
||||
|
||||
this.setLastTrendingRefreshTimestamp(new Date())
|
||||
},
|
||||
|
||||
getTrendingInfoLocal: async function () {
|
||||
|
@ -252,6 +259,10 @@ export default defineComponent({
|
|||
|
||||
...mapActions([
|
||||
'showOutlines'
|
||||
]),
|
||||
|
||||
...mapMutations([
|
||||
'setLastTrendingRefreshTimestamp'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -86,12 +86,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>
|
||||
|
|
|
@ -265,6 +265,9 @@ Settings:
|
|||
Ask Before Opening Link: اسأل قبل فتح الرابط
|
||||
Open Link: افتح الرابط
|
||||
External Link Handling: معالجة الارتباط الخارجي
|
||||
Auto Load Next Page:
|
||||
Label: تحميل تلقائي للصفحة التالية
|
||||
Tooltip: قم بتحميل الصفحات والتعليقات الإضافية تلقائيًا.
|
||||
Theme Settings:
|
||||
Theme Settings: 'إعدادات السِمة'
|
||||
Match Top Bar with Main Color: 'طابق الشريط العلوي مع اللون الأساسي'
|
||||
|
@ -950,6 +953,15 @@ Playlist:
|
|||
#* Published
|
||||
#& Views
|
||||
Playlist: قائمة التشغيل
|
||||
Sort By:
|
||||
Sort By: ترتيب حسب
|
||||
DateAddedNewest: آخر مضاف أولا
|
||||
DateAddedOldest: الأقدم إضافتا أولا
|
||||
AuthorAscending: الكاتب (A-Z)
|
||||
AuthorDescending: الكاتب (Z-A)
|
||||
VideoTitleAscending: العنوان (A-Z)
|
||||
VideoTitleDescending: العنوان (Z-A)
|
||||
Custom: مُخصّص
|
||||
Toggle Theatre Mode: 'تمكين وضع المسرح'
|
||||
Change Format:
|
||||
Change Media Formats: 'تغيير تنسيقات الفيديو'
|
||||
|
|
|
@ -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
|
||||
|
@ -1071,6 +1073,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
|
||||
|
|
|
@ -122,7 +122,7 @@ User Playlists:
|
|||
a la lista de reproducción «Favoritos».
|
||||
Search bar placeholder: Buscar listas de reproducción
|
||||
Empty Search Message: No hay vídeos en esta lista de reproducción que coincidan
|
||||
con tu búsqueda
|
||||
con su búsqueda
|
||||
Are you sure you want to remove all watched videos from this playlist? This cannot be undone: ¿Estás
|
||||
seguro de que quieres eliminar todos los vídeos vistos de esta lista de reproducción?
|
||||
Esto no se puede deshacer.
|
||||
|
@ -976,6 +976,15 @@ Playlist:
|
|||
#* Published
|
||||
#& Views
|
||||
Playlist: Lista de reproducción
|
||||
Sort By:
|
||||
Custom: Personalizado
|
||||
DateAddedNewest: Último añadido primero
|
||||
DateAddedOldest: Primeros añadidos primero
|
||||
AuthorAscending: Autor (A-Z)
|
||||
AuthorDescending: Autor (Z-A)
|
||||
VideoTitleAscending: Título (A-Z)
|
||||
Sort By: Ordenar por
|
||||
VideoTitleDescending: Título (Z-A)
|
||||
Toggle Theatre Mode: 'Activar modo cine'
|
||||
Change Format:
|
||||
Change Media Formats: 'Cambiar formato de vídeo'
|
||||
|
@ -1215,3 +1224,7 @@ Age Restricted:
|
|||
This video is age restricted: Este vídeo está restringido por edad
|
||||
checkmark: ✓
|
||||
Display Label: '{label}: {value}'
|
||||
Feed:
|
||||
Feed Last Updated: '{feedName} feed actualizado por última vez: {date}'
|
||||
Refresh Feed: Actualizar {subscriptionName}
|
||||
Moments Ago: hace unos instantes
|
||||
|
|
|
@ -266,6 +266,9 @@ Settings:
|
|||
Ask Before Opening Link: Pitaj prije otvaranja poveznice
|
||||
Open Link: Otvori poveznicu
|
||||
External Link Handling: Rukovanje vanjskim poveznicama
|
||||
Auto Load Next Page:
|
||||
Label: Automatski učitaj sljedeću stranicu
|
||||
Tooltip: Automatski učitaj dodatne stranice i komentare.
|
||||
Theme Settings:
|
||||
Theme Settings: 'Postavke teme'
|
||||
Match Top Bar with Main Color: 'Koristi glavnu boju u gornjoj traci'
|
||||
|
@ -961,6 +964,8 @@ Playlist:
|
|||
#* Published
|
||||
#& Views
|
||||
Playlist: Zbirka
|
||||
Sort By:
|
||||
Sort By: Redoslijed
|
||||
Toggle Theatre Mode: 'Uključi/isključi kazališni modus'
|
||||
Change Format:
|
||||
Change Media Formats: 'Promijeni videoformate'
|
||||
|
|
|
@ -1227,3 +1227,7 @@ Age Restricted:
|
|||
Close Banner: Chiudi banner
|
||||
checkmark: ✓
|
||||
Display Label: '{label}: {value}'
|
||||
Moments Ago: pochi istanti fa
|
||||
Feed:
|
||||
Feed Last Updated: 'Ultimo aggiornamento del feed {feedName}: {date}'
|
||||
Refresh Feed: Aggiorna {subscriptionName}
|
||||
|
|
|
@ -218,7 +218,7 @@ User Playlists:
|
|||
History:
|
||||
# On History Page
|
||||
History: 'Histórico'
|
||||
Watch History: 'Histórico de visualizações'
|
||||
Watch History: 'Histórico de exibição'
|
||||
Your history list is currently empty.: 'Seu histórico está vazio no momento.'
|
||||
Search bar placeholder: Pesquisar no histórico
|
||||
Empty Search Message: Não há vídeos em seu histórico que correspondam à sua pesquisa
|
||||
|
@ -436,15 +436,15 @@ Settings:
|
|||
#& No
|
||||
|
||||
Privacy Settings:
|
||||
Watch history has been cleared: Histórico de visualizações foi apagado
|
||||
Watch history has been cleared: O histórico de exibição foi apagado
|
||||
Are you sure you want to remove your entire watch history?: Tem certeza de que
|
||||
deseja remover todo o seu histórico de visualizações?
|
||||
Remove Watch History: Remover histórico
|
||||
deseja remover todo o seu histórico de exibição?
|
||||
Remove Watch History: Remover histórico de exibição
|
||||
Search cache has been cleared: Cache de pesquisas foi apagado
|
||||
Are you sure you want to clear out your search cache?: Tem certeza de que deseja
|
||||
limpar o cache de pesquisa?
|
||||
Clear Search Cache: Limpar cache de buscas
|
||||
Save Watched Progress: Salvar progresso de visualização
|
||||
Save Watched Progress: Habilitar o histórico de exibição
|
||||
Remember History: Lembrar histórico
|
||||
Privacy Settings: Configurações de privacidade
|
||||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: Tem
|
||||
|
@ -483,13 +483,13 @@ Settings:
|
|||
Unknown data key: Chave de dados desconhecida
|
||||
Unable to write file: O arquivo não pôde ser salvo
|
||||
Unable to read file: O arquivo não pôde ser lido
|
||||
All watched history has been successfully exported: Todo o histórico de visualizações
|
||||
foi exportado com sucesso
|
||||
All watched history has been successfully imported: Todo o histórico de visualizações
|
||||
foi importado com sucesso
|
||||
All watched history has been successfully exported: O histórico de exibição foi
|
||||
exportado com sucesso
|
||||
All watched history has been successfully imported: O histórico de exibição foi
|
||||
importado com sucesso
|
||||
History object has insufficient data, skipping item: O histórico tem dados insuficientes,
|
||||
pulando item
|
||||
This might take a while, please wait: Este processo pode demorar, por favor espere
|
||||
This might take a while, please wait: Este processo pode demorar, por favor aguarde
|
||||
Invalid subscriptions file: Arquivo de inscrições inválido
|
||||
All subscriptions and profiles have been successfully imported: Todas as inscrições
|
||||
e perfis foram importados com sucesso
|
||||
|
@ -1221,3 +1221,7 @@ Age Restricted:
|
|||
This video is age restricted: Este vídeo tem restrição de idade
|
||||
checkmark: ✓
|
||||
Display Label: '{label}: {value}'
|
||||
Feed:
|
||||
Feed Last Updated: '{feedName} última atualização do feed: {date}'
|
||||
Refresh Feed: Atualizar {subscriptionName}
|
||||
Moments Ago: momentos atrás
|
||||
|
|
Loading…
Reference in New Issue