├── README.md ├── api.php ├── assets ├── app │ ├── android.svg │ └── ios.svg ├── capital_logo.png ├── capitallogo_preto.png ├── cover.png ├── default_logo.png ├── favicon.png ├── favicon_demo.png ├── jailson.png ├── jailson_cover.png ├── jailson_logo.png └── radioplayer.svg ├── css └── main.min.css ├── custom.css ├── documentation.pdf ├── index.html ├── index_multiradio.html ├── js └── main.js └── manifest.json /README.md: -------------------------------------------------------------------------------- 1 | ## Single or Multi-Station Radio Player 2 | 3 | This document provides a detailed guide on the structure, configuration, and customization of a single / multi-station radio player built with HTML, CSS, and JavaScript. This player dynamically fetches song information and offers the flexibility to use a local API or a pre-configured web-based API. 4 | 5 | 6 | ## Demo Screenshots 7 | 8 |  9 | 10 | 11 | ### 1. Overview 12 | 13 | This radio player offers a user-friendly interface for enjoying online radio stations. It allows for the addition of multiple stations, each with its own live stream, song information, social media links, and more. Station configuration is done directly within the HTML, simplifying the customization process. 14 | 15 | ### 2. File Structure 16 | 17 | * **`index.html`:** Contains the main HTML for the player, including: 18 | * Visual structure and interactive elements. 19 | * Station configurations within a ` 78 | ``` 79 | 80 | #### 3.2. Local API (Optional) 81 | 82 | If you choose to use the local API (`api.php`), follow these instructions to set it up: 83 | 84 | * **Configuration:** 85 | * In the `api.php` file, the `$allowedUrls` variable should list all allowed stream URLs. 86 | * **Functionality:** 87 | * `getMp3StreamTitle()`: Extracts the song title from the stream metadata. 88 | * `extractArtistAndSong()`: Separates artist and song title. 89 | * `getAlbumArt()`: Fetches album art (currently set up to use the iTunes API). 90 | * `updateHistory()`: Maintains a history of played songs. 91 | 92 | **Note:** 93 | 94 | If the `api` field is left blank in the station configuration, will default to the pre-configured web API. Make sure the web API you are using is functioning and correctly set up within the JavaScript code. 95 | 96 | ### 4. Customization, Interface, Interaction, and Publication 97 | 98 | The sections regarding: 99 | 100 | * **Customizing visual styles** (`css/main.min.css` and `custom.css`) 101 | * **Using custom images and icons** (`assets/`) 102 | * **User interface elements** (header, station selector, history, etc.) 103 | * **User navigation and interaction** 104 | * **Publishing the player to a web server** 105 | 106 | #### 4.1. Key Elements 107 | 108 | * **Header:** Displays the station logo and buttons for accessing the history, station list, and mobile menu. 109 | * **Player Section:** Contains the album art, song information (artist and title), playback controls (play/pause, next/previous station), and volume control. 110 | * **Visualizer:** A simple audio visualizer that responds dynamically to the music. 111 | * **Off-Canvas Sidebar:** 112 | * **Station List:** Displays all available stations with thumbnails. 113 | * **History:** Shows a history of recently played songs. 114 | * **Lyrics Modal:** Displays the lyrics of the currently playing song (if available through the Vagalume API). 115 | 116 | #### 4.2. Navigation 117 | 118 | * **Station Selection:** Click on a station in the station list to begin playback. 119 | * **Song History:** Access the history through the button in the header. 120 | * **Song Lyrics:** Click the "Lyrics" button to open the lyrics modal. 121 | * **Mobile Menu:** The menu button in the header provides access to the same functionality on mobile devices. 122 | 123 | ### 5. Customization 124 | 125 | #### 5.1. Visual Styles (`css/main.min.css` and `custom.css`) 126 | 127 | * Colors, fonts, spacing, element sizes, and other visual properties can be customized by editing the CSS rules. 128 | 129 | #### 5.2. Images and Icons (`assets/`) 130 | 131 | * Replace the default images in the `assets` folder with your own to customize the station logo, album art, and icons. 132 | 133 | ### 6. Publication 134 | 135 | 1. Make sure the local API (`api.php`), if used, is configured correctly and accessible on your server. 136 | 2. Upload all files and folders (HTML, CSS, JavaScript, PHP, images) to your web server. 137 | 138 | ## Free Hosting 139 | 140 | [](https://vercel.com/new/clone?repository-url=https://github.com/jailsonsb2/Radioplayer_api) 141 | [](https://app.netlify.com/start/deploy?repository=https://github.com/jailsonsb2/Radioplayer_api) 142 | 143 | ### 7. Additional Considerations 144 | 145 | * **Copyright:** Ensure that you have the rights to use all images, music, and other content used in your radio player. 146 | * **Stream Metadata:** The accuracy of song information is dependent on the quality of the metadata provided by the radio station's stream. 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /api.php: -------------------------------------------------------------------------------- 1 | 'client_credentials' 18 | ]; 19 | 20 | $options = [ 21 | 'http' => [ 22 | 'header' => implode("\r\n", $headers), 23 | 'method' => 'POST', 24 | 'content' => http_build_query($data) 25 | ] 26 | ]; 27 | 28 | $context = stream_context_create($options); 29 | $response = @file_get_contents($url, false, $context); 30 | 31 | if ($response === false) { 32 | return null; 33 | } 34 | 35 | $tokenData = json_decode($response, true); 36 | return $tokenData['access_token'] ?? null; 37 | } 38 | 39 | function getMp3StreamTitle($streamingUrl, $interval) { 40 | $needle = 'StreamTitle='; 41 | $headers = [ 42 | 'Icy-MetaData: 1', 43 | 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36' 44 | ]; 45 | 46 | $context = stream_context_create([ 47 | 'http' => [ 48 | 'header' => implode("\r\n", $headers), 49 | 'timeout' => 60 // Incrementar timeout a 60 segundos 50 | ] 51 | ]); 52 | 53 | $stream = @fopen($streamingUrl, 'r', false, $context); 54 | if ($stream === false) { 55 | return null; 56 | } 57 | 58 | $metaDataInterval = null; 59 | foreach ($http_response_header as $header) { 60 | if (stripos($header, 'icy-metaint') !== false) { 61 | $metaDataInterval = (int)trim(explode(':', $header)[1]); 62 | break; 63 | } 64 | } 65 | 66 | if ($metaDataInterval === null) { 67 | fclose($stream); 68 | return null; 69 | } 70 | 71 | while (!feof($stream)) { 72 | fread($stream, $metaDataInterval); 73 | $buffer = fread($stream, $interval); 74 | $titleIndex = strpos($buffer, $needle); 75 | if ($titleIndex !== false) { 76 | $title = substr($buffer, $titleIndex + strlen($needle)); 77 | $title = substr($title, 0, strpos($title, ';')); 78 | fclose($stream); 79 | return trim($title, "' "); 80 | } 81 | } 82 | fclose($stream); 83 | return null; 84 | } 85 | 86 | function extractArtistAndSong($title) { 87 | $title = trim($title, "'"); 88 | if (strpos($title, '-') !== false) { 89 | [$artist, $song] = explode('-', $title, 2); 90 | return [trim($artist), trim($song)]; 91 | } 92 | return ['', trim($title)]; 93 | } 94 | 95 | function getAlbumInfo($artist, $song) { 96 | $token = getSpotifyToken(); 97 | if (!$token) { 98 | return [null, 'No disponible', 'No disponible', 'No disponible', 0]; 99 | } 100 | 101 | $url = 'https://api.spotify.com/v1/search?q=' . urlencode("track:$song artist:$artist") . '&type=track&limit=1'; 102 | $headers = [ 103 | 'Authorization: Bearer ' . $token 104 | ]; 105 | 106 | $options = [ 107 | 'http' => [ 108 | 'header' => implode("\r\n", $headers), 109 | 'method' => 'GET' 110 | ] 111 | ]; 112 | 113 | $context = stream_context_create($options); 114 | $response = @file_get_contents($url, false, $context); 115 | if ($response === false) { 116 | return [null, 'No disponible', 'No disponible', 'No disponible', 0]; 117 | } 118 | 119 | $data = json_decode($response, true); 120 | if (isset($data['tracks']['items'][0])) { 121 | $track = $data['tracks']['items'][0]; 122 | $album = $track['album']['name'] ?? 'No disponible'; 123 | $artworkUrl = $track['album']['images'][0]['url'] ?? null; 124 | $year = isset($track['album']['release_date']) ? substr($track['album']['release_date'], 0, 4) : 'No disponible'; 125 | 126 | // Duración en milisegundos 127 | $durationMs = $track['duration_ms'] ?? 0; 128 | 129 | // Obtener el género del artista 130 | $artistId = $track['artists'][0]['id']; 131 | $artistUrl = "https://api.spotify.com/v1/artists/$artistId"; 132 | $artistResponse = @file_get_contents($artistUrl, false, $context); 133 | $artistData = json_decode($artistResponse, true); 134 | $genres = $artistData['genres'] ?? []; 135 | $genre = !empty($genres) ? implode(', ', $genres) : 'No disponible'; 136 | 137 | return [$artworkUrl, $album, $year, $genre, $durationMs]; 138 | } 139 | 140 | return [null, 'No disponible', 'No disponible', 'No disponible', 0]; 141 | } 142 | 143 | function updateHistory($url, $artist, $song) { 144 | $historyFile = 'history_' . md5($url) . '.json'; 145 | $historyLimit = 10; 146 | 147 | if (!file_exists($historyFile)) { 148 | $history = []; 149 | } else { 150 | $history = json_decode(file_get_contents($historyFile), true); 151 | if ($history === null) { 152 | $history = []; 153 | } 154 | } 155 | 156 | $currentSong = ["title" => $song, "artist" => $artist]; 157 | $existingIndex = array_search($currentSong, array_column($history, 'song')); 158 | if ($existingIndex !== false) { 159 | array_splice($history, $existingIndex, 1); 160 | } 161 | 162 | array_unshift($history, ["song" => $currentSong]); 163 | $history = array_slice($history, 0, $historyLimit); 164 | file_put_contents($historyFile, json_encode($history)); 165 | 166 | return $history; 167 | } 168 | 169 | // Funcion Para Leer Las Canciones 170 | header('Content-Type: application/json'); 171 | 172 | // URL de streaming 173 | $url = isset($_GET['url']) ? $_GET['url'] : null; 174 | $interval = isset($_GET['interval']) ? (int)$_GET['interval'] : 19200; 175 | 176 | if ($url === null) { 177 | echo json_encode(["error" => "URL parameter is missing"]); // User-friendly error message 178 | exit; 179 | } 180 | 181 | // Intentar obtener el start_time desde el archivo 182 | $start_time_file = 'start_time_' . md5($url) . '.txt'; 183 | $previous_song_file = 'previous_song_' . md5($url) . '.txt'; 184 | 185 | if (file_exists($previous_song_file)) { 186 | // Leer la canción anterior desde el archivo 187 | $previous_song = file_get_contents($previous_song_file); 188 | } else { 189 | $previous_song = null; 190 | } 191 | 192 | if (file_exists($start_time_file)) { 193 | // Si el archivo existe, leer el start_time desde él 194 | $start_time = (int)file_get_contents($start_time_file); 195 | } else { 196 | // Si no existe, asignar un start_time basado en la hora actual 197 | $start_time = time(); 198 | // Guardar el start_time en el archivo 199 | file_put_contents($start_time_file, $start_time); 200 | } 201 | 202 | if (!filter_var($url, FILTER_VALIDATE_URL)) { 203 | echo json_encode(["error" => "Invalid URL format"]); // More specific error message 204 | exit; 205 | } 206 | 207 | 208 | $title = getMp3StreamTitle($url, $interval); 209 | if ($title) { 210 | [$artist, $song] = extractArtistAndSong($title); 211 | 212 | // Si la canción ha cambiado, reiniciar el start_time 213 | if ($song !== $previous_song) { 214 | // Reiniciar el start_time 215 | $start_time = time(); 216 | file_put_contents($start_time_file, $start_time); 217 | file_put_contents($previous_song_file, $song); // Guardar la canción actual 218 | } 219 | 220 | [$artUrl, $album, $year, $genre, $durationMs] = getAlbumInfo($artist, $song); 221 | 222 | // Convertimos la duración de la canción de milisegundos a segundos 223 | $duration = $durationMs / 1000; // Duración de la canción en segundos 224 | 225 | // Calcular el tiempo transcurrido desde que se inició la canción 226 | $elapsed = time() - $start_time; // Tiempo transcurrido en segundos 227 | $elapsed = min($elapsed, $duration); // Limitar el tiempo transcurrido al tiempo total de la canción 228 | 229 | // Calcular el tiempo restante 230 | $remaining = max(0, $duration - $elapsed); // Tiempo restante, no puede ser negativo 231 | 232 | // Convertir todo a enteros antes de enviar la respuesta 233 | $elapsed = (int) $elapsed; // Elapsed como entero 234 | $remaining = (int) $remaining; // Remaining como entero 235 | $duration = (int) $duration; // Duration como entero 236 | 237 | // Actualizar historial de canciones 238 | $history = updateHistory($url, $artist, $song); 239 | $filteredHistory = array_slice($history, 1); 240 | 241 | $response = [ 242 | "songtitle" => "$artist - $song", 243 | "artist" => $artist, 244 | "song" => $song, 245 | "source" => $url, 246 | "artwork" => $artUrl, 247 | "album" => $album, 248 | "year" => $year, 249 | "genre" => $genre, 250 | "song_history" => $filteredHistory, 251 | "now_playing" => [ 252 | "elapsed" => $elapsed, // Elapsed como entero 253 | "remaining" => $remaining, // Remaining como entero 254 | "duration" => $duration // Duration como entero 255 | ] 256 | ]; 257 | 258 | // Responder con la información en formato JSON 259 | echo json_encode($response); 260 | } else { 261 | echo json_encode(["error" => "The stream title could not be retrieved."]); 262 | } -------------------------------------------------------------------------------- /assets/app/android.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/app/ios.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/capital_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/capital_logo.png -------------------------------------------------------------------------------- /assets/capitallogo_preto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/capitallogo_preto.png -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/cover.png -------------------------------------------------------------------------------- /assets/default_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/default_logo.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/favicon.png -------------------------------------------------------------------------------- /assets/favicon_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/favicon_demo.png -------------------------------------------------------------------------------- /assets/jailson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/jailson.png -------------------------------------------------------------------------------- /assets/jailson_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/jailson_cover.png -------------------------------------------------------------------------------- /assets/jailson_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/assets/jailson_logo.png -------------------------------------------------------------------------------- /assets/radioplayer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/main.min.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #6c6fc6; 3 | --bg-body: #262626; 4 | --bg-app: #0a0a0a; 5 | --bg-inset: #404040; 6 | --bg-transparent: rgb(255 255 255 / 10%); 7 | --bg-modal: rgb(255 255 255 / 20%); 8 | --bg-dark: rgb(0 0 0 / 75%); 9 | --bg-gradient: linear-gradient(140deg, #a92bcd, #439bc1); 10 | --color-title: #ffffff; 11 | --color-text: rgb(255 255 255 / 50%); 12 | --duration: 0.3s; 13 | --container: 1480px; 14 | --spacer: 1rem; 15 | --shadow-l: 0px 8px 17px 2px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12), 0px 5px 5px -3px rgba(0, 0, 0, 0.2); 16 | --shadow-xl: 0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12), 0px 8px 10px -7px rgba(0, 0, 0, 0.2); 17 | --scrollbar-color: rgb(255 255 255 / 50%); 18 | --main-padding: 1rem; 19 | } 20 | html { 21 | line-height: 1.5; 22 | -webkit-text-size-adjust: 100%; 23 | -webkit-font-smoothing: antialiased; 24 | } 25 | *, 26 | ::after, 27 | ::before { 28 | box-sizing: border-box; 29 | } 30 | body, 31 | html { 32 | height: 100%; 33 | } 34 | * { 35 | margin: 0; 36 | } 37 | fieldset, 38 | legend { 39 | padding: 0; 40 | } 41 | fieldset, 42 | iframe { 43 | border-width: 0; 44 | } 45 | a { 46 | color: inherit; 47 | text-decoration: none; 48 | color: var(--primary); 49 | transition: color var(--duration); 50 | } 51 | h1, 52 | h2, 53 | h3, 54 | h4, 55 | h5, 56 | h6 { 57 | font-size: inherit; 58 | font-weight: inherit; 59 | overflow-wrap: break-word; 60 | } 61 | address { 62 | font-style: normal; 63 | line-height: inherit; 64 | } 65 | abbr[title] { 66 | text-decoration: underline dotted; 67 | } 68 | small { 69 | font-size: 80%; 70 | } 71 | sub, 72 | sup { 73 | font-size: 75%; 74 | line-height: 0; 75 | position: relative; 76 | vertical-align: baseline; 77 | } 78 | sub { 79 | bottom: -0.25em; 80 | } 81 | sup { 82 | top: -0.5em; 83 | } 84 | button, 85 | input, 86 | optgroup, 87 | select, 88 | textarea { 89 | padding: 0; 90 | border-width: 0; 91 | font-size: 100%; 92 | font-family: inherit; 93 | line-height: inherit; 94 | color: inherit; 95 | } 96 | input:focus, 97 | textarea:focus { 98 | outline: 0; 99 | } 100 | textarea { 101 | resize: vertical; 102 | } 103 | button, 104 | select { 105 | text-transform: none; 106 | } 107 | [type="button"], 108 | [type="reset"], 109 | [type="submit"], 110 | button { 111 | -webkit-appearance: button; 112 | background-color: transparent; 113 | display: inline-block; 114 | vertical-align: middle; 115 | } 116 | [type="button"]:not(:disabled), 117 | [type="reset"]:not(:disabled), 118 | [type="submit"]:not(:disabled), 119 | button:not(:disabled) { 120 | cursor: pointer; 121 | } 122 | progress { 123 | vertical-align: baseline; 124 | } 125 | ::-webkit-file-upload-button { 126 | -webkit-appearance: button; 127 | font: inherit; 128 | } 129 | summary { 130 | display: list-item; 131 | } 132 | [hidden] { 133 | display: none; 134 | } 135 | dd, 136 | dl, 137 | ol, 138 | ul { 139 | list-style: none; 140 | padding: 0; 141 | } 142 | table { 143 | border-collapse: collapse; 144 | max-width: 100%; 145 | } 146 | tbody, 147 | td, 148 | th, 149 | thead, 150 | tr { 151 | border-width: 0; 152 | text-align: inherit; 153 | } 154 | tr > * { 155 | padding: 0.75rem; 156 | word-break: normal; 157 | } 158 | canvas, 159 | img, 160 | svg, 161 | video { 162 | height: auto; 163 | } 164 | source { 165 | display: none; 166 | } 167 | canvas, 168 | embed, 169 | iframe, 170 | img, 171 | object, 172 | svg, 173 | video { 174 | display: block; 175 | max-width: 100%; 176 | } 177 | audio, 178 | video { 179 | width: 100%; 180 | } 181 | body { 182 | background-size: cover; 183 | background-color: var(--bg-body); 184 | color: var(--color-text); 185 | font-family: Montserrat, sans-serif; 186 | transition: background-color var(--duration); 187 | } 188 | body.preload * { 189 | transition: none !important; 190 | } 191 | @media (max-width: 575px) { 192 | body, 193 | html { 194 | overflow: hidden; 195 | } 196 | } 197 | ::-webkit-resizer { 198 | display: none; 199 | } 200 | b, 201 | strong { 202 | font-weight: 700; 203 | color: var(--color-title); 204 | } 205 | .btn { 206 | display: inline-flex; 207 | align-items: center; 208 | justify-content: center; 209 | column-gap: var(--btn-gap, 0.5rem); 210 | background-color: var(--btn-bg, var(--bg-transparent)); 211 | padding: var(--btn-padding, 0.75rem); 212 | color: var(--btn-color, var(--color-title)); 213 | font-size: var(--btn-fs, 0.875rem); 214 | font-weight: 700; 215 | border-radius: 999px; 216 | line-height: 1.5; 217 | transition-property: box-shadow, background-color, color; 218 | transition-duration: var(--duration); 219 | text-transform: uppercase; 220 | } 221 | .btn:hover { 222 | color: var(--btn-color-hover, var(--color-title)); 223 | } 224 | .btn-full { 225 | width: 100%; 226 | justify-content: center; 227 | } 228 | .truncate { 229 | overflow: hidden; 230 | text-overflow: ellipsis; 231 | white-space: nowrap; 232 | } 233 | .truncate-line { 234 | display: -webkit-box; 235 | -webkit-line-clamp: var(--line-clamp, 2); 236 | -webkit-box-orient: vertical; 237 | overflow: hidden; 238 | } 239 | .app { 240 | background-color: var(--bg-app); 241 | position: relative; 242 | overflow: hidden; 243 | height: 100vh; 244 | width: 100vw; 245 | } 246 | .app::after { 247 | content: ""; 248 | inset: 0; 249 | position: absolute; 250 | background-image: linear-gradient(transparent 70%, #000); 251 | z-index: 5; 252 | } 253 | .header { 254 | position: absolute; 255 | width: 100%; 256 | z-index: 50; 257 | } 258 | .header-wrapper { 259 | padding: var(--main-padding); 260 | } 261 | .header-logo-img { 262 | height: 80px; 263 | } 264 | .toggle-options { 265 | gap: 0.5rem; 266 | } 267 | @media (min-width: 992px) { 268 | :root { 269 | --main-padding: 3vw; 270 | } 271 | .btn { 272 | --btn-fs: 0.875vw; 273 | --btn-padding: 0.75vw; 274 | --i-size: 1.25vw; 275 | --btn-gap: 0.5vw; 276 | } 277 | .header-logo-img { 278 | max-width: 10vw; 279 | height: auto; 280 | } 281 | .toggle-options { 282 | gap: 0.5vw; 283 | } 284 | } 285 | @media (max-width: 991px) { 286 | .header { 287 | background: var(--accent, var(--bg-gradient)); 288 | box-shadow: var(--shadow-l); 289 | } 290 | .toggle-options { 291 | --btn-fs: 0; 292 | --btn-gap: 0; 293 | --i-size: 16px; 294 | } 295 | } 296 | .main > * + * { 297 | margin-top: 3rem; 298 | } 299 | .scrollbar { 300 | overflow: auto; 301 | scrollbar-color: var(--scrollbar-color) transparent; 302 | scrollbar-width: thin; 303 | } 304 | .scrollbar::-webkit-scrollbar { 305 | width: 5px; 306 | height: 5px; 307 | background-color: transparent; 308 | } 309 | .scrollbar::-webkit-scrollbar-track { 310 | background-color: transparent; 311 | border-radius: 5px; 312 | } 313 | .scrollbar::-webkit-scrollbar-thumb { 314 | background-color: var(--scrollbar-color); 315 | border-radius: 10px; 316 | } 317 | .dropdown { 318 | position: absolute; 319 | width: 140px; 320 | background-color: var(--bg-dark); 321 | padding: 1.5rem; 322 | border-radius: 1rem; 323 | left: 50%; 324 | box-shadow: var(--shadow-l); 325 | transform: translateX(-50%); 326 | bottom: calc(100% + 0.5rem); 327 | transition: opacity var(--duration), transform var(--duration); 328 | } 329 | .dropdown:not(.is-active) { 330 | pointer-events: none; 331 | opacity: 0; 332 | transform: translateX(-50%) translateY(-1rem); 333 | } 334 | @media (min-width: 992px) { 335 | .footer { 336 | position: absolute; 337 | padding: var(--main-padding); 338 | bottom: 0; 339 | left: 0; 340 | display: inline-flex; 341 | z-index: 10; 342 | } 343 | .footer-wrapper { 344 | gap: 1.25vw; 345 | } 346 | } 347 | .footer small { 348 | font-size: 1rem; 349 | } 350 | @media (max-width: 991px) { 351 | .footer-app { 352 | padding: var(--main-padding); 353 | border-top: 1px solid; 354 | border-bottom: 1px solid; 355 | justify-content: center; 356 | } 357 | .footer-copyright { 358 | padding: var(--main-padding); 359 | text-align: center; 360 | } 361 | .footer-tv { 362 | padding-bottom: 1rem; 363 | text-align: center; 364 | } 365 | .mobile-menu { 366 | position: fixed; 367 | height: 100vh; 368 | padding-top: calc(2rem + 72px); 369 | z-index: 40; 370 | background: var(--accent, var(--bg-gradient)); 371 | transition: transform var(--duration); 372 | width: 100%; 373 | } 374 | .mobile-menu:not(.is-active) { 375 | pointer-events: none; 376 | transform: translateY(-100%); 377 | } 378 | .player-social { 379 | justify-content: center; 380 | padding: var(--main-padding); 381 | } 382 | } 383 | .i { 384 | stroke-width: var(--i-stroke, 2); 385 | width: var(--i-size, 24px); 386 | height: var(--i-size, 24px); 387 | stroke: currentColor; 388 | stroke-linecap: round; 389 | stroke-linejoin: round; 390 | fill: none; 391 | } 392 | @keyframes pulse { 393 | from { 394 | opacity: 0; 395 | } 396 | 50% { 397 | opacity: 0.2; 398 | } 399 | to { 400 | transform: scale(1.5); 401 | opacity: 0; 402 | } 403 | } 404 | .player { 405 | padding: 2rem; 406 | position: fixed; 407 | inset: 0; 408 | z-index: 10; 409 | overflow-y: auto; 410 | } 411 | @media (max-width: 991px) { 412 | .player { 413 | padding-top: calc(2rem + 72px); 414 | } 415 | } 416 | .player-cover-title { 417 | text-shadow: 0 0.052vw 0.052vw #000; 418 | } 419 | .player-cover-image { 420 | --cover-blurred: 1rem; 421 | position: absolute; 422 | z-index: 0; 423 | object-fit: cover; 424 | object-position: center; 425 | transition: opacity calc(var(--duration) * 3); 426 | filter: blur(var(--cover-blurred)); 427 | max-width: initial; 428 | inset: calc(var(--cover-blurred) * -5); 429 | width: calc(100% + var(--cover-blurred) * 10); 430 | height: calc(100% + var(--cover-blurred) * 10); 431 | } 432 | .player-wrapper { 433 | margin-top: auto; 434 | margin-bottom: auto; 435 | position: relative; 436 | z-index: 10; 437 | } 438 | .player-artwork { 439 | background-color: var(--primary); 440 | border-radius: calc(1rem + 1vw); 441 | box-shadow: var(--shadow-l); 442 | overflow: hidden; 443 | width: 100%; 444 | max-width: 400px; 445 | aspect-ratio: 1/1; 446 | display: flex; 447 | } 448 | .player-artwork img { 449 | transition: transform calc(var(--duration) * 3); 450 | } 451 | .player-controller { 452 | display: flex; 453 | align-items: center; 454 | gap: 1rem; 455 | } 456 | .player-volume { 457 | position: absolute; 458 | inset: 0; 459 | opacity: 0; 460 | pointer-events: none; 461 | } 462 | .player-range-fill { 463 | position: absolute; 464 | top: 0; 465 | left: 0; 466 | height: 100%; 467 | transition: background-color var(--duration); 468 | background-color: var(--accent, var(--primary)); 469 | } 470 | .player-range-wrapper { 471 | position: relative; 472 | height: 2px; 473 | width: 100%; 474 | background-color: rgba(255, 255, 255, 0.25); 475 | } 476 | .player-range-thumb { 477 | width: 15px; 478 | height: 15px; 479 | transition: background-color var(--duration); 480 | background-color: var(--accent, var(--primary)); 481 | border-radius: 5rem; 482 | top: 50%; 483 | position: absolute; 484 | transform: translateY(-50%); 485 | cursor: pointer; 486 | } 487 | .player-button { 488 | color: rgba(255, 255, 255, 0.75); 489 | transition: color var(--duration), background-color var(--duration); 490 | position: relative; 491 | } 492 | @media (min-width: 992px) { 493 | .footer small { 494 | font-size: 0.8vw; 495 | } 496 | .player-controller { 497 | padding-top: 1.5rem; 498 | gap: 2rem; 499 | } 500 | .player-button { 501 | --i-size: 1.5vw; 502 | } 503 | .player-section-audio { 504 | max-width: 390px; 505 | } 506 | } 507 | .player-button-volume { 508 | display: flex; 509 | align-items: center; 510 | } 511 | @media (max-width: 991px) { 512 | .player-button-volume { 513 | opacity: 0.25; 514 | pointer-events: none; 515 | } 516 | } 517 | .player-button.is-active, 518 | .player-button:hover { 519 | color: #fff; 520 | } 521 | .player-button-play { 522 | padding: 1rem; 523 | border-radius: 999px; 524 | transition: background-color var(--duration); 525 | background-color: var(--accent, var(--bg-transparent)); 526 | } 527 | .player-button-play::after, 528 | .player-button-play::before { 529 | pointer-events: none; 530 | content: ""; 531 | position: absolute; 532 | height: 100%; 533 | width: 100%; 534 | background-color: #fff; 535 | border-radius: 50%; 536 | z-index: -1; 537 | inset: 0; 538 | opacity: 0; 539 | animation: 2s ease-out infinite pulse; 540 | display: var(--pulse-state, none); 541 | } 542 | .player-button-play:after { 543 | animation-delay: 1s; 544 | } 545 | .player-button-play:active, 546 | .player-button-play:focus { 547 | outline: 0; 548 | } 549 | .player-button-play.is-active { 550 | --pulse-state: block; 551 | } 552 | .player-section-audio { 553 | flex: none; 554 | } 555 | .player-section-meta { 556 | width: 100%; 557 | } 558 | .player-social { 559 | filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 3px 1px rgba(0, 0, 0, 0.075)); 560 | gap: 0.5rem; 561 | } 562 | @media (min-width: 992px) { 563 | .player-social { 564 | position: absolute; 565 | padding: var(--main-padding); 566 | z-index: 50; 567 | max-width: 40vw; 568 | bottom: 1vw; 569 | right: 0; 570 | gap: 1.125vw; 571 | } 572 | } 573 | .player-social-item { 574 | border: 1px solid #fff; 575 | border-radius: 999px; 576 | padding: 0.75rem; 577 | --i-size: 20px; 578 | } 579 | .player-social-item:not(:hover) { 580 | color: #fff; 581 | } 582 | .player-apps-item { 583 | transition: filter var(--duration); 584 | } 585 | @media (min-width: 992px) { 586 | .player-social-item { 587 | padding: 0.75vw; 588 | --i-size: 1.25vw; 589 | } 590 | .player-apps-item img { 591 | width: auto; 592 | height: 3vw; 593 | } 594 | } 595 | .player-apps-item:hover { 596 | filter: drop-shadow(0 0px 10px white); 597 | } 598 | .player-program { 599 | position: absolute; 600 | display: flex; 601 | flex-direction: column; 602 | justify-content: center; 603 | align-items: center; 604 | width: 100%; 605 | padding: 1rem; 606 | color: #fff; 607 | text-transform: uppercase; 608 | background-image: linear-gradient(transparent, rgba(0, 0, 0, 0.6)); 609 | z-index: 10; 610 | inset: auto 0 0; 611 | } 612 | .player-program-badge { 613 | font-size: 0.75rem; 614 | padding: 0.125rem 0.5rem; 615 | background-color: #c62828; 616 | border-radius: 0.5rem; 617 | } 618 | .player-program-time-container { 619 | display: flex; 620 | align-items: center; 621 | gap: 0.5rem; 622 | } 623 | .player-program-name { 624 | font-weight: 700; 625 | font-family: "Akira Expanded", sans-serif; 626 | } 627 | .player-program-description { 628 | font-size: 0.875rem; 629 | } 630 | .station { 631 | transition: opacity var(--duration); 632 | } 633 | .station-img { 634 | width: 120px; 635 | aspect-ratio: 1/1; 636 | box-shadow: var(--shadow-l); 637 | border-radius: 0.5rem; 638 | } 639 | .station:not(.is-active) { 640 | opacity: 0.5; 641 | } 642 | .station:hover { 643 | opacity: 1; 644 | } 645 | .history { 646 | --cols-min: 20rem; 647 | } 648 | .history-item { 649 | padding: 0.75rem; 650 | background-color: rgba(255, 255, 255, 0.1); 651 | border-radius: 0.5rem; 652 | box-shadow: var(--shadow-l); 653 | width: 100%; 654 | position: relative; 655 | padding-right: calc(0.75rem + 35px); 656 | max-width: 290px; 657 | overflow: hidden; 658 | z-index: 1; 659 | } 660 | .history-item::before { 661 | content: ""; 662 | inset: 0; 663 | position: absolute; 664 | z-index: -1; 665 | background: var(--accent); 666 | opacity: 0.5; 667 | } 668 | .history-spotify { 669 | bottom: 0.75rem; 670 | right: 0.75rem; 671 | position: absolute; 672 | color: #fff; 673 | transition: opacity var(--duration); 674 | } 675 | .history-spotify:not(:hover) { 676 | opacity: 0.5; 677 | } 678 | .history-spotify[href="#not-found"] { 679 | opacity: 0.1; 680 | pointer-events: none; 681 | } 682 | .history-image { 683 | width: 64px; 684 | aspect-ratio: 1/1; 685 | } 686 | .history-image img { 687 | object-fit: cover; 688 | height: 100%; 689 | width: 100%; 690 | } 691 | .visualizer { 692 | position: absolute; 693 | filter: url(#gooey); 694 | inset: auto -20px -20px; 695 | z-index: 0; 696 | pointer-events: none; 697 | display: flex; 698 | align-items: flex-end; 699 | justify-content: space-around; 700 | height: 100%; 701 | opacity: 0.5; 702 | } 703 | .visualizer-filter { 704 | display: none; 705 | } 706 | .modal { 707 | position: fixed; 708 | max-width: 900px; 709 | margin: 0 auto; 710 | z-index: 120; 711 | inset: 1rem; 712 | transition: opacity var(--duration); 713 | display: flex; 714 | } 715 | .modal:not(.is-active) { 716 | pointer-events: none; 717 | opacity: 0; 718 | } 719 | .modal-content { 720 | max-height: 100%; 721 | width: 100%; 722 | background-color: var(--bg-dark); 723 | border-radius: 0.5rem; 724 | box-shadow: var(--shadow-xl); 725 | display: flex; 726 | flex-direction: column; 727 | padding: var(--main-padding); 728 | margin: auto; 729 | } 730 | .modal-title { 731 | margin-bottom: 1.5rem; 732 | line-height: 1; 733 | padding-bottom: 1.5rem; 734 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 735 | } 736 | .modal-body { 737 | font-size: 1.125rem; 738 | } 739 | .modal-overlay { 740 | opacity: var(--modal-overlay-opacity, 0); 741 | z-index: 100; 742 | position: absolute; 743 | inset: 0; 744 | pointer-events: none; 745 | background: rgba(0, 0, 0, 0.5); 746 | backdrop-filter: blur(1rem); 747 | transition: opacity var(--duration); 748 | } 749 | .modal.is-active ~ * { 750 | --modal-overlay-opacity: 1; 751 | } 752 | .modal-video { 753 | inset: 50% auto auto 50%; 754 | position: absolute; 755 | margin: auto; 756 | transform: translate(-50%, -50%); 757 | width: 100%; 758 | max-width: 880px; 759 | padding: 1rem; 760 | background-color: var(--bg-modal); 761 | border-radius: 0.5rem; 762 | z-index: 150; 763 | box-shadow: var(--shadow-xl); 764 | transition: opacity var(--duration); 765 | } 766 | .modal-video [data-close] { 767 | position: absolute; 768 | right: -1.25rem; 769 | top: -1.25rem; 770 | } 771 | .modal-video:not(.is-active) { 772 | visibility: hidden; 773 | pointer-events: none; 774 | opacity: 0; 775 | } 776 | .modal-video iframe { 777 | aspect-ratio: 16/9; 778 | width: 100%; 779 | height: auto; 780 | } 781 | .modal-video.is-active ~ * { 782 | --modal-overlay-opacity: 1; 783 | } 784 | .offcanvas { 785 | background-color: var(--bg-modal); 786 | inset: 0 0 0 auto; 787 | position: absolute; 788 | padding: 1.5rem; 789 | z-index: 120; 790 | box-shadow: var(--shadow-xl); 791 | transition: transform var(--duration), opacity var(--duration); 792 | backdrop-filter: blur(1rem); 793 | } 794 | .offcanvas:not(.is-active) { 795 | transform: translateX(110%); 796 | pointer-events: none; 797 | opacity: 0; 798 | } 799 | .offcanvas [data-close] { 800 | margin-bottom: 1rem; 801 | } 802 | .absolute { 803 | position: absolute; 804 | } 805 | .relative { 806 | position: relative; 807 | } 808 | .fixed { 809 | position: fixed; 810 | } 811 | .sticky { 812 | position: sticky; 813 | } 814 | .z-10 { 815 | z-index: 10; 816 | } 817 | .z-20 { 818 | z-index: 20; 819 | } 820 | .z-30 { 821 | z-index: 30; 822 | } 823 | .z-40 { 824 | z-index: 40; 825 | } 826 | .z-50 { 827 | z-index: 50; 828 | } 829 | .z-60 { 830 | z-index: 60; 831 | } 832 | .z-70 { 833 | z-index: 70; 834 | } 835 | .z-80 { 836 | z-index: 80; 837 | } 838 | .z-90 { 839 | z-index: 90; 840 | } 841 | .z-100 { 842 | z-index: 100; 843 | } 844 | .g-0\.25 { 845 | gap: 0.25rem; 846 | } 847 | .g-0\.5 { 848 | gap: 0.5rem; 849 | } 850 | .g-0\.75 { 851 | gap: 0.75rem; 852 | } 853 | .g-0\.875 { 854 | gap: 0.875rem; 855 | } 856 | .g-1 { 857 | gap: 1rem; 858 | } 859 | .g-1\.25 { 860 | gap: 1.25rem; 861 | } 862 | .g-1\.5 { 863 | gap: 1.5rem; 864 | } 865 | .g-1\.75 { 866 | gap: 1.75rem; 867 | } 868 | .g-2 { 869 | gap: 2rem; 870 | } 871 | .block { 872 | display: block; 873 | } 874 | .inline-block { 875 | display: inline-block; 876 | } 877 | .inline { 878 | display: inline; 879 | } 880 | .flex { 881 | display: flex; 882 | } 883 | .inline-flex { 884 | display: inline-flex; 885 | } 886 | .grid { 887 | display: grid; 888 | } 889 | .inline-grid { 890 | display: inline-grid; 891 | } 892 | .none { 893 | display: none; 894 | } 895 | .items-start { 896 | align-items: flex-start; 897 | } 898 | .items-end { 899 | align-items: flex-end; 900 | } 901 | .items-center { 902 | align-items: center; 903 | } 904 | .justify-start { 905 | justify-content: flex-start; 906 | } 907 | .justify-end { 908 | justify-content: flex-end; 909 | } 910 | .justify-center { 911 | justify-content: center; 912 | } 913 | .justify-between { 914 | justify-content: space-between; 915 | } 916 | .justify-around { 917 | justify-content: space-around; 918 | } 919 | .justify-evenly { 920 | justify-content: space-evenly; 921 | } 922 | .row { 923 | flex-direction: row; 924 | } 925 | .column { 926 | flex-direction: column; 927 | } 928 | @media (min-width: 576px) { 929 | .s\:g-0\.25 { 930 | gap: 0.25rem; 931 | } 932 | .s\:g-0\.5 { 933 | gap: 0.5rem; 934 | } 935 | .s\:g-0\.75 { 936 | gap: 0.75rem; 937 | } 938 | .s\:g-0\.875 { 939 | gap: 0.875rem; 940 | } 941 | .s\:g-1 { 942 | gap: 1rem; 943 | } 944 | .s\:g-1\.25 { 945 | gap: 1.25rem; 946 | } 947 | .s\:g-1\.5 { 948 | gap: 1.5rem; 949 | } 950 | .s\:g-1\.75 { 951 | gap: 1.75rem; 952 | } 953 | .s\:g-2 { 954 | gap: 2rem; 955 | } 956 | .s\:block { 957 | display: block; 958 | } 959 | .s\:inline-block { 960 | display: inline-block; 961 | } 962 | .s\:inline { 963 | display: inline; 964 | } 965 | .s\:flex { 966 | display: flex; 967 | } 968 | .s\:inline-flex { 969 | display: inline-flex; 970 | } 971 | .s\:grid { 972 | display: grid; 973 | } 974 | .s\:inline-grid { 975 | display: inline-grid; 976 | } 977 | .s\:none { 978 | display: none; 979 | } 980 | .s\:row { 981 | flex-direction: row; 982 | } 983 | .s\:column { 984 | flex-direction: column; 985 | } 986 | } 987 | @media (min-width: 768px) { 988 | .m\:g-0\.25 { 989 | gap: 0.25rem; 990 | } 991 | .m\:g-0\.5 { 992 | gap: 0.5rem; 993 | } 994 | .m\:g-0\.75 { 995 | gap: 0.75rem; 996 | } 997 | .m\:g-0\.875 { 998 | gap: 0.875rem; 999 | } 1000 | .m\:g-1 { 1001 | gap: 1rem; 1002 | } 1003 | .m\:g-1\.25 { 1004 | gap: 1.25rem; 1005 | } 1006 | .m\:g-1\.5 { 1007 | gap: 1.5rem; 1008 | } 1009 | .m\:g-1\.75 { 1010 | gap: 1.75rem; 1011 | } 1012 | .m\:g-2 { 1013 | gap: 2rem; 1014 | } 1015 | .m\:block { 1016 | display: block; 1017 | } 1018 | .m\:inline-block { 1019 | display: inline-block; 1020 | } 1021 | .m\:inline { 1022 | display: inline; 1023 | } 1024 | .m\:flex { 1025 | display: flex; 1026 | } 1027 | .m\:inline-flex { 1028 | display: inline-flex; 1029 | } 1030 | .m\:grid { 1031 | display: grid; 1032 | } 1033 | .m\:inline-grid { 1034 | display: inline-grid; 1035 | } 1036 | .m\:none { 1037 | display: none; 1038 | } 1039 | .m\:row { 1040 | flex-direction: row; 1041 | } 1042 | .m\:column { 1043 | flex-direction: column; 1044 | } 1045 | } 1046 | @media (min-width: 992px) { 1047 | .l\:g-0\.25 { 1048 | gap: 0.25rem; 1049 | } 1050 | .l\:g-0\.5 { 1051 | gap: 0.5rem; 1052 | } 1053 | .l\:g-0\.75 { 1054 | gap: 0.75rem; 1055 | } 1056 | .l\:g-0\.875 { 1057 | gap: 0.875rem; 1058 | } 1059 | .l\:g-1 { 1060 | gap: 1rem; 1061 | } 1062 | .l\:g-1\.25 { 1063 | gap: 1.25rem; 1064 | } 1065 | .l\:g-1\.5 { 1066 | gap: 1.5rem; 1067 | } 1068 | .l\:g-1\.75 { 1069 | gap: 1.75rem; 1070 | } 1071 | .l\:g-2 { 1072 | gap: 2rem; 1073 | } 1074 | .l\:block { 1075 | display: block; 1076 | } 1077 | .l\:inline-block { 1078 | display: inline-block; 1079 | } 1080 | .l\:inline { 1081 | display: inline; 1082 | } 1083 | .l\:flex { 1084 | display: flex; 1085 | } 1086 | .l\:inline-flex { 1087 | display: inline-flex; 1088 | } 1089 | .l\:grid { 1090 | display: grid; 1091 | } 1092 | .l\:inline-grid { 1093 | display: inline-grid; 1094 | } 1095 | .l\:none { 1096 | display: none; 1097 | } 1098 | .l\:row { 1099 | flex-direction: row; 1100 | } 1101 | .l\:column { 1102 | flex-direction: column; 1103 | } 1104 | } 1105 | @media (min-width: 1200px) { 1106 | .xl\:g-0\.25 { 1107 | gap: 0.25rem; 1108 | } 1109 | .xl\:g-0\.5 { 1110 | gap: 0.5rem; 1111 | } 1112 | .xl\:g-0\.75 { 1113 | gap: 0.75rem; 1114 | } 1115 | .xl\:g-0\.875 { 1116 | gap: 0.875rem; 1117 | } 1118 | .xl\:g-1 { 1119 | gap: 1rem; 1120 | } 1121 | .xl\:g-1\.25 { 1122 | gap: 1.25rem; 1123 | } 1124 | .xl\:g-1\.5 { 1125 | gap: 1.5rem; 1126 | } 1127 | .xl\:g-1\.75 { 1128 | gap: 1.75rem; 1129 | } 1130 | .xl\:g-2 { 1131 | gap: 2rem; 1132 | } 1133 | .xl\:block { 1134 | display: block; 1135 | } 1136 | .xl\:inline-block { 1137 | display: inline-block; 1138 | } 1139 | .xl\:inline { 1140 | display: inline; 1141 | } 1142 | .xl\:flex { 1143 | display: flex; 1144 | } 1145 | .xl\:inline-flex { 1146 | display: inline-flex; 1147 | } 1148 | .xl\:grid { 1149 | display: grid; 1150 | } 1151 | .xl\:inline-grid { 1152 | display: inline-grid; 1153 | } 1154 | .xl\:none { 1155 | display: none; 1156 | } 1157 | .xl\:row { 1158 | flex-direction: row; 1159 | } 1160 | .xl\:column { 1161 | flex-direction: column; 1162 | } 1163 | } 1164 | .wrap { 1165 | flex-wrap: wrap; 1166 | } 1167 | .wrap-reverse { 1168 | flex-wrap: wrap-reverse; 1169 | } 1170 | .nowrap { 1171 | flex-wrap: nowrap; 1172 | } 1173 | .flex-1 { 1174 | flex: 1 1 0; 1175 | } 1176 | .flex-auto { 1177 | flex: auto; 1178 | } 1179 | .flex-initial { 1180 | flex: initial; 1181 | } 1182 | .flex-none { 1183 | flex: none; 1184 | } 1185 | .content-start { 1186 | align-content: flex-start; 1187 | } 1188 | .content-end { 1189 | align-content: flex-end; 1190 | } 1191 | .content-center { 1192 | align-content: center; 1193 | } 1194 | .content-between { 1195 | align-content: space-between; 1196 | } 1197 | .content-around { 1198 | align-content: space-around; 1199 | } 1200 | .content-evenly { 1201 | align-content: space-evenly; 1202 | } 1203 | .auto-fill { 1204 | grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--cols-min, 16rem)), 1fr)); 1205 | } 1206 | .auto-fit { 1207 | grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--cols-min, 16rem)), 1fr)); 1208 | } 1209 | .o-auto { 1210 | overflow: auto; 1211 | } 1212 | .o-hidden { 1213 | overflow: hidden; 1214 | } 1215 | .ox-auto { 1216 | overflow-x: auto; 1217 | } 1218 | .ox-hidden { 1219 | overflow-x: hidden; 1220 | } 1221 | .oy-auto { 1222 | overflow-y: auto; 1223 | } 1224 | .oy-hidden { 1225 | overflow-y: hidden; 1226 | } 1227 | .events-none { 1228 | pointer-events: none; 1229 | } 1230 | .events-auto { 1231 | pointer-events: auto; 1232 | } 1233 | .color-primary { 1234 | color: var(--primary); 1235 | } 1236 | .color-text { 1237 | color: var(--color-text); 1238 | } 1239 | .color-title { 1240 | color: var(--color-title); 1241 | } 1242 | .fs-1 { 1243 | font-size: 2.5rem; 1244 | } 1245 | .fs-2 { 1246 | font-size: 1.75rem; 1247 | } 1248 | .fs-3 { 1249 | font-size: 1.5rem; 1250 | } 1251 | .fs-4 { 1252 | font-size: 1.25rem; 1253 | } 1254 | .fs-5 { 1255 | font-size: 1.125rem; 1256 | } 1257 | .fs-6 { 1258 | font-size: 1rem; 1259 | } 1260 | .fs-7 { 1261 | font-size: 0.875rem; 1262 | } 1263 | .fs-8 { 1264 | font-size: 0.75rem; 1265 | } 1266 | @media (min-width: 576px) { 1267 | .s\:fs-1 { 1268 | font-size: 2.5rem; 1269 | } 1270 | .s\:fs-2 { 1271 | font-size: 1.75rem; 1272 | } 1273 | .s\:fs-3 { 1274 | font-size: 1.5rem; 1275 | } 1276 | .s\:fs-4 { 1277 | font-size: 1.25rem; 1278 | } 1279 | .s\:fs-5 { 1280 | font-size: 1.125rem; 1281 | } 1282 | .s\:fs-6 { 1283 | font-size: 1rem; 1284 | } 1285 | .s\:fs-7 { 1286 | font-size: 0.875rem; 1287 | } 1288 | .s\:fs-8 { 1289 | font-size: 0.75rem; 1290 | } 1291 | } 1292 | @media (min-width: 768px) { 1293 | .m\:fs-1 { 1294 | font-size: 2.5rem; 1295 | } 1296 | .m\:fs-2 { 1297 | font-size: 1.75rem; 1298 | } 1299 | .m\:fs-3 { 1300 | font-size: 1.5rem; 1301 | } 1302 | .m\:fs-4 { 1303 | font-size: 1.25rem; 1304 | } 1305 | .m\:fs-5 { 1306 | font-size: 1.125rem; 1307 | } 1308 | .m\:fs-6 { 1309 | font-size: 1rem; 1310 | } 1311 | .m\:fs-7 { 1312 | font-size: 0.875rem; 1313 | } 1314 | .m\:fs-8 { 1315 | font-size: 0.75rem; 1316 | } 1317 | } 1318 | @media (min-width: 992px) { 1319 | .l\:fs-1 { 1320 | font-size: 2.5rem; 1321 | } 1322 | .l\:fs-2 { 1323 | font-size: 1.75rem; 1324 | } 1325 | .l\:fs-3 { 1326 | font-size: 1.5rem; 1327 | } 1328 | .l\:fs-4 { 1329 | font-size: 1.25rem; 1330 | } 1331 | .l\:fs-5 { 1332 | font-size: 1.125rem; 1333 | } 1334 | .l\:fs-6 { 1335 | font-size: 1rem; 1336 | } 1337 | .l\:fs-7 { 1338 | font-size: 0.875rem; 1339 | } 1340 | .l\:fs-8 { 1341 | font-size: 0.75rem; 1342 | } 1343 | } 1344 | @media (min-width: 1200px) { 1345 | .xl\:fs-1 { 1346 | font-size: 2.5rem; 1347 | } 1348 | .xl\:fs-2 { 1349 | font-size: 1.75rem; 1350 | } 1351 | .xl\:fs-3 { 1352 | font-size: 1.5rem; 1353 | } 1354 | .xl\:fs-4 { 1355 | font-size: 1.25rem; 1356 | } 1357 | .xl\:fs-5 { 1358 | font-size: 1.125rem; 1359 | } 1360 | .xl\:fs-6 { 1361 | font-size: 1rem; 1362 | } 1363 | .xl\:fs-7 { 1364 | font-size: 0.875rem; 1365 | } 1366 | .xl\:fs-8 { 1367 | font-size: 0.75rem; 1368 | } 1369 | } 1370 | .fw-100 { 1371 | font-weight: 100; 1372 | } 1373 | .fw-200 { 1374 | font-weight: 200; 1375 | } 1376 | .fw-300 { 1377 | font-weight: 300; 1378 | } 1379 | .fw-400 { 1380 | font-weight: 400; 1381 | } 1382 | .fw-500 { 1383 | font-weight: 500; 1384 | } 1385 | .fw-600 { 1386 | font-weight: 600; 1387 | } 1388 | .fw-700 { 1389 | font-weight: 700; 1390 | } 1391 | .fw-800 { 1392 | font-weight: 800; 1393 | } 1394 | .fw-900 { 1395 | font-weight: 900; 1396 | } 1397 | .text-center { 1398 | text-align: center; 1399 | } 1400 | .text-left { 1401 | text-align: left; 1402 | } 1403 | .text-right { 1404 | text-align: right; 1405 | } 1406 | .text-justify { 1407 | text-align: justify; 1408 | } 1409 | .capitalize { 1410 | text-transform: capitalize; 1411 | } 1412 | .uppercase { 1413 | text-transform: uppercase; 1414 | } 1415 | .lowercase { 1416 | text-transform: lowercase; 1417 | } 1418 | .underline { 1419 | text-decoration: underline; 1420 | } 1421 | .line-through { 1422 | text-decoration: line-through; 1423 | } 1424 | -------------------------------------------------------------------------------- /custom.css: -------------------------------------------------------------------------------- 1 | .station-description { 2 | color: #fff; 3 | opacity: 0.6; 4 | } 5 | .station-img { 6 | border: 3px solid #ffffff00; 7 | padding: 0.15rem; 8 | } 9 | .player-artwork { 10 | padding: 0.75rem; 11 | border-radius: 1rem; 12 | background-color: #ffffff00; 13 | } 14 | .player-artwork img { 15 | border-radius: 0.65rem; 16 | box-shadow: var(--shadow-xl); 17 | } 18 | .player-cover-image { 19 | animation: bga 60s linear infinite; 20 | } 21 | @keyframes bga { 22 | 50% { 23 | transform: scale(2); 24 | } 25 | } 26 | .items-start { 27 | align-items: flex-start; 28 | margin-top: 15px; 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /documentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jailsonsb2/Radioplayer_api/83bc7e378cccef30c8953381485606d51f66e6b4/documentation.pdf -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |