Add:Player setting to scale elapsed time by playback speed #1028

- Moved player settings to new playerSettings key in storage preferences
This commit is contained in:
advplyr 2024-01-04 15:28:25 -06:00
parent 5e7d039454
commit 3fe943e989
20 changed files with 118 additions and 82 deletions

View file

@ -15,7 +15,7 @@
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
</div>
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div class="flex">
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
<div class="flex-grow" />
@ -66,17 +66,17 @@
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
<div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" />
</div>
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div>
</div>
@ -90,7 +90,7 @@
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor">
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': playerSettings.lockUi || !showFullscreen }" @touchstart="touchstartCursor">
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
</div>
</div>
@ -98,7 +98,7 @@
</div>
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
</div>
</template>
@ -146,9 +146,12 @@ export default {
touchStartY: 0,
touchStartTime: 0,
touchEndY: 0,
useChapterTrack: false,
useTotalTrack: true,
lockUi: false,
playerSettings: {
useChapterTrack: false,
useTotalTrack: true,
scaleElapsedTimeBySpeed: true,
lockUi: false
},
isLoading: false,
isDraggingCursor: false,
draggingTouchStartX: 0,
@ -164,7 +167,7 @@ export default {
showFullscreen(val) {
this.updateScreenSize()
this.$store.commit('setPlayerFullscreen', !!val)
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ""
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ''
},
bookCoverAspectRatio() {
this.updateScreenSize()
@ -187,17 +190,22 @@ export default {
{
text: this.$strings.LabelTotalTrack,
value: 'total_track',
icon: this.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
icon: this.playerSettings.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.$strings.LabelChapterTrack,
value: 'chapter_track',
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
icon: this.playerSettings.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
text: this.$strings.LabelScaleElapsedTimeBySpeed,
value: 'scale_elapsed_time',
icon: this.playerSettings.scaleElapsedTimeBySpeed ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.playerSettings.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
value: 'lock',
icon: this.lockUi ? 'lock' : 'lock_open'
icon: this.playerSettings.lockUi ? 'lock' : 'lock_open'
},
{
text: this.$strings.LabelClosePlayer,
@ -322,11 +330,14 @@ export default {
},
currentTimePretty() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
if (this.playerSettings.scaleElapsedTimeBySpeed) {
currentTimeToUse = currentTimeToUse / this.currentPlaybackRate
}
return this.$secondsToTimestamp(currentTimeToUse)
},
timeRemaining() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
var currChapTime = currentTimeToUse - this.currentChapter.start
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
}
@ -494,7 +505,7 @@ export default {
this.updateReadyTrack()
},
updateReadyTrack() {
if (this.useChapterTrack) {
if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalReadyTrack) {
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
}
@ -511,11 +522,14 @@ export default {
}
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
currentTime = Math.max(0, currentTime - this.currentChapter.start)
}
if (this.playerSettings.scaleElapsedTimeBySpeed) {
currentTime = currentTime / this.currentPlaybackRate
}
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
ts.innerText = this.$secondsToTimestamp(currentTime)
},
timeupdate() {
if (!this.$refs.playedTrack) {
@ -543,7 +557,7 @@ export default {
let bufferedPercent = this.bufferedTime / this.totalDuration
const totalBufferedPercent = bufferedPercent
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
const currChapTime = currentTimeToUse - this.currentChapter.start
percentDone = currChapTime / this.currentChapterDuration
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
@ -557,7 +571,7 @@ export default {
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
}
if (this.useChapterTrack) {
if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
}
@ -584,7 +598,7 @@ export default {
}
},
async touchstartCursor(e) {
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.playerSettings.lockUi) return
await this.$hapticsImpact()
this.isDraggingCursor = true
@ -670,7 +684,7 @@ export default {
let duration = this.totalDuration
let minTime = 0
let maxTime = duration
if (this.useChapterTrack && this.currentChapter) {
if (this.playerSettings.useChapterTrack && this.currentChapter) {
duration = this.currentChapterDuration
minTime = this.currentChapter.start
maxTime = minTime + duration
@ -690,37 +704,40 @@ export default {
if (action === 'history') {
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
this.showFullscreen = false
} else if (action === 'scale_elapsed_time') {
this.playerSettings.scaleElapsedTimeBySpeed = !this.playerSettings.scaleElapsedTimeBySpeed
this.updateTimestamp()
this.savePlayerSettings()
} else if (action === 'lock') {
this.lockUi = !this.lockUi
this.$localStore.setPlayerLock(this.lockUi)
this.playerSettings.lockUi = !this.playerSettings.lockUi
this.savePlayerSettings()
} else if (action === 'chapter_track') {
this.useChapterTrack = !this.useChapterTrack
this.useTotalTrack = !this.useChapterTrack || this.useTotalTrack
this.playerSettings.useChapterTrack = !this.playerSettings.useChapterTrack
this.playerSettings.useTotalTrack = !this.playerSettings.useChapterTrack || this.playerSettings.useTotalTrack
this.updateTimestamp()
this.updateTrack()
this.updateReadyTrack()
this.updateUseChapterTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack)
this.savePlayerSettings()
} else if (action === 'total_track') {
this.useTotalTrack = !this.useTotalTrack
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack
this.playerSettings.useTotalTrack = !this.playerSettings.useTotalTrack
this.playerSettings.useChapterTrack = !this.playerSettings.useTotalTrack || this.playerSettings.useChapterTrack
this.updateTimestamp()
this.updateTrack()
this.updateReadyTrack()
this.updateUseChapterTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack)
this.savePlayerSettings()
} else if (action === 'close') {
this.closePlayback()
}
})
},
updateUseChapterTrack() {
this.$localStore.setUseChapterTrack(this.useChapterTrack)
// Chapter track in NowPlaying only supported on iOS for now
if (this.$platform === 'ios') {
AbsAudioPlayer.setChapterTrack({ enabled: this.useChapterTrack })
AbsAudioPlayer.setChapterTrack({ enabled: this.playerSettings.useChapterTrack })
}
},
forceCloseDropdownMenu() {
@ -739,6 +756,30 @@ export default {
this.isLoading = false
this.playbackSession = null
},
async loadPlayerSettings() {
const savedPlayerSettings = await this.$localStore.getPlayerSettings()
if (!savedPlayerSettings) {
// In 0.9.72-beta 'useChapterTrack', 'useTotalTrack' and 'playerLock' was replaced with 'playerSettings' JSON object
// Check if this old key was set and if so migrate them over to 'playerSettings'
const chapterTrackPref = await this.$localStore.getPreferenceByKey('useChapterTrack')
if (chapterTrackPref) {
this.playerSettings.useChapterTrack = chapterTrackPref === '1'
const totalTrackPref = await this.$localStore.getPreferenceByKey('useTotalTrack')
this.playerSettings.useTotalTrack = totalTrackPref === '1'
const playerLockPref = await this.$localStore.getPreferenceByKey('playerLock')
this.playerSettings.lockUi = playerLockPref === '1'
}
this.savePlayerSettings()
} else {
this.playerSettings.useChapterTrack = !!savedPlayerSettings.useChapterTrack
this.playerSettings.useTotalTrack = !!savedPlayerSettings.useTotalTrack
this.playerSettings.lockUi = !!savedPlayerSettings.lockUi
this.playerSettings.scaleElapsedTimeBySpeed = !!savedPlayerSettings.scaleElapsedTimeBySpeed
}
},
savePlayerSettings() {
return this.$localStore.setPlayerSettings({ ...this.playerSettings })
},
//
// Listeners from audio AbsAudioPlayer
//
@ -805,9 +846,7 @@ export default {
this.updateTimestamp()
},
async init() {
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
this.useTotalTrack = await this.$localStore.getUseTotalTrack()
this.lockUi = await this.$localStore.getPlayerLock()
await this.loadPlayerSettings()
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)

View file

@ -40,7 +40,7 @@ export default {
default: '16px'
},
width: {
type: Number,
type: [String, Number],
default: 300
}
},

View file

@ -42,56 +42,20 @@ class LocalStorage {
}
}
async setUseChapterTrack(useChapterTrack) {
async setPlayerSettings(playerSettings) {
try {
await Preferences.set({ key: 'useChapterTrack', value: useChapterTrack ? '1' : '0' })
await Preferences.set({ key: 'playerSettings', value: JSON.stringify(playerSettings) })
} catch (error) {
console.error('[LocalStorage] Failed to set use chapter track', error)
console.error('[LocalStorage] Failed to set player settings', error)
}
}
async getUseChapterTrack() {
async getPlayerSettings() {
try {
var obj = await Preferences.get({ key: 'useChapterTrack' }) || {}
return obj.value === '1'
const playerSettingsObj = await Preferences.get({ key: 'playerSettings' }) || {}
return playerSettingsObj.value ? JSON.parse(playerSettingsObj.value) : null
} catch (error) {
console.error('[LocalStorage] Failed to get use chapter track', error)
return false
}
}
async setUseTotalTrack(useTotalTrack) {
try {
await Preferences.set({ key: 'useTotalTrack', value: useTotalTrack ? '1' : '0' })
} catch (error) {
console.error('[LocalStorage] Failed to set use total track', error)
}
}
async getUseTotalTrack() {
try {
var obj = await Preferences.get({ key: 'useTotalTrack' }) || {}
return obj.value === '1'
} catch (error) {
console.error('[LocalStorage] Failed to get use total track', error)
return false
}
}
async setPlayerLock(lock) {
try {
await Preferences.set({ key: 'playerLock', value: lock ? '1' : '0' })
} catch (error) {
console.error('[LocalStorage] Failed to set player lock', error)
}
}
async getPlayerLock() {
try {
var obj = await Preferences.get({ key: 'playerLock' }) || {}
return obj.value === '1'
} catch (error) {
console.error('[LocalStorage] Failed to get player lock', error)
console.error('[LocalStorage] Failed to get player settings', error)
return false
}
}
@ -179,6 +143,22 @@ class LocalStorage {
return false
}
}
/**
* Get preference value by key
*
* @param {string} key
* @returns {Promise<string>}
*/
async getPreferenceByKey(key) {
try {
const obj = await Preferences.get({ key }) || {}
return obj.value || null
} catch (error) {
console.error(`[LocalStorage] Failed to get preference "${key}"`, error)
return null
}
}
}

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
"LabelRSSFeedSlug": "RSS kanál Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sezóna",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Série",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sæson",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serie",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Staffel",
"LabelSelectADevice": "Wähle ein Gerät",
"LabelSeries": "Serien",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Series",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
"LabelRSSFeedPreventIndexing": "Prevenir Indexado",
"LabelRSSFeedSlug": "Fuente RSS Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Temporada",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Series",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedPreventIndexing": "Empêcher lindexation",
"LabelRSSFeedSlug": "Identificateur dadresse du Flux RSS ",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Saison",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Séries",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Series",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Series",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sezona",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serije",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Stagione",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serie",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas",
"LabelRSSFeedPreventIndexing": "Neleisti indeksuoti",
"LabelRSSFeedSlug": "RSS srauto identifikatorius",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sezonas",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serija",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
"LabelRSSFeedSlug": "RSS-feed slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Seizoen",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serie",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
"LabelRSSFeedPreventIndexing": "Forhindre indeksering",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sesong",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serier",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sezon",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serie",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
"LabelRSSFeedSlug": "Встроить RSS-канал",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Сезон",
"LabelSelectADevice": "Выбор девайса",
"LabelSeries": "Серия",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
"LabelRSSFeedSlug": "RSS-flödesslag",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Säsong",
"LabelSelectADevice": "Select a device",
"LabelSeries": "Serie",

View file

@ -194,6 +194,7 @@
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
"LabelRSSFeedPreventIndexing": "防止索引",
"LabelRSSFeedSlug": "RSS 源段",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "季",
"LabelSelectADevice": "选择设备",
"LabelSeries": "系列",