diff --git a/packages/app/package.json b/packages/app/package.json index 7f653a91..2afb7f7a 100755 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/components/Layout/bottomBar/index.jsx b/packages/app/src/components/Layout/bottomBar/index.jsx index 1749f42b..86c96c6a 100755 --- a/packages/app/src/components/Layout/bottomBar/index.jsx +++ b/packages/app/src/components/Layout/bottomBar/index.jsx @@ -83,6 +83,75 @@ const AccountButton = (props) => { } + +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
+ { + QuickNavMenuItems.map((item, index) => { + return
+ { + createIconRender(item.icon) + } +

+ + { + item.label + } +

+
+ }) + } +
+} + export default (props) => { return { + 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
@@ -173,63 +332,75 @@ export class BottomBar extends React.Component { return null } - return - {({ y }) =>
-
-
app.setLocation("/")} - > -
- {createIconRender("PlusCircle")} -
-
+ return <> + - { - this.context.currentManifest &&
+ {({ y }) =>
+
+
app.setLocation("/")} > - +
+ {createIconRender("PlusCircle")} +
- } - -
} - +
} + + } } \ No newline at end of file diff --git a/packages/app/src/components/Layout/bottomBar/index.less b/packages/app/src/components/Layout/bottomBar/index.less index 13ac626d..f5635ee3 100755 --- a/packages/app/src/components/Layout/bottomBar/index.less +++ b/packages/app/src/components/Layout/bottomBar/index.less @@ -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; diff --git a/packages/app/src/components/PagePanels/components/NavMenu/index.jsx b/packages/app/src/components/PagePanels/components/NavMenu/index.jsx index d9c171a7..ff4e389b 100644 --- a/packages/app/src/components/PagePanels/components/NavMenu/index.jsx +++ b/packages/app/src/components/PagePanels/components/NavMenu/index.jsx @@ -34,22 +34,20 @@ const NavMenuMobile = (props) => {
{ props.items.map((item) => { - return
props.onClickItem(item.key)} + type="ghost" + disabled={item.disabled} >
{item.icon}
- -
- {item.label} -
-
+ }) }
diff --git a/packages/app/src/components/PagePanels/components/NavMenu/index.less b/packages/app/src/components/PagePanels/components/NavMenu/index.less index 64bbdbdb..90aa882f 100644 --- a/packages/app/src/components/PagePanels/components/NavMenu/index.less +++ b/packages/app/src/components/PagePanels/components/NavMenu/index.less @@ -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; + } + } } } \ No newline at end of file diff --git a/packages/app/src/cores/contextMenu/context_menu.core.js b/packages/app/src/cores/contextMenu/context_menu.core.js index 27eb83c7..93ce9569 100755 --- a/packages/app/src/cores/contextMenu/context_menu.core.js +++ b/packages/app/src/cores/contextMenu/context_menu.core.js @@ -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)) } diff --git a/packages/app/src/cores/haptics/haptics.core.js b/packages/app/src/cores/haptics/haptics.core.js new file mode 100644 index 00000000..dfb34ec4 --- /dev/null +++ b/packages/app/src/cores/haptics/haptics.core.js @@ -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) + } +} \ No newline at end of file diff --git a/packages/app/src/cores/player/processors/gainNode/index.js b/packages/app/src/cores/player/processors/gainNode/index.js index d446b65f..bf0f53d0 100644 --- a/packages/app/src/cores/player/processors/gainNode/index.js +++ b/packages/app/src/cores/player/processors/gainNode/index.js @@ -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) { diff --git a/packages/app/src/cores/sound/sound.core.js b/packages/app/src/cores/sound/sound.core.js index b7077b8c..b6876fd2 100755 --- a/packages/app/src/cores/sound/sound.core.js +++ b/packages/app/src/cores/sound/sound.core.js @@ -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") diff --git a/packages/app/src/cores/style/style.core.jsx b/packages/app/src/cores/style/style.core.jsx index b219a7e4..1479cbcf 100755 --- a/packages/app/src/cores/style/style.core.jsx +++ b/packages/app/src/cores/style/style.core.jsx @@ -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} @@ -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 = { diff --git a/packages/app/src/layout.jsx b/packages/app/src/layout.jsx index 5c4d970d..32ecaccd 100755 --- a/packages/app/src/layout.jsx +++ b/packages/app/src/layout.jsx @@ -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 diff --git a/packages/app/src/pages/live/[key].jsx b/packages/app/src/pages/live/[key].jsx index cc69452a..22484d34 100755 --- a/packages/app/src/pages/live/[key].jsx +++ b/packages/app/src/pages/live/[key].jsx @@ -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 }) } diff --git a/packages/app/src/pages/login/index.mobile.jsx b/packages/app/src/pages/login/index.mobile.jsx index 2cc46086..b999d90a 100755 --- a/packages/app/src/pages/login/index.mobile.jsx +++ b/packages/app/src/pages/login/index.mobile.jsx @@ -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
diff --git a/packages/app/src/theme/index.less b/packages/app/src/theme/index.less index 4d448005..c484d748 100755 --- a/packages/app/src/theme/index.less +++ b/packages/app/src/theme/index.less @@ -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; diff --git a/packages/app/src/theme/mobile.less b/packages/app/src/theme/mobile.less index 79baee64..0b80aa19 100755 --- a/packages/app/src/theme/mobile.less +++ b/packages/app/src/theme/mobile.less @@ -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%; + } + } } } \ No newline at end of file diff --git a/packages/app/src/theme/vars.less b/packages/app/src/theme/vars.less index 5e2a49db..13cf0e3c 100755 --- a/packages/app/src/theme/vars.less +++ b/packages/app/src/theme/vars.less @@ -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); \ No newline at end of file +@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); \ No newline at end of file