Compare commits

...

4 Commits

Author SHA1 Message Date
Matthías Páll Gissurarson
08103d5a99
Merge d968f5fe2f into 78d6e5931c 2026-02-03 22:51:31 +01:00
Jake Kenneally
78d6e5931c
fix: Correct debugging_monitor.py script instructions (#676)
Some checks are pending
CI / build (push) Waiting to run
## Summary

**What is the goal of this PR?**
- Minor correction to the `debugging_monitor.py` script instructions

**What changes are included?**
- `pyserial` should be installed, NOT `serial`, which is a [different
lib](https://pypi.org/project/serial/)
- Added macOS serial port

## Additional Context

- Just a minor docs update. I can confirm the debugging script is
working great on macOS

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-04 00:33:20 +03:00
Luke Stein
dac11c3fdd
fix: Correct instruction text to match actual button text (#672)
## Summary

* Instruction text says "Press OK to scan again" but button label is
actually "Connect" (not OK)
* Corrects instruction text

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-04 00:32:52 +03:00
Matthías Páll Gissurarson
d968f5fe2f feat: rename and move in file manager 2026-01-31 16:33:41 +01:00
5 changed files with 481 additions and 11 deletions

View File

@ -102,13 +102,18 @@ After flashing the new features, its recommended to capture detailed logs fro
First, make sure all required Python packages are installed:
```python
python3 -m pip install serial colorama matplotlib
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
Minor adjustments may be required for Windows.
## Internals

View File

@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
} else {
// Calculate how many networks we can display
constexpr int startY = 60;

View File

@ -44,6 +44,36 @@ void clearEpubCacheIfNeeded(const String& filePath) {
Serial.printf("[%lu] [WEB] Cleared epub cache for: %s\n", millis(), filePath.c_str());
}
}
String normalizeWebPath(const String& inputPath) {
if (inputPath.isEmpty() || inputPath == "/") {
return "/";
}
std::string normalized = FsHelpers::normalisePath(inputPath.c_str());
String result = normalized.c_str();
if (result.isEmpty()) {
return "/";
}
if (!result.startsWith("/")) {
result = "/" + result;
}
if (result.length() > 1 && result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
bool isProtectedItemName(const String& name) {
if (name.startsWith(".")) {
return true;
}
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
if (name.equals(HIDDEN_ITEMS[i])) {
return true;
}
}
return false;
}
} // namespace
// File listing page template - now using generated headers:
@ -109,6 +139,12 @@ void CrossPointWebServer::begin() {
// Create folder endpoint
server->on("/mkdir", HTTP_POST, [this] { handleCreateFolder(); });
// Rename file endpoint
server->on("/rename", HTTP_POST, [this] { handleRename(); });
// Move file endpoint
server->on("/move", HTTP_POST, [this] { handleMove(); });
// Delete file/folder endpoint
server->on("/delete", HTTP_POST, [this] { handleDelete(); });
@ -705,6 +741,181 @@ void CrossPointWebServer::handleCreateFolder() const {
}
}
void CrossPointWebServer::handleRename() const {
if (!server->hasArg("path") || !server->hasArg("name")) {
server->send(400, "text/plain", "Missing path or new name");
return;
}
String itemPath = normalizeWebPath(server->arg("path"));
String newName = server->arg("name");
newName.trim();
if (itemPath.isEmpty() || itemPath == "/") {
server->send(400, "text/plain", "Invalid path");
return;
}
if (newName.isEmpty()) {
server->send(400, "text/plain", "New name cannot be empty");
return;
}
if (newName.indexOf('/') >= 0 || newName.indexOf('\\') >= 0) {
server->send(400, "text/plain", "Invalid file name");
return;
}
if (isProtectedItemName(newName)) {
server->send(403, "text/plain", "Cannot rename to protected name");
return;
}
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
if (isProtectedItemName(itemName)) {
server->send(403, "text/plain", "Cannot rename protected item");
return;
}
if (newName == itemName) {
server->send(200, "text/plain", "Name unchanged");
return;
}
if (!SdMan.exists(itemPath.c_str())) {
server->send(404, "text/plain", "Item not found");
return;
}
FsFile file = SdMan.open(itemPath.c_str());
if (!file) {
server->send(500, "text/plain", "Failed to open file");
return;
}
if (file.isDirectory()) {
file.close();
server->send(400, "text/plain", "Only files can be renamed");
return;
}
String parentPath = itemPath.substring(0, itemPath.lastIndexOf('/'));
if (parentPath.isEmpty()) {
parentPath = "/";
}
String newPath = parentPath;
if (!newPath.endsWith("/")) {
newPath += "/";
}
newPath += newName;
if (SdMan.exists(newPath.c_str())) {
file.close();
server->send(409, "text/plain", "Target already exists");
return;
}
clearEpubCacheIfNeeded(itemPath);
const bool success = file.rename(newPath.c_str());
file.close();
if (success) {
Serial.printf("[%lu] [WEB] Renamed file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
server->send(200, "text/plain", "Renamed successfully");
} else {
Serial.printf("[%lu] [WEB] Failed to rename file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
server->send(500, "text/plain", "Failed to rename file");
}
}
void CrossPointWebServer::handleMove() const {
if (!server->hasArg("path") || !server->hasArg("dest")) {
server->send(400, "text/plain", "Missing path or destination");
return;
}
String itemPath = normalizeWebPath(server->arg("path"));
String destPath = normalizeWebPath(server->arg("dest"));
if (itemPath.isEmpty() || itemPath == "/") {
server->send(400, "text/plain", "Invalid path");
return;
}
if (destPath.isEmpty()) {
server->send(400, "text/plain", "Invalid destination");
return;
}
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
if (isProtectedItemName(itemName)) {
server->send(403, "text/plain", "Cannot move protected item");
return;
}
if (destPath != "/") {
const String destName = destPath.substring(destPath.lastIndexOf('/') + 1);
if (isProtectedItemName(destName)) {
server->send(403, "text/plain", "Cannot move into protected folder");
return;
}
}
if (!SdMan.exists(itemPath.c_str())) {
server->send(404, "text/plain", "Item not found");
return;
}
FsFile file = SdMan.open(itemPath.c_str());
if (!file) {
server->send(500, "text/plain", "Failed to open file");
return;
}
if (file.isDirectory()) {
file.close();
server->send(400, "text/plain", "Only files can be moved");
return;
}
if (!SdMan.exists(destPath.c_str())) {
file.close();
server->send(404, "text/plain", "Destination not found");
return;
}
FsFile destDir = SdMan.open(destPath.c_str());
if (!destDir || !destDir.isDirectory()) {
if (destDir) {
destDir.close();
}
file.close();
server->send(400, "text/plain", "Destination is not a folder");
return;
}
destDir.close();
String newPath = destPath;
if (!newPath.endsWith("/")) {
newPath += "/";
}
newPath += itemName;
if (newPath == itemPath) {
file.close();
server->send(200, "text/plain", "Already in destination");
return;
}
if (SdMan.exists(newPath.c_str())) {
file.close();
server->send(409, "text/plain", "Target already exists");
return;
}
clearEpubCacheIfNeeded(itemPath);
const bool success = file.rename(newPath.c_str());
file.close();
if (success) {
Serial.printf("[%lu] [WEB] Moved file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
server->send(200, "text/plain", "Moved successfully");
} else {
Serial.printf("[%lu] [WEB] Failed to move file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
server->send(500, "text/plain", "Failed to move file");
}
}
void CrossPointWebServer::handleDelete() const {
// Get path from form data
if (!server->hasArg("path")) {

View File

@ -77,5 +77,7 @@ class CrossPointWebServer {
void handleUpload() const;
void handleUploadPost() const;
void handleCreateFolder() const;
void handleRename() const;
void handleMove() const;
void handleDelete() const;
};

View File

@ -322,25 +322,47 @@
.folder-btn:hover {
background-color: #d68910;
}
/* Delete button styles */
.delete-btn {
/* Action button styles */
.delete-btn,
.rename-btn,
.move-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1.1em;
padding: 4px 8px;
border-radius: 4px;
color: #95a5a6;
transition: all 0.15s;
}
.delete-btn {
color: #95a5a6;
}
.delete-btn:hover {
background-color: #fee;
color: #e74c3c;
}
.rename-btn {
color: #2980b9;
}
.rename-btn:hover {
background-color: #e8f4fd;
}
.move-btn {
color: #16a085;
}
.move-btn:hover {
background-color: #e6f7f4;
}
.actions-col {
width: 60px;
width: 140px;
text-align: center;
}
.action-icon-group {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
/* Failed uploads banner */
.failed-uploads-banner {
background-color: #fff3cd;
@ -463,6 +485,32 @@
.delete-btn-cancel:hover {
background-color: #7f8c8d;
}
.rename-btn-confirm {
background-color: #3498db;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
width: 100%;
}
.rename-btn-confirm:hover {
background-color: #2e86c1;
}
.move-btn-confirm {
background-color: #16a085;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
width: 100%;
}
.move-btn-confirm:hover {
background-color: #138d75;
}
.loader-container {
display: flex;
justify-content: center;
@ -558,12 +606,17 @@
font-size: 1.1em;
}
.actions-col {
width: 40px;
width: 120px;
}
.delete-btn {
.delete-btn,
.rename-btn,
.move-btn {
font-size: 1em;
padding: 2px 4px;
}
.action-icon-group {
gap: 4px;
}
.no-files {
padding: 20px;
font-size: 0.9em;
@ -665,6 +718,37 @@
</div>
</div>
<!-- Rename Modal -->
<div class="modal-overlay" id="renameModal">
<div class="modal">
<button class="modal-close" onclick="closeRenameModal()">&times;</button>
<h3>✏️ Rename File</h3>
<div class="folder-form">
<p class="file-info">Renaming <strong id="renameItemName"></strong></p>
<input type="text" id="renameNewName" class="folder-input" placeholder="New file name...">
<input type="hidden" id="renameItemPath">
<button class="rename-btn-confirm" onclick="confirmRename()">Rename</button>
<button class="delete-btn-cancel" onclick="closeRenameModal()">Cancel</button>
</div>
</div>
</div>
<!-- Move Modal -->
<div class="modal-overlay" id="moveModal">
<div class="modal">
<button class="modal-close" onclick="closeMoveModal()">&times;</button>
<h3>📂 Move File</h3>
<div class="folder-form">
<p class="file-info">Moving <strong id="moveItemName"></strong></p>
<input type="text" id="moveDestPath" class="folder-input" list="moveFolderOptions" placeholder="/Destination/Folder">
<datalist id="moveFolderOptions"></datalist>
<input type="hidden" id="moveItemPath">
<button class="move-btn-confirm" onclick="confirmMove()">Move</button>
<button class="delete-btn-cancel" onclick="closeMoveModal()">Cancel</button>
</div>
</div>
</div>
<script>
// get current path from query parameter
const currentPath = decodeURIComponent(new URLSearchParams(window.location.search).get('path') || '/');
@ -760,7 +844,7 @@
fileTableContent += `<td><span class="file-icon">📁</span><a href="/files?path=${encodeURIComponent(folderPath)}" class="folder-link">${escapeHtml(file.name)}</a><span class="folder-badge">FOLDER</span></td>`;
fileTableContent += '<td>Folder</td>';
fileTableContent += '<td>-</td>';
fileTableContent += `<td class="actions-col"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${folderPath.replaceAll("'", "\\'")}', true)" title="Delete folder">🗑️</button></td>`;
fileTableContent += `<td class="actions-col"><div class="action-icon-group"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${folderPath.replaceAll("'", "\\'")}', true)" title="Delete folder">🗑️</button></div></td>`;
fileTableContent += '</tr>';
} else {
let filePath = currentPath;
@ -773,7 +857,11 @@
fileTableContent += '</td>';
fileTableContent += `<td>${file.name.split('.').pop().toUpperCase()}</td>`;
fileTableContent += `<td>${formatFileSize(file.size)}</td>`;
fileTableContent += `<td class="actions-col"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}', false)" title="Delete file">🗑️</button></td>`;
fileTableContent += `<td class="actions-col"><div class="action-icon-group">`;
fileTableContent += `<button class="move-btn" onclick="openMoveModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}' )" title="Move file">📂</button>`;
fileTableContent += `<button class="rename-btn" onclick="openRenameModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}' )" title="Rename file">✏️</button>`;
fileTableContent += `<button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}', false)" title="Delete file">🗑️</button>`;
fileTableContent += `</div></td>`;
fileTableContent += '</tr>';
}
});
@ -1175,6 +1263,170 @@ function retryAllFailedUploads() {
xhr.send(formData);
}
// Rename functions
function openRenameModal(name, path) {
document.getElementById('renameItemName').textContent = '📄 ' + name;
document.getElementById('renameItemPath').value = path;
document.getElementById('renameNewName').value = name;
document.getElementById('renameModal').classList.add('open');
setTimeout(() => {
const input = document.getElementById('renameNewName');
input.focus();
input.select();
}, 50);
}
function closeRenameModal() {
document.getElementById('renameModal').classList.remove('open');
}
function confirmRename() {
const path = document.getElementById('renameItemPath').value;
const newName = document.getElementById('renameNewName').value.trim();
if (!newName) {
alert('Please enter a new name.');
return;
}
if (newName.includes('/') || newName.includes('\\')) {
alert('File name cannot include slashes.');
return;
}
const formData = new FormData();
formData.append('path', path);
formData.append('name', newName);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/rename', true);
xhr.onload = function() {
if (xhr.status === 200) {
window.location.reload();
} else {
alert('Failed to rename: ' + xhr.responseText);
}
closeRenameModal();
};
xhr.onerror = function() {
alert('Failed to rename - network error');
closeRenameModal();
};
xhr.send(formData);
}
// Move functions
function normalizePath(path) {
if (!path) return '/';
let normalized = path.trim();
if (!normalized.startsWith('/')) normalized = '/' + normalized;
if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
function getParentPath(path) {
const normalized = normalizePath(path);
if (normalized === '/') return '/';
const idx = normalized.lastIndexOf('/');
return idx <= 0 ? '/' : normalized.slice(0, idx);
}
async function loadMoveFolderOptions() {
const options = new Set();
options.add('/');
const parent = getParentPath(currentPath);
if (parent) options.add(parent);
async function fetchFolders(path) {
try {
const response = await fetch('/api/files?path=' + encodeURIComponent(path));
if (!response.ok) return [];
return await response.json();
} catch (e) {
return [];
}
}
const rootFiles = await fetchFolders('/');
rootFiles.forEach(file => {
if (file.isDirectory) {
options.add('/' + file.name);
}
});
if (currentPath !== '/') {
const currentFiles = await fetchFolders(currentPath);
currentFiles.forEach(file => {
if (file.isDirectory) {
let folderPath = currentPath;
if (!folderPath.endsWith('/')) folderPath += '/';
folderPath += file.name;
options.add(folderPath);
}
});
}
const dataList = document.getElementById('moveFolderOptions');
dataList.innerHTML = '';
Array.from(options).sort().forEach(path => {
const option = document.createElement('option');
option.value = path;
dataList.appendChild(option);
});
}
function openMoveModal(name, path) {
document.getElementById('moveItemName').textContent = '📄 ' + name;
document.getElementById('moveItemPath').value = path;
document.getElementById('moveDestPath').value = currentPath === '/' ? '/' : currentPath;
document.getElementById('moveModal').classList.add('open');
loadMoveFolderOptions();
setTimeout(() => {
document.getElementById('moveDestPath').focus();
}, 50);
}
function closeMoveModal() {
document.getElementById('moveModal').classList.remove('open');
}
function confirmMove() {
const path = document.getElementById('moveItemPath').value;
const destPath = normalizePath(document.getElementById('moveDestPath').value);
if (!destPath) {
alert('Please enter a destination folder.');
return;
}
const formData = new FormData();
formData.append('path', path);
formData.append('dest', destPath);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/move', true);
xhr.onload = function() {
if (xhr.status === 200) {
window.location.reload();
} else {
alert('Failed to move: ' + xhr.responseText);
}
closeMoveModal();
};
xhr.onerror = function() {
alert('Failed to move - network error');
closeMoveModal();
};
xhr.send(formData);
}
// Delete functions
function openDeleteModal(name, path, isFolder) {
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;