improve code & performance & desing of sidebar

This commit is contained in:
SrGooglo 2024-09-13 00:00:34 +00:00
parent 0ce6d39a59
commit 105395fe76
6 changed files with 290 additions and 279 deletions

View File

@ -0,0 +1,24 @@
[
{
"id": "search",
"label": "Search",
"icon": "Search"
},
{
"id": "messages",
"label": "Messages",
"icon": "MessageCircle",
"path": "/messages"
},
{
"id": "notifications",
"label": "Notifications",
"icon": "Bell"
},
{
"id": "settings",
"label": "Settings",
"icon": "Settings",
"path": "/settings"
}
]

View File

@ -2,25 +2,25 @@
{ {
"id": "home", "id": "home",
"path": "/", "path": "/",
"title": "Home", "label": "Home",
"icon": "Home" "icon": "Home"
}, },
{ {
"id": "timeline", "id": "timeline",
"path": "/", "path": "/",
"title": "Timeline", "label": "Timeline",
"icon": "MdTag" "icon": "MdTag"
}, },
{ {
"id": "tv", "id": "tv",
"path": "/tv", "path": "/tv",
"title": "Tv", "label": "Tv",
"icon": "Tv" "icon": "Tv"
}, },
{ {
"id": "music", "id": "music",
"path": "/music", "path": "/music",
"title": "Music", "label": "Music",
"icon": "MdMusicNote" "icon": "MdMusicNote"
} }
] ]

View File

@ -6,7 +6,7 @@
top: 0; top: 0;
left: 0; left: 0;
z-index: 500; z-index: 1200;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -17,10 +17,6 @@
height: 100dvh; height: 100dvh;
height: 100vh; height: 100vh;
// &.hidden {
// display: none;
// }
} }
.drawers-mask { .drawers-mask {
@ -29,7 +25,7 @@
top: 0; top: 0;
left: 0; left: 0;
z-index: 500; z-index: 1100;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@ -41,7 +37,7 @@
.drawer { .drawer {
position: relative; position: relative;
z-index: 550; z-index: 1300;
top: 0; top: 0;
left: 0; left: 0;

View File

@ -2,109 +2,22 @@ import React from "react"
import config from "@config" import config from "@config"
import classnames from "classnames" import classnames from "classnames"
import { Translation } from "react-i18next" import { Translation } from "react-i18next"
import { Motion, spring } from "react-motion" import { motion, AnimatePresence } from "framer-motion"
import { Menu, Avatar, Dropdown, Tag, Empty } from "antd" import { Menu, Avatar, Dropdown, Tag } from "antd"
import Drawer from "@layouts/components/drawer" import Drawer from "@layouts/components/drawer"
import { Icons, createIconRender } from "@components/Icons" import { Icons } from "@components/Icons"
import sidebarItems from "@config/sidebar" import GenerateSidebarMenuItems from "@utils/generateSidebarMenuItems"
import TopMenuItems from "@config/sidebar/TopItems"
import BottomMenuItems from "@config/sidebar/BottomItems"
import ItemsClickHandlers from "./itemClickHandlers"
import "./index.less" import "./index.less"
const onClickHandlers = {
apps: () => {
app.controls.openAppsMenu()
},
addons: () => {
window.app.location.push("/addons")
},
studio: () => {
window.app.location.push("/studio")
},
settings: () => {
window.app.navigation.goToSettings()
},
notifications: () => {
window.app.controls.openNotifications()
},
search: () => {
window.app.controls.openSearcher()
},
messages: () => {
window.app.controls.openMessages()
},
create: () => {
window.app.controls.openCreator()
},
profile: () => {
window.app.navigation.goToAccount()
},
login: () => {
window.app.navigation.goAuth()
},
logout: () => {
app.eventBus.emit("app.logout_request")
}
}
const generateTopItems = (extra = []) => {
const items = [...sidebarItems, ...extra]
return items.map((item) => {
return {
id: item.id,
key: item.id,
path: item.path,
icon: createIconRender(item.icon),
label: <Translation>
{t => t(item.title ?? item.id)}
</Translation>,
disabled: item.disabled,
children: item.children,
}
})
}
const BottomMenuDefaultItems = [
{
key: "search",
label: <Translation>
{(t) => t("Search")}
</Translation>,
icon: <Icons.Search />,
},
{
key: "messages",
label: <Translation>
{(t) => t("Messages")}
</Translation>,
icon: <Icons.MessageCircle />,
},
{
key: "notifications",
label: <Translation>
{(t) => t("Notifications")}
</Translation>,
icon: <Icons.Bell />,
},
// {
// key: "apps",
// label: <Translation>
// {(t) => t("Apps")}
// </Translation>,
// icon: <Icons.MdApps />,
// },
{
key: "settings",
label: <Translation>
{(t) => t("Settings")}
</Translation>,
icon: <Icons.Settings />,
}
]
const ActionMenuItems = [ const ActionMenuItems = [
{ {
key: "profile", key: "profile",
@ -162,10 +75,10 @@ export default class Sidebar extends React.Component {
visible: false, visible: false,
expanded: false, expanded: false,
topItems: generateTopItems(), topItems: GenerateSidebarMenuItems(TopMenuItems),
bottomItems: [], bottomItems: GenerateSidebarMenuItems(BottomMenuItems),
lockAutocollapse: false, selectedMenuItem: null,
navigationRender: null, navigationRender: null,
} }
@ -175,9 +88,15 @@ export default class Sidebar extends React.Component {
interface = window.app.layout.sidebar = { interface = window.app.layout.sidebar = {
toggleVisibility: (to) => { toggleVisibility: (to) => {
if (to === false) {
this.interface.toggleExpanded(false, {
instant: true,
})
}
this.setState({ visible: to ?? !this.state.visible }) this.setState({ visible: to ?? !this.state.visible })
}, },
toggleCollapse: (to, force) => { toggleExpanded: async (to, { instant = false, isDropdown = false } = {}) => {
to = to ?? !this.state.expanded to = to ?? !this.state.expanded
if (this.collapseDebounce) { if (this.collapseDebounce) {
@ -185,7 +104,7 @@ export default class Sidebar extends React.Component {
this.collapseDebounce = null this.collapseDebounce = null
} }
if (!to & this.state.dropdownOpen && !force) { 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 // 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 // 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 }) //this.setState({ dropdownOpen: false })
@ -193,18 +112,14 @@ export default class Sidebar extends React.Component {
return false return false
} }
if (!to) { if (to === false) {
if (this.state.lockAutocollapse) { if (instant === false) {
return false await new Promise((resolve) => setTimeout(resolve, window.app.cores.settings.get("sidebar.collapse_delay_time") ?? 500))
} }
this.collapseDebounce = setTimeout(() => {
this.setState({ expanded: to })
}, window.app.cores.settings.get("sidebar.collapse_delay_time") ?? 500)
} else {
this.setState({ expanded: to })
} }
this.setState({ expanded: to })
app.eventBus.emit("sidebar.expanded", to) app.eventBus.emit("sidebar.expanded", to)
}, },
isVisible: () => this.state.visible, isVisible: () => this.state.visible,
@ -217,60 +132,23 @@ export default class Sidebar extends React.Component {
} }
}) })
}, },
updateBottomItemProps: (id, newProps) => { updateMenuItemProps: this.updateBottomItemProps,
let updatedValue = this.state.bottomItems addMenuItem: this.addMenuItem,
removeMenuItem: this.removeMenuItem,
updatedValue = updatedValue.map((item) => {
if (item.id === id) {
item.props = {
...item.props,
...newProps,
}
}
})
this.setState({
bottomItems: updatedValue
})
},
attachBottomItem: (id, children, options) => {
if (!id) {
throw new Error("ID is required")
}
if (!children) {
throw new Error("Children is required")
}
if (this.state.bottomItems.find((item) => item.id === id)) {
throw new Error("Item already exists")
}
let updatedValue = this.state.bottomItems
updatedValue.push({
id,
children,
...options
})
this.setState({
bottomItems: updatedValue
})
},
removeBottomItem: (id) => {
let updatedValue = this.state.bottomItems
updatedValue = updatedValue.filter((item) => item.id !== id)
this.setState({
bottomItems: updatedValue
})
},
} }
events = { events = {
"router.navigate": (path) => {
// recalculate sidebar selected item
const item = [...this.state.topItems, ...this.state.bottomItems].find((item) => item.path === path)
console.log(`Recalculate sidebar selected item: path [${path}]`, item)
this.setState({
selectedMenuItem: item
})
}
} }
componentDidMount = async () => { componentDidMount = async () => {
@ -282,7 +160,7 @@ export default class Sidebar extends React.Component {
this.interface.toggleVisibility(true) this.interface.toggleVisibility(true)
if (app.cores.settings.is("sidebar.collapsable", false)) { if (app.cores.settings.is("sidebar.collapsable", false)) {
this.interface.toggleCollapse(true) this.interface.toggleExpanded(true)
} }
}, 10) }, 10)
} }
@ -292,84 +170,81 @@ export default class Sidebar extends React.Component {
app.eventBus.off(event, handler) app.eventBus.off(event, handler)
} }
//delete app.layout.sidebar delete app.layout.sidebar
} }
handleClick = (e) => { addMenuItem = (group, item) => {
if (e.item.props.ignore_click === "true") { group = this.getMenuItemGroupStateKey(group)
return
if (!group) {
throw new Error("Invalid group")
} }
if (e.item.props.override_event) { const newItems = [...this.state[group], item]
return app.eventBus.emit(e.item.props.override_event, e.item.props.override_event_props)
}
if (typeof e.key === "undefined") { this.setState({
app.eventBus.emit("invalidSidebarKey", e) [group]: newItems
return false })
}
if (typeof onClickHandlers[e.key] === "function") { return newItems
return onClickHandlers[e.key](e)
}
app.cores.sfx.play("sidebar.switch_tab")
const item = this.state.topItems.find((item) => item.id === e.key)
return app.location.push(`/${item.path ?? e.key}`, 150)
} }
onMouseEnter = (event) => { removeMenuItem = (group, id) => {
if (!this.state.visible) return group = this.getMenuItemGroupStateKey(group)
if (window.app.cores.settings.is("sidebar.collapsable", false)) { if (!group) {
if (!this.state.expanded) { throw new Error("Invalid group")
this.interface.toggleCollapse(true) }
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,
}
} }
})
return this.setState({
} [group]: updatedValue
})
// do nothing if is mask visible return updatedValue
if (app.layout.drawer.isMaskVisible()) {
return false
}
this.interface.toggleCollapse(true)
} }
handleMouseLeave = (event) => { getMenuItemGroupStateKey = (group) => {
if (!this.state.visible) return switch (group) {
case "top": {
if (window.app.cores.settings.is("sidebar.collapsable", false)) return return "topItems"
}
this.interface.toggleCollapse(false) case "bottom": {
} return "bottomItems"
}
onDropdownOpenChange = (to) => { default: {
// this is another walkaround for a bug in antd, causing when dropdown set to close, item click event is not fired return null
if (!to && this.state.expanded) { }
this.interface.toggleCollapse(false, true)
}
this.setState({ dropdownOpen: to })
}
onClickDropdownItem = (item) => {
const handler = onClickHandlers[item.key]
if (typeof handler === "function") {
handler()
} }
} }
getBottomItems = () => { injectUserItems(items = []) {
const items = [
...BottomMenuDefaultItems,
...this.state.bottomItems,
]
if (app.userData) { if (app.userData) {
items.push({ items.push({
key: "account", key: "account",
@ -387,9 +262,8 @@ export default class Sidebar extends React.Component {
<Avatar shape="square" src={app.userData?.avatar} /> <Avatar shape="square" src={app.userData?.avatar} />
</Dropdown>, </Dropdown>,
}) })
}
if (!app.userData) { } else {
items.push({ items.push({
key: "login", key: "login",
label: <Translation> label: <Translation>
@ -402,43 +276,123 @@ export default class Sidebar extends React.Component {
return items 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) {
return false
}
if (window.app.cores.settings.is("sidebar.collapsable", false)) {
if (!this.state.expanded) {
this.interface.toggleExpanded(true)
}
return false
}
// do nothing if is mask visible
if (app.layout.drawer.isMaskVisible()) {
return false
}
this.interface.toggleExpanded(true)
}
handleMouseLeave = () => {
if (!this.state.visible) {
return false
}
if (window.app.cores.settings.is("sidebar.collapsable", false)) {
return false
}
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 = onClickHandlers[item.key]
if (typeof handler === "function") {
handler()
}
}
render() { render() {
const defaultSelectedKey = window.location.pathname.replace("/", "") const selectedKeyId = this.state.selectedMenuItem?.id ?? "home"
return <Motion style={{ return <div
x: spring(!this.state.visible ? 100 : 0), className="app_sidebar_wrapper"
}}> onMouseEnter={this.onMouseEnter}
{({ x }) => { onMouseLeave={this.handleMouseLeave}
return <div >
className={classnames( {
"app_sidebar_wrapper", window.__TAURI__ && navigator.platform.includes("Mac") && <div
{ className="app_sidebar_tauri"
visible: this.state.visible, data-tauri-drag-region
} />
)} }
style={{
transform: `translateX(-${x}%)`,
}}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{
window.__TAURI__ && navigator.platform.includes("Mac") && <div
className="app_sidebar_tauri"
data-tauri-drag-region
/>
}
<div <AnimatePresence
mode="popLayout"
>
{
this.state.visible && <motion.div
className={classnames( className={classnames(
"app_sidebar", "app_sidebar",
{ {
["expanded"]: this.state.visible && this.state.expanded, ["expanded"]: this.state.expanded,
["hidden"]: !this.state.visible,
} }
) )}
}
ref={this.sidebarRef} ref={this.sidebarRef}
initial={{
x: -500
}}
animate={{
x: 0,
}}
exit={{
x: -500
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20
}}
layout
> >
<div className="app_sidebar_header"> <div className="app_sidebar_header">
<div className="app_sidebar_header_logo"> <div className="app_sidebar_header_logo">
@ -455,9 +409,8 @@ export default class Sidebar extends React.Component {
<Menu <Menu
mode="inline" mode="inline"
onClick={this.handleClick} onClick={this.handleClick}
defaultSelectedKeys={[defaultSelectedKey]} selectedKeys={[selectedKeyId]}
items={this.state.topItems} items={this.state.topItems}
selectable
/> />
</div> </div>
@ -469,17 +422,17 @@ export default class Sidebar extends React.Component {
)} )}
> >
<Menu <Menu
selectable={false}
mode="inline" mode="inline"
onClick={this.handleClick} onClick={this.handleClick}
items={this.getBottomItems()} items={[...this.state.bottomItems, ...this.injectUserItems()]}
selectedKeys={[selectedKeyId]}
/> />
</div> </div>
</div> </motion.div>
}
</AnimatePresence>
<Drawer /> <Drawer />
</div> </div >
}}
</Motion>
} }
} }

View File

@ -25,9 +25,7 @@
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
&.visible { padding: 10px;
padding: 10px;
}
} }
.app_sidebar { .app_sidebar {
@ -47,7 +45,7 @@
padding: 10px 0; padding: 10px 0;
transition: all 150ms ease-in-out; transition: width 150ms ease-in-out;
background-color: var(--sidebar-background-color); background-color: var(--sidebar-background-color);
@ -113,8 +111,16 @@
user-select: none; user-select: none;
--webkit-user-select: none; --webkit-user-select: none;
width: 80%; width: fit-content;
max-height: 40px; max-height: 46px;
transition: all 150ms ease-in-out;
&:hover {
cursor: pointer;
scale: 1.1;
filter: drop-shadow(0 0 5px var(--shadow-color));
}
} }
} }
} }

View File

@ -0,0 +1,32 @@
export default {
apps: () => {
app.controls.openAppsMenu()
},
addons: () => {
window.app.location.push("/addons")
},
studio: () => {
window.app.location.push("/studio")
},
settings: () => {
window.app.navigation.goToSettings()
},
notifications: () => {
window.app.controls.openNotifications()
},
search: () => {
window.app.controls.openSearcher()
},
create: () => {
window.app.controls.openCreator()
},
profile: () => {
window.app.navigation.goToAccount()
},
login: () => {
window.app.navigation.goAuth()
},
logout: () => {
app.eventBus.emit("app.logout_request")
}
}