Merge pull request #105 from ragestudio/mobile-mode-nav-improve

Mobile mode nav improve
This commit is contained in:
srgooglo 2023-06-14 00:56:41 +02:00 committed by GitHub
commit b2851ccd8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 558 additions and 165 deletions

View File

@ -44,6 +44,8 @@
"@mui/material": "^5.11.9",
"@paciolan/remote-component": "^2.13.0",
"@tsmx/human-readable": "^1.0.7",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"antd": "^5.2.1",
"antd-mobile": "^5.0.0-rc.17",
"axios": "^1.4.0",

View File

@ -83,6 +83,75 @@ const AccountButton = (props) => {
</div>
}
const QuickNavMenuItems = [
{
id: "music",
icon: "MdAlbum",
label: "Music",
location: "/music"
},
{
id: "tv",
icon: "Tv",
label: "Tv",
location: "/tv"
},
{
id: "groups",
icon: "MdGroups",
label: "Groups",
location: "/groups",
disabled: true,
},
{
id: "marketplace",
icon: "Box",
label: "Marketplace",
location: "/marketplace",
disabled: true
},
]
const QuickNavMenu = ({
visible,
}) => {
return <div
className={classnames(
"quick-nav",
{
["active"]: visible
}
)}
>
{
QuickNavMenuItems.map((item, index) => {
return <div
key={index}
className={classnames(
"quick-nav_item",
{
["disabled"]: item.disabled
}
)}
quicknav-item={item.id}
disabled={item.disabled}
>
{
createIconRender(item.icon)
}
<h1>
{
item.label
}
</h1>
</div>
})
}
</div>
}
export default (props) => {
return <WithPlayerContext>
<BottomBar
@ -99,6 +168,7 @@ export class BottomBar extends React.Component {
show: true,
visible: true,
render: null,
quickNavVisible: false
}
busEvents = {
@ -162,6 +232,95 @@ export class BottomBar extends React.Component {
}
}
handleNavTouchStart = (e) => {
this._navTouchStart = setTimeout(() => {
this.setState({ quickNavVisible: true })
app.cores.haptics.vibrate(80)
// remove the timeout
this._navTouchStart = null
}, 400)
}
handleNavTouchEnd = (event) => {
if (this._lastHovered) {
this._lastHovered.classList.remove("hover")
}
if (this._navTouchStart) {
clearTimeout(this._navTouchStart)
this._navTouchStart = null
return false
}
this.setState({ quickNavVisible: false })
// get cords of the touch
const x = event.changedTouches[0].clientX
const y = event.changedTouches[0].clientY
// get the element at the touch
const element = document.elementFromPoint(x, y)
// get the closest element with the attribute
const closest = element.closest(".quick-nav_item")
if (!closest) {
return false
}
const item = QuickNavMenuItems.find((item) => {
return item.id === closest.getAttribute("quicknav-item")
})
if (!item) {
return false
}
if (item.location) {
app.setLocation(item.location)
app.cores.haptics.vibrate([40, 80])
}
}
handleNavTouchMove = (event) => {
// check if the touch is hovering a quicknav item
const x = event.changedTouches[0].clientX
const y = event.changedTouches[0].clientY
// get the element at the touch
const element = document.elementFromPoint(x, y)
// get the closest element with the attribute
const closest = element.closest("[quicknav-item]")
if (!closest) {
if (this._lastHovered) {
this._lastHovered.classList.remove("hover")
}
this._lastHovered = null
return false
}
if (this._lastHovered !== closest) {
if (this._lastHovered) {
this._lastHovered.classList.remove("hover")
}
this._lastHovered = closest
closest.classList.add("hover")
app.cores.haptics.vibrate(40)
}
}
render() {
if (this.state.render) {
return <div className="bottomBar">
@ -173,63 +332,75 @@ export class BottomBar extends React.Component {
return null
}
return <Motion style={{ y: spring(this.state.show ? 0 : 300) }}>
{({ y }) => <div
className="bottomBar"
style={{
WebkitTransform: `translate3d(0, ${y}px, 0)`,
transform: `translate3d(0, ${y}px, 0)`,
}}
>
<div className="items">
<div
key="creator"
id="creator"
className={classnames("item", "primary")}
onClick={() => app.setLocation("/")}
>
<div className="icon">
{createIconRender("PlusCircle")}
</div>
</div>
return <>
<QuickNavMenu
visible={this.state.quickNavVisible}
/>
{
this.context.currentManifest && <div
className="item"
<Motion style={{ y: spring(this.state.show ? 0 : 300) }}>
{({ y }) => <div
className="bottomBar"
style={{
WebkitTransform: `translate3d(0, ${y}px, 0)`,
transform: `translate3d(0, ${y}px, 0)`,
}}
>
<div className="items">
<div
key="creator"
id="creator"
className={classnames("item", "primary")}
onClick={() => app.setLocation("/")}
>
<PlayerButton
manifest={this.context.currentManifest}
playback={this.context.playbackStatus}
colorAnalysis={this.context.coverColorAnalysis}
/>
<div className="icon">
{createIconRender("PlusCircle")}
</div>
</div>
}
<div
key="navigator"
id="navigator"
className="item"
onClick={() => app.setLocation("/")}
>
<div className="icon">
{createIconRender("Home")}
{
this.context.currentManifest && <div
className="item"
>
<PlayerButton
manifest={this.context.currentManifest}
playback={this.context.playbackStatus}
colorAnalysis={this.context.coverColorAnalysis}
/>
</div>
}
<div
key="navigator"
id="navigator"
className="item"
onClick={() => app.setLocation("/")}
onTouchMove={this.handleNavTouchMove}
onTouchStart={this.handleNavTouchStart}
onTouchEnd={this.handleNavTouchEnd}
onTouchCancel={() => {
this.setState({ quickNavVisible: false })
}}
>
<div className="icon">
{createIconRender("Home")}
</div>
</div>
<div
key="searcher"
id="searcher"
className="item"
onClick={app.controls.openSearcher}
>
<div className="icon">
{createIconRender("Search")}
</div>
</div>
<AccountButton />
</div>
<div
key="searcher"
id="searcher"
className="item"
onClick={app.controls.openSearcher}
>
<div className="icon">
{createIconRender("Search")}
</div>
</div>
<AccountButton />
</div>
</div>}
</Motion>
</div>}
</Motion>
</>
}
}

View File

@ -1,6 +1,114 @@
@import "theme/vars.less";
@import "theme/animations.less";
.quick-nav {
position: absolute;
box-sizing: border-box;
bottom: @app_bottomBar_height;
left: 0;
right: 0;
z-index: 500;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
width: 100%;
height: 0;
opacity: 0;
background-color: var(--background-color-accent);
box-shadow: @card-shadow-top;
transform: translateY(10px);
padding-bottom: 20px;
gap: 20px;
pointer-events: none;
transition: all 150ms ease-in-out;
border-radius: 12px 12px 0 0;
&.active {
pointer-events: all;
height: 100px;
opacity: 1;
}
.quick-nav_item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 500;
width: 20vw;
height: 100%;
//padding: 0 30px;
transition: all 150ms ease-in-out;
color: var(--text-color);
h1 {
margin: 0;
font-size: 0px;
transition: all 150ms ease-in-out;
}
svg {
margin: 0 !important;
transition: all 150ms ease-in-out;
font-size: 2rem;
}
&.disabled {
pointer-events: none;
h1 {
font-size: 0px;
}
svg {
font-size: 2rem;
color: var(--disabled-color);
}
color: var(--disabled-color);
}
&.hover {
h1 {
font-size: 1rem;
}
svg {
font-size: 2.2rem;
color: var(--colorPrimary);
}
color: var(--colorPrimary);
}
}
}
.player_btn {
color: var(--color);
@ -23,6 +131,7 @@
}
.bottomBar {
position: absolute;
display: flex;
flex-direction: row;
@ -31,6 +140,8 @@
position: relative;
z-index: 550;
left: 0;
bottom: 0;
@ -46,7 +157,7 @@
background-color: var(--background-color-accent);
border-radius: 12px 12px 0 0;
box-shadow: @card-shadow;
box-shadow: @card-shadow-top;
.items {
display: inline-flex;

View File

@ -34,22 +34,20 @@ const NavMenuMobile = (props) => {
<div className="card">
{
props.items.map((item) => {
return <div
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>
<div className="label">
{item.label}
</div>
</div>
</antd.Button>
})
}
</div>

View File

@ -1,3 +1,5 @@
@import "theme/vars.less";
.navmenu_wrapper {
position: relative;
}
@ -5,7 +7,7 @@
.__mobile__navmenu_wrapper {
box-sizing: border-box;
position: sticky;
position: relative;
top: 0;
left: 0;
@ -13,9 +15,50 @@
width: 100%;
.card {
width: 100%;
height: @app_topBar_height;
display: flex;
flex-direction: row;
justify-content: space-evenly;
box-shadow: @card-shadow;
gap: 10px;
padding: 5px;
border-radius: 12px;
isolation: unset;
overflow: visible;
.item {
display: flex;
flex-direction: column;
align-items: center;
height: fit-content;
gap: 4px;
padding: 8px 10px;
&.active {
color: var(--colorPrimary);
}
.icon {
margin: 0;
line-height: 1rem;
font-size: 1.5rem;
}
.label {
height: fit-content;
line-height: 1rem;
font-size: 0.8rem;
}
}
}
}

View File

@ -25,6 +25,11 @@ export default class ContextMenuCore extends Core {
})
async onInitialize() {
if (app.isMobile) {
console.warn("Context menu is not available on mobile")
return false
}
document.addEventListener("contextmenu", this.handleEvent.bind(this))
}

View File

@ -0,0 +1,28 @@
import Core from "evite/src/core"
export default class HapticsCore extends Core {
static refName = "haptics"
static namespace = "haptics"
static dependencies = [
"settings"
]
static get isGlobalDisabled() {
return app.cores.settings.get("haptic_feedback")
}
public = {
isGlobalDisabled: HapticsCore.isGlobalDisabled,
vibration: this.vibration.bind(this),
}
vibration(...args) {
const disabled = this.isGlobalDisabled
if (disabled) {
return false
}
return navigator.vibrate(...args)
}
}

View File

@ -7,11 +7,35 @@ export default class GainProcessorNode extends ProcessorNode {
static lock = true
static defaultValues = {
volume: 0.3,
gain: 1,
}
state = {
volume: AudioPlayerStorage.get("volume") ?? GainProcessorNode.defaultValues,
gain: AudioPlayerStorage.get("gain") ?? GainProcessorNode.defaultValues.gain,
}
exposeToPublic = {
modifyValues: function (values) {
this.state = {
...this.state,
...values,
}
AudioPlayerStorage.set("gain", this.state.gain)
this.applyValues()
}.bind(this),
resetDefaultValues: function () {
this.exposeToPublic.modifyValues(GainProcessorNode.defaultValues)
return this.state
}.bind(this),
values: () => this.state,
}
applyValues() {
// apply to current instance
this.processor.gain.value = app.cores.player.volume() * this.state.gain
}
async init() {
@ -21,8 +45,7 @@ export default class GainProcessorNode extends ProcessorNode {
this.processor = this.audioContext.createGain()
// set the default values
this.processor.gain.value = parseFloat(this.state.volume)
this.applyValues()
}
mutateInstance(instance) {

View File

@ -73,7 +73,7 @@ export default class SoundCore extends Core {
async injectUseUIAudio() {
const injectOnButtons = (event) => {
// search for closest button
const button = event.target.closest("button")
const button = event.target.closest("button") || event.target.closest(".ant-btn")
// if button exist and has aria-checked attribute then play switch_on or switch_off
if (button) {
@ -82,7 +82,7 @@ export default class SoundCore extends Core {
}
if (app.cores.settings.get("haptic_feedback")) {
Haptics.impact({ style: ImpactStyle.Medium })
Haptics.impact({ style: ImpactStyle.Light })
}
return this.public.useUIAudio("generic_click")

View File

@ -6,7 +6,6 @@ import Core from "evite/src/core"
import config from "config"
import store from "store"
import { ConfigProvider, theme } from "antd"
import RemoteSVGToComponent from "components/RemoteSVGToComponent"
const variantToAlgorithm = {
light: theme.defaultAlgorithm,
@ -57,6 +56,9 @@ export class ThemeProvider extends React.Component {
},
algorithm: themeAlgorithms,
}}
componentSize={
app.isMobile ? "large" : "middle"
}
>
{this.props.children}
</ConfigProvider>
@ -142,6 +144,13 @@ export default class StyleCore extends Core {
this.applyVariant(StyleCore.variant)
})
// if mobile set fontScale to 1
if (app.isMobile) {
this.update({
fontScale: 1
})
}
}
onEvents = {

View File

@ -112,6 +112,11 @@ export default class Layout extends React.PureComponent {
toogleCenteredContent: (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

View File

@ -313,7 +313,9 @@ export default class StreamViewer extends React.Component {
to = !this.state.cinemaMode
}
app.SidebarController.toggleVisibility(!to)
if (app.SidebarController) {
app.SidebarController.toggleVisibility(!to)
}
this.setState({ cinemaMode: to })
}

View File

@ -22,7 +22,9 @@ export default (props) => {
React.useEffect(() => {
if (app.userData) {
return app.navigation.goMain()
app.navigation.goMain()
return false
}
setRandomWallpaper()
@ -30,6 +32,8 @@ export default (props) => {
app.controls.openLoginForm({
defaultLocked: true,
})
return true
}, [])
return <div className="loginPage">

View File

@ -8,6 +8,7 @@
*:before,
*:after {
box-sizing: inherit;
//font-size: calc(1rem * var(--fontScale));
}
html {
@ -124,6 +125,8 @@ html {
background-color: var(--layoutBackgroundColor) !important;
font-size: calc(16px * var(--fontScale));
&.electron {
.page_layout {
padding-top: 35px;

View File

@ -12,8 +12,6 @@
width: 100%;
overflow-y: scroll;
//padding-top: 20px;
}
.content_layout {
@ -26,10 +24,70 @@
width: 100%;
padding: 7px;
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%;
.panel {
width: 100%;
//height: 100%;
&.left {
z-index: 310;
top: 0;
left: 0;
overflow: visible;
overflow-x: visible;
}
&.center {
z-index: 300;
overflow: visible;
overflow-x: visible;
height: calc(100vh - @app_bottomBar_height - @app_topBar_height);
-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 {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
padding: 5px;
justify-content: space-evenly;
box-shadow: @card-shadow;
border-radius: 12px;
isolation: unset;
overflow: visible;
}
}
.playlist_view {
display: flex;
flex-direction: column;
@ -52,95 +110,6 @@
}
}
.pagePanels {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: visible;
width: 100%;
height: 100%;
.panel {
width: 100%;
//height: 100%;
&.left {
z-index: 500;
overflow: visible;
overflow-x: visible;
}
&.right {
z-index: 400;
overflow-x: visible;
}
}
.card {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
padding: 5px;
justify-content: space-between;
box-shadow: @card-shadow;
border-radius: 12px;
isolation: unset;
overflow: visible;
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px 10px;
border-radius: 8px;
color: var(--text-color);
.icon {
svg {
margin: 0 !important;
font-size: 1rem;
}
}
.label {
font-size: 0.6rem;
}
&.active {
.icon {
svg {
color: var(--colorPrimary);
}
}
.label {
color: var(--colorPrimary);
}
background-color: var(--background-color-primary);
}
}
}
}
.post-list_wrapper {
.post-list {
padding-top: 10px;
@ -164,5 +133,26 @@
}
}
}
.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

@ -1,8 +1,6 @@
//* Now this only works as an fallback for unset dynamic theme values
@app_frameDecorator_height: 20px;
@app_bottomBar_height: 60px;
@app_bottomBar_height: 80px;
@app_bottomBar_iconSize: 45px;
@app_topBar_height: 52px;
@app_sidebar_width: 80px;
@app_sidebar_width_expanded: 230px;
@ -19,4 +17,5 @@
@transition-ease-out: all 0.3s ease-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);