diff --git a/packages/app/config/context-menu/default/index.js b/packages/app/config/context-menu/default/index.js new file mode 100644 index 00000000..a33b4755 --- /dev/null +++ b/packages/app/config/context-menu/default/index.js @@ -0,0 +1,41 @@ +export default { + "default-context": (items) => { + const text = window.getSelection().toString() + + if (text) { + items.push({ + label: "Copy", + icon: "Copy", + action: (clickedItem, ctx) => { + copyToClipboard(text) + + ctx.close() + } + }) + } + + items.push({ + label: "Paste", + icon: "Clipboard", + action: (clickedItem, ctx) => { + app.message.error("This action is not supported by your browser") + + ctx.close() + } + }) + + items.push({ + label: "Report a bug", + icon: "AlertTriangle", + action: (clickedItem, ctx) => { + app.eventBus.emit("app.reportBug", { + clickedItem, + }) + + ctx.close() + } + }) + + return items + } +} \ No newline at end of file diff --git a/packages/app/config/context-menu/index.js b/packages/app/config/context-menu/index.js deleted file mode 100755 index 60da25d9..00000000 --- a/packages/app/config/context-menu/index.js +++ /dev/null @@ -1,119 +0,0 @@ -import download from "@utils/download" -import copyToClipboard from "@utils/copyToClipboard" - -export default { - "default-context": () => { - const items = [] - - const text = window.getSelection().toString() - - if (text) { - items.push({ - label: "Copy", - icon: "Copy", - action: (clickedItem, ctx) => { - copyToClipboard(text) - - ctx.close() - } - }) - } - - items.push({ - label: "Paste", - icon: "Clipboard", - action: (clickedItem, ctx) => { - app.message.error("This action is not supported by your browser") - - ctx.close() - } - }) - - items.push({ - label: "Report a bug", - icon: "AlertTriangle", - action: (clickedItem, ctx) => { - app.eventBus.emit("app.reportBug", { - clickedItem, - }) - - ctx.close() - } - }) - - return items - }, - "postCard-context": (parent, element, control) => { - const items = [ - { - label: "Copy ID", - icon: "Copy", - action: () => { - copyToClipboard(parent.id) - control.close() - } - }, - { - label: "Copy Link", - icon: "Link", - action: () => { - copyToClipboard(`${window.location.origin}/post/${parent.id}`) - control.close() - } - } - ] - - let media = null - - // if element div has `addition` class, search inside it for video or image - if (element.classList.contains("addition") || element.classList.contains("image-wrapper")) { - media = element.querySelector("video, img") - } - - // if element div has `plyr__poster` class, search outside it for video or image - if (element.classList.contains("plyr__poster")) { - console.log(element.parentElement) - media = element.parentElement.querySelector("video") - } - - // if media is found, and is a video, search for the source - if (media && media.tagName === "VIDEO") { - media = media.querySelector("source") - } - - if (media) { - items.push({ - type: "separator" - }) - - items.push({ - label: "Copy media URL", - icon: "Copy", - action: () => { - copyToClipboard(media.src) - control.close() - } - }) - - items.push({ - label: "Open media in new tab", - icon: "ExternalLink", - action: () => { - window.open(media.src, "_blank") - control.close() - } - }) - - items.push({ - label: "Download media", - icon: "Download", - action: () => { - download(media.src) - control.close() - } - }) - } - - return items - } -} \ No newline at end of file diff --git a/packages/app/config/context-menu/post/index.js b/packages/app/config/context-menu/post/index.js new file mode 100644 index 00000000..e27de217 --- /dev/null +++ b/packages/app/config/context-menu/post/index.js @@ -0,0 +1,70 @@ +import copyToClipboard from "@utils/copyToClipboard" +import download from "@utils/download" + +export default { + "post-card": (items, parent, element, control) => { + items.push({ + label: "Copy ID", + icon: "Copy", + action: () => { + copyToClipboard(parent.id) + control.close() + } + }) + + items.push({ + label: "Copy Link", + icon: "Link", + action: () => { + copyToClipboard(`${window.location.origin}/post/${parent.id}`) + control.close() + } + }) + + let media = null + + if (parent.querySelector(".attachment")) { + media = parent.querySelector(".attachment") + media = media.querySelector("video, img") + + if (media.querySelector("source")) { + media = media.querySelector("source") + } + } + + if (media) { + items.push({ + type: "separator", + }) + + items.push({ + label: "Copy media URL", + icon: "Copy", + action: () => { + copyToClipboard(media.src) + control.close() + } + }) + + items.push({ + label: "Open media in new tab", + icon: "ExternalLink", + action: () => { + window.open(media.src, "_blank") + control.close() + } + }) + + items.push({ + label: "Download media", + icon: "Download", + action: () => { + download(media.src) + control.close() + } + }) + } + + return items + } +} \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index d8d23d15..403ffaad 100755 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -40,11 +40,12 @@ "@mui/material": "^5.11.9", "@ragestudio/cordova-nfc": "^1.2.0", "@sentry/browser": "^7.64.0", + "@tanstack/react-virtual": "^3.5.0", "@tauri-apps/api": "^1.5.4", "@tsmx/human-readable": "^1.0.7", - "antd": "^5.6.4", + "antd": "^5.17.0", "antd-mobile": "^5.31.0", - "axios": "^1.4.0", + "axios": "^1.6.8", "bear-react-carousel": "^4.0.10-alpha.0", "buffer": "^6.0.3", "capacitor-music-controls-plugin-v3": "^1.1.0", @@ -65,7 +66,7 @@ "lottie-react": "^2.4.0", "lru-cache": "^10.0.0", "luxon": "^3.0.4", - "million": "^2.5.4-beta.1", + "million": "^2.6.4", "mime": "^3.0.0", "moment": "2.29.4", "mpegts.js": "^1.6.10", @@ -95,61 +96,15 @@ "remark-gfm": "^3.0.1", "rxjs": "^7.5.5", "store": "^2.0.12", - "ua-parser-js": "^1.0.36" + "ua-parser-js": "^1.0.36", + "vite": "^5.2.11" }, "devDependencies": { "@capacitor/assets": "^2.0.4", - "@electron-forge/cli": "^6.0.0-beta.66", - "@electron-forge/maker-deb": "^6.0.0-beta.66", - "@electron-forge/maker-rpm": "^6.0.0-beta.66", - "@electron-forge/maker-squirrel": "^6.0.0-beta.66", - "@electron-forge/maker-zip": "^6.0.0-beta.66", - "@electron-forge/plugin-vite": "^6.4.2", - "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@esbuild-plugins/node-modules-polyfill": "^0.2.2", - "@types/jest": "^26.0.24", - "@types/node": "^16.4.10", - "@types/react": "^17.0.15", - "@types/react-dom": "^17.0.9", - "@types/react-router-config": "^5.0.3", - "@types/react-router-dom": "^5.1.8", - "@typescript-eslint/eslint-plugin": "^4.29.0", "concurrently": "^7.4.0", "cors": "2.8.5", "cross-env": "^7.0.3", - "electron": "^21.0.1", - "electron-builder": "^24.6.4", - "electron-is": "^3.0.0", - "electron-log": "^4.4.8", - "electron-squirrel-startup": "^1.0.0", "express": "^4.17.1", "typescript": "^4.3.5" - }, - "config": { - "forge": { - "packagerConfig": {}, - "makers": [ - { - "name": "@electron-forge/maker-squirrel", - "config": { - "name": "comty" - } - }, - { - "name": "@electron-forge/maker-zip", - "platforms": [ - "darwin" - ] - }, - { - "name": "@electron-forge/maker-deb", - "config": {} - }, - { - "name": "@electron-forge/maker-rpm", - "config": {} - } - ] - } } } diff --git a/packages/app/public/dev-logo_alt.svg b/packages/app/public/dev-logo_alt.svg new file mode 100644 index 00000000..5e1be543 --- /dev/null +++ b/packages/app/public/dev-logo_alt.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/public/dev-logo_full.svg b/packages/app/public/dev-logo_full.svg new file mode 100644 index 00000000..0d5d036d --- /dev/null +++ b/packages/app/public/dev-logo_full.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/App.jsx b/packages/app/src/App.jsx index ca0d2054..e8bca0a1 100755 --- a/packages/app/src/App.jsx +++ b/packages/app/src/App.jsx @@ -28,7 +28,6 @@ import { NotificationsCenter, PostCreator, } from "@components" -import { DOMWindow } from "@components/RenderWindow" import { Icons } from "@components/Icons" import DesktopTopBar from "@components/DesktopTopBar" @@ -40,7 +39,9 @@ import Splash from "./splash" import "@styles/index.less" -CapacitorUpdater.notifyAppReady() +if (IS_MOBILE_HOST) { + CapacitorUpdater.notifyAppReady() +} class ComtyApp extends React.Component { constructor(props) { @@ -52,7 +53,6 @@ class ComtyApp extends React.Component { } state = { - desktopMode: false, session: null, initialized: false, } @@ -62,7 +62,7 @@ class ComtyApp extends React.Component { window.app.version = config.package.version window.app.confirm = antd.Modal.confirm window.app.message = antd.message - window.app.isCapacitor = window.navigator.userAgent === "capacitor" + window.app.isCapacitor = IS_MOBILE_HOST if (window.app.version !== window.localStorage.getItem("last_version")) { app.message.info(`Comty has been updated to version ${window.app.version}!`) @@ -157,16 +157,14 @@ class ComtyApp extends React.Component { framed: false }) }, + openMessages: () => { + app.location.push("/messages") + }, openFullImageViewer: (src) => { - const win = new DOMWindow({ - id: "fullImageViewer", - className: "fullImageViewer", - }) - - win.render( win.remove()} + onClose={() => app.cores.window_mng.close("image_lightbox")} hideDownload showRotate />) @@ -298,17 +296,6 @@ class ComtyApp extends React.Component { }, }) }, - "app.no_session": async () => { - const location = window.location.pathname - - if (location !== "/auth" && location !== "/register") { - antd.notification.info({ - message: "You are not logged in, to use some features you will need to log in.", - btn: app.controls.openLoginForm()}>Login, - duration: 15, - }) - } - }, "session.invalid": async (error) => { const token = await SessionModel.token diff --git a/packages/app/src/components/Icons/index.jsx b/packages/app/src/components/Icons/index.jsx index 6e7e1dd1..c465a14f 100755 --- a/packages/app/src/components/Icons/index.jsx +++ b/packages/app/src/components/Icons/index.jsx @@ -11,6 +11,7 @@ import * as lib3 from "react-icons/md" import * as lib4 from "react-icons/io" import * as lib5 from "react-icons/si" import * as lib6 from "react-icons/fa" +import * as lib7 from "react-icons/tb" const marginedStyle = { width: "1em", @@ -42,6 +43,7 @@ export const Icons = { ...lib4, ...lib5, ...lib6, + ...lib7, } export function createIconRender(icon, props) { diff --git a/packages/app/src/components/Layout/index.js b/packages/app/src/components/Layout/index.js deleted file mode 100755 index 50ccd4b6..00000000 --- a/packages/app/src/components/Layout/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export { default as TopBar } from "./topBar" -export { default as BottomBar } from "./bottomBar" -export { default as Drawer } from "./drawer" -export { default as Sidebar } from "./sidebar" -export { default as Sidedrawer } from "./sidedrawer" -export { default as Modal } from "./modal" -export { default as Header } from "./header" -export { default as ToolsBar } from "./toolsBar" \ No newline at end of file diff --git a/packages/app/src/components/Layout/modal/index.jsx b/packages/app/src/components/Layout/modal/index.jsx deleted file mode 100755 index 8b1cbac9..00000000 --- a/packages/app/src/components/Layout/modal/index.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react" -import { Modal, Button } from "antd" -import { Icons } from "@components/Icons" - -import "./index.less" - -// TODO: Implement translucid mode -// TODO: Implement close on click mask (and Should remove the close button) - -export default class AppModal extends React.Component { - constructor(props) { - super(props) - - this.controller = app.ModalController = { - open: this.open, - close: this.close, - modalRef: this.modalRef, - } - } - - state = { - currentRender: null, - renderParams: {} - } - - modalRef = React.createRef() - - open = (render, params = {}) => { - this.setState({ - currentRender: render, - renderParams: params - }) - } - - close = () => { - this.setState({ - currentRender: null, - renderParams: {} - }) - } - - handleModalClose = () => { - this.close() - } - - renderModal = () => { - return
-
- } - - render() { - return - } -} \ No newline at end of file diff --git a/packages/app/src/components/Layout/modal/index.less b/packages/app/src/components/Layout/modal/index.less deleted file mode 100755 index eb93a8ce..00000000 --- a/packages/app/src/components/Layout/modal/index.less +++ /dev/null @@ -1,45 +0,0 @@ -.appModalWrapper { - .closeButton { - background-color: var(--background-color-primary); - - pointer-events: all; - - font-size: 1.2rem; - - transform: translate(-25px, -10px); - - svg { - margin: 0 !important; - - color: var(--text-color); - } - - &:hover { - background-color: var(--background-color-primary); - } - } - - .appModal { - pointer-events: all; - - display: flex; - flex-direction: column; - - align-self: center; - - border-radius: 8px; - - width: fit-content; - height: fit-content; - - transition: all 0.2s ease-in-out; - - //min-height: 500px; - min-width: 600px; - - padding: 30px; - - background-color: var(--background-color-accent); - color: var(--text-color); - } -} \ No newline at end of file diff --git a/packages/app/src/components/LoadMore/index.jsx b/packages/app/src/components/LoadMore/index.jsx index 9a1cf9df..9ed6f0a3 100755 --- a/packages/app/src/components/LoadMore/index.jsx +++ b/packages/app/src/components/LoadMore/index.jsx @@ -18,8 +18,6 @@ export default React.forwardRef((props, ref) => { const insideViewportCb = (entries) => { const { fetching, onBottom } = props - console.log("entries", entries) - entries.forEach(element => { if (element.intersectionRatio > 0 && !fetching) { onBottom() @@ -50,14 +48,14 @@ export default React.forwardRef((props, ref) => { > {children} - +
- {loadingComponent && React.createElement(loadingComponent)} - +
}) \ No newline at end of file diff --git a/packages/app/src/components/Music/PlaylistView/index.jsx b/packages/app/src/components/Music/PlaylistView/index.jsx index b096aaf9..4ef1530b 100755 --- a/packages/app/src/components/Music/PlaylistView/index.jsx +++ b/packages/app/src/components/Music/PlaylistView/index.jsx @@ -21,19 +21,19 @@ import MusicModel from "@models/music" import "./index.less" const PlaylistTypeDecorators = { - "single": (props) => + "single": () => Single , - "album": (props) => + "album": () => Album , - "ep": (props) => + "ep": () => EP , - "mix": (props) => + "mix": () => Mix , @@ -228,12 +228,14 @@ export default (props) => { return } + const playlistType = playlist.type?.toLowerCase() ?? "playlist" + return
@@ -257,9 +259,9 @@ export default (props) => {
{ - playlist.type && PlaylistTypeDecorators[playlist.type] &&
+ playlistType && PlaylistTypeDecorators[playlistType] &&
{ - PlaylistTypeDecorators[playlist.type]() + PlaylistTypeDecorators[playlistType]() }
} diff --git a/packages/app/src/components/PagePanels/index.jsx b/packages/app/src/components/PagePanels/index.jsx index 1655dbe4..13bde614 100755 --- a/packages/app/src/components/PagePanels/index.jsx +++ b/packages/app/src/components/PagePanels/index.jsx @@ -84,6 +84,10 @@ export class PagePanelWithNavMenu extends React.Component { return <> } + if (this.props.tabs.length === 0) { + return <> + } + // slip the active tab by splitting on "." if (!this.state.activeTab) { console.error("PagePanelWithNavMenu: activeTab is not defined") diff --git a/packages/app/src/components/PostCard/components/actions/index.jsx b/packages/app/src/components/PostCard/components/actions/index.jsx index 0550df22..d095439d 100755 --- a/packages/app/src/components/PostCard/components/actions/index.jsx +++ b/packages/app/src/components/PostCard/components/actions/index.jsx @@ -64,7 +64,7 @@ export default (props) => { onClickReply, } = props.actions ?? {} - const genItems = () => { + const generateMoreMenuItems = () => { let items = MoreActionsItems if (isSelf) { @@ -104,7 +104,7 @@ export default (props) => {
- -
-
+ + +
+
+ { + processString(messageRegexs)(this.state.data.message ?? "") + } +
+ { - processString(messageRegexs)(this.state.data.message ?? "") + !this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && }
+ + { - !this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && + !this.props.disableHasReplies && !!this.state.hasReplies &&
app.navigation.goToPost(this.state.data._id)} + > + View {this.state.hasReplies} replies +
}
- - - { - !this.props.disableHasReplies && !!this.state.hasReplies &&
app.navigation.goToPost(this.state.data._id)} - > - View {this.state.hasReplies} replies -
- } - + } } \ No newline at end of file diff --git a/packages/app/src/components/PostCard/index.less b/packages/app/src/components/PostCard/index.less index cc178712..5ec61855 100755 --- a/packages/app/src/components/PostCard/index.less +++ b/packages/app/src/components/PostCard/index.less @@ -33,6 +33,13 @@ color: var(--text-color); } + .post_card_content { + display: flex; + flex-direction: column; + + gap: 15px; + } + .post_content { position: relative; diff --git a/packages/app/src/components/RenderWindow/index.jsx b/packages/app/src/components/RenderWindow/index.jsx deleted file mode 100755 index d990bc70..00000000 --- a/packages/app/src/components/RenderWindow/index.jsx +++ /dev/null @@ -1,233 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom" -import { Rnd } from "react-rnd" -import { Icons } from "@components/Icons" - -import "./index.less" - -class DOMWindow { - constructor(props = {}) { - this.props = props - - this.id = this.props.id - this.key = 0 - - this.currentRender = null - this.root = document.getElementById("app_windows") - this.element = document.createElement("div") - - this.element.setAttribute("id", this.id) - this.element.setAttribute("key", this.key) - this.element.setAttribute("classname", this.props.className) - - // if props clickOutsideToClose is true, add event listener to close window - if (this.props.clickOutsideToClose) { - document.addEventListener("click", this.handleWrapperClick) - } - - // handle root container - if (!this.root) { - this.root = document.createElement("div") - this.root.setAttribute("id", "app_windows") - - document.body.append(this.root) - } - - // get all windows opened has container - const rootNodes = this.root.childNodes - - // ensure this window has last key from rootNode - if (rootNodes.length > 0) { - const lastChild = rootNodes[rootNodes.length - 1] - const lastChildKey = Number(lastChild.getAttribute("key")) - - this.key = lastChildKey + 1 - } - } - - handleWrapperClick = (event) => { - if (!this.currentRender) { - return - } - - // if click in not renderer fragment, close window - if (!this.element.contains(event.target)) { - this.remove() - } - } - - render = (fragment) => { - this.root.appendChild(this.element) - - this.currentRender = fragment - - return ReactDOM.render(fragment, this.element) - } - - remove = () => { - this.root.removeChild(this.element) - this.currentRender = null - } - - destroy = () => { - this.element.remove() - this.currentRender = null - } - - createDefaultWindow = (children, props) => { - return this.render( - {children} - ) - } -} - -class DefaultWindowRender extends React.Component { - state = { - actions: [], - dimensions: { - height: this.props.height ?? 600, - width: this.props.width ?? 400, - }, - position: this.props.defaultPosition, - visible: false, - } - - componentDidMount = () => { - this.setDefaultActions() - - if (typeof this.props.actions !== "undefined") { - if (Array.isArray(this.props.actions)) { - const actions = this.state.actions ?? [] - - this.props.actions.forEach((action) => { - actions.push(action) - }) - - this.setState({ actions }) - } - } - - if (!this.state.position) { - this.setState({ position: this.getCenterPosition() }) - } - - this.toggleVisibility(true) - } - - toggleVisibility = (to) => { - this.setState({ visible: to ?? !this.state.visible }) - } - - getCenterPosition = () => { - const dimensions = this.state?.dimensions ?? {} - - const windowHeight = dimensions.height ?? 600 - const windowWidth = dimensions.width ?? 400 - - return { - x: window.innerWidth / 2 - windowWidth / 2, - y: window.innerHeight / 2 - windowHeight / 2, - } - } - - setDefaultActions = () => { - const { actions } = this.state - - actions.push({ - key: "close", - render: () => , - onClick: () => { - this.props.destroy() - }, - }) - - this.setState({ actions }) - } - - renderActions = () => { - const actions = this.state.actions - - if (Array.isArray(actions)) { - return actions.map((action) => { - return ( -
- {React.isValidElement(action.render) ? action.render : React.createElement(action.render)} -
- ) - }) - } - - return null - } - - getComponentRender = () => { - return React.isValidElement(this.props.children) - ? React.cloneElement(this.props.children, this.props.renderProps) - : React.createElement(this.props.children, this.props.renderProps) - } - - render() { - const { position, dimensions, visible } = this.state - - if (!visible) { - return null - } - - return ( - { - this.setState({ - dimensions: { - width: ref.offsetWidth, - height: ref.offsetHeight, - }, - position, - }) - }} - minWidth={ - this.props.minWidth - } - minHeight={ - this.props.minHeight - } - enableResizing={ - this.props.enableResizing ?? true - } - disableDragging={ - this.props.disableDragging ?? false - } - dragHandleClassName={ - this.props.dragHandleClassName ?? "window_topbar" - } - bounds={ - this.props.bounds ?? "#root" - } - > - { - this.props.useWrapper - ?
-
-
{this.props.id}
-
{this.renderActions()}
-
- -
{this.getComponentRender()}
-
- : this.props.children - } -
- ) - } -} - -export { DOMWindow, DefaultWindowRender } \ No newline at end of file diff --git a/packages/app/src/components/RenderWindow/index.less b/packages/app/src/components/RenderWindow/index.less deleted file mode 100755 index 921e0306..00000000 --- a/packages/app/src/components/RenderWindow/index.less +++ /dev/null @@ -1,93 +0,0 @@ -@wrapper_background: rgba(255, 255, 255, 1); - -@topbar_height: 30px; -@topbar_background: rgba(0, 0, 0, 0.4); - -.window_wrapper { - border-radius: 12px; - - background-color: @wrapper_background; - border: 1px solid rgba(161, 133, 133, 0.2); - overflow: hidden; - - &.translucid { - border: unset; - background-color: rgba(0, 0, 0, 0.2); - - backdrop-filter: blur(10px); - --webkit-backdrop-filter: blur(10px); - filter: drop-shadow(8px 8px 10px rgba(0, 0, 0, 0.5)); - } -} - -.window_topbar { - position: sticky; - z-index: 51; - background-color: @topbar_background; - - height: @topbar_height; - width: 100%; - - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - - > div { - margin: 0 5px; - line-height: 0; - } - - .title { - margin-left: 20px; - - color: #fff - @topbar_background; - - font-size: 13px; - font-style: italic; - font-family: "JetBrains Mono", monospace; - } - - .actions { - display: flex; - flex-direction: row-reverse; - align-items: center; - justify-content: end; - - color: #fff - @topbar_background; - - > div { - transition: all 150ms ease-in-out; - margin-right: 10px; - cursor: pointer; - - height: fit-content; - width: fit-content; - line-height: 0; - - display: flex; - align-items: center; - justify-content: end; - } - > div:hover { - color: var(--primary-color); - } - } -} - -.window_body { - z-index: 50; - - padding: 10px 20px; - height: calc(100% - @topbar_height); - width: 100%; - - overflow: overlay; - user-select: text !important; - --webkit-user-select: text !important; - - > div { - user-select: text !important; - --webkit-user-select: text !important; - } -} diff --git a/packages/app/src/components/WidgetsBrowser/index.jsx b/packages/app/src/components/WidgetsBrowser/index.jsx index 1a987f4a..2d74ed9e 100755 --- a/packages/app/src/components/WidgetsBrowser/index.jsx +++ b/packages/app/src/components/WidgetsBrowser/index.jsx @@ -5,12 +5,11 @@ import { Icons } from "@components/Icons" import WidgetItemPreview from "@components/WidgetItemPreview" import useRequest from "comty.js/hooks/useRequest" -import WidgetModel from "comty.js/models/widget" import "./index.less" export const WidgetBrowser = (props) => { - const [L_Widgets, R_Widgets, E_Widgets, M_Widgets] = useRequest(WidgetModel.browse) + const [L_Widgets, R_Widgets, E_Widgets, M_Widgets] = [] const [searchValue, setSearchValue] = React.useState("") diff --git a/packages/app/src/components/index.js b/packages/app/src/components/index.js index 35b86883..4502f3b8 100755 --- a/packages/app/src/components/index.js +++ b/packages/app/src/components/index.js @@ -1,4 +1,3 @@ -import * as Layout from "./Layout" export { default as Footer } from "./Footer" export { default as RenderError } from "./RenderError" @@ -40,9 +39,4 @@ export { default as PostCreator } from "./PostCreator" export { default as UserBadges } from "./UserBadges" export { default as UserCard } from "./UserCard" export { default as FollowersList } from "./FollowersList" -export { default as UserPreview } from "./UserPreview" - -// OTHERS -export * as Window from "./RenderWindow" - -export { Layout } \ No newline at end of file +export { default as UserPreview } from "./UserPreview" \ No newline at end of file diff --git a/packages/app/src/cores/contextMenu/components/contextMenu/index.less b/packages/app/src/cores/contextMenu/components/contextMenu/index.less index 19ff4dfe..3900466f 100755 --- a/packages/app/src/cores/contextMenu/components/contextMenu/index.less +++ b/packages/app/src/cores/contextMenu/components/contextMenu/index.less @@ -1,5 +1,5 @@ .contextMenu { - position: absolute; + position: fixed; z-index: 100000; display: flex; @@ -8,7 +8,7 @@ top: 0; left: 0; - width: 200px; + width: 230px; height: fit-content; background-color: var(--background-color-primary); @@ -16,11 +16,11 @@ border-radius: 8px; - padding: 10px; + padding: 7px; - font-family: "Recursive", sans-serif; + font-weight: 600; + font-family: var(--fontFamily); font-size: 0.8rem; - font-weight: 500; color: var(--text-color); h1, @@ -42,7 +42,7 @@ align-items: center; justify-content: space-between; - padding: 10px 10px 10px 20px; + padding: 10px 10px 10px 15px; transition: all 50ms ease-in-out; @@ -52,7 +52,7 @@ &:hover { background-color: var(--background-color-accent); - padding-left: 25px; + padding-left: 18px; } &:active { diff --git a/packages/app/src/cores/contextMenu/context_menu.core.js b/packages/app/src/cores/contextMenu/context_menu.core.js index 407d9def..f618a295 100755 --- a/packages/app/src/cores/contextMenu/context_menu.core.js +++ b/packages/app/src/cores/contextMenu/context_menu.core.js @@ -1,10 +1,10 @@ import React from "react" import Core from "evite/src/core" -import { DOMWindow } from "@components/RenderWindow" import ContextMenu from "./components/contextMenu" -import InternalContexts from "@config/context-menu" +import DefaultContenxt from "@config/context-menu/default" +import PostCardContext from "@config/context-menu/post" export default class ContextMenuCore extends Core { static namespace = "contextMenu" @@ -15,13 +15,10 @@ export default class ContextMenuCore extends Core { registerContext: this.registerContext.bind(this), } - contexts = Object() - - DOMWindow = new DOMWindow({ - id: "contextMenu", - className: "contextMenuWrapper", - clickOutsideToClose: true, - }) + contexts = { + ...DefaultContenxt, + ...PostCardContext, + } async onInitialize() { if (app.isMobile) { @@ -65,27 +62,26 @@ export default class ContextMenuCore extends Core { contexts.push("default-context") } - for await (const context of contexts) { - let contextObject = this.contexts[context] || InternalContexts[context] + for await (const [index, context] of contexts.entries()) { + let contextObject = this.contexts[context] + + if (!contextObject) { + this.console.warn(`Context ${context} not found`) + continue + } if (typeof contextObject === "function") { - contextObject = await contextObject(parentElement, element, { + contextObject = await contextObject(items, parentElement, element, { close: this.hide, }) } // push divider - if (items.length > 0) { + if (contexts.length > 0 && index !== contexts.length - 1) { items.push({ type: "separator" }) } - - if (Array.isArray(contextObject)) { - items.push(...contextObject) - } else { - items.push(contextObject) - } } // fullfill each item with a correspondent index if missing declared @@ -142,10 +138,13 @@ export default class ContextMenuCore extends Core { } show(payload) { - this.DOMWindow.render(React.createElement(ContextMenu, payload)) + app.cores.window_mng.render("context-menu", React.createElement(ContextMenu, payload), { + createOrUpdate: true, + closeOnClickOutside: true, + }) } hide() { - this.DOMWindow.remove() + app.cores.window_mng.close("context-menu") } } \ No newline at end of file diff --git a/packages/app/src/cores/notifications/feedback.js b/packages/app/src/cores/notifications/feedback.js index 9095f064..aaab127b 100644 --- a/packages/app/src/cores/notifications/feedback.js +++ b/packages/app/src/cores/notifications/feedback.js @@ -12,7 +12,7 @@ class NotificationFeedback { return (window.app.cores.settings.get("sfx:notifications_volume") ?? 50) / 100 } - static playHaptic = async (options = {}) => { + static playHaptic = async () => { if (app.cores.settings.get("haptics:notifications_feedback")) { await Haptics.vibrate() } @@ -28,7 +28,11 @@ class NotificationFeedback { } } - static async feedback(type) { + static async feedback({ type = "notification", feedback = true }) { + if (!feedback) { + return false + } + NotificationFeedback.playHaptic(type) NotificationFeedback.playAudio(type) } diff --git a/packages/app/src/cores/notifications/notifications.core.js b/packages/app/src/cores/notifications/notifications.core.js index 314afff5..2776e29d 100755 --- a/packages/app/src/cores/notifications/notifications.core.js +++ b/packages/app/src/cores/notifications/notifications.core.js @@ -22,10 +22,15 @@ export default class NotificationCore extends Core { public = { new: this.new, + close: this.close, } - async new(notification, options = {}) { - NotificationUI.notify(notification, options) - NotificationFeedback.feedback(options.type) + async new(notification) { + NotificationUI.notify(notification) + NotificationFeedback.feedback(notification) + } + + async close(id) { + NotificationUI.close(id) } } \ No newline at end of file diff --git a/packages/app/src/cores/notifications/ui.jsx b/packages/app/src/cores/notifications/ui.jsx index 770f4a71..a7c296e9 100644 --- a/packages/app/src/cores/notifications/ui.jsx +++ b/packages/app/src/cores/notifications/ui.jsx @@ -5,12 +5,7 @@ import { notification as Notf, Space, Button } from "antd" import { Icons, createIconRender } from "@components/Icons" class NotificationUI { - static async notify( - notification, - options = { - type: "info" - } - ) { + static async notify(notification) { if (typeof notification === "string") { notification = { title: "New notification", @@ -19,8 +14,8 @@ class NotificationUI { } const notfObj = { - duration: options.duration ?? 4, - key: options.key ?? Date.now(), + duration: typeof notification.duration === "undefined" ? 4 : notification.duration, + key: notification.key ?? Date.now(), } if (notification.title) { @@ -73,11 +68,11 @@ class NotificationUI { notfObj.icon = React.isValidElement(notification.icon) ? notification.icon : (createIconRender(notification.icon) ?? ) } - if (Array.isArray(options.actions)) { + if (Array.isArray(notification.actions)) { notfObj.btn = ( { - options.actions.map((action, index) => { + notification.actions.map((action, index) => { const handleClick = () => { if (typeof action.onClick === "function") { action.onClick() @@ -101,11 +96,24 @@ class NotificationUI { ) } - if (typeof Notf[options.type] !== "function") { - options.type = "info" + if (typeof notification.closable) { + notfObj.closable = notification.closable } - return Notf[options.type](notfObj) + if (notification.type === "loading") { + notification.type = "open" + notfObj.icon = + } + + if (typeof Notf[notification.type] !== "function") { + notification.type = "info" + } + + return Notf[notification.type](notfObj) + } + + static close(key) { + Notf.destroy(key) } } diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js index 177b0572..9eeb8115 100755 --- a/packages/app/src/cores/player/player.core.js +++ b/packages/app/src/cores/player/player.core.js @@ -110,7 +110,8 @@ export default class Player extends Core { set: (target, prop, value) => { return false } - }) + }), + gradualFadeMs: Player.gradualFadeMs, } internalEvents = { @@ -128,10 +129,6 @@ export default class Player extends Core { }, } - wsEvents = { - - } - async onInitialize() { this.native_controls.initialize() @@ -695,6 +692,10 @@ export default class Player extends Core { } async resumePlayback() { + if (!this.state.playback_status === "playing") { + return true + } + return await new Promise((resolve, reject) => { if (!this.track_instance) { this.console.error("No audio instance") diff --git a/packages/app/src/cores/remoteStorage/remoteStorage.core.js b/packages/app/src/cores/remoteStorage/remoteStorage.core.js index f3f71bae..62810b5b 100755 --- a/packages/app/src/cores/remoteStorage/remoteStorage.core.js +++ b/packages/app/src/cores/remoteStorage/remoteStorage.core.js @@ -32,8 +32,7 @@ export default class RemoteStorage extends Core { const fn = async () => new Promise((resolve, reject) => { const uploader = new ChunkedUpload({ endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`, - // TODO: get chunk size from settings - splitChunkSize: 5 * 1024 * 1024, // 5MB in bytes + splitChunkSize: 5 * 1024 * 1024, file: file, service: service, }) diff --git a/packages/app/src/cores/style/style.core.jsx b/packages/app/src/cores/style/style.core.jsx index 151b9669..12096da6 100755 --- a/packages/app/src/cores/style/style.core.jsx +++ b/packages/app/src/cores/style/style.core.jsx @@ -239,8 +239,6 @@ export default class StyleCore extends Core { app.eventBus.emit("style.update", { ...this.public.mutation, }) - - ConfigProvider.config({ theme: this.public.mutation }) } applyVariant(variant = (this.public.theme.defaultVariant ?? "light")) { diff --git a/packages/app/src/cores/windows/components/defaultWindow/context.js b/packages/app/src/cores/windows/components/defaultWindow/context.js new file mode 100644 index 00000000..084160fb --- /dev/null +++ b/packages/app/src/cores/windows/components/defaultWindow/context.js @@ -0,0 +1,17 @@ +import React from "react" + +export default React.createContext({ + title: null, + close: () => { }, + updatePosition: () => { }, + updateDimensions: () => { }, + updateTitle: () => { }, + position: { + x: 0, + y: 0, + }, + dimensions: { + width: 0, + height: 0, + }, +}) \ No newline at end of file diff --git a/packages/app/src/cores/windows/components/defaultWindow/index.jsx b/packages/app/src/cores/windows/components/defaultWindow/index.jsx new file mode 100644 index 00000000..44e4b238 --- /dev/null +++ b/packages/app/src/cores/windows/components/defaultWindow/index.jsx @@ -0,0 +1,196 @@ +import React from "react" +import { Rnd } from "react-rnd" + +import { Icons } from "@components/Icons" + +import WindowContext from "./context" + +export default class DefaultWindowRender extends React.Component { + static contextType = WindowContext + + ref = React.createRef() + + state = { + renderError: false, + title: null, + actions: [], + dimensions: { + height: this.props.height ?? 600, + width: this.props.width ?? 400, + }, + position: this.props.defaultPosition, + visible: false, + } + + componentDidMount = () => { + this.setDefaultActions() + + if (typeof this.props.actions !== "undefined") { + if (Array.isArray(this.props.actions)) { + const actions = this.state.actions ?? [] + + this.props.actions.forEach((action) => { + actions.push(action) + }) + + this.setState({ actions }) + } + } + + if (!this.state.position) { + this.setState({ position: this.getCenterPosition() }) + } + + this.toggleVisibility(true) + } + + componentDidCatch = (error) => { + console.error(error) + + this.setState({ + renderError: error, + }) + } + + updateTitle = (title) => { + this.setState({ title }) + } + + updateDimensions = (dimensions = { + width: 0, + height: 0, + }) => { + this.ref.current?.updateSize({ + width: dimensions.width, + height: dimensions.height, + }) + } + + updatePosition = (position = { + x: 0, + y: 0, + }) => { + this.ref.current?.updatePosition({ + x: position.x, + y: position.y, + }) + } + + toggleVisibility = (to) => { + this.setState({ visible: to ?? !this.state.visible }) + } + + getCenterPosition = () => { + const dimensions = this.state?.dimensions ?? {} + + const windowHeight = dimensions.height ?? 600 + const windowWidth = dimensions.width ?? 400 + + return { + x: window.innerWidth / 2 - windowWidth / 2, + y: window.innerHeight / 2 - windowHeight / 2, + } + } + + setDefaultActions = () => { + const { actions } = this.state + + actions.push({ + key: "close", + render: () => , + onClick: () => { + this.props.close() + }, + }) + + this.setState({ actions }) + } + + renderActions = () => { + const actions = this.state.actions + + if (Array.isArray(actions)) { + return actions.map((action) => { + return ( +
+ {React.isValidElement(action.render) ? action.render : React.createElement(action.render)} +
+ ) + }) + } + + return null + } + + getComponentRender = () => { + const ctx = { + ...this.props, + updateTitle: this.updateTitle, + updateDimensions: this.updateDimensions, + updatePosition: this.updatePosition, + close: this.props.close, + position: this.state.position, + dimensions: this.state.dimensions, + } + + return + { + React.isValidElement(this.props.children) + ? React.cloneElement(this.props.children, ctx) + : React.createElement(this.props.children, ctx) + } + + } + + render() { + const { position, dimensions, visible } = this.state + + if (!visible) { + return null + } + + return +
+
{this.state.title}
+
{this.renderActions()}
+
+ +
+ { + this.state.renderError &&
+

Render Error

+ {this.state.renderError.message} +
+ } + { + !this.state.renderError && this.getComponentRender() + } +
+
+ } +} \ No newline at end of file diff --git a/packages/app/src/cores/windows/index.less b/packages/app/src/cores/windows/index.less new file mode 100644 index 00000000..83cd8a9c --- /dev/null +++ b/packages/app/src/cores/windows/index.less @@ -0,0 +1,115 @@ +@topbar_height: 30px; +@topbar_background: var(--background-color-accent); + +.window_wrapper { + border-radius: 12px; + + background-color: var(--background-color-primary); + border: 2px solid var(--border-color); + + //filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.5)); + + overflow: hidden; +} + +.window_topbar { + position: sticky; + + z-index: 51; + + background-color: @topbar_background; + + color: var(--text-color); + + height: @topbar_height; + width: 100%; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + >div { + margin: 0 5px; + line-height: 0; + } + + .title { + margin-left: 20px; + + color: var(--text-color); + + font-size: 13px; + font-style: italic; + font-family: "JetBrains Mono", monospace; + } + + .actions { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: end; + + color: var(--text-color); + + >div { + transition: all 150ms ease-in-out; + margin-right: 10px; + cursor: pointer; + + height: fit-content; + width: fit-content; + line-height: 0; + + display: flex; + align-items: center; + justify-content: end; + } + + >div:hover { + color: var(--primary-color); + } + } +} + +.window_body { + z-index: 50; + + padding: 10px 20px; + height: calc(100% - @topbar_height); + width: 100%; + + overflow: overlay; + user-select: text !important; + --webkit-user-select: text !important; + + color: var(--text-color); + + >div { + user-select: text !important; + --webkit-user-select: text !important; + } + + .render_error { + display: flex; + flex-direction: column; + + align-items: center; + + width: 100%; + height: 100%; + + h1 { + font-size: 2rem; + } + + code { + background-color: var(--background-color-accent); + + padding: 5px; + border-radius: 8px; + + user-select: text; + } + } +} \ No newline at end of file diff --git a/packages/app/src/cores/windows/windows.core.jsx b/packages/app/src/cores/windows/windows.core.jsx new file mode 100644 index 00000000..0ebe4dda --- /dev/null +++ b/packages/app/src/cores/windows/windows.core.jsx @@ -0,0 +1,151 @@ +import React from "react" +import Core from "evite/src/core" + +import { createRoot } from "react-dom/client" + +import DefaultWindow from "./components/defaultWindow" + +import DefaultWindowContext from "./components/defaultWindow/context" + +import "./index.less" + +export default class WindowManager extends Core { + static namespace = "window_mng" + + static idMount = "windows" + + root = null + + windows = [] + + public = { + close: this.close.bind(this), + render: this.render.bind(this), + } + + async onInitialize() { + this.root = document.createElement("div") + + this.root.setAttribute("id", this.constructor.idMount) + + document.body.append(this.root) + } + + handleWrapperClick = (id, event) => { + const element = this.root.querySelector(`#${id}`) + + if (element) { + if (!element.contains(event.target)) { + this.close(id) + } + } + } + + /** + * Creates a new element with the specified id and appends it to the root element. + * If the element already exists and createOrUpdate option is false, it will throw an error. + * If the element already exists and createOrUpdate option is true, it will update the element. + * If useFrame option is true, it wraps the fragment with a DefaultWindow component before rendering. + * + * @param {string} id - The id of the element to create or update. + * @param {ReactElement} fragment - The React element to render inside the created element. + * @param {Object} options - The options for creating or updating the element. + * @param {boolean} options.useFrame - Specifies whether to wrap the fragment with a DefaultWindow component. + * @param {boolean} options.createOrUpdate - Specifies whether to create a new element or update an existing one. + * @return {HTMLElement} The created or updated element. + */ + render( + id, + fragment, + { + useFrame = false, + createOrUpdate = false, + closeOnClickOutside = false, + } = {} + ) { + let element = document.createElement("div") + let node = null + let win = null + + // check if window already exist + if (this.root.querySelector(`#${id}`) && !createOrUpdate) { + const newId = `${id}_${Date.now()}` + + this.console.warn(`Window ${id} already exist, overwritting id to ${newId}.\nYou can use {createOrUpdate = true} option to force refresh render of window`) + + id = newId + } + + if (this.root.querySelector(`#${id}`) && createOrUpdate) { + element = document.getElementById(id) + + win = this.windows.find((_node) => { + return _node.id === id + }) + + if (win) { + node = win.node + } + } else { + element.setAttribute("id", id) + + this.root.appendChild(element) + + node = createRoot(element) + + win = { + id: id, + node: node, + } + + this.windows.push(win) + } + + if (useFrame) { + fragment = + {fragment} + + } + + if (closeOnClickOutside) { + document.addEventListener("click", (e) => this.handleWrapperClick(id, e)) + } + + node.render(React.cloneElement(fragment, { + close: () => { + this.close(id) + } + })) + + return element + } + + /** + * Closes the window with the given ID. + * + * @param {string} id - The ID of the window to be closed. + * @return {boolean} Returns true if the window was successfully closed, false otherwise. + */ + close(id) { + const element = document.getElementById(id) + + const win = this.windows.find((node) => { + return node.id === id + }) + + if (!win) { + this.console.warn(`Window ${id} not found`) + + return false + } + + win.node.unmount() + this.root.removeChild(element) + + this.windows = this.windows.filter((node) => { + return node.id !== id + }) + + return true + } +} \ No newline at end of file diff --git a/packages/app/src/hooks/useChat/index.js b/packages/app/src/hooks/useChat/index.js new file mode 100644 index 00000000..a19d4139 --- /dev/null +++ b/packages/app/src/hooks/useChat/index.js @@ -0,0 +1,98 @@ +import React from "react" + +export default (to_user_id) => { + const [socket, setSocket] = React.useState(null) + const [messages, setMessages] = React.useState([]) + const [scroller, setScroller] = React.useState(null) + + const [isLocalTyping, setIsLocalTyping] = React.useState(false) + const [isRemoteTyping, setIsRemoteTyping] = React.useState(false) + + const [timeoutOffTypingEvent, setTimeoutOffTypingEvent] = React.useState(null) + + async function sendMessage(message) { + emitTypingEvent(false) + + await socket.emit("chat:send:message", { + to_user_id: to_user_id, + content: message, + }) + } + + async function emitTypingEvent(to) { + if (isLocalTyping === true && to === true) { + return debouncedOffTypingEvent() + } + + await socket.emit("chat:state:typing", { + to_user_id: to_user_id, + is_typing: to, + }) + + setIsLocalTyping(to) + } + + async function debouncedOffTypingEvent() { + if (timeoutOffTypingEvent) { + clearTimeout(timeoutOffTypingEvent) + } + + setTimeoutOffTypingEvent(setTimeout(() => { + emitTypingEvent(false) + }, 5000)) + } + + const listenEvents = { + "chat:receive:message": (message) => { + setMessages((messages) => { + return [ + ...messages, + message + ] + }) + }, + "chat:state:typing": (state) => { + setIsRemoteTyping(state.is_typing) + }, + } + + React.useEffect(() => { + if (scroller?.current) { + const paddingBottom = scroller.current.style.paddingBottom.replace("px", "") + + scroller.current?.scrollTo({ + top: scroller.current.scrollHeight + paddingBottom, + behavior: "smooth", + }) + } + }, [messages]) + + React.useEffect(() => { + const targetSocket = app.cores.api.client().sockets.chats + + setSocket(targetSocket) + + for (const [event, handler] of Object.entries(listenEvents)) { + targetSocket.on(event, handler) + } + + return () => { + for (const [event, handler] of Object.entries(listenEvents)) { + targetSocket.off(event, handler) + } + + if (timeoutOffTypingEvent) { + clearTimeout(timeoutOffTypingEvent) + } + } + }, []) + + return { + sendMessage, + messages, + setMessages, + setScroller, + emitTypingEvent, + isRemoteTyping, + } +} \ No newline at end of file diff --git a/packages/app/src/hooks/useTextRoom/index.jsx b/packages/app/src/hooks/useTextRoom/index.jsx new file mode 100644 index 00000000..7008bff6 --- /dev/null +++ b/packages/app/src/hooks/useTextRoom/index.jsx @@ -0,0 +1,70 @@ +import React from "react" +import { EventEmitter } from "@foxify/events" + +function useTextRoom(route, options = { + persistent: false, +}) { + const eventEmitter = new EventEmitter() + + const [lines, setLines] = React.useState([]) + + const socket = app.cores.api.client().sockets.chats + + function pushToLines(line) { + setLines((lines) => { + return [ + ...lines, + line, + ] + }) + } + + function deleteLine(message) { + setLines((lines) => { + return lines.filter((line) => line._id !== message._id) + }) + } + + function send(payload) { + socket.emit("room:send:message", { + ...payload, + route: route, + }) + } + + const socketEvents = { + "room:message": (message) => { + eventEmitter.emit("room:message", message) + pushToLines(message) + }, + "room:delete:message": (message) => { + eventEmitter.emit("room:delete:message", message) + deleteLine(message) + } + } + + React.useEffect(() => { + socket.emit("join:room", { + ...options, + room: route, + }) + + for (const [event, handler] of Object.entries(socketEvents)) { + socket.on(event, handler) + } + + return () => { + socket.emit("leave:room", { + room: route, + }) + + for (const [event, handler] of Object.entries(socketEvents)) { + socket.off(event, handler) + } + } + }, []) + + return [send, lines, setLines, eventEmitter] +} + +export default useTextRoom \ No newline at end of file diff --git a/packages/app/src/layout.jsx b/packages/app/src/layout.jsx index e66e7e1a..665e09df 100755 --- a/packages/app/src/layout.jsx +++ b/packages/app/src/layout.jsx @@ -44,14 +44,7 @@ export default class Layout extends React.PureComponent { } transitionLayer.classList.remove("fade-opacity-leave") - }, - "router.navigate": async (path, options) => { - this.progressBar.start() - - await this.makePageTransition(options) - - this.progressBar.done() - }, + } } componentDidMount() { @@ -82,33 +75,6 @@ export default class Layout extends React.PureComponent { this.setState({ renderError: { info, stack } }) } - async makePageTransition(options = {}) { - if (document.startViewTransition) { - return document.startViewTransition(async () => { - await new Promise((resolve) => { - setTimeout(resolve, options.state?.transitionDelay ?? 250) - }) - }) - } - - const content_layout = document.getElementById("content_layout") - - if (!content_layout) { - console.warn("content_layout not found, no animation will be played") - - return false - } - - content_layout.classList.add("fade-transverse-leave") - - return await new Promise((resolve) => { - setTimeout(() => { - resolve() - content_layout.classList.remove("fade-transverse-leave") - }, options.state?.transitionDelay ?? 250) - }) - } - layoutInterface = window.app.layout = { set: (layout) => { if (typeof Layouts[layout] !== "function") { diff --git a/packages/app/src/components/Layout/bottomBar/index.jsx b/packages/app/src/layouts/components/bottomBar/index.jsx similarity index 99% rename from packages/app/src/components/Layout/bottomBar/index.jsx rename to packages/app/src/layouts/components/bottomBar/index.jsx index 09eed364..eff618e9 100755 --- a/packages/app/src/components/Layout/bottomBar/index.jsx +++ b/packages/app/src/layouts/components/bottomBar/index.jsx @@ -8,7 +8,7 @@ import { Icons, createIconRender } from "@components/Icons" import { WithPlayerContext, Context } from "@contexts/WithPlayerContext" -import { QuickNavMenuItems, QuickNavMenu } from "@components/Layout/quickNav" +import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/quickNav" import PlayerView from "@pages/@mobile-views/player" import CreatorView from "@pages/@mobile-views/creator" diff --git a/packages/app/src/components/Layout/bottomBar/index.less b/packages/app/src/layouts/components/bottomBar/index.less similarity index 100% rename from packages/app/src/components/Layout/bottomBar/index.less rename to packages/app/src/layouts/components/bottomBar/index.less diff --git a/packages/app/src/components/Layout/draggableDrawer/index.jsx b/packages/app/src/layouts/components/draggableDrawer/index.jsx similarity index 100% rename from packages/app/src/components/Layout/draggableDrawer/index.jsx rename to packages/app/src/layouts/components/draggableDrawer/index.jsx diff --git a/packages/app/src/components/Layout/draggableDrawer/index.less b/packages/app/src/layouts/components/draggableDrawer/index.less similarity index 100% rename from packages/app/src/components/Layout/draggableDrawer/index.less rename to packages/app/src/layouts/components/draggableDrawer/index.less diff --git a/packages/app/src/components/Layout/drawer/index.jsx b/packages/app/src/layouts/components/drawer/index.jsx similarity index 100% rename from packages/app/src/components/Layout/drawer/index.jsx rename to packages/app/src/layouts/components/drawer/index.jsx diff --git a/packages/app/src/layouts/components/floatingStack/index.jsx b/packages/app/src/layouts/components/floatingStack/index.jsx deleted file mode 100755 index c2852cbb..00000000 --- a/packages/app/src/layouts/components/floatingStack/index.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import React from "react" -import classnames from "classnames" - -import { DOMWindow } from "@components/RenderWindow" - -import "./index.less" - -class FloatingStackItem extends React.PureComponent { - state = { - renderError: null - } - - componentDidCatch(error, info) { - console.log(error, info) - - this.setState({ - renderError: error, - }) - } - - render() { - if (this.state.renderError) { - return
-

Render Error

-
- } - - return
- - {this.props.children} - -
- } -} - -export default class FloatingStack extends React.Component { - state = { - renders: [], - globalVisibility: true, - } - - public = { - add: (id, render) => { - try { - if (!id) { - console.error(`FloatingStack: id is required`) - return false - } - if (!render) { - console.error(`FloatingStack: render is required`) - return false - } - - if (this.state.renders.find((item) => item.id === id)) { - console.error(`FloatingStack: id ${id} already exists`) - return false - } - - this.setState({ - renders: [ - ...this.state.renders, - { - id, - render: React.createElement(render), - }, - ] - }) - - return render - } catch (error) { - console.log(error) - return null - } - }, - remove: (id) => { - this.setState({ - renders: this.state.renders.filter((item) => { - return item.id !== id - }) - }) - - return true - }, - toggleGlobalVisibility: (to) => { - if (typeof to !== "boolean") { - to = !this.state.globalVisibility - } - - this.setState({ - globalVisibility: to, - }) - } - } - - componentDidMount() { - window.app.layout.floatingStack = this.public - } - - componentWillUnmount() { - window.app.layout.floatingStack = null - delete window.app.layout.floatingStack - } - - render() { - return
- { - this.state.renders.map((item) => { - return - {item.render} - - }) - } -
- } -} - -export const createWithDom = () => { - const dom = new DOMWindow({ - id: "FloatingStack", - }) - - dom.render() - - return dom -} \ No newline at end of file diff --git a/packages/app/src/layouts/components/floatingStack/index.less b/packages/app/src/layouts/components/floatingStack/index.less deleted file mode 100755 index 0570a2bc..00000000 --- a/packages/app/src/layouts/components/floatingStack/index.less +++ /dev/null @@ -1,30 +0,0 @@ -.floating_stack { - position: absolute; - z-index: 300; - - right: 0; - bottom: 0; - - display: flex; - flex-direction: column; - - align-items: center; - justify-content: center; - - max-width: 450px; - - gap: 20px; - - margin: 20px; - - transition: all 0.3s ease-in-out; - - &.hidden { - transform: translateX(100%); - opacity: 0; - } -} - -.floating_stack_item { - width: 100%; -} \ No newline at end of file diff --git a/packages/app/src/components/Layout/header/index.jsx b/packages/app/src/layouts/components/header/index.jsx similarity index 100% rename from packages/app/src/components/Layout/header/index.jsx rename to packages/app/src/layouts/components/header/index.jsx diff --git a/packages/app/src/components/Layout/header/index.less b/packages/app/src/layouts/components/header/index.less similarity index 91% rename from packages/app/src/components/Layout/header/index.less rename to packages/app/src/layouts/components/header/index.less index 5aa4f24a..078c6ee0 100755 --- a/packages/app/src/components/Layout/header/index.less +++ b/packages/app/src/layouts/components/header/index.less @@ -12,7 +12,9 @@ width: 100%; .page_header { - display: block; + display: flex; + flex-direction: column; + margin: 10px 0 20px 0; padding: 5px; diff --git a/packages/app/src/layouts/components/modals/index.jsx b/packages/app/src/layouts/components/modals/index.jsx index 24f53b76..150387ab 100755 --- a/packages/app/src/layouts/components/modals/index.jsx +++ b/packages/app/src/layouts/components/modals/index.jsx @@ -3,7 +3,6 @@ import { Modal as AntdModal } from "antd" import classnames from "classnames" import { Icons } from "@components/Icons" -import { DOMWindow } from "@components/RenderWindow" import useLayoutInterface from "@hooks/useLayoutInterface" @@ -118,8 +117,6 @@ export default () => { render, { framed = true, - frameContentStyle = null, - includeCloseButton = false, confirmOnOutsideClick = false, confirmOnClickTitle, @@ -129,20 +126,13 @@ export default () => { props, } = {} ) { - const win = new DOMWindow({ - id: id, - className: className, - }) - - win.render( { - win.destroy() + app.cores.window_mng.close(id) }} - includeCloseButton={includeCloseButton} framed={framed} - frameContentStyle={frameContentStyle} + className={className} confirmOnOutsideClick={confirmOnOutsideClick} confirmOnClickTitle={confirmOnClickTitle} confirmOnClickContent={confirmOnClickContent} diff --git a/packages/app/src/layouts/components/modals/index.less b/packages/app/src/layouts/components/modals/index.less index 3e30784c..45275f76 100755 --- a/packages/app/src/layouts/components/modals/index.less +++ b/packages/app/src/layouts/components/modals/index.less @@ -23,7 +23,7 @@ top: 0; left: 0; - + width: 100vw; height: 100vh; @@ -43,7 +43,7 @@ } &.active { - background-color: rgba(var(--bg_color_6), 0.5); + background-color: rgba(var(--bg_color_6), 0.1); backdrop-filter: blur(@modal_background_blur); -webkit-backdrop-filter: blur(@modal_background_blur); diff --git a/packages/app/src/components/Layout/quickNav/index.jsx b/packages/app/src/layouts/components/quickNav/index.jsx similarity index 100% rename from packages/app/src/components/Layout/quickNav/index.jsx rename to packages/app/src/layouts/components/quickNav/index.jsx diff --git a/packages/app/src/components/Layout/sidebar/index.jsx b/packages/app/src/layouts/components/sidebar/index.jsx similarity index 68% rename from packages/app/src/components/Layout/sidebar/index.jsx rename to packages/app/src/layouts/components/sidebar/index.jsx index a49a658f..523dffb1 100755 --- a/packages/app/src/components/Layout/sidebar/index.jsx +++ b/packages/app/src/layouts/components/sidebar/index.jsx @@ -31,6 +31,9 @@ const onClickHandlers = { search: () => { window.app.controls.openSearcher() }, + messages: () => { + window.app.controls.openMessages() + }, create: () => { window.app.controls.openCreator() }, @@ -63,6 +66,37 @@ const generateTopItems = (extra = []) => { }) } +const BottomMenuDefaultItems = [ + { + key: "search", + label: + {(t) => t("Search")} + , + icon: , + }, + { + key: "messages", + label: + {(t) => t("Messages")} + , + icon: , + }, + { + key: "notifications", + label: + {(t) => t("Notifications")} + , + icon: , + }, + { + key: "settings", + label: + {(t) => t("Settings")} + , + icon: , + } +] + const ActionMenuItems = [ { key: "account", @@ -268,7 +302,7 @@ export default class Sidebar extends React.Component { } handleClick = (e) => { - if (e.item.props.ignoreClick) { + if (e.item.props.ignore_click === "true") { return } @@ -331,144 +365,117 @@ export default class Sidebar extends React.Component { } } + getBottomItems = () => { + const items = [ + ...BottomMenuDefaultItems, + ...this.state.bottomItems, + ] + + if (app.userData) { + items.push({ + key: "account", + ignore_click: "true", + className: "user_avatar", + label: + + , + }) + } + + if (!app.userData) { + items.push({ + key: "login", + label: + {t => t("Login")} + , + icon: , + }) + } + + return items + } + render() { const defaultSelectedKey = window.location.pathname.replace("/", "") - return <> - - {({ x }) => { - return
+ return + {({ x }) => { + return
+ visible: this.state.visible, } + )} + style={{ + transform: `translateX(-${x}%)`, + }} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.handleMouseLeave} + > + { + window.__TAURI__ && navigator.platform.includes("Mac") &&
+ } -
+ ) + } + ref={this.sidebarRef} + > -
-
- -
-
- -
- -
- -
- - { - this.state.bottomItems.map((item) => { - if (item.noContainer) { - return React.createElement(item.children, item.childrenProps) - } - - return - { - React.createElement(item.children, item.childrenProps) - } - - }) - } - } > - - {(t) => t("Search")} - - - }> - - {t => t("Notifications")} - - - }> - - {t => t("Settings")} - - - - { - app.userData && - - - - - } - - { - !app.userData && }> - - {t => t("Login")} - - - } - +
+
+
+ +
+ +
+ +
+ +
- }} - - +
+ }} + } } \ No newline at end of file diff --git a/packages/app/src/components/Layout/sidebar/index.less b/packages/app/src/layouts/components/sidebar/index.less similarity index 93% rename from packages/app/src/components/Layout/sidebar/index.less rename to packages/app/src/layouts/components/sidebar/index.less index 84e1d961..a26c31e7 100755 --- a/packages/app/src/components/Layout/sidebar/index.less +++ b/packages/app/src/layouts/components/sidebar/index.less @@ -173,15 +173,27 @@ &.user_avatar { .ant-menu-title-content { + width: 100%; + display: inline-flex; - + align-items: flex-start; justify-content: center; - width: fit-content; opacity: 1; + + .ant-dropdown-trigger { + width: 100%; + + img { + width: fit-content; + border-radius: 10px; + } + } } + + padding: 0 !important; } } diff --git a/packages/app/src/components/Layout/sidedrawer/index.jsx b/packages/app/src/layouts/components/sidedrawer/index.jsx similarity index 100% rename from packages/app/src/components/Layout/sidedrawer/index.jsx rename to packages/app/src/layouts/components/sidedrawer/index.jsx diff --git a/packages/app/src/components/Layout/sidedrawer/index.less b/packages/app/src/layouts/components/sidedrawer/index.less similarity index 100% rename from packages/app/src/components/Layout/sidedrawer/index.less rename to packages/app/src/layouts/components/sidedrawer/index.less diff --git a/packages/app/src/components/Layout/toolsBar/index.jsx b/packages/app/src/layouts/components/toolsBar/index.jsx similarity index 100% rename from packages/app/src/components/Layout/toolsBar/index.jsx rename to packages/app/src/layouts/components/toolsBar/index.jsx diff --git a/packages/app/src/components/Layout/toolsBar/index.less b/packages/app/src/layouts/components/toolsBar/index.less similarity index 98% rename from packages/app/src/components/Layout/toolsBar/index.less rename to packages/app/src/layouts/components/toolsBar/index.less index 4e0a6a9e..a34b8e07 100755 --- a/packages/app/src/components/Layout/toolsBar/index.less +++ b/packages/app/src/layouts/components/toolsBar/index.less @@ -6,7 +6,7 @@ top: 0; right: 0; - max-width: 20vw; + max-width: 420px; min-width: 320px; height: 100vh; diff --git a/packages/app/src/components/Layout/topBar/index.jsx b/packages/app/src/layouts/components/topBar/index.jsx similarity index 100% rename from packages/app/src/components/Layout/topBar/index.jsx rename to packages/app/src/layouts/components/topBar/index.jsx diff --git a/packages/app/src/components/Layout/topBar/index.less b/packages/app/src/layouts/components/topBar/index.less similarity index 100% rename from packages/app/src/components/Layout/topBar/index.less rename to packages/app/src/layouts/components/topBar/index.less diff --git a/packages/app/src/layouts/default/index.jsx b/packages/app/src/layouts/default/index.jsx index 3ff362b4..fb864856 100755 --- a/packages/app/src/layouts/default/index.jsx +++ b/packages/app/src/layouts/default/index.jsx @@ -2,32 +2,20 @@ import React from "react" import classnames from "classnames" import { Layout } from "antd" -import { - Sidebar, - Drawer, - Sidedrawer, - BottomBar, - TopBar, - ToolsBar, - Header, -} from "@components/Layout" +import Sidebar from "@layouts/components/sidebar" +import Drawer from "@layouts/components/drawer" +import Sidedrawer from "@layouts/components/sidedrawer" +import BottomBar from "@layouts/components/bottomBar" +import TopBar from "@layouts/components/topBar" +import ToolsBar from "@layouts/components/toolsBar" +import Header from "@layouts/components/header" +import InitializeModalsController from "@layouts/components/modals" import BackgroundDecorator from "@components/BackgroundDecorator" -import { createWithDom as FloatingStack } from "../components/floatingStack" -import InitializeModalsController from "../components/modals" - const DesktopLayout = (props) => { InitializeModalsController() - React.useEffect(() => { - const floatingStack = FloatingStack() - - return () => { - floatingStack.remove() - } - }, []) - return <> diff --git a/packages/app/src/layouts/minimal/index.jsx b/packages/app/src/layouts/minimal/index.jsx index 3b71a073..1abc646b 100755 --- a/packages/app/src/layouts/minimal/index.jsx +++ b/packages/app/src/layouts/minimal/index.jsx @@ -2,7 +2,8 @@ import React from "react" import * as antd from "antd" import classnames from "classnames" -import { Drawer, Sidedrawer } from "@components/Layout" +import Drawer from "@layouts/components/drawer" +import Sidedrawer from "@layouts/components/sidedrawer" export default (props) => { return diff --git a/packages/app/src/pages/account/index.jsx b/packages/app/src/pages/account/index.jsx index 83035c39..39e342ef 100755 --- a/packages/app/src/pages/account/index.jsx +++ b/packages/app/src/pages/account/index.jsx @@ -94,7 +94,6 @@ export default class Account extends React.Component { }) } - onClickFollow = async () => { const result = await FollowsModel.toggleFollow({ user_id: this.state.user._id, @@ -183,6 +182,13 @@ export default class Account extends React.Component { followed={this.state.following} self={this.state.isSelf} /> + + { + !this.state.isSelf && } + onClick={() => app.location.push(`/messages/${user._id}`)} + /> + }
diff --git a/packages/app/src/pages/account/index.less b/packages/app/src/pages/account/index.less index a832c54b..d9888f58 100755 --- a/packages/app/src/pages/account/index.less +++ b/packages/app/src/pages/account/index.less @@ -108,7 +108,9 @@ .actions { display: flex; - flex-direction: column; + flex-direction: row; + + gap: 10px; height: fit-content; width: 20vw; diff --git a/packages/app/src/pages/auth/forms/selector/index.jsx b/packages/app/src/pages/auth/forms/selector/index.jsx index 7b38bc30..f47247b7 100755 --- a/packages/app/src/pages/auth/forms/selector/index.jsx +++ b/packages/app/src/pages/auth/forms/selector/index.jsx @@ -17,14 +17,14 @@ const MainSelector = (props) => {
{ app.userData && { - app.navigation.goMain() - }} - > - Continue as {app.userData.username} - + type="default" + size="large" + onClick={() => { + app.navigation.goMain() + }} + > + Continue as {app.userData.username} + } element.clientHeight || element.scrollWidth > element.clientWidth; +} + +const RenderArtist = (props) => { + const { artist } = props + + if (!artist) { + return null + } + + if (Array.isArray(artist)) { + return

{artist.join(",")}

+ } + + return

{artist}

+} + +const RenderAlbum = (props) => { + const { album } = props + + if (!album) { + return null + } + + if (Array.isArray(album)) { + return

{album.join(",")}

+ } + + return

{album}

+} + +const PlayerController = React.forwardRef((props, ref) => { + const context = React.useContext(Context) + + const titleRef = React.useRef() + + const [titleIsOverflown, setTitleIsOverflown] = React.useState(false) + + const [currentTime, setCurrentTime] = React.useState(0) + const [trackDuration, setTrackDuration] = React.useState(0) + const [draggingTime, setDraggingTime] = React.useState(false) + const [currentDragWidth, setCurrentDragWidth] = React.useState(0) + const [syncInterval, setSyncInterval] = React.useState(null) + + async function onDragEnd(seekTime) { + setDraggingTime(false) + + app.cores.player.seek(seekTime) + } + + async function syncPlayback() { + if (!context.track_manifest) { + return false + } + + const currentTrackTime = app.cores.player.seek() + + setCurrentTime(currentTrackTime) + } + + //* Handle when playback status change + React.useEffect(() => { + if (context.playback_status === "playing") { + setSyncInterval(setInterval(syncPlayback, 1000)) + } else { + if (syncInterval) { + clearInterval(syncInterval) + } + } + }, [context.playback_status]) + + React.useEffect(() => { + setTitleIsOverflown(isOverflown(titleRef.current)) + setTrackDuration(app.cores.player.duration()) + }, [context.track_manifest]) + + const isStopped = context.playback_status === "stopped" + + return
+
+
+
+ { +

+ { + context.playback_status === "stopped" ? "Nothing is playing" : <> + {context.track_manifest?.title ?? "Nothing is playing"} + + } +

+ } + + { + titleIsOverflown && +

+ { + isStopped ? + "Nothing is playing" : + <> + {context.track_manifest?.title ?? "Untitled"} + + } +

+
+ } +
+ +
+ + - + +
+
+ + + +
+
{ + setDraggingTime(true) + }} + onMouseUp={(e) => { + const rect = e.currentTarget.getBoundingClientRect() + const seekTime = trackDuration * (e.clientX - rect.left) / rect.width + + onDragEnd(seekTime) + }} + onMouseMove={(e) => { + const rect = e.currentTarget.getBoundingClientRect() + const atWidth = (e.clientX - rect.left) / rect.width * 100 + + setCurrentDragWidth(atWidth) + }} + > +
+
+
+ +
+ { + context.track_manifest?.metadata.lossless && } + bordered={false} + > + Lossless + + } + { + context.track_manifest?.explicit && + Explicit + + } +
+
+
+}) + +export default PlayerController \ No newline at end of file diff --git a/packages/app/src/pages/lyrics/components/text/index.jsx b/packages/app/src/pages/lyrics/components/text/index.jsx new file mode 100644 index 00000000..f4b0485e --- /dev/null +++ b/packages/app/src/pages/lyrics/components/text/index.jsx @@ -0,0 +1,147 @@ +import React from "react" +import classnames from "classnames" +import { Motion, spring } from "react-motion" + +import { Context } from "@contexts/WithPlayerContext" + +const LyricsText = React.forwardRef((props, textRef) => { + const context = React.useContext(Context) + + const { lyrics } = props + + const [syncInterval, setSyncInterval] = React.useState(null) + const [currentLineIndex, setCurrentLineIndex] = React.useState(0) + const [visible, setVisible] = React.useState(false) + + function syncPlayback() { + if (!lyrics) { + return false + } + + const currentTrackTime = app.cores.player.seek() * 1000 + + const lineIndex = lyrics.lrc.findIndex((line) => { + return currentTrackTime >= line.startTimeMs && currentTrackTime <= line.endTimeMs + }) + + if (lineIndex === -1) { + if (!visible) { + setVisible(false) + } + + return false + } + + const line = lyrics.lrc[lineIndex] + + setCurrentLineIndex(lineIndex) + + if (line.break) { + return setVisible(false) + } + + if (line.text) { + return setVisible(true) + } + } + + function startSyncInterval() { + setSyncInterval(setInterval(syncPlayback, 100)) + } + + //* Handle when current line index change + React.useEffect(() => { + if (currentLineIndex === 0) { + setVisible(false) + } else { + // find line element by id + const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`) + + // center scroll to current line + if (lineElement) { + lineElement.scrollIntoView({ + behavior: "smooth", + block: "center", + }) + } + } + }, [currentLineIndex]) + + //* Handle when playback status change + React.useEffect(() => { + if (lyrics) { + if (typeof lyrics?.lrc !== "undefined") { + if (context.playback_status === "playing") { + startSyncInterval() + } else { + if (syncInterval) { + clearInterval(syncInterval) + } + } startSyncInterval() + } + } + + }, [context.playback_status]) + + //* Handle when lyrics object change + React.useEffect(() => { + clearInterval(syncInterval) + + if (lyrics) { + if (typeof lyrics?.lrc !== "undefined") { + if (context.playback_status === "playing") { + startSyncInterval() + } + } + } + }, [lyrics]) + + React.useEffect(() => { + return () => { + clearInterval(syncInterval) + } + }, []) + + if (!lyrics?.lrc) { + return null + } + + return
+ + {({ opacity }) => { + return
+ { + lyrics.lrc.map((line, index) => { + return

+ {line.text} +

+ }) + } +
+ }} +
+
+}) + +export default LyricsText \ No newline at end of file diff --git a/packages/app/src/pages/lyrics/components/video/index.jsx b/packages/app/src/pages/lyrics/components/video/index.jsx new file mode 100644 index 00000000..d8df998f --- /dev/null +++ b/packages/app/src/pages/lyrics/components/video/index.jsx @@ -0,0 +1,166 @@ +import React from "react" + +import { Context } from "@contexts/WithPlayerContext" + +const maxLatencyInMs = 55 + +const LyricsVideo = React.forwardRef((props, videoRef) => { + const context = React.useContext(Context) + + const { lyrics } = props + + const [syncInterval, setSyncInterval] = React.useState(null) + const [syncingVideo, setSyncingVideo] = React.useState(false) + const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0) + + async function seekVideoToSyncAudio() { + if (lyrics) { + if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") { + const currentTrackTime = app.cores.player.seek() + + setSyncingVideo(true) + + videoRef.current.currentTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000 + } + } + } + + async function syncPlayback() { + if (!lyrics) { + return false + } + + // if `sync_audio_at_ms` is present, it means the video must be synced with audio + if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") { + const currentTrackTime = app.cores.player.seek() + const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000) + + //console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`) + + const maxOffset = maxLatencyInMs / 1000 + const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime) + + setCurrentVideoLatency(currentVideoTimeDiff) + + if (syncingVideo === true) { + console.log(`Syncing video...`) + return false + } + + if (currentVideoTimeDiff > maxOffset) { + console.warn(`Video offset exceeds`, maxOffset) + seekVideoToSyncAudio() + } + } + } + + function startSyncInterval() { + setSyncInterval(setInterval(syncPlayback, 100)) + } + + React.useEffect(() => { + videoRef.current.addEventListener("seeked", (event) => { + setSyncingVideo(false) + }) + + // videoRef.current.addEventListener("error", (event) => { + // console.log("Failed to load", event) + // }) + + // videoRef.current.addEventListener("ended", (event) => { + // console.log("Video ended", event) + // }) + + // videoRef.current.addEventListener("stalled", (event) => { + // console.log("Failed to fetch data, but trying") + // }) + + // videoRef.current.addEventListener("waiting", (event) => { + // console.log("Waiting for data...") + // }) + }, []) + + //* Handle when playback status change + React.useEffect(() => { + if (typeof lyrics?.sync_audio_at_ms !== "undefined") { + if (context.playback_status === "playing") { + videoRef.current.play() + + setSyncInterval(setInterval(syncPlayback, 500)) + } else { + videoRef.current.pause() + + if (syncInterval) { + clearInterval(syncInterval) + } + } + } + }, [context.playback_status]) + + //* Handle when lyrics object change + React.useEffect(() => { + if (lyrics) { + clearInterval(syncInterval) + setCurrentVideoLatency(0) + setSyncingVideo(false) + + if (lyrics.video_source) { + videoRef.current.src = lyrics.video_source + + videoRef.current.load() + + if (typeof lyrics.sync_audio_at_ms !== "undefined") { + videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000 + + if (context.playback_status === "playing") { + videoRef.current.play() + startSyncInterval() + } else { + videoRef.current.pause() + } + + const currentTime = app.cores.player.seek() + + if (currentTime > 0) { + seekVideoToSyncAudio() + } + } else { + videoRef.current.loop = true + videoRef.current.play() + } + } + } + }, [lyrics]) + + React.useEffect(() => { + clearInterval(syncInterval) + + return () => { + clearInterval(syncInterval) + } + }, []) + + return <> +
+
+

Maximun latency

+

{maxLatencyInMs}ms

+
+
+

Video Latency

+

{(currentVideoLatency * 1000).toFixed(2)}ms

+
+ {syncingVideo ?

Syncing video...

: null} +
+ +