Newer
Older
Eliot Berriot
committed
<template>
<section class="ui inverted segment player-wrapper" :aria-label="labels.audioPlayer" :style="style">
<div class="player">
<div v-if="currentTrack" class="track-area ui unstackable items">
<div class="ui inverted item">
<div class="ui tiny image">
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover && currentTrack.album.cover.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.medium_square_crop)">
<img v-else src="../../assets/audio/default-cover.png">
Eliot Berriot
committed
</div>
<div class="middle aligned content">
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
{{ currentTrack.title }}
</router-link>
<div class="meta">
<router-link class="artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">
{{ currentTrack.artist.name }}
</router-link> /
<router-link class="album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">
{{ currentTrack.album.title }}
</router-link>
</div>
<div class="description">
<track-favorite-icon
v-if="$store.state.auth.authenticated"
Eliot Berriot
committed
:class="{'inverted': !$store.getters['favorites/isFavorite'](currentTrack.id)}"
:track="currentTrack"></track-favorite-icon>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
Eliot Berriot
committed
:class="['inverted']"
:track="currentTrack"></track-playlist-icon>
<button
v-if="$store.state.auth.authenticated"
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
:class="['ui', 'really', 'basic', 'circular', 'inverted', 'icon', 'button']"
:aria-label="labels.addArtistContentFilter"
:title="labels.addArtistContentFilter">
<i :class="['eye slash outline', 'basic', 'icon']"></i>
</button>
</div>
Eliot Berriot
committed
</div>
</div>
</div>
Eliot Berriot
committed
<div class="progress-area" v-if="currentTrack && !errored">
<div class="ui grid">
<div class="left floated four wide column">
<p class="timer start" @click="setCurrentTime(0)">{{currentTimeFormatted}}</p>
</div>
Eliot Berriot
committed
Eliot Berriot
committed
<div v-if="!isLoadingAudio" class="right floated four wide column">
<p class="timer total">{{durationFormatted}}</p>
</div>
</div>
Eliot Berriot
committed
<div
ref="progress"
:class="['ui', 'small', 'orange', 'inverted', {'indicating': isLoadingAudio}, 'progress']"
@click="touchProgress">
<div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div>
<div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
Eliot Berriot
committed
</div>
</div>
Eliot Berriot
committed
<div class="ui small warning message" v-if="currentTrack && errored">
<div class="header">
<translate translate-context="Sidebar/Player/Error message.Title">The track cannot be loaded</translate>
Eliot Berriot
committed
</div>
<p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
<translate translate-context="Sidebar/Player/Error message.Paragraph">The next track will play automatically in a few seconds…</translate>
Eliot Berriot
committed
<i class="loading spinner icon"></i>
</p>
<p>
<translate translate-context="Sidebar/Player/Error message.Paragraph">You may have a connectivity issue.</translate>
Eliot Berriot
committed
</p>
</div>
<div class="two wide column controls ui grid">
<span
role="button"
Eliot Berriot
committed
:aria-label="labels.previousTrack"
Eliot Berriot
committed
@click.prevent.stop="previous"
<i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'secondary', 'icon']"></i>
</span>
<span
role="button"
v-if="!playing"
Eliot Berriot
committed
:aria-label="labels.play"
@click.prevent.stop="togglePlay"
class="two wide column control">
Eliot Berriot
committed
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</span>
<span
role="button"
v-else
Eliot Berriot
committed
:aria-label="labels.pause"
@click.prevent.stop="togglePlay"
class="two wide column control">
Eliot Berriot
committed
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</span>
<span
role="button"
Eliot Berriot
committed
:aria-label="labels.next"
class="two wide column control"
Eliot Berriot
committed
@click.prevent.stop="next"
:disabled="!hasNext">
<i :class="['ui', {'disabled': !hasNext}, 'forward step', 'secondary', 'icon']" ></i>
</span>
<div
class="wide column control volume-control"
v-on:mouseover="showVolume = true"
v-on:mouseleave="showVolume = false"
v-bind:class="{ active : showVolume }">
<span
role="button"
Eliot Berriot
committed
v-if="volume === 0"
Eliot Berriot
committed
:aria-label="labels.unmute"
@click.prevent.stop="unmute">
<i class="volume off secondary icon"></i>
</span>
<span
role="button"
Eliot Berriot
committed
v-else-if="volume < 0.5"
Eliot Berriot
committed
:aria-label="labels.mute"
@click.prevent.stop="mute">
<i class="volume down secondary icon"></i>
</span>
<span
role="button"
Eliot Berriot
committed
v-else
Eliot Berriot
committed
:aria-label="labels.mute"
@click.prevent.stop="mute">
<i class="volume up secondary icon"></i>
</span>
<input
type="range"
step="0.05"
min="0"
max="1"
v-model="sliderVolume"
v-if="showVolume" />
</div>
<div class="two wide column control looping" v-if="!showVolume">
<span
role="button"
v-if="looping === 0"
Eliot Berriot
committed
:title="labels.loopingDisabled"
:aria-label="labels.loopingDisabled"
@click.prevent.stop="$store.commit('player/looping', 1)"
:disabled="!currentTrack">
<i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
</span>
<span
role="button"
Eliot Berriot
committed
@click.prevent.stop="$store.commit('player/looping', 2)"
Eliot Berriot
committed
:aria-label="labels.loopingSingle"
v-if="looping === 1"
Eliot Berriot
committed
:disabled="!currentTrack">
<i
class="repeat secondary icon">
<span class="ui circular tiny orange label">1</span>
</i>
</span>
<span
role="button"
Eliot Berriot
committed
:aria-label="labels.loopingWhole"
v-if="looping === 2"
:disabled="!currentTrack"
Eliot Berriot
committed
@click.prevent.stop="$store.commit('player/looping', 0)">
<i
class="repeat orange secondary icon">
</i>
</span>
</div>
<span
role="button"
:disabled="queue.tracks.length === 0"
Eliot Berriot
committed
:aria-label="labels.shuffle"
Eliot Berriot
committed
@click.prevent.stop="shuffle()"
class="two wide column control">
<div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div>
Eliot Berriot
committed
<i v-else :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</span>
<div class="one wide column" v-if="!showVolume"></div>
<span
role="button"
:disabled="queue.tracks.length === 0"
Eliot Berriot
committed
:aria-label="labels.clear"
Eliot Berriot
committed
@click.prevent.stop="clean()"
class="two wide column control">
Eliot Berriot
committed
<i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</span>
Eliot Berriot
committed
</div>
<GlobalEvents
@keydown.space.prevent.exact="togglePlay"
@keydown.ctrl.left.prevent.exact="previous"
@keydown.ctrl.right.prevent.exact="next"
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
@keydown.s.prevent.exact="shuffle"
/>
Eliot Berriot
committed
</div>
</section>
Eliot Berriot
committed
</template>
<script>
import { mapState, mapGetters, mapActions } from "vuex"
import GlobalEvents from "@/components/utils/global-events"
import ColorThief from "@/vendor/color-thief"
import { Howl } from "howler"
import $ from 'jquery'
import _ from '@/lodash'
import url from '@/utils/url'
import axios from 'axios'
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
Eliot Berriot
committed
export default {
components: {
Eliot Berriot
committed
},
data() {
let defaultAmbiantColors = [
[46, 46, 46],
[46, 46, 46],
[46, 46, 46],
[46, 46, 46]
]
Eliot Berriot
committed
return {
sliderVolume: this.volume,
defaultAmbiantColors: defaultAmbiantColors,
Eliot Berriot
committed
ambiantColors: defaultAmbiantColors,
currentSound: null,
dummyAudio: null,
isUpdatingTime: false,
sourceErrors: 0,
progressInterval: null,
maxPreloaded: 3,
preloadDelay: 15,
soundsCache: [],
Eliot Berriot
committed
soundId: null,
playTimeout: null
Eliot Berriot
committed
}
},
mounted() {
Howler.unload() // clear existing cache, if any
Eliot Berriot
committed
// we trigger the watcher explicitely it does not work otherwise
this.sliderVolume = this.volume
Eliot Berriot
committed
// this is needed to unlock audio playing under some browsers,
// cf https://github.com/goldfire/howler.js#mobilechrome-playback
// but we never actually load those audio files
this.dummyAudio = new Howl({
preload: false,
autoplay: false,
src: ["noop.webm", "noop.mp3"]
Eliot Berriot
committed
})
if (this.currentTrack) {
this.getSound(this.currentTrack)
}
Eliot Berriot
committed
},
destroyed() {
Eliot Berriot
committed
this.dummyAudio.unload()
Eliot Berriot
committed
},
methods: {
togglePlay: "player/togglePlay",
mute: "player/mute",
unmute: "player/unmute",
clean: "queue/clean",
async getTrackData (trackData) {
let data = null
if (!trackData.uploads.length || trackData.uploads.length === 0) {
// we don't have upload informations for this track, we need to fetch it
await axios.get(`tracks/${trackData.id}/`).then((response) => {
data = response.data
}, error => {
data = null
})
} else {
return trackData
}
if (data === null) {
return
}
return data
},
shuffle() {
let disabled = this.queue.tracks.length === 0
if (this.isShuffling || disabled) {
return
}
let self = this
let msg = this.$pgettext('Content/Queue/Message', "Queue shuffled!")
this.isShuffling = true
setTimeout(() => {
self.$store.dispatch("queue/shuffle", () => {
self.$store.commit("ui/addMessage", {
date: new Date()
})
})
}, 100)
},
next() {
this.$store.dispatch("queue/next").then(() => {
self.$emit("next")
previous() {
this.$store.dispatch("queue/previous").then(() => {
self.$emit("previous")
touchProgress(e) {
Eliot Berriot
committed
let time
let target = this.$refs.progress
time = (e.layerX / target.offsetWidth) * this.duration
},
updateBackground() {
// delete existing canvas, if any
$('canvas.color-thief').remove()
if (!this.currentTrack.album.cover) {
this.ambiantColors = this.defaultAmbiantColors
return
}
let image = this.$refs.cover
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
Eliot Berriot
committed
},
handleError({ sound, error }) {
this.$store.commit("player/isLoadingAudio", false)
this.$store.dispatch("player/trackErrored")
},
getSound (trackData) {
let cached = this.getSoundFromCache(trackData)
if (cached) {
return cached.sound
}
let srcs = this.getSrcs(trackData)
let self = this
let sound = new Howl({
src: srcs.map((s) => { return s.url }),
format: srcs.map((s) => { return s.type }),
autoplay: false,
loop: false,
html5: true,
preload: true,
volume: this.volume,
onend: function () {
self.ended()
},
onunlock: function () {
if (this.$store.state.player.playing) {
}
},
onload: function () {
let sound = this
let node = this._sounds[0]._node;
node.addEventListener('progress', () => {
if (sound != self.currentSound) {
return
}
self.updateBuffer(node)
})
},
onplay: function () {
self.$store.commit('player/isLoadingAudio', false)
self.$store.commit('player/resetErrorCount')
self.$store.commit('player/errored', false)
self.$store.commit('player/duration', this.duration())
},
onloaderror: function (sound, error) {
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
if (this != self.currentSound) {
return
}
console.log('Error while playing:', sound, error)
self.handleError({sound, error})
},
})
this.addSoundToCache(sound, trackData)
return sound
},
getSrcs: function (trackData) {
let sources = trackData.uploads.map(u => {
return {
type: u.extension,
url: this.$store.getters['instance/absoluteUrl'](u.listen_url),
}
})
// We always add a transcoded MP3 src at the end
// because transcoding is expensive, but we want browsers that do
// not support other codecs to be able to play it :)
sources.push({
type: 'mp3',
url: url.updateQueryString(
this.$store.getters['instance/absoluteUrl'](trackData.listen_url),
'to',
'mp3'
)
})
if (this.$store.state.auth.authenticated) {
// we need to send the token directly in url
// so authentication can be checked by the backend
// because for audio files we cannot use the regular Authentication
// header
sources.forEach(e => {
e.url = url.updateQueryString(e.url, 'jwt', this.$store.state.auth.token)
})
}
return sources
},
updateBuffer (node) {
// from https://github.com/goldfire/howler.js/issues/752#issuecomment-372083163
let range = 0;
let bf = node.buffered;
let time = node.currentTime;
try {
while(!(bf.start(range) <= time && time <= bf.end(range))) {
range += 1;
}
} catch (IndexSizeError) {
return
}
let loadPercentage
let start = bf.start(range)
let end = bf.end(range)
if (range === 0) {
// easy case, no user-seek
let loadStartPercentage = start / node.duration;
let loadEndPercentage = end / node.duration;
loadPercentage = loadEndPercentage - loadStartPercentage;
} else {
let loaded = end - start
let remainingToLoad = node.duration - start
// user seeked a specific position in the audio, our progress must be
// computed based on the remaining portion of the track
loadPercentage = loaded / remainingToLoad;
}
if (loadPercentage * 100 === this.bufferProgress) {
return
}
this.$store.commit('player/bufferProgress', loadPercentage * 100)
},
updateProgress: function () {
this.isUpdatingTime = true
if (this.currentSound && this.currentSound.state() === 'loaded') {
let t = this.currentSound.seek()
let d = this.currentSound.duration()
this.$store.dispatch('player/updateProgress', t)
this.updateBuffer(this.currentSound._sounds[0]._node)
let toPreload = this.$store.state.queue.tracks[this.currentIndex + 1]
if (toPreload && !this.getSoundFromCache(toPreload) && (t > this.preloadDelay || d - t < 30)) {
this.getSound(toPreload)
}
}
},
observeProgress: function (enable) {
let self = this
if (enable) {
if (self.progressInterval) {
clearInterval(self.progressInterval)
}
self.progressInterval = setInterval(() => {
self.updateProgress()
}, 1000)
} else {
clearInterval(self.progressInterval)
}
},
setCurrentTime (t) {
if (t < 0 | t > this.duration) {
return
}
if (!this.currentSound || !this.currentSound._sounds[0]) {
return
}
if (t === this.currentSound.seek()) {
return
}
if (t === 0) {
this.updateProgressThrottled.cancel()
}
this.currentSound.seek(t)
},
ended: function () {
let onlyTrack = this.$store.state.queue.tracks.length === 1
if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
this.currentSound.seek(0)
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
} else {
this.$store.dispatch('player/trackEnded', this.currentTrack)
}
},
getSoundFromCache (trackData) {
return this.soundsCache.filter((d) => {
if (d.track.id !== trackData.id) {
return false
}
return true
})[0]
},
addSoundToCache (sound, trackData) {
let data = {
date: new Date(),
track: trackData,
sound: sound
}
this.soundsCache.push(data)
this.checkCache()
},
checkCache () {
let self = this
let toKeep = []
_.reverse(this.soundsCache).forEach((e) => {
if (toKeep.length < self.maxPreloaded) {
toKeep.push(e)
} else {
let src = e.sound._src
e.sound.unload()
}
})
this.soundsCache = _.reverse(toKeep)
},
removeFromCache (sound) {
let toKeep = []
this.soundsCache.forEach((e) => {
if (e.sound === sound) {
e.sound.unload()
} else {
toKeep.push(e)
}
})
this.soundsCache = toKeep
},
async loadSound (newValue, oldValue) {
let trackData = newValue
let oldSound = this.currentSound
if (oldSound && trackData !== oldValue) {
oldSound.stop(this.soundId)
this.soundId = null
}
if (!trackData) {
return
}
if (!this.isShuffling && trackData != oldValue) {
trackData = await this.getTrackData(trackData)
if (trackData === null) {
this.handleError({})
}
this.currentSound = this.getSound(trackData)
this.$store.commit('player/isLoadingAudio', true)
if (this.playing) {
this.$store.commit('player/errored', false)
this.$store.commit('player/playing', true)
Eliot Berriot
committed
this.$store.dispatch('player/updateProgress', 0)
this.observeProgress(true)
}
}
Eliot Berriot
committed
}
},
computed: {
...mapState({
currentIndex: state => state.queue.currentIndex,
playing: state => state.player.playing,
Eliot Berriot
committed
isLoadingAudio: state => state.player.isLoadingAudio,
volume: state => state.player.volume,
looping: state => state.player.looping,
duration: state => state.player.duration,
Eliot Berriot
committed
bufferProgress: state => state.player.bufferProgress,
Eliot Berriot
committed
errored: state => state.player.errored,
currentTime: state => state.player.currentTime,
queue: state => state.queue
}),
...mapGetters({
currentTrack: "queue/currentTrack",
hasNext: "queue/hasNext",
emptyQueue: "queue/isEmpty",
durationFormatted: "player/durationFormatted",
currentTimeFormatted: "player/currentTimeFormatted",
progress: "player/progress"
}),
updateProgressThrottled () {
return _.throttle(this.updateProgress, 250)
},
labels() {
let audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', "Media player")
let previousTrack = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Previous track")
let play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Play track")
let pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Pause track")
let next = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Next track")
let unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Unmute")
let mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Mute")
let loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip',
"Looping disabled. Click to switch to single-track looping."
)
let loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip',
"Looping on a single track. Click to switch to whole queue looping."
)
let loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip',
"Looping on whole queue. Click to disable looping."
)
let shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Shuffle your queue")
let clear = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Clear your queue")
let addArtistContentFilter = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…')
audioPlayer,
previousTrack,
play,
pause,
next,
unmute,
mute,
loopingDisabled,
loopingSingle,
loopingWhole,
shuffle,
style: function() {
let style = {
background: this.ambiantGradiant
}
return style
},
ambiantGradiant: function() {
let indexConf = [
{ orientation: 330, percent: 100, opacity: 0.7 },
{ orientation: 240, percent: 90, opacity: 0.7 },
{ orientation: 150, percent: 80, opacity: 0.7 },
{ orientation: 60, percent: 70, opacity: 0.7 }
]
let gradients = this.ambiantColors
.map((e, i) => {
let [r, g, b] = e
let conf = indexConf[i]
return `linear-gradient(${
conf.orientation
}deg, rgba(${r}, ${g}, ${b}, ${
conf.opacity
}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
})
.join(", ")
return gradients
Eliot Berriot
committed
},
watch: {
currentTrack: {
async handler (newValue, oldValue) {
Eliot Berriot
committed
clearTimeout(this.playTimeout)
let self = this
if (this.currentSound) {
this.currentSound.pause()
Eliot Berriot
committed
this.$store.commit("player/isLoadingAudio", true)
this.playTimeout = setTimeout(async () => {
await self.loadSound(newValue, oldValue)
if (!newValue || !newValue.album.cover) {
self.ambiantColors = self.defaultAmbiantColors
}
}, 500);
},
volume(newValue) {
Eliot Berriot
committed
this.sliderVolume = newValue
if (this.currentSound) {
this.currentSound.volume(newValue)
}
Eliot Berriot
committed
},
sliderVolume(newValue) {
this.$store.commit("player/volume", newValue)
},
playing: async function (newValue) {
if (this.currentSound) {
if (newValue === true) {
this.soundId = this.currentSound.play(this.soundId)
}
} else {
await this.loadSound(this.currentTrack, null)
}
this.observeProgress(newValue)
},
currentTime (newValue) {
if (!this.isUpdatingTime) {
this.setCurrentTime(newValue)
}
this.isUpdatingTime = false
},
emptyQueue (newValue) {
if (newValue) {
Howler.unload()
}
Eliot Berriot
committed
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.ui.progress {
margin: 0.5rem 0 1rem;
}
.progress {
cursor: pointer;
.bar {
min-width: 0 !important;
}
}
.ui.inverted.item > .content > .description {
color: rgba(255, 255, 255, 0.9) !important;
}
.ui.item {
.meta {
font-size: 90%;
line-height: 1.2;
Eliot Berriot
committed
}
}
.timer.total {
text-align: right;
Eliot Berriot
committed
}
.timer.start {
cursor: pointer;
Eliot Berriot
committed
}
.track-area {
.header,
.meta,
.artist,
.album {
Eliot Berriot
committed
color: white !important;
}
}
Eliot Berriot
committed
.controls a {
color: white;
}
Eliot Berriot
committed
.controls .icon.big {
cursor: pointer;
font-size: 2em !important;
}
.controls .icon {
cursor: pointer;
vertical-align: middle;
}
.secondary.icon {
font-size: 1.5em;
}
.progress-area .actions {
text-align: center;
}
.volume-control {
position: relative;
Eliot Berriot
committed
[type="range"] {
Eliot Berriot
committed
position: absolute;
Eliot Berriot
committed
cursor: pointer;
background-color: transparent;
input[type="range"]:focus {
outline: none;
}
input[type="range"]::-webkit-slider-runnable-track {
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
background: white;
cursor: pointer;
-webkit-appearance: none;
input[type="range"]::-moz-range-track {
cursor: pointer;
background: white;
input[type="range"]::-moz-focus-outer {
input[type="range"]::-moz-range-thumb {
background: white;
cursor: pointer;
input[type="range"]::-ms-track {
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type="range"]::-ms-fill-lower {
background: white;
input[type="range"]::-ms-fill-upper {
background: white;
input[type="range"]::-ms-thumb {
background: white;
cursor: pointer;
input[type="range"]:focus::-ms-fill-lower {
background: white;
}
input[type="range"]:focus::-ms-fill-upper {
Eliot Berriot
committed
background: white;
}
.active.volume-control {
width: 60% !important;
}
.looping.control {
i {
position: relative;
}
.label {
position: absolute;
font-size: 0.7rem;
bottom: -0.7rem;
right: -0.7rem;
Eliot Berriot
committed
}
}
.ui.feed.icon {
margin: 0;
}
.shuffling.loader.inline {
margin: 0;
}
Eliot Berriot
committed
@keyframes MOVE-BG {
from {
transform: translateX(0px);
}
to {
transform: translateX(46px);
}
Eliot Berriot
committed
}
.indicating.progress {
overflow: hidden;
}
.ui.progress .bar {
transition: none;
}
.ui.inverted.progress .buffer.bar {
position: absolute;
background-color: rgba(255, 255, 255, 0.15);
Eliot Berriot
committed
}
.indicating.progress .bar {
left: -46px;
width: 200% !important;
color: grey;
background: repeating-linear-gradient(
-55deg,
grey 1px,
grey 10px,
transparent 10px,
transparent 20px
) !important;
Eliot Berriot
committed
animation-name: MOVE-BG;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
Eliot Berriot
committed
}