esp32tool 1.5.0 → 1.6.0
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/README.md +2 -0
- package/apple-touch-icon.png +0 -0
- package/css/style.css +388 -2
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +7 -4
- package/js/nvs-editor.js +809 -0
- package/js/script.js +142 -4
- package/package.json +4 -4
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/sw.js +2 -1
package/js/nvs-editor.js
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESP32Tool NVS (Non-Volatile Storage) Editor
|
|
3
|
+
*
|
|
4
|
+
* A full-screen editor for viewing and editing NVS partition contents.
|
|
5
|
+
* Features:
|
|
6
|
+
* - Parse and display NVS pages, namespaces and key/value entries
|
|
7
|
+
* - Inline editing of values (primitives, strings, blobs)
|
|
8
|
+
* - Delete entries
|
|
9
|
+
* - Add new entries and namespaces
|
|
10
|
+
* - Write modified NVS data back to flash
|
|
11
|
+
* - Filter / search entries
|
|
12
|
+
* - Page state indicator
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { HexEditor } from './hex-editor.js';
|
|
16
|
+
|
|
17
|
+
export class NVSEditor {
|
|
18
|
+
/**
|
|
19
|
+
* @param {HTMLElement} container - The container element (#nvseditor-container)
|
|
20
|
+
*/
|
|
21
|
+
constructor(container) {
|
|
22
|
+
this.container = container;
|
|
23
|
+
/** @type {Uint8Array|null} raw NVS partition data */
|
|
24
|
+
this.data = null;
|
|
25
|
+
/** @type {Uint8Array|null} original snapshot for diff */
|
|
26
|
+
this.originalData = null;
|
|
27
|
+
this.baseAddress = 0; // flash offset of the NVS partition
|
|
28
|
+
this.partitionSize = 0;
|
|
29
|
+
this.partitionName = '';
|
|
30
|
+
|
|
31
|
+
/** Parsed pages with items */
|
|
32
|
+
this.pages = [];
|
|
33
|
+
/** Whether data has been modified */
|
|
34
|
+
this.modified = false;
|
|
35
|
+
|
|
36
|
+
// Callbacks
|
|
37
|
+
this.onClose = null;
|
|
38
|
+
/** @type {((data: Uint8Array) => Promise<void>)|null} */
|
|
39
|
+
this.onWriteFlash = null;
|
|
40
|
+
|
|
41
|
+
// DOM cache
|
|
42
|
+
this._progressOverlay = null;
|
|
43
|
+
this._progressText = null;
|
|
44
|
+
this._progressBarInner = null;
|
|
45
|
+
|
|
46
|
+
// Filter state
|
|
47
|
+
this._filterText = '';
|
|
48
|
+
|
|
49
|
+
// Sub hex-editor for large entries
|
|
50
|
+
this._hexEditorInstance = null;
|
|
51
|
+
this._hexEditorContainer = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────── CRC32 helpers (same as esp32-parser NVSParser) ───────
|
|
55
|
+
|
|
56
|
+
static crc32Byte(crc, d) {
|
|
57
|
+
for (let i = 0; i < 8; i++) {
|
|
58
|
+
const bit = d & 1;
|
|
59
|
+
crc ^= bit;
|
|
60
|
+
crc = (crc & 1) ? (crc >>> 1) ^ 0xEDB88320 : crc >>> 1;
|
|
61
|
+
d >>>= 1;
|
|
62
|
+
}
|
|
63
|
+
return crc >>> 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static crc32(data, offset = 0, length = null) {
|
|
67
|
+
let crc = 0;
|
|
68
|
+
const len = length ?? data.length - offset;
|
|
69
|
+
for (let i = 0; i < len; i++) {
|
|
70
|
+
crc = NVSEditor.crc32Byte(crc, data[offset + i]);
|
|
71
|
+
}
|
|
72
|
+
return (~crc) >>> 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static crc32Header(data, offset = 0) {
|
|
76
|
+
const buf = new Uint8Array(0x20 - 4);
|
|
77
|
+
buf.set(data.subarray(offset, offset + 4), 0);
|
|
78
|
+
buf.set(data.subarray(offset + 8, offset + 8 + 0x18), 4);
|
|
79
|
+
return NVSEditor.crc32(buf, 0, 0x1C);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static bytesToHex(bytes, separator = '') {
|
|
83
|
+
return Array.from(bytes)
|
|
84
|
+
.map(b => b.toString(16).padStart(2, '0').toUpperCase())
|
|
85
|
+
.join(separator);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─────── Public API ───────
|
|
89
|
+
|
|
90
|
+
/** Show a progress overlay (before open()) */
|
|
91
|
+
initProgressUI() {
|
|
92
|
+
this.container.innerHTML = `
|
|
93
|
+
<div class="nvseditor-body" style="flex:1;display:flex;align-items:center;justify-content:center;">
|
|
94
|
+
<div class="nvseditor-progress-overlay" id="nvsProgress">
|
|
95
|
+
<div class="progress-text" id="nvsProgressText">Initiating...</div>
|
|
96
|
+
<div class="progress-bar-outer">
|
|
97
|
+
<div class="progress-bar-inner" id="nvsProgressBar"></div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>`;
|
|
101
|
+
this._progressOverlay = this.container.querySelector('#nvsProgress');
|
|
102
|
+
this._progressText = this.container.querySelector('#nvsProgressText');
|
|
103
|
+
this._progressBarInner = this.container.querySelector('#nvsProgressBar');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
showProgress(text, percent) {
|
|
107
|
+
if (this._progressOverlay) {
|
|
108
|
+
this._progressOverlay.classList.remove('hidden');
|
|
109
|
+
this._progressText.textContent = text;
|
|
110
|
+
this._progressBarInner.style.width = percent + '%';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
hideProgress() {
|
|
115
|
+
if (this._progressOverlay) {
|
|
116
|
+
this._progressOverlay.classList.add('hidden');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Open the NVS editor with data.
|
|
122
|
+
* @param {Uint8Array} data - raw NVS partition bytes
|
|
123
|
+
* @param {number} baseAddress - flash offset of the partition
|
|
124
|
+
* @param {string} name - partition name
|
|
125
|
+
*/
|
|
126
|
+
open(data, baseAddress, name) {
|
|
127
|
+
this.data = new Uint8Array(data);
|
|
128
|
+
this.originalData = new Uint8Array(data);
|
|
129
|
+
this.baseAddress = baseAddress;
|
|
130
|
+
this.partitionSize = data.length;
|
|
131
|
+
this.partitionName = name || 'nvs';
|
|
132
|
+
this.modified = false;
|
|
133
|
+
this._filterText = '';
|
|
134
|
+
|
|
135
|
+
this.pages = this._parse();
|
|
136
|
+
this._buildUI();
|
|
137
|
+
|
|
138
|
+
this.container.classList.remove('hidden');
|
|
139
|
+
document.body.classList.add('nvseditor-active');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
close() {
|
|
143
|
+
if (this._hexEditorInstance) {
|
|
144
|
+
this._hexEditorInstance.onClose = null; // prevent re-render on already-closing editor
|
|
145
|
+
try { this._hexEditorInstance.close(); } catch (_) {}
|
|
146
|
+
this._hexEditorInstance = null;
|
|
147
|
+
}
|
|
148
|
+
this.container.classList.add('hidden');
|
|
149
|
+
document.body.classList.remove('nvseditor-active');
|
|
150
|
+
this.container.innerHTML = '';
|
|
151
|
+
if (this.onClose) this.onClose();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─────── NVS parsing (synchronous, operates on Uint8Array) ───────
|
|
155
|
+
|
|
156
|
+
_readString(offset, maxLen) {
|
|
157
|
+
let r = '';
|
|
158
|
+
for (let i = 0; i < maxLen; i++) {
|
|
159
|
+
const b = this.data[offset + i];
|
|
160
|
+
if (b === 0) break;
|
|
161
|
+
if (b >= 32 && b <= 126) r += String.fromCharCode(b);
|
|
162
|
+
else return r;
|
|
163
|
+
}
|
|
164
|
+
return r;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_u8(off) { return this.data[off]; }
|
|
168
|
+
_u16(off) { return this.data[off] | (this.data[off + 1] << 8); }
|
|
169
|
+
_u32(off) { return (this.data[off] | (this.data[off + 1] << 8) | (this.data[off + 2] << 16) | (this.data[off + 3] << 24)) >>> 0; }
|
|
170
|
+
_i32(off) { return this._u32(off) | 0; }
|
|
171
|
+
_u64(off) {
|
|
172
|
+
const lo = this._u32(off);
|
|
173
|
+
const hi = this._u32(off + 4);
|
|
174
|
+
return (BigInt(hi) << 32n) | BigInt(lo);
|
|
175
|
+
}
|
|
176
|
+
_i64(off) { return BigInt.asIntN(64, this._u64(off)); }
|
|
177
|
+
|
|
178
|
+
_getNVSTypeName(dt) {
|
|
179
|
+
const m = {
|
|
180
|
+
0x01: 'U8', 0x02: 'U16', 0x04: 'U32', 0x08: 'U64',
|
|
181
|
+
0x11: 'I8', 0x12: 'I16', 0x14: 'I32', 0x18: 'I64',
|
|
182
|
+
0x21: 'String', 0x42: 'Blob', 0x48: 'Blob Index'
|
|
183
|
+
};
|
|
184
|
+
return m[dt] || `0x${dt.toString(16)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_getNVSItemState(bitmap, index) {
|
|
188
|
+
const bmpIdx = Math.floor(index / 4);
|
|
189
|
+
const bmpBit = (index % 4) * 2;
|
|
190
|
+
return (bitmap[bmpIdx] >> bmpBit) & 3;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_setNVSItemState(bitmap, index, state) {
|
|
194
|
+
const bmpIdx = Math.floor(index / 4);
|
|
195
|
+
const bmpBit = (index % 4) * 2;
|
|
196
|
+
bitmap[bmpIdx] &= ~(3 << bmpBit);
|
|
197
|
+
bitmap[bmpIdx] |= (state << bmpBit);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_parse() {
|
|
201
|
+
const NVS_SECTOR_SIZE = 4096;
|
|
202
|
+
const MAX_ENTRY_COUNT = 126;
|
|
203
|
+
const NVS_PAGE_STATE = {
|
|
204
|
+
UNINIT: 0xFFFFFFFF, ACTIVE: 0xFFFFFFFE,
|
|
205
|
+
FULL: 0xFFFFFFFC, FREEING: 0xFFFFFFF8, CORRUPT: 0xFFFFFFF0
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const pages = [];
|
|
209
|
+
const namespaces = new Map();
|
|
210
|
+
namespaces.set(0, '');
|
|
211
|
+
|
|
212
|
+
// ── Pass 1: collect all namespace definitions ──────────────────────────
|
|
213
|
+
for (let secOff = 0; secOff < this.data.length; secOff += NVS_SECTOR_SIZE) {
|
|
214
|
+
if (secOff + 64 > this.data.length) break;
|
|
215
|
+
const state = this._u32(secOff);
|
|
216
|
+
if (state === NVS_PAGE_STATE.UNINIT || state === NVS_PAGE_STATE.CORRUPT) continue;
|
|
217
|
+
const stateBitmap = this.data.slice(secOff + 32, secOff + 64);
|
|
218
|
+
for (let entry = 0; entry < MAX_ENTRY_COUNT; entry++) {
|
|
219
|
+
if (this._getNVSItemState(stateBitmap, entry) !== 2) continue;
|
|
220
|
+
const eOff = secOff + 64 + entry * 32;
|
|
221
|
+
if (eOff + 32 > this.data.length) break;
|
|
222
|
+
const span = this._u8(eOff + 2);
|
|
223
|
+
if (this._u8(eOff) === 0 && this._u8(eOff + 1) !== 0xFF && this._u8(eOff + 1) !== 0x00) {
|
|
224
|
+
namespaces.set(this._u8(eOff + 24), this._readString(eOff + 8, 16));
|
|
225
|
+
}
|
|
226
|
+
if (span > 1) entry += span - 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (let secOff = 0; secOff < this.data.length; secOff += NVS_SECTOR_SIZE) {
|
|
231
|
+
if (secOff + 64 > this.data.length) break;
|
|
232
|
+
const state = this._u32(secOff);
|
|
233
|
+
|
|
234
|
+
let stateName = 'UNKNOWN';
|
|
235
|
+
if (state === NVS_PAGE_STATE.UNINIT) { stateName = 'UNINIT'; }
|
|
236
|
+
else if (state === NVS_PAGE_STATE.ACTIVE) { stateName = 'ACTIVE'; }
|
|
237
|
+
else if (state === NVS_PAGE_STATE.FULL) { stateName = 'FULL'; }
|
|
238
|
+
else if (state === NVS_PAGE_STATE.FREEING) { stateName = 'FREEING'; }
|
|
239
|
+
else if (state === NVS_PAGE_STATE.CORRUPT) { stateName = 'CORRUPT'; }
|
|
240
|
+
|
|
241
|
+
if (stateName === 'UNINIT' || stateName === 'CORRUPT') continue;
|
|
242
|
+
|
|
243
|
+
const seq = this._u32(secOff + 4);
|
|
244
|
+
const version = this._u8(secOff + 8);
|
|
245
|
+
const crc32 = this._u32(secOff + 28);
|
|
246
|
+
const stateBitmap = this.data.slice(secOff + 32, secOff + 64);
|
|
247
|
+
|
|
248
|
+
const page = { offset: secOff, state: stateName, seq, version, crc32, items: [] };
|
|
249
|
+
|
|
250
|
+
for (let entry = 0; entry < MAX_ENTRY_COUNT; entry++) {
|
|
251
|
+
const itemState = this._getNVSItemState(stateBitmap, entry);
|
|
252
|
+
if (itemState !== 2) continue; // only WRITTEN entries
|
|
253
|
+
|
|
254
|
+
const eOff = secOff + 64 + entry * 32;
|
|
255
|
+
if (eOff + 32 > this.data.length) break;
|
|
256
|
+
|
|
257
|
+
const nsIndex = this._u8(eOff);
|
|
258
|
+
const datatype = this._u8(eOff + 1);
|
|
259
|
+
const span = this._u8(eOff + 2);
|
|
260
|
+
const chunkIndex = this._u8(eOff + 3);
|
|
261
|
+
|
|
262
|
+
if (span === 0 || span > 126) continue;
|
|
263
|
+
if (datatype === 0xFF || datatype === 0x00) continue;
|
|
264
|
+
if (nsIndex === 0xFF) continue;
|
|
265
|
+
|
|
266
|
+
const crc = this._u32(eOff + 4);
|
|
267
|
+
const key = this._readString(eOff + 8, 16);
|
|
268
|
+
|
|
269
|
+
if (nsIndex !== 0 && (!key || key.length === 0)) continue;
|
|
270
|
+
|
|
271
|
+
const headerCrcCalc = NVSEditor.crc32Header(this.data, eOff);
|
|
272
|
+
|
|
273
|
+
const item = {
|
|
274
|
+
nsIndex, datatype, span, chunkIndex,
|
|
275
|
+
crc32: crc >>> 0,
|
|
276
|
+
headerCrcCalc: headerCrcCalc >>> 0,
|
|
277
|
+
headerCrcValid: (crc >>> 0) === (headerCrcCalc >>> 0),
|
|
278
|
+
key,
|
|
279
|
+
value: null,
|
|
280
|
+
typeName: this._getNVSTypeName(datatype),
|
|
281
|
+
offset: eOff,
|
|
282
|
+
entrySize: 32,
|
|
283
|
+
pageOffset: secOff
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Namespace definition
|
|
287
|
+
if (nsIndex === 0) {
|
|
288
|
+
const namespaceIndex = this._u8(eOff + 24);
|
|
289
|
+
item.value = namespaceIndex;
|
|
290
|
+
item.namespace = key;
|
|
291
|
+
namespaces.set(namespaceIndex, key);
|
|
292
|
+
} else {
|
|
293
|
+
switch (datatype) {
|
|
294
|
+
case 0x01: item.value = this._u8(eOff + 24); break;
|
|
295
|
+
case 0x02: item.value = this._u16(eOff + 24); break;
|
|
296
|
+
case 0x04: item.value = this._u32(eOff + 24); break;
|
|
297
|
+
case 0x08: item.value = this._u64(eOff + 24).toString(); break;
|
|
298
|
+
case 0x11: item.value = (this._u8(eOff + 24) > 127 ? this._u8(eOff + 24) - 256 : this._u8(eOff + 24)); break;
|
|
299
|
+
case 0x12: { const v = this._u16(eOff + 24); item.value = v > 32767 ? v - 65536 : v; break; }
|
|
300
|
+
case 0x14: item.value = this._i32(eOff + 24); break;
|
|
301
|
+
case 0x18: item.value = this._i64(eOff + 24).toString(); break;
|
|
302
|
+
case 0x21: { // String
|
|
303
|
+
const strSize = this._u16(eOff + 24);
|
|
304
|
+
const strCrc = this._u32(eOff + 28);
|
|
305
|
+
if (strSize > 0 && strSize < 4096 && eOff + 32 + strSize <= this.data.length) {
|
|
306
|
+
const strData = this.data.slice(eOff + 32, eOff + 32 + strSize);
|
|
307
|
+
const allErased = strData.every(b => b === 0xFF);
|
|
308
|
+
// Find first NUL byte and decode as UTF-8
|
|
309
|
+
let nullIndex = strData.length;
|
|
310
|
+
for (let i = 0; i < strData.length; i++) {
|
|
311
|
+
if (strData[i] === 0) { nullIndex = i; break; }
|
|
312
|
+
}
|
|
313
|
+
const sv = nullIndex > 0 ? new TextDecoder('utf-8').decode(strData.subarray(0, nullIndex)) : '';
|
|
314
|
+
item.value = allErased ? '<erased>' : sv;
|
|
315
|
+
item.rawValue = strData;
|
|
316
|
+
item.dataCrcStored = strCrc >>> 0;
|
|
317
|
+
item.dataCrcCalc = NVSEditor.crc32(strData, 0, strSize) >>> 0;
|
|
318
|
+
item.dataCrcValid = item.dataCrcCalc === item.dataCrcStored;
|
|
319
|
+
item.size = strSize;
|
|
320
|
+
item.entrySize = 32 + strSize;
|
|
321
|
+
} else {
|
|
322
|
+
item.value = '<invalid string>';
|
|
323
|
+
item.size = 0;
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 0x42: { // Blob
|
|
328
|
+
const blobSize = this._u16(eOff + 24);
|
|
329
|
+
const blobCrc = this._u32(eOff + 28);
|
|
330
|
+
if (blobSize > 0 && blobSize < 4096 && eOff + 32 + blobSize <= this.data.length) {
|
|
331
|
+
const blobData = this.data.slice(eOff + 32, eOff + 32 + blobSize);
|
|
332
|
+
const allErased = blobData.every(b => b === 0xFF);
|
|
333
|
+
item.value = allErased ? '<erased>' : NVSEditor.bytesToHex(blobData, ' ');
|
|
334
|
+
item.rawValue = blobData;
|
|
335
|
+
item.dataCrcStored = blobCrc >>> 0;
|
|
336
|
+
item.dataCrcCalc = NVSEditor.crc32(blobData, 0, blobSize) >>> 0;
|
|
337
|
+
item.dataCrcValid = item.dataCrcCalc === item.dataCrcStored;
|
|
338
|
+
item.size = blobSize;
|
|
339
|
+
item.entrySize = 32 + blobSize;
|
|
340
|
+
} else {
|
|
341
|
+
item.value = '<invalid blob>';
|
|
342
|
+
item.size = 0;
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case 0x48: { // Blob Index
|
|
347
|
+
item.totalSize = this._u32(eOff + 24);
|
|
348
|
+
item.chunkCount = this._u8(eOff + 28);
|
|
349
|
+
item.chunkStart = this._u8(eOff + 29);
|
|
350
|
+
item.value = `${item.chunkCount} chunks, ${item.totalSize} bytes total`;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
page.items.push(item);
|
|
357
|
+
if (span > 1) entry += span - 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pages.push(page);
|
|
361
|
+
}
|
|
362
|
+
// Resolve all item namespaces after both passes
|
|
363
|
+
for (const page of pages)
|
|
364
|
+
for (const it of page.items)
|
|
365
|
+
if (it.nsIndex && it.nsIndex !== 0)
|
|
366
|
+
it.namespace = namespaces.get(it.nsIndex) || `ns_${it.nsIndex}`;
|
|
367
|
+
|
|
368
|
+
return pages;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─────── NVS write helpers ───────
|
|
372
|
+
|
|
373
|
+
/** Delete an NVS entry by zeroing it and updating the bitmap */
|
|
374
|
+
_deleteEntry(item) {
|
|
375
|
+
const pageOff = item.pageOffset;
|
|
376
|
+
const entryIdx = (item.offset - pageOff - 64) / 32;
|
|
377
|
+
|
|
378
|
+
const stateBitmap = this.data.slice(pageOff + 32, pageOff + 64);
|
|
379
|
+
|
|
380
|
+
for (let s = 0; s < item.span; s++) {
|
|
381
|
+
const off = item.offset + s * 32;
|
|
382
|
+
this.data.fill(0xFF, off, off + 32);
|
|
383
|
+
this._setNVSItemState(stateBitmap, entryIdx + s, 0); // ERASED = 0
|
|
384
|
+
}
|
|
385
|
+
// Write back bitmap
|
|
386
|
+
this.data.set(stateBitmap, pageOff + 32);
|
|
387
|
+
this.modified = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
static _parseIntStrict(s, label, min, max) {
|
|
391
|
+
const n = Number(s);
|
|
392
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < min || n > max)
|
|
393
|
+
throw new Error(`${label} must be an integer in [${min}, ${max}], got: ${s}`);
|
|
394
|
+
return n;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Write a primitive value back to an existing entry at item.offset */
|
|
398
|
+
_writeValue(item, newValue) {
|
|
399
|
+
const off = item.offset;
|
|
400
|
+
switch (item.datatype) {
|
|
401
|
+
case 0x01: { const v = NVSEditor._parseIntStrict(newValue, 'U8', 0, 255); this.data[off + 24] = v; break; }
|
|
402
|
+
case 0x02: { const v = parseInt(newValue, 10); if (!Number.isFinite(v) || v < 0 || v > 65535) throw new Error('U16 must be 0-65535'); this.data[off + 24] = v & 0xFF; this.data[off + 25] = (v >> 8) & 0xFF; break; }
|
|
403
|
+
case 0x04: { const v = parseInt(newValue, 10); if (!Number.isFinite(v) || v < 0 || v > 4294967295) throw new Error('U32 must be 0-4294967295'); const dv = new DataView(this.data.buffer, off + 24, 4); dv.setUint32(0, v >>> 0, true); break; }
|
|
404
|
+
case 0x08: { let v; try { v = BigInt(newValue); } catch(e) { throw new Error('U64 must be valid BigInt'); } if (v < 0n || v > 0xFFFFFFFFFFFFFFFFn) throw new Error('U64 out of range'); const dv = new DataView(this.data.buffer, off + 24, 8); dv.setBigUint64(0, v, true); break; }
|
|
405
|
+
case 0x11: { const v = parseInt(newValue, 10); if (!Number.isFinite(v) || v < -128 || v > 127) throw new Error('I8 must be -128 to 127'); this.data[off + 24] = v < 0 ? v + 256 : v; break; }
|
|
406
|
+
case 0x12: { const v = parseInt(newValue, 10); if (!Number.isFinite(v) || v < -32768 || v > 32767) throw new Error('I16 must be -32768 to 32767'); const dv = new DataView(this.data.buffer, off + 24, 2); dv.setInt16(0, v, true); break; }
|
|
407
|
+
case 0x14: { const v = parseInt(newValue, 10); if (!Number.isFinite(v) || v < -2147483648 || v > 2147483647) throw new Error('I32 must be -2147483648 to 2147483647'); const dv = new DataView(this.data.buffer, off + 24, 4); dv.setInt32(0, v, true); break; }
|
|
408
|
+
case 0x18: { let v; try { v = BigInt(newValue); } catch(e) { throw new Error('I64 must be valid BigInt'); } if (v < -0x8000000000000000n || v > 0x7FFFFFFFFFFFFFFFn) throw new Error('I64 out of range'); const dv = new DataView(this.data.buffer, off + 24, 8); dv.setBigInt64(0, v, true); break; }
|
|
409
|
+
case 0x21: { // String – rewrite in-place with trailing NUL
|
|
410
|
+
const enc = new TextEncoder().encode(newValue);
|
|
411
|
+
const maxPayload = (item.span - 1) * 32;
|
|
412
|
+
// Create NUL-terminated buffer
|
|
413
|
+
const encWithNul = new Uint8Array(enc.length + 1);
|
|
414
|
+
encWithNul.set(enc, 0);
|
|
415
|
+
encWithNul[enc.length] = 0;
|
|
416
|
+
if (encWithNul.length > maxPayload) { alert('String too long for existing slot (max ' + maxPayload + ' bytes)'); return; }
|
|
417
|
+
// Update size (includes NUL)
|
|
418
|
+
this.data[off + 24] = encWithNul.length & 0xFF;
|
|
419
|
+
this.data[off + 25] = (encWithNul.length >> 8) & 0xFF;
|
|
420
|
+
// Clear old data
|
|
421
|
+
this.data.fill(0xFF, off + 32, off + 32 + maxPayload);
|
|
422
|
+
// Write new data with NUL
|
|
423
|
+
this.data.set(encWithNul, off + 32);
|
|
424
|
+
// Update data CRC over NUL-terminated buffer
|
|
425
|
+
const crc = NVSEditor.crc32(encWithNul);
|
|
426
|
+
const dv = new DataView(this.data.buffer, off + 28, 4);
|
|
427
|
+
dv.setUint32(0, crc, true);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case 0x42: { // Blob – rewrite hex bytes in-place
|
|
431
|
+
const hexBytes = newValue.split(/[\s,]+/).filter(b => b).map(b => parseInt(b, 16));
|
|
432
|
+
if (hexBytes.some(b => isNaN(b) || b < 0 || b > 255)) { alert('Invalid hex bytes'); return; }
|
|
433
|
+
const blobData = new Uint8Array(hexBytes);
|
|
434
|
+
const maxPayload = (item.span - 1) * 32;
|
|
435
|
+
if (blobData.length > maxPayload) { alert('Blob too long for existing slot (max ' + maxPayload + ' bytes)'); return; }
|
|
436
|
+
// Update size
|
|
437
|
+
this.data[off + 24] = blobData.length & 0xFF;
|
|
438
|
+
this.data[off + 25] = (blobData.length >> 8) & 0xFF;
|
|
439
|
+
// Clear old data
|
|
440
|
+
this.data.fill(0xFF, off + 32, off + 32 + maxPayload);
|
|
441
|
+
// Write new data
|
|
442
|
+
this.data.set(blobData, off + 32);
|
|
443
|
+
// Update data CRC
|
|
444
|
+
const crc = NVSEditor.crc32(blobData);
|
|
445
|
+
const dv = new DataView(this.data.buffer, off + 28, 4);
|
|
446
|
+
dv.setUint32(0, crc, true);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case 0x48: { // Blob Index – edit totalSize
|
|
450
|
+
const v = parseInt(newValue, 10);
|
|
451
|
+
if (!Number.isFinite(v) || v < 0 || v > 0xFFFFFFFF) { alert('Invalid total size (must be 0-4294967295)'); return; }
|
|
452
|
+
const dv = new DataView(this.data.buffer, off + 24, 4);
|
|
453
|
+
dv.setUint32(0, v, true);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
default:
|
|
457
|
+
alert('Editing this type is not supported');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// Recalculate header CRC
|
|
461
|
+
const hcrc = NVSEditor.crc32Header(this.data, off);
|
|
462
|
+
const dv = new DataView(this.data.buffer, off + 4, 4);
|
|
463
|
+
dv.setUint32(0, hcrc, true);
|
|
464
|
+
this.modified = true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ─────── UI ───────
|
|
468
|
+
|
|
469
|
+
_buildUI() {
|
|
470
|
+
const sizeStr = this.partitionSize >= 1024 * 1024
|
|
471
|
+
? (this.partitionSize / (1024 * 1024)).toFixed(1) + ' MB'
|
|
472
|
+
: this.partitionSize >= 1024
|
|
473
|
+
? (this.partitionSize / 1024).toFixed(1) + ' KB'
|
|
474
|
+
: this.partitionSize + ' B';
|
|
475
|
+
|
|
476
|
+
const totalItems = this.pages.reduce((s, p) => s + p.items.length, 0);
|
|
477
|
+
|
|
478
|
+
this.container.innerHTML = `
|
|
479
|
+
<div class="nvseditor-toolbar">
|
|
480
|
+
<h3>NVS Editor</h3>
|
|
481
|
+
<span class="nvs-info">
|
|
482
|
+
Partition: <b>${this._esc(this.partitionName)}</b> |
|
|
483
|
+
Offset: 0x${this.baseAddress.toString(16).toUpperCase()} |
|
|
484
|
+
Size: ${sizeStr} |
|
|
485
|
+
${this.pages.length} page(s), ${totalItems} entries
|
|
486
|
+
</span>
|
|
487
|
+
<span class="spacer"></span>
|
|
488
|
+
<div class="nvseditor-filter">
|
|
489
|
+
<input id="nvsFilter" type="text" placeholder="Filter by namespace or key..." />
|
|
490
|
+
</div>
|
|
491
|
+
<button id="nvsRefresh" title="Re-parse data">Refresh</button>
|
|
492
|
+
<button id="nvsWrite" class="primary" disabled>Write to Flash</button>
|
|
493
|
+
<button id="nvsClose">Close</button>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="nvseditor-body">
|
|
496
|
+
<div class="nvseditor-progress-overlay hidden" id="nvsProgress">
|
|
497
|
+
<div class="progress-text" id="nvsProgressText">Loading...</div>
|
|
498
|
+
<div class="progress-bar-outer">
|
|
499
|
+
<div class="progress-bar-inner" id="nvsProgressBar"></div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
<div class="nvseditor-content" id="nvsContent"></div>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="nvseditor-statusbar">
|
|
505
|
+
<span id="nvsStatus">${totalItems} entries in ${this.pages.length} page(s)</span>
|
|
506
|
+
</div>
|
|
507
|
+
<div id="nvsHexEditorContainer" class="hexeditor-container hidden"></div>`;
|
|
508
|
+
|
|
509
|
+
this._hexEditorContainer = this.container.querySelector('#nvsHexEditorContainer');
|
|
510
|
+
|
|
511
|
+
this._progressOverlay = this.container.querySelector('#nvsProgress');
|
|
512
|
+
this._progressText = this.container.querySelector('#nvsProgressText');
|
|
513
|
+
this._progressBarInner = this.container.querySelector('#nvsProgressBar');
|
|
514
|
+
|
|
515
|
+
// Close
|
|
516
|
+
this.container.querySelector('#nvsClose').addEventListener('click', () => {
|
|
517
|
+
if (this.modified) {
|
|
518
|
+
if (!confirm('You have unsaved modifications. Close anyway?')) return;
|
|
519
|
+
}
|
|
520
|
+
this.close();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Write
|
|
524
|
+
const butWrite = this.container.querySelector('#nvsWrite');
|
|
525
|
+
butWrite.addEventListener('click', async () => {
|
|
526
|
+
if (!this.onWriteFlash) return;
|
|
527
|
+
butWrite.disabled = true;
|
|
528
|
+
try {
|
|
529
|
+
this.showProgress('Writing NVS to flash...', 0);
|
|
530
|
+
await this.onWriteFlash(this.data);
|
|
531
|
+
this.originalData = new Uint8Array(this.data);
|
|
532
|
+
this.modified = false;
|
|
533
|
+
butWrite.disabled = true;
|
|
534
|
+
this.showProgress('Write complete!', 100);
|
|
535
|
+
setTimeout(() => this.hideProgress(), 1000);
|
|
536
|
+
} catch (e) {
|
|
537
|
+
alert('Write failed: ' + e);
|
|
538
|
+
this.hideProgress();
|
|
539
|
+
} finally {
|
|
540
|
+
butWrite.disabled = this.modified === false;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Refresh
|
|
545
|
+
this.container.querySelector('#nvsRefresh').addEventListener('click', () => {
|
|
546
|
+
this.pages = this._parse();
|
|
547
|
+
this._renderContent();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Filter
|
|
551
|
+
this.container.querySelector('#nvsFilter').addEventListener('input', (e) => {
|
|
552
|
+
this._filterText = e.target.value.toLowerCase();
|
|
553
|
+
this._renderContent();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
this._renderContent();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
_esc(s) {
|
|
560
|
+
const d = document.createElement('span');
|
|
561
|
+
d.textContent = s;
|
|
562
|
+
return d.innerHTML;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
_renderContent() {
|
|
566
|
+
const content = this.container.querySelector('#nvsContent');
|
|
567
|
+
if (!content) return;
|
|
568
|
+
|
|
569
|
+
const filter = this._filterText;
|
|
570
|
+
|
|
571
|
+
let html = '';
|
|
572
|
+
|
|
573
|
+
for (const page of this.pages) {
|
|
574
|
+
// Group items by namespace
|
|
575
|
+
const nsGroups = new Map();
|
|
576
|
+
const nsDefs = [];
|
|
577
|
+
|
|
578
|
+
for (const item of page.items) {
|
|
579
|
+
if (item.nsIndex === 0) {
|
|
580
|
+
nsDefs.push(item);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const ns = item.namespace || `ns_${item.nsIndex}`;
|
|
584
|
+
if (!nsGroups.has(ns)) nsGroups.set(ns, []);
|
|
585
|
+
nsGroups.get(ns).push(item);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Apply filter
|
|
589
|
+
let hasVisibleItems = false;
|
|
590
|
+
if (filter) {
|
|
591
|
+
for (const [ns, items] of nsGroups) {
|
|
592
|
+
const filtered = items.filter(it =>
|
|
593
|
+
ns.toLowerCase().includes(filter) ||
|
|
594
|
+
it.key.toLowerCase().includes(filter) ||
|
|
595
|
+
String(it.value).toLowerCase().includes(filter)
|
|
596
|
+
);
|
|
597
|
+
if (filtered.length > 0) hasVisibleItems = true;
|
|
598
|
+
}
|
|
599
|
+
// Also check namespace defs
|
|
600
|
+
for (const nd of nsDefs) {
|
|
601
|
+
if (nd.key.toLowerCase().includes(filter)) hasVisibleItems = true;
|
|
602
|
+
}
|
|
603
|
+
if (!hasVisibleItems) continue;
|
|
604
|
+
} else {
|
|
605
|
+
hasVisibleItems = true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const stateClass = page.state === 'ACTIVE' ? 'state-active' :
|
|
609
|
+
page.state === 'FULL' ? 'state-full' :
|
|
610
|
+
page.state === 'FREEING' ? 'state-freeing' : 'state-other';
|
|
611
|
+
|
|
612
|
+
html += `<div class="nvs-page">
|
|
613
|
+
<div class="nvs-page-header ${stateClass}">
|
|
614
|
+
<span class="nvs-page-state">${page.state}</span>
|
|
615
|
+
<span>Page @ 0x${page.offset.toString(16).toUpperCase()}</span>
|
|
616
|
+
<span>Seq: ${page.seq}</span>
|
|
617
|
+
<span>Version: ${page.version}</span>
|
|
618
|
+
<span>${page.items.length} entries</span>
|
|
619
|
+
</div>`;
|
|
620
|
+
|
|
621
|
+
// Render namespace groups
|
|
622
|
+
for (const [ns, items] of nsGroups) {
|
|
623
|
+
const filteredItems = filter
|
|
624
|
+
? items.filter(it =>
|
|
625
|
+
ns.toLowerCase().includes(filter) ||
|
|
626
|
+
it.key.toLowerCase().includes(filter) ||
|
|
627
|
+
String(it.value).toLowerCase().includes(filter))
|
|
628
|
+
: items;
|
|
629
|
+
if (filteredItems.length === 0) continue;
|
|
630
|
+
|
|
631
|
+
html += `<div class="nvs-namespace">
|
|
632
|
+
<div class="nvs-namespace-header">
|
|
633
|
+
<span class="nvs-ns-icon">📁</span>
|
|
634
|
+
<span class="nvs-ns-name">${this._esc(ns)}</span>
|
|
635
|
+
<span class="nvs-ns-count">${filteredItems.length} item(s)</span>
|
|
636
|
+
</div>
|
|
637
|
+
<table class="nvs-table">
|
|
638
|
+
<thead><tr>
|
|
639
|
+
<th>Key</th><th>Type</th><th>Value</th><th>CRC</th><th>Offset</th><th>Actions</th>
|
|
640
|
+
</tr></thead>
|
|
641
|
+
<tbody>`;
|
|
642
|
+
|
|
643
|
+
for (const item of filteredItems) {
|
|
644
|
+
const crcOk = item.headerCrcValid !== false;
|
|
645
|
+
const dataCrcOk = item.dataCrcValid !== undefined ? item.dataCrcValid : true;
|
|
646
|
+
const crcClass = (crcOk && dataCrcOk) ? 'crc-ok' : 'crc-bad';
|
|
647
|
+
const crcText = (crcOk && dataCrcOk) ? '✓' : '✗';
|
|
648
|
+
|
|
649
|
+
let displayValue = String(item.value ?? '');
|
|
650
|
+
if (displayValue.length > 120) displayValue = displayValue.substring(0, 120) + '…';
|
|
651
|
+
|
|
652
|
+
const editable = true;
|
|
653
|
+
|
|
654
|
+
html += `<tr>
|
|
655
|
+
<td class="nvs-key" title="${this._esc(item.key)}">${this._esc(item.key)}</td>
|
|
656
|
+
<td class="nvs-type">${this._esc(item.typeName)}</td>
|
|
657
|
+
<td class="nvs-value" title="${this._esc(String(item.value ?? ''))}">${this._esc(displayValue)}</td>
|
|
658
|
+
<td class="nvs-crc ${crcClass}">${crcText}</td>
|
|
659
|
+
<td class="nvs-offset">0x${(this.baseAddress + item.offset).toString(16).toUpperCase()}</td>
|
|
660
|
+
<td class="nvs-actions">
|
|
661
|
+
${editable ? `<button class="nvs-btn-edit" data-offset="${item.offset}" title="Edit value">✎</button>` : ''}
|
|
662
|
+
<button class="nvs-btn-delete" data-offset="${item.offset}" title="Delete entry">✕</button>
|
|
663
|
+
</td>
|
|
664
|
+
</tr>`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
html += `</tbody></table></div>`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
html += `</div>`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (html === '') {
|
|
674
|
+
html = '<div class="nvs-empty">No NVS entries found' + (filter ? ' matching filter' : '') + '</div>';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
content.innerHTML = html;
|
|
678
|
+
|
|
679
|
+
// Bind edit buttons
|
|
680
|
+
content.querySelectorAll('.nvs-btn-edit').forEach(btn => {
|
|
681
|
+
btn.addEventListener('click', () => {
|
|
682
|
+
const off = parseInt(btn.dataset.offset, 10);
|
|
683
|
+
this._editItem(off);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Bind delete buttons
|
|
688
|
+
content.querySelectorAll('.nvs-btn-delete').forEach(btn => {
|
|
689
|
+
btn.addEventListener('click', () => {
|
|
690
|
+
const off = parseInt(btn.dataset.offset, 10);
|
|
691
|
+
this._deleteItemUI(off);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
this._updateWriteButton();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
_findItem(offset) {
|
|
699
|
+
for (const page of this.pages) {
|
|
700
|
+
for (const item of page.items) {
|
|
701
|
+
if (item.offset === offset) return item;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
_editItem(offset) {
|
|
708
|
+
const item = this._findItem(offset);
|
|
709
|
+
if (!item) return;
|
|
710
|
+
this._editItemInHexEditor(item);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Open the HexEditor for any NVS entry.
|
|
715
|
+
* Primitive types (U8..I64, Blob Index) → 8-byte data field at off+24.
|
|
716
|
+
* String / Blob → multi-span payload at off+32.
|
|
717
|
+
*/
|
|
718
|
+
_editItemInHexEditor(item) {
|
|
719
|
+
const off = item.offset;
|
|
720
|
+
const isPrimitive = !(item.datatype === 0x21 || item.datatype === 0x42);
|
|
721
|
+
|
|
722
|
+
let dataOffset, dataSize, maxSize;
|
|
723
|
+
if (isPrimitive) {
|
|
724
|
+
// Primitive types store value in 8 bytes at header offset 24
|
|
725
|
+
dataOffset = off + 24;
|
|
726
|
+
dataSize = 8;
|
|
727
|
+
maxSize = 8;
|
|
728
|
+
} else {
|
|
729
|
+
// String / Blob: payload after the 32-byte header
|
|
730
|
+
dataOffset = off + 32;
|
|
731
|
+
dataSize = item.size || (item.rawValue ? item.rawValue.length : 0);
|
|
732
|
+
maxSize = (item.span - 1) * 32;
|
|
733
|
+
if (dataSize <= 0) { alert('No data to edit'); return; }
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const entryData = this.data.slice(dataOffset, dataOffset + dataSize);
|
|
737
|
+
|
|
738
|
+
if (!this._hexEditorInstance) {
|
|
739
|
+
this._hexEditorInstance = new HexEditor(this._hexEditorContainer);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
this._hexEditorContainer.classList.remove('hidden');
|
|
743
|
+
this._hexEditorInstance.open(entryData, 0);
|
|
744
|
+
|
|
745
|
+
// Relabel button and show entry info
|
|
746
|
+
const writeBtn = this._hexEditorContainer.querySelector('#hexedWrite');
|
|
747
|
+
if (writeBtn) writeBtn.textContent = 'Apply Changes';
|
|
748
|
+
|
|
749
|
+
this._hexEditorInstance.onWriteFlash = async (editedData, modifiedOffsets) => {
|
|
750
|
+
if (modifiedOffsets.size === 0) return;
|
|
751
|
+
|
|
752
|
+
if (editedData.length > maxSize) {
|
|
753
|
+
alert('Edited data exceeds available slot size (' + maxSize + ' bytes)');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (isPrimitive) {
|
|
758
|
+
// Write the 8 data bytes back into the header
|
|
759
|
+
this.data.set(editedData.slice(0, 8), dataOffset);
|
|
760
|
+
} else {
|
|
761
|
+
// Clear payload area, then write
|
|
762
|
+
this.data.fill(0xFF, dataOffset, dataOffset + maxSize);
|
|
763
|
+
this.data.set(editedData, dataOffset);
|
|
764
|
+
// Update size field
|
|
765
|
+
this.data[off + 24] = editedData.length & 0xFF;
|
|
766
|
+
this.data[off + 25] = (editedData.length >> 8) & 0xFF;
|
|
767
|
+
// Update data CRC
|
|
768
|
+
const crc = NVSEditor.crc32(editedData);
|
|
769
|
+
const dv = new DataView(this.data.buffer, off + 28, 4);
|
|
770
|
+
dv.setUint32(0, crc, true);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Recalculate header CRC
|
|
774
|
+
const hcrc = NVSEditor.crc32Header(this.data, off);
|
|
775
|
+
const hdv = new DataView(this.data.buffer, off + 4, 4);
|
|
776
|
+
hdv.setUint32(0, hcrc, true);
|
|
777
|
+
|
|
778
|
+
this.modified = true;
|
|
779
|
+
|
|
780
|
+
this._hexEditorInstance.showProgress('Applied to NVS!', 100);
|
|
781
|
+
await new Promise(r => setTimeout(r, 500));
|
|
782
|
+
this._hexEditorInstance.hideProgress();
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
this._hexEditorInstance.onClose = () => {
|
|
786
|
+
this._hexEditorContainer.classList.add('hidden');
|
|
787
|
+
this._hexEditorInstance = null;
|
|
788
|
+
this.pages = this._parse();
|
|
789
|
+
this._renderContent();
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
_deleteItemUI(offset) {
|
|
794
|
+
const item = this._findItem(offset);
|
|
795
|
+
if (!item) return;
|
|
796
|
+
|
|
797
|
+
const ns = item.namespace || `ns_${item.nsIndex}`;
|
|
798
|
+
if (!confirm(`Delete ${ns}.${item.key}?`)) return;
|
|
799
|
+
|
|
800
|
+
this._deleteEntry(item);
|
|
801
|
+
this.pages = this._parse();
|
|
802
|
+
this._renderContent();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
_updateWriteButton() {
|
|
806
|
+
const btn = this.container.querySelector('#nvsWrite');
|
|
807
|
+
if (btn) btn.disabled = !this.modified;
|
|
808
|
+
}
|
|
809
|
+
}
|