|
859 | 859 | border-color: var(--cp-cyan); |
860 | 860 | } |
861 | 861 |
|
| 862 | + /* monospace code block used for inline documentation (e.g. the sync manifest example) */ |
| 863 | + .cp-code-block { |
| 864 | + border: 1px solid var(--cp-border); |
| 865 | + background: rgba(5, 7, 13, .6); |
| 866 | + color: var(--cp-cyan); |
| 867 | + padding: .75rem 1rem; |
| 868 | + border-radius: 4px; |
| 869 | + overflow-x: auto; |
| 870 | + font-size: .85rem; |
| 871 | + } |
| 872 | + |
862 | 873 | #track { |
863 | 874 | font-family: "Share Tech Mono", monospace; |
864 | 875 | color: var(--cp-cyan); |
@@ -2001,6 +2012,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5> |
2001 | 2012 | <a class="nav-item nav-link" id="nav-wifi-tab" data-bs-toggle="tab" data-bs-target="#nav-wifi" href="#nav-wifi" role="tab" aria-controls="nav-wifi"><i class="fas fa-wifi"></i> <span class="d-none d-md-inline" data-i18n="nav.wifi"></span></a> |
2002 | 2013 | <a class="nav-item nav-link" id="nav-mqtt-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-mqtt" href="#nav-mqtt" role="tab" aria-controls="nav-mqtt"><i class="fas fa-network-wired"></i> <span class="d-none d-md-inline" data-i18n="nav.mqtt"></span></a> |
2003 | 2014 | <a class="nav-item nav-link" id="nav-ftp-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-ftp" href="#nav-ftp" role="tab" aria-controls="nav-ftp"><i class="fas fa-folder"></i> <span class="d-none d-md-inline" data-i18n="nav.ftp"></span></a> |
| 2015 | + <a class="nav-item nav-link" id="nav-sync-tab" data-bs-toggle="tab" data-bs-target="#nav-sync" href="#nav-sync" role="tab" aria-controls="nav-sync"><i class="fas fa-sync"></i> <span class="d-none d-md-inline" data-i18n="nav.sync"></span></a> |
2004 | 2016 | <a class="nav-item nav-link" id="nav-bt-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-bt" href="#nav-bt" role="tab" aria-controls="nav-bt"><i class="fab fa-bluetooth"></i> <span class="d-none d-md-inline" data-i18n="nav.bluetooth"></span></a> |
2005 | 2017 | </div> |
2006 | 2018 | </nav> |
@@ -2733,6 +2745,67 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5> |
2733 | 2745 | </div> |
2734 | 2746 | </div> |
2735 | 2747 | </div> |
| 2748 | + <div class="tab-pane fade" id="nav-sync" role="tabpanel" aria-labelledby="nav-sync-tab"> |
| 2749 | + <div class="container" id="syncConfig"> |
| 2750 | + <form action="#syncConfig" method="POST" onsubmit="syncSettings(); return false"> |
| 2751 | + <div class="mb-3 col-md-12"> |
| 2752 | + <legend data-i18n="sync.title"></legend> |
| 2753 | + <p data-i18n="[html]sync.intro"></p> |
| 2754 | + <label for="syncUrl" data-i18n="[prepend]sync.url.title">:</label> |
| 2755 | + <input type="url" class="form-control" id="syncUrl" data-i18n="[placeholder]sync.url.placeholder" |
| 2756 | + name="syncUrl" value=""> |
| 2757 | + <label for="syncUser" data-i18n="[prepend]sync.user.title">:</label> |
| 2758 | + <input type="text" class="form-control" id="syncUser" |
| 2759 | + data-i18n="[placeholder]sync.user.placeholder" name="syncUser" value="" autocomplete="off"> |
| 2760 | + <label for="syncPwd" data-i18n="[prepend]sync.pwd.title">:</label> |
| 2761 | + <div class="input-group"> |
| 2762 | + <input type="password" class="form-control" id="syncPwd" |
| 2763 | + data-i18n="[placeholder]sync.pwd.placeholder" name="syncPwd" value="" autocomplete="off"> |
| 2764 | + <button class="btn cp-reveal-btn" type="button" id="syncPwdToggle" tabindex="-1" |
| 2765 | + data-i18n="[title]sync.pwd.show" onclick="togglePasswordField('syncPwd', 'syncPwdToggle')"> |
| 2766 | + <i class="fas fa-eye"></i> |
| 2767 | + </button> |
| 2768 | + </div> |
| 2769 | + <small class="text-muted" data-i18n="sync.pwd.hint"></small> |
| 2770 | + </div> |
| 2771 | + <div class="text-center"> |
| 2772 | + <button type="reset" class="btn btn-warning" data-i18n="reset"></button> <button |
| 2773 | + type="submit" class="btn btn-primary" data-i18n="submit"></button> |
| 2774 | + </div> |
| 2775 | + </form> |
| 2776 | + <hr> |
| 2777 | + <div class="col-md-12"> |
| 2778 | + <legend data-i18n="sync.run.title"></legend> |
| 2779 | + <div data-i18n="sync.run.desc"></div> |
| 2780 | + <br> |
| 2781 | + <div id="syncProgress" class="progress upload-progress-bar position-relative"> |
| 2782 | + <div class="progress-bar" role="progressbar"></div> |
| 2783 | + <div |
| 2784 | + class="label d-flex position-absolute flex-column align-self-center text-center w-100 text-nowrap text-light"> |
| 2785 | + </div> |
| 2786 | + </div> |
| 2787 | + <br> |
| 2788 | + <div class="text-center"> |
| 2789 | + <button type="button" class="btn btn-primary" id="syncRunBtn" onclick="startSync()" |
| 2790 | + data-i18n="sync.run.button"></button> |
| 2791 | + </div> |
| 2792 | + </div> |
| 2793 | + <hr> |
| 2794 | + <div class="col-md-12"> |
| 2795 | + <legend data-i18n="sync.manifest.title"></legend> |
| 2796 | + <p data-i18n="[html]sync.manifest.desc"></p> |
| 2797 | + <pre class="cp-code-block"><code>{ |
| 2798 | + "version": 1, |
| 2799 | + "files": [ |
| 2800 | + { "path": "Pumuckl/01.mp3", "size": 4823104 }, |
| 2801 | + { "path": "Pumuckl/02.mp3", "size": 5012992 } |
| 2802 | + ] |
| 2803 | +}</code></pre> |
| 2804 | + <p data-i18n="[html]sync.manifest.fields"></p> |
| 2805 | + <p data-i18n="[html]sync.manifest.nginx"></p> |
| 2806 | + </div> |
| 2807 | + </div> |
| 2808 | + </div> |
2736 | 2809 | <div class="tab-pane fade" id="nav-bt" role="tabpanel" aria-labelledby="nav-bt-tab"> |
2737 | 2810 | <div class="container" id="btConfig"> |
2738 | 2811 | <form action="#btConfig" method="POST" onsubmit="btSettings('btConfig'); return false"> |
@@ -4465,6 +4538,13 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5> |
4465 | 4538 | document.getElementById("ftpPwd").value = (ftpSettings.password === "-1") ? "" : ftpSettings.password; |
4466 | 4539 | document.getElementById("ftpPwd").setAttribute('maxlength', ftpSettings.maxPwdLength); |
4467 | 4540 | } |
| 4541 | + // http file sync |
| 4542 | + let syncSettingsData = settings.sync; |
| 4543 | + if (syncSettingsData) { |
| 4544 | + document.getElementById("syncUrl").value = syncSettingsData.url || ""; |
| 4545 | + document.getElementById("syncUser").value = syncSettingsData.username || ""; |
| 4546 | + document.getElementById("syncPwd").value = syncSettingsData.password || ""; |
| 4547 | + } |
4468 | 4548 | // mqtt |
4469 | 4549 | let mqttSettings = settings.mqtt; |
4470 | 4550 | if (mqttSettings) { |
@@ -4928,6 +5008,68 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5> |
4928 | 5008 | socket.send(myJSON); |
4929 | 5009 | } |
4930 | 5010 |
|
| 5011 | + /* save the HTTP file-sync configuration (manifest URL + optional Basic Auth) */ |
| 5012 | + async function syncSettings() { |
| 5013 | + var myObj = { |
| 5014 | + "sync": { |
| 5015 | + url: document.getElementById('syncUrl').value, |
| 5016 | + username: document.getElementById('syncUser').value, |
| 5017 | + password: document.getElementById('syncPwd').value |
| 5018 | + } |
| 5019 | + }; |
| 5020 | + await fetch("http://" + host + "/settings", { |
| 5021 | + method: "POST", |
| 5022 | + headers: { "Content-Type": "application/json" }, |
| 5023 | + body: JSON.stringify(myObj) |
| 5024 | + }); |
| 5025 | + toaster.info(i18next.t("toast.saved")); |
| 5026 | + } |
| 5027 | + |
| 5028 | + let syncPollTimer = null; |
| 5029 | + function renderSyncProgress(status, progress, message) { |
| 5030 | + const bar = document.querySelector('#syncProgress .progress-bar'); |
| 5031 | + const label = document.querySelector('#syncProgress .label'); |
| 5032 | + if (bar) bar.style.width = progress + '%'; |
| 5033 | + let text = ''; |
| 5034 | + if (status === 1) text = i18next.t('sync.status.syncing', { percent: progress }); |
| 5035 | + else if (status === 2) text = i18next.t('sync.status.done') + (message ? ' (' + message + ')' : ''); |
| 5036 | + else if (status === 3) text = i18next.t('sync.status.failed') + (message ? ': ' + message : ''); |
| 5037 | + if (label) label.textContent = text; |
| 5038 | + } |
| 5039 | + |
| 5040 | + async function pollSync() { |
| 5041 | + try { |
| 5042 | + const r = await fetch("http://" + host + "/sync"); |
| 5043 | + const d = await r.json(); |
| 5044 | + renderSyncProgress(d.status, d.progress, d.message); |
| 5045 | + if (d.status === 1) return; // keep polling |
| 5046 | + // terminal state reached |
| 5047 | + clearInterval(syncPollTimer); |
| 5048 | + syncPollTimer = null; |
| 5049 | + document.getElementById('syncRunBtn').disabled = false; |
| 5050 | + if (d.status === 2) toaster.info(i18next.t('sync.status.done')); |
| 5051 | + else if (d.status === 3) toaster.error(i18next.t('sync.status.failed') + (d.message ? ': ' + d.message : '')); |
| 5052 | + } catch (e) { |
| 5053 | + console.warn('sync poll failed', e); |
| 5054 | + } |
| 5055 | + } |
| 5056 | + |
| 5057 | + /* kick off a sync and poll the device for progress */ |
| 5058 | + async function startSync() { |
| 5059 | + const btn = document.getElementById('syncRunBtn'); |
| 5060 | + btn.disabled = true; |
| 5061 | + renderSyncProgress(1, 0, ''); |
| 5062 | + try { |
| 5063 | + await fetch("http://" + host + "/sync", { method: "POST" }); |
| 5064 | + } catch (e) { |
| 5065 | + btn.disabled = false; |
| 5066 | + toaster.error(i18next.t('toast.error') + ': ' + e.message); |
| 5067 | + return; |
| 5068 | + } |
| 5069 | + if (syncPollTimer) clearInterval(syncPollTimer); |
| 5070 | + syncPollTimer = setInterval(pollSync, 1500); |
| 5071 | + } |
| 5072 | + |
4931 | 5073 | /* Turns the FTP start button into a stop button while the server is running */ |
4932 | 5074 | function updateFtpButton(running) { |
4933 | 5075 | const btn = document.getElementById('ftpModeBtn'); |
|
0 commit comments