iobroker.script-restore 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/admin/tab_m.html CHANGED
@@ -148,6 +148,7 @@
148
148
 
149
149
  .script-item:hover { background-color: #f0f7ff; }
150
150
  .script-item.active { background-color: #e7f1ff; border-left: 3px solid var(--primary); padding-left: calc(12px - 3px); }
151
+ .script-item.selected { background-color: #fff3cd; border-left: 3px solid #ffc107; padding-left: calc(12px - 3px); }
151
152
  .tree-script.active { padding-left: calc(32px - 3px); }
152
153
  .tree-children .tree-children .tree-script.active { padding-left: calc(50px - 3px); }
153
154
  .tree-children .tree-children .tree-children .tree-script.active { padding-left: calc(68px - 3px); }
@@ -225,7 +226,7 @@
225
226
  @keyframes spin { 100% { transform: rotate(360deg); } }
226
227
  #loaderText { color: #495057; font-size: 0.95rem; }
227
228
 
228
- /* Local files dropdown */
229
+ /* Files dropdown */
229
230
  .dropdown-wrapper { position: relative; }
230
231
  .dropdown-menu {
231
232
  display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
@@ -322,12 +323,41 @@
322
323
  <input type="file" id="fileInput" accept=".tar,.gz,.tar.gz,.json,.jsonl">
323
324
  </label>
324
325
  <div class="dropdown-wrapper" id="localDropdown">
325
- <button class="btn btn-outline" onclick="toggleLocalFiles()">
326
+ <button class="btn btn-outline" onclick="toggleDropdown('local')">
326
327
  🗂️ Lokale Backups ▾
327
328
  </button>
328
329
  <div class="dropdown-menu" id="localMenu"></div>
329
330
  </div>
330
- <span class="status-msg" id="statusMsg">Backup laden oder lokale Datei wählen</span>
331
+ <div class="dropdown-wrapper" id="ftpDropdown" style="display:none;">
332
+ <button class="btn btn-outline" onclick="toggleDropdown('ftp')">
333
+ 🌐 FTP Backups ▾
334
+ </button>
335
+ <div class="dropdown-menu" id="ftpMenu"></div>
336
+ </div>
337
+ <div class="dropdown-wrapper" id="smbDropdown" style="display:none;">
338
+ <button class="btn btn-outline" onclick="toggleDropdown('smb')">
339
+ 🗄️ SMB Backups ▾
340
+ </button>
341
+ <div class="dropdown-menu" id="smbMenu"></div>
342
+ </div>
343
+ <div class="dropdown-wrapper" id="sftpDropdown" style="display:none;">
344
+ <button class="btn btn-outline" onclick="toggleDropdown('sftp')">
345
+ 🔒 SFTP Backups ▾
346
+ </button>
347
+ <div class="dropdown-menu" id="sftpMenu"></div>
348
+ </div>
349
+ <div class="dropdown-wrapper" id="webdavDropdown" style="display:none;">
350
+ <button class="btn btn-outline" onclick="toggleDropdown('webdav')">
351
+ ☁️ WebDAV Backups ▾
352
+ </button>
353
+ <div class="dropdown-menu" id="webdavMenu"></div>
354
+ </div>
355
+ <div id="httpInputWrapper" style="display:none; display:flex; align-items:center; gap:6px;">
356
+ <input type="text" id="httpUrlInput" placeholder="https://..." style="padding:0.35rem 0.6rem; border:1px solid #ced4da; border-radius:4px; font-size:0.875rem; min-width:260px; font-family:inherit;">
357
+ <button class="btn btn-outline" onclick="loadHttpUrl()">🌐 URL laden</button>
358
+ </div>
359
+ <button id="zipBtn" class="btn btn-outline" onclick="downloadZip()" style="display:none;">📦 ZIP</button>
360
+ <span class="status-msg" id="statusMsg">Backup laden oder Quelle wählen</span>
331
361
  </div>
332
362
  </div>
333
363
 
@@ -675,7 +705,7 @@
675
705
  const scripts = parseJsonContent(reader.result, file.name);
676
706
  hideLoader();
677
707
  loadScripts(scripts);
678
- setStatus(scripts.length + ' Skripte geladen aus: ' + file.name, 'success');
708
+ saveLastBackup('Upload', file.name); setStatus(scripts.length + ' Skripte geladen aus: ' + file.name, 'success');
679
709
  } catch(e) {
680
710
  hideLoader();
681
711
  setStatus('Fehler beim Parsen: ' + e.message, 'error');
@@ -698,7 +728,7 @@
698
728
  const scripts = await parseArchiveInBrowser(archiveReader.result, file.name); // result is ArrayBuffer
699
729
  hideLoader();
700
730
  loadScripts(scripts);
701
- setStatus(scripts.length + ' Skripte geladen aus: ' + file.name, 'success');
731
+ saveLastBackup('Upload', file.name); setStatus(scripts.length + ' Skripte geladen aus: ' + file.name, 'success');
702
732
  } catch(e) {
703
733
  hideLoader();
704
734
  setStatus('Fehler: ' + e.message, 'error');
@@ -712,58 +742,111 @@
712
742
  this.value = '';
713
743
  });
714
744
 
715
- // === Local Files ===
716
- let localMenuOpen = false;
745
+ // === localStorage: last loaded backup ===
746
+ const LS_KEY = 'scriptRestore_lastBackup';
747
+ function saveLastBackup(source, label) {
748
+ try { localStorage.setItem(LS_KEY, JSON.stringify({ source, label })); } catch {}
749
+ }
750
+ function restoreLastBackup() {
751
+ try {
752
+ const d = JSON.parse(localStorage.getItem(LS_KEY) || 'null');
753
+ if (d) setStatus('Zuletzt geladen: [' + escapeHTML(d.source) + '] ' + escapeHTML(d.label), '');
754
+ } catch {}
755
+ }
756
+ restoreLastBackup();
757
+
758
+ // === Source Config ===
759
+ function loadSourceConfig(attempt) {
760
+ attempt = attempt || 0;
761
+ sendTo('getSourceConfig', {}, function(result) {
762
+ if (!result || result.error) {
763
+ if (attempt < 10) setTimeout(function() { loadSourceConfig(attempt + 1); }, 500);
764
+ return;
765
+ }
766
+ if (result.localEnabled === false) document.getElementById('localDropdown').style.display = 'none';
767
+ if (result.ftpEnabled) document.getElementById('ftpDropdown').style.display = '';
768
+ if (result.smbEnabled) document.getElementById('smbDropdown').style.display = '';
769
+ if (result.sftpEnabled) document.getElementById('sftpDropdown').style.display = '';
770
+ if (result.webdavEnabled) document.getElementById('webdavDropdown').style.display = '';
771
+ if (result.httpEnabled) document.getElementById('httpInputWrapper').style.display = 'flex';
772
+ });
773
+ }
774
+ setTimeout(function() { loadSourceConfig(0); }, 300);
775
+
776
+ // === Dropdowns (local / ftp / smb / sftp / webdav) ===
777
+ const dropdownState = { local: false, ftp: false, smb: false, sftp: false, webdav: false };
778
+ const dropdownConfig = {
779
+ local: { listCmd: 'listLocalFiles', parseCmd: 'parseLocalFile', menuId: 'localMenu', wrapperId: 'localDropdown' },
780
+ ftp: { listCmd: 'listFtpFiles', parseCmd: 'parseFtpFile', menuId: 'ftpMenu', wrapperId: 'ftpDropdown' },
781
+ smb: { listCmd: 'listSmbFiles', parseCmd: 'parseSmbFile', menuId: 'smbMenu', wrapperId: 'smbDropdown' },
782
+ sftp: { listCmd: 'listSftpFiles', parseCmd: 'parseSftpFile', menuId: 'sftpMenu', wrapperId: 'sftpDropdown' },
783
+ webdav: { listCmd: 'listWebdavFiles', parseCmd: 'parseWebdavFile', menuId: 'webdavMenu', wrapperId: 'webdavDropdown' },
784
+ };
785
+
786
+ function toggleDropdown(src) {
787
+ const cfg = dropdownConfig[src];
788
+ const menu = document.getElementById(cfg.menuId);
789
+ const isOpen = dropdownState[src];
790
+
791
+ // Close all other open dropdowns first
792
+ Object.keys(dropdownState).forEach(k => {
793
+ if (k !== src && dropdownState[k]) {
794
+ dropdownState[k] = false;
795
+ document.getElementById(dropdownConfig[k].menuId).classList.remove('open');
796
+ }
797
+ });
717
798
 
718
- function toggleLocalFiles() {
719
- const menu = document.getElementById('localMenu');
720
- localMenuOpen = !localMenuOpen;
721
- if (localMenuOpen) {
799
+ dropdownState[src] = !isOpen;
800
+ if (dropdownState[src]) {
722
801
  menu.classList.add('open');
723
802
  menu.innerHTML = '<div class="dropdown-loading">⏳ Lade Dateiliste...</div>';
724
- sendTo('listLocalFiles', {}, function(result) {
803
+ sendTo(cfg.listCmd, {}, function(result) {
725
804
  if (result && result.error) {
726
805
  menu.innerHTML = '<div class="dropdown-empty">⚠️ ' + escapeHTML(result.error) + '</div>';
727
806
  } else if (result && result.files && result.files.length > 0) {
728
807
  menu.innerHTML = result.files.map(f =>
729
- '<div class="dropdown-item" data-file="' + escapeHTML(f) + '">' +
730
- escapeHTML(f) + '</div>'
808
+ '<div class="dropdown-item" data-file="' + escapeHTML(f) + '">' + escapeHTML(f) + '</div>'
731
809
  ).join('');
732
810
  menu.querySelectorAll('.dropdown-item').forEach(el => {
733
- el.addEventListener('click', function() { loadLocalFile(this.dataset.file); });
811
+ el.addEventListener('click', function() { loadRemoteFile(src, this.dataset.file); });
734
812
  });
735
813
  } else {
736
814
  menu.innerHTML = '<div class="dropdown-empty">Keine Dateien gefunden in:<br>' + escapeHTML((result && result.path) || '') + '</div>';
737
815
  }
738
816
  });
739
- // Close when clicking outside
740
- setTimeout(() => document.addEventListener('click', closeLocalMenuOutside), 0);
817
+ setTimeout(() => document.addEventListener('click', closeDropdownOutside), 0);
741
818
  } else {
742
819
  menu.classList.remove('open');
743
- document.removeEventListener('click', closeLocalMenuOutside);
744
820
  }
745
821
  }
746
822
 
747
- function closeLocalMenuOutside(e) {
748
- const wrapper = document.getElementById('localDropdown');
749
- if (!wrapper.contains(e.target)) {
750
- document.getElementById('localMenu').classList.remove('open');
751
- localMenuOpen = false;
752
- document.removeEventListener('click', closeLocalMenuOutside);
753
- }
823
+ function closeDropdownOutside(e) {
824
+ let anyOpen = false;
825
+ Object.keys(dropdownState).forEach(src => {
826
+ if (!dropdownState[src]) return;
827
+ const wrapper = document.getElementById(dropdownConfig[src].wrapperId);
828
+ if (!wrapper.contains(e.target)) {
829
+ dropdownState[src] = false;
830
+ document.getElementById(dropdownConfig[src].menuId).classList.remove('open');
831
+ } else {
832
+ anyOpen = true;
833
+ }
834
+ });
835
+ if (!anyOpen) document.removeEventListener('click', closeDropdownOutside);
754
836
  }
755
837
 
756
- function loadLocalFile(filename) {
757
- document.getElementById('localMenu').classList.remove('open');
758
- localMenuOpen = false;
759
- document.removeEventListener('click', closeLocalMenuOutside);
838
+ function loadRemoteFile(src, filename) {
839
+ const cfg = dropdownConfig[src];
840
+ document.getElementById(cfg.menuId).classList.remove('open');
841
+ dropdownState[src] = false;
760
842
  showLoaderSpinner('Lade und verarbeite ' + filename + '...');
761
- sendTo('parseLocalFile', { filename: filename }, function(result) {
843
+ sendTo(cfg.parseCmd, { filename: filename }, function(result) {
762
844
  hideLoader();
763
845
  if (result && result.error) {
764
846
  setStatus('Fehler: ' + result.error, 'error');
765
847
  } else if (result && result.scripts) {
766
848
  loadScripts(result.scripts);
849
+ saveLastBackup(src.toUpperCase(), filename);
767
850
  setStatus(result.scripts.length + ' Skripte geladen aus: ' + filename, 'success');
768
851
  } else {
769
852
  setStatus('Keine Skripte gefunden.', 'error');
@@ -771,9 +854,61 @@
771
854
  });
772
855
  }
773
856
 
857
+ // === HTTP URL ===
858
+ function loadHttpUrl() {
859
+ const url = document.getElementById('httpUrlInput').value.trim();
860
+ if (!url) return;
861
+ const filename = url.split('/').pop() || 'backup';
862
+ showLoaderSpinner('Lade URL...');
863
+ sendTo('parseHttpUrl', { url }, function(result) {
864
+ hideLoader();
865
+ if (result && result.error) {
866
+ setStatus('Fehler: ' + result.error, 'error');
867
+ } else if (result && result.scripts) {
868
+ loadScripts(result.scripts);
869
+ saveLastBackup('HTTP', filename);
870
+ setStatus(result.scripts.length + ' Skripte geladen von URL', 'success');
871
+ } else {
872
+ setStatus('Keine Skripte gefunden.', 'error');
873
+ }
874
+ });
875
+ }
876
+
877
+ // === Multi-Select & ZIP ===
878
+ let selectedIndices = new Set();
879
+
880
+ function toggleSelect(idx, el) {
881
+ if (selectedIndices.has(idx)) {
882
+ selectedIndices.delete(idx);
883
+ el.classList.remove('selected');
884
+ } else {
885
+ selectedIndices.add(idx);
886
+ el.classList.add('selected');
887
+ }
888
+ document.getElementById('zipBtn').style.display = selectedIndices.size > 1 ? '' : 'none';
889
+ }
890
+
891
+ async function downloadZip() {
892
+ if (selectedIndices.size < 2) return;
893
+ const { default: JSZip } = await import('https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js');
894
+ const zip = new JSZip();
895
+ selectedIndices.forEach(idx => {
896
+ const s = scriptsData[idx];
897
+ const ext = s.type === 'TypeScript' ? '.ts' : '.js';
898
+ zip.file(s.path.replace(/\./g, '/') + ext, s.source || '');
899
+ });
900
+ const blob = await zip.generateAsync({ type: 'blob' });
901
+ const a = document.createElement('a');
902
+ a.href = URL.createObjectURL(blob);
903
+ a.download = 'scripts.zip';
904
+ a.click();
905
+ }
906
+
774
907
  function loadScripts(scripts) {
775
908
  scriptsData = scripts;
776
909
  cur = { index: -1 };
910
+ selectedIndices.clear();
911
+ document.getElementById('zipBtn').style.display = 'none';
777
912
  openFolders.clear();
778
913
  isAllExpanded = false;
779
914
  document.getElementById('expandToggleBtn').innerHTML = '📂';
@@ -781,7 +916,7 @@
781
916
  document.getElementById('actionBar').style.display = 'none';
782
917
  document.getElementById('codeContainer').className = 'code-empty';
783
918
  document.getElementById('codeContainer').innerHTML = scripts.length > 0
784
- ? '// Skript im Baum links auswählen...'
919
+ ? '// Skript im Baum links auswählen… oder mehrere mit Strg+Klick für ZIP'
785
920
  : '<span style="color:#dc3545">Keine Skripte in diesem Backup gefunden.</span>';
786
921
  }
787
922
 
@@ -878,9 +1013,15 @@
878
1013
  function createScriptNode(s, idx) {
879
1014
  const badgeText = s.type === 'TypeScript' ? 'TS' : (s.type === 'Blockly' ? 'Blockly' : (s.type === 'Rules' ? 'RULES' : 'JS'));
880
1015
  const div = document.createElement('div');
881
- div.className = 'script-item' + (cur.index === idx ? ' active' : '');
1016
+ div.className = 'script-item' + (cur.index === idx ? ' active' : '') + (selectedIndices.has(idx) ? ' selected' : '');
882
1017
  div.dataset.index = idx;
883
- div.onclick = () => selectScript(idx);
1018
+ div.onclick = (e) => {
1019
+ if (e.ctrlKey || e.metaKey) {
1020
+ toggleSelect(idx, div);
1021
+ } else {
1022
+ selectScript(idx);
1023
+ }
1024
+ };
884
1025
  div.innerHTML = '<div class="script-name" title="' + escapeHTML(s.path) + '">📄 ' + escapeHTML(s.name) + '</div>' +
885
1026
  '<span class="type-badge badge-' + s.type + '">' + badgeText + '</span>';
886
1027
  return div;