* no unauthorized init
* more dynamic Preview element
This commit is contained in:
parent
57a7a9a827
commit
fdcecb0a75
@ -15,7 +15,6 @@ const (
|
|||||||
TagNode = iota
|
TagNode = iota
|
||||||
SettingsNode = iota
|
SettingsNode = iota
|
||||||
ActorNode = iota
|
ActorNode = iota
|
||||||
InitNode = iota
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type actionStruct struct {
|
type actionStruct struct {
|
||||||
@ -42,9 +41,6 @@ func ServerInit() {
|
|||||||
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
|
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
|
||||||
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
|
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
|
||||||
|
|
||||||
// initialization api calls to check if password is neccessaray
|
|
||||||
http.Handle(APIPREFIX+"/init", http.HandlerFunc(initHandler))
|
|
||||||
|
|
||||||
// initialize oauth service and add corresponding auth routes
|
// initialize oauth service and add corresponding auth routes
|
||||||
oauth.InitOAuth()
|
oauth.InitOAuth()
|
||||||
}
|
}
|
||||||
@ -85,10 +81,6 @@ func settingsHandler(rw http.ResponseWriter, req *http.Request) {
|
|||||||
handlefunc(rw, req, SettingsNode)
|
handlefunc(rw, req, SettingsNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initHandler(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
handlefunc(rw, req, InitNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlefunc(rw http.ResponseWriter, req *http.Request, node int) {
|
func handlefunc(rw http.ResponseWriter, req *http.Request, node int) {
|
||||||
// only allow post requests
|
// only allow post requests
|
||||||
if req.Method != "POST" {
|
if req.Method != "POST" {
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"openmediacenter/apiGo/database/settings"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddInitHandlers() {
|
|
||||||
passwordNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
func passwordNeeded() {
|
|
||||||
AddHandler("loadInitialData", InitNode, nil, func() []byte {
|
|
||||||
sett := settings.LoadSettings()
|
|
||||||
|
|
||||||
type InitialDataTypeResponse struct {
|
|
||||||
DarkMode bool
|
|
||||||
Pasword bool
|
|
||||||
MediacenterName string
|
|
||||||
VideoPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
regexMatchUrl := regexp.MustCompile("^http(|s):\\/\\/([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}")
|
|
||||||
videoUrl := regexMatchUrl.FindString(sett.VideoPath)
|
|
||||||
serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl)
|
|
||||||
|
|
||||||
res := InitialDataTypeResponse{
|
|
||||||
DarkMode: sett.DarkMode,
|
|
||||||
Pasword: sett.Pasword != "-1",
|
|
||||||
MediacenterName: sett.Mediacenter_name,
|
|
||||||
VideoPath: serverVideoPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
str, _ := json.Marshal(res)
|
|
||||||
return str
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,9 +1,13 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"openmediacenter/apiGo/api/types"
|
"openmediacenter/apiGo/api/types"
|
||||||
"openmediacenter/apiGo/database"
|
"openmediacenter/apiGo/database"
|
||||||
|
"openmediacenter/apiGo/database/settings"
|
||||||
"openmediacenter/apiGo/videoparser"
|
"openmediacenter/apiGo/videoparser"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddSettingsHandlers() {
|
func AddSettingsHandlers() {
|
||||||
@ -17,6 +21,30 @@ func getSettingsFromDB() {
|
|||||||
result := database.GetSettings()
|
result := database.GetSettings()
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
})
|
})
|
||||||
|
AddHandler("loadInitialData", SettingsNode, nil, func() []byte {
|
||||||
|
sett := settings.LoadSettings()
|
||||||
|
|
||||||
|
type InitialDataTypeResponse struct {
|
||||||
|
DarkMode bool
|
||||||
|
Pasword bool
|
||||||
|
MediacenterName string
|
||||||
|
VideoPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
regexMatchUrl := regexp.MustCompile("^http(|s):\\/\\/([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}")
|
||||||
|
videoUrl := regexMatchUrl.FindString(sett.VideoPath)
|
||||||
|
serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl)
|
||||||
|
|
||||||
|
res := InitialDataTypeResponse{
|
||||||
|
DarkMode: sett.DarkMode,
|
||||||
|
Pasword: sett.Pasword != "-1",
|
||||||
|
MediacenterName: sett.Mediacenter_name,
|
||||||
|
VideoPath: serverVideoPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
str, _ := json.Marshal(res)
|
||||||
|
return str
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSettingsToDB() {
|
func saveSettingsToDB() {
|
||||||
|
@ -30,7 +30,6 @@ func main() {
|
|||||||
api.AddSettingsHandlers()
|
api.AddSettingsHandlers()
|
||||||
api.AddTagHandlers()
|
api.AddTagHandlers()
|
||||||
api.AddActorsHandlers()
|
api.AddActorsHandlers()
|
||||||
api.AddInitHandlers()
|
|
||||||
|
|
||||||
// add the static files
|
// add the static files
|
||||||
static.ServeStaticFiles()
|
static.ServeStaticFiles()
|
||||||
|
14
src/App.tsx
14
src/App.tsx
@ -9,7 +9,7 @@ import style from './App.module.css';
|
|||||||
|
|
||||||
import SettingsPage from './pages/SettingsPage/SettingsPage';
|
import SettingsPage from './pages/SettingsPage/SettingsPage';
|
||||||
import CategoryPage from './pages/CategoryPage/CategoryPage';
|
import CategoryPage from './pages/CategoryPage/CategoryPage';
|
||||||
import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api';
|
import {APINode, apiTokenValid, callAPI, refreshAPIToken} from './utils/Api';
|
||||||
|
|
||||||
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
|
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
|
||||||
import Player from './pages/Player/Player';
|
import Player from './pages/Player/Player';
|
||||||
@ -75,7 +75,7 @@ class App extends React.Component<{}, state> {
|
|||||||
|
|
||||||
initialAPICall(): void {
|
initialAPICall(): void {
|
||||||
// this is the first api call so if it fails we know there is no connection to backend
|
// this is the first api call so if it fails we know there is no connection to backend
|
||||||
callApiUnsafe(APINode.Init, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => {
|
callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => {
|
||||||
// set theme
|
// set theme
|
||||||
GlobalInfos.enableDarkTheme(result.DarkMode);
|
GlobalInfos.enableDarkTheme(result.DarkMode);
|
||||||
|
|
||||||
@ -99,7 +99,15 @@ class App extends React.Component<{}, state> {
|
|||||||
|
|
||||||
if (this.state.password === true) {
|
if (this.state.password === true) {
|
||||||
// render authentication page if auth is neccessary
|
// render authentication page if auth is neccessary
|
||||||
return <AuthenticationPage onSuccessLogin={(): void => this.setState({password: false})} />;
|
return (
|
||||||
|
<AuthenticationPage
|
||||||
|
onSuccessLogin={(): void => {
|
||||||
|
this.setState({password: false});
|
||||||
|
// reinit general infos
|
||||||
|
this.initialAPICall();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (this.state.password === false) {
|
} else if (this.state.password === false) {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
|
@ -9,7 +9,6 @@ interface Props<T> {
|
|||||||
|
|
||||||
interface state<T> {
|
interface state<T> {
|
||||||
loadeditems: T[];
|
loadeditems: T[];
|
||||||
selectionnr: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,8 +23,7 @@ class DynamicContentContainer<T> extends React.Component<Props<T>, state<T>> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loadeditems: [],
|
loadeditems: []
|
||||||
selectionnr: 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +33,22 @@ class DynamicContentContainer<T> extends React.Component<Props<T>, state<T>> {
|
|||||||
this.loadPreviewBlock(this.props.initialLoadNr ? this.props.initialLoadNr : 16);
|
this.loadPreviewBlock(this.props.initialLoadNr ? this.props.initialLoadNr : 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props<T>): void {
|
||||||
|
// when source props change force update!
|
||||||
|
if (prevProps.data.length !== this.props.data.length) {
|
||||||
|
this.clean();
|
||||||
|
this.loadPreviewBlock(this.props.initialLoadNr ? this.props.initialLoadNr : 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear all elements rendered...
|
||||||
|
*/
|
||||||
|
clean(): void {
|
||||||
|
this.loadindex = 0;
|
||||||
|
this.setState({loadeditems: []});
|
||||||
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={style.maincontent}>
|
<div className={style.maincontent}>
|
||||||
|
@ -3,17 +3,18 @@ import style from './Preview.module.css';
|
|||||||
import {Spinner} from 'react-bootstrap';
|
import {Spinner} from 'react-bootstrap';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import GlobalInfos from '../../utils/GlobalInfos';
|
import GlobalInfos from '../../utils/GlobalInfos';
|
||||||
import {APINode, callAPIPlain} from '../../utils/Api';
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
interface PreviewProps {
|
interface PreviewProps {
|
||||||
name: string;
|
name: string;
|
||||||
movieId: number;
|
picLoader: (callback: (pic: string) => void) => void;
|
||||||
|
linkPath?: string;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreviewState {
|
interface PreviewState {
|
||||||
previewpicture: string | null;
|
picLoaded: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,49 +22,61 @@ interface PreviewState {
|
|||||||
* floating side by side
|
* floating side by side
|
||||||
*/
|
*/
|
||||||
class Preview extends React.Component<PreviewProps, PreviewState> {
|
class Preview extends React.Component<PreviewProps, PreviewState> {
|
||||||
|
// store the picture to display
|
||||||
|
pic?: string;
|
||||||
|
|
||||||
constructor(props: PreviewProps) {
|
constructor(props: PreviewProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
previewpicture: null
|
picLoaded: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
callAPIPlain(APINode.Video, {action: 'readThumbnail', movieid: this.props.movieId}, (result) => {
|
this.props.picLoader((result) => {
|
||||||
|
this.pic = result;
|
||||||
this.setState({
|
this.setState({
|
||||||
previewpicture: result
|
picLoaded: result !== ''
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
|
if (this.props.linkPath !== undefined) {
|
||||||
|
return <Link to={this.props.linkPath}>{this.content()}</Link>;
|
||||||
|
} else {
|
||||||
|
return this.content();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content(): JSX.Element {
|
||||||
const themeStyle = GlobalInfos.getThemeStyle();
|
const themeStyle = GlobalInfos.getThemeStyle();
|
||||||
return (
|
return (
|
||||||
<Link to={'/player/' + this.props.movieId}>
|
<div
|
||||||
<div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
|
className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}
|
||||||
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
|
onClick={this.props.onClick}>
|
||||||
<div className={style.previewpic}>
|
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
|
||||||
{this.state.previewpicture === '' ? (
|
<div className={style.previewpic}>
|
||||||
<FontAwesomeIcon
|
{this.state.picLoaded === false ? (
|
||||||
style={{
|
<FontAwesomeIcon
|
||||||
color: 'white',
|
style={{
|
||||||
marginTop: '55px'
|
color: 'white',
|
||||||
}}
|
marginTop: '55px'
|
||||||
icon={faPhotoVideo}
|
}}
|
||||||
size='5x'
|
icon={faPhotoVideo}
|
||||||
/>
|
size='5x'
|
||||||
) : this.state.previewpicture === null ? (
|
/>
|
||||||
<span className={style.loadAnimation}>
|
) : this.state.picLoaded === null ? (
|
||||||
<Spinner animation='border' />
|
<span className={style.loadAnimation}>
|
||||||
</span>
|
<Spinner animation='border' />
|
||||||
) : (
|
</span>
|
||||||
<img className={style.previewimage} src={this.state.previewpicture} alt='Pic loading.' />
|
) : (
|
||||||
)}
|
<img className={style.previewimage} src={this.pic} alt='Pic loading.' />
|
||||||
</div>
|
)}
|
||||||
<div className={style.previewbottom} />
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
<div className={style.previewbottom} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import Preview from '../Preview/Preview';
|
import Preview from '../Preview/Preview';
|
||||||
import {VideoTypes} from '../../types/ApiTypes';
|
import {VideoTypes} from '../../types/ApiTypes';
|
||||||
import DynamicContentContainer from '../DynamicContentContainer/DynamicContentContainer';
|
import DynamicContentContainer from '../DynamicContentContainer/DynamicContentContainer';
|
||||||
|
import {APINode, callAPIPlain} from '../../utils/Api';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: VideoTypes.VideoUnloadedType[];
|
data: VideoTypes.VideoUnloadedType[];
|
||||||
@ -11,7 +12,23 @@ interface Props {
|
|||||||
const VideoContainer = (props: Props): JSX.Element => {
|
const VideoContainer = (props: Props): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<DynamicContentContainer
|
<DynamicContentContainer
|
||||||
renderElement={(el): JSX.Element => <Preview key={el.MovieId} name={el.MovieName} movieId={el.MovieId} />}
|
renderElement={(el): JSX.Element => (
|
||||||
|
<Preview
|
||||||
|
key={el.MovieId}
|
||||||
|
picLoader={(callback: (pic: string) => void): void => {
|
||||||
|
callAPIPlain(
|
||||||
|
APINode.Video,
|
||||||
|
{
|
||||||
|
action: 'readThumbnail',
|
||||||
|
movieid: el.MovieId
|
||||||
|
},
|
||||||
|
(result) => callback(result)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
name={el.MovieName}
|
||||||
|
linkPath={'/player/' + el.MovieId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
data={props.data}>
|
data={props.data}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</DynamicContentContainer>
|
</DynamicContentContainer>
|
||||||
|
@ -4,9 +4,10 @@ import {ActorType} from '../../types/VideoTypes';
|
|||||||
import ActorTile from '../../elements/ActorTile/ActorTile';
|
import ActorTile from '../../elements/ActorTile/ActorTile';
|
||||||
import PageTitle from '../../elements/PageTitle/PageTitle';
|
import PageTitle from '../../elements/PageTitle/PageTitle';
|
||||||
import SideBar from '../../elements/SideBar/SideBar';
|
import SideBar from '../../elements/SideBar/SideBar';
|
||||||
import style from './ActorOverviewPage.module.css';
|
// import style from './ActorOverviewPage.module.css';
|
||||||
import {Button} from '../../elements/GPElements/Button';
|
import {Button} from '../../elements/GPElements/Button';
|
||||||
import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup';
|
import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup';
|
||||||
|
import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer';
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
@ -36,11 +37,12 @@ class ActorOverviewPage extends React.Component<Props, state> {
|
|||||||
<SideBar>
|
<SideBar>
|
||||||
<Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})} />
|
<Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})} />
|
||||||
</SideBar>
|
</SideBar>
|
||||||
<div className={style.container}>
|
<DynamicContentContainer
|
||||||
{this.state.actors.map((el) => (
|
renderElement={(el): JSX.Element => <ActorTile key={el.ActorId} actor={el} />}
|
||||||
<ActorTile key={el.ActorId} actor={el} />
|
data={this.state.actors}
|
||||||
))}
|
initialLoadNr={36}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{this.state.NActorPopupVisible ? (
|
{this.state.NActorPopupVisible ? (
|
||||||
<NewActorPopup
|
<NewActorPopup
|
||||||
onHide={(): void => {
|
onHide={(): void => {
|
||||||
|
@ -48,7 +48,7 @@ export class ActorPage extends React.Component<Props, state> {
|
|||||||
</div>
|
</div>
|
||||||
<SideBarTitle>Attention: This is an early preview!</SideBarTitle>
|
<SideBarTitle>Attention: This is an early preview!</SideBarTitle>
|
||||||
</SideBar>
|
</SideBar>
|
||||||
{this.state.data.length !== 0 ? <VideoContainer data={this.state.data} /> : <div>No Data found!</div>}
|
<VideoContainer data={this.state.data} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -53,19 +53,15 @@ class TagView extends React.Component<Props, TagViewState> {
|
|||||||
Add a new Tag!
|
Add a new Tag!
|
||||||
</button>
|
</button>
|
||||||
</SideBar>
|
</SideBar>
|
||||||
{this.state.loadedtags.length !== 0 ? (
|
<DynamicContentContainer
|
||||||
<DynamicContentContainer
|
data={this.state.loadedtags}
|
||||||
data={this.state.loadedtags}
|
renderElement={(m): JSX.Element => (
|
||||||
renderElement={(m): JSX.Element => (
|
<Link to={'/categories/' + m.TagId} key={m.TagId}>
|
||||||
<Link to={'/categories/' + m.TagId} key={m.TagId}>
|
<TagPreview name={m.TagName} />
|
||||||
<TagPreview name={m.TagName} />
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
)}
|
initialLoadNr={20}
|
||||||
initialLoadNr={20}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'loading'
|
|
||||||
)}
|
|
||||||
{this.handlePopups()}
|
{this.handlePopups()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -157,7 +157,7 @@ export class HomePage extends React.Component<Props, state> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SideBar>
|
</SideBar>
|
||||||
{this.state.data.length !== 0 ? <VideoContainer data={this.state.data} /> : <div>No Data found!</div>}
|
<VideoContainer data={this.state.data} />
|
||||||
<div className={style.rightinfo} />
|
<div className={style.rightinfo} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Preview from '../../elements/Preview/Preview';
|
||||||
|
|
||||||
class TVShowPage extends React.Component {
|
class TVShowPage extends React.Component {
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
return <>TvShowPage</>;
|
return (
|
||||||
|
<>
|
||||||
|
<Preview name='myTestItem' picLoader={(callback): void => callback('')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +279,5 @@ export enum APINode {
|
|||||||
Settings = 'settings',
|
Settings = 'settings',
|
||||||
Tags = 'tags',
|
Tags = 'tags',
|
||||||
Actor = 'actor',
|
Actor = 'actor',
|
||||||
Video = 'video',
|
Video = 'video'
|
||||||
Init = 'init'
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user