Bootcore feature
[MiSTer] ; lastcore - Autoboot the last loaded core (corename autosaved in CONFIG/lastcore.dat) first found on the SD/USB ; lastexactcore - Autoboot the last loaded exact core (corename_yyyymmdd.rbf autosaved in CONFIG/lastcore.dat) first found on the SD/USB ; corename - Autoboot first corename_*.rbf found on the SD/USB ; corename_yyyymmdd.rbf - Autoboot first corename_yyyymmdd.rbf found on the SD/USB ;bootcore=lastcore ; uncomment to autoboot a core, as the last loaded core. bootcore_timeout=10 ; 10-30 timeout before autoboot, comment for autoboot without timeout.
This commit is contained in:
@@ -17,6 +17,13 @@ vscale_border=0 ; set vertical border for TVs cutting the upper/bottom pa
|
||||
rbf_hide_datecode=0 ; 1 - hides datecodes from rbf file names. Press F2 for quick temporary toggle
|
||||
menu_pal=0 ; 1 - PAL mode for menu core
|
||||
|
||||
; lastcore - Autoboot the last loaded core (corename autosaved in CONFIG/lastcore.dat) first found on the SD/USB
|
||||
; lastexactcore - Autoboot the last loaded exact core (corename_yyyymmdd.rbf autosaved in CONFIG/lastcore.dat) first found on the SD/USB
|
||||
; corename - Autoboot first corename_*.rbf found on the SD/USB
|
||||
; corename_yyyymmdd.rbf - Autoboot first corename_yyyymmdd.rbf found on the SD/USB
|
||||
;bootcore=lastcore ; uncomment to autoboot a core, as the last loaded core.
|
||||
bootcore_timeout=10 ; 10-30 timeout before autoboot, comment for autoboot without timeout.
|
||||
|
||||
; USER button emulation by keybaord. Usually it's reset button.
|
||||
; 0 - lctrl+lalt+ralt (lctrl+lgui+rgui on keyrah)
|
||||
; 1 - lctrl+lgui+rgui
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="battery.cpp" />
|
||||
<ClCompile Include="bootcore.cpp" />
|
||||
<ClCompile Include="brightness.cpp" />
|
||||
<ClCompile Include="cfg.cpp" />
|
||||
<ClCompile Include="DiskImage.cpp" />
|
||||
@@ -84,6 +85,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="battery.h" />
|
||||
<ClInclude Include="bootcore.h" />
|
||||
<ClInclude Include="brightness.h" />
|
||||
<ClInclude Include="cfg.h" />
|
||||
<ClInclude Include="charrom.h" />
|
||||
|
||||
@@ -127,6 +127,9 @@
|
||||
<ClCompile Include="lib\miniz\miniz_zip.c">
|
||||
<Filter>Source Files\miniz</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="bootcore.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="battery.h">
|
||||
@@ -258,5 +261,8 @@
|
||||
<ClInclude Include="lib\miniz\miniz_zip.h">
|
||||
<Filter>Header Files\miniz</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="bootcore.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
240
bootcore.cpp
Normal file
240
bootcore.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
// bootcore.cpp
|
||||
// 2019, Aitor Gomez Garcia (spark2k06@gmail.com)
|
||||
// Thanks to Sorgelig and BBond007 for their help and advice in the development of this feature.
|
||||
|
||||
#include "file_io.h"
|
||||
#include "cfg.h"
|
||||
#include "fpga_io.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <dirent.h>
|
||||
|
||||
int16_t btimeout;
|
||||
char bootcoretype[64];
|
||||
|
||||
bool isExactcoreName(char *path)
|
||||
{
|
||||
char *spl = strrchr(path, '.');
|
||||
return (spl && !strcmp(spl, ".rbf"));
|
||||
}
|
||||
|
||||
char *getcoreName(char *path)
|
||||
{
|
||||
char *spl = strrchr(path, '.');
|
||||
if (spl && !strcmp(spl, ".rbf"))
|
||||
{
|
||||
*spl = '\0';
|
||||
}
|
||||
else
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
if ((spl = strrchr(path, '/')) != NULL)
|
||||
{
|
||||
path = spl + 1;
|
||||
}
|
||||
if ((spl = strrchr(path, '_')) != NULL)
|
||||
{
|
||||
*spl = 0;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
char *getcoreExactName(char *path)
|
||||
{
|
||||
char *spl;
|
||||
if ((spl = strrchr(path, '/')) != NULL)
|
||||
{
|
||||
path = spl + 1;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
char *replaceStr(const char *str, const char *oldstr, const char *newstr)
|
||||
{
|
||||
char *result;
|
||||
int i, cnt = 0;
|
||||
int newstrlen = strlen(newstr);
|
||||
int oldstrlen = strlen(oldstr);
|
||||
|
||||
for (i = 0; str[i] != '\0'; i++)
|
||||
{
|
||||
if (strstr(&str[i], oldstr) == &str[i])
|
||||
{
|
||||
cnt++;
|
||||
i += oldstrlen - 1;
|
||||
}
|
||||
}
|
||||
|
||||
result = new char[i + cnt * (newstrlen - oldstrlen) + 1];
|
||||
|
||||
i = 0;
|
||||
while (*str)
|
||||
{
|
||||
if (strstr(str, oldstr) == str)
|
||||
{
|
||||
strcpy(&result[i], newstr);
|
||||
i += newstrlen;
|
||||
str += oldstrlen;
|
||||
}
|
||||
else
|
||||
result[i++] = *str++;
|
||||
}
|
||||
|
||||
result[i] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
char* loadLastcore()
|
||||
{
|
||||
char full_path[2100];
|
||||
char path[256] = { CONFIG_DIR"/" };
|
||||
strcat(path, "lastcore.dat");
|
||||
sprintf(full_path, "%s/%s", getRootDir(), path);
|
||||
FILE *fd = fopen(full_path, "r");
|
||||
if (!fd)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
fseek(fd, 0L, SEEK_END);
|
||||
long size = ftell(fd);
|
||||
|
||||
fseek(fd, 0L, SEEK_SET);
|
||||
char *lastcore = new char[size + 1];
|
||||
|
||||
int ret = fread(lastcore, sizeof(char), size, fd);
|
||||
fclose(fd);
|
||||
if (ret == size)
|
||||
{
|
||||
return lastcore;
|
||||
}
|
||||
delete[] lastcore;
|
||||
return NULL;
|
||||
|
||||
}
|
||||
|
||||
char *findCore(const char *name, char *coreName, int indent)
|
||||
{
|
||||
char *spl;
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
|
||||
if (!(dir = opendir(name)))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
char *indir;
|
||||
char* path = new char[256];
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_type == DT_DIR) {
|
||||
if (entry->d_name[0] != '_')
|
||||
continue;
|
||||
snprintf(path, 256, "%s/%s", name, entry->d_name);
|
||||
indir = findCore(path, coreName, indent + 2);
|
||||
if (indir != NULL)
|
||||
{
|
||||
closedir(dir);
|
||||
delete[] path;
|
||||
return indir;
|
||||
}
|
||||
}
|
||||
else {
|
||||
snprintf(path, 256, "%s/%s", name, entry->d_name);
|
||||
if (strstr(path, coreName) != NULL) {
|
||||
spl = strrchr(path, '.');
|
||||
if (spl && !strcmp(spl, ".rbf"))
|
||||
{
|
||||
closedir(dir);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
delete[] path;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void bootcore_init(const char *path)
|
||||
{
|
||||
MiSTer_ini_parse();
|
||||
if (cfg.bootcore[0] == '\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char *auxpointer;
|
||||
char auxstr[256];
|
||||
char bootcore[256];
|
||||
bool is_lastcore;
|
||||
const char *rootdir = getRootDir();
|
||||
cfg.bootcore_timeout = cfg.bootcore_timeout * 10;
|
||||
btimeout = cfg.bootcore_timeout;
|
||||
strcpy(bootcore, cfg.bootcore);
|
||||
|
||||
is_lastcore = (!strcmp(cfg.bootcore, "lastcore") || !strcmp(cfg.bootcore, "lastexactcore"));
|
||||
|
||||
if (is_lastcore)
|
||||
{
|
||||
strcpy(bootcoretype, cfg.bootcore);
|
||||
auxpointer = loadLastcore();
|
||||
if (auxpointer != NULL)
|
||||
{
|
||||
strcpy(bootcore, auxpointer);
|
||||
delete[] auxpointer;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(bootcoretype, isExactcoreName(cfg.bootcore) ? "exactcorename" : "corename");
|
||||
}
|
||||
|
||||
auxpointer = findCore(rootdir, bootcore, 0);
|
||||
if (auxpointer != NULL)
|
||||
{
|
||||
strcpy(bootcore, auxpointer);
|
||||
delete[] auxpointer;
|
||||
|
||||
sprintf(auxstr, "%s/", rootdir);
|
||||
auxpointer = replaceStr(bootcore, auxstr, "");
|
||||
if (auxpointer != NULL)
|
||||
{
|
||||
strcpy(bootcore, auxpointer);
|
||||
delete[] auxpointer;
|
||||
|
||||
if (path[0] == '\0')
|
||||
{
|
||||
if (!cfg.bootcore_timeout)
|
||||
{
|
||||
fpga_load_rbf(bootcore);
|
||||
}
|
||||
|
||||
strcpy(cfg.bootcore, strcmp(bootcore, "menu.rbf") ? bootcore : "");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_lastcore && path[0] != '\0')
|
||||
{
|
||||
|
||||
strcpy(auxstr, path);
|
||||
auxpointer = !strcmp(cfg.bootcore, "lastexactcore") ? getcoreExactName(auxstr) : getcoreName(auxstr);
|
||||
|
||||
if (auxpointer != NULL)
|
||||
{
|
||||
if (strcmp(bootcore, auxpointer))
|
||||
{
|
||||
FileSaveConfig("lastcore.dat", (char*)auxpointer, strlen(auxpointer));
|
||||
}
|
||||
}
|
||||
}
|
||||
strcpy(cfg.bootcore, "");
|
||||
|
||||
}
|
||||
|
||||
18
bootcore.h
Normal file
18
bootcore.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// bootcore.h
|
||||
// 2019, Aitor Gomez Garcia (spark2k06@gmail.com)
|
||||
// Thanks to Sorgelig and BBond007 for their help and advice in the development of this feature.
|
||||
|
||||
#ifndef __BOOTCORE_H__
|
||||
#define __BOOTCORE_H__
|
||||
|
||||
char *getcoreName(char *path);
|
||||
char *getcoreExactName(char *path);
|
||||
char *replaceStr(const char *str, const char *oldstr, const char *newstr);
|
||||
char *loadLastcore();
|
||||
char *findCore(const char *name, char *coreName, int indent);
|
||||
void bootcore_init(const char *path);
|
||||
|
||||
extern char bootcoretype[64];
|
||||
extern int16_t btimeout;
|
||||
|
||||
#endif // __BOOTCORE_H__
|
||||
2
cfg.cpp
2
cfg.cpp
@@ -46,6 +46,8 @@ const ini_var_t ini_vars[] = {
|
||||
{ "VSCALE_BORDER", (void*)(&(cfg.vscale_border)), UINT8, 0, 100, 1 },
|
||||
{ "RBF_HIDE_DATECODE", (void*)(&(cfg.rbf_hide_datecode)), UINT8, 0, 1, 1 },
|
||||
{ "MENU_PAL", (void*)(&(cfg.menu_pal)), UINT8, 0, 1, 1 },
|
||||
{ "BOOTCORE", (void*)(&(cfg.bootcore)), STRING, 0, sizeof(cfg.bootcore) - 1, 1 },
|
||||
{ "BOOTCORE_TIMEOUT", (void*)(&(cfg.bootcore_timeout)), INT8, 10, 30, 1 },
|
||||
};
|
||||
|
||||
// mist ini config
|
||||
|
||||
2
cfg.h
2
cfg.h
@@ -32,6 +32,8 @@ typedef struct {
|
||||
uint8_t vscale_border;
|
||||
uint8_t rbf_hide_datecode;
|
||||
uint8_t menu_pal;
|
||||
int16_t bootcore_timeout;
|
||||
char bootcore[256];
|
||||
char video_conf[1024];
|
||||
char video_conf_pal[1024];
|
||||
char video_conf_ntsc[1024];
|
||||
|
||||
@@ -737,7 +737,6 @@ void FindStorage(void)
|
||||
{
|
||||
int saveddev = device;
|
||||
device = 0;
|
||||
MiSTer_ini_parse();
|
||||
device = saveddev;
|
||||
parse_video_mode();
|
||||
user_io_send_buttons(1);
|
||||
|
||||
@@ -516,7 +516,7 @@ int fpga_load_rbf(const char *name, const char *cfg)
|
||||
}
|
||||
}
|
||||
close(rbf);
|
||||
app_restart(!strcasecmp(name, "menu.rbf") ? NULL : path);
|
||||
app_restart(!strcasecmp(name, "menu.rbf") ? "menu.rbf" : path);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
2
main.cpp
2
main.cpp
@@ -31,6 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "input.h"
|
||||
#include "fpga_io.h"
|
||||
#include "scheduler.h"
|
||||
#include "bootcore.h"
|
||||
|
||||
const char *version = "$VER:HPS" VDATE;
|
||||
|
||||
@@ -65,6 +66,7 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
|
||||
FindStorage();
|
||||
bootcore_init(argc > 1 ? argv[1] : "");
|
||||
user_io_init((argc > 1) ? argv[1] : "");
|
||||
|
||||
scheduler_init();
|
||||
|
||||
103
menu.cpp
103
menu.cpp
@@ -47,6 +47,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "cfg.h"
|
||||
#include "input.h"
|
||||
#include "battery.h"
|
||||
#include "bootcore.h"
|
||||
|
||||
#include "support.h"
|
||||
|
||||
@@ -643,6 +644,26 @@ static void MenuWrite(unsigned char n, const char *s, unsigned char invert, unsi
|
||||
OsdWriteOffset(row, s, invert, stipple, 0, (row == 0 && firstmenu) ? 17 : (row == (OsdGetSize()-1) && !arrow) ? 16 : 0, 0);
|
||||
}
|
||||
|
||||
const char* get_rbf_name_bootcore(char *str)
|
||||
{
|
||||
if (!strlen(cfg.bootcore)) return "";
|
||||
char *p = strrchr(str, '/');
|
||||
if (!p) return str;
|
||||
|
||||
char *spl = strrchr(p + 1, '.');
|
||||
if (spl && !strcmp(spl, ".rbf"))
|
||||
{
|
||||
*spl = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
return p + 1;
|
||||
|
||||
|
||||
}
|
||||
|
||||
void HandleUI(void)
|
||||
{
|
||||
switch (user_io_core_type())
|
||||
@@ -689,6 +710,11 @@ void HandleUI(void)
|
||||
plus = false;
|
||||
minus = false;
|
||||
|
||||
if (c && c != KEY_F12 && cfg.bootcore[0] != '\0')
|
||||
{
|
||||
cfg.bootcore[0] = '\0';
|
||||
}
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case KEY_F12:
|
||||
@@ -3556,8 +3582,35 @@ void HandleUI(void)
|
||||
|
||||
if (!rtc_timer || CheckTimer(rtc_timer))
|
||||
{
|
||||
rtc_timer = GetTimer(1000);
|
||||
rtc_timer = GetTimer(cfg.bootcore[0] != '\0' ? 100 : 1000);
|
||||
char str[64] = { 0 };
|
||||
char straux[64];
|
||||
|
||||
if (cfg.bootcore[0] != '\0')
|
||||
{
|
||||
if (btimeout >= 10)
|
||||
{
|
||||
sprintf(str, " Bootcore -> %s", bootcoretype);
|
||||
OsdWrite(13, str, 0, 0);
|
||||
strcpy(straux, cfg.bootcore);
|
||||
sprintf(str, " %s", get_rbf_name_bootcore(straux));
|
||||
PrintFileName(str, 14, (32 * btimeout) / cfg.bootcore_timeout);
|
||||
sprintf(str, " Press any key to cancel");
|
||||
OsdWrite(15, str, 0, 0);
|
||||
btimeout--;
|
||||
if (btimeout < 10)
|
||||
{
|
||||
OsdWrite(13, "", 0, 0);
|
||||
strcpy(straux, cfg.bootcore);
|
||||
sprintf(str, " %s", get_rbf_name_bootcore(straux));
|
||||
PrintFileName(str, 14, 0);
|
||||
sprintf(str, " Loading...");
|
||||
OsdWrite(15, str, 1, 0);
|
||||
fpga_load_rbf(cfg.bootcore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sprintf(str, " MiSTer ");
|
||||
|
||||
time_t t = time(NULL);
|
||||
@@ -3624,6 +3677,54 @@ void ScrollLongName(void)
|
||||
ScrollText(flist_iSelectedEntry()-flist_iFirstEntry(), flist_SelectedItem()->d_name, 2, len, max_len, 1);
|
||||
}
|
||||
|
||||
void PrintFileName(char *name, int row, int maxinv)
|
||||
{
|
||||
int len;
|
||||
|
||||
char s[40];
|
||||
s[32] = 0; // set temporary string length to OSD line length
|
||||
|
||||
len = strlen(name); // get name length
|
||||
memset(s, ' ', 32); // clear line buffer
|
||||
char *p = 0;
|
||||
if ((fs_Options & SCANO_CORES) && len > 9 && !strncmp(name + len - 9, "_20", 3))
|
||||
{
|
||||
p = name + len - 6;
|
||||
len -= 9;
|
||||
}
|
||||
|
||||
if (len > 28)
|
||||
{
|
||||
len = 27; // trim display length if longer than 30 characters
|
||||
s[28] = 22;
|
||||
}
|
||||
strncpy(s + 1, name, len); // display only name
|
||||
|
||||
if (!cfg.rbf_hide_datecode && (fs_Options & SCANO_CORES))
|
||||
{
|
||||
if (p)
|
||||
{
|
||||
int n = 19;
|
||||
s[n++] = ' ';
|
||||
s[n++] = p[0];
|
||||
s[n++] = p[1];
|
||||
s[n++] = '.';
|
||||
s[n++] = p[2];
|
||||
s[n++] = p[3];
|
||||
s[n++] = '.';
|
||||
s[n++] = p[4];
|
||||
s[n++] = p[5];
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(&s[19], " --.--.--");
|
||||
}
|
||||
}
|
||||
|
||||
OsdWrite(row, s, 1, 0, 0, maxinv);
|
||||
|
||||
}
|
||||
|
||||
// print directory contents
|
||||
void PrintDirectory(void)
|
||||
{
|
||||
|
||||
1
menu.h
1
menu.h
@@ -13,6 +13,7 @@ extern const char *config_chipset_msg[];
|
||||
|
||||
void HandleUI(void);
|
||||
void menu_key_set(unsigned int c);
|
||||
void PrintFileName(char *name, int row, int maxinv);
|
||||
void PrintDirectory(void);
|
||||
void ScrollLongName(void);
|
||||
|
||||
|
||||
7
osd.cpp
7
osd.cpp
@@ -200,13 +200,13 @@ void OsdSetArrow(int a)
|
||||
arrow = a;
|
||||
}
|
||||
|
||||
void OsdWrite(unsigned char n, const char *s, unsigned char invert, unsigned char stipple, char usebg)
|
||||
void OsdWrite(unsigned char n, const char *s, unsigned char invert, unsigned char stipple, char usebg, int maxinv)
|
||||
{
|
||||
OsdWriteOffset(n, s, invert, stipple, 0, 0, usebg);
|
||||
OsdWriteOffset(n, s, invert, stipple, 0, 0, usebg, maxinv);
|
||||
}
|
||||
|
||||
// write a null-terminated string <s> to the OSD buffer starting at line <n>
|
||||
void OsdWriteOffset(unsigned char n, const char *s, unsigned char invert, unsigned char stipple, char offset, char leftchar, char usebg)
|
||||
void OsdWriteOffset(unsigned char n, const char *s, unsigned char invert, unsigned char stipple, char offset, char leftchar, char usebg, int maxinv)
|
||||
{
|
||||
//printf("OsdWriteOffset(%d)\n", n);
|
||||
unsigned short i;
|
||||
@@ -237,6 +237,7 @@ void OsdWriteOffset(unsigned char n, const char *s, unsigned char invert, unsign
|
||||
// send all characters in string to OSD
|
||||
while (1)
|
||||
{
|
||||
if (invert && i / 8 >= maxinv) invert = 0;
|
||||
if (i == 0 && (n < osd_size))
|
||||
{ // Render sidestripe
|
||||
unsigned char j;
|
||||
|
||||
4
osd.h
4
osd.h
@@ -68,8 +68,8 @@
|
||||
/*functions*/
|
||||
void OsdSetTitle(const char *s, int arrow = 0); // arrow > 0 = display right arrow in bottom right, < 0 = display left arrow
|
||||
void OsdSetArrow(int arrow);
|
||||
void OsdWrite(unsigned char n, const char *s="", unsigned char inver=0, unsigned char stipple=0, char usebg = 0);
|
||||
void OsdWriteOffset(unsigned char n, const char *s, unsigned char inver, unsigned char stipple, char offset, char leftchar, char usebg = 0); // Used for scrolling "Exit" text downwards...
|
||||
void OsdWrite(unsigned char n, const char *s="", unsigned char inver=0, unsigned char stipple=0, char usebg = 0, int maxinv = 32);
|
||||
void OsdWriteOffset(unsigned char n, const char *s, unsigned char inver, unsigned char stipple, char offset, char leftchar, char usebg = 0, int maxinv = 32); // Used for scrolling "Exit" text downwards...
|
||||
void OsdClear(void);
|
||||
void OsdEnable(unsigned char mode);
|
||||
void InfoEnable(int x, int y, int width, int height);
|
||||
|
||||
@@ -500,7 +500,6 @@ void user_io_init(const char *path)
|
||||
user_io_8bit_set_status(UIO_STATUS_RESET, UIO_STATUS_RESET);
|
||||
}
|
||||
|
||||
MiSTer_ini_parse();
|
||||
parse_video_mode();
|
||||
FileLoadConfig("Volume.dat", &vol_att, 1);
|
||||
vol_att &= 0x1F;
|
||||
|
||||
Reference in New Issue
Block a user