파일 커멘트 추가, listen-only모드, tranmit에만 normal모드
This commit is contained in:
364
index.h
364
index.h
@@ -251,6 +251,78 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.status-card .value { font-size: 1.5em; font-weight: bold; word-break: break-all; }
|
||||
.status-on { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important; }
|
||||
.status-off { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%) !important; }
|
||||
|
||||
/* SD 카드 용량 표시 */
|
||||
.sd-capacity {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 3px 10px rgba(79, 172, 254, 0.3);
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.sd-capacity-label {
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.sd-capacity-values {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sd-capacity-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.sd-capacity-item-label {
|
||||
font-size: 0.7em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sd-capacity-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 1.2em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 파일 선택 체크박스 */
|
||||
.file-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-selection-controls {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.file-selection-controls button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.selection-info {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
@@ -383,6 +455,38 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.file-comment {
|
||||
color: #888;
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.file-comment:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
.file-comment.empty {
|
||||
color: #ccc;
|
||||
}
|
||||
.comment-input {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
margin-top: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.comment-actions button {
|
||||
padding: 4px 12px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -448,8 +552,8 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚗 Byun CAN Logger v1.5</h1>
|
||||
<p>Real-time CAN Bus Monitor & Logger + File Management</p>
|
||||
<h1>🚗 Byun CAN Logger v1.6</h1>
|
||||
<p>Listen-Only Mode - No CAN Bus Impact (RX Only)</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
@@ -503,6 +607,27 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sd-capacity" id="sd-capacity">
|
||||
<div class="sd-capacity-label">
|
||||
<span>💾</span>
|
||||
<span>SD CARD CAPACITY</span>
|
||||
</div>
|
||||
<div class="sd-capacity-values">
|
||||
<div class="sd-capacity-item">
|
||||
<div class="sd-capacity-item-label">TOTAL</div>
|
||||
<div class="sd-capacity-value" id="sd-total">0 MB</div>
|
||||
</div>
|
||||
<div class="sd-capacity-item">
|
||||
<div class="sd-capacity-item-label">USED</div>
|
||||
<div class="sd-capacity-value" id="sd-used">0 MB</div>
|
||||
</div>
|
||||
<div class="sd-capacity-item">
|
||||
<div class="sd-capacity-item-label">FREE</div>
|
||||
<div class="sd-capacity-value" id="sd-free">0 MB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-grid">
|
||||
<div class="status-card status-off" id="logging-status">
|
||||
<h3>LOGGING</h3>
|
||||
@@ -572,6 +697,15 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
</div>
|
||||
|
||||
<h2>Log Files</h2>
|
||||
<div class="file-selection-controls">
|
||||
<button onclick="selectAllFiles()">Select All</button>
|
||||
<button onclick="deselectAllFiles()">Deselect All</button>
|
||||
<button onclick="downloadSelectedFiles()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">Download Selected</button>
|
||||
<button onclick="deleteSelectedFiles()" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);">Delete Selected</button>
|
||||
<div class="selection-info">
|
||||
<span id="selection-count">0 files selected</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-list" id="file-list">
|
||||
<p style="text-align: center; color: #666; font-size: 0.9em;">Loading...</p>
|
||||
</div>
|
||||
@@ -585,6 +719,166 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
let lastMessageData = {};
|
||||
const speedNames = {0: '125K', 1: '250K', 2: '500K', 3: '1M'};
|
||||
let currentLoggingFile = '';
|
||||
let selectedFiles = new Set();
|
||||
|
||||
function updateSelectionCount() {
|
||||
document.getElementById('selection-count').textContent = selectedFiles.size + ' files selected';
|
||||
}
|
||||
|
||||
function selectAllFiles() {
|
||||
document.querySelectorAll('.file-checkbox').forEach(cb => {
|
||||
if (!cb.disabled) {
|
||||
cb.checked = true;
|
||||
selectedFiles.add(cb.dataset.filename);
|
||||
}
|
||||
});
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function deselectAllFiles() {
|
||||
document.querySelectorAll('.file-checkbox').forEach(cb => {
|
||||
cb.checked = false;
|
||||
});
|
||||
selectedFiles.clear();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function toggleFileSelection(filename, checked) {
|
||||
if (checked) {
|
||||
selectedFiles.add(filename);
|
||||
} else {
|
||||
selectedFiles.delete(filename);
|
||||
}
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function downloadSelectedFiles() {
|
||||
if (selectedFiles.size === 0) {
|
||||
alert('Please select files to download');
|
||||
return;
|
||||
}
|
||||
|
||||
// 각 파일을 순차적으로 다운로드
|
||||
let filesArray = Array.from(selectedFiles);
|
||||
let index = 0;
|
||||
|
||||
function downloadNext() {
|
||||
if (index < filesArray.length) {
|
||||
downloadFile(filesArray[index]);
|
||||
index++;
|
||||
setTimeout(downloadNext, 500); // 500ms 간격으로 다운로드
|
||||
}
|
||||
}
|
||||
|
||||
downloadNext();
|
||||
}
|
||||
|
||||
function deleteSelectedFiles() {
|
||||
if (selectedFiles.size === 0) {
|
||||
alert('Please select files to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
let filesArray = Array.from(selectedFiles);
|
||||
let fileList = filesArray.join('\\n');
|
||||
|
||||
if (!confirm('Are you sure you want to delete ' + selectedFiles.size + ' files?\\n\\n' + fileList + '\\n\\nThis action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
let filenames = JSON.stringify(filesArray);
|
||||
ws.send(JSON.stringify({cmd: 'deleteFiles', filenames: filesArray}));
|
||||
console.log('Delete multiple files command sent:', filesArray);
|
||||
|
||||
// 선택 해제
|
||||
selectedFiles.clear();
|
||||
updateSelectionCount();
|
||||
}
|
||||
}
|
||||
|
||||
function editComment(filename, currentComment) {
|
||||
const fileItem = event.target.closest('.file-item');
|
||||
const commentDiv = fileItem.querySelector('.file-comment');
|
||||
|
||||
// 이미 편집 중이면 무시
|
||||
if (fileItem.querySelector('.comment-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 코멘트 편집 UI 생성
|
||||
const input = document.createElement('textarea');
|
||||
input.className = 'comment-input';
|
||||
input.value = currentComment;
|
||||
input.rows = 2;
|
||||
input.placeholder = 'Enter comment for this log file...';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'comment-actions';
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = 'Save';
|
||||
saveBtn.style.background = 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)';
|
||||
saveBtn.onclick = function() {
|
||||
saveComment(filename, input.value, fileItem);
|
||||
};
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
cancelBtn.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)';
|
||||
cancelBtn.onclick = function() {
|
||||
cancelEditComment(fileItem, currentComment);
|
||||
};
|
||||
|
||||
actions.appendChild(saveBtn);
|
||||
actions.appendChild(cancelBtn);
|
||||
|
||||
// 기존 코멘트 숨기고 편집 UI 표시
|
||||
commentDiv.style.display = 'none';
|
||||
fileItem.querySelector('.file-info').appendChild(input);
|
||||
fileItem.querySelector('.file-info').appendChild(actions);
|
||||
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function saveComment(filename, comment, fileItem) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
// JSON 이스케이프
|
||||
const escapedComment = comment.replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
||||
ws.send(JSON.stringify({cmd: 'saveComment', filename: filename, comment: escapedComment}));
|
||||
console.log('Save comment:', filename, comment);
|
||||
|
||||
// UI 업데이트
|
||||
const commentDiv = fileItem.querySelector('.file-comment');
|
||||
const input = fileItem.querySelector('.comment-input');
|
||||
const actions = fileItem.querySelector('.comment-actions');
|
||||
|
||||
if (comment.trim() === '') {
|
||||
commentDiv.textContent = '💬 Click to add comment';
|
||||
commentDiv.className = 'file-comment empty';
|
||||
} else {
|
||||
commentDiv.textContent = '💬 ' + comment;
|
||||
commentDiv.className = 'file-comment';
|
||||
}
|
||||
|
||||
commentDiv.style.display = 'block';
|
||||
commentDiv.onclick = function() { editComment(filename, comment); };
|
||||
|
||||
if (input) input.remove();
|
||||
if (actions) actions.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditComment(fileItem, originalComment) {
|
||||
const commentDiv = fileItem.querySelector('.file-comment');
|
||||
const input = fileItem.querySelector('.comment-input');
|
||||
const actions = fileItem.querySelector('.comment-actions');
|
||||
|
||||
commentDiv.style.display = 'block';
|
||||
|
||||
if (input) input.remove();
|
||||
if (actions) actions.remove();
|
||||
}
|
||||
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
@@ -659,6 +953,20 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
updateFileList(data.files);
|
||||
} else if (data.type === 'deleteResult') {
|
||||
handleDeleteResult(data);
|
||||
} else if (data.type === 'autoTimeSyncRequest') {
|
||||
// 서버에서 자동 시간 동기화 요청
|
||||
console.log('Auto time sync requested by server');
|
||||
syncTime();
|
||||
// 동기화 완료 알림
|
||||
setTimeout(() => {
|
||||
ws.send(JSON.stringify({cmd: 'requestAutoTimeSync'}));
|
||||
}, 500);
|
||||
} else if (data.type === 'commentResult') {
|
||||
if (data.success) {
|
||||
console.log('Comment saved successfully');
|
||||
} else {
|
||||
alert('Failed to save comment: ' + data.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse error:', e);
|
||||
@@ -763,6 +1071,27 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
} else {
|
||||
document.getElementById('power-status').classList.remove('low');
|
||||
}
|
||||
|
||||
// SD 카드 용량 업데이트
|
||||
if (data.sdTotalMB !== undefined) {
|
||||
const totalGB = (data.sdTotalMB / 1024).toFixed(2);
|
||||
const usedMB = data.sdUsedMB || 0;
|
||||
const freeMB = data.sdFreeMB || 0;
|
||||
|
||||
document.getElementById('sd-total').textContent = totalGB + ' GB';
|
||||
|
||||
if (usedMB >= 1024) {
|
||||
document.getElementById('sd-used').textContent = (usedMB / 1024).toFixed(2) + ' GB';
|
||||
} else {
|
||||
document.getElementById('sd-used').textContent = usedMB + ' MB';
|
||||
}
|
||||
|
||||
if (freeMB >= 1024) {
|
||||
document.getElementById('sd-free').textContent = (freeMB / 1024).toFixed(2) + ' GB';
|
||||
} else {
|
||||
document.getElementById('sd-free').textContent = freeMB + ' MB';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addCanMessage(data) {
|
||||
@@ -876,10 +1205,26 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
}
|
||||
nameHtml += '</div>';
|
||||
|
||||
// 코멘트 표시
|
||||
const comment = file.comment || '';
|
||||
let commentHtml = '';
|
||||
if (comment.trim() === '') {
|
||||
commentHtml = '<div class="file-comment empty" onclick="editComment(\'' + file.name + '\', \'\')">💬 Click to add comment</div>';
|
||||
} else {
|
||||
const escapedComment = comment.replace(/'/g, "\\'");
|
||||
commentHtml = '<div class="file-comment" onclick="editComment(\'' + file.name + '\', \'' + escapedComment + '\')">💬 ' + comment + '</div>';
|
||||
}
|
||||
|
||||
const isChecked = selectedFiles.has(file.name);
|
||||
|
||||
fileItem.innerHTML =
|
||||
'<input type="checkbox" class="file-checkbox" data-filename="' + file.name + '" ' +
|
||||
'onchange="toggleFileSelection(\'' + file.name + '\', this.checked)" ' +
|
||||
(isLogging ? 'disabled' : '') + (isChecked ? ' checked' : '') + '>' +
|
||||
'<div class="file-info">' +
|
||||
nameHtml +
|
||||
'<div class="file-size">' + formatBytes(file.size) + '</div>' +
|
||||
commentHtml +
|
||||
'</div>' +
|
||||
'<div class="file-actions">' +
|
||||
'<button class="download-btn" onclick="downloadFile(\'' + file.name + '\')">Download</button>' +
|
||||
@@ -888,6 +1233,8 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
'</div>';
|
||||
fileList.appendChild(fileItem);
|
||||
});
|
||||
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
@@ -961,7 +1308,18 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
|
||||
function handleDeleteResult(data) {
|
||||
if (data.success) {
|
||||
console.log('File deleted successfully');
|
||||
if (data.deletedCount !== undefined) {
|
||||
// 복수 파일 삭제 결과
|
||||
let message = 'Deleted ' + data.deletedCount + ' file(s) successfully';
|
||||
if (data.failedCount > 0) {
|
||||
message += '\nFailed: ' + data.failedCount + ' file(s)';
|
||||
}
|
||||
alert(message);
|
||||
console.log('Multiple files deleted:', data);
|
||||
} else {
|
||||
// 단일 파일 삭제 결과
|
||||
console.log('File deleted successfully');
|
||||
}
|
||||
// 파일 목록은 서버에서 자동으로 갱신됨
|
||||
} else {
|
||||
alert('Failed to delete file: ' + data.message);
|
||||
|
||||
Reference in New Issue
Block a user