mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
440 lines
8.7 KiB
JavaScript
Executable File
440 lines
8.7 KiB
JavaScript
Executable File
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>
|
||
)
|
||
}
|
||
}
|