mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
improve layout for mobile & use of top-bar
This commit is contained in:
parent
11f978cca9
commit
c52834c0c8
@ -17,6 +17,7 @@
|
||||
"backgroundPosition": "center",
|
||||
"backgroundRepeat": "no-repeat",
|
||||
"backgroundAttachment": "fixed",
|
||||
"top-bar-height": "52px",
|
||||
"compact-mode": false
|
||||
},
|
||||
"defaultVariant": "dark",
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default [
|
||||
{
|
||||
path: "/login",
|
||||
useLayout: "none",
|
||||
useLayout: "minimal",
|
||||
public: true
|
||||
},
|
||||
{
|
||||
@ -46,12 +46,12 @@ export default [
|
||||
},
|
||||
{
|
||||
path: "/landing/*",
|
||||
useLayout: "blank",
|
||||
useLayout: "minimal",
|
||||
public: true
|
||||
},
|
||||
{
|
||||
path: "/nfc/*",
|
||||
useLayout: "blank",
|
||||
useLayout: "minimal",
|
||||
public: true
|
||||
}
|
||||
]
|
@ -1,3 +1,4 @@
|
||||
export { default as TopBar } from "./topBar"
|
||||
export { default as BottomBar } from "./bottomBar"
|
||||
export { default as Drawer } from "./drawer"
|
||||
export { default as Sidebar } from "./sidebar"
|
||||
|
157
packages/app/src/components/Layout/topBar/index.jsx
Normal file
157
packages/app/src/components/Layout/topBar/index.jsx
Normal file
@ -0,0 +1,157 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const useLayoutInterface = (namespace, ctx) => {
|
||||
React.useEffect(() => {
|
||||
if (app.layout["namespace"] === "object") {
|
||||
throw new Error(`Layout namespace [${namespace}] already exists`)
|
||||
}
|
||||
|
||||
app.layout[namespace] = ctx
|
||||
}, [])
|
||||
|
||||
return app.layout[namespace]
|
||||
}
|
||||
|
||||
const useDefaultVisibility = (defaultValue) => {
|
||||
const [visible, setVisible] = React.useState(defaultValue ?? false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setVisible(true)
|
||||
}, 10)
|
||||
}, [])
|
||||
|
||||
return [visible, setVisible]
|
||||
}
|
||||
|
||||
export const UseTopBar = (props) => {
|
||||
app.layout.top_bar.render(
|
||||
<React.Fragment>
|
||||
{props.children}
|
||||
</React.Fragment>,
|
||||
props.options)
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
app.layout.top_bar.renderDefault()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
const [visible, setVisible] = useDefaultVisibility()
|
||||
const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] = React.useState(true)
|
||||
const [render, setRender] = React.useState(null)
|
||||
|
||||
useLayoutInterface("top_bar", {
|
||||
toggleVisibility: (to) => {
|
||||
setVisible((prev) => {
|
||||
if (typeof to === undefined) {
|
||||
to = !prev
|
||||
}
|
||||
|
||||
return to
|
||||
})
|
||||
},
|
||||
render: (component, options) => {
|
||||
handleUpdateRender(component, options)
|
||||
},
|
||||
renderDefault: () => {
|
||||
setRender(null)
|
||||
},
|
||||
shouldUseTopBarSpacer: (to) => {
|
||||
app.layout.toogleTopBarSpacer(to)
|
||||
setShouldUseTopBarSpacer(to)
|
||||
}
|
||||
})
|
||||
|
||||
const handleUpdateRender = (...args) => {
|
||||
if (document.startViewTransition) {
|
||||
return document.startViewTransition(() => {
|
||||
updateRender(...args)
|
||||
})
|
||||
}
|
||||
|
||||
return updateRender(...args)
|
||||
}
|
||||
|
||||
const updateRender = (component, options = {}) => {
|
||||
setRender({
|
||||
component,
|
||||
options
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!shouldUseTopBarSpacer) {
|
||||
app.layout.tooglePagePanelSpacer(true)
|
||||
} else {
|
||||
app.layout.tooglePagePanelSpacer(false)
|
||||
}
|
||||
}, [shouldUseTopBarSpacer])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldUseTopBarSpacer) {
|
||||
if (visible) {
|
||||
app.layout.toogleTopBarSpacer(true)
|
||||
} else {
|
||||
app.layout.toogleTopBarSpacer(false)
|
||||
}
|
||||
} else {
|
||||
if (visible) {
|
||||
app.layout.tooglePagePanelSpacer(true)
|
||||
} else {
|
||||
app.layout.tooglePagePanelSpacer(false)
|
||||
}
|
||||
|
||||
app.layout.toogleTopBarSpacer(false)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (render) {
|
||||
setVisible(true)
|
||||
} else {
|
||||
setVisible(false)
|
||||
}
|
||||
}, [render])
|
||||
|
||||
const heightValue = visible ? Number(app.cores.style.defaultVar("top-bar-height").replace("px", "")) : 0
|
||||
|
||||
console.log(render)
|
||||
|
||||
return <Motion style={{
|
||||
y: spring(visible ? 0 : 300,),
|
||||
height: spring(heightValue,),
|
||||
}}>
|
||||
{({ y, height }) => {
|
||||
return <>
|
||||
<div
|
||||
className="top-bar_wrapper"
|
||||
style={{
|
||||
WebkitTransform: `translateY(-${y}px)`,
|
||||
transform: `translateY(-${y}px)`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
"top-bar",
|
||||
render?.options?.className,
|
||||
)}
|
||||
>
|
||||
{
|
||||
render?.component && React.cloneElement(render?.component, render?.options?.props ?? {})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}}
|
||||
</Motion>
|
||||
}
|
52
packages/app/src/components/Layout/topBar/index.less
Normal file
52
packages/app/src/components/Layout/topBar/index.less
Normal file
@ -0,0 +1,52 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#top-bar_spacer {
|
||||
position: relative;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
height: calc(@top_bar_height + @top_bar_padding * 2);
|
||||
min-height: calc(@top_bar_height + @top_bar_padding * 2);
|
||||
|
||||
//height: 0;
|
||||
//min-height: 0;
|
||||
}
|
||||
|
||||
.top-bar_wrapper {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 310;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
width: 100%;
|
||||
height: @top_bar_height;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
box-shadow: @card-shadow-top;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
color: var(--text-color);
|
||||
}
|
@ -29,26 +29,24 @@ const NavMenu = (props) => {
|
||||
|
||||
const NavMenuMobile = (props) => {
|
||||
return <div className="__mobile__navmenu_wrapper">
|
||||
<div className="card">
|
||||
{
|
||||
props.items.map((item) => {
|
||||
return <antd.Button
|
||||
key={item.key}
|
||||
className={classnames(
|
||||
"item",
|
||||
item.key === props.activeKey && "active",
|
||||
)}
|
||||
onClick={() => props.onClickItem(item.key)}
|
||||
type="ghost"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<div className="icon">
|
||||
{item.icon}
|
||||
</div>
|
||||
</antd.Button>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{
|
||||
props.items.map((item) => {
|
||||
return <antd.Button
|
||||
key={item.key}
|
||||
className={classnames(
|
||||
"card_item",
|
||||
item.key === props.activeKey && "active",
|
||||
)}
|
||||
onClick={() => props.onClickItem(item.key)}
|
||||
type="ghost"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<div className="icon">
|
||||
{item.icon}
|
||||
</div>
|
||||
</antd.Button>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -5,60 +5,58 @@
|
||||
}
|
||||
|
||||
.__mobile__navmenu_wrapper {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
position: relative;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
justify-content: space-evenly;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: @app_topBar_height;
|
||||
.card {
|
||||
width: 100%;
|
||||
height: @app_topBar_height;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
justify-content: space-evenly;
|
||||
justify-content: space-evenly;
|
||||
|
||||
box-shadow: @card-shadow;
|
||||
box-shadow: @card-shadow;
|
||||
|
||||
gap: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
gap: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
|
||||
isolation: unset;
|
||||
overflow: visible;
|
||||
isolation: unset;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.card_item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
|
||||
height: fit-content;
|
||||
height: fit-content;
|
||||
|
||||
gap: 4px;
|
||||
gap: 4px;
|
||||
|
||||
padding: 8px 10px;
|
||||
padding: 8px 10px;
|
||||
|
||||
&.active {
|
||||
color: var(--colorPrimary);
|
||||
}
|
||||
&.active {
|
||||
color: var(--colorPrimary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0;
|
||||
line-height: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.icon {
|
||||
margin: 0;
|
||||
line-height: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
height: fit-content;
|
||||
line-height: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
height: fit-content;
|
||||
line-height: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import * as antd from "antd"
|
||||
|
||||
import { createIconRender } from "components/Icons"
|
||||
|
||||
import { UseTopBar } from "components/Layout/topBar"
|
||||
import NavMenu from "./components/NavMenu"
|
||||
|
||||
import "./index.less"
|
||||
@ -29,6 +30,14 @@ export class PagePanelWithNavMenu extends React.Component {
|
||||
|
||||
primaryPanelRef = React.createRef()
|
||||
|
||||
componentDidMount() {
|
||||
app.layout.top_bar.shouldUseTopBarSpacer(false)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
app.layout.top_bar.shouldUseTopBarSpacer(true)
|
||||
}
|
||||
|
||||
renderActiveTab() {
|
||||
if (!Array.isArray(this.props.tabs)) {
|
||||
console.error("PagePanelWithNavMenu: tabs must be an array")
|
||||
@ -158,14 +167,27 @@ export class PagePanelWithNavMenu extends React.Component {
|
||||
},
|
||||
]
|
||||
|
||||
if (app.isMobile) {
|
||||
delete panels[0]
|
||||
}
|
||||
|
||||
if (this.props.extraPanel) {
|
||||
panels.push(this.props.extraPanel)
|
||||
}
|
||||
|
||||
return <PagePanels
|
||||
primaryPanelClassName={this.props.primaryPanelClassName}
|
||||
panels={panels}
|
||||
/>
|
||||
return <>
|
||||
{
|
||||
app.isMobile && app.layout.top_bar.render(<NavMenu
|
||||
activeKey={this.state.activeTab}
|
||||
items={this.getItems(this.props.tabs)}
|
||||
onClickItem={(key) => this.handleTabChange(key)}
|
||||
/>)
|
||||
}
|
||||
<PagePanels
|
||||
primaryPanelClassName={this.props.primaryPanelClassName}
|
||||
panels={panels}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,57 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#root {
|
||||
&.mobile {
|
||||
&.page-panel-spacer {
|
||||
.pagePanels {
|
||||
.panel {
|
||||
&.center {
|
||||
padding-top: calc(@top_bar_height + @top_bar_padding * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagePanels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow: unset;
|
||||
overflow-x: visible;
|
||||
overflow-y: overlay;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
padding: 0;
|
||||
|
||||
-webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 1) 5%, rgba(0, 0, 0, 1) 97%, transparent 100%);
|
||||
mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 1) 5%, rgba(0, 0, 0, 1) 97%, transparent 100%);
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
|
||||
height: unset;
|
||||
|
||||
&.center {
|
||||
z-index: 300;
|
||||
|
||||
overflow: unset;
|
||||
overflow-x: unset;
|
||||
overflow-y: unset;
|
||||
|
||||
height: unset;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagePanels {
|
||||
display: grid;
|
||||
|
||||
|
@ -36,6 +36,46 @@ const typeToComponent = {
|
||||
"playlist": (args) => <PlaylistTimelineEntry {...args} />,
|
||||
}
|
||||
|
||||
const PostList = (props) => {
|
||||
return <LoadMore
|
||||
ref={props.listRef}
|
||||
className="post-list"
|
||||
loadingComponent={LoadingComponent}
|
||||
noResultComponent={NoResultComponent}
|
||||
hasMore={props.hasMore}
|
||||
fetching={props.loading}
|
||||
onBottom={props.onLoadMore}
|
||||
>
|
||||
{
|
||||
!props.realtimeUpdates && !app.isMobile && <div className="resume_btn_wrapper">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={props.onResumeRealtimeUpdates}
|
||||
loading={props.resumingLoading}
|
||||
icon={<Icons.SyncOutlined />}
|
||||
>
|
||||
Resume
|
||||
</antd.Button>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
props.list.map((data) => {
|
||||
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
||||
key: data._id,
|
||||
data: data,
|
||||
events: {
|
||||
onClickLike: props.onLikePost,
|
||||
onClickSave: props.onSavePost,
|
||||
onClickDelete: props.onDeletePost,
|
||||
onClickEdit: props.onEditPost,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</LoadMore>
|
||||
}
|
||||
|
||||
export class PostsListsComponent extends React.Component {
|
||||
state = {
|
||||
openPost: null,
|
||||
@ -366,44 +406,34 @@ export class PostsListsComponent extends React.Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
const PostListProps = {
|
||||
listRef: this.listRef,
|
||||
list: this.state.list,
|
||||
|
||||
onLikePost: this.onLikePost,
|
||||
onSavePost: this.onSavePost,
|
||||
onDeletePost: this.onDeletePost,
|
||||
onEditPost: this.onEditPost,
|
||||
|
||||
onLoadMore: this.onLoadMore,
|
||||
hasMore: this.state.hasMore,
|
||||
loading: this.state.loading,
|
||||
|
||||
realtimeUpdates: this.state.realtimeUpdates,
|
||||
resumingLoading: this.state.resumingLoading,
|
||||
onResumeRealtimeUpdates: this.onResumeRealtimeUpdates,
|
||||
}
|
||||
|
||||
if (app.isMobile) {
|
||||
return <PostList
|
||||
{...PostListProps}
|
||||
/>
|
||||
}
|
||||
|
||||
return <div className="post-list_wrapper">
|
||||
<LoadMore
|
||||
ref={this.listRef}
|
||||
className="post-list"
|
||||
loadingComponent={LoadingComponent}
|
||||
noResultComponent={NoResultComponent}
|
||||
hasMore={this.state.hasMore}
|
||||
fetching={this.state.loading}
|
||||
onBottom={this.onLoadMore}
|
||||
>
|
||||
{
|
||||
!this.state.realtimeUpdates && !app.isMobile && <div className="resume_btn_wrapper">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={this.onResumeRealtimeUpdates}
|
||||
loading={this.state.resumingLoading}
|
||||
icon={<Icons.SyncOutlined />}
|
||||
>
|
||||
Resume
|
||||
</antd.Button>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.list.map((data) => {
|
||||
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
||||
key: data._id,
|
||||
data: data,
|
||||
events: {
|
||||
onClickLike: this.onLikePost,
|
||||
onClickSave: this.onSavePost,
|
||||
onClickDelete: this.onDeletePost,
|
||||
onClickEdit: this.onEditPost,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</LoadMore>
|
||||
<PostList
|
||||
{...PostListProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,37 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#root {
|
||||
&.mobile {
|
||||
.post-list {
|
||||
overflow: unset;
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
|
||||
height: 100%;
|
||||
|
||||
//padding-top: 5px;
|
||||
border-radius: 0;
|
||||
|
||||
.playlistTimelineEntry {
|
||||
width: 100%;
|
||||
|
||||
.playlistTimelineEntry_content {
|
||||
.playlistTimelineEntry_thumbnail {
|
||||
img {
|
||||
width: 20vw;
|
||||
height: 20vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.postCard {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-list_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -7,7 +7,7 @@ export default class Layout extends React.PureComponent {
|
||||
progressBar = progressBar.configure({ parent: "html", showSpinner: false })
|
||||
|
||||
state = {
|
||||
layoutType: app.isMobile ? "mobile" : "default",
|
||||
layoutType: "default",
|
||||
renderError: null,
|
||||
}
|
||||
|
||||
@ -51,14 +51,18 @@ export default class Layout extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// if (window.app.cores.settings.get("forceMobileMode") || app.isMobile) {
|
||||
// app.layout.set("mobile")
|
||||
// }
|
||||
|
||||
// register events
|
||||
Object.keys(this.events).forEach((event) => {
|
||||
window.app.eventBus.on(event, this.events[event])
|
||||
})
|
||||
|
||||
if (app.isMobile) {
|
||||
this.layoutInterface.toogleMobileStyle(true)
|
||||
}
|
||||
|
||||
if (app.cores.settings.get("reduceAnimations")) {
|
||||
this.layoutInterface.toogleRootContainerClassname("reduce-animations", true)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -75,25 +79,22 @@ export default class Layout extends React.PureComponent {
|
||||
makePageTransition(path, options = {}) {
|
||||
this.progressBar.start()
|
||||
|
||||
if (app.cores.settings.get("reduceAnimations") || options.state?.noTransition) {
|
||||
const content_layout = document.getElementById("content_layout")
|
||||
|
||||
if (!content_layout) {
|
||||
console.warn("content_layout not found, no animation will be played")
|
||||
|
||||
this.progressBar.done()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const transitionLayer = document.getElementById("transitionLayer")
|
||||
|
||||
if (!transitionLayer) {
|
||||
console.warn("transitionLayer not found, no animation will be played")
|
||||
return false
|
||||
}
|
||||
|
||||
transitionLayer.classList.add("fade-transverse-leave")
|
||||
content_layout.classList.add("fade-transverse-leave")
|
||||
|
||||
setTimeout(() => {
|
||||
this.progressBar.done()
|
||||
|
||||
transitionLayer.classList.remove("fade-transverse-leave")
|
||||
content_layout.classList.remove("fade-transverse-leave")
|
||||
}, options.state?.transitionDelay ?? 250)
|
||||
}
|
||||
|
||||
@ -110,25 +111,53 @@ export default class Layout extends React.PureComponent {
|
||||
})
|
||||
},
|
||||
toogleCenteredContent: (to) => {
|
||||
return this.layoutInterface.toogleRootContainerClassname("centered-content", to)
|
||||
},
|
||||
toogleMobileStyle: (to) => {
|
||||
return this.layoutInterface.toogleRootContainerClassname("mobile", to)
|
||||
},
|
||||
toogleReducedAnimations: (to) => {
|
||||
return this.layoutInterface.toogleRootContainerClassname("reduce-animations", to)
|
||||
},
|
||||
toogleTopBarSpacer: (to) => {
|
||||
return this.layoutInterface.toogleRootContainerClassname("top-bar-spacer", to)
|
||||
},
|
||||
tooglePagePanelSpacer: (to) => {
|
||||
return this.layoutInterface.toogleRootContainerClassname("page-panel-spacer", to)
|
||||
},
|
||||
toogleRootContainerClassname: (classname, to) => {
|
||||
const root = document.getElementById("root")
|
||||
|
||||
if (app.isMobile) {
|
||||
console.warn("Skipping centered content on mobile")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!root) {
|
||||
console.error("root not found")
|
||||
return false
|
||||
}
|
||||
|
||||
to = typeof to === "boolean" ? to : !root.classList.contains("centered-content")
|
||||
to = typeof to === "boolean" ? to : !root.classList.contains(classname)
|
||||
|
||||
if (root.classList.contains(classname) === to) {
|
||||
// ignore
|
||||
return false
|
||||
}
|
||||
|
||||
if (to === true) {
|
||||
root.classList.add("centered_content")
|
||||
root.classList.add(classname)
|
||||
} else {
|
||||
root.classList.remove("centered_content")
|
||||
root.classList.remove(classname)
|
||||
}
|
||||
},
|
||||
scrollTo: (to) => {
|
||||
const content_layout = document.getElementById("content_layout")
|
||||
|
||||
if (!content_layout) {
|
||||
console.error("content_layout not found")
|
||||
return false
|
||||
}
|
||||
|
||||
content_layout.scrollTo({
|
||||
...to,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +177,7 @@ export default class Layout extends React.PureComponent {
|
||||
return JSON.stringify(this.state.renderError)
|
||||
}
|
||||
|
||||
const Layout = Layouts[app.isMobile ? "mobile" : layoutType]
|
||||
const Layout = Layouts[layoutType]
|
||||
|
||||
if (!Layout) {
|
||||
return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`))
|
||||
|
@ -2,13 +2,13 @@ import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Layout } from "antd"
|
||||
|
||||
import { Sidebar, Drawer, Sidedrawer, Modal } from "components/Layout"
|
||||
import { Sidebar, Drawer, Sidedrawer, Modal, BottomBar, TopBar } from "components/Layout"
|
||||
|
||||
import BackgroundDecorator from "components/BackgroundDecorator"
|
||||
|
||||
import { createWithDom as FloatingStack } from "../components/floatingStack"
|
||||
|
||||
export default (props) => {
|
||||
const DesktopLayout = (props) => {
|
||||
React.useEffect(() => {
|
||||
const floatingStack = FloatingStack()
|
||||
|
||||
@ -20,30 +20,55 @@ export default (props) => {
|
||||
return <>
|
||||
<BackgroundDecorator />
|
||||
|
||||
<Layout className="app_layout" style={{ height: "100%" }}>
|
||||
<Layout id="app_layout" className="app_layout">
|
||||
<Modal />
|
||||
<Drawer />
|
||||
<Sidebar />
|
||||
<Sidedrawer />
|
||||
<Layout.Content
|
||||
id="content_layout"
|
||||
className={classnames(
|
||||
"content_layout",
|
||||
...props.contentClassnames ?? [],
|
||||
"content_layout",
|
||||
{
|
||||
["floating-sidebar"]: window.app?.cores.settings.get("sidebar.floating")
|
||||
}
|
||||
},
|
||||
"fade-transverse-active",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="transitionLayer"
|
||||
className={classnames(
|
||||
"page_layout",
|
||||
"fade-transverse-active",
|
||||
)}
|
||||
>
|
||||
{React.cloneElement(props.children, props)}
|
||||
</div>
|
||||
{
|
||||
React.cloneElement(props.children, props)
|
||||
}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
}
|
||||
|
||||
const MobileLayout = (props) => {
|
||||
return <Layout id="app_layout" className="app_layout">
|
||||
<Modal />
|
||||
|
||||
<TopBar />
|
||||
|
||||
<Layout.Content
|
||||
id="content_layout"
|
||||
className={classnames(
|
||||
...props.layoutPageModesClassnames ?? [],
|
||||
"content_layout",
|
||||
"fade-transverse-active",
|
||||
)}
|
||||
>
|
||||
{
|
||||
React.cloneElement(props.children, props)
|
||||
}
|
||||
</Layout.Content>
|
||||
|
||||
<BottomBar />
|
||||
<Sidedrawer />
|
||||
<Drawer />
|
||||
</Layout>
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
return window.app.isMobile ? <MobileLayout {...props} /> : <DesktopLayout {...props} />
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
import Default from "./default"
|
||||
import Mobile from "./mobile"
|
||||
import None from "./none"
|
||||
import Minimal from "./minimal"
|
||||
import Blank from "./blank"
|
||||
|
||||
export default {
|
||||
default: Default,
|
||||
mobile: Mobile,
|
||||
none: None,
|
||||
minimal: Minimal,
|
||||
blank: Blank,
|
||||
}
|
@ -5,7 +5,7 @@ import classnames from "classnames"
|
||||
import { Drawer, Sidedrawer } from "components/Layout"
|
||||
|
||||
export default (props) => {
|
||||
return <antd.Layout className={classnames("app_layout", { ["mobile"]: app.isMobile })} style={{ height: "100%" }}>
|
||||
return <antd.Layout className={classnames("app_layout")} style={{ height: "100%" }}>
|
||||
<Drawer />
|
||||
<Sidedrawer />
|
||||
<div id="transitionLayer" className="fade-transverse-active">
|
@ -1,21 +0,0 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import * as antd from "antd"
|
||||
|
||||
import { BottomBar, Drawer, Sidedrawer, Modal } from "components/Layout"
|
||||
|
||||
export default (props) => {
|
||||
return <antd.Layout id="app_layout" className={classnames("app_layout", ["mobile"])}>
|
||||
<Modal />
|
||||
|
||||
<antd.Layout.Content className={classnames("content_layout", ...props.layoutPageModesClassnames ?? [])}>
|
||||
<div id="transitionLayer" className={classnames("content_wrapper", "fade-transverse-active")}>
|
||||
{React.cloneElement(props.children, props)}
|
||||
</div>
|
||||
</antd.Layout.Content>
|
||||
|
||||
<BottomBar />
|
||||
<Sidedrawer />
|
||||
<Drawer />
|
||||
</antd.Layout>
|
||||
}
|
@ -1,5 +1,60 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
@borderRadius: 12px;
|
||||
|
||||
#root {
|
||||
&.mobile {
|
||||
.accountProfile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.panels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tabMenuWrapper {
|
||||
position: fixed;
|
||||
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
||||
z-index: 300;
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: @top_bar_height;
|
||||
|
||||
padding: @top_bar_padding;
|
||||
|
||||
box-shadow: @card-shadow-top;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
.ant-menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.ant-menu-item {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.topHidden {
|
||||
.tabMenuWrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accountProfile {
|
||||
display: flex;
|
||||
|
||||
|
@ -1,6 +1,31 @@
|
||||
@panel-width: 500px;
|
||||
@chatbox-header-height: 50px;
|
||||
|
||||
#root {
|
||||
&.mobile {
|
||||
.livestream {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.livestream_panel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.livestream_player {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.plyr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plyr__controls {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
|
@ -1,3 +1,38 @@
|
||||
#root {
|
||||
&.mobile {
|
||||
.playlist_view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.play_info_wrapper {
|
||||
position: relative;
|
||||
|
||||
.play_info {
|
||||
width: 100%;
|
||||
|
||||
.play_info_cover {
|
||||
width: 20vh;
|
||||
height: 20vh;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 30px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_view {
|
||||
display: grid;
|
||||
|
||||
@ -60,7 +95,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Translation } from "react-i18next"
|
||||
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
||||
|
||||
import { Icons, createIconRender } from "components/Icons"
|
||||
import { UseTopBar } from "components/Layout/topBar"
|
||||
|
||||
import {
|
||||
composedSettingsByGroups as settingsGroups,
|
||||
@ -20,38 +21,31 @@ const SettingsHeader = ({
|
||||
activeKey,
|
||||
back = () => { }
|
||||
} = {}) => {
|
||||
if (activeKey) {
|
||||
const currentTab = composedTabs[activeKey]
|
||||
const currentTab = composedTabs[activeKey]
|
||||
|
||||
return <div className="__mobile__settings_header nav">
|
||||
<antd.Button
|
||||
return <UseTopBar
|
||||
options={{
|
||||
className: "settings_nav"
|
||||
}}
|
||||
>
|
||||
{
|
||||
activeKey && <antd.Button
|
||||
icon={<Icons.MdChevronLeft />}
|
||||
onClick={back}
|
||||
size="large"
|
||||
type="ghost"
|
||||
/>
|
||||
}
|
||||
|
||||
<h1>
|
||||
{
|
||||
createIconRender(currentTab?.icon)
|
||||
}
|
||||
<Translation>
|
||||
{(t) => t(currentTab?.label ?? activeKey)}
|
||||
</Translation>
|
||||
</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className="__mobile__settings_header">
|
||||
<h1>
|
||||
{
|
||||
createIconRender("Settings")
|
||||
createIconRender(currentTab?.icon ?? "Settings")
|
||||
}
|
||||
<Translation>
|
||||
{(t) => t("Settings")}
|
||||
{(t) => t(currentTab?.label ?? activeKey ?? "Settings")}
|
||||
</Translation>
|
||||
</h1>
|
||||
</div>
|
||||
</UseTopBar>
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
@ -80,6 +74,11 @@ export default (props) => {
|
||||
const changeTab = (key) => {
|
||||
lastKey = key
|
||||
setActiveKey(key)
|
||||
|
||||
// scroll to top
|
||||
app.layout.scrollTo({
|
||||
top: 0,
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="__mobile__settings">
|
||||
|
@ -1,4 +1,32 @@
|
||||
@top_nav_height: 52px;
|
||||
.settings_nav {
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
view-transition-name: main-header-text;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--colorPrimary);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
font-size: 2rem;
|
||||
|
||||
svg {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.__mobile__settings {
|
||||
display: flex;
|
||||
@ -7,47 +35,6 @@
|
||||
|
||||
gap: 30px;
|
||||
|
||||
.__mobile__settings_header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
|
||||
width: 100%;
|
||||
height: @top_nav_height;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
padding: 5px 20px;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
view-transition-name: main-header-text;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--colorPrimary);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
font-size: 2rem;
|
||||
|
||||
svg {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings_list {
|
||||
view-transition-name: settings-list;
|
||||
|
||||
|
@ -128,25 +128,19 @@ html {
|
||||
font-size: calc(16px * var(--fontScale));
|
||||
|
||||
&.electron {
|
||||
.page_layout {
|
||||
padding-top: 35px;
|
||||
}
|
||||
|
||||
.ant-layout-sider {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.centered_content {
|
||||
.content_layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.app_layout {
|
||||
.content_layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.page_layout {
|
||||
width: 60%;
|
||||
height: calc(100vh - var(--layoutPadding) * 2);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -192,27 +186,19 @@ html {
|
||||
z-index: 200;
|
||||
transition: all 200ms ease-in-out;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
//padding-top: 20px;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content_layout {
|
||||
position: relative;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@ -232,11 +218,6 @@ html {
|
||||
&.floating-sidebar {
|
||||
margin-left: @app_sidebar_width;
|
||||
}
|
||||
|
||||
.page_layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.root_background {
|
||||
|
@ -1,183 +1,66 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
@top_bar_height: 52px;
|
||||
|
||||
// #root,body,html {
|
||||
// height: 100%!important;
|
||||
// max-height: 100%!important;
|
||||
// }
|
||||
|
||||
.app_layout {
|
||||
#root {
|
||||
&.mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&.centered-content {
|
||||
.app_layout {
|
||||
.content_layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
padding: 0;
|
||||
|
||||
.content_wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
overflow-y: scroll;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content_layout {
|
||||
&.top-bar-spacer {
|
||||
.app_layout {
|
||||
.content_layout {
|
||||
padding-top: calc(@top_bar_height + @top_bar_padding * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app_layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
padding: 7px 0;
|
||||
padding-bottom: 0px;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pagePanels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-x: visible;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 0;
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
|
||||
&.left {
|
||||
position: absolute;
|
||||
|
||||
height: @top_bar_height;
|
||||
|
||||
padding: 10px;
|
||||
z-index: 310;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
overflow: visible;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
&.center {
|
||||
z-index: 300;
|
||||
|
||||
overflow-y: visible;
|
||||
overflow-x: hidden;
|
||||
|
||||
margin-top: calc(@top_bar_height + 10px);
|
||||
margin-bottom: 10px;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
height: 100%;
|
||||
|
||||
//-webkit-mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 10px, rgb(0, 0, 0) calc(100% - 10px), rgba(0, 0, 0, 0) 100%);
|
||||
//mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 10px, rgb(0, 0, 0) calc(100% - 10px), rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
.content_layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 5px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
justify-content: space-evenly;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 0px;
|
||||
|
||||
box-shadow: @card-shadow;
|
||||
overflow: unset;
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
isolation: unset;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.play_info_wrapper {
|
||||
position: relative;
|
||||
|
||||
.play_info {
|
||||
width: 100%;
|
||||
|
||||
.play_info_cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 30px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-list_wrapper {
|
||||
.post-list {
|
||||
padding-top: 10px;
|
||||
border-radius: 0;
|
||||
|
||||
.playlistTimelineEntry {
|
||||
width: 100%;
|
||||
|
||||
.playlistTimelineEntry_content {
|
||||
.playlistTimelineEntry_thumbnail {
|
||||
img {
|
||||
width: 20vw;
|
||||
height: 20vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.postCard {
|
||||
>div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.livestream {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.livestream_panel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.livestream_player {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.plyr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,3 +19,7 @@
|
||||
|
||||
@card-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 var(--shadow-color);
|
||||
@card-shadow-top: 0 -4px 3px 0 rgba(63, 63, 68, 0.05), 0 0 0 2px var(--shadow-color);
|
||||
|
||||
// Mobile
|
||||
@top_bar_height: 52px;
|
||||
@top_bar_padding: 10px;
|
Loading…
x
Reference in New Issue
Block a user