Merge branch 'feat-sdcard-info' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -300,3 +300,15 @@ STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay"
|
|||||||
STR_SYSTEM_INFO: "System Information"
|
STR_SYSTEM_INFO: "System Information"
|
||||||
STR_LOAD_XTC_FAILED: "Failed to load XTC file"
|
STR_LOAD_XTC_FAILED: "Failed to load XTC file"
|
||||||
STR_LOAD_EPUB_FAILED: "Failed to load EPUB file"
|
STR_LOAD_EPUB_FAILED: "Failed to load EPUB file"
|
||||||
|
STR_FW_VERSION: "FW version"
|
||||||
|
STR_CHIP: "Chip"
|
||||||
|
STR_CPU: "CPU"
|
||||||
|
STR_MHZ: "MHz"
|
||||||
|
STR_FREE_RAM: "Free RAM"
|
||||||
|
STR_MIN_FREE: "Min free"
|
||||||
|
STR_MAX_BLOCK: "Max block"
|
||||||
|
STR_FLASH_USED: "Flash used"
|
||||||
|
STR_UPTIME: "Uptime"
|
||||||
|
STR_CHARGING: "Charging"
|
||||||
|
STR_GATHERING_DATA: "Gathering data..."
|
||||||
|
STR_READING: "Reading..."
|
||||||
|
|||||||
+39
-4
@@ -3,30 +3,53 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
|
#include "HalPowerManager.h"
|
||||||
|
|
||||||
// Snapshot of device system status, shared between the web server and the
|
// Snapshot of device system status, shared between the web server and the
|
||||||
// System Information activity so both surfaces show consistent data.
|
// System Information activity so both surfaces show consistent data.
|
||||||
struct SystemStatus {
|
struct SystemStatus {
|
||||||
const char* version;
|
const char* version;
|
||||||
|
std::string chipVersion;
|
||||||
|
uint32_t cpuFreqMHz;
|
||||||
std::string ip;
|
std::string ip;
|
||||||
std::string wifiMode; // "STA", "AP", or "Off"
|
std::string wifiMode; // "STA", "AP", or "Off"
|
||||||
int rssi; // dBm; 0 when not in STA mode
|
int rssi; // dBm; 0 when not in STA mode
|
||||||
std::string macAddress;
|
std::string macAddress;
|
||||||
uint32_t freeHeapBytes;
|
uint32_t freeHeapBytes;
|
||||||
|
uint32_t minFreeHeapBytes;
|
||||||
|
uint32_t maxAllocHeapBytes;
|
||||||
|
uint64_t flashBytes;
|
||||||
|
uint64_t flashAppUsedBytes;
|
||||||
|
uint64_t flashAppFreeBytes;
|
||||||
|
uint16_t batteryPercent;
|
||||||
|
bool charging;
|
||||||
uint32_t uptimeSeconds;
|
uint32_t uptimeSeconds;
|
||||||
uint64_t sdTotalBytes;
|
uint64_t sdTotalBytes;
|
||||||
uint64_t sdUsedBytes;
|
uint64_t sdUsedBytes;
|
||||||
uint64_t sdFreeBytes;
|
uint64_t sdFreeBytes;
|
||||||
|
|
||||||
static SystemStatus collect() {
|
static SystemStatus collectFast() {
|
||||||
SystemStatus s;
|
SystemStatus s;
|
||||||
s.version = CROSSPOINT_VERSION;
|
s.version = CROSSPOINT_VERSION;
|
||||||
|
s.chipVersion = ESP.getChipModel();
|
||||||
|
s.chipVersion += " rev ";
|
||||||
|
s.chipVersion += std::to_string(ESP.getChipRevision());
|
||||||
|
s.cpuFreqMHz = static_cast<uint32_t>(getCpuFrequencyMhz());
|
||||||
s.freeHeapBytes = ESP.getFreeHeap();
|
s.freeHeapBytes = ESP.getFreeHeap();
|
||||||
|
s.minFreeHeapBytes = ESP.getMinFreeHeap();
|
||||||
|
s.maxAllocHeapBytes = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
|
||||||
|
s.flashBytes = static_cast<uint64_t>(ESP.getFlashChipSize());
|
||||||
|
s.flashAppUsedBytes = static_cast<uint64_t>(ESP.getSketchSize());
|
||||||
|
s.flashAppFreeBytes = static_cast<uint64_t>(ESP.getFreeSketchSpace());
|
||||||
|
s.batteryPercent = powerManager.getBatteryPercentage();
|
||||||
|
s.charging = digitalRead(UART0_RXD) == HIGH;
|
||||||
s.uptimeSeconds = millis() / 1000;
|
s.uptimeSeconds = millis() / 1000;
|
||||||
s.macAddress = WiFi.macAddress().c_str();
|
s.macAddress = WiFi.macAddress().c_str();
|
||||||
s.sdTotalBytes = Storage.sdTotalBytes();
|
s.sdTotalBytes = 0;
|
||||||
s.sdUsedBytes = Storage.sdUsedBytes();
|
s.sdUsedBytes = 0;
|
||||||
s.sdFreeBytes = Storage.sdFreeBytes();
|
s.sdFreeBytes = 0;
|
||||||
|
|
||||||
const wifi_mode_t mode = WiFi.getMode();
|
const wifi_mode_t mode = WiFi.getMode();
|
||||||
const bool isAP = (mode == WIFI_MODE_AP) || (mode == WIFI_MODE_APSTA);
|
const bool isAP = (mode == WIFI_MODE_AP) || (mode == WIFI_MODE_APSTA);
|
||||||
@@ -47,4 +70,16 @@ struct SystemStatus {
|
|||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void fillSdStatus(SystemStatus& s) {
|
||||||
|
s.sdTotalBytes = Storage.sdTotalBytes();
|
||||||
|
s.sdUsedBytes = Storage.sdUsedBytes();
|
||||||
|
s.sdFreeBytes = Storage.sdFreeBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
static SystemStatus collect() {
|
||||||
|
SystemStatus s = collectFast();
|
||||||
|
fillSdStatus(s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ static std::string formatBytes(uint64_t bytes) {
|
|||||||
void SystemInformationActivity::onEnter() {
|
void SystemInformationActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
status_.reset();
|
status_.reset();
|
||||||
|
sdStatusReady_ = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +36,17 @@ void SystemInformationActivity::loop() {
|
|||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Collect status (includes the potentially slow SD FAT walk) outside of render()
|
// Collect fast fields first so this page appears immediately.
|
||||||
// so the screen is shown with a "Reading..." placeholder first.
|
|
||||||
if (!status_.has_value()) {
|
if (!status_.has_value()) {
|
||||||
status_ = SystemStatus::collect();
|
status_ = SystemStatus::collectFast();
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SD stats can be slower to compute on large cards.
|
||||||
|
if (!sdStatusReady_) {
|
||||||
|
SystemStatus::fillSdStatus(*status_);
|
||||||
|
sdStatusReady_ = true;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +54,6 @@ void SystemInformationActivity::loop() {
|
|||||||
void SystemInformationActivity::render(RenderLock&&) {
|
void SystemInformationActivity::render(RenderLock&&) {
|
||||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
@@ -67,40 +74,46 @@ void SystemInformationActivity::render(RenderLock&&) {
|
|||||||
|
|
||||||
if (!status_.has_value()) {
|
if (!status_.has_value()) {
|
||||||
// Stats not yet collected — show a placeholder so the screen updates immediately
|
// Stats not yet collected — show a placeholder so the screen updates immediately
|
||||||
drawRow(0, "Version", CROSSPOINT_VERSION);
|
drawRow(0, tr(STR_FW_VERSION), CROSSPOINT_VERSION);
|
||||||
drawRow(3, "SD card", "Reading...");
|
drawRow(2, "", tr(STR_GATHERING_DATA));
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& status = *status_;
|
const auto& status = *status_;
|
||||||
|
|
||||||
// Device
|
drawRow(0, tr(STR_FW_VERSION), status.version);
|
||||||
drawRow(0, "Version", status.version);
|
drawRow(1, tr(STR_CHIP), status.chipVersion);
|
||||||
drawRow(1, "Free heap", std::to_string(status.freeHeapBytes / 1024) + " KB");
|
drawRow(2, tr(STR_CPU), std::to_string(status.cpuFreqMHz) + " " + tr(STR_MHZ));
|
||||||
|
drawRow(3, tr(STR_FREE_RAM), formatBytes(status.freeHeapBytes));
|
||||||
|
drawRow(4, tr(STR_MIN_FREE), formatBytes(status.minFreeHeapBytes));
|
||||||
|
drawRow(5, tr(STR_MAX_BLOCK), formatBytes(status.maxAllocHeapBytes));
|
||||||
|
drawRow(
|
||||||
|
6, tr(STR_FLASH_USED),
|
||||||
|
formatBytes(status.flashAppUsedBytes) + " / " + formatBytes(status.flashAppUsedBytes + status.flashAppFreeBytes));
|
||||||
|
std::string batteryLabel = std::to_string(status.batteryPercent) + "%";
|
||||||
|
if (status.charging) {
|
||||||
|
batteryLabel += " (";
|
||||||
|
batteryLabel += tr(STR_CHARGING);
|
||||||
|
batteryLabel += ")";
|
||||||
|
}
|
||||||
|
drawRow(7, tr(STR_BATTERY), batteryLabel);
|
||||||
|
|
||||||
const uint32_t h = status.uptimeSeconds / 3600;
|
const uint32_t h = status.uptimeSeconds / 3600;
|
||||||
const uint32_t m = (status.uptimeSeconds % 3600) / 60;
|
const uint32_t m = (status.uptimeSeconds % 3600) / 60;
|
||||||
const uint32_t s = status.uptimeSeconds % 60;
|
const uint32_t s = status.uptimeSeconds % 60;
|
||||||
char uptimeBuf[16];
|
char uptimeBuf[16];
|
||||||
snprintf(uptimeBuf, sizeof(uptimeBuf), "%uh %02um %02us", h, m, s);
|
snprintf(uptimeBuf, sizeof(uptimeBuf), "%uh %02um %02us", h, m, s);
|
||||||
drawRow(2, "Uptime", uptimeBuf);
|
drawRow(8, tr(STR_UPTIME), uptimeBuf);
|
||||||
|
|
||||||
// SD card
|
if (!sdStatusReady_) {
|
||||||
const std::string sdUsed = formatBytes(status.sdUsedBytes) + " / " + formatBytes(status.sdTotalBytes);
|
drawRow(9, tr(STR_SD_CARD), tr(STR_READING));
|
||||||
drawRow(3, "SD card", status.sdTotalBytes > 0 ? sdUsed : "N/A");
|
} else if (status.sdTotalBytes > 0) {
|
||||||
|
drawRow(9, tr(STR_SD_CARD), formatBytes(status.sdUsedBytes) + " / " + formatBytes(status.sdTotalBytes));
|
||||||
// WiFi (shown as-is; "Off" when not connected)
|
|
||||||
std::string wifiLabel = status.wifiMode;
|
|
||||||
if (status.rssi != 0) {
|
|
||||||
wifiLabel += " (" + std::to_string(status.rssi) + " dBm)";
|
|
||||||
}
|
|
||||||
drawRow(4, "WiFi", wifiLabel);
|
|
||||||
if (status.wifiMode != "Off") {
|
|
||||||
drawRow(5, "IP address", status.ip);
|
|
||||||
drawRow(6, "MAC address", status.macAddress);
|
|
||||||
} else {
|
} else {
|
||||||
drawRow(5, "MAC address", status.macAddress);
|
drawRow(9, tr(STR_SD_CARD), tr(STR_NOT_SET));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ class SystemInformationActivity final : public Activity {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::optional<SystemStatus> status_;
|
std::optional<SystemStatus> status_;
|
||||||
|
bool sdStatusReady_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -317,17 +317,34 @@ void CrossPointWebServer::handleNotFound() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleStatus() const {
|
void CrossPointWebServer::handleStatus() const {
|
||||||
const auto status = SystemStatus::collect();
|
const bool fastOnly = server->hasArg("phase") && server->arg("phase") == "fast";
|
||||||
|
|
||||||
|
SystemStatus status = SystemStatus::collectFast();
|
||||||
|
if (!fastOnly) {
|
||||||
|
SystemStatus::fillSdStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["version"] = status.version;
|
doc["version"] = status.version;
|
||||||
|
doc["chipVersion"] = status.chipVersion;
|
||||||
|
doc["cpuMHz"] = status.cpuFreqMHz;
|
||||||
doc["ip"] = status.ip;
|
doc["ip"] = status.ip;
|
||||||
doc["mode"] = status.wifiMode;
|
doc["mode"] = status.wifiMode;
|
||||||
doc["rssi"] = status.rssi;
|
doc["rssi"] = status.rssi;
|
||||||
|
doc["macAddress"] = status.macAddress;
|
||||||
doc["freeHeap"] = status.freeHeapBytes;
|
doc["freeHeap"] = status.freeHeapBytes;
|
||||||
|
doc["minFreeHeap"] = status.minFreeHeapBytes;
|
||||||
|
doc["maxAllocHeap"] = status.maxAllocHeapBytes;
|
||||||
|
doc["flashTotal"] = status.flashBytes;
|
||||||
|
doc["flashAppUsed"] = status.flashAppUsedBytes;
|
||||||
|
doc["flashAppFree"] = status.flashAppFreeBytes;
|
||||||
|
doc["batteryPercent"] = status.batteryPercent;
|
||||||
|
doc["charging"] = status.charging;
|
||||||
doc["uptime"] = status.uptimeSeconds;
|
doc["uptime"] = status.uptimeSeconds;
|
||||||
|
doc["sdReady"] = !fastOnly;
|
||||||
doc["sdTotal"] = status.sdTotalBytes;
|
doc["sdTotal"] = status.sdTotalBytes;
|
||||||
doc["sdUsed"] = status.sdUsedBytes;
|
doc["sdUsed"] = status.sdUsedBytes;
|
||||||
|
doc["sdFree"] = status.sdFreeBytes;
|
||||||
|
|
||||||
String json;
|
String json;
|
||||||
serializeJson(doc, json);
|
serializeJson(doc, json);
|
||||||
|
|||||||
@@ -128,18 +128,50 @@
|
|||||||
<span class="label">Version</span>
|
<span class="label">Version</span>
|
||||||
<span class="value" id="version">Loading...</span>
|
<span class="value" id="version">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Chip</span>
|
||||||
|
<span class="value" id="chip-version">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">CPU</span>
|
||||||
|
<span class="value" id="cpu-mhz">Loading...</span>
|
||||||
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">WiFi Status</span>
|
<span class="label">WiFi Status</span>
|
||||||
<span class="status">Connected</span>
|
<span class="status" id="wifi-status">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">IP Address</span>
|
<span class="label">IP Address</span>
|
||||||
<span class="value" id="ip-address">Loading...</span>
|
<span class="value" id="ip-address">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">MAC Address</span>
|
||||||
|
<span class="value" id="mac-address">Loading...</span>
|
||||||
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">Free Memory</span>
|
<span class="label">Free Memory</span>
|
||||||
<span class="value" id="free-heap">Loading...</span>
|
<span class="value" id="free-heap">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Min Free</span>
|
||||||
|
<span class="value" id="min-free-heap">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Max Block</span>
|
||||||
|
<span class="value" id="max-alloc-heap">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Flash Used</span>
|
||||||
|
<span class="value" id="flash-used">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Battery</span>
|
||||||
|
<span class="value" id="battery">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Uptime</span>
|
||||||
|
<span class="value" id="uptime">Loading...</span>
|
||||||
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">SD Card</span>
|
<span class="label">SD Card</span>
|
||||||
<span class="value" id="sd-space">Loading...</span>
|
<span class="value" id="sd-space">Loading...</span>
|
||||||
@@ -160,21 +192,64 @@
|
|||||||
return parseFloat(val.toFixed(2)).toLocaleString() + ' ' + units[i];
|
return parseFloat(val.toFixed(2)).toLocaleString() + ' ' + units[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchStatus() {
|
function formatUptime(seconds) {
|
||||||
try {
|
if (seconds == null) return 'N/A';
|
||||||
const response = await fetch('/api/status');
|
const h = Math.floor(seconds / 3600);
|
||||||
if (!response.ok) {
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
throw new Error('Failed to fetch status: ' + response.status + ' ' + response.statusText);
|
const s = seconds % 60;
|
||||||
}
|
return h + 'h ' + String(m).padStart(2, '0') + 'm ' + String(s).padStart(2, '0') + 's';
|
||||||
const data = await response.json();
|
}
|
||||||
document.getElementById('version').textContent = data.version || 'N/A';
|
|
||||||
document.getElementById('ip-address').textContent = data.ip || 'N/A';
|
function applyStatus(data, includeSd) {
|
||||||
document.getElementById('free-heap').textContent = data.freeHeap
|
document.getElementById('version').textContent = data.version || 'N/A';
|
||||||
? data.freeHeap.toLocaleString() + ' bytes'
|
document.getElementById('chip-version').textContent = data.chipVersion || 'N/A';
|
||||||
: 'N/A';
|
document.getElementById('cpu-mhz').textContent = data.cpuMHz
|
||||||
|
? data.cpuMHz.toLocaleString() + ' MHz'
|
||||||
|
: 'N/A';
|
||||||
|
const wifiStatus = document.getElementById('wifi-status');
|
||||||
|
wifiStatus.textContent = data.mode || 'N/A';
|
||||||
|
document.getElementById('ip-address').textContent = data.ip || 'N/A';
|
||||||
|
document.getElementById('mac-address').textContent = data.macAddress || 'N/A';
|
||||||
|
document.getElementById('free-heap').textContent = data.freeHeap
|
||||||
|
? formatBytes(data.freeHeap)
|
||||||
|
: 'N/A';
|
||||||
|
document.getElementById('min-free-heap').textContent = data.minFreeHeap
|
||||||
|
? formatBytes(data.minFreeHeap)
|
||||||
|
: 'N/A';
|
||||||
|
document.getElementById('max-alloc-heap').textContent = data.maxAllocHeap
|
||||||
|
? formatBytes(data.maxAllocHeap)
|
||||||
|
: 'N/A';
|
||||||
|
document.getElementById('flash-used').textContent = data.flashAppUsed
|
||||||
|
? (formatBytes(data.flashAppUsed) + ' / ' + formatBytes((data.flashAppUsed || 0) + (data.flashAppFree || 0)))
|
||||||
|
: 'N/A';
|
||||||
|
document.getElementById('battery').textContent = (data.batteryPercent != null)
|
||||||
|
? (data.batteryPercent + '%' + (data.charging ? ' (Charging)' : ''))
|
||||||
|
: 'N/A';
|
||||||
|
document.getElementById('uptime').textContent = formatUptime(data.uptime);
|
||||||
|
|
||||||
|
if (includeSd) {
|
||||||
document.getElementById('sd-space').textContent = data.sdTotal
|
document.getElementById('sd-space').textContent = data.sdTotal
|
||||||
? formatBytes(data.sdUsed) + ' / ' + formatBytes(data.sdTotal)
|
? formatBytes(data.sdUsed) + ' / ' + formatBytes(data.sdTotal)
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStatus() {
|
||||||
|
try {
|
||||||
|
const fastResponse = await fetch('/api/status?phase=fast');
|
||||||
|
if (!fastResponse.ok) {
|
||||||
|
throw new Error('Failed to fetch fast status: ' + fastResponse.status + ' ' + fastResponse.statusText);
|
||||||
|
}
|
||||||
|
const fastData = await fastResponse.json();
|
||||||
|
applyStatus(fastData, false);
|
||||||
|
document.getElementById('sd-space').textContent = 'Reading...';
|
||||||
|
|
||||||
|
const response = await fetch('/api/status');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch full status: ' + response.status + ' ' + response.statusText);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
applyStatus(data, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching status:', error);
|
console.error('Error fetching status:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user