Browse Source

Merge branch 'feature_filebrowser'

master
Torsten Stauder 5 years ago
parent
commit
9c5e0acbeb
  1. 5
      README.md
  2. 410
      html/management.html
  3. 12
      platformio.ini
  4. 410
      src/HTMLmanagement.h
  5. 251
      src/main.cpp

5
README.md

@ -25,7 +25,7 @@ Finally, the long announced Tonuino-PCB for Wemos' Lolin32 is [there](https://gi
* 20.12.2020: Due to memory-issues with webstreams, FTP needs to be activated by pressing pause+next-button now
<br />More to come...
* 23.12.2020: User-config is now split into general part (settings.h) and develboard-specific part (e.g. settings-lolin32.h)
* 13.01.2020: Added fileexlorer to webgui. Now files and directories can be renamed, uploaded and deleted via webgui.
## Known bugs
* Some webstreams don't run. Guess it's a combination of saturated connection-pool and lack of heap-memory. Works probably better if ESP32-WROVER is used, as this chip has PSRAM. Advice: Don't enable modules (e.g. MQTT) if you don't need them as this could save memory.
* English translation for webgui is currently outdated. This will be fixed soon when i18n-support will be integrated.
@ -60,7 +60,7 @@ The heart of my project is an ESP32 on a [Wemos Lolin32 development-board](https
## Getting Started
* Arduino-IDE can be used but you need to satisfy dependencies for all the libraries listed in `platformio.ini` manually.
* Instead I recommend to install Microsoft's [Visual Studio Code](https://code.visualstudio.com/). This is a popular and powerful IDE that gives you the ability to install tons of (well-supported) plugins.
* Install [Platformio Plugin](https://platformio.org/install/ide?install=vscode) into [Visual Studio Code](https://code.visualstudio.com/) and make sure to have a look at the [documentation](https://docs.platformio.org/en/latest/integration/ide/pioide.html).
* Install [Platformio Plugin](https://platformio.org/install/ide?install=vscode) into [Visual Studio Code](https://code.visualstudio.com/) and make sure to have a look at the [documentation](https://docs.platformio.org/en/latest/integration/ide/pioide.html). Step-by-step-manual is available [here](https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/.)
* Install [Git](https://git-scm.com/downloads) and make a copy ("clone") my repository to your local computer using `git clone https://github.com/biologist79/Tonuino-ESP32-I2S.git`. Using git you can keep your local repository easily up to date without doing copy'n'paste. To keep it up to date run `git pull origin master`. Further infos [here}(https://stackoverflow.com/questions/1443210/updating-a-local-repository-with-changes-from-a-github-repository).
* (Optional) Install [Gitlens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) as plugin (to have advanced Git-support).
* Now, that the git-repository is saved locally, import this folder into Platformio as a project.
@ -367,6 +367,7 @@ After having Tonuino running on your ESP32 in your local WiFi, the webinterface-
* In order to avoid exposing uSD-card or disassembling Tonuino all the time for adding new music, it's possible to transfer music to the uSD-card using FTP.
* Default-user and password are set to `esp32` / `esp32` but can be changed later via GUI.
* Make sure to set the max. number of parallel connections to ONE in your FTP-client and the charset to ISO8859-1.
* Secured FTP is not available. So make sure to disable SSL/TLS.
* Software: my recommendation is [Filezilla](https://filezilla-project.org/) as it's free and available for multiple platforms.
* Don't expect a super fast data-transfer; it's around 185 kB/s (SPI-mode) and 310 kB/s (MMC-mode).
* Please note: if music is played in parallel, this rate decrases dramatically! So better stop playback when doing a FTP-transfer.

410
html/management.html

@ -117,6 +117,7 @@
<nav>
<div class="container nav nav-tabs" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-rfid-tab" data-toggle="tab" href="#nav-rfid" role="tab" aria-controls="nav-rfid" aria-selected="true"><i class="fas fa-dot-circle"></i> RFID</a>
<a class="nav-item nav-link" id="nav-files-tab" data-toggle="tab" href="#nav-files" role="tab" aria-controls="nav-files" aria-selected="false"><i class="fas fa-folder"></i> Dateien</a>
<a class="nav-item nav-link" id="nav-wifi-tab" data-toggle="tab" href="#nav-wifi" role="tab" aria-controls="nav-wifi" aria-selected="false"><i class="fas fa-wifi"></i><span class=".d-sm-none .d-md-block"> WLAN</span></a>
%SHOW_MQTT_TAB%
%SHOW_FTP_TAB%
@ -126,6 +127,21 @@
</nav>
<br>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade" id="nav-files" role="tabpanel" aria-labelledby="nav-files-tab">
<div class="container" id="fileExplorer">
<div class="ui-widget">
<div class="filetree demo" id="explorerTree"></div>
<form id="explorerUploadForm" method="POST" enctype="multipart/form-data" action="/explorer" accept-charset="iso-8859-1">
<input id="explorerUploadedFiles" type="file" class="form-control-file" name="explorerUploadFiles" multiple> <input type="submit" class="btn btn-primary" id="submit" value="Hochladen">
</form>
<div>
<progress id="explorerUploadProgress" style="margin-top:10px" value="0" max="100"></progress> <span id="explorerUploadPercent"></span>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="nav-wifi" role="tabpanel" aria-labelledby="nav-wifi-tab">
<div class="container" id="wifiConfig">
<form action="#wifiConfig" method="POST" onsubmit="wifiConfig('wifiConfig'); return false">
@ -443,6 +459,399 @@
});
}
/* File Explorer functions begin*/
var lastSelectedNodePath = "";
$('#explorerTree').on('select_node.jstree', function (e, data) {
$('input[name=fileOrUrl]').val(data.node.data.path);
if (data.node.type == "folder") {
$('.option-folder').show();
$('.option-file').hide();
$('#playMode option').removeAttr('selected').filter('[value=3]').attr('selected', true);
}
if (data.node.type == "audio") {
$('.option-file').show();
$('.option-folder').hide();
$('#playMode option').removeAttr('selected').filter('[value=1]').attr('selected', true);
}
if(lastSelectedNodePath != data.node.data.path) {
if (data.node.data.directory) {
var ref = $('#explorerTree').jstree(true),
sel = ref.get_selected();
if(!sel.length) { return false; }
sel = sel[0];
var children = $("#explorerTree").jstree("get_children_dom",sel);
/* refresh only, when there is no child -> possible not yet updated */
if(children.length < 1){
refreshNode(sel);
}
}
lastSelectedNodePath = data.node.data.path;
}
});
function doRest(path, callback, obj) {
obj.url = path;
obj.dataType = "json";
obj.contentType= "application/json;charset=iso-8859-1",
obj.scriptCharset= "iso-8859-1",
obj.success = function(data, textStatus, jqXHR) {
if (callback) {
callback(data);
}
};
obj.error = function(jqXHR, textStatus, errorThrown) {
console.log("AJAX error");
/*debugger; */
};
jQuery.ajax(obj);
} /* doRest */
function getData(path, callback) {
doRest(path, callback, {
method : "GET"
});
} /* getData */
function deleteData(path, callback, _data) {
doRest(path, callback, {
method : "DELETE",
data: _data
});
} /* deleteData */
function patchData(path, callback, _data) {
doRest(path, callback, {
method : "PATCH",
data: _data
});
} /* patchData */
function postData(path, callback, _data) {
doRest(path, callback, {
method : "POST",
data: _data
});
} /* postData */
function putData(path, callback, _data) {
doRest(path, callback, {
method : "PUT",
data: _data
});
} /* putData */
/* File Upload */
$('#explorerUploadForm').submit(function(e){
e.preventDefault();
console.log("Upload!");
var data = new FormData(this);
var ref = $('#explorerTree').jstree(true),
sel = ref.get_selected(),
path = "/";
if(!sel.length) { alert("Please select the upload location!");return false; }
sel = sel[0];
selectedNode = ref.get_node(sel);
if(selectedNode.data.directory){
path = selectedNode.data.path
} else {
/* remap sel to parent folder */
sel = ref.get_node(ref.get_parent(sel));
path = parentNode.data.path;
console.log("Parent path: " + path);
}
$.ajax({
url: '/explorer?path=' + path,
type: 'POST',
data: data,
contentType: false,
processData:false,
xhr: function() {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total;
percentComplete = parseInt(percentComplete * 100);
console.log(percentComplete);
document.getElementById("explorerUploadProgress").value = percentComplete;
document.getElementById("explorerUploadPercent").innerHTML = percentComplete + "%";
if (percentComplete === 100) {
}
Sleep(1000);
}
}, false);
return xhr;
},
success: function(data, textStatus, jqXHR) {
console.log("Upload success!");
getData("/explorer?path=" + path, function(data) {
/* We now have data! */
deleteChildrenNodes(sel);
addFileDirectory(sel, data);
ref.open_node(sel);
});
}
});
});
/* File Delete */
function handleDeleteData(nodeId) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
var children = $("#explorerTree").jstree("get_children_dom",nodeId);
console.log(children.length);
if(node.data.directory) {
if(children.length > 0) {
for(var i=0;i<children.length;i++)
{
console.log("call delete function for: " + children[i].text);
handleDeleteData(children[i].id);
}
}
}
console.log("call delete request: " + node.data.path);
deleteData("/explorer?path=" + node.data.path);
}
function Sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function fileNameSort( a, b ) {
if ( a.dir && !b.dir ) {
return -1
}
if ( !a.dir && b.dir ) {
return 1
}
if ( a.name < b.name ){
return -1;
}
if ( a.name > b.name ){
return 1;
}
return 0;
}
function createChild(nodeId, data) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
var child = {
text: data.name,
type: getType(data),
data: {
path: node.data.path + "/" + data.name,
directory: data.dir
}
};
return child;
}
function deleteChildrenNodes(nodeId) {
var ref = $('#explorerTree').jstree(true);
var children = $("#explorerTree").jstree("get_children_dom",nodeId);
for(var i=0;i<children.length;i++)
{
ref.delete_node(children[i].id);
}
}
function refreshNode(nodeId) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
getData("/explorer?path=" + node.data.path, function(data) {
/* We now have data! */
deleteChildrenNodes(nodeId);
addFileDirectory(nodeId, data);
ref.open_node(nodeId);
});
}
function getType(data) {
var type = "";
if(data.dir) {
type = "folder";
} else if ((/\.(mp3|MP3|ogg|wav|WAV|OGG|wma|WMA|acc|ACC|flac|FLAC)$/i).test(data.name)) {
type = "audio";
} else {
type = "file";
}
return type;
}
function addFileDirectory(parent, data) {
data.sort( fileNameSort );
var ref = $('#explorerTree').jstree(true);
for (var i=0; i<data.length; i++) {
console.log("Create Node");
ref.create_node(parent, createChild(parent, data[i]));
}
} /* addFileDirectory */
function buildFileSystemTree(path) {
$('#explorerTree').jstree({
"core" : {
"check_callback" : true,
'force_text' : true,
"themes" : { "stripes" : true },
'data' : { text: '/',
state: {
opened: true
},
type: 'folder',
children: [],
data: {
path: '/',
directory: true
}}
},
'types': {
'folder': {
'icon': "fa fa-folder"
},
'file': {
'icon': "fa fa-file"
},
'audio': {
'icon': "fa fa-file-audio"
},
'default': {
'icon': "fa fa-folder"
}
},
plugins: ["contextmenu", "themes", "types"],
contextmenu: {
items: function(nodeId) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
var items = {};
if (node.data.directory) {
items.createDir = {
label: "Neuer Ordner",
action: function(x) {
var childNode = ref.create_node(nodeId, {text: "Neuer Ordner", type: "folder"});
if(childNode) {
ref.edit(childNode, null, function(childNode, status){
putData("/explorer?path=" + node.data.path + "/" + childNode.text);
refreshNode(nodeId);
});
}
}
};
}
/* Play */
items.play = {
label: "Abspielen",
action: function(x) {
var playMode = node.data.directory?"5":"1";
postData("/exploreraudio?path=" + node.data.path + "&playmode=" + playMode);
}
};
/* Refresh */
items.refresh = {
label: "Aktualisieren",
action: function(x) {
refreshNode(nodeId);
}
};
/* Delete */
items.delete = {
label: "Loeschen",
action: function(x) {
handleDeleteData(nodeId);
refreshNode(ref.get_parent(nodeId));
}
};
/* Rename */
items.rename = {
label: "Umbenennen",
action: function(x) {
var srcPath = node.data.path;
ref.edit(nodeId, null, function(node, status){
node.data.path = node.data.path.substring(0,node.data.path.lastIndexOf("/")+1) + node.text;
patchData("/explorer?srcpath=" + srcPath + "&dstpath=" + node.data.path);
refreshNode(ref.get_parent(nodeId));
});
}
};
return items;
}
}
});
if (path.length == 0) {
return;
}
getData("/explorer?path=/", function(data) {
/* We now have data! */
$('#explorerTree').jstree(true).settings.core.data.children = [];
data.sort( fileNameSort );
for (var i=0; i<data.length; i++) {
var newChild = {
text: data[i].name,
type: getType(data[i]),
data: {
path: "/" + data[i].name,
directory: data[i].dir
},
children: []
};
$('#explorerTree').jstree(true).settings.core.data.children.push(newChild);
}
$("#explorerTree").jstree(true).refresh();
});
} /* buildFileSystemTree */
/* File Explorer functions end */
function renderFileTree() {
var filesURI = "/files";
@ -715,6 +1124,7 @@
$(document).ready(function () {
connect();
renderFileTree();
buildFileSystemTree("/");
console.log(parseInt(document.getElementById('warningLowVoltage').value));
$(function () {

12
platformio.ini

@ -36,7 +36,7 @@ platform = espressif32
board = esp-wrover-kit
framework = arduino
monitor_speed = 115200
board_build.partitions = no_ota.csv
board_build.partitions = huge_ap.csv
build_flags = -DHAL=2
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
@ -52,7 +52,7 @@ platform = espressif32
board = lolin32
framework = arduino
monitor_speed = 115200
board_build.partitions = no_ota.csv
board_build.partitions = huge_app.csv
lib_deps =
${common.lib_deps_builtin}
${common.lib_deps_external}
@ -68,7 +68,7 @@ platform = espressif32
board = lolin_d32
framework = arduino
monitor_speed = 115200
board_build.partitions = no_ota.csv
board_build.partitions = huge_app.csv
lib_deps =
${common.lib_deps_builtin}
${common.lib_deps_external}
@ -84,7 +84,7 @@ platform = espressif32
board = lolin_d32_pro
framework = arduino
monitor_speed = 115200
board_build.partitions = no_ota.csv
board_build.partitions = huge_app.csv
lib_deps =
${common.lib_deps_builtin}
${common.lib_deps_external}
@ -102,7 +102,7 @@ platform = espressif32
board = nodemcu-32s
framework = arduino
monitor_speed = 115200
board_build.partitions = no_ota.csv
board_build.partitions = huge_app.csv
lib_deps =
${common.lib_deps_builtin}
${common.lib_deps_external}
@ -118,7 +118,7 @@ platform = espressif32
board = az-delivery-devkit-v4
framework = arduino
monitor_speed = 115200
board_build.partitions = no_ota.csv
board_build.partitions = huge_app.csv
lib_deps =
${common.lib_deps_builtin}
${common.lib_deps_external}

410
src/HTMLmanagement.h

@ -117,6 +117,7 @@ static const char management_HTML[] PROGMEM = "<!DOCTYPE html>\
<nav>\
<div class=\"container nav nav-tabs\" id=\"nav-tab\" role=\"tablist\">\
<a class=\"nav-item nav-link active\" id=\"nav-rfid-tab\" data-toggle=\"tab\" href=\"#nav-rfid\" role=\"tab\" aria-controls=\"nav-rfid\" aria-selected=\"true\"><i class=\"fas fa-dot-circle\"></i> RFID</a>\
<a class=\"nav-item nav-link\" id=\"nav-files-tab\" data-toggle=\"tab\" href=\"#nav-files\" role=\"tab\" aria-controls=\"nav-files\" aria-selected=\"false\"><i class=\"fas fa-folder\"></i> Dateien</a>\
<a class=\"nav-item nav-link\" id=\"nav-wifi-tab\" data-toggle=\"tab\" href=\"#nav-wifi\" role=\"tab\" aria-controls=\"nav-wifi\" aria-selected=\"false\"><i class=\"fas fa-wifi\"></i><span class=\".d-sm-none .d-md-block\"> WLAN</span></a>\
%SHOW_MQTT_TAB%\
%SHOW_FTP_TAB%\
@ -126,6 +127,21 @@ static const char management_HTML[] PROGMEM = "<!DOCTYPE html>\
</nav>\
<br>\
<div class=\"tab-content\" id=\"nav-tabContent\">\
<div class=\"tab-pane fade\" id=\"nav-files\" role=\"tabpanel\" aria-labelledby=\"nav-files-tab\">\
<div class=\"container\" id=\"fileExplorer\">\
<div class=\"ui-widget\">\
<div class=\"filetree demo\" id=\"explorerTree\"></div>\
\
<form id=\"explorerUploadForm\" method=\"POST\" enctype=\"multipart/form-data\" action=\"/explorer\" accept-charset=\"iso-8859-1\">\
<input id=\"explorerUploadedFiles\" type=\"file\" class=\"form-control-file\" name=\"explorerUploadFiles\" multiple> <input type=\"submit\" class=\"btn btn-primary\" id=\"submit\" value=\"Hochladen\">\
</form>\
<div>\
<progress id=\"explorerUploadProgress\" style=\"margin-top:10px\" value=\"0\" max=\"100\"></progress> <span id=\"explorerUploadPercent\"></span>\
</div>\
\
</div>\
</div>\
</div>\
<div class=\"tab-pane fade\" id=\"nav-wifi\" role=\"tabpanel\" aria-labelledby=\"nav-wifi-tab\">\
<div class=\"container\" id=\"wifiConfig\">\
<form action=\"#wifiConfig\" method=\"POST\" onsubmit=\"wifiConfig('wifiConfig'); return false\">\
@ -443,6 +459,399 @@ static const char management_HTML[] PROGMEM = "<!DOCTYPE html>\
});\
}\
\
/* File Explorer functions begin*/\
var lastSelectedNodePath = \"\";\
\
$('#explorerTree').on('select_node.jstree', function (e, data) {\
\
$('input[name=fileOrUrl]').val(data.node.data.path);\
\
if (data.node.type == \"folder\") {\
$('.option-folder').show();\
$('.option-file').hide();\
$('#playMode option').removeAttr('selected').filter('[value=3]').attr('selected', true);\
}\
\
if (data.node.type == \"audio\") {\
$('.option-file').show();\
$('.option-folder').hide();\
$('#playMode option').removeAttr('selected').filter('[value=1]').attr('selected', true);\
}\
\
if(lastSelectedNodePath != data.node.data.path) {\
if (data.node.data.directory) {\
\
var ref = $('#explorerTree').jstree(true),\
sel = ref.get_selected();\
if(!sel.length) { return false; }\
sel = sel[0];\
var children = $(\"#explorerTree\").jstree(\"get_children_dom\",sel);\
/* refresh only, when there is no child -> possible not yet updated */\
if(children.length < 1){\
refreshNode(sel);\
}\
\
}\
lastSelectedNodePath = data.node.data.path;\
}\
\
\
});\
\
function doRest(path, callback, obj) {\
obj.url = path;\
obj.dataType = \"json\";\
obj.contentType= \"application/json;charset=iso-8859-1\",\
obj.scriptCharset= \"iso-8859-1\",\
obj.success = function(data, textStatus, jqXHR) {\
if (callback) {\
callback(data);\
}\
};\
obj.error = function(jqXHR, textStatus, errorThrown) {\
console.log(\"AJAX error\");\
/*debugger; */\
};\
jQuery.ajax(obj);\
} /* doRest */\
\
function getData(path, callback) {\
doRest(path, callback, {\
method : \"GET\"\
});\
} /* getData */\
\
function deleteData(path, callback, _data) {\
doRest(path, callback, {\
method : \"DELETE\",\
data: _data\
});\
} /* deleteData */\
\
function patchData(path, callback, _data) {\
doRest(path, callback, {\
method : \"PATCH\",\
data: _data\
});\
} /* patchData */\
\
function postData(path, callback, _data) {\
doRest(path, callback, {\
method : \"POST\",\
data: _data\
});\
} /* postData */\
\
\
function putData(path, callback, _data) {\
doRest(path, callback, {\
method : \"PUT\",\
data: _data\
});\
} /* putData */\
\
\
/* File Upload */\
$('#explorerUploadForm').submit(function(e){\
e.preventDefault();\
console.log(\"Upload!\");\
var data = new FormData(this);\
\
var ref = $('#explorerTree').jstree(true),\
sel = ref.get_selected(),\
path = \"/\";\
if(!sel.length) { alert(\"Please select the upload location!\");return false; }\
sel = sel[0];\
selectedNode = ref.get_node(sel);\
if(selectedNode.data.directory){\
path = selectedNode.data.path\
} else {\
/* remap sel to parent folder */\
sel = ref.get_node(ref.get_parent(sel));\
path = parentNode.data.path;\
console.log(\"Parent path: \" + path);\
}\
\
$.ajax({\
url: '/explorer?path=' + path,\
type: 'POST',\
data: data,\
contentType: false,\
processData:false,\
xhr: function() {\
var xhr = new window.XMLHttpRequest();\
\
xhr.upload.addEventListener(\"progress\", function(evt) {\
if (evt.lengthComputable) {\
var percentComplete = evt.loaded / evt.total;\
percentComplete = parseInt(percentComplete * 100);\
console.log(percentComplete);\
document.getElementById(\"explorerUploadProgress\").value = percentComplete;\
document.getElementById(\"explorerUploadPercent\").innerHTML = percentComplete + \"%\";\
\
if (percentComplete === 100) {\
\
}\
\
Sleep(1000);\
\
}\
}, false);\
\
return xhr;\
},\
success: function(data, textStatus, jqXHR) {\
console.log(\"Upload success!\");\
\
getData(\"/explorer?path=\" + path, function(data) {\
/* We now have data! */\
deleteChildrenNodes(sel);\
addFileDirectory(sel, data);\
ref.open_node(sel);\
\
\
});\
\
}\
});\
});\
\
/* File Delete */\
function handleDeleteData(nodeId) {\
var ref = $('#explorerTree').jstree(true);\
var node = ref.get_node(nodeId);\
var children = $(\"#explorerTree\").jstree(\"get_children_dom\",nodeId);\
console.log(children.length);\
if(node.data.directory) {\
if(children.length > 0) {\
for(var i=0;i<children.length;i++)\
{\
console.log(\"call delete function for: \" + children[i].text);\
handleDeleteData(children[i].id);\
}\
}\
}\
console.log(\"call delete request: \" + node.data.path);\
deleteData(\"/explorer?path=\" + node.data.path);\
}\
function Sleep(milliseconds) {\
return new Promise(resolve => setTimeout(resolve, milliseconds));\
}\
function fileNameSort( a, b ) {\
if ( a.dir && !b.dir ) {\
return -1\
}\
if ( !a.dir && b.dir ) {\
return 1\
}\
if ( a.name < b.name ){\
return -1;\
}\
if ( a.name > b.name ){\
return 1;\
}\
return 0;\
}\
\
function createChild(nodeId, data) {\
var ref = $('#explorerTree').jstree(true);\
var node = ref.get_node(nodeId);\
var child = {\
text: data.name,\
type: getType(data),\
data: {\
path: node.data.path + \"/\" + data.name,\
directory: data.dir\
}\
};\
\
return child;\
\
}\
\
function deleteChildrenNodes(nodeId) {\
var ref = $('#explorerTree').jstree(true);\
var children = $(\"#explorerTree\").jstree(\"get_children_dom\",nodeId);\
for(var i=0;i<children.length;i++)\
{\
ref.delete_node(children[i].id);\
}\
\
}\
\
function refreshNode(nodeId) {\
\
var ref = $('#explorerTree').jstree(true);\
\
var node = ref.get_node(nodeId);\
\
getData(\"/explorer?path=\" + node.data.path, function(data) {\
/* We now have data! */\
\
deleteChildrenNodes(nodeId);\
addFileDirectory(nodeId, data);\
ref.open_node(nodeId);\
\
});\
\
\
}\
\
function getType(data) {\
var type = \"\";\
\
if(data.dir) {\
type = \"folder\";\
} else if ((/\\.(mp3|MP3|ogg|wav|WAV|OGG|wma|WMA|acc|ACC|flac|FLAC)$/i).test(data.name)) {\
type = \"audio\";\
} else {\
type = \"file\";\
}\
\
return type;\
}\
\
\
function addFileDirectory(parent, data) {\
\
data.sort( fileNameSort );\
var ref = $('#explorerTree').jstree(true);\
\
for (var i=0; i<data.length; i++) {\
console.log(\"Create Node\");\
ref.create_node(parent, createChild(parent, data[i]));\
}\
} /* addFileDirectory */\
\
function buildFileSystemTree(path) {\
\
$('#explorerTree').jstree({\
\"core\" : {\
\"check_callback\" : true,\
'force_text' : true,\
\"themes\" : { \"stripes\" : true },\
'data' : { text: '/',\
state: {\
opened: true\
},\
type: 'folder',\
children: [],\
data: {\
path: '/',\
directory: true\
}}\
},\
'types': {\
'folder': {\
'icon': \"fa fa-folder\"\
},\
'file': {\
'icon': \"fa fa-file\"\
},\
'audio': {\
'icon': \"fa fa-file-audio\"\
},\
'default': {\
'icon': \"fa fa-folder\"\
}\
},\
plugins: [\"contextmenu\", \"themes\", \"types\"],\
contextmenu: {\
items: function(nodeId) {\
var ref = $('#explorerTree').jstree(true);\
var node = ref.get_node(nodeId);\
var items = {};\
\
\
if (node.data.directory) {\
items.createDir = {\
label: \"Neuer Ordner\",\
action: function(x) {\
var childNode = ref.create_node(nodeId, {text: \"Neuer Ordner\", type: \"folder\"});\
if(childNode) {\
ref.edit(childNode, null, function(childNode, status){\
putData(\"/explorer?path=\" + node.data.path + \"/\" + childNode.text);\
refreshNode(nodeId);\
});\
}\
}\
};\
}\
\
/* Play */\
items.play = {\
label: \"Abspielen\",\
action: function(x) {\
var playMode = node.data.directory?\"5\":\"1\";\
postData(\"/exploreraudio?path=\" + node.data.path + \"&playmode=\" + playMode);\
}\
};\
\
/* Refresh */\
items.refresh = {\
label: \"Aktualisieren\",\
action: function(x) {\
refreshNode(nodeId);\
}\
};\
\
/* Delete */\
items.delete = {\
label: \"Loeschen\",\
action: function(x) {\
handleDeleteData(nodeId);\
refreshNode(ref.get_parent(nodeId));\
}\
};\
\
/* Rename */\
items.rename = {\
label: \"Umbenennen\",\
action: function(x) {\
var srcPath = node.data.path;\
ref.edit(nodeId, null, function(node, status){\
node.data.path = node.data.path.substring(0,node.data.path.lastIndexOf(\"/\")+1) + node.text;\
patchData(\"/explorer?srcpath=\" + srcPath + \"&dstpath=\" + node.data.path);\
refreshNode(ref.get_parent(nodeId));\
});\
}\
};\
\
return items;\
}\
}\
});\
\
if (path.length == 0) {\
return;\
}\
getData(\"/explorer?path=/\", function(data) {\
/* We now have data! */\
$('#explorerTree').jstree(true).settings.core.data.children = [];\
\
data.sort( fileNameSort );\
\
\
for (var i=0; i<data.length; i++) {\
var newChild = {\
text: data[i].name,\
type: getType(data[i]),\
data: {\
path: \"/\" + data[i].name,\
directory: data[i].dir\
},\
children: []\
};\
$('#explorerTree').jstree(true).settings.core.data.children.push(newChild);\
}\
\
$(\"#explorerTree\").jstree(true).refresh();\
\
\
});\
} /* buildFileSystemTree */\
\
/* File Explorer functions end */\
function renderFileTree() {\
\
var filesURI = \"/files\";\
@ -715,6 +1124,7 @@ static const char management_HTML[] PROGMEM = "<!DOCTYPE html>\
$(document).ready(function () {\
connect();\
renderFileTree();\
buildFileSystemTree(\"/\");\
\
console.log(parseInt(document.getElementById('warningLowVoltage').value));\
$(function () {\

251
src/main.cpp

@ -358,6 +358,13 @@ void freeMultiCharArray(char **arr, const uint32_t cnt);
uint8_t getRepeatMode(void);
bool getWifiEnableStatusFromNVS(void);
void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void convertUtf8ToAscii(String utf8String, char *asciiString);
void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void explorerHandleListRequest(AsyncWebServerRequest *request);
void explorerHandleDeleteRequest(AsyncWebServerRequest *request);
void explorerHandleCreateRequest(AsyncWebServerRequest *request);
void explorerHandleRenameRequest(AsyncWebServerRequest *request);
void explorerHandleAudioRequest(AsyncWebServerRequest *request);
void headphoneVolumeManager(void);
bool isNumber(const char *str);
void loggerNl(const char *str, const uint8_t logLevel);
@ -1446,8 +1453,11 @@ void playAudio(void *parameter) {
// If we're in audiobook-mode and apply a modification-card, we don't
// want to save lastPlayPosition for the mod-card but for the card that holds the playlist
if(currentRfidTagId != NULL){
strncpy(playProperties.playRfidTag, currentRfidTagId, sizeof(playProperties.playRfidTag) / sizeof(playProperties.playRfidTag[0]));
}
}
if (playProperties.trackFinished) {
playProperties.trackFinished = false;
if (playProperties.playMode == NO_PLAYLIST) {
@ -3585,24 +3595,43 @@ void webserverStart(void) {
// attach AsyncEventSource
wServer.addHandler(&events);
// Default
wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send_P(200, "text/html", management_HTML, templateProcessor);
});
// NVS-backup-upload
wServer.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", backupRecoveryWebsite);
}, handleUpload);
// ESP-restart
wServer.on("/restart", HTTP_GET, [] (AsyncWebServerRequest *request) {
request->send_P(200, "text/html", restartWebsite);
Serial.flush();
ESP.restart();
});
// Filebrowser (json-precached)
wServer.on("/files", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(FSystem, DIRECTORY_INDEX_FILE, "application/json");
});
// Fileexplorer (realtime)
wServer.on("/explorer", HTTP_GET, explorerHandleListRequest);
wServer.on("/explorer", HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200);
}, explorerHandleFileUpload);
wServer.on("/explorer", HTTP_DELETE, explorerHandleDeleteRequest);
wServer.on("/explorer", HTTP_PUT, explorerHandleCreateRequest);
wServer.on("/explorer", HTTP_PATCH, explorerHandleRenameRequest);
wServer.on("/exploreraudio", HTTP_POST, explorerHandleAudioRequest);
wServer.onNotFound(notFound);
// allow cors for local debug
@ -3637,7 +3666,8 @@ void webserverStart(void) {
partname,
nvs->size ) ;
} else {
Serial.printf("Partition %s not found!", partname) ;
snprintf(logBuf, serialLoglength, "Partition %s not found!", partname);
loggerNl(logBuf, LOGLEVEL_ERROR);
return NULL;
}
namespace_ID = FindNsID (nvs, _namespace) ; // Find ID of our namespace in NVS
@ -3650,7 +3680,8 @@ void webserverStart(void) {
&buf,
sizeof(nvs_page));
if (result != ESP_OK) {
Serial.println(F("Error reading NVS!"));
snprintf(logBuf, serialLoglength, "Error reading NVS!");
loggerNl(logBuf, LOGLEVEL_ERROR);
return false;
}
@ -3683,6 +3714,222 @@ void webserverStart(void) {
return true;
}
// Conversion routine for http UTF-8 strings
void convertUtf8ToAscii(String utf8String, char *asciiString) {
uint16_t i = 0, j=0;
bool f_C3_seen = false;
while(utf8String[i] != 0) { // convert UTF8 to ASCII
if(utf8String[i] == 195){ // C3
i++;
f_C3_seen = true;
continue;
}
asciiString[j] = utf8String[i];
if(asciiString[j] > 128 && asciiString[j] < 189 && f_C3_seen == true) {
asciiString[j] = asciiString[j] + 64; // found a related ASCII sign
f_C3_seen = false;
}
i++; j++;
}
asciiString[j] = 0;
}
// Handles file upload request from the explorer
// requires a GET parameter path, as directory path to the file
void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
if (request->hasParam("path")) {
AsyncWebParameter *param = request->getParam("path");
String utf8FilePath = param->value() + "/" + filename;
char asciiFilePath[256];
convertUtf8ToAscii(utf8FilePath, asciiFilePath);
request->_tempFile = FSystem.open( asciiFilePath, "w");
} else {
request->_tempFile = FSystem.open("/" + filename, "w");
}
snprintf(logBuf, serialLoglength, "%s: %s", (char *) FPSTR(writingFile), filename);
loggerNl(logBuf, LOGLEVEL_INFO);
// open the file on first call and store the file handle in the request object
}
if (len) {
// stream the incoming chunk to the opened file
request->_tempFile.write(data, len);
}
if (final) {
// close the file handle as the upload is now done
request->_tempFile.close();
}
}
// Sends a list of the content of a directory as JSON file
// requires a GET parameter path for the directory
void explorerHandleListRequest(AsyncWebServerRequest *request) {
DynamicJsonDocument jsonBuffer(8192);
//StaticJsonDocument<4096> jsonBuffer;
String serializedJsonString;
AsyncWebParameter *param;
char asciiFilePath[256];
JsonArray obj = jsonBuffer.createNestedArray();
File root;
if(request->hasParam("path")){
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
root = FSystem.open(asciiFilePath);
} else {
root = FSystem.open("/");
}
if (!root) {
snprintf(logBuf, serialLoglength, (char *) FPSTR(failedToOpenDirectory));
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
if (!root.isDirectory()) {
snprintf(logBuf, serialLoglength, (char *) FPSTR(notADirectory));
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
File file = root.openNextFile();
while(file) {
JsonObject entry = obj.createNestedObject();
std::string path = file.name();
std::string fileName = path.substr(path.find_last_of("/") + 1);
entry["name"] = fileName;
entry["dir"].set(file.isDirectory());
file = root.openNextFile();
}
serializeJson(obj, serializedJsonString);
request->send(200, "application/json; charset=iso-8859-1", serializedJsonString);
}
// Handles delete request of a file or directory
// requires a GET parameter path to the file or directory
void explorerHandleDeleteRequest(AsyncWebServerRequest *request) {
File file;
bool isDir;
AsyncWebParameter *param;
char asciiFilePath[256];
if(request->hasParam("path")){
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
if(FSystem.exists(asciiFilePath)) {
file = FSystem.open(asciiFilePath);
isDir = file.isDirectory();
file.close();
if(isDir) {
if(FSystem.rmdir(asciiFilePath)) {
snprintf(logBuf, serialLoglength, "DELETE: %s deleted", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "DELETE: Cannot delete %s", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
if(FSystem.remove(asciiFilePath)) {
snprintf(logBuf, serialLoglength, "DELETE: %s deleted", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "DELETE: Cannot delete %s", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
}
} else {
snprintf(logBuf, serialLoglength, "DELETE: Path %s does not exist", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
loggerNl("DELETE: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles create request of a directory
// requires a GET parameter path to the new directory
void explorerHandleCreateRequest(AsyncWebServerRequest *request) {
AsyncWebParameter *param;
char asciiFilePath[256];
if(request->hasParam("path")){
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
if(FSystem.mkdir(asciiFilePath)) {
snprintf(logBuf, serialLoglength, "CREATE: %s created", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "CREATE: Cannot create %s", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
loggerNl("CREATE: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles rename request of a file or directory
// requires a GET parameter srcpath to the old file or directory name
// requires a GET parameter dstpath to the new file or directory name
void explorerHandleRenameRequest(AsyncWebServerRequest *request) {
AsyncWebParameter *srcPath;
AsyncWebParameter *dstPath;
char srcFullFilePath[256];
char dstFullFilePath[256];
if(request->hasParam("srcpath") && request->hasParam("dstpath")) {
srcPath = request->getParam("srcpath");
dstPath = request->getParam("dstpath");
convertUtf8ToAscii(srcPath->value(), srcFullFilePath);
convertUtf8ToAscii(dstPath->value(), dstFullFilePath);
if(FSystem.exists(srcFullFilePath)) {
if(FSystem.rename(srcFullFilePath, dstFullFilePath)) {
snprintf(logBuf, serialLoglength, "RENAME: %s renamed to %s", srcFullFilePath, dstFullFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "RENAME: Cannot rename %s", srcFullFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
snprintf(logBuf, serialLoglength, "RENAME: Path %s does not exist", srcFullFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
loggerNl("RENAME: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles audio play requests
// requires a GET parameter path to the audio file or directory
// requires a GET parameter playmode
void explorerHandleAudioRequest(AsyncWebServerRequest *request) {
AsyncWebParameter *param;
String playModeString;
uint32_t playMode;
char asciiFilePath[256];
if(request->hasParam("path") && request->hasParam("playmode")) {
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
param = request->getParam("playmode");
playModeString = param->value();
playMode = atoi(playModeString.c_str());
trackQueueDispatcher(asciiFilePath,0,playMode,0);
} else {
loggerNl("AUDIO: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles uploaded backup-file and writes valid entries into NVS
void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {

Loading…
Cancel
Save