feat: support changing theme in antd 5 (#1430)

* feat: add global theme change function

* feat: add icons

* feat: in app theme changer

* feat: use antd built-in themes

* fix: multiple styling problem

* fix: theme init from localstorage

* feat: dark mode footer

* feat: casdoor logo color theme

* feat: select theme box icon adaptive to theme

* fix: menu bar style

* fix: language box style

* feat: translation

* feat: update translation of select theme box without reloading

* fix: mobile view

* fix: better structured select theme box

* feat: add compact icon

* fix: redundant theme fetch

* fix: redundant theme fetch

* fix: various styling problems
This commit is contained in:
Chell
2022-12-29 15:30:37 +01:00
committed by GitHub
parent c952c2f2f4
commit f465fc6ce0
12 changed files with 252 additions and 87 deletions

View File

@ -17,7 +17,7 @@ import "./App.less";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {BarsOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons"; import {BarsOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd"; import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result, theme} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom"; import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage"; import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage"; import OrganizationEditPage from "./OrganizationEditPage";
@ -69,6 +69,7 @@ import SystemInfo from "./SystemInfo";
import AdapterListPage from "./AdapterListPage"; import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage"; import AdapterEditPage from "./AdapterEditPage";
import {withTranslation} from "react-i18next"; import {withTranslation} from "react-i18next";
import SelectThemeBox from "./SelectThemeBox";
const {Header, Footer, Content} = Layout; const {Header, Footer, Content} = Layout;
@ -81,6 +82,8 @@ class App extends Component {
account: undefined, account: undefined,
uri: null, uri: null,
menuVisible: false, menuVisible: false,
themeAlgorithm: null,
logo: null,
}; };
Setting.initServerUrl(); Setting.initServerUrl();
@ -95,6 +98,16 @@ class App extends Component {
this.getAccount(); this.getAccount();
} }
componentDidMount() {
localStorage.getItem("theme") ?
this.setState({"themeAlgorithm": this.getTheme()}) : this.setState({"themeAlgorithm": theme.defaultAlgorithm});
this.setState({"logo": Setting.getLogo(localStorage.getItem("theme"))});
addEventListener("themeChange", (e) => {
this.setState({"themeAlgorithm": this.getTheme()});
this.setState({"logo": Setting.getLogo(localStorage.getItem("theme"))});
});
}
componentDidUpdate() { componentDidUpdate() {
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
const uri = location.pathname; const uri = location.pathname;
@ -184,6 +197,10 @@ class App extends Component {
return ""; return "";
} }
getTheme() {
return Setting.Themes.find(t => t.key === localStorage.getItem("theme"))["style"];
}
setLanguage(account) { setLanguage(account) {
const language = account?.language; const language = account?.language;
if (language !== "" && language !== i18next.language) { if (language !== "" && language !== i18next.language) {
@ -465,54 +482,62 @@ class App extends Component {
renderRouter() { renderRouter() {
return ( return (
<div> <ConfigProvider theme={{
<Switch> token: {
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} /> colorPrimary: "rgb(89,54,213)",
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} /> colorInfo: "rgb(89,54,213)",
<Route exact path="/" render={(props) => this.renderLoginIfNotLoggedIn(<HomePage account={this.state.account} {...props} />)} /> },
<Route exact path="/account" render={(props) => this.renderLoginIfNotLoggedIn(<AccountPage account={this.state.account} {...props} />)} /> algorithm: this.state.themeAlgorithm,
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} /> }}>
<Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} {...props} />)} /> <div>
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} /> <Switch>
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} /> <Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} /> <Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)} /> <Route exact path="/" render={(props) => this.renderLoginIfNotLoggedIn(<HomePage account={this.state.account} {...props} />)} />
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)} /> <Route exact path="/account" render={(props) => this.renderLoginIfNotLoggedIn(<AccountPage account={this.state.account} {...props} />)} />
<Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)} /> <Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)} /> <Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)} /> <Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)} /> <Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} /> <Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} />
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} /> <Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} /> <Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} /> <Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} /> <Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} /> <Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} /> <Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)} />
{/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/} <Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} /> <Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} /> <Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} /> <Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)} /> <Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} /> <Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} /> <Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} /> {/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/}
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} /> <Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} /> <Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} /> <Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} />
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} /> <Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} /> <Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} /> <Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} /> <Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} /> <Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} /> <Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} /> <Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} /> <Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} /> <Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")} <Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} /> <Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} />
</Switch> <Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
</div> <Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
</div>
</ConfigProvider>
); );
} }
@ -532,30 +557,27 @@ class App extends Component {
if (!Setting.isMobile()) { if (!Setting.isMobile()) {
return ( return (
<Layout id="parent-area"> <Layout id="parent-area">
<Header style={{marginBottom: "3px", paddingInline: 0}}> <Header style={{marginBottom: "3px", paddingInline: 0, backgroundColor: this.state.themeAlgorithm === theme.darkAlgorithm ? "black" : "white"}}>
{ {
Setting.isMobile() ? null : ( Setting.isMobile() ? null : (
<Link to={"/"}> <Link to={"/"}>
<div className="logo" /> <div className="logo" style={{background: `url(${this.state.logo})`}} />
</Link> </Link>
) )
} }
<div> <Menu
<Menu items={this.renderMenu()}
// theme="dark" mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
items={this.renderMenu()} selectedKeys={[`${this.state.selectedMenuKey}`]}
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"} style={{position: "absolute", left: "145px", backgroundColor: this.state.themeAlgorithm === theme.darkAlgorithm ? "black" : "white"}}
selectedKeys={[`${this.state.selectedMenuKey}`]} />
style={{lineHeight: "64px", position: "absolute", left: "145px", right: "200px"}} {
> this.renderAccount()
</Menu> }
{ {this.state.account && <SelectThemeBox themes={this.state.account.organization.themes} />}
this.renderAccount() {this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
}
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</div>
</Header> </Header>
<Content style={{backgroundColor: "#f5f5f5", alignItems: "stretch", display: "flex", flexDirection: "column"}}> <Content style={{alignItems: "stretch", display: "flex", flexDirection: "column"}}>
<Card className="content-warp-card"> <Card className="content-warp-card">
{ {
this.renderRouter() this.renderRouter()
@ -570,11 +592,11 @@ class App extends Component {
} else { } else {
return ( return (
<Layout> <Layout>
<Header style={{padding: "0", marginBottom: "3px"}}> <Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm === theme.darkAlgorithm ? "black" : "white"}}>
{ {
Setting.isMobile() ? null : ( Setting.isMobile() ? null : (
<Link to={"/"}> <Link to={"/"}>
<div className="logo" /> <div className="logo" style={{background: `url(${this.state.logo})`}} />
</Link> </Link>
) )
} }
@ -592,12 +614,11 @@ class App extends Component {
<Button icon={<BarsOutlined />} onClick={this.showMenu} type="text"> <Button icon={<BarsOutlined />} onClick={this.showMenu} type="text">
{i18next.t("general:Menu")} {i18next.t("general:Menu")}
</Button> </Button>
<div style = {{float: "right"}}> {
{ this.renderAccount()
this.renderAccount() }
} {this.state.account && <SelectThemeBox themes={this.state.account.organization.themes} />}
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />} {this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</div>
</Header> </Header>
<Content style={{display: "flex", flexDirection: "column"}} >{ <Content style={{display: "flex", flexDirection: "column"}} >{
this.renderRouter()} this.renderRouter()}
@ -617,12 +638,10 @@ class App extends Component {
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />} {!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
<Footer id="footer" style={ <Footer id="footer" style={
{ {
borderTop: "1px solid #e8e8e8",
backgroundColor: "white",
textAlign: "center", textAlign: "center",
} }
}> }>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={`${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`} /></a> Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
</Footer> </Footer>
</React.Fragment> </React.Fragment>
); );
@ -690,6 +709,7 @@ class App extends Component {
colorPrimary: "rgb(89,54,213)", colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)", colorInfo: "rgb(89,54,213)",
}, },
algorithm: this.state.themeAlgorithm,
}}> }}>
{ {
this.renderPage() this.renderPage()
@ -711,6 +731,7 @@ class App extends Component {
colorPrimary: "rgb(89,54,213)", colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)", colorInfo: "rgb(89,54,213)",
}, },
algorithm: this.state.themeAlgorithm,
}}> }}>
{ {
this.renderPage() this.renderPage()

View File

@ -13,14 +13,12 @@
} }
.App-header { .App-header {
background-color: #282c34;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: white;
} }
.App-link { .App-link {
@ -41,7 +39,6 @@ img {
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
min-height: 100vh; min-height: 100vh;
background-color: #f5f5f5;
} }
.panel-logo { .panel-logo {
@ -55,7 +52,7 @@ img {
background-repeat: no-repeat; background-repeat: no-repeat;
border-radius: 5px; border-radius: 5px;
width: 45px; width: 45px;
height: 65px; height: 100%;
float: right; float: right;
cursor: pointer; cursor: pointer;
@ -64,9 +61,32 @@ img {
} }
} }
.login-form .language-box {
height: 65px;
}
.theme-box {
background: url("@{StaticBaseUrl}/img/muti_language.svg");
background-size: 25px, 25px;
background-position: center !important;
background-repeat: no-repeat !important;
border-radius: 5px;
width: 45px;
height: 100%;
float: right;
cursor: pointer;
&:hover {
background-color: #f5f5f5 !important;
}
}
.rightDropDown { .rightDropDown {
border-radius: 7px;
&:hover { &:hover {
background-color: #f5f5f5; background-color: #f5f5f5;
color: black;
} }
} }
@ -128,3 +148,9 @@ img {
background-size: 100% 100%; background-size: 100% 100%;
background-attachment: fixed; background-attachment: fixed;
} }
.ant-menu-horizontal {
border-bottom: none !important;
margin-right: 30px;
right: 230px;
}

81
web/src/SelectThemeBox.js Normal file
View File

@ -0,0 +1,81 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import * as Setting from "./Setting";
import {Dropdown} from "antd";
import "./App.less";
import i18next from "i18next";
function themeIcon(themeKey) {
return <img width={24} alt={themeKey} src={getLogoURL(themeKey)} />;
}
function getLogoURL(themeKey) {
if (themeKey) {
return Setting.Themes.find(t => t.key === themeKey)["selectThemeLogo"];
} else {
return Setting.Themes.find(t => t.key === localStorage.getItem("theme"))["selectThemeLogo"];
}
}
class SelectThemeBox extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
themes: props.theme ?? ["Default", "Dark", "Compact"],
icon: null,
};
}
items = this.getThemes();
componentDidMount() {
i18next.on("languageChanged", () => {
this.items = this.getThemes();
});
localStorage.getItem("theme") ? this.setState({"icon": getLogoURL()}) : this.setState({"icon": getLogoURL("Default")});
addEventListener("themeChange", (e) => {
this.setState({"icon": getLogoURL()});
});
}
getThemes() {
return Setting.Themes.map((theme) => Setting.getItem(i18next.t(`general:${theme.label}`), theme.key, themeIcon(theme.key)));
}
getOrganizationThemes(themes) {
const select = [];
for (const theme of themes) {
this.items.map((item, index) => item.key === theme ? select.push(item) : null);
}
return select;
}
render() {
const themeItems = this.getOrganizationThemes(this.state.themes);
const onClick = (e) => {
Setting.setTheme(e.key);
};
return (
<Dropdown menu={{items: themeItems, onClick}} >
<div className="theme-box" style={{display: themeItems.length === 0 ? "none" : null, background: `url(${this.state.icon})`, ...this.props.style}} />
</Dropdown>
);
}
}
export default SelectThemeBox;

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Tag, Tooltip, message} from "antd"; import {Tag, Tooltip, message, theme} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons"; import {QuestionCircleTwoTone} from "@ant-design/icons";
import {isMobile as isMobileDevice} from "react-device-detect"; import {isMobile as isMobileDevice} from "react-device-detect";
import "./i18n"; import "./i18n";
@ -43,6 +43,13 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
{label: "Русский", key: "ru", country: "RU", alt: "Русский"}, {label: "Русский", key: "ru", country: "RU", alt: "Русский"},
]; ];
const {defaultAlgorithm, darkAlgorithm, compactAlgorithm} = theme;
export const Themes = [{label: "Dark", key: "Dark", style: darkAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/dark.svg`},
{label: "Compact", key: "Compact", style: compactAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/compact.svg`},
{label: "Default", key: "Default", style: defaultAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/light.svg`},
];
export const OtherProviderInfo = { export const OtherProviderInfo = {
SMS: { SMS: {
"Aliyun SMS": { "Aliyun SMS": {
@ -562,6 +569,14 @@ export function getAvatarColor(s) {
return colorList[hash % 4]; return colorList[hash % 4];
} }
export function getLogo(theme) {
if (theme === "Dark") {
return `${StaticBaseUrl}/img/casdoor-logo_1185x256_dark.png`;
} else {
return `${StaticBaseUrl}/img/casdoor-logo_1185x256.png`;
}
}
export function getLanguageText(text) { export function getLanguageText(text) {
if (!text.includes("|")) { if (!text.includes("|")) {
return text; return text;
@ -587,6 +602,11 @@ export function setLanguage(language) {
i18next.changeLanguage(language); i18next.changeLanguage(language);
} }
export function setTheme(themeKey) {
localStorage.setItem("theme", themeKey);
dispatchEvent(new Event("themeChange"));
}
export function getAcceptLanguage() { export function getAcceptLanguage() {
if (i18next.language === null || i18next.language === "") { if (i18next.language === null || i18next.language === "") {
return "en;q=0.9,en;q=0.8"; return "en;q=0.9,en;q=0.8";

View File

@ -28,7 +28,7 @@ code {
.logo { .logo {
background: url("https://cdn.casbin.org/img/casdoor-logo_1185x256.png"); background: url("https://cdn.casbin.org/img/casdoor-logo_1185x256.png");
background-size: 130px, 27px; background-size: 130px, 27px !important;
width: 130px; width: 130px;
height: 27px; height: 27px;
margin: 17px 0 16px 15px; margin: 17px 0 16px 15px;
@ -45,7 +45,3 @@ code {
.ant-list-sm .ant-list-item { .ant-list-sm .ant-list-item {
padding: 2px !important; padding: 2px !important;
} }
.ant-layout-header {
background-color: white !important;
}

View File

@ -156,7 +156,10 @@
"Click to Upload": "Click to Upload", "Click to Upload": "Click to Upload",
"Client IP": "Client-IP", "Client IP": "Client-IP",
"Close": "Close", "Close": "Close",
"Compact": "Compact",
"Created time": "Erstellte Zeit", "Created time": "Erstellte Zeit",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip", "Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Standard Avatar", "Default avatar": "Standard Avatar",

View File

@ -156,7 +156,10 @@
"Click to Upload": "Click to Upload", "Click to Upload": "Click to Upload",
"Client IP": "Client IP", "Client IP": "Client IP",
"Close": "Close", "Close": "Close",
"Compact": "Compact",
"Created time": "Created time", "Created time": "Created time",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip", "Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",

View File

@ -156,7 +156,10 @@
"Click to Upload": "Click to Upload", "Click to Upload": "Click to Upload",
"Client IP": "IP du client", "Client IP": "IP du client",
"Close": "Close", "Close": "Close",
"Compact": "Compact",
"Created time": "Date de création", "Created time": "Date de création",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip", "Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Avatar par défaut", "Default avatar": "Avatar par défaut",

View File

@ -156,7 +156,10 @@
"Click to Upload": "Click to Upload", "Click to Upload": "Click to Upload",
"Client IP": "クライアント IP", "Client IP": "クライアント IP",
"Close": "Close", "Close": "Close",
"Compact": "Compact",
"Created time": "作成日時", "Created time": "作成日時",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip", "Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "デフォルトのアバター", "Default avatar": "デフォルトのアバター",

View File

@ -156,7 +156,10 @@
"Click to Upload": "Click to Upload", "Click to Upload": "Click to Upload",
"Client IP": "Client IP", "Client IP": "Client IP",
"Close": "Close", "Close": "Close",
"Compact": "Compact",
"Created time": "Created time", "Created time": "Created time",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip", "Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",

View File

@ -156,7 +156,10 @@
"Click to Upload": "Нажмите здесь, чтобы загрузить", "Click to Upload": "Нажмите здесь, чтобы загрузить",
"Client IP": "IP клиента", "Client IP": "IP клиента",
"Close": "Close", "Close": "Close",
"Compact": "Compact",
"Created time": "Время создания", "Created time": "Время создания",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip", "Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Аватар по умолчанию", "Default avatar": "Аватар по умолчанию",

View File

@ -156,7 +156,10 @@
"Click to Upload": "点击上传", "Click to Upload": "点击上传",
"Client IP": "客户端IP", "Client IP": "客户端IP",
"Close": "关闭", "Close": "关闭",
"Compact": "紧凑",
"Created time": "创建时间", "Created time": "创建时间",
"Dark": "黑暗",
"Default": "默认",
"Default application": "默认应用", "Default application": "默认应用",
"Default application - Tooltip": "默认应用", "Default application - Tooltip": "默认应用",
"Default avatar": "默认头像", "Default avatar": "默认头像",