Added menu mode selection menu
This commit is contained in:
parent
f93e5d7628
commit
3a70970356
|
@ -15,12 +15,12 @@ board = esp32dev
|
|||
framework = arduino
|
||||
board_build.partitions = ABpartition.csv
|
||||
upload_speed = 921600
|
||||
upload_port = 192.168.20.120
|
||||
upload_protocol = espota
|
||||
upload_flags =
|
||||
--port=3232
|
||||
;upload_port = COM16
|
||||
;upload_protocol = esptool
|
||||
;upload_protocol = espota
|
||||
;upload_port = 192.168.20.120
|
||||
;upload_flags =
|
||||
; --port=3232
|
||||
upload_port = COM16
|
||||
upload_protocol = esptool
|
||||
;monitor_speed = 115200
|
||||
extra_scripts = post:add_CRC.py
|
||||
; replace shitty Arduino millis with a linear time version
|
||||
|
|
|
@ -394,14 +394,10 @@ void setup() {
|
|||
// Initialize the rtc object
|
||||
Clock.begin();
|
||||
|
||||
bool bNoClock = true;
|
||||
const BTCDateTime& now = Clock.get();
|
||||
if(now.day() != 0xa5)
|
||||
bNoClock = false;
|
||||
BootTime = Clock.get().secondstime();
|
||||
|
||||
ScreenManager.begin(bNoClock);
|
||||
if(!bNoClock && Clock.lostPower()) {
|
||||
ScreenManager.begin();
|
||||
if(Clock.lostPower()) {
|
||||
ScreenManager.selectMenu(CScreenManager::BranchMenu, CScreenManager::SetClockUI);
|
||||
}
|
||||
|
||||
|
@ -470,7 +466,6 @@ void setup() {
|
|||
FilteredSamples.Fan.setRounding(10);
|
||||
FilteredSamples.Fan.setAlpha(0.7);
|
||||
FilteredSamples.AmbientTemp.reset(-100.0);
|
||||
FilteredSamples.AmbientTemp.setAlpha(0); // no average - for test
|
||||
FilteredSamples.FastipVolts.setRounding(0.1);
|
||||
FilteredSamples.FastipVolts.setAlpha(0.7);
|
||||
FilteredSamples.FastGlowAmps.setRounding(0.01);
|
||||
|
@ -1826,3 +1821,7 @@ void showMainmenu()
|
|||
DebugPort.println("");
|
||||
}
|
||||
|
||||
void reloadScreens()
|
||||
{
|
||||
ScreenManager.reqReload();
|
||||
}
|
|
@ -78,7 +78,6 @@ CDetailedScreen::CDetailedScreen(C128x64_OLED& display, CScreenManager& mgr) : C
|
|||
_showTarget = 0;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
CDetailedScreen::show()
|
||||
{
|
||||
|
@ -197,6 +196,10 @@ CDetailedScreen::keyHandler(uint8_t event)
|
|||
if(event & keyRepeat) {
|
||||
if(_keyRepeatCount >= 0) {
|
||||
_keyRepeatCount++;
|
||||
if((event & (key_Left | key_Right)) == (key_Left | key_Right)) {
|
||||
_ScreenManager.selectMenu(CScreenManager::BranchMenu, CScreenManager::HtrSettingsUI);
|
||||
return true;
|
||||
}
|
||||
// hold LEFT to toggle GPIO output #1
|
||||
if(event & key_Left) {
|
||||
if(_keyRepeatCount > 2) {
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* This file is part of the "bluetoothheater" distribution
|
||||
* (https://gitlab.com/mrjones.id.au/bluetoothheater)
|
||||
*
|
||||
* Copyright (C) 2019 Ray Jones <ray@mrjones.id.au>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "128x64OLED.h"
|
||||
#include "MenuSelScreen.h"
|
||||
#include "KeyPad.h"
|
||||
#include "fonts/Icons.h"
|
||||
|
||||
|
||||
|
||||
CMenuSelScreen::CMenuSelScreen(C128x64_OLED& display, CScreenManager& mgr) : CPasswordScreen(display, mgr)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
CMenuSelScreen::onSelect()
|
||||
{
|
||||
CScreenHeader::onSelect();
|
||||
_rowSel = 0;
|
||||
_menuMode = NVstore.getUserSettings().menuMode;
|
||||
_scrollChar = 0;
|
||||
_bReload = false;
|
||||
}
|
||||
|
||||
void
|
||||
CMenuSelScreen::_initUI()
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
CMenuSelScreen::show()
|
||||
{
|
||||
char msg[16];
|
||||
|
||||
_display.clearDisplay();
|
||||
|
||||
if(!CPasswordScreen::show()) { // for showing "saving settings"
|
||||
|
||||
if(_bReload) {
|
||||
_ScreenManager.reqReload();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(_rowSel == SaveConfirm) {
|
||||
_showConfirmMessage();
|
||||
}
|
||||
else {
|
||||
_showTitle("Menu Mode");
|
||||
|
||||
_drawBitmap(30, 16, MenuIconInfo);
|
||||
switch(_menuMode) {
|
||||
case 0: strcpy(msg, "Standard"); break;
|
||||
case 1: strcpy(msg, "Basic"); break;
|
||||
case 2: strcpy(msg, "No heater"); break;
|
||||
}
|
||||
_printMenuText(50, 16, msg, _rowSel == 1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
CMenuSelScreen::animate()
|
||||
{
|
||||
if(!CPasswordScreen::_busy() && !CPasswordScreen::isPasswordBusy()) {
|
||||
if(_bReload) {
|
||||
_ScreenManager.reqReload();
|
||||
return false;
|
||||
}
|
||||
if(_rowSel != SaveConfirm) {
|
||||
const char* pMsg = NULL;
|
||||
switch(_menuMode) {
|
||||
case 0:
|
||||
pMsg = " Standard, complete menu allows full access to features. ";
|
||||
break;
|
||||
case 1:
|
||||
pMsg = " Basic simplified menu. ";
|
||||
break;
|
||||
case 2:
|
||||
pMsg = " No heater mode. ";
|
||||
break;
|
||||
}
|
||||
_display.drawFastHLine(0, 52, 128, WHITE);
|
||||
if(pMsg != NULL)
|
||||
_scrollMessage(56, pMsg, _scrollChar);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
CMenuSelScreen::keyHandler(uint8_t event)
|
||||
{
|
||||
if(CPasswordScreen::keyHandler(event)) {
|
||||
if(_isPasswordOK()) {
|
||||
_rowSel = 1;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
_scrollChar = 0;
|
||||
sUserSettings us;
|
||||
if(event & keyPressed) {
|
||||
// UP press
|
||||
if(event & key_Up) {
|
||||
if(_rowSel == SaveConfirm) {
|
||||
_enableStoringMessage();
|
||||
us = NVstore.getUserSettings();
|
||||
us.menuMode = _menuMode;
|
||||
NVstore.setUserSettings(us);
|
||||
NVstore.save();
|
||||
switch(us.menuMode) {
|
||||
case 0: DebugPort.println("Invoking Full menu control mode"); break;
|
||||
case 1: DebugPort.println("Invoking Basic menu mode"); break;
|
||||
case 2: DebugPort.println("Invoking No Heater menu mode"); break;
|
||||
}
|
||||
_bReload = true;
|
||||
_rowSel = 0;
|
||||
}
|
||||
else {
|
||||
_getPassword();
|
||||
}
|
||||
}
|
||||
// DOWN press
|
||||
if(event & key_Down) {
|
||||
_rowSel--;
|
||||
LOWERLIMIT(_rowSel, 0);
|
||||
}
|
||||
// CENTRE press
|
||||
if(event & key_Centre) {
|
||||
if(_rowSel == 0) {
|
||||
_ScreenManager.selectMenu(CScreenManager::RootMenuLoop); // force return to main menu
|
||||
}
|
||||
else {
|
||||
_rowSel = SaveConfirm;
|
||||
}
|
||||
}
|
||||
// LEFT press
|
||||
if(event & key_Left) {
|
||||
if(_rowSel == 0)
|
||||
_ScreenManager.prevMenu();
|
||||
else
|
||||
adjust(-1);
|
||||
}
|
||||
// RIGHT press
|
||||
if(event & key_Right) {
|
||||
if(_rowSel == 0)
|
||||
_ScreenManager.nextMenu();
|
||||
else
|
||||
adjust(+1);
|
||||
}
|
||||
}
|
||||
|
||||
_ScreenManager.reqUpdate();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
CMenuSelScreen::adjust(int dir)
|
||||
{
|
||||
switch(_rowSel) {
|
||||
case 1:
|
||||
_menuMode += dir;
|
||||
WRAPLIMITS(_menuMode, 0, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* This file is part of the "bluetoothheater" distribution
|
||||
* (https://gitlab.com/mrjones.id.au/bluetoothheater)
|
||||
*
|
||||
* Copyright (C) 2019 Ray Jones <ray@mrjones.id.au>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef __MENUSELSCREEN_H__
|
||||
#define __MENUSELSCREEN_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include "PasswordScreen.h"
|
||||
#include "../Utility/NVStorage.h"
|
||||
|
||||
class C128x64_OLED;
|
||||
class CScreenManager;
|
||||
|
||||
class CMenuSelScreen : public CPasswordScreen
|
||||
{
|
||||
int _rowSel;
|
||||
int _scrollChar;
|
||||
int _menuMode;
|
||||
bool _bReload;
|
||||
void _initUI();
|
||||
public:
|
||||
CMenuSelScreen(C128x64_OLED& display, CScreenManager& mgr);
|
||||
bool show();
|
||||
bool animate();
|
||||
bool keyHandler(uint8_t event);
|
||||
void onSelect();
|
||||
void adjust(int dir);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -42,6 +42,7 @@ enum eJUSTIFY {
|
|||
|
||||
const int border = 3;
|
||||
const int radius = 4;
|
||||
const int SaveConfirm = 10;
|
||||
|
||||
class CScreen {
|
||||
protected:
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
#include "GPIOSetupScreen.h"
|
||||
#include "VersionInfoScreen.h"
|
||||
#include "HomeMenuSelScreen.h"
|
||||
#include "MenuSelScreen.h"
|
||||
#include "TimeoutsScreen.h"
|
||||
#include "HourMeterScreen.h"
|
||||
#include "BTScreen.h"
|
||||
|
@ -351,6 +352,7 @@ CScreenManager::CScreenManager()
|
|||
_MenuTimeout = millis() + 60000;
|
||||
_pRebootScreen = NULL;
|
||||
_bDimmed = false;
|
||||
_bReload = true;
|
||||
}
|
||||
|
||||
CScreenManager::~CScreenManager()
|
||||
|
@ -369,7 +371,7 @@ CScreenManager::~CScreenManager()
|
|||
}
|
||||
|
||||
void
|
||||
CScreenManager::begin(bool bNoClock)
|
||||
CScreenManager::begin()
|
||||
{
|
||||
|
||||
// 128 x 64 OLED support (I2C)
|
||||
|
@ -389,6 +391,24 @@ CScreenManager::begin(bool bNoClock)
|
|||
|
||||
delay(2000);
|
||||
|
||||
_loadScreens();
|
||||
}
|
||||
|
||||
void CScreenManager::_unloadScreens()
|
||||
{
|
||||
for (auto menuloop : _Screens) {
|
||||
for(auto menu : menuloop) {
|
||||
delete menu;
|
||||
}
|
||||
}
|
||||
_Screens.clear();
|
||||
}
|
||||
|
||||
void
|
||||
CScreenManager::_loadScreens()
|
||||
{
|
||||
_unloadScreens();
|
||||
|
||||
DebugPort.println("Creating Screens");
|
||||
|
||||
std::vector<CScreen*> menuloop;
|
||||
|
@ -396,8 +416,7 @@ CScreenManager::begin(bool bNoClock)
|
|||
if(NVstore.getUserSettings().menuMode == 0) {
|
||||
menuloop.push_back(new CDetailedScreen(*_pDisplay, *this)); // detail control
|
||||
menuloop.push_back(new CBasicScreen(*_pDisplay, *this)); // basic control
|
||||
if(!bNoClock)
|
||||
menuloop.push_back(new CClockScreen(*_pDisplay, *this)); // clock
|
||||
menuloop.push_back(new CClockScreen(*_pDisplay, *this)); // clock
|
||||
menuloop.push_back(new CPrimingScreen(*_pDisplay, *this)); // mode / priming
|
||||
if(getBoardRevision() != 0 && getBoardRevision() != BRD_V2_NOGPIO) // has GPIO support
|
||||
menuloop.push_back(new CGPIOInfoScreen(*_pDisplay, *this)); // GPIO info
|
||||
|
@ -406,14 +425,12 @@ CScreenManager::begin(bool bNoClock)
|
|||
else if(NVstore.getUserSettings().menuMode == 1) {
|
||||
menuloop.push_back(new CMenuTrunkScreen(*_pDisplay, *this));
|
||||
menuloop.push_back(new CBasicScreen(*_pDisplay, *this)); // basic control
|
||||
if(!bNoClock)
|
||||
menuloop.push_back(new CClockScreen(*_pDisplay, *this)); // clock
|
||||
menuloop.push_back(new CClockScreen(*_pDisplay, *this)); // clock
|
||||
}
|
||||
else if(NVstore.getUserSettings().menuMode == 2) {
|
||||
menuloop.push_back(new CMenuTrunkScreen(*_pDisplay, *this));
|
||||
menuloop.push_back(new CBasicScreen(*_pDisplay, *this)); // basic control
|
||||
if(!bNoClock)
|
||||
menuloop.push_back(new CClockScreen(*_pDisplay, *this)); // clock
|
||||
menuloop.push_back(new CClockScreen(*_pDisplay, *this)); // clock
|
||||
if(getBoardRevision() != 0 && getBoardRevision() != BRD_V2_NOGPIO) // has GPIO support
|
||||
menuloop.push_back(new CGPIOInfoScreen(*_pDisplay, *this)); // GPIO info
|
||||
}
|
||||
|
@ -444,16 +461,16 @@ CScreenManager::begin(bool bNoClock)
|
|||
menuloop.push_back(new CThermostatModeScreen(*_pDisplay, *this)); // thermostat settings screen
|
||||
menuloop.push_back(new CHomeMenuSelScreen(*_pDisplay, *this)); // Home menu settings screen
|
||||
menuloop.push_back(new CTimeoutsScreen(*_pDisplay, *this)); // Other options screen
|
||||
menuloop.push_back(new CMenuSelScreen(*_pDisplay, *this)); // Menu mode screen
|
||||
if(getBoardRevision() != 0 && getBoardRevision() != BRD_V2_NOGPIO) // has GPIO support ?
|
||||
menuloop.push_back(new CGPIOSetupScreen(*_pDisplay, *this)); // GPIO settings screen
|
||||
}
|
||||
else if(NVstore.getUserSettings().menuMode == 1) {
|
||||
menuloop.push_back(new CThermostatModeScreen(*_pDisplay, *this)); // thermostat settings screen
|
||||
menuloop.push_back(new CHomeMenuSelScreen(*_pDisplay, *this)); // Home menu settings screen
|
||||
menuloop.push_back(new CTimeoutsScreen(*_pDisplay, *this)); // Other options screen
|
||||
menuloop.push_back(new CMenuSelScreen(*_pDisplay, *this)); // Menu mode screen
|
||||
}
|
||||
else if(NVstore.getUserSettings().menuMode == 2) {
|
||||
menuloop.push_back(new CNoHeaterHomeMenuSelScreen(*_pDisplay, *this)); // No Heater Home menu settings screen
|
||||
menuloop.push_back(new CMenuSelScreen(*_pDisplay, *this)); // Menu mode screen
|
||||
if(getBoardRevision() != 0 && getBoardRevision() != BRD_V2_NOGPIO) // has GPIO support ?
|
||||
menuloop.push_back(new CGPIOSetupScreen(*_pDisplay, *this)); // GPIO settings screen
|
||||
}
|
||||
|
@ -495,13 +512,17 @@ CScreenManager::begin(bool bNoClock)
|
|||
_rootMenu = 1; // basic control screen
|
||||
_subMenu = 1;
|
||||
#endif
|
||||
|
||||
_bReload = false;
|
||||
reqUpdate();
|
||||
_enterScreen();
|
||||
}
|
||||
|
||||
bool
|
||||
CScreenManager::checkUpdate()
|
||||
{
|
||||
if(_bReload)
|
||||
_loadScreens();
|
||||
|
||||
long dimTimeout = NVstore.getUserSettings().dimTime;
|
||||
|
||||
// manage dimming or blanking the display, according to user defined inactivity interval
|
||||
|
|
|
@ -37,6 +37,7 @@ class CScreenManager {
|
|||
int _subMenu;
|
||||
int _rootMenu;
|
||||
bool _bDimmed;
|
||||
bool _bReload;
|
||||
unsigned long _DimTime_ms;
|
||||
unsigned long _MenuTimeout;
|
||||
bool _bReqUpdate;
|
||||
|
@ -44,6 +45,8 @@ class CScreenManager {
|
|||
void _leaveScreen();
|
||||
void _changeSubMenu(int dir);
|
||||
void _dim(bool state);
|
||||
void _loadScreens();
|
||||
void _unloadScreens();
|
||||
public:
|
||||
enum eUIMenuSets { RootMenuLoop, TimerMenuLoop, UserSettingsLoop, SystemSettingsLoop, TuningMenuLoop, BranchMenu };
|
||||
enum eUIRootMenus { DetailedControlUI, BasicControlUI, ClockUI, ModeUI, GPIOInfoUI, TrunkUI };
|
||||
|
@ -56,7 +59,7 @@ public:
|
|||
public:
|
||||
CScreenManager();
|
||||
~CScreenManager();
|
||||
void begin(bool bNoClock);
|
||||
void begin();
|
||||
bool checkUpdate();
|
||||
bool animate();
|
||||
void keyHandler(uint8_t event);
|
||||
|
@ -70,6 +73,7 @@ public:
|
|||
void clearDisplay();
|
||||
void bumpTimeout();
|
||||
void showSplash();
|
||||
void reqReload() { _bReload = true; };
|
||||
};
|
||||
|
||||
#endif // __SCREEN_MANAGER_H__
|
||||
|
|
|
@ -791,6 +791,21 @@ const uint8_t PROGMEM menuTimeoutIcon[] =
|
|||
};
|
||||
const BITMAP_INFO MenuTimeoutIconInfo(24, 10, menuTimeoutIcon);
|
||||
|
||||
const uint8_t PROGMEM menuIcon[] =
|
||||
{
|
||||
0x00, 0x00, //
|
||||
0xFF, 0xC0, // ##########
|
||||
0x00, 0x00, //
|
||||
0xFF, 0x00, // ########
|
||||
0x00, 0x00, //
|
||||
0xFC, 0x00, // ######
|
||||
0x00, 0x00, //
|
||||
0xFF, 0x80, // #########
|
||||
0x00, 0x00, //
|
||||
0x00, 0x00, //
|
||||
};
|
||||
const BITMAP_INFO MenuIconInfo(10, 10, menuIcon);
|
||||
|
||||
const uint8_t PROGMEM timeoutIcon[] =
|
||||
{
|
||||
0xFF, 0x80, // #########
|
||||
|
|
|
@ -108,6 +108,7 @@ extern const BITMAP_INFO DisplayTimeoutIconInfo;
|
|||
|
||||
// Bitmap for menuTimeout
|
||||
extern const BITMAP_INFO MenuTimeoutIconInfo;
|
||||
extern const BITMAP_INFO MenuIconInfo;
|
||||
|
||||
// Bitmap for timeout
|
||||
extern const BITMAP_INFO TimeoutIconInfo;
|
||||
|
|
|
@ -332,17 +332,17 @@ void interpretJsonCommand(char* pLine)
|
|||
if(us.menuMode <=2) {
|
||||
NVstore.setUserSettings(us);
|
||||
NVstore.save();
|
||||
NVstore.doSave();
|
||||
switch(us.menuMode) {
|
||||
case 0: DebugPort.println("Restarting ESP to invoke Full menu control mode"); break;
|
||||
case 1: DebugPort.println("Restarting ESP to invoke Basic menu mode"); break;
|
||||
case 2: DebugPort.println("Restarting ESP to invoke cut back No Heater mode"); break;
|
||||
case 0: DebugPort.println("Invoking Full menu control mode"); break;
|
||||
case 1: DebugPort.println("Invoking Basic menu mode"); break;
|
||||
case 2: DebugPort.println("Invoking cut back No Heater mode"); break;
|
||||
}
|
||||
DebugPort.handle();
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
reloadScreens();
|
||||
}
|
||||
}
|
||||
else if(strcmp("SysHourMeters", it->key) == 0) {
|
||||
pHourMeter->resetHard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,6 +88,8 @@ extern float getGlowCurrent();
|
|||
extern float getFanSpeed();
|
||||
extern int sysUptime();
|
||||
extern void doJSONwatchdog(int topup);
|
||||
extern void reloadScreens();
|
||||
|
||||
|
||||
void setSSID(const char* name);
|
||||
void setAPpassword(const char* name);
|
||||
|
|
|
@ -129,7 +129,7 @@ bool initWifi(int initpin,const char *failedssid, const char *failedpassword)
|
|||
DebugPort.printf(" AP SSID: %s\r\n", WiFi.softAPgetHostname());
|
||||
DebugPort.printf(" AP IP address: %s\r\n", getWifiAPAddrStr());
|
||||
DebugPort.printf("WifiMode after initWifi = %d\r\n", WiFi.getMode());
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// even though we may have started in STA mode - start the config portal if demanded via the NV flag
|
||||
if(shouldBootIntoConfigPortal()) {
|
||||
|
@ -138,6 +138,8 @@ bool initWifi(int initpin,const char *failedssid, const char *failedpassword)
|
|||
isPortalAP = true; // we started portal, we have to flag it!
|
||||
}
|
||||
|
||||
// WiFi.setTxPower(WIFI_POWER_MINUS_1dBm);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue