mirror of
https://github.com/lobehub/sd-webui-lobe-theme.git
synced 2026-01-09 06:23:44 +08:00
♻️ refactor(wip): refactor with @lobehub/ui
This commit is contained in:
parent
e9e51e66b2
commit
e116458d10
@ -1 +1 @@
|
|||||||
module.exports = require('@lobehub/lint/dist/semantic-release');
|
module.exports = require('@lobehub/lint').semanticRelease;
|
||||||
|
|||||||
1051
javascript/index.js
1051
javascript/index.js
File diff suppressed because one or more lines are too long
@ -51,6 +51,7 @@
|
|||||||
"@ant-design/icons": "^5",
|
"@ant-design/icons": "^5",
|
||||||
"@commitlint/cli": "^17",
|
"@commitlint/cli": "^17",
|
||||||
"@lobehub/lint": "latest",
|
"@lobehub/lint": "latest",
|
||||||
|
"@lobehub/ui": "latest",
|
||||||
"@types/lodash-es": "^4",
|
"@types/lodash-es": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
@ -62,7 +63,7 @@
|
|||||||
"@umijs/lint": "^4",
|
"@umijs/lint": "^4",
|
||||||
"ahooks": "^3",
|
"ahooks": "^3",
|
||||||
"antd": "^5",
|
"antd": "^5",
|
||||||
"antd-style": "^3",
|
"antd-style": "latest",
|
||||||
"babel-plugin-styled-components": "^2",
|
"babel-plugin-styled-components": "^2",
|
||||||
"browserslist": "^4",
|
"browserslist": "^4",
|
||||||
"commitlint": "^17",
|
"commitlint": "^17",
|
||||||
@ -83,14 +84,13 @@
|
|||||||
"react-dnd": "^16",
|
"react-dnd": "^16",
|
||||||
"react-dnd-html5-backend": "^16",
|
"react-dnd-html5-backend": "^16",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-font-loader": "^2",
|
|
||||||
"react-layout-kit": "^1",
|
"react-layout-kit": "^1",
|
||||||
"react-rnd": "^10",
|
"react-rnd": "^10",
|
||||||
"react-tag-input": "^6",
|
"react-tag-input": "^6",
|
||||||
"remark": "^14",
|
"remark": "^14",
|
||||||
"remark-cli": "^11",
|
"remark-cli": "^11",
|
||||||
"semantic-release": "^21",
|
"semantic-release": "^21",
|
||||||
"styled-components": "^5",
|
"styled-components": "latest",
|
||||||
"stylelint": "^15",
|
"stylelint": "^15",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"umi": "^4",
|
"umi": "^4",
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { ZoomInOutlined } from '@ant-design/icons';
|
import {ZoomInOutlined} from '@ant-design/icons';
|
||||||
import { Slider } from 'antd';
|
import {DraggablePanel} from '@lobehub/ui';
|
||||||
import { useResponsive } from 'antd-style';
|
import {Slider} from 'antd';
|
||||||
import { type CSSProperties, type ReactNode, memo, useEffect, useState } from 'react';
|
import {useResponsive} from 'antd-style';
|
||||||
import styled, { createGlobalStyle } from 'styled-components';
|
import {type CSSProperties, type ReactNode, memo, useEffect, useState} from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import styled, {createGlobalStyle} from 'styled-components';
|
||||||
|
import {shallow} from 'zustand/shallow';
|
||||||
|
|
||||||
import { DraggablePanel } from '@/components';
|
import {useAppStore} from '@/store';
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
button#txt2img_extra_networks,
|
button#txt2img_extra_networks,
|
||||||
@ -40,11 +40,11 @@ const SidebarView = styled.div<{ size: number }>`
|
|||||||
.extra-network-cards,
|
.extra-network-cards,
|
||||||
.extra-network-thumbs {
|
.extra-network-thumbs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(${({ size }) => size}px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(${({size}) => size}px, 1fr));
|
||||||
|
|
||||||
> .card {
|
> .card {
|
||||||
width: var(--fill-available) !important;
|
width: var(--fill-available) !important;
|
||||||
height: ${({ size }) => size * 1.5}px !important;
|
height: ${({size}) => size * 1.5}px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -71,44 +71,44 @@ interface SidebarProps {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar = memo<SidebarProps>(({ children, style }) => {
|
const Sidebar = memo<SidebarProps>(({children, style}) => {
|
||||||
const { mobile } = useResponsive();
|
const {mobile} = useResponsive();
|
||||||
const [setting] = useAppStore((st) => [st.setting], shallow);
|
const [setting] = useAppStore((st) => [st.setting], shallow);
|
||||||
const [mode] = useState<'fixed' | 'float'>(setting.extraNetworkFixedMode);
|
const [mode] = useState<'fixed' | 'float'>(setting.extraNetworkFixedMode);
|
||||||
const [expand, setExpand] = useState<boolean>(setting.extraNetworkSidebarExpand);
|
const [expand, setExpand] = useState<boolean>(setting.extraNetworkSidebarExpand);
|
||||||
const [size, setSize] = useState<number>(setting?.extraNetworkCardSize || 86);
|
const [size, setSize] = useState<number>(setting?.extraNetworkCardSize || 86);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mobile) setExpand(false);
|
if (mobile) setExpand(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<DraggablePanel
|
<DraggablePanel
|
||||||
defaultSize={{ width: setting.extraNetworkSidebarWidth }}
|
defaultSize={{width: setting.extraNetworkSidebarWidth}}
|
||||||
expand={expand}
|
expand={expand}
|
||||||
minWidth={setting.extraNetworkSidebarWidth}
|
minWidth={setting.extraNetworkSidebarWidth}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onExpandChange={setExpand}
|
onExpandChange={setExpand}
|
||||||
pin={mode === 'fixed'}
|
pin={mode === 'fixed'}
|
||||||
placement="right"
|
placement="right"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<SidebarView size={size}>{children}</SidebarView>
|
<SidebarView size={size}>{children}</SidebarView>
|
||||||
<Footer>
|
<Footer>
|
||||||
<ZoomInOutlined />
|
<ZoomInOutlined />
|
||||||
<ZoomSlider defaultValue={size} max={256} min={64} onChange={setSize} step={8} />
|
<ZoomSlider defaultValue={size} max={256} min={64} onChange={setSize} step={8} />
|
||||||
</Footer>
|
</Footer>
|
||||||
</View>
|
</View>
|
||||||
</DraggablePanel>
|
</DraggablePanel>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
@ -1,77 +1,33 @@
|
|||||||
import type {MenuProps} from 'antd';
|
import {TabsNav, type TabsNavProps} from '@lobehub/ui';
|
||||||
import {Menu} from 'antd';
|
|
||||||
import {memo, useEffect, useState} from 'react';
|
import {memo, useEffect, useState} from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
/******************************************************
|
const getNavButtons: HTMLButtonElement[] | any = () =>
|
||||||
*********************** Style *************************
|
gradioApp().querySelectorAll('#tabs > .tab-nav:first-child button') || [];
|
||||||
******************************************************/
|
|
||||||
|
|
||||||
const NavBar = styled(Menu)`
|
const onChange: TabsNavProps['onChange'] = (activeKey) => {
|
||||||
overflow: hidden;
|
const buttons = getNavButtons();
|
||||||
flex: 1;
|
buttons[Number(activeKey)]?.click();
|
||||||
line-height: 1;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
.ant-menu-overflow-item {
|
|
||||||
padding: 8px 12px;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
border-radius: 4px !important;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-fill-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ant-menu-item-selected {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/******************************************************
|
|
||||||
************************* Dom *************************
|
|
||||||
******************************************************/
|
|
||||||
|
|
||||||
const onClick: MenuProps['onClick'] = (e: any) => {
|
|
||||||
const buttons: HTMLButtonElement[] | any = gradioApp().querySelectorAll(
|
|
||||||
'#tabs > .tab-nav:first-child button',
|
|
||||||
);
|
|
||||||
buttons[Number(e.key)]?.click();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Nav = memo(() => {
|
const Nav = memo(() => {
|
||||||
const [items, setItems] = useState<MenuProps['items']>([]);
|
const [items, setItems] = useState<TabsNavProps['items']>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onUiLoaded(() => {
|
onUiLoaded(() => {
|
||||||
const buttons = gradioApp().querySelectorAll('#tabs > .tab-nav:first-child button');
|
const buttons = getNavButtons();
|
||||||
const list: MenuProps['items'] | any = [];
|
const list: TabsNavProps['items'] | any = [];
|
||||||
buttons.forEach((button: HTMLButtonElement | any, index) => {
|
buttons.forEach((button: HTMLButtonElement | any, index: number) => {
|
||||||
button.id = `kitchen-nav-${index}`;
|
button.id = `kitchen-nav-${index}`;
|
||||||
list.push({
|
list.push({
|
||||||
key: String(index),
|
key: String(index),
|
||||||
label: button.textContent,
|
label: button.textContent,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setItems(list);
|
setItems(list.filter(Boolean));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return <TabsNav items={items} onChange={onChange} />;
|
||||||
<NavBar
|
|
||||||
defaultActiveFirst
|
|
||||||
defaultSelectedKeys={['0']}
|
|
||||||
items={items}
|
|
||||||
mode="horizontal"
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Nav;
|
export default Nav;
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import {BoldOutlined, GithubOutlined} from '@ant-design/icons';
|
import {BoldOutlined, GithubOutlined} from '@ant-design/icons';
|
||||||
|
import {Header as H} from '@lobehub/ui';
|
||||||
import {Button, Modal, Space} from 'antd';
|
import {Button, Modal, Space} from 'antd';
|
||||||
import {useResponsive} from 'antd-style';
|
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import {type ReactNode, memo, useCallback, useEffect, useState} from 'react';
|
import {type ReactNode, memo, useCallback, useState} from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
import {shallow} from 'zustand/shallow';
|
import {shallow} from 'zustand/shallow';
|
||||||
|
|
||||||
import {DraggablePanel} from '@/components';
|
|
||||||
import {useAppStore} from '@/store';
|
import {useAppStore} from '@/store';
|
||||||
|
|
||||||
import Giscus from './Giscus';
|
import Giscus from './Giscus';
|
||||||
@ -15,39 +13,14 @@ import Nav from './Nav';
|
|||||||
import Setting from './Setting';
|
import Setting from './Setting';
|
||||||
import {civitaiLogo, themeIcon} from './style';
|
import {civitaiLogo, themeIcon} from './style';
|
||||||
|
|
||||||
/******************************************************
|
|
||||||
*********************** Style *************************
|
|
||||||
******************************************************/
|
|
||||||
|
|
||||||
const HeaderView = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
height: var(--fill-available);
|
|
||||||
padding: 16px 24px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
/******************************************************
|
|
||||||
************************* Dom *************************
|
|
||||||
******************************************************/
|
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = memo<HeaderProps>(({children}) => {
|
const Header = memo<HeaderProps>(({children}) => {
|
||||||
const [themeMode] = useAppStore((st) => [st.themeMode], shallow);
|
const [themeMode] = useAppStore((st) => [st.themeMode], shallow);
|
||||||
const {mobile} = useResponsive();
|
|
||||||
const [expand, setExpand] = useState<boolean>(true);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mobile) setExpand(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSetTheme = useCallback(() => {
|
const handleSetTheme = useCallback(() => {
|
||||||
const theme = themeMode === 'light' ? 'dark' : 'light';
|
const theme = themeMode === 'light' ? 'dark' : 'light';
|
||||||
const gradioURL = qs.parseUrl(window.location.href);
|
const gradioURL = qs.parseUrl(window.location.href);
|
||||||
@ -61,25 +34,8 @@ const Header = memo<HeaderProps>(({children}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DraggablePanel
|
<H
|
||||||
defaultSize={{height: 'auto'}}
|
actions={
|
||||||
expand={expand}
|
|
||||||
minHeight={64}
|
|
||||||
onExpandChange={setExpand}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<HeaderView id="header" style={{flexDirection: mobile ? 'column' : 'row'}}>
|
|
||||||
<a
|
|
||||||
href="https://github.com/canisminor1990/sd-webui-kitchen-theme"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Logo themeMode={themeMode} />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<Nav />
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Space.Compact>
|
<Space.Compact>
|
||||||
<a href="https://civitai.com/" rel="noreferrer" target="_blank">
|
<a href="https://civitai.com/" rel="noreferrer" target="_blank">
|
||||||
<Button icon={civitaiLogo} title="Civitai" />
|
<Button icon={civitaiLogo} title="Civitai" />
|
||||||
@ -95,8 +51,23 @@ const Header = memo<HeaderProps>(({children}) => {
|
|||||||
<Setting />
|
<Setting />
|
||||||
<Button icon={themeIcon[themeMode]} onClick={handleSetTheme} title="Switch Theme" />
|
<Button icon={themeIcon[themeMode]} onClick={handleSetTheme} title="Switch Theme" />
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</HeaderView>
|
}
|
||||||
</DraggablePanel>
|
logo={
|
||||||
|
<a
|
||||||
|
href="https://github.com/canisminor1990/sd-webui-kitchen-theme"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo themeMode={themeMode} />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
nav={
|
||||||
|
<>
|
||||||
|
<Nav />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
footer={false}
|
footer={false}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { useResponsive } from 'antd-style';
|
import {DraggablePanel} from '@lobehub/ui';
|
||||||
import { type CSSProperties, type ReactNode, memo, useEffect, useState } from 'react';
|
import {useResponsive} from 'antd-style';
|
||||||
|
import {type CSSProperties, type ReactNode, memo, useEffect, useState} from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { shallow } from 'zustand/shallow';
|
import {shallow} from 'zustand/shallow';
|
||||||
|
|
||||||
import { DraggablePanel } from '@/components';
|
import {useAppStore} from '@/store';
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
|
|
||||||
import PromptGroup from './PromptGroup';
|
import PromptGroup from './PromptGroup';
|
||||||
|
|
||||||
@ -29,37 +29,37 @@ interface SidebarProps {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar = memo<SidebarProps>(({ children, loading, style }) => {
|
const Sidebar = memo<SidebarProps>(({children, loading, style}) => {
|
||||||
const [setting] = useAppStore((st) => [st.setting], shallow);
|
const [setting] = useAppStore((st) => [st.setting], shallow);
|
||||||
const [mode] = useState<'fixed' | 'float'>(setting.sidebarFixedMode);
|
const [mode] = useState<'fixed' | 'float'>(setting.sidebarFixedMode);
|
||||||
const { mobile } = useResponsive();
|
const {mobile} = useResponsive();
|
||||||
const [expand, setExpand] = useState<boolean>(setting.sidebarExpand);
|
const [expand, setExpand] = useState<boolean>(setting.sidebarExpand);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mobile) setExpand(false);
|
if (mobile) setExpand(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DraggablePanel
|
<DraggablePanel
|
||||||
defaultSize={{ width: setting.sidebarWidth }}
|
defaultSize={{width: setting.sidebarWidth}}
|
||||||
expand={expand}
|
expand={expand}
|
||||||
minWidth={setting.sidebarWidth}
|
minWidth={setting.sidebarWidth}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onExpandChange={setExpand}
|
onExpandChange={setExpand}
|
||||||
pin={mode === 'fixed'}
|
pin={mode === 'fixed'}
|
||||||
placement="left"
|
placement="left"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SidebarView>
|
<SidebarView>
|
||||||
{!loading && <PromptGroup />}
|
{!loading && <PromptGroup />}
|
||||||
{children}
|
{children}
|
||||||
</SidebarView>
|
</SidebarView>
|
||||||
</DraggablePanel>
|
</DraggablePanel>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import {ThemeProvider, setupStyled} from 'antd-style';
|
import {ThemeProvider} from '@lobehub/ui';
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import {memo, useEffect, useState} from 'react';
|
import {memo, useEffect, useState} from 'react';
|
||||||
import {createRoot} from 'react-dom/client';
|
import {createRoot} from 'react-dom/client';
|
||||||
// @ts-ignore
|
|
||||||
import ReactFontLoader from 'react-font-loader';
|
|
||||||
import {ThemeContext} from 'styled-components';
|
|
||||||
import {shallow} from 'zustand/shallow';
|
import {shallow} from 'zustand/shallow';
|
||||||
|
|
||||||
import {useIsDarkMode} from '@/components/theme/useIsDarkMode';
|
import {useIsDarkMode} from '@/components/theme/useIsDarkMode';
|
||||||
@ -15,11 +12,8 @@ import {useAppStore} from '@/store';
|
|||||||
import '@/theme/style.less';
|
import '@/theme/style.less';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import GlobalStyle from './GlobalStyle';
|
|
||||||
import {baseToken} from './style';
|
|
||||||
|
|
||||||
const Root = memo(() => {
|
const Root = memo(() => {
|
||||||
setupStyled({ThemeContext});
|
|
||||||
const [onSetThemeMode, onInit] = useAppStore((st) => [st.onSetThemeMode, st.onInit], shallow);
|
const [onSetThemeMode, onInit] = useAppStore((st) => [st.onSetThemeMode, st.onInit], shallow);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const [appearance, setAppearance] = useState<'light' | 'dark'>('light');
|
const [appearance, setAppearance] = useState<'light' | 'dark'>('light');
|
||||||
@ -47,10 +41,7 @@ const Root = memo(() => {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
return (
|
return (
|
||||||
<ThemeProvider appearance={appearance} theme={{token: baseToken}}>
|
<ThemeProvider themeMode={appearance}>
|
||||||
<GlobalStyle />
|
|
||||||
<ReactFontLoader url="https://raw.githubusercontent.com/IKKI2000/harmonyos-fonts/main/css/harmonyos_sans.css" />
|
|
||||||
<ReactFontLoader url="https://raw.githubusercontent.com/IKKI2000/harmonyos-fonts/main/css/harmonyos_sans_sc.css" />
|
|
||||||
<App />
|
<App />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user