improve layout for mobile & use of top-bar

This commit is contained in:
SrGooglo 2023-06-26 23:08:44 +00:00
parent 11f978cca9
commit c52834c0c8
25 changed files with 761 additions and 414 deletions

View File

@ -17,6 +17,7 @@
"backgroundPosition": "center", "backgroundPosition": "center",
"backgroundRepeat": "no-repeat", "backgroundRepeat": "no-repeat",
"backgroundAttachment": "fixed", "backgroundAttachment": "fixed",
"top-bar-height": "52px",
"compact-mode": false "compact-mode": false
}, },
"defaultVariant": "dark", "defaultVariant": "dark",

View File

@ -1,7 +1,7 @@
export default [ export default [
{ {
path: "/login", path: "/login",
useLayout: "none", useLayout: "minimal",
public: true public: true
}, },
{ {
@ -46,12 +46,12 @@ export default [
}, },
{ {
path: "/landing/*", path: "/landing/*",
useLayout: "blank", useLayout: "minimal",
public: true public: true
}, },
{ {
path: "/nfc/*", path: "/nfc/*",
useLayout: "blank", useLayout: "minimal",
public: true public: true
} }
] ]

View File

@ -1,3 +1,4 @@
export { default as TopBar } from "./topBar"
export { default as BottomBar } from "./bottomBar" export { default as BottomBar } from "./bottomBar"
export { default as Drawer } from "./drawer" export { default as Drawer } from "./drawer"
export { default as Sidebar } from "./sidebar" export { default as Sidebar } from "./sidebar"

View 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>
}

View 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);
}

View File

@ -29,26 +29,24 @@ const NavMenu = (props) => {
const NavMenuMobile = (props) => { const NavMenuMobile = (props) => {
return <div className="__mobile__navmenu_wrapper"> return <div className="__mobile__navmenu_wrapper">
<div className="card"> {
{ props.items.map((item) => {
props.items.map((item) => { return <antd.Button
return <antd.Button key={item.key}
key={item.key} className={classnames(
className={classnames( "card_item",
"item", item.key === props.activeKey && "active",
item.key === props.activeKey && "active", )}
)} onClick={() => props.onClickItem(item.key)}
onClick={() => props.onClickItem(item.key)} type="ghost"
type="ghost" disabled={item.disabled}
disabled={item.disabled} >
> <div className="icon">
<div className="icon"> {item.icon}
{item.icon} </div>
</div> </antd.Button>
</antd.Button> })
}) }
}
</div>
</div> </div>
} }

View File

@ -5,60 +5,58 @@
} }
.__mobile__navmenu_wrapper { .__mobile__navmenu_wrapper {
box-sizing: border-box; display: flex;
flex-direction: row;
position: relative; justify-content: space-evenly;
top: 0;
left: 0;
width: 100%; width: 100%;
}
.card { .card {
width: 100%; width: 100%;
height: @app_topBar_height; height: @app_topBar_height;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-evenly; justify-content: space-evenly;
box-shadow: @card-shadow; box-shadow: @card-shadow;
gap: 10px; gap: 10px;
padding: 5px; padding: 5px;
border-radius: 12px; border-radius: 12px;
isolation: unset; isolation: unset;
overflow: visible; overflow: visible;
}
.item { .card_item {
display: flex; display: flex;
flex-direction: column; 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 { &.active {
color: var(--colorPrimary); color: var(--colorPrimary);
} }
.icon { .icon {
margin: 0; margin: 0;
line-height: 1rem; line-height: 1rem;
font-size: 1.5rem; font-size: 1.5rem;
} }
.label { .label {
height: fit-content; height: fit-content;
line-height: 1rem; line-height: 1rem;
font-size: 0.8rem; font-size: 0.8rem;
}
}
} }
} }

View File

@ -4,6 +4,7 @@ import * as antd from "antd"
import { createIconRender } from "components/Icons" import { createIconRender } from "components/Icons"
import { UseTopBar } from "components/Layout/topBar"
import NavMenu from "./components/NavMenu" import NavMenu from "./components/NavMenu"
import "./index.less" import "./index.less"
@ -29,6 +30,14 @@ export class PagePanelWithNavMenu extends React.Component {
primaryPanelRef = React.createRef() primaryPanelRef = React.createRef()
componentDidMount() {
app.layout.top_bar.shouldUseTopBarSpacer(false)
}
componentWillUnmount() {
app.layout.top_bar.shouldUseTopBarSpacer(true)
}
renderActiveTab() { renderActiveTab() {
if (!Array.isArray(this.props.tabs)) { if (!Array.isArray(this.props.tabs)) {
console.error("PagePanelWithNavMenu: tabs must be an array") 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) { if (this.props.extraPanel) {
panels.push(this.props.extraPanel) panels.push(this.props.extraPanel)
} }
return <PagePanels return <>
primaryPanelClassName={this.props.primaryPanelClassName} {
panels={panels} 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}
/>
</>
} }
} }

View File

@ -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 { .pagePanels {
display: grid; display: grid;

View File

@ -36,6 +36,46 @@ const typeToComponent = {
"playlist": (args) => <PlaylistTimelineEntry {...args} />, "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 { export class PostsListsComponent extends React.Component {
state = { state = {
openPost: null, openPost: null,
@ -366,44 +406,34 @@ export class PostsListsComponent extends React.Component {
</div> </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"> return <div className="post-list_wrapper">
<LoadMore <PostList
ref={this.listRef} {...PostListProps}
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>
</div> </div>
} }
} }

View File

@ -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 { .post-list_wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -20,7 +54,7 @@
//will-change: transform; //will-change: transform;
overflow: hidden; overflow: hidden;
overflow-y: overlay; overflow-y: overlay;
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -7,7 +7,7 @@ export default class Layout extends React.PureComponent {
progressBar = progressBar.configure({ parent: "html", showSpinner: false }) progressBar = progressBar.configure({ parent: "html", showSpinner: false })
state = { state = {
layoutType: app.isMobile ? "mobile" : "default", layoutType: "default",
renderError: null, renderError: null,
} }
@ -51,14 +51,18 @@ export default class Layout extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
// if (window.app.cores.settings.get("forceMobileMode") || app.isMobile) {
// app.layout.set("mobile")
// }
// register events // register events
Object.keys(this.events).forEach((event) => { Object.keys(this.events).forEach((event) => {
window.app.eventBus.on(event, this.events[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() { componentWillUnmount() {
@ -75,25 +79,22 @@ export default class Layout extends React.PureComponent {
makePageTransition(path, options = {}) { makePageTransition(path, options = {}) {
this.progressBar.start() 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() this.progressBar.done()
return false return false
} }
const transitionLayer = document.getElementById("transitionLayer") content_layout.classList.add("fade-transverse-leave")
if (!transitionLayer) {
console.warn("transitionLayer not found, no animation will be played")
return false
}
transitionLayer.classList.add("fade-transverse-leave")
setTimeout(() => { setTimeout(() => {
this.progressBar.done() this.progressBar.done()
transitionLayer.classList.remove("fade-transverse-leave") content_layout.classList.remove("fade-transverse-leave")
}, options.state?.transitionDelay ?? 250) }, options.state?.transitionDelay ?? 250)
} }
@ -110,25 +111,53 @@ export default class Layout extends React.PureComponent {
}) })
}, },
toogleCenteredContent: (to) => { 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") const root = document.getElementById("root")
if (app.isMobile) {
console.warn("Skipping centered content on mobile")
return false
}
if (!root) { if (!root) {
console.error("root not found") console.error("root not found")
return false 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) { if (to === true) {
root.classList.add("centered_content") root.classList.add(classname)
} else { } 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) return JSON.stringify(this.state.renderError)
} }
const Layout = Layouts[app.isMobile ? "mobile" : layoutType] const Layout = Layouts[layoutType]
if (!Layout) { if (!Layout) {
return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`)) return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`))

View File

@ -2,4 +2,4 @@ import React from "react"
export default (props) => { export default (props) => {
return props.children return props.children
} }

View File

@ -2,13 +2,13 @@ import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Layout } from "antd" 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 BackgroundDecorator from "components/BackgroundDecorator"
import { createWithDom as FloatingStack } from "../components/floatingStack" import { createWithDom as FloatingStack } from "../components/floatingStack"
export default (props) => { const DesktopLayout = (props) => {
React.useEffect(() => { React.useEffect(() => {
const floatingStack = FloatingStack() const floatingStack = FloatingStack()
@ -20,30 +20,55 @@ export default (props) => {
return <> return <>
<BackgroundDecorator /> <BackgroundDecorator />
<Layout className="app_layout" style={{ height: "100%" }}> <Layout id="app_layout" className="app_layout">
<Modal /> <Modal />
<Drawer /> <Drawer />
<Sidebar /> <Sidebar />
<Sidedrawer /> <Sidedrawer />
<Layout.Content <Layout.Content
id="content_layout"
className={classnames( className={classnames(
"content_layout",
...props.contentClassnames ?? [], ...props.contentClassnames ?? [],
"content_layout",
{ {
["floating-sidebar"]: window.app?.cores.settings.get("sidebar.floating") ["floating-sidebar"]: window.app?.cores.settings.get("sidebar.floating")
} },
"fade-transverse-active",
)} )}
> >
<div {
id="transitionLayer" React.cloneElement(props.children, props)
className={classnames( }
"page_layout",
"fade-transverse-active",
)}
>
{React.cloneElement(props.children, props)}
</div>
</Layout.Content> </Layout.Content>
</Layout> </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} />
}

View File

@ -1,11 +1,9 @@
import Default from "./default" import Default from "./default"
import Mobile from "./mobile" import Minimal from "./minimal"
import None from "./none"
import Blank from "./blank" import Blank from "./blank"
export default { export default {
default: Default, default: Default,
mobile: Mobile, minimal: Minimal,
none: None,
blank: Blank, blank: Blank,
} }

View File

@ -5,7 +5,7 @@ import classnames from "classnames"
import { Drawer, Sidedrawer } from "components/Layout" import { Drawer, Sidedrawer } from "components/Layout"
export default (props) => { 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 /> <Drawer />
<Sidedrawer /> <Sidedrawer />
<div id="transitionLayer" className="fade-transverse-active"> <div id="transitionLayer" className="fade-transverse-active">

View File

@ -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>
}

View File

@ -1,5 +1,60 @@
@import "theme/vars.less";
@borderRadius: 12px; @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 { .accountProfile {
display: flex; display: flex;

View File

@ -1,6 +1,31 @@
@panel-width: 500px; @panel-width: 500px;
@chatbox-header-height: 50px; @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 { .plyr__controls {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;

View File

@ -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 { .playlist_view {
display: grid; display: grid;
@ -60,7 +95,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: contain;
} }
} }

View File

@ -5,6 +5,7 @@ import { Translation } from "react-i18next"
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey" import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
import { Icons, createIconRender } from "components/Icons" import { Icons, createIconRender } from "components/Icons"
import { UseTopBar } from "components/Layout/topBar"
import { import {
composedSettingsByGroups as settingsGroups, composedSettingsByGroups as settingsGroups,
@ -20,38 +21,31 @@ const SettingsHeader = ({
activeKey, activeKey,
back = () => { } back = () => { }
} = {}) => { } = {}) => {
if (activeKey) { const currentTab = composedTabs[activeKey]
const currentTab = composedTabs[activeKey]
return <div className="__mobile__settings_header nav"> return <UseTopBar
<antd.Button options={{
className: "settings_nav"
}}
>
{
activeKey && <antd.Button
icon={<Icons.MdChevronLeft />} icon={<Icons.MdChevronLeft />}
onClick={back} onClick={back}
size="large" size="large"
type="ghost" type="ghost"
/> />
}
<h1>
{
createIconRender(currentTab?.icon)
}
<Translation>
{(t) => t(currentTab?.label ?? activeKey)}
</Translation>
</h1>
</div>
}
return <div className="__mobile__settings_header">
<h1> <h1>
{ {
createIconRender("Settings") createIconRender(currentTab?.icon ?? "Settings")
} }
<Translation> <Translation>
{(t) => t("Settings")} {(t) => t(currentTab?.label ?? activeKey ?? "Settings")}
</Translation> </Translation>
</h1> </h1>
</div> </UseTopBar>
} }
export default (props) => { export default (props) => {
@ -80,6 +74,11 @@ export default (props) => {
const changeTab = (key) => { const changeTab = (key) => {
lastKey = key lastKey = key
setActiveKey(key) setActiveKey(key)
// scroll to top
app.layout.scrollTo({
top: 0,
})
} }
return <div className="__mobile__settings"> return <div className="__mobile__settings">

View File

@ -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 { .__mobile__settings {
display: flex; display: flex;
@ -7,47 +35,6 @@
gap: 30px; 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 { .settings_list {
view-transition-name: settings-list; view-transition-name: settings-list;

View File

@ -128,25 +128,19 @@ html {
font-size: calc(16px * var(--fontScale)); font-size: calc(16px * var(--fontScale));
&.electron { &.electron {
.page_layout {
padding-top: 35px;
}
.ant-layout-sider { .ant-layout-sider {
padding-top: 0px; padding-top: 0px;
} }
} }
&.centered_content { &.centered_content {
.content_layout { .app_layout {
display: flex; .content_layout {
flex-direction: column; display: flex;
justify-content: center; flex-direction: column;
align-items: center;
.page_layout { justify-content: center;
width: 60%; align-items: center;
height: calc(100vh - var(--layoutPadding) * 2);
} }
} }
} }
@ -192,27 +186,19 @@ html {
z-index: 200; z-index: 200;
transition: all 200ms ease-in-out; transition: all 200ms ease-in-out;
} }
&.mobile {
//padding-top: 20px;
::-webkit-scrollbar {
display: none !important;
width: 0;
height: 0;
z-index: 0;
}
}
} }
.content_layout { .content_layout {
position: relative; position: relative;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -232,11 +218,6 @@ html {
&.floating-sidebar { &.floating-sidebar {
margin-left: @app_sidebar_width; margin-left: @app_sidebar_width;
} }
.page_layout {
width: 100%;
height: 100%;
}
} }
.root_background { .root_background {

View File

@ -1,183 +1,66 @@
@import "theme/vars.less"; @import "theme/vars.less";
@top_bar_height: 52px; #root {
// #root,body,html {
// height: 100%!important;
// max-height: 100%!important;
// }
.app_layout {
&.mobile { &.mobile {
display: flex; &.centered-content {
flex-direction: column; .app_layout {
.content_layout {
display: flex;
flex-direction: column;
height: 100% !important; justify-content: flex-start;
max-height: 100% !important; align-items: center;
padding: 0; padding: 10px;
}
.content_wrapper { }
height: 100%;
width: 100%;
max-height: 100%;
overflow-y: scroll;
} }
.content_layout { &.top-bar-spacer {
.app_layout {
.content_layout {
padding-top: calc(@top_bar_height + @top_bar_padding * 2);
}
}
}
.app_layout {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
align-items: center;
justify-content: center;
width: 100%;
height: 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%; width: 100%;
height: 100%;
padding: 0; padding: 0;
.panel { ::-webkit-scrollbar {
width: 100%; display: none !important;
width: 0;
&.left { height: 0;
position: absolute; z-index: 0;
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%);
}
} }
.card { transition: all 150ms ease-in-out;
width: 100%;
height: 100%;
.content_layout {
display: flex; 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; >div {
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 {
width: 100%; 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%;
}
}
} }
} }

View File

@ -18,4 +18,8 @@
@transition-ease-inout: all 150ms ease-in-out; @transition-ease-inout: all 150ms ease-in-out;
@card-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05), 0 1px 3px 0 var(--shadow-color); @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); @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;