"TEBİMTEBİTAGEM GAZETESİ RADYO TELEVİZYONU Music Player "
Bootstrap 4.1.1 Snippet by harunpehlivan

<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <!------ Include the above in your HEAD tag ----------> <!-- 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://raw.githubusercontent.com/rainner/soma-fm-player/master/public/img/bg.jpg'; // 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(); } });

Related: See More


Questions / Comments: