You've already forked factorio-server-manager
mirror of
https://github.com/OpenFactorioServerManager/factorio-server-manager.git
synced 2025-07-17 01:22:28 +02:00
Merge remote-tracking branch 'jannaahs/rework-ui' into rework-ui-cleanup-mod-portal
# Conflicts: # src/handlers.go # src/mods_handler.go # src/routes.go # ui/App/views/Controls.jsx # ui/App/views/Saves/Saves.jsx
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 910 KiB |
Binary file not shown.
Before Width: | Height: | Size: 190 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -4,7 +4,6 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>Factorio Server Manager</title>
|
<title>Factorio Server Manager</title>
|
||||||
<link rel="icon" type="image/png" href="./images/favicon.ico">
|
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
|
||||||
|
@ -5,7 +5,18 @@ module.exports = {
|
|||||||
purge: [],
|
purge: [],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
width: {
|
||||||
|
72: "18rem",
|
||||||
|
80: "20rem",
|
||||||
|
88: "22rem",
|
||||||
|
96: "24rem",
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
72: "18rem",
|
||||||
|
80: "20rem",
|
||||||
|
88: "22rem",
|
||||||
|
96: "24rem",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
"gray": {
|
"gray": {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import React, {useEffect} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import server from "../../api/resources/server";
|
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faBars} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {
|
const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {
|
||||||
|
|
||||||
|
const [isNavCollapsed, setIsNavCollapsed] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
updateServerStatus()
|
updateServerStatus()
|
||||||
@ -34,6 +37,7 @@ const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {
|
|||||||
const Link = ({children, to, last}) => {
|
const Link = ({children, to, last}) => {
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
onClick={() => setIsNavCollapsed(true)}
|
||||||
exact={true}
|
exact={true}
|
||||||
to={to}
|
to={to}
|
||||||
activeClassName="bg-orange"
|
activeClassName="bg-orange"
|
||||||
@ -42,55 +46,62 @@ const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex md:flex-row-reverse flex-wrap">
|
<>
|
||||||
|
{/*Sidebar*/}
|
||||||
|
<div className="w-full md:w-88 md:fixed md:top-0 md:left-0 bg-gray-dark md:h-screen">
|
||||||
|
<div className="py-4 px-2 accentuated">
|
||||||
|
<div className="mx-4 justify-between flex text-center">
|
||||||
|
<span className="text-dirty-white text-xl">Factorio Server Manager</span>
|
||||||
|
<button
|
||||||
|
className="md:hidden cursor-pointer text-white hover:text-dirty-white"
|
||||||
|
onClick={() => setIsNavCollapsed(!isNavCollapsed)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBars}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={isNavCollapsed ? "hidden md:block" : "block"}>
|
||||||
|
<div className="py-4 px-2 accentuated">
|
||||||
|
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Status</h1>
|
||||||
|
<div className="mx-4 mb-4 text-center">
|
||||||
|
<Status info={serverStatus}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-4 px-2 accentuated">
|
||||||
|
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Management</h1>
|
||||||
|
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
||||||
|
<Link to="/">Controls</Link>
|
||||||
|
<Link to="/saves">Saves</Link>
|
||||||
|
<Link to="/mods">Mods</Link>
|
||||||
|
<Link to="/server-settings">Server Settings</Link>
|
||||||
|
<Link to="/game-settings">Game Settings</Link>
|
||||||
|
<Link to="/console">Console</Link>
|
||||||
|
<Link to="/logs" last={true}>Logs</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-4 px-2 accentuated">
|
||||||
|
<h1 className="text-dirty-white text-lg mb-2 mx-4">FSM Administration</h1>
|
||||||
|
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
||||||
|
<Link to="/user-management">Users</Link>
|
||||||
|
<Link to="/help" last={true}>Help</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-4 px-2 accentuated">
|
||||||
|
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
||||||
|
<Button type="danger" className="w-full" onClick={handleLogout}>Logout</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="accentuated-t accentuated-x md:block hidden"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/*Main*/}
|
{/*Main*/}
|
||||||
<div className="w-full md:w-5/6 bg-gray-100 bg-banner bg-fixed min-h-screen">
|
<div className="md:ml-88 bg-gray-100 bg-black min-h-screen">
|
||||||
<div className="container mx-auto bg-gray-100 pt-16 px-6">
|
<div className="container mx-auto bg-gray-100 pt-16 px-6">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
{/*Sidebar*/}
|
|
||||||
<div
|
|
||||||
className="w-full md:w-1/6 bg-gray-dark fixed bottom-0 md:top-0 md:left-0 h-16 md:h-screen">
|
|
||||||
<div className="py-4 px-2 accentuated items-center text-center">
|
|
||||||
<img src="/images/factorio.jpg" className="inline h-8" alt="Factorio Logo"/>
|
|
||||||
<span className="text-dirty-white pl-2 text-xl">Factorio Server Manager</span>
|
|
||||||
</div>
|
|
||||||
<div className="py-4 px-2 accentuated">
|
|
||||||
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Status</h1>
|
|
||||||
<div className="mx-4 mb-4 text-center">
|
|
||||||
<Status info={serverStatus}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="py-4 px-2 accentuated">
|
|
||||||
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Management</h1>
|
|
||||||
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
|
||||||
<Link to="/">Controls</Link>
|
|
||||||
<Link to="/saves">Saves</Link>
|
|
||||||
<Link to="/mods">Mods</Link>
|
|
||||||
<Link to="/server-settings">Server Settings</Link>
|
|
||||||
<Link to="/game-settings">Game Settings</Link>
|
|
||||||
<Link to="/console">Console</Link>
|
|
||||||
<Link to="/logs" last={true}>Logs</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="py-4 px-2 accentuated">
|
|
||||||
<h1 className="text-dirty-white text-lg mb-2 mx-4">FSM Administration</h1>
|
|
||||||
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
|
||||||
<Link to="/user-management">Users</Link>
|
|
||||||
<Link to="/help" last={true}>Help</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="py-4 px-2 accentuated">
|
|
||||||
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
|
||||||
<Button type="danger" className="w-full" onClick={handleLogout}>Logout</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="accentuated h-full"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,75 +50,86 @@ const Controls = ({serverStatus, updateServerStatus}) => {
|
|||||||
<Panel
|
<Panel
|
||||||
title="Server Status"
|
title="Server Status"
|
||||||
content={
|
content={
|
||||||
<div className="flex">
|
<div className="lg:flex">
|
||||||
<table className="w-full">
|
{ isRunning
|
||||||
<thead>
|
? <>
|
||||||
<tr className="text-left py-1">
|
<div className="lg:w-1/5 mb-2">
|
||||||
<th>Status</th>
|
<div className="font-bold">Status</div>
|
||||||
<th>IP</th>
|
<div>{serverStatus.status}</div>
|
||||||
<th>Port</th>
|
</div>
|
||||||
<th>Factorio Version</th>
|
<div className="lg:w-1/5 mb-2">
|
||||||
<th>Save File</th>
|
<div className="font-bold">IP</div>
|
||||||
</tr>
|
<div>{serverStatus.address}</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
<div className="lg:w-1/5 mb-2">
|
||||||
{isRunning
|
<div className="font-bold">Port</div>
|
||||||
? <tr className="py-1">
|
<div>{serverStatus.port}</div>
|
||||||
<td className="pr-4 py-2">{serverStatus.status}</td>
|
</div>
|
||||||
<td className="pr-4 py-2">{serverStatus.address}</td>
|
<div className="lg:w-1/5 mb-2">
|
||||||
<td className="pr-4 py-2">{serverStatus.port}</td>
|
<div className="font-bold">Factorio Version</div>
|
||||||
<td className="pr-4 py-2">{factorioVersion}</td>
|
<div>{factorioVersion}</div>
|
||||||
<td className="pr-4 py-2">{serverStatus.savefile}</td>
|
</div>
|
||||||
</tr>
|
<div className="lg:w-1/5 mb-2">
|
||||||
: <tr className="py-1">
|
<div className="font-bold">Save</div>
|
||||||
<td className="pr-4 py-2">{serverStatus.status}</td>
|
<div>{serverStatus.savefile}</div>
|
||||||
<td className="pr-4">
|
</div>
|
||||||
<input
|
</>
|
||||||
name="ip"
|
: <>
|
||||||
className="shadow appearance-none w-full py-2 px-3 text-black"
|
<div className="lg:w-1/5 mb-2">
|
||||||
type="text"
|
<div className="font-bold">Status</div>
|
||||||
defaultValue={"0.0.0.0"}
|
<div>{serverStatus.status}</div>
|
||||||
ref={register({required: true, pattern: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'})}
|
</div>
|
||||||
/>
|
<div className="lg:w-1/5 mb-2 mr-0 lg:mr-4">
|
||||||
{errors.ip && <span className="block text-red">IP is required and must be valid.</span>}
|
<div className="font-bold">IP</div>
|
||||||
</td>
|
<input
|
||||||
<td className="pr-4">
|
name="ip"
|
||||||
<input
|
className="shadow appearance-none w-full mr-2 py-2 px-3 text-black"
|
||||||
name="port"
|
type="text"
|
||||||
className="shadow appearance-none w-full py-2 px-3 text-black"
|
defaultValue={"0.0.0.0"}
|
||||||
type="number"
|
ref={register({required: true, pattern: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'})}
|
||||||
min={1}
|
/>
|
||||||
defaultValue={"34197"}
|
{errors.ip && <span className="block text-red">IP is required and must be valid.</span>}
|
||||||
ref={register({required: true})}
|
</div>
|
||||||
/>
|
<div className="lg:w-1/5 mb-2 mr-0 lg:mr-4">
|
||||||
{errors.port && <span className="block text-red">Port is required</span>}
|
<div className="font-bold">Port</div>
|
||||||
</td>
|
<input
|
||||||
<td className="pr-4 py-2">{factorioVersion}</td>
|
name="port"
|
||||||
<td className="pr-4 py-2">
|
className="shadow appearance-none w-full py-2 px-3 text-black"
|
||||||
<div className="relative">
|
type="number"
|
||||||
<Select
|
min={1}
|
||||||
name="save"
|
defaultValue={"34197"}
|
||||||
inputRef={register({required: true})}
|
ref={register({required: true})}
|
||||||
>
|
/>
|
||||||
{saves.map(save => (
|
{errors.port && <span className="block text-red">Port is required</span>}
|
||||||
<option value={save.name} key={save.name}>{save.name}</option>))}
|
</div>
|
||||||
</Select>
|
<div className="lg:w-1/5 mb-2 mr-0 lg:mr-4">
|
||||||
</div>
|
<div className="font-bold">Factorio Version</div>
|
||||||
</td>
|
<div>{factorioVersion}</div>
|
||||||
</tr>
|
</div>
|
||||||
}
|
<div className="lg:w-1/5 mb-2">
|
||||||
</tbody>
|
<div className="font-bold">Save</div>
|
||||||
</table>
|
<div className="relative">
|
||||||
|
<Select
|
||||||
|
name="save"
|
||||||
|
inputRef={register({required: true})}
|
||||||
|
>
|
||||||
|
{saves.map(save => (
|
||||||
|
<option value={save.name} key={save.name}>{save.name}</option>))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex">
|
<div className="md:flex">
|
||||||
{isRunning
|
{isRunning
|
||||||
? <>
|
? <>
|
||||||
<Button onClick={stopServer} size="sm" className="mr-2" type="default">Save & Stop Server</Button>
|
<Button onClick={stopServer} size="sm" className="w-full md:w-auto mb-2 md:mb-0 md:mr-2" type="default">Save & Stop Server</Button>
|
||||||
<Button onClick={killServer} size="sm" type="danger">Kill Server</Button>
|
<Button onClick={killServer} size="sm" type="danger" className="w-full md:w-auto">Kill Server</Button>
|
||||||
</>
|
</>
|
||||||
: <Button isSubmit={true} size="sm" type="success">Start Server</Button>
|
: <Button isSubmit={true} size="sm" type="success" className="w-full md:w-auto">Start Server</Button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {useHistory, useLocation} from "react-router";
|
|||||||
import Panel from "../components/Panel";
|
import Panel from "../components/Panel";
|
||||||
import Input from "../components/Input";
|
import Input from "../components/Input";
|
||||||
import Label from "../components/Label";
|
import Label from "../components/Label";
|
||||||
|
import {Flash} from "../components/Flash";
|
||||||
|
|
||||||
const Login = ({handleLogin}) => {
|
const Login = ({handleLogin}) => {
|
||||||
const {register, handleSubmit, errors} = useForm();
|
const {register, handleSubmit, errors} = useForm();
|
||||||
@ -32,7 +33,7 @@ const Login = ({handleLogin}) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen overflow-hidden flex items-center justify-center bg-banner">
|
<div className="h-screen overflow-hidden flex items-center justify-center bg-black">
|
||||||
<Panel
|
<Panel
|
||||||
title="Login"
|
title="Login"
|
||||||
content={
|
content={
|
||||||
@ -58,6 +59,7 @@ const Login = ({handleLogin}) => {
|
|||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Flash/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,12 +11,12 @@ const Saves = ({serverStatus}) => {
|
|||||||
const [saves, setSaves] = useState([]);
|
const [saves, setSaves] = useState([]);
|
||||||
|
|
||||||
const updateList = () => {
|
const updateList = () => {
|
||||||
savesResource.list()
|
savesResource.list()
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res) {
|
if (res) {
|
||||||
setSaves(res);
|
setSaves(res);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,10 +33,10 @@ const Saves = ({serverStatus}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex mb-6">
|
<div className="lg:flex mb-6">
|
||||||
<Panel
|
<Panel
|
||||||
title="Create Save"
|
title="Create Save"
|
||||||
className="w-1/2 mr-3"
|
className="lg:w-1/2 lg:mr-3 mb-6 lg:mb-0"
|
||||||
content={
|
content={
|
||||||
serverStatus.status === "running"
|
serverStatus.status === "running"
|
||||||
? <p className="text-red-light pt-4 pb-24">
|
? <p className="text-red-light pt-4 pb-24">
|
||||||
@ -48,7 +48,7 @@ const Saves = ({serverStatus}) => {
|
|||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
title="Upload Save"
|
title="Upload Save"
|
||||||
className="w-1/2 ml-3"
|
className="lg:w-1/2 lg:ml-3"
|
||||||
content={<UploadSaveForm onSuccess={updateList}/>}
|
content={<UploadSaveForm onSuccess={updateList}/>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -57,31 +57,36 @@ const Saves = ({serverStatus}) => {
|
|||||||
className="mb-4"
|
className="mb-4"
|
||||||
title="Saves"
|
title="Saves"
|
||||||
content={
|
content={
|
||||||
<table className="w-full">
|
<div className="overflow-x-auto">
|
||||||
<thead>
|
<table style={{"width" : "max-content"}}>
|
||||||
<tr className="text-left py-1">
|
<thead>
|
||||||
<th>Name</th>
|
<tr className="text-left py-1">
|
||||||
<th>Last Modified At</th>
|
<th>Name</th>
|
||||||
<th>Size</th>
|
<th>Last Modified At</th>
|
||||||
<th>Actions</th>
|
<th>Size</th>
|
||||||
</tr>
|
<th>Actions</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{saves.map(save =>
|
|
||||||
<tr className="py-1" key={save.name}>
|
|
||||||
<td className="pr-4">{save.name}</td>
|
|
||||||
<td className="pr-4">{(new Date(save.last_mod)).toISOString().replace('T', ' ').split('.')[0]}</td>
|
|
||||||
<td>{parseFloat(save.size / 1024 / 1024).toFixed(3)} MB</td>
|
|
||||||
<td>
|
|
||||||
<a href={`/api/saves/dl/${save.name}`} className="mr-2">
|
|
||||||
<FontAwesomeIcon className="text-gray-light cursor-pointer hover:text-orange" icon={faDownload}/>
|
|
||||||
</a>
|
|
||||||
<FontAwesomeIcon className="text-red cursor-pointer hover:text-red-light mr-2" onClick={() => deleteSave(save)} icon={faTrashAlt}/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{saves.map(save =>
|
||||||
|
<tr className="py-2 md:py-1" key={save.name}>
|
||||||
|
<td className="pr-4">{save.name}</td>
|
||||||
|
<td className="pr-4">{(new Date(save.last_mod)).toISOString().replace('T', ' ').split('.')[0]}</td>
|
||||||
|
<td className="pr-4">{parseFloat(save.size / 1024 / 1024).toFixed(3)} MB</td>
|
||||||
|
<td>
|
||||||
|
<a href={`/api/saves/dl/${save.name}`} className="mr-2">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="text-gray-light cursor-pointer hover:text-orange"
|
||||||
|
icon={faDownload}/>
|
||||||
|
</a>
|
||||||
|
<FontAwesomeIcon className="text-red cursor-pointer hover:text-red-light mr-2"
|
||||||
|
onClick={() => deleteSave(save)} icon={faTrashAlt}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -7,4 +7,13 @@ const client = Axios.create({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.interceptors.response.use(res => res, err => {
|
||||||
|
if(err.response.status === 502) {
|
||||||
|
window.flash("Service not available", "red");
|
||||||
|
} else if (err.response.status !== 401) {
|
||||||
|
window.flash(err.response.data, "red");
|
||||||
|
}
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
export default client;
|
export default client;
|
@ -2,11 +2,6 @@
|
|||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
@import "tailwindcss/components";
|
@import "tailwindcss/components";
|
||||||
|
|
||||||
.bg-banner {
|
|
||||||
background-image: url("/images/factorio-main-banner.jpg");
|
|
||||||
background-position: center center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accentuated-t {
|
.accentuated-t {
|
||||||
border-top: 2px solid rgba(255, 255, 255, 0.1);
|
border-top: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user