2025-02-11 16:13:13 +00:00

440 lines
8.7 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from "react"
import config from "@config"
import classnames from "classnames"
import { Translation } from "react-i18next"
import { motion, AnimatePresence } from "motion/react"
import { Menu, Avatar, Dropdown, Tag } from "antd"
import Drawer from "@layouts/components/drawer"
import { Icons } from "@components/Icons"
import GenerateMenuItems from "@utils/generateMenuItems"
import TopMenuItems from "@config/sidebar/TopItems"
import BottomMenuItems from "@config/sidebar/BottomItems"
import ItemsClickHandlers from "./itemClickHandlers"
import "./index.less"
const ActionMenuItems = [
{
key: "profile",
label: (
<>
<Icons.FiUser />
<Translation>{(t) => t("Profile")}</Translation>
</>
),
},
{
key: "studio",
label: (
<>
<Icons.MdHardware />
<Translation>{(t) => t("Studio")}</Translation>
</>
),
},
{
key: "addons",
label: (
<>
<Icons.FiBox />
<Translation>{(t) => t("Addons")}</Translation>
</>
),
},
{
type: "divider",
},
{
key: "switch_account",
label: (
<>
<Icons.MdSwitchAccount />
<Translation>{(t) => t("Switch account")}</Translation>
</>
),
},
{
key: "logout",
label: (
<>
<Icons.FiLogOut />
<Translation>{(t) => t("Logout")}</Translation>
</>
),
danger: true,
},
]
export default class Sidebar extends React.Component {
state = {
visible: false,
expanded: false,
topItems: GenerateMenuItems(TopMenuItems),
bottomItems: GenerateMenuItems(BottomMenuItems),
selectedMenuItem: null,
navigationRender: null,
}
sidebarRef = React.createRef()
collapseDebounce = null
interface = (window.app.layout.sidebar = {
toggleVisibility: (to) => {
if (to === false) {
this.interface.toggleExpanded(false, {
instant: true,
})
}
this.setState({ visible: to ?? !this.state.visible })
},
toggleExpanded: async (
to,
{ instant = false, isDropdown = false } = {},
) => {
to = to ?? !this.state.expanded
if (this.collapseDebounce) {
clearTimeout(this.collapseDebounce)
this.collapseDebounce = null
}
if (
(to === false) & (this.state.dropdownOpen === true) &&
isDropdown === true
) {
// FIXME: This is a walkaround for a bug in antd, causing when dropdown set to close, item click event is not fired
// The desing defines when sidebar should be collapsed, dropdown should be closed, but in this case, gonna to keep it open untils dropdown is closed
//this.setState({ dropdownOpen: false })
return false
}
if (to === false) {
if (instant === false) {
await new Promise((resolve) =>
setTimeout(
resolve,
window.app.cores.settings.get(
"sidebar.collapse_delay_time",
) ?? 500,
),
)
}
}
this.setState({ expanded: to })
app.eventBus.emit("sidebar.expanded", to)
},
isVisible: () => this.state.visible,
isExpanded: () => this.state.expanded,
renderNavigationBar: (component, options) => {
this.setState({
navigationRender: {
component,
options,
},
})
},
updateMenuItemProps: this.updateBottomItemProps,
addMenuItem: this.addMenuItem,
removeMenuItem: this.removeMenuItem,
})
events = {
"router.navigate": (path) => {
this.calculateSelectedMenuItem(path)
},
}
componentDidMount = async () => {
this.calculateSelectedMenuItem(window.location.pathname)
for (const [event, handler] of Object.entries(this.events)) {
app.eventBus.on(event, handler)
}
setTimeout(() => {
this.interface.toggleVisibility(true)
}, 10)
}
componentWillUnmount = () => {
for (const [event, handler] of Object.entries(this.events)) {
app.eventBus.off(event, handler)
}
delete app.layout.sidebar
}
calculateSelectedMenuItem = (path) => {
const items = [...this.state.topItems, ...this.state.bottomItems]
this.setState({
selectedMenuItem: items.find((item) =>
String(item.path).includes(path),
),
})
}
addMenuItem = (group, item) => {
group = this.getMenuItemGroupStateKey(group)
if (!group) {
throw new Error("Invalid group")
}
const newItems = [...this.state[group], item]
this.setState({
[group]: newItems,
})
return newItems
}
removeMenuItem = (group, id) => {
group = this.getMenuItemGroupStateKey(group)
if (!group) {
throw new Error("Invalid group")
}
const newItems = this.state[group].filter((item) => item.id !== id)
this.setState({
[group]: newItems,
})
return newItems
}
updateBottomItemProps = (group, id, newProps) => {
group = this.getMenuItemGroupStateKey(group)
if (!group) {
throw new Error("Invalid group")
}
let updatedValue = this.state[group]
updatedValue = updatedValue.map((item) => {
if (item.id === id) {
item.props = {
...item.props,
...newProps,
}
}
})
this.setState({
[group]: updatedValue,
})
return updatedValue
}
getMenuItemGroupStateKey = (group) => {
switch (group) {
case "top": {
return "topItems"
}
case "bottom": {
return "bottomItems"
}
default: {
return null
}
}
}
injectUserItems(items = []) {
if (app.userData) {
items.push({
key: "account",
ignore_click: "true",
className: "user_avatar",
label: (
<Dropdown
menu={{
items: ActionMenuItems,
onClick: this.onClickDropdownItem,
}}
autoFocus
placement="top"
trigger={["click"]}
>
<Avatar shape="square" src={app.userData?.avatar} />
</Dropdown>
),
})
} else {
items.push({
key: "login",
label: <Translation>{(t) => t("Login")}</Translation>,
icon: <Icons.FiLogIn />,
})
}
return items
}
handleClick = (e) => {
if (e.item.props.ignore_click === "true") {
return
}
if (e.item.props.override_event) {
return app.eventBus.emit(
e.item.props.override_event,
e.item.props.override_event_props,
)
}
if (typeof e.key === "undefined") {
app.eventBus.emit("invalidSidebarKey", e)
return false
}
if (typeof ItemsClickHandlers[e.key] === "function") {
return ItemsClickHandlers[e.key](e)
}
app.cores.sfx.play("sidebar.switch_tab")
let item = [...this.state.topItems, ...this.state.bottomItems].find(
(item) => item.id === e.key,
)
return app.location.push(`/${item.path ?? e.key}`, 150)
}
onMouseEnter = () => {
if (!this.state.visible || app.layout.drawer.isMaskVisible()) {
return false
}
return this.interface.toggleExpanded(true)
}
handleMouseLeave = () => {
if (!this.state.visible) {
return false
}
return this.interface.toggleExpanded(false)
}
onDropdownOpenChange = (to) => {
// this is another walkaround for a bug in antd, causing when dropdown set to close, item click event is not fired
if (!to && this.state.expanded) {
this.interface.toggleExpanded(false, true)
}
this.setState({ dropdownOpen: to })
}
onClickDropdownItem = (item) => {
const handler = ItemsClickHandlers[item.key]
if (typeof handler === "function") {
handler()
}
}
render() {
const selectedKeyId = this.state.selectedMenuItem?.id
return (
<div
className={classnames("app_sidebar_wrapper", {
["hidden"]: !this.state.visible,
})}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{window.__TAURI__ && navigator.platform.includes("Mac") && (
<div className="app_sidebar_tauri" data-tauri-drag-region />
)}
<AnimatePresence mode="popLayout">
{this.state.visible && (
<motion.div
className={classnames("app_sidebar", {
["expanded"]: this.state.expanded,
})}
ref={this.sidebarRef}
initial={{
x: -500,
}}
animate={{
x: 0,
}}
exit={{
x: -500,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
<div className="app_sidebar_header">
<div className="app_sidebar_header_logo">
<img
src={config.logo?.alt}
onClick={() => app.navigation.goMain()}
/>
<Tag>αlpha</Tag>
</div>
</div>
<div
key="menu"
className="app_sidebar_menu_wrapper"
>
<Menu
mode="inline"
onClick={this.handleClick}
selectedKeys={[selectedKeyId]}
items={this.state.topItems}
/>
</div>
<div
key="bottom"
className={classnames(
"app_sidebar_menu_wrapper",
"bottom",
)}
>
<Menu
mode="inline"
onClick={this.handleClick}
items={[
...this.state.bottomItems,
...this.injectUserItems(),
]}
selectedKeys={[selectedKeyId]}
/>
</div>
</motion.div>
)}
</AnimatePresence>
<Drawer />
</div>
)
}
}