<!doctype html> <html> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1'> <title>NetSpeaker Demo (on [IP here])</title> <link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css' rel='stylesheet' integrity='sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN' crossorigin='anonymous'> <style>#playbackMuteButton:hover { background-color: #00000000; }</style> </head> <body data-bs-theme='dark'> <div class='text-center container-sm placeholder-glow' style='padding: 4em 0em'> <p>Welcome to</p> <h1 id='updatedLabel_FRNAME_title'><span class="placeholder col-3"></span></h1> <p id="title_onIP"><span class="placeholder col-1"></span></p> <p style='font-size: .875em; color: var(--bs-code-color); padding-top: .5rem' id="updatedLabel_VERSION_title"><span class="placeholder col-2"></span></p> <hr style='margin: 3em 0em;'> <!----------------------------- ---- INFORMATIONS ACCORDION --- ------------------------------> <div class='accordion text-start' id='accordion-info'> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-info-playback' aria-expanded='true' aria-controls='accordion-info-playback'> Playback information </button> </h2> <div id='accordion-info-playback' class='accordion-collapse collapse' data-bs-parent='#accordion-info'> <div class='accordion-body'> <table class='table table-striped'> <tbody> <tr> <td>Playing</td> <td class='text-end' id="updatedLabel_PLAYING_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Volume</td> <td class='text-end' id="updatedLabel_VOLUME_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Muted</td> <td class='text-end' id="updatedLabel_MUTED_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Equalizer Low</td> <td class='text-end' id="updatedLabel_EQLOW_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Equalizer Mid</td> <td class='text-end' id="updatedLabel_EQMID_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Equalizer High</td> <td class='text-end' id="updatedLabel_EQHIGH_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Balance (range -16 | +16)</td> <td class='text-end' id="updatedLabel_BALANCE_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Path to playlist</td> <td class='text-end' id="updatedLabel_RS_PLPATH_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Index in playlist (starting from 0)</td> <td class='text-end' id="updatedLabel_RS_PLIDX_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource path</td> <td class='text-end' id="updatedLabel_RS_PATH_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource type</td> <td class='text-end' id="updatedLabel_RS_TYPE_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource title</td> <td class='text-end' id="updatedLabel_RS_TITLE_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource album</td> <td class='text-end' id="updatedLabel_RS_ALBUM_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource artist</td> <td class='text-end' id="updatedLabel_RS_ARTIST_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource track number</td> <td class='text-end' id="updatedLabel_RS_TRACKNR_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource year</td> <td class='text-end' id="updatedLabel_RS_YEAR_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource genre</td> <td class='text-end' id="updatedLabel_RS_GENRE_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource copyright</td> <td class='text-end' id="updatedLabel_RS_COPYRIGHT_table_top"><span class="placeholder col-12"></span></td> </tr> <tr> <td>Resource language (for TTS)</td> <td class='text-end' id="updatedLabel_RS_LANGUAGE_table_top"><span class="placeholder col-12"></span></td> </tr> </tbody> </table> </div> </div> </div> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-info-general' aria-expanded='true' aria-controls='accordion-info-general'> General information </button> </h2> <div id='accordion-info-general' class='accordion-collapse collapse' data-bs-parent='#accordion-info'> <div class='accordion-body'> <table class='table table-striped'> <tbody> <tr> <td>Version</td> <td class='text-end' id="updatedLabel_VERSION_table"><span class="placeholder col-4"></span></td> </tr> <tr> <td>Friendly name</td> <td class='text-end' id="updatedLabel_FRNAME_table"><span class="placeholder col-4"></span></td> </tr> <tr> <td>Save & Restore state</td> <td class='text-end' id="updatedLabel_RESTORESTATE_table"><span class="placeholder col-4"></span></td> </tr> <tr> <td>Save & Restore playing state (default no)</td> <td class='text-end' id="updatedLabel_RESTOREPLAYING_table"><span class="placeholder col-4"></span></td> </tr> <tr> <td>WiFi SSID</td> <td class='text-end' id="updatedLabel_WIFISSID_table"><span class="placeholder col-4"></span></td> </tr> </tbody> </table> </div> </div> </div> </div> <hr style='margin: 3em 0em;'> <!----------------------------------> <!---- API FUNCTIONS ACCODRION ----> <!----------------------------------> <div class='accordion text-start' id='accordion'> <!-- PLAYBACK Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-playback' aria-expanded='true' aria-controls='accordion-collapse-playback'> Playback <span class='badge text-bg-success'>/api/v1/playback/</span> <span class='badge text-bg-info'>/api/v1/volume/</span> </button> </h2> <div id='accordion-collapse-playback' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <table class='table table-striped'> <tbody> <tr> <td>Playing</td> <td class='text-end' id='updatedLabel_PLAYING_table_bottom'><span class="placeholder col-4"></span></td> </tr> <tr> <td>Volume</td> <td class='text-end' id='updatedLabel_VOLUME_table_bottom'><span class="placeholder col-4"></span></td> </tr> <tr> <td>Muted</td> <td class='text-end' id='updatedLabel_MUTED_table_bottom'><span class="placeholder col-4"></span></td> </tr> </tbody> </table> <hr> <div class='btn-group' role='group' aria-label='Basic playback option (play, pause, next, previous)'> <button type='button' class='btn btn-secondary' onclick="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/playback/previous');http.send();updateStrings();"> <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-rewind-circle' viewBox='0 0 16 16'> <path d='M7.729 5.055a.5.5 0 0 0-.52.038l-3.5 2.5a.5.5 0 0 0 0 .814l3.5 2.5A.5.5 0 0 0 8 10.5V8.614l3.21 2.293A.5.5 0 0 0 12 10.5v-5a.5.5 0 0 0-.79-.407L8 7.386V5.5a.5.5 0 0 0-.271-.445'/> <path d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8'/> </svg> </button> <button type='button' class='btn btn-success' onclick="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/playback/play');http.send();updateStrings();"> <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-play-circle' viewBox='0 0 16 16'> <path d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'></path> <path d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445'></path> </svg> </button> <button type='button' class='btn btn-success' onclick="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/playback/pause');http.send();updateStrings();"> <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-pause-circle' viewBox='0 0 16 16'> <path d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/> <path d='M5 6.25a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0zm3.5 0a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0z'/> </svg> </button> <button type='button' class='btn btn-secondary' onclick="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/playback/next');http.send();updateStrings();"> <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-skip-forward-circle' viewBox='0 0 16 16'> <path d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/> <path d='M4.271 5.055a.5.5 0 0 1 .52.038L7.5 7.028V5.5a.5.5 0 0 1 .79-.407L11 7.028V5.5a.5.5 0 0 1 1 0v5a.5.5 0 0 1-1 0V8.972l-2.71 1.935a.5.5 0 0 1-.79-.407V8.972l-2.71 1.935A.5.5 0 0 1 4 10.5v-5a.5.5 0 0 1 .271-.445'/> </svg> </button> </div> <br> <hr> <br><label for='volumeRange' class='form-label'>Volume</label> <input type='range' class='form-range' value='0' min='0' max='0' id='updatedLabel_VOLUME_range_bottom' onchange="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/volume/' + this.value);http.send();updateStrings();"> <button type='button' id='playbackMuteButton' onclick= "http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/volume/toggle_mute');http.send();updateStrings();" class='btn btn-danger'> <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-volume-mute' viewBox='0 0 16 16'> <path d='M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04 4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96zm7.854.606a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z'></path> </svg> Mute/Unmute </button> </div> </div> </div> <!--PLAYLIST Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-playlist' aria-expanded='true' aria-controls='accordion-collapse-playlist'> Playlist <span class='badge text-bg-success'>/api/v1/playlist/</span> </button> </h2> <div id='accordion-collapse-playlist' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <!--play playlist (select playlist) api demo --> <p>Demo for playing a specific playlist on the SD card over the API.</p> <div class='input-group mb-3'> <input type='text' class='form-control' placeholder='Playlist path (must start with a /)' aria-label='Playlist path (must start with a /)' aria-describedby='playlist_playBtn' id='playlist_playPathInput'> <button class='btn btn-success' type='button' id='playlist_playBtn' onclick= "playlistPath = document.getElementById('playlist_playPathInput').value; http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/playlist/play?playlist_path='+playlistPath);http.send(); updateStrings();"> Change </button> </div> <hr> <!-- Create playlist api demo --> <p>Demo for creating a new playlist containing all the contents of the given folder (on the SD card).</p> <p>The new playlist will be located in the given folder and is named <code>.directory.m3u</code>. This can't be changed. </p> <p>Creating fully personalized playlist or even uploading one will be subject of further development (see also the Roadmap of the NetSpeaker project!).</p> <div class='input-group mb-3'> <input type='text' class='form-control' placeholder='Folder path (must start with and end without a /)' aria-label='Folder path (must start with a /)' aria-describedby='playlist_createBtn' id='playlist_createPathInput'> <button class='btn btn-success' type='button' id='playlist_createBtn' onclick= "folderPath = document.getElementById('playlist_createPathInput').value; http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/playlist/create?folder_path='+folderPath);http.send(); updateStrings();"> Change </button> </div> </div> </div> </div> <!--BALANCE Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-balance' aria-expanded='true' aria-controls='accordion-collapse-balance'> Balance <span class='badge text-bg-info'>/api/v1/balance/</span> </button> </h2> <div id='accordion-collapse-balance' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <p>The Audio library used maps -16 to most right and 16 to most left. Don't be confused by the appearently wrong direction the slider moves the balance to right and left.</p> <table class='table table-striped'> <tbody> <tr> <td>Balance (range -16 | +16)</td> <td class='text-end' id='updatedLabel_BALANCE_table_bottom'><span class="placeholder col-12"></span></td> </tr> </tbody> </table> <br> <hr> <label for='balanceRange' class='form-label'>Balance (-16 to 16)</label> <input type='range' class='form-range' value='16' min='0' max='32' id='updatedLabel_BALANCE_range_bottom' onchange="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/balance/' + this.value);http.send();updateStrings();"> </div> </div> </div> <!-- EQUALIZER Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-eq' aria-expanded='true' aria-controls='accordion-collapse-eq'> Equalizer <span class='badge text-bg-info'>/api/v1/eq/</span> </button> </h2> <div id='accordion-collapse-eq' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <table class='table table-striped'> <tbody> <tr> <td>Equalizer Low (-40dB to 6dB)</td> <td class='text-end' id='updatedLabel_EQLOW_table_bottom'><span class="placeholder col-12"></span></td> </tr> <tr> <td>Equalizer Mid (-40dB to 6dB)</td> <td class='text-end' id='updatedLabel_EQMID_table_bottom'><span class="placeholder col-12"></span></td> </tr> <tr> <td>Equalizer High (-40dB to 6dB)</td> <td class='text-end' id='updatedLabel_EQHIGH_table_bottom'><span class="placeholder col-12"></span></td> </tr> </tbody> </table> <br> <hr> <label for='eqLowRange' class='form-label'>Equalizer for lows (-40dB to 6dB)</label> <input type='range' class='form-range' value='" + String(eqLow+40) + "' min='0' max='46' id='updatedLabel_EQLOW_range_bottom' onchange="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/eq/low/' + this.value);http.send();updateStrings();"> <br> <hr> <br><label for='eqMidRange' class='form-label'>Equalizer for mids (-40dB to 6dB)</label> <input type='range' class='form-range' value='" + String(eqMid+40) + "' min='0' max='46' id='updatedLabel_EQMID_range_bottom' onchange="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/eq/mid/' + this.value);http.send();updateStrings();"> <br> <hr> <br><label for='eqHighRange' class='form-label'>Equalizer for highs (-40dB to 6dB)</label> <input type='range' class='form-range' value='" + String(eqHigh+40) + "' min='0' max='46' id='updatedLabel_EQHIGH_range_bottom' onchange="http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/eq/high/' + this.value);http.send();updateStrings();"> </div> </div> </div> <!-- FRIENDLY NAME Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-frname' aria-expanded='true' aria-controls='accordion-collapse-frname'> Friendly name <span class='badge text-bg-warning'>/api/v1/system/name</span> <!--explicitly no trailing / as there are the two endpoints /name and /name/change --> </button> </h2> <div id='accordion-collapse-frname' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <div class='input-group mb-3'> <span class='input-group-text' id='updatedLabel_FRNAME_oldinputtext'>...</span> <span class='input-group-text'> -> </span> <input type='text' class='form-control' placeholder='New friendly name' aria-label='New friendly name' aria-describedby='frnameChange_BTN_submitName' id='frnameChange_newNameInput'> <button class='btn btn-success' type='button' id='frnameChange_BTN_submitName' onclick=" newName=document.getElementById('frnameChange_newNameInput').value;oldName=document.getElementById('updatedLabel_FRNAME_oldinputtext').innerHTML; if(oldName != newName) { http = new XMLHttpRequest();http.open('GET', 'http://' + document.cookie + '/api/v1/system/name/change?friendly_name=' + newName);http.send(); updateStrings(); }"> Change </button> </div> </div> </div> </div> <!-- WiFi CONFIGURATION Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-wifi' aria-expanded='true' aria-controls='accordion-collapse-wifi'> WiFi configuration <span class='badge text-bg-warning'>/api/v1/system/wifi/</span> </button> </h2> <div id='accordion-collapse-wifi' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <p>Demo for changing the wifi SSID and PSK (= Pre-Shared Key; well-known as 'Password' or 'Key'). There's no need to give both - if you just wanna change the password - do it!</p> <p>After doing this, you have to go down to the restart collapsible. There, restart the NetSpeaker and it should connect to the configured WiFi.</p> <p>If anything is wrong (either SSID or PSK wrong/typo, or the wifi's not available), the NetSpeaker opens an HotSpot with the SSID <code></code> and the PSK <code></code>! </p> <div class='input-group mb-3'> <span class='input-group-text'>SSID</span> <input type='text' class='form-control' placeholder='Type in a new SSID here (optional)' aria-label='Type in a new SSID here (optional)' aria-describedby='wifiChange_submitBtn' id='wifiChange_ssidInput'> <span class='input-group-text'>PSK</span> <input type='password' class='form-control' placeholder='Type in a new PSK here (optional)' aria-label='Type in a new PSK here (optional)' aria-describedby='wifiChange_submitBtn' id='wifiChange_pskInput'> <button class='btn btn-success' type='button' id='wifiChange_submitBtn' onclick= "newSSID = document.getElementById('wifiChange_ssidInput').value; newPSK = document.getElementById('wifiChange_pskInput').value; http = new XMLHttpRequest();http.open('GET', '/api/v1/system/wifi/change?ssid='+newSSID+'&psk='+newPSK);http.send();"> Change </button> </div> </div> </div> </div> <!-- SAVE AND RESTORE Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-restore' aria-expanded='true' aria-controls='accordion-collapse-restore'> Save & Restore <span class='badge text-bg-warning'>/api/v1/system/restore_state/</span> <span class='badge text-bg-warning'>/api/v1/system/restore_playing/</span> </button> </h2> <div id='accordion-collapse-restore' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <p>Restoring the old/last state can be toggeled on or off over the API. Doing so can have many reasons, one could be that you wanted to build a device always playing some startup sound, or so.</p> <div class='form-check form-switch'> <input onclick="this.value ? console.log('Toggeled on') : console.log('Toggeled off');" class='form-check-input' type='checkbox' role='switch' id='restoreStateSwitch' " + configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true) ? "checked" : String() + " > <label class='form-check-label' for='restoreStateSwitch'>Restore last saved state</label> </div> <div class='form-check form-switch'> <input onclick="this.value ? console.log('Toggeled on') : console.log('Toggeled off');" class='form-check-input' type='checkbox' role='switch' id='restorePlayingSwitch' " + configuration.getBool(PREFERENCES_KEY_RESTORE_PLAYING, true) ? "checked" : String() + " > <label class='form-check-label' for='restorePlayingSwitch'>Also restore last playing state</label> </div> </div> </div> </div> <!-- RESTART Collapsible --> <div class='accordion-item'> <h2 class='accordion-header'> <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-restart' aria-expanded='true' aria-controls='accordion-collapse-restart'> Restart <span class='badge text-bg-danger'>/api/v1/system/restart</span> </button> </h2> <div id='accordion-collapse-restart' class='accordion-collapse collapse' data-bs-parent='#accordion'> <div class='accordion-body'> <button type='button' class='btn btn-danger' onclick="function wait(ms){var start=new Date().getTime();var end=start;while(end<start+ms){end = new Date().getTime();}}http = new XMLHttpRequest();http.open('GET', '/api/v1/system/restart');http.send();wait(3000);window.location.reload();"> <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-bootstrap-reboot' viewBox='0 0 16 16'> <path d='M1.161 8a6.84 6.84 0 1 0 6.842-6.84.58.58 0 1 1 0-1.16 8 8 0 1 1-6.556 3.412l-.663-.577a.58.58 0 0 1 .227-.997l2.52-.69a.58.58 0 0 1 .728.633l-.332 2.592a.58.58 0 0 1-.956.364l-.643-.56A6.812 6.812 0 0 0 1.16 8z'></path> <path d='M6.641 11.671V8.843h1.57l1.498 2.828h1.314L9.377 8.665c.897-.3 1.427-1.106 1.427-2.1 0-1.37-.943-2.246-2.456-2.246H5.5v7.352zm0-3.75V5.277h1.57c.881 0 1.416.499 1.416 1.32 0 .84-.504 1.324-1.386 1.324h-1.6z'></path> </svg> Restart </button> </div> </div> </div> </div> <!-- Modal --> <div class="modal fade" id="selectIPAddressModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="selectIPAddressModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h1 class="modal-title fs-5" id="selectIPAddressModalLabel">NetSpeaker configuration</h1> </div> <div class="text-start modal-body"> <p>Please give the IP adress of you NetSpeaker here, so that this site can access its API.</p> <div class="mb-3"> <div class="input-group"> <span class="input-group-text">http://</span> <input type="text" class="form-control" id="ipAdressInput"> </div> </div> <div id="configWorkingAlertPlaceholder"></div> </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" id="ipAdressModalApplyBtn">Apply & Test</button> <button type="button" class="btn btn-success" id="ipAdressModalOkBtn">Ok</button> </div> </div> </div> </div> </div> <script src='https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js' integrity='sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL' crossorigin='anonymous'></script> <script type="text/javascript"> function isValidIP(ipaddress) { if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) { return true; } return false; } function appendAlert(alertPlaceholder, message, type) { const wrapper = document.createElement('div') wrapper.innerHTML = [ `<div class="alert alert-${type} alert-dismissible" role="alert">`, ` <div>${message}</div>`, ' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>', '</div>' ].join('') alertPlaceholder.append(wrapper) } versionLabel1 = document.getElementById('updatedLabel_VERSION_title'); versionLabel2 = document.getElementById('updatedLabel_VERSION_table'); frnameLabel1 = document.getElementById('updatedLabel_FRNAME_title'); frnameLabel2 = document.getElementById('updatedLabel_FRNAME_table'); frnameLabel3 = document.getElementById('updatedLabel_FRNAME_oldinputtext'); restoreStateLabel1 = document.getElementById('updatedLabel_RESTORESTATE_table') restorePlayingStateLabel1 = document.getElementById('updatedLabel_RESTOREPLAYING_table') wifiLabel1 = document.getElementById('updatedLabel_WIFISSID_table'); const playingLabel1 = document.getElementById("updatedLabel_PLAYING_table_top"); const playingLabel2 = document.getElementById("updatedLabel_PLAYING_table_bottom"); const rs_plpathLabel1 = document.getElementById("updatedLabel_RS_PLPATH_table_top"); const rs_plidxLabel1 = document.getElementById("updatedLabel_RS_PLIDX_table_top"); const rs_pathLabel1 = document.getElementById("updatedLabel_RS_PATH_table_top"); const rs_typeLabel1 = document.getElementById("updatedLabel_RS_TYPE_table_top"); const rs_titleLabel1 = document.getElementById("updatedLabel_RS_TITLE_table_top"); const rs_albumLabel1 = document.getElementById("updatedLabel_RS_ALBUM_table_top"); const rs_artistLabel1 = document.getElementById("updatedLabel_RS_ARTIST_table_top"); const rs_tracknrLabel1 = document.getElementById("updatedLabel_RS_TRACKNR_table_top"); const rs_yearLabel1 = document.getElementById("updatedLabel_RS_YEAR_table_top"); const rs_genreLabel1 = document.getElementById("updatedLabel_RS_GENRE_table_top"); const rs_copyrightLabel1 = document.getElementById("updatedLabel_RS_COPYRIGHT_table_top"); const rs_languageLabel1 = document.getElementById("updatedLabel_RS_LANGUAGE_table_top"); volumeLabel1 = document.getElementById('updatedLabel_VOLUME_table_top'); volumeLabel2 = document.getElementById('updatedLabel_VOLUME_table_bottom'); volumeRange1 = document.getElementById('updatedLabel_VOLUME_range_bottom'); mutedLabel1 = document.getElementById('updatedLabel_MUTED_table_top'); mutedLabel2 = document.getElementById('updatedLabel_MUTED_table_bottom'); eqLowLabel1 = document.getElementById('updatedLabel_EQLOW_table_top'); eqLowLabel2 = document.getElementById('updatedLabel_EQLOW_table_bottom'); eqLowRange1 = document.getElementById('updatedLabel_EQLOW_range_bottom'); eqMidLabel1 = document.getElementById('updatedLabel_EQMID_table_top'); eqMidLabel2 = document.getElementById('updatedLabel_EQMID_table_bottom'); eqMidRange1 = document.getElementById('updatedLabel_EQMID_range_bottom'); eqHighLabel1 = document.getElementById('updatedLabel_EQHIGH_table_top'); eqHighLabel2 = document.getElementById('updatedLabel_EQHIGH_table_bottom'); eqHighRange1 = document.getElementById('updatedLabel_EQHIGH_range_bottom'); balanceLabel1 = document.getElementById('updatedLabel_BALANCE_table_top'); balanceLabel2 = document.getElementById('updatedLabel_BALANCE_table_bottom'); balanceRange1 = document.getElementById('updatedLabel_BALANCE_range_bottom'); function updateStrings() { apiBase = "http://" + document.cookie; versionApiEndpoint = "/api/v1/system/version"; frnameApiEndpoint = "/api/v1/system/name"; restoreStateApiEndpoint = "/api/v1/system/restore_state/get"; restorePlayingStateApiEndpoint = "/api/v1/system/restore_playing/get"; wifiApiEndpoint = "/api/v1/system/wifi/get_ssid"; playbackInfoApiEndpoint = "/api/v1/playback/info"; volumeApiEndpoint = "/api/v1/volume/get"; eqApiEndpoint = "/api/v1/eq/get"; balanceApiEndpoint = "/api/v1/balance/get"; /* Version string parser */ var versionRequest = new XMLHttpRequest(); versionRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var versionString = JSON.parse(this.responseText).version; versionLabel1.innerHTML = versionString; versionLabel2.innerHTML = versionString; } } versionRequest.open("GET", apiBase + versionApiEndpoint, true); versionRequest.send(); /* friendly name parser */ var frnameRequest = new XMLHttpRequest(); frnameRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var frnameString = JSON.parse(this.responseText).friendly_name; frnameLabel1.innerHTML = frnameString; frnameLabel2.innerHTML = frnameString; frnameLabel3.innerHTML = frnameString; } } frnameRequest.open("GET", apiBase + frnameApiEndpoint, true); frnameRequest.send(); /* Save & Restore parser */ var restoreStateRequest = new XMLHttpRequest(); restoreStateRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var restoreStateString = JSON.parse(this.responseText).restore_state; restoreStateLabel1.innerHTML = restoreStateString ? "yes" : "no"; } } restoreStateRequest.open("GET", apiBase + restoreStateApiEndpoint, true); restoreStateRequest.send(); var restorePlayingRequest = new XMLHttpRequest(); restorePlayingRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var restorePlayingString = JSON.parse(this.responseText).restore_playing; restorePlayingStateLabel1.innerHTML = restorePlayingString ? "yes" : "no"; } } restorePlayingRequest.open("GET", apiBase + restorePlayingStateApiEndpoint, true); restorePlayingRequest.send(); /* WiFi SSID parser */ var wifiRequest = new XMLHttpRequest(); wifiRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var wifiSsidString = JSON.parse(this.responseText).wifi_ssid; wifiLabel1.innerHTML = wifiSsidString; } } wifiRequest.open("GET", apiBase + wifiApiEndpoint, true); wifiRequest.send(); /* Playback info parser */ var playbackInfoRequest = new XMLHttpRequest(); playbackInfoRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var playbackInfoResponse = JSON.parse(this.responseText); playingLabel1.innerHTML = playbackInfoResponse.state == "playing" ? "yes" : "no"; playingLabel2.innerHTML = playbackInfoResponse.state == "playing" ? "yes" : "no"; rs_plpathLabel1.innerHTML = playbackInfoResponse.resource_playlist_path; rs_plidxLabel1.innerHTML = playbackInfoResponse.resource_playlist_index; rs_pathLabel1.innerHTML = playbackInfoResponse.resource_path; rs_typeLabel1.innerHTML = playbackInfoResponse.resource_type; rs_titleLabel1.innerHTML = playbackInfoResponse.resource_title; rs_albumLabel1.innerHTML = playbackInfoResponse.resource_album; rs_artistLabel1.innerHTML = playbackInfoResponse.resource_artist; rs_tracknrLabel1.innerHTML = playbackInfoResponse.resource_track; rs_yearLabel1.innerHTML = playbackInfoResponse.resource_year; rs_genreLabel1.innerHTML = playbackInfoResponse.resource_genre; rs_copyrightLabel1.innerHTML = playbackInfoResponse.resource_copyright; rs_languageLabel1.innerHTML = playbackInfoResponse.resource_tts_language; } } playbackInfoRequest.open("GET", apiBase + playbackInfoApiEndpoint, true); playbackInfoRequest.send(); /* Volume+Muted parser */ var volumeRequest = new XMLHttpRequest(); volumeRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var volumeValue = JSON.parse(this.responseText).volume; var maxVolumeValue = JSON.parse(this.responseText).volume_max; var mutedValue = JSON.parse(this.responseText).muted; volumeLabel1.innerHTML = volumeValue + "/" + maxVolumeValue; volumeLabel2.innerHTML = volumeValue + "/" + maxVolumeValue; mutedLabel1.innerHTML = mutedValue ? "yes" : "no"; mutedLabel2.innerHTML = mutedValue ? "yes" : "no"; volumeRange1.max = maxVolumeValue; volumeRange1.value = volumeValue; } } volumeRequest.open("GET", apiBase + volumeApiEndpoint, true); volumeRequest.send(); /* Equalizer parser */ var eqRequest = new XMLHttpRequest(); eqRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var eqLowValue = JSON.parse(this.responseText).equalizer_low; var eqMidValue = JSON.parse(this.responseText).equalizer_mid; var eqHighValue = JSON.parse(this.responseText).equalizer_high; eqLowLabel1.innerHTML = eqLowValue + "dB"; eqLowLabel2.innerHTML = eqLowValue + "dB"; eqLowRange1.value = eqLowValue + 40; eqMidLabel1.innerHTML = eqMidValue + "dB"; eqMidLabel2.innerHTML = eqMidValue + "dB"; eqMidRange1.value = eqMidValue + 40; eqHighLabel1.innerHTML = eqHighValue + "dB"; eqHighLabel2.innerHTML = eqHighValue + "dB"; eqHighRange1.value = eqHighValue + 40; } } eqRequest.open("GET", apiBase + eqApiEndpoint, true); eqRequest.send(); /* Balance parser */ var balanceRequest = new XMLHttpRequest(); balanceRequest.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var balanceValue = JSON.parse(this.responseText).balance; balanceLabel1.innerHTML = balanceValue; balanceLabel2.innerHTML = balanceValue; balanceRange1.value = balanceValue + 16; // as the slider goes from 0 to 32 (as the API accepts it) } } balanceRequest.open("GET", apiBase + balanceApiEndpoint, true); balanceRequest.send(); } function setupSite() { document.title = "NetSpeaker Demo (on " + document.cookie + ")"; document.getElementById("title_onIP").innerHTML = "on " + document.cookie; setTimeout(updateStrings, 1000); // to make user see the epic placeholder glow effect of the bootstrap library setInterval(updateStrings, 10000); } window.onload = function(e) { if (document.cookie == "" || !isValidIP(document.cookie)) { var selectIPModal = new bootstrap.Modal(document.getElementById('selectIPAddressModal'), {}); selectIPModal.show(); const alertPlaceholder = document.getElementById('configWorkingAlertPlaceholder'); function configButtonEventListener(hideModalOnSuccess) { var ipAddressEntered = document.getElementById('ipAdressInput').value; if(isValidIP(ipAddressEntered)) { var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { appendAlert(alertPlaceholder, 'Working Configuration!', 'success'); document.cookie = ipAddressEntered; setupSite() setTimeout(function(){ if(hideModalOnSuccess) selectIPModal.hide(); }, 500); } else if (this.readyState != 4) { // just do nothing, wait! } else { appendAlert(alertPlaceholder, 'Configuration not working', 'danger'); } }; request.open("GET", "http://" + ipAddressEntered + "/api/v1/system/version", true); request.send() } else { appendAlert(alertPlaceholder, 'Configuration not working', 'danger'); } } const ipAdressModalApplyBtn = document.getElementById('ipAdressModalApplyBtn'); ipAdressModalApplyBtn.addEventListener('click', () => { configButtonEventListener(false); }); const ipAdressModalOkBtn = document.getElementById('ipAdressModalOkBtn'); ipAdressModalOkBtn.addEventListener('click', () => { configButtonEventListener(true); }); } else if (isValidIP(document.cookie)) setupSite(); } // end of onload function </script> </body> </html> <!-- An uncomplete list of XML IDs used in this piece of HTML: updatedLabel_PLAYING_table_top updatedLabel_PLAYING_table_bottom updatedLabel_VOLUME_table_top updatedLabel_VOLUME_table_bottom updatedLabel_MUTED_table_top updatedLabel_MUTED_table_bottom updatedLabel_BALANCE_table_top updatedLabel_BALANCE_table_bottom updatedLabel_EQLOW_table_top updatedLabel_EQLOW_table_bottom updatedLabel_EQMID_table_top updatedLabel_EQMID_table_bottom updatedLabel_EQHIGH_table_top updatedLabel_EQHIGH_table_bottom updatedLabel_RS_PLPATH_table_top updatedLabel_RS_PLIDX_table_top updatedLabel_RS_PATH_table_top updatedLabel_RS_TYPE_table_top updatedLabel_RS_TITLE_table_top updatedLabel_RS_ALBUM_table_top updatedLabel_RS_ARTIST_table_top updatedLabel_RS_TRACKNR_table_top updatedLabel_RS_YEAR_table_top updatedLabel_RS_GENRE_table_top updatedLabel_RS_COPYRIGHT_table_top updatedLabel_RS_LANGUAGE_table_top -->