HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<!-- app root container -->
<div class="app-wrap" id="app" v-cloak>
<!-- app player container -->
<main class="player-wrap fx fx-fade-in" ref="playerWrap" style="opacity: 0">
<!-- bg absolute elements -->
<figure class="player-bg" ref="playerBg"></figure>
<canvas class="player-canvas" ref="playerCanvas"></canvas>
<!-- main player layout -->
<section class="player-layout">
<!-- player top header -->
<header class="player-header flex-row flex-middle flex-stretch">
<h2 class="text-clip flex-1"><i class="fa fa-headphones"></i> <span>TEBİMTEBİTAGEM GAZETESİ RADYO TELEVİZYONU </span></h2>
<button class="text-nowrap common-btn" @click="toggleSidebar( true )"><i class="fa fa-bars"></i></button>
</header>
<!-- player middle content area -->
<main class="player-content flex-row">
<!-- default greet message -->
<section class="player-greet" v-if="!hasChannel && !hasErrors">
<div class="fx fx-slide-left push-bottom"><h1>Pick a Station</h1></div>
<div class="fx fx-slide-left fx-delay-1 push-bottom">This is a music streaming player for the channels provided by harunpehlivan.fm.tc Just pick a station from the sidebar to the right to start listening.</div>
<div class="fx fx-slide-up fx-delay-2 pad-top"><button class="cta-btn" @click="toggleSidebar( true )"><i class="fa fa-headphones"> </i> View Stations</button></div>
</section>
<!-- show selected channel info if possible -->
<section class="player-channel flex-1" v-if="hasChannel && !hasErrors" :key="channel.id">
<div class="flex-autorow flex-middle flex-stretch">
<!-- station details -->
<div class="flex-item flex-1">
<!-- station -->
<div class="push-bottom pad-bottom border-bottom">
<div class="flex-row flex-middle">
<img class="img-round fx fx-drop-in fx-delay-1" :src="channel.largeimage" width="80" height="80" :alt="channel.title" />
<div class="pad-left fx fx-slide-left fx-delay-2">
<div class="text-clip text-uppercase">{{ channel.genre | toSpaces }}</div>
<h2 class="text-clip">{{ channel.title }}</h2>
</div>
</div>
</div>
<!-- description -->
<div class="push-bottom pad-bottom border-bottom fx fx-slide-up fx-delay-3">
{{ channel.description }}
</div>
<!-- current track -->
<div class="push-bottom pad-bottom border-bottom fx fx-slide-up fx-delay-4" :key="track.date">
<div><span class="text-faded">DJ:</span> <span class="text-default">{{ channel.dj | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">Playing:</span> <span class="text-secondary">{{ track.title | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">From:</span> <span class="text-bright">{{ track.album | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">By:</span> <span class="text-default">{{ track.artist | toText( 'N/A' ) }}</span></div>
</div>
<!-- buttons -->
<div class="push-bottom">
<a class="cta-btn text-nowrap fx fx-slide-up fx-delay-5" :href="channel.twitter" title="Open link" target="_blank">
<i class="fa fa-twitter"></i> Twitter
</a>
<a class="cta-btn text-nowrap fx fx-slide-up fx-delay-6" :href="channel.infourl" title="Channel page" target="_blank">
<span class="fx fx-notx fx-ibk fx-drop-in fx-delay-1" :key="channel.listeners"><i class="fa fa-headphones"></i> {{ channel.listeners | toCommas( 0 ) }}</span>
</a>
<a class="cta-btn text-nowrap fx fx-slide-up fx-delay-7" :href="channel.plsfile" title="Download PLS" target="_blank">
<i class="fa fa-download"></i>
</a>
</div>
</div>
<!-- songs list -->
<div class="flex-item flex-1">
<div class="push-bottom">
<h5 class="fx fx-slide-left fx-delay-1">Recent Tracks</h5>
</div>
<div class="card push-bottom" v-if="!hasSongs">
There are no songs loaded yet for this station.
</div>
<ul class="player-tracklist push-bottom" v-if="hasSongs">
<li v-for="( s, i ) of songsList" :key="s.date" class="card fx" :class="'fx-slide-left fx-delay-' + ( i + 2 )">
<div><span class="text-secondary">{{ s.title | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">From:</span> <span class="text-bright">{{ s.album | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">By:</span> <span class="text-default">{{ s.artist | toText( 'N/A' ) }}</span></div>
</li>
</ul>
</div>
</div>
</section>
<!-- show tracks for selected channel if possible -->
<section class="player-errors flex-1 text-center" v-if="hasErrors" key="errors">
<div class="push-bottom fx fx-drop-in fx-delay-1">
<i class="fa fa-plug text-huge text-faded"></i>
</div>
<div class="push-bottom fx fx-slide-up fx-delay-2">
<h3>Oops, there's a problem!</h3>
</div>
<hr />
<div class="text-primary push-bottom fx fx-slide-up fx-delay-3" v-if="errors.init" v-text="errors.init"></div>
<div class="text-primary push-bottom fx fx-slide-up fx-delay-4" v-if="errors.stream" v-text="errors.stream"></div>
<hr />
<button class="cta-btn text-nowrap fx fx-slide-up fx-delay-5" @click="tryAgain">
<i class="fa fa-refresh"></i> Try again
</button>
</section>
</main>
<!-- player footer with controls -->
<footer class="player-footer flex-row flex-middle flex-space">
<!-- player controls -->
<section class="player-controls flex-row flex-middle push-right" :class="{ 'disabled': !canPlay }">
<button class="common-btn" @click="togglePlay()">
<i v-if="playing" class="fa fa-stop fx fx-drop-in" key="stop"></i>
<i v-else class="fa fa-play fx fx-drop-in" key="play"></i>
</button>
<div class="form-slider push-left">
<i class="fa fa-volume-down"></i>
<input class="common-slider" type="range" min="0.0" max="1.0" step="0.1" value="0.5" v-model="volume" />
<i class="fa fa-volume-up"></i>
</div>
<div class="text-clip push-left">
<span>{{ timeDisplay }}</span>
<span class="fx fx-fade-in fx-delay-1" v-if="hasChannel" :key="channel.id"> | {{ channel.title }}</span>
</div>
</section>
<!-- player links -->
<section class="player-links text-nowrap">
<a class="common-btn text-faded" href="https://github.com/harunpehlivan" title="View on Github" target="_blank">
<i class="fa fa-github"></i>
</a>
<a class="common-btn text-faded" href="https://codepen.io/harunpehlivan" title="Codepen Projects" target="_blank">
<i class="fa fa-codepen"></i>
</a>
</section>
</footer>
</section> <!-- layout wrapper -->
<!-- player stations overlay + sidebar -->
<section class="player-stations" :class="{ 'visible': sidebar }" @click="toggleSidebar( false )">
<aside class="player-stations-sidebar" @click.stop>
<!-- sidebar search -->
<header class="player-stations-header flex-row flex-middle flex-stretch">
<div class="form-input push-right">
<i class="fa fa-search"></i>
<input type="text" placeholder="Search station..." v-model="searchText" />
</div>
<button class="common-btn" @click="toggleSidebar( false )"><i class="fa fa-times-circle"></i></button>
</header>
<!-- sidebar stations list -->
<ul class="player-stations-list">
<li class="player-stations-list-item flex-row flex-top flex-stretch" v-for="c of channelsList" :key="c.id" @click="selectChannel( c )" :class="{ 'active': c.active }">
<figure class="push-right if-small">
<img class="img-round" width="70" height="70" :src="c.largeimage" :alt="c.title" />
</figure>
<aside class="flex-1">
<div class="flex-row flex-middle flex-space">
<h6 class="text-bright text-clip">{{ c.title }}</h6>
<div class="text-secondary"><i class="fa fa-headphones"></i> {{ c.listeners | toCommas( 0 ) }}</div>
</div>
<div class="text-small">
<span class="text-faded text-uppercase text-small">{{ c.genre | toSpaces }}</span> <br />
{{ c.description }}
</div>
</aside>
</li>
</ul>
<!-- sidebar sort options -->
<footer class="player-stations-footer flex-row flex-middle flex-stretch">
<div class="flex-1 push-right">
<span @click="toggleSortOrder()" class="fa clickable" :class="{ 'fa-sort-amount-desc': sortOrder === 'desc', 'fa-sort-amount-asc': sortOrder === 'asc' }"> </span>
<span class="text-faded">Sort: </span>
<span class="text-secondary popover">
<span class="clickable">{{ sortLabel }}</span>
<span class="popover-box popover-top">
<button @click="sortBy( 'title', 'asc' )">Station Name</button>
<button @click="sortBy( 'listeners', 'desc' )">Listeners Count</button>
<button @click="sortBy( 'genre', 'asc' )">Music Genre</button>
</span>
</span>
</div>
<div> </div>
</footer>
</aside>
</section>
</main> <!-- player -->
</div> <!-- wrapper -->
// spacing and padding
$padSpace: 1em;
$padSmall: $padSpace / 2;
$headerHeight: 3.5em;
$bgImg: 'https://res.cloudinary.com/tercuman-b-l-m-merkez/image/upload/v1561460680/ServQuick.jpg_g0rquc.png';
// document colors
$colorDocument: #8086a0;
$colorDocumentDark: #1e1f30;
$colorDocumentDarker: darken( $colorDocumentDark, 10% );
$colorDocumentLight: #a0a6b0;
$colorDocumentText: desaturate( lighten( $colorDocumentDark, 40% ), 5% );
// common colors
$colorPrimary: crimson;
$colorPrimaryText: lighten( $colorPrimary, 40% );
$colorSecondary: cornflowerblue;
$colorSecondaryText: darken( $colorSecondary, 40% );
$colorDefault: lightslategray;
$colorDefaultText: darken( $colorDefault, 40% );
$colorGrey: slategray;
$colorGreyText: darken( $colorGrey, 40% );
$colorBright: whitesmoke;
$colorBrightText: darken( $colorBright, 40% );
$colorOverlay: rgba( black, 0.4 );
$colorCard: rgba( black, 0.08 );
// borders and lines
$lineWidth: 2px;
$lineStyle: solid;
$lineColor: rgba( black, 0.08 );
$lineJoin: 6px;
// base font options
$fontFamily: 'Roboto Condensed', sans-serif;
$fontSize: 20px;
$fontSpace: 1.2em;
$fontWeight: 700;
// shadow styles
$shadowContainer: 0 1px 30px rgba( black, 0.8 );
$shadowOverlay: 0 1px 20px rgba( black, 0.6 );
$shadowPaper: 0 1px 3px rgba( black, 0.5 );
// transition props
$fxSpeed: 400ms;
$fxEase: cubic-bezier( 0.215, 0.610, 0.355, 1.000 );
$fxSpeedOffset: calc( #{$fxSpeed} / 3 );
$fxSlideDist: 80px;
$fxShrinkScale: .4;
$fxGrowScale: 1.4;
$fxRotateAmount: 8deg;
// screen sizes
$sizeSmall: 420px;
$sizeMedium: 720px;
$sizeLarge: 1200px;
// screen breakpoints
$screenSmall: "only screen and (min-width : #{$sizeSmall})";
$screenMedium: "only screen and (min-width : #{$sizeMedium})";
$screenLarge: "only screen and (min-width : #{$sizeLarge})";
// page reset
*, *:before, *:after {
margin: 0;
padding: 0;
border: 0;
outline: none;
background-color: transparent;
text-transform: none;
text-shadow: none;
box-shadow: none;
box-sizing: border-box;
appearance: none;
-webkit-overflow-scrolling: touch;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform-style: flat;
transition:
border-color $fxSpeed $fxEase,
background-color $fxSpeed $fxEase,
opacity $fxSpeed $fxEase,
transform $fxSpeed $fxEase;
}
// block types
article, aside, details, figcaption, figure, footer, header, hgroup,
menu, nav, section, main, summary, div, h1, h2, h3, h4, h5, h6, hr,
p, ol, ul, form, img {
display: block;
}
// form elements
input, textarea, select, optgroup, option, button {
font-family: inherit;
font-size: inherit;
font-weight: normal;
line-height: inherit;
color: inherit;
}
select, button {
cursor: pointer;
}
// links
a {
color: $colorSecondary;
&:hover {
color: lighten( $colorSecondary, 10% );
}
}
// horizontal lines
hr {
display: block;
overflow: hidden;
margin: $padSpace 0;
height: 0;
border: 0;
border-bottom: $lineWidth $lineStyle $lineColor;
}
// document setup
html, body {
display: block;
position: relative;
max-width: 100vw;
min-height: 100vh;
}
html {
overflow: hidden;
overflow-y: auto;
}
body {
font-family: $fontFamily;
font-weight: $fontWeight;
font-size: calc( #{$fontSize} - 6px );
line-height: $fontSpace;
color: $colorDocumentText;
background-size: cover;
background-color: $colorDocument;
background-image:
linear-gradient( 217deg, rgba( $colorPrimary, .8 ), rgba( $colorPrimary, 0 ) 70.71% ),
linear-gradient( 127deg, rgba( $colorDocument, 1 ), rgba( $colorDocument, 0 ) 70.71% ),
linear-gradient( 336deg, rgba( $colorSecondary, .8 ), rgba( $colorSecondary, 0 ) 70.71% );
@media #{$screenSmall} {
font-size: calc( #{$fontSize} - 4px );
}
@media #{$screenMedium} {
font-size: calc( #{$fontSize} - 2px );
}
@media #{$screenLarge} {
font-size: $fontSize;
}
}
// media query helpers
.if-small {
display: none;
@media #{$screenSmall} {
display: initial;
}
}
.if-medium {
display: none;
@media #{$screenMedium} {
display: initial;
}
}
.if-large {
display: none;
@media #{$screenLarge} {
display: initial;
}
}
// not rendered
.hidden, [hidden], [v-cloak] {
display: none;
}
// visible but not usable
.disabled, [disabled] {
pointer-events: none;
opacity: 0.5;
}
// clickable elms
.clickable {
cursor: pointer;
}
// common card style
.card {
padding: $padSpace;
background-color: $colorCard;
border-radius: $lineJoin;
}
// margin helpers
.push-top { margin-top: $padSpace; }
.push-right { margin-right: $padSpace; }
.push-bottom { margin-bottom: $padSpace; }
.push-left { margin-left: $padSpace; }
.push-all { margin: $padSpace; }
// padding helpers
.pad-top { padding-top: $padSpace; }
.pad-right { padding-right: $padSpace; }
.pad-bottom { padding-bottom: $padSpace; }
.pad-left { padding-left: $padSpace; }
.pad-all { padding: $padSpace; }
// border helpers
.border-top { border-top: $lineWidth $lineStyle $lineColor; }
.border-right { border-right: $lineWidth $lineStyle $lineColor; }
.border-bottom { border-bottom: $lineWidth $lineStyle $lineColor; }
.border-left { border-left: $lineWidth $lineStyle $lineColor; }
// shadow helpers
.shadow-box { box-shadow: $shadowPaper; }
.shadow-text { text-shadow: $shadowPaper; }
// animations on
.fx {
position: relative;
animation-direction: normal;
animation-duration: $fxSpeed;
animation-timing-function: $fxEase;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
// disable transitions on element
.fx-notx {
transition: none !important;
}
// convert inline elements into inline-block
.fx-ibk {
display: inline-block !important;
}
// effect delays
@for $i from 1 through 8 {
.fx-delay-#{$i} {
animation-delay: calc( #{$fxSpeedOffset} * #{$i} );
}
}
// spin right animation
@keyframes spinRight {
0% { transform: rotate( 0deg ); }
100% { transform: rotate( 359deg ); }
}
.fx-spin-right {
animation-name: spinRight;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
// spin right animation
@keyframes spinLeft {
0% { transform: rotate( 359deg ); }
100% { transform: rotate( 0deg ); }
}
.fx-spin-left {
animation-name: spinLeft;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
// fade-in animation
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
.fx-fade-in {
opacity: 0;
animation-name: fadeIn;
}
// fade-out animation
@keyframes fadeOut {
0% { opacity: 1; }
100% { opacity: 0; }
}
.fx-fade-out {
opacity: 1;
animation-name: fadeOut;
}
// drop-in animation (scale)
@keyframes dropIn {
0% { opacity: 0; transform: scale( $fxGrowScale ); }
100% { opacity: 1; transform: scale( 1 ); }
}
.fx-drop-in {
opacity: 0;
transform: scale( $fxGrowScale );
animation-name: dropIn;
}
// zoom-in animation (modal, alert, etc)
@keyframes zoomIn {
0% { opacity: 0; transform: scale( $fxShrinkScale ); }
100% { opacity: 1; transform: scale( 1 ); }
}
.fx-zoom-in {
opacity: 0;
transform: scale( $fxShrinkScale );
animation-name: zoomIn;
}
// zoom-out animation (modal, alert, etc)
@keyframes zoomOut {
0% { opacity: 1; transform: scale( 1 ); }
100% { opacity: 0; transform: scale( $fxShrinkScale ); }
}
.fx-zoom-out {
opacity: 1;
transform: scale( 1 );
animation-name: zoomOut;
}
// slide in to the left
@keyframes slideLeft {
0% { opacity: 0; transform: translateX( $fxSlideDist ); }
100% { opacity: 1; transform: translateX( 0 ); }
}
.fx-slide-left {
opacity: 0;
transform: translateX( $fxSlideDist );
animation-name: slideLeft;
}
// slide in to the right
@keyframes slideRight {
0% { opacity: 0; transform: translateX( calc( 0 - #{$fxSlideDist} ) ); }
100% { opacity: 1; transform: translateX( 0 ); }
}
.fx-slide-right {
opacity: 0;
transform: translateX( calc( 0 - #{$fxSlideDist} ) );
animation-name: slideRight;
}
// slide in to the top
@keyframes slideUp {
0% { opacity: 0; transform: translateY( $fxSlideDist ); }
100% { opacity: 1; transform: translateY( 0 ); }
}
.fx-slide-up {
opacity: 0;
transform: translateY( $fxSlideDist );
animation-name: slideUp;
}
// slide in to the bottom
@keyframes slideDown {
0% { opacity: 0; transform: translateY( calc( 0 - #{$fxSlideDist} ) ); }
100% { opacity: 1; transform: translateY( 0 ); }
}
.fx-slide-down {
opacity: 0;
transform: translateY( calc( 0 - #{$fxSlideDist} ) );
animation-name: slideDown;
}
// pulse opacity
@keyframes pulseFade {
0% { opacity: 0.7; }
50% { opacity: 1.0; }
100% { opacity: 0.7; }
}
.fx-pulse {
opacity: 0.7;
animation-name: pulseFade;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
// flex helpers
.flex-row { display: flex; flex-direction: row; flex-wrap: nowrap; }
.flex-wrap { flex-wrap: wrap; }
.flex-left { justify-content: flex-start; }
.flex-center { justify-content: center; }
.flex-right { justify-content: flex-end; }
.flex-space { justify-content: space-between; }
.flex-around { justify-content: space-around; }
.flex-stretch { justify-content: stretch; }
.flex-top { align-items: flex-start; }
.flex-middle { align-items: center; }
.flex-bottom { align-items: flex-end; }
.flex-half { flex: .5; }
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.flex-3 { flex: 3; }
.flex-4 { flex: 4; }
.flex-5 { flex: 5; }
// auto switch between column and row
.flex-autorow {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
& > .flex-item {
flex: 1;
width: 100%;
margin: 0 0 $padSpace 0; // push bottom
&:last-of-type { margin: 0; }
}
@media #{$screenMedium} {
flex-direction: row;
& > .flex-item {
margin: 0 $padSpace 0 0; // push right
&:last-of-type { margin: 0; }
}
}
}
// rouded image
.img-round {
overflow: hidden;
text-indent: -1000px;
border-radius: 1000px;
border: $lineWidth solid $colorBright;
background-color: lighten( $colorDocumentDark, 10% );
background-image: linear-gradient( 45deg, lighten( $colorDocumentDark, 10% ), lighten( $colorDocumentDark, 25% ) );
box-shadow: $shadowPaper;
}
// centered image
.img-center {
display: block;
margin: 0 auto;
}
// common large bright text buttons
.common-btn {
display: inline-block;
text-align: center;
font-size: 180%;
font-weight: normal;
line-height: 1em;
width: 1em;
color: $colorBright;
&:hover {
color: darken( $colorBright, 20% );
}
}
// common cta button/link
.cta-btn {
display: inline-block;
text-decoration: none;
padding: ( $padSpace / 2 ) $padSpace;
color: $colorPrimaryText;
background-color: darken( desaturate( $colorPrimary, 10% ), 10% );
border-radius: 100px;
box-shadow: $shadowPaper;
line-height: 1.1em;
&:hover {
color: lighten( $colorPrimaryText, 5% );
background-color: darken( $colorPrimary, 5% );
}
}
// common form input wrapper
.form-input {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: stretch;
color: $colorBright;
& > input {
flex: 1;
line-height: 1.5em;
padding: 0 ( $padSpace / 2 );
}
}
// common form slider container
@mixin sliderTrack {
width: 100%;
height: 3px;
background-color: lighten( $colorDocumentDark, 10% );
color: transparent !important;
border-color: transparent !important;
border-radius: $lineJoin !important;
border: 0 !important;
}
@mixin sliderThumb {
width: 1em;
height: 1em;
margin: -.4em 0 0 0;
border-radius: 50%;
box-shadow: $shadowPaper;
background-color: $colorBright;
transition: background $fxSpeed $fxEase;
color: transparent !important;
border-color: transparent !important;
border: 0 !important;
cursor: pointer;
&:hover {
background-color: darken( $colorBright, 20% );
}
}
.form-slider {
display: flex;
position: relative;
flex-direction: row;
align-items: center;
justify-content: stretch;
width: 100%;
max-width: 6em;
line-height: 1em;
& > input {
-webkit-appearance: none;
appearance: none;
width: 100%;
margin: 0 .5em;
// track
&::-webkit-slider-runnable-track { @include sliderTrack; }
&::-moz-range-track { @include sliderTrack; }
&::-ms-track { @include sliderTrack; }
// thumb
&::-webkit-slider-thumb { -webkit-appearance: none; @include sliderThumb; }
&::-moz-range-thumb { @include sliderThumb; }
&::-ms-thumb { @include sliderThumb; }
}
}
// common absolute popover
@keyframes popoverShow {
0% { transform: translateX( -50% ) scale( .8 ); opacity: 0; }
35% { transform: translateX( -50% ) scale( 1.2 ); opacity: .8; }
100% { transform: translateX( -50% ) scale( 1 ); opacity: 1; }
}
.popover {
position: relative;
.popover-box {
display: none;
position: absolute;
padding: ( $padSpace / 2 ) 0;
max-width: 300px;
min-height: 100px;
left: 50%;
bottom: 50%;
transition: none;
transform: translateX( -50% );
background-color: lighten( $colorDocumentDark, 8% );
border-radius: $lineJoin;
box-shadow: $shadowOverlay;
animation: popoverShow $fxSpeed $fxEase forwards;
z-index: 2000;
&:before {
content: '';
display: none;
position: absolute;
transition: none;
width: 0;
height: 0;
transform: translateX( -50% );
left: 50%;
z-index: 2001;
}
& > button {
display: block;
width: 100%;
text-align: left;
padding: ( $padSpace / 2 ) $padSpace;
line-height: 1.2em;
white-space: nowrap;
background-color: rgba( $colorDocumentDark, 0 );
&:hover {
background-color: rgba( $colorDocumentDark, .2 );
}
& + button {
border-top: $lineWidth $lineStyle $lineColor;
}
}
&.popover-left {
transform: none;
left: auto;
right: 0;
}
&.popover-right {
transform: none;
left: 0;
right: auto;
}
&.popover-top {
top: auto;
bottom: 100%;
&:before {
display: block;
top: auto;
bottom: -10px;;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid lighten( $colorDocumentDark, 8% );
}
}
&.popover-bottom {
top: 100%;
bottom: auto;
&:before {
display: block;
top: -10px;
bottom: auto;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid lighten( $colorDocumentDark, 8% );
}
}
}
&:hover > .popover-box,
&:active > .popover-box {
display: block;
}
}
// headings
h1, h2, h3, h4, h5, h6 {
display: block;
font-weight: normal;
line-height: 1.1em;
color: $colorBright;
}
h1 { font-size: 220%; }
h2 { font-size: 200%; }
h3 { font-size: 180%; }
h4 { font-size: 160%; }
h5 { font-size: 140%; }
h6 { font-size: 120%; }
// text helpers
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-justify { text-align: justify; }
.text-uppercase { text-transform: uppercase; }
.text-lowercase { text-transform: lowercase; }
.text-capitalize { text-transform: capitalize; }
.text-underline { text-decoration: underline; }
.text-striked { text-decoration: line-through; }
.text-italic { font-style: italic; }
.text-bold { font-weight: bold; }
.text-nowrap { white-space: nowrap; }
.text-clip { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.text-primary { color: $colorPrimary; }
.text-secondary { color: $colorSecondary; }
.text-grey { color: $colorGrey; }
.text-bright { color: $colorBright; }
.text-faded { opacity: 0.5; }
.text-big { font-size: 120%; }
.text-bigger { font-size: 180%; }
.text-huge { font-size: 240%; }
.text-small { font-size: 90%; }
.text-condense { letter-spacing: -1px; }
// app root
.app-wrap {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
min-height: 100vh;
width: 100%;
}
// player container
.player-wrap {
display: block;
overflow: hidden;
position: relative;
flex: 1;
width: 100%;
height: 100vh;
background-color: $colorDocumentDark;
& > .player-bg,
& > .player-canvas {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
& > .player-bg {
background-image: url( $bgImg );
background-position: bottom right;
background-repeat: no-repeat;
background-size: cover;
opacity: .4;
}
@media #{$screenMedium} {
margin: 0 ( $padSpace * 2 );
max-width: 1080px;
height: calc( 100vh - ( #{$padSpace} * 4 ) );
max-height: 700px;
border-radius: $lineJoin;
box-shadow: $shadowContainer;
}
}
// player layout container
.player-layout {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
height: 100%;
.player-header,
.player-content,
.player-footer {
position: relative;
}
.player-header,
.player-footer {
padding: 0 $padSpace;
height: $headerHeight;
min-height: $headerHeight;
background-color: $colorCard;
}
.player-header {
& > h2 {
color: $colorPrimary;
i { vertical-align: bottom; }
}
}
.player-content {
flex: 1;
height: 100%;
overflow: hidden;
overflow-y: auto;
padding: $padSpace;
& > section {
margin: auto 0; // prevent vertical aligned flex item from overflowing
}
@media #{$screenMedium} {
padding: $padSpace ( $padSpace * 2 );
}
}
}
// player greeting message
.player-greet {
flex: 1;
@media #{$screenMedium} { flex: .5; }
}
// player tracklist
.player-tracklist {
display: block;
position: relative;
list-style: none;
& > li + li {
margin-top: ( $padSpace / 2 );
}
}
// player footer controls
.player-controls {
position: relative;
}
// player stations sidebar
.player-stations {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba( $colorOverlay, 0 );
pointer-events: none;
z-index: 1;
.player-stations-sidebar {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: stretch;
position: absolute;
top: 0;
right: -320px;
width: 320px;
min-height: 100%;
max-height: 100%;
background-color: lighten( $colorDocumentDark, 2% );
@media #{$screenSmall} {
right: -420px;
width: 420px;
}
.player-stations-header,
.player-stations-footer {
padding: 0 $padSpace;
min-height: $headerHeight;
box-shadow: 0 0 3px rgba( black, 0.3 );
}
.player-stations-list {
display: block;
list-style: none;
overflow: hidden;
overflow-y: auto;
margin-left: -10px;
padding-left: 10px;
flex: 1;
.player-stations-list-item {
position: relative;
padding: $padSpace;
background-color: rgba( black, 0.1 );
cursor: pointer;
&:nth-child( odd ) {
background-color: rgba( black, 0.18 );
}
&:hover {
background-color: rgba( black, 0 );
}
&.active {
background-color: darken( $colorDocumentDark, 2% );
h6 { color: $colorPrimary; }
}
}
}
}
// slide out
&.visible {
background-color: $colorOverlay;
pointer-events: auto;
z-index: 1000;
.player-stations-sidebar {
transform: translateX( -320px );
box-shadow: $shadowOverlay;
@media #{$screenSmall} { transform: translateX( -420px ); }
}
.player-stations-list-item.active:before {
content: '';
display: block;
position: absolute;
transition: none;
transform: translateY( -50% );
top: 50%;
left: -10px;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid darken( $colorDocumentDark, 2% );
}
}
}
/**
* TEBİMTEBİTAGEM GAZETESİ RADYO TELEVİZYONU Web Player
* Author: Rainner Lins (2018)
* Site: http://harunpehlivan.fm.tc/
/**
* Sphere object
*/
const Sphere = {
group : null,
shapes : [],
move : new THREE.Vector3( 0, 0, 0 ),
ease : 8,
create( box, scene ) {
this.group = new THREE.Object3D();
let shape1 = new THREE.CircleGeometry( 1, 10 );
let shape2 = new THREE.CircleGeometry( 2, 20 );
let points = new THREE.SphereGeometry( 100, 30, 14 ).vertices;
let material = new THREE.MeshLambertMaterial( { color: 0xffffff, opacity: 0, side: THREE.DoubleSide } );
let center = new THREE.Vector3( 0, 0, 0 );
let radius = 12;
for ( let i = 0; i < points.length; i++ ) {
let { x, y, z } = points[ i ];
let home = { x, y, z };
let cycle = THREE.Math.randInt( 0, 100 );
let pace = THREE.Math.randInt( 10, 30 );
let shape = new THREE.Mesh( ( i % 2 ) ? shape1 : shape2, material );
shape.position.set( x, y, z );
shape.lookAt( center );
shape.userData = { radius, cycle, pace, home };
this.group.add( shape );
}
this.group.position.set( 500, 0, 0 );
this.group.rotation.x = ( Math.PI / 2 ) + .6;
scene.add( this.group );
},
update( box, mouse, freq ) {
let bass = ( Math.floor( freq[ 1 ] | 0 ) / 255 );
this.move.x = ( box.width * .06 ) + -( mouse.x * 0.02 );
this.group.position.x += ( this.move.x - this.group.position.x ) / this.ease;
this.group.position.y += ( this.move.y - this.group.position.y ) / this.ease;
this.group.position.z = 10 + ( bass * 80 );
this.group.rotation.y -= 0.003;
for ( let i = 0; i < this.group.children.length; i++ ) {
let shape = this.group.children[ i ];
let { radius, cycle, pace, home } = shape.userData;
shape.position.set( home.x, home.y, home.z );
shape.translateZ( bass * Math.sin( cycle / pace ) * radius );
shape.userData.cycle++;
}
},
};
/**
* Vue filters
*/
Vue.filter( 'toCommas', ( num, decimals ) => {
let o = { style: 'decimal', minimumFractionDigits: decimals, maximumFractionDigits: decimals };
return new Intl.NumberFormat( 'en-US', o ).format( num );
});
Vue.filter( 'toSpaces', ( str ) => {
return String( str || '' ).trim().replace( /[^\w\`\'\-]+/g, ' ' ).trim();
});
Vue.filter( 'toText', ( str, def ) => {
str = String( str || '' ).replace( /[^\w\`\'\-\.\!\?]+/g, ' ' ).trim();
return str || String( def || '' );
});
/**
* Vue app
*/
new Vue({
el: '#app',
data: {
// toggles
init: false,
playing: false,
loading: false,
sidebar: false,
// channels stuff
channels: [], // all channels
channel: {}, // selected channel
songs: [], // recent tracks
track: {}, // current track
errors: {}, // error messages
// animation stuff
fxBox: null,
fxRenderer: null,
fxScene: null,
fxColor: null,
fxLight: null,
fxCamera: null,
fxMouse: { x: 0, y: 0 },
fxObjects: [],
// audio stuff
audio: new Audio(),
context: new AudioContext(),
freqData: new Uint8Array(),
audioSrc: null,
audioGain: null,
analyser: null,
volume: 0.5,
// timer stuff
timeStart: 0,
timeDisplay: '00:00:00',
timeItv: null,
// sorting stuff
searchText: '',
sortParam: 'listeners',
sortOrder: 'desc',
// timer stuff
anf: null,
sto: null,
itv: null,
},
// watch methods
watch: {
// when app is ready
init() {
setTimeout( this.setupCanvas, 100 );
setTimeout( this.initSidebar, 500 );
},
// watch playing status
playing() {
if ( this.playing ) { this.startClock(); }
else { this.stopClock(); }
},
// update player volume
volume() {
this.setVolume( this.volume );
}
},
// computed methods
computed: {
// filter channels list
channelsList() {
let list = this.channels.slice();
let search = this.searchText.replace( /[^\w\s\-]+/g, '' ).replace( /[\r\s\t\n]+/g, ' ' ).trim();
if ( search && search.length > 1 ) {
let reg = new RegExp( '^('+ search +')', 'i' );
list = list.filter( i => reg.test( i.title +' '+ i.description ) );
}
if ( this.sortParam ) {
list = this.sortList( list, this.sortParam, this.sortOrder );
}
if ( this.channel.id ) {
list = list.map( i => {
i.active = ( this.channel.id === i.id ) ? true : false;
return i;
});
}
return list;
},
// filter songs list
songsList() {
let list = this.songs.slice();
return list;
},
// sort-by label for buttons, etc
sortLabel() {
switch ( this.sortParam ) {
case 'title' : return 'Station Name';
case 'listeners' : return 'Listeners Count';
case 'genre' : return 'Music Genre';
}
},
// check if audio can be played
canPlay() {
return ( this.channel.id && !this.loading ) ? true : false;
},
// check if a channel is selected
hasChannel() {
return this.channel.id ? true : false;
},
// check if there are tracks loaded
hasSongs() {
return this.songs.length ? true : false;
},
// check if there are errors to show
hasErrors() {
return ( this.checkError( 'init' ) || this.checkError( 'stream' ) ) ? true : false;
},
},
// custom methods
methods: {
// set an erro message
setError( key, err ) {
let errors = Object.assign( {}, this.errors );
errors[ key ] = String( err || '' ).trim();
if ( err ) console.warn( 'ERROR('+ key +'):', err );
this.errors = errors;
this.init = true;
},
// check if an error has been set for a key
checkError( key ) {
return ( key && this.errors.hasOwnProperty( key ) && this.errors[ key ] );
},
// clear all error messages
clearErrors() {
Object.keys( this.errors ).forEach( key => {
this.errors[ key ] = '';
});
},
// reset selected channel
resetPlayer() {
this.channel = {};
this.songs = [];
this.clearErrors();
this.getChannels( true );
},
// try resuming stream problem if possible
tryAgain() {
if ( this.checkError( 'init' ) ) return this.resetPlayer();
if ( this.channel.id ) return this.playChannel( this.channel );
},
// show/hide the sidebar
toggleSidebar( toggle ) {
this.sidebar = ( typeof toggle === 'boolean' ) ? toggle : false;
},
// show sidebar at startup if there are no errors
initSidebar() {
if ( this.checkError( 'init' ) ) return;
this.toggleSidebar( true );
},
// toggle stream playback for current selected channel
togglePlay() {
if ( this.loading ) return;
if ( this.playing ) return this.closeAudio();
if ( this.channel.id ) return this.playChannel( this.channel );
},
// toggle sort order
toggleSortOrder() {
this.sortOrder = ( this.sortOrder === 'asc' ) ? 'desc' : 'asc';
},
// apply sorting and toggle order
sortBy( param, order ) {
if ( this.sortParam === param ) { this.toggleSortOrder(); }
else { this.sortOrder = order || 'asc'; }
this.sortParam = param;
},
// sort an array by key and order
sortList( list, param, order ) {
return list.sort( ( a, b ) => {
if ( a.hasOwnProperty( param ) && b.hasOwnProperty( param ) ) {
let _a = a[ param ];
let _b = b[ param ];
_a = ( typeof _a === 'string' ) ? _a.toUpperCase() : _a;
_b = ( typeof _b === 'string' ) ? _b.toUpperCase() : _b;
if ( order === 'asc' ) {
if ( _a < _b ) return -1;
if ( _a > _b ) return 1;
}
if ( order === 'desc' ) {
if ( _a > _b ) return -1;
if ( _a < _b ) return 1;
}
}
return 0;
});
},
// get channels data from api
getChannels( sidebar ) {
let endpoint = 'http://harunpehlivan.fm.tc/channels.json';
let emsg = [ 'There was a problem trying to load the list of available channels from TEBİMTEBİTAGEM GAZETESİ RADYO TELEVİZYONU.' ];
axios.get( endpoint ).then( res => {
if ( !res || !res.data || !res.data.channels ) {
emsg.push( 'The API response did not have any channels data available at this time.' );
emsg.push( 'Status: Channels API Error.' );
return this.setError( 'channels', emsg.join( ' ' ) );
}
for ( let c of res.data.channels ) {
if ( !Array.isArray( c.playlists ) ) continue;
// filter and sanitize list of channels
c.twitter = c.twitter ? 'https://twitter.com/@'+ c.twitter : ''; // full twitter url
c.plsfile = c.playlists.filter( p => ( p.format === 'mp3' && /^(highest|high)$/.test( p.quality ) ) ).shift().url || '';
c.mp3file = 'http://ice1.harunpehlivan.fm.tc/'+ c.id +'-128-mp3'; // assumed stream url
c.songsurl = 'http://harunpehlivan.fm.tc/songs/'+ c.id +'.json'; // songs data url
c.infourl = 'http://harunpehlivan.fm.tc'+ c.id +'/'; // channel page url
c.listeners = c.listeners | 0; // force numeric
c.updated = c.updated | 0; // force numeric
c.active = false; // select state
// update selected channel
if ( this.isCurrentChannel( c ) ) {
c.active = true;
this.channel = Object.assign( this.channel, c );
}
}
this.channels = res.data.channels.slice();
if ( sidebar ) this.toggleSidebar( true );
this.setError( 'init', '' );
this.setError( 'channels', '' );
})
.catch( e => {
emsg.push( 'Try again, or check your internet connection.' );
emsg.push( 'Status: '+ String( e.message || 'Channels API Error' ) +'.' );
let errstr = emsg.join( ' ' );
if ( !this.channels.length ) this.setError( 'init', errstr );
this.setError( 'channels', errstr );
});
},
// fetch songs for a channel
fetchSongs( channel, cb ) {
if ( !channel || !channel.id || !channel.songsurl ) return;
if ( !this.isCurrentChannel( channel ) ) { this.songs = []; this.track = {}; }
let emsg = [ 'There was a problem trying to load the list of songs for channel '+ channel.title +' from SomaFM.' ];
axios.get( channel.songsurl ).then( res => {
if ( !res || !res.data || !res.data.songs ) {
emsg.push( 'The API response did not have any songs data available at this time.' );
emsg.push( 'Status: Songs API Error.' );
return this.setError( 'songs', emsg.join( ' ' ) );
}
let songs = res.data.songs.slice();
this.track = songs.shift();
this.songs = songs.slice( 0, 3 );
this.setError( 'songs', '' );
if ( typeof cb === 'function' ) cb( songs );
})
.catch( e => {
emsg.push( 'Try again, or check your internet connection.' );
emsg.push( 'Status: '+ String( e.message || 'Songs API Error' ) +'.' );
this.setError( 'songs', emsg.join( ' ' ) );
});
},
// run maintenance tasks on a timer
setupMaintenance() {
this.itv = setInterval( () => {
this.getChannels(); // update channels
this.fetchSongs( this.channel ); // update channel tracks
// ...
}, 1000 * 30 );
},
// setup animation canvas
setupCanvas() {
if ( !this.$refs.playerWrap ) return;
if ( !this.$refs.playerCanvas ) return;
// default canvas and player dimensions
const player = this.$refs.playerWrap;
const canvas = this.$refs.playerCanvas;
// setup THREE renderer and replace default canvas
this.fxBox = player.getBoundingClientRect();
this.fxScene = new THREE.Scene();
this.fxRenderer = new THREE.WebGLRenderer( { alpha: true, antialias: true, precision: 'highp' } );
this.fxRenderer.setClearColor( 0x000000, 0 );
this.fxRenderer.setPixelRatio( window.devicePixelRatio );
this.fxRenderer.domElement.className = canvas.className;
// setup camera
this.fxCamera = new THREE.PerspectiveCamera( 60, ( this.fxBox.width / this.fxBox.height ), 0.1, 20000 );
this.fxCamera.lookAt( this.fxScene.position );
this.fxCamera.position.set( 0, 0, 300 );
this.fxCamera.rotation.set( 0, 0, 0 );
// light color
this.fxColor = new THREE.Color();
this.fxColor.setHSL( this.fxHue, 1, .5 );
// setup light source
this.fxLight = new THREE.PointLight( 0xffffff, 4, 400 );
this.fxLight.position.set( 0, 0, 420 );
this.fxLight.castShadow = false;
this.fxLight.target = this.fxScene;
this.fxLight.color = this.fxColor;
this.fxScene.add( this.fxLight );
// setup canvas and events
canvas.parentNode.replaceChild( this.fxRenderer.domElement, canvas );
window.addEventListener( 'mousemove', this.updateMousePosition );
window.addEventListener( 'resize', this.updateStageSize );
// add objects
this.fxObjects.push( Sphere );
// setup objects and start animation
for ( let o of this.fxObjects ) o.create( this.fxBox, this.fxScene );
this.updateStageSize();
this.updateAnimations();
},
// update mouse position from center of canvas
updateMousePosition( e ) {
if ( !this.fxBox || !e ) return;
this.fxMouse.x = Math.max( 0, e.pageX || e.clientX || 0 ) - ( this.fxBox.left + ( this.fxBox.width / 2 ) );
this.fxMouse.y = Math.max( 0, e.pageY || e.clientY || 0 ) - ( this.fxBox.top + ( this.fxBox.height / 2 ) );
},
// update canvas size
updateStageSize() {
if ( !this.$refs.playerWrap || !this.fxRenderer ) return;
this.fxBox = this.$refs.playerWrap.getBoundingClientRect();
this.fxCamera.aspect = ( this.fxBox.width / this.fxBox.height );
this.fxCamera.updateProjectionMatrix();
this.fxRenderer.setSize( this.fxBox.width, this.fxBox.height );
},
// update light color based on audio freq
updateStageLight() {
let dist = Math.floor( this.freqData[ 1 ] | 0 ) / 255;
let color = Math.floor( this.freqData[ 16 ] | 0 ) / 255;
this.fxLight.distance = 360 + ( 140 * dist );
this.fxColor.setHSL( color, .5, .5 );
},
// update custom objects in 3d scene
updateSceneObjects() {
for ( let o of this.fxObjects ) {
o.update( this.fxBox, this.fxMouse, this.freqData );
}
},
// audio visualizer animation loop
updateAnimations() {
this.anf = requestAnimationFrame( this.updateAnimations );
if ( !this.fxRenderer || !this.fxCamera || !this.analyser || !this.freqData ) return;
this.analyser.getByteFrequencyData( this.freqData );
this.updateSceneObjects();
this.updateStageLight();
this.fxRenderer.render( this.fxScene, this.fxCamera );
},
// setup audio routing and stream events
setupAudio() {
// setup audio sources
this.audioSrc = this.context.createMediaElementSource( this.audio );
this.audioGain = this.context.createGain();
this.analyser = this.context.createAnalyser();
// connect sources
this.audioSrc.connect( this.audioGain );
this.audioSrc.connect( this.analyser );
this.audioGain.connect( this.context.destination );
this.setVolume( this.volume );
// check when stream can start playing
this.audio.addEventListener( 'canplay', e => {
this.audio.play();
this.freqData = new Uint8Array( this.analyser.frequencyBinCount );
});
// check if stream is buffering
this.audio.addEventListener( 'waiting', e => {
this.playing = false;
this.loading = true;
});
// check if stream is done buffering
this.audio.addEventListener( 'playing', e => {
this.setError( 'stream', '' );
this.playing = true;
this.loading = false;
});
// check if stream has ended
this.audio.addEventListener( 'ended', e => {
this.playing = false;
this.loading = false;
});
// check for steam error
this.audio.addEventListener( 'error', e => {
let emsg = [];
emsg.push( 'The selected audio stream could not load, or has stopped loading.' );
emsg.push( 'Try again, or check your internet connection.' );
emsg.push( 'Status: '+ String( e.message || 'Stream URL Error' ) +'.' );
this.setError( 'stream', emsg.join( ' ' ) );
this.playing = false;
this.loading = false;
});
},
// set audio volume
setVolume( volume ) {
if ( !this.audioGain ) return;
volume = parseFloat( volume ) || 0;
volume = ( volume < 0 ) ? 0 : volume;
volume = ( volume > 1 ) ? 1 : volume;
this.audioGain.gain.value = volume;
},
// checks is a channel is currently selected
isCurrentChannel( channel ) {
if ( !channel || !channel.id || !this.channel.id ) return false;
if ( this.channel.id !== channel.id ) return false;
return true;
},
// play audio stream for a channel
playChannel( channel ) {
if ( this.playing ) return;
this.clearErrors();
this.audio.src = channel.mp3file +'/?x='+ Date.now();
this.audio.crossOrigin = 'anonymous';
this.audio.load();
},
// select a channel to play
selectChannel( channel ) {
if ( !channel || !channel.id ) return;
if ( this.isCurrentChannel( channel ) ) return;
this.closeAudio();
this.toggleSidebar( false );
this.playChannel( channel );
this.fetchSongs( channel );
this.channel = channel;
},
// close active audio
closeAudio() {
this.setError( 'stream', '' );
try { this.audio.pause(); } catch ( e ) {}
try { this.audio.stop(); } catch ( e ) {}
try { this.audio.close(); } catch ( e ) {}
this.playing = false;
},
// start tracking playback time
startClock() {
this.stopClock();
this.timeStart = Date.now();
this.timeItv = setInterval( this.updateClock, 1000 );
this.updateClock();
},
// update tracking playback time
updateClock() {
let p = n => ( n < 10 ) ? '0'+n : ''+n;
let elapsed = ( Date.now() - this.timeStart ) / 1000;
let seconds = Math.floor( elapsed % 60 );
let minutes = Math.floor( elapsed / 60 % 60 );
let hours = Math.floor( elapsed / 3600 );
this.timeDisplay = p( hours ) +':'+ p( minutes ) +':'+ p( seconds );
},
// stop tracking playback time
stopClock() {
if ( this.timeItv ) clearInterval( this.timeItv );
this.timeItv = null;
},
// clear timer refs
clearTimers() {
if ( this.sto ) clearTimeout( this.sto );
if ( this.itv ) clearInterval( this.itv );
if ( this.anf ) cancelAnimationFrame( this.anf );
},
},
// on app mounted
mounted() {
this.getChannels();
this.setupAudio();
this.setupMaintenance();
},
// on app destroyed
destroyed() {
this.closeAudio();
this.clearTimers();
}
});
Also see: Tab Triggers