mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
fix depecrated deps
This commit is contained in:
parent
79f64eaec2
commit
3cf055c72c
@ -1,17 +1,18 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
"@": path.join(__dirname, "src"),
|
"@": path.join(__dirname, "src"),
|
||||||
"@config": path.join(__dirname, "config"),
|
"@config": path.join(__dirname, "config"),
|
||||||
"@cores": path.join(__dirname, "src/cores"),
|
"@cores": path.join(__dirname, "src/cores"),
|
||||||
"@pages": path.join(__dirname, "src/pages"),
|
"@pages": path.join(__dirname, "src/pages"),
|
||||||
"@styles": path.join(__dirname, "src/styles"),
|
"@styles": path.join(__dirname, "src/styles"),
|
||||||
"@components": path.join(__dirname, "src/components"),
|
"@components": path.join(__dirname, "src/components"),
|
||||||
"@contexts": path.join(__dirname, "src/contexts"),
|
"@contexts": path.join(__dirname, "src/contexts"),
|
||||||
"@utils": path.join(__dirname, "src/utils"),
|
"@utils": path.join(__dirname, "src/utils"),
|
||||||
"@layouts": path.join(__dirname, "src/layouts"),
|
"@layouts": path.join(__dirname, "src/layouts"),
|
||||||
"@hooks": path.join(__dirname, "src/hooks"),
|
"@hooks": path.join(__dirname, "src/hooks"),
|
||||||
"@classes": path.join(__dirname, "src/classes"),
|
"@classes": path.join(__dirname, "src/classes"),
|
||||||
"@models": path.join(__dirname, "../../", "comty.js/src/models"),
|
"@models": path.join(__dirname, "../../", "comty.js/src/models"),
|
||||||
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
||||||
}
|
"@ragestudio/vessel": path.join(__dirname, "../../", "vessel", "src"),
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"version": "1.25.0-a",
|
"version": "1.25.0-a",
|
||||||
"license": "ComtyLicense",
|
"license": "ComtyLicense",
|
||||||
"main": "electron/main",
|
"main": "electron/main",
|
||||||
|
"type": "module",
|
||||||
"author": "RageStudio",
|
"author": "RageStudio",
|
||||||
"description": "A prototype of a social network.",
|
"description": "A prototype of a social network.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -13,78 +14,61 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.4.0",
|
"@ant-design/icons": "^5.4.0",
|
||||||
"@capacitor/android": "^5.0.5",
|
|
||||||
"@capacitor/app": "^5.0.3",
|
|
||||||
"@capacitor/assets": "^2.0.4",
|
|
||||||
"@capacitor/cli": "^5.0.5",
|
|
||||||
"@capacitor/core": "^5.0.5",
|
|
||||||
"@capacitor/haptics": "1.1.4",
|
|
||||||
"@capacitor/splash-screen": "^5.0.4",
|
|
||||||
"@capacitor/status-bar": "^5.0.4",
|
|
||||||
"@capacitor/storage": "^1.2.5",
|
|
||||||
"@capgo/capacitor-updater": "^5.0.1",
|
|
||||||
"@dnd-kit/core": "^6.0.8",
|
"@dnd-kit/core": "^6.0.8",
|
||||||
"@dnd-kit/sortable": "^7.0.2",
|
"@dnd-kit/sortable": "^7.0.2",
|
||||||
"@emotion/react": "^11.13.0",
|
"@emotion/react": "^11.13.0",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||||
"@ffmpeg/util": "^0.12.1",
|
"@ffmpeg/util": "^0.12.1",
|
||||||
"@loadable/component": "5.15.2",
|
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
"@ragestudio/cordova-nfc": "^1.2.0",
|
"@ragestudio/cordova-nfc": "^1.2.0",
|
||||||
"@ragestudio/vessel": "^0.18.1",
|
"@ragestudio/vessel": "^0.19.0",
|
||||||
"@sentry/browser": "^7.64.0",
|
"@sentry/browser": "^7.64.0",
|
||||||
"@tauri-apps/api": "^1.5.4",
|
"@tauri-apps/api": "^1.5.4",
|
||||||
"@tsmx/human-readable": "^1.0.7",
|
"@tsmx/human-readable": "^1.0.7",
|
||||||
"antd": "^5.20.6",
|
"antd": "^5.20.6",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||||
"capacitor-music-controls-plugin-v3": "^1.1.0",
|
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"dashjs": "^4.7.4",
|
"dashjs": "^4.7.4",
|
||||||
"dompurify": "^3.0.0",
|
"dompurify": "^3.0.0",
|
||||||
"fast-average-color": "^9.2.0",
|
"fast-average-color": "^9.2.0",
|
||||||
"framer-motion": "^10.12.17",
|
|
||||||
"fuse.js": "6.5.3",
|
"fuse.js": "6.5.3",
|
||||||
"hls.js": "^1.5.17",
|
"hls.js": "^1.5.17",
|
||||||
"howler": "2.2.3",
|
"howler": "2.2.3",
|
||||||
"i18next": "21.6.6",
|
"i18next": "21.6.6",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"jsmediatags": "^3.9.7",
|
"jsmediatags": "^3.9.7",
|
||||||
"less": "4.1.2",
|
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"luxon": "^3.0.4",
|
"luxon": "^3.0.4",
|
||||||
"million": "^2.6.4",
|
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
|
"motion": "^12.4.2",
|
||||||
"mpegts.js": "^1.6.10",
|
"mpegts.js": "^1.6.10",
|
||||||
"nprogress": "^0.2.0",
|
"plyr": "^3.7.8",
|
||||||
"plyr": "^3.6.12",
|
|
||||||
"plyr-react": "^3.2.1",
|
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-color": "2.19.3",
|
"react-color": "2.19.3",
|
||||||
"react-countup": "^6.4.1",
|
"react-countup": "^6.4.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
"react-fast-marquee": "^1.3.5",
|
"react-fast-marquee": "^1.3.5",
|
||||||
"react-helmet": "6.1.0",
|
|
||||||
"react-i18next": "11.15.3",
|
"react-i18next": "11.15.3",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-lazy-load-image-component": "^1.5.4",
|
"react-lazy-load-image-component": "^1.5.4",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-modal-image": "^2.6.0",
|
"react-modal-image": "^2.6.0",
|
||||||
"react-motion": "0.5.2",
|
"react-player": "^2.16.0",
|
||||||
"react-rnd": "10.3.5",
|
"react-rnd": "^10.4.14",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-useanimations": "^2.10.0",
|
"react-useanimations": "^2.10.0",
|
||||||
"realtime-bpm-analyzer": "^3.2.1",
|
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"rxjs": "^7.5.5",
|
"rxjs": "^7.5.5",
|
||||||
"store": "^2.0.12",
|
"store": "^2.0.12",
|
||||||
|
"swapy": "^1.0.5",
|
||||||
"ua-parser-js": "^1.0.36",
|
"ua-parser-js": "^1.0.36",
|
||||||
"vaul": "^0.9.2",
|
"vaul": "^1.1.2",
|
||||||
"vite": "^5.4.4"
|
"vite": "^5.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import "./patches"
|
|||||||
import config from "@config"
|
import config from "@config"
|
||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
import { Runtime } from "@ragestudio/vessel"
|
import { Runtime } from "@ragestudio/vessel"
|
||||||
import { Helmet } from "react-helmet"
|
import { Helmet } from "react-helmet"
|
||||||
import { Translation } from "react-i18next"
|
import { Translation } from "react-i18next"
|
||||||
@ -10,10 +11,6 @@ import { invoke } from "@tauri-apps/api/tauri"
|
|||||||
import { Lightbox } from "react-modal-image"
|
import { Lightbox } from "react-modal-image"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
|
||||||
import { StatusBar, Style } from "@capacitor/status-bar"
|
|
||||||
import { App as CapacitorApp } from "@capacitor/app"
|
|
||||||
import { CapacitorUpdater } from "@capgo/capacitor-updater"
|
|
||||||
|
|
||||||
import AppsMenu from "@components/AppMenu"
|
import AppsMenu from "@components/AppMenu"
|
||||||
|
|
||||||
import AuthModel from "@models/auth"
|
import AuthModel from "@models/auth"
|
||||||
@ -41,9 +38,9 @@ import Splash from "./splash"
|
|||||||
|
|
||||||
import "@styles/index.less"
|
import "@styles/index.less"
|
||||||
|
|
||||||
if (IS_MOBILE_HOST) {
|
// if (IS_MOBILE_HOST) {
|
||||||
CapacitorUpdater.notifyAppReady()
|
// CapacitorUpdater.notifyAppReady()
|
||||||
}
|
// }
|
||||||
|
|
||||||
class ComtyApp extends React.Component {
|
class ComtyApp extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -66,8 +63,12 @@ class ComtyApp extends React.Component {
|
|||||||
window.app.message = antd.message
|
window.app.message = antd.message
|
||||||
window.app.isCapacitor = IS_MOBILE_HOST
|
window.app.isCapacitor = IS_MOBILE_HOST
|
||||||
|
|
||||||
if (window.app.version !== window.localStorage.getItem("last_version")) {
|
if (
|
||||||
app.message.info(`Comty has been updated to version ${window.app.version}!`)
|
window.app.version !== window.localStorage.getItem("last_version")
|
||||||
|
) {
|
||||||
|
app.message.info(
|
||||||
|
`Comty has been updated to version ${window.app.version}!`,
|
||||||
|
)
|
||||||
window.localStorage.setItem("last_version", window.app.version)
|
window.localStorage.setItem("last_version", window.app.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +114,7 @@ class ComtyApp extends React.Component {
|
|||||||
sessionController: this.sessionController,
|
sessionController: this.sessionController,
|
||||||
onDone: () => {
|
onDone: () => {
|
||||||
app.layout.draggable.destroy("login")
|
app.layout.draggable.destroy("login")
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -129,19 +130,23 @@ class ComtyApp extends React.Component {
|
|||||||
props: {
|
props: {
|
||||||
bodyStyle: {
|
bodyStyle: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// Opens the notification window and sets up the UI for the notification to be displayed
|
// Opens the notification window and sets up the UI for the notification to be displayed
|
||||||
openNotifications: () => {
|
openNotifications: () => {
|
||||||
window.app.layout.drawer.open("notifications", NotificationsCenter, {
|
window.app.layout.drawer.open(
|
||||||
props: {
|
"notifications",
|
||||||
width: "fit-content",
|
NotificationsCenter,
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
width: "fit-content",
|
||||||
|
},
|
||||||
|
allowMultiples: false,
|
||||||
|
escClosable: true,
|
||||||
},
|
},
|
||||||
allowMultiples: false,
|
)
|
||||||
escClosable: true,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
openSearcher: (options) => {
|
openSearcher: (options) => {
|
||||||
if (app.isMobile) {
|
if (app.isMobile) {
|
||||||
@ -150,34 +155,44 @@ class ComtyApp extends React.Component {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
renderResults: true,
|
renderResults: true,
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.layout.modal.open("searcher", (props) => <Searcher autoFocus renderResults {...props} />, {
|
return app.layout.modal.open(
|
||||||
framed: false
|
"searcher",
|
||||||
})
|
(props) => <Searcher autoFocus renderResults {...props} />,
|
||||||
|
{
|
||||||
|
framed: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
openMessages: () => {
|
openMessages: () => {
|
||||||
app.location.push("/messages")
|
app.location.push("/messages")
|
||||||
},
|
},
|
||||||
openFullImageViewer: (src) => {
|
openFullImageViewer: (src) => {
|
||||||
app.cores.window_mng.render("image_lightbox", <Lightbox
|
app.cores.window_mng.render(
|
||||||
small={src}
|
"image_lightbox",
|
||||||
large={src}
|
<Lightbox
|
||||||
onClose={() => app.cores.window_mng.close("image_lightbox")}
|
small={src}
|
||||||
hideDownload
|
large={src}
|
||||||
showRotate
|
onClose={() =>
|
||||||
/>)
|
app.cores.window_mng.close("image_lightbox")
|
||||||
|
}
|
||||||
|
hideDownload
|
||||||
|
showRotate
|
||||||
|
/>,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
openPostCreator: (params) => {
|
openPostCreator: (params) => {
|
||||||
app.layout.modal.open("post_creator", (props) => <PostCreator
|
app.layout.modal.open(
|
||||||
{...props}
|
"post_creator",
|
||||||
{...params}
|
(props) => <PostCreator {...props} {...params} />,
|
||||||
/>, {
|
{
|
||||||
framed: false
|
framed: false,
|
||||||
})
|
},
|
||||||
}
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
reload: () => {
|
reload: () => {
|
||||||
@ -198,14 +213,16 @@ class ComtyApp extends React.Component {
|
|||||||
goToSettings: (setting_id) => {
|
goToSettings: (setting_id) => {
|
||||||
return app.location.push(`/settings`, {
|
return app.location.push(`/settings`, {
|
||||||
query: {
|
query: {
|
||||||
setting: setting_id
|
setting: setting_id,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
goToAccount: (username) => {
|
goToAccount: (username) => {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
if (!app.userData) {
|
if (!app.userData) {
|
||||||
console.error("Cannot go to account, no username provided and no user logged in")
|
console.error(
|
||||||
|
"Cannot go to account, no username provided and no user logged in",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,27 +236,33 @@ class ComtyApp extends React.Component {
|
|||||||
},
|
},
|
||||||
goToPlaylist: (playlist_id) => {
|
goToPlaylist: (playlist_id) => {
|
||||||
return app.location.push(`/play/${playlist_id}`)
|
return app.location.push(`/play/${playlist_id}`)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
capacitor: {
|
capacitor: {
|
||||||
isAppCapacitor: () => window.navigator.userAgent === "capacitor",
|
isAppCapacitor: () => window.navigator.userAgent === "capacitor",
|
||||||
setStatusBarStyleDark: async () => {
|
setStatusBarStyleDark: async () => {
|
||||||
if (!window.app.capacitor.isAppCapacitor()) {
|
if (!window.app.capacitor.isAppCapacitor()) {
|
||||||
console.warn("[App] setStatusBarStyleDark is only available on capacitor")
|
console.warn(
|
||||||
|
"[App] setStatusBarStyleDark is only available on capacitor",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return await StatusBar.setStyle({ style: Style.Dark })
|
return await StatusBar.setStyle({ style: Style.Dark })
|
||||||
},
|
},
|
||||||
setStatusBarStyleLight: async () => {
|
setStatusBarStyleLight: async () => {
|
||||||
if (!window.app.capacitor.isAppCapacitor()) {
|
if (!window.app.capacitor.isAppCapacitor()) {
|
||||||
console.warn("[App] setStatusBarStyleLight is not supported on this platform")
|
console.warn(
|
||||||
|
"[App] setStatusBarStyleLight is not supported on this platform",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return await StatusBar.setStyle({ style: Style.Light })
|
return await StatusBar.setStyle({ style: Style.Light })
|
||||||
},
|
},
|
||||||
hideStatusBar: async () => {
|
hideStatusBar: async () => {
|
||||||
if (!window.app.capacitor.isAppCapacitor()) {
|
if (!window.app.capacitor.isAppCapacitor()) {
|
||||||
console.warn("[App] hideStatusBar is not supported on this platform")
|
console.warn(
|
||||||
|
"[App] hideStatusBar is not supported on this platform",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +270,9 @@ class ComtyApp extends React.Component {
|
|||||||
},
|
},
|
||||||
showStatusBar: async () => {
|
showStatusBar: async () => {
|
||||||
if (!window.app.capacitor.isAppCapacitor()) {
|
if (!window.app.capacitor.isAppCapacitor()) {
|
||||||
console.warn("[App] showStatusBar is not supported on this platform")
|
console.warn(
|
||||||
|
"[App] showStatusBar is not supported on this platform",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return await StatusBar.show()
|
return await StatusBar.show()
|
||||||
@ -257,13 +282,14 @@ class ComtyApp extends React.Component {
|
|||||||
clearInternalStorage: async () => {
|
clearInternalStorage: async () => {
|
||||||
antd.Modal.confirm({
|
antd.Modal.confirm({
|
||||||
title: "Clear internal storage",
|
title: "Clear internal storage",
|
||||||
content: "Are you sure you want to clear all internal storage? This will remove all your data from the app, including your session.",
|
content:
|
||||||
|
"Are you sure you want to clear all internal storage? This will remove all your data from the app, including your session.",
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
Utils.deleteInternalStorage()
|
Utils.deleteInternalStorage()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static staticRenders = {
|
static staticRenders = {
|
||||||
@ -309,12 +335,10 @@ class ComtyApp extends React.Component {
|
|||||||
app.navigation.goAuth()
|
app.navigation.goAuth()
|
||||||
|
|
||||||
antd.notification.open({
|
antd.notification.open({
|
||||||
message: <Translation>
|
message: (
|
||||||
{(t) => t("Invalid Session")}
|
<Translation>{(t) => t("Invalid Session")}</Translation>
|
||||||
</Translation>,
|
),
|
||||||
description: <Translation>
|
description: <Translation>{(t) => t(error)}</Translation>,
|
||||||
{(t) => t(error)}
|
|
||||||
</Translation>,
|
|
||||||
icon: <Icons.MdOutlineAccessTimeFilled />,
|
icon: <Icons.MdOutlineAccessTimeFilled />,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -339,7 +363,7 @@ class ComtyApp extends React.Component {
|
|||||||
"auth:disabled_account": async () => {
|
"auth:disabled_account": async () => {
|
||||||
await SessionModel.removeToken()
|
await SessionModel.removeToken()
|
||||||
app.navigation.goAuth()
|
app.navigation.goAuth()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
flushState = async () => {
|
flushState = async () => {
|
||||||
@ -355,13 +379,13 @@ class ComtyApp extends React.Component {
|
|||||||
|
|
||||||
StatusBar.setOverlaysWebView({ overlay: false })
|
StatusBar.setOverlaysWebView({ overlay: false })
|
||||||
|
|
||||||
CapacitorApp.addListener("backButton", ({ canGoBack }) => {
|
// CapacitorApp.addListener("backButton", ({ canGoBack }) => {
|
||||||
if (!canGoBack) {
|
// if (!canGoBack) {
|
||||||
CapacitorApp.exitApp()
|
// CapacitorApp.exitApp()
|
||||||
} else {
|
// } else {
|
||||||
app.location.back()
|
// app.location.back()
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.initialization()
|
await this.initialization()
|
||||||
@ -389,7 +413,10 @@ class ComtyApp extends React.Component {
|
|||||||
try {
|
try {
|
||||||
await this.__SessionInit()
|
await this.__SessionInit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[App] Error while initializing session`, error)
|
console.error(
|
||||||
|
`[App] Error while initializing session`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
cause: "Cannot initialize session",
|
cause: "Cannot initialize session",
|
||||||
@ -436,32 +463,33 @@ class ComtyApp extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <React.Fragment>
|
return (
|
||||||
<Helmet>
|
<React.Fragment>
|
||||||
<title>{config.app.siteName}</title>
|
<Helmet>
|
||||||
<meta name="og:description" content={config.app.siteDescription} />
|
<title>{config.app.siteName}</title>
|
||||||
<meta property="og:title" content={config.app.siteName} />
|
<meta
|
||||||
</Helmet>
|
name="og:description"
|
||||||
<Router.InternalRouter>
|
content={config.app.siteDescription}
|
||||||
<ThemeProvider>
|
/>
|
||||||
{
|
<meta property="og:title" content={config.app.siteName} />
|
||||||
window.__TAURI__ && <DesktopTopBar />
|
</Helmet>
|
||||||
}
|
<Router.InternalRouter>
|
||||||
<Layout
|
<ThemeProvider>
|
||||||
user={this.state.user}
|
{window.__TAURI__ && <DesktopTopBar />}
|
||||||
staticRenders={ComtyApp.staticRenders}
|
<Layout
|
||||||
bindProps={{
|
user={this.state.user}
|
||||||
user: this.state.user,
|
staticRenders={ComtyApp.staticRenders}
|
||||||
}}
|
bindProps={{
|
||||||
>
|
user: this.state.user,
|
||||||
{
|
}}
|
||||||
this.state.initialized && <Router.PageRender />
|
>
|
||||||
}
|
{this.state.initialized && <Router.PageRender />}
|
||||||
</Layout>
|
</Layout>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Router.InternalRouter>
|
</Router.InternalRouter>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Runtime(ComtyApp)
|
export default new Runtime(ComtyApp)
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
const PageTransition = (props) => {
|
const PageTransition = (props) => {
|
||||||
return <AnimatePresence>
|
return (
|
||||||
<motion.div
|
<AnimatePresence>
|
||||||
layout
|
<motion.div
|
||||||
initial={{ y: 10, opacity: 0 }}
|
layout
|
||||||
animate={{ y: 0, opacity: 1 }}
|
initial={{ y: 10, opacity: 0 }}
|
||||||
exit={{ y: -10, opacity: 0 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.2 }}
|
exit={{ y: -10, opacity: 0 }}
|
||||||
{...props}
|
transition={{ duration: 0.2 }}
|
||||||
>
|
{...props}
|
||||||
{props.children}
|
>
|
||||||
</motion.div>
|
{props.children}
|
||||||
</AnimatePresence>
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PageTransition
|
export default PageTransition
|
||||||
|
@ -2,7 +2,7 @@ import React from "react"
|
|||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Icons, createIconRender } from "@components/Icons"
|
import { Icons, createIconRender } from "@components/Icons"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "motion/react"
|
||||||
|
|
||||||
import useWsEvents from "@hooks/useWsEvents"
|
import useWsEvents from "@hooks/useWsEvents"
|
||||||
|
|
||||||
@ -11,214 +11,220 @@ import PostModel from "@models/post"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const PollOption = (props) => {
|
const PollOption = (props) => {
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
if (typeof props.onClick === "function") {
|
if (typeof props.onClick === "function") {
|
||||||
await props.onClick(props.option.id)
|
await props.onClick(props.option.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
className={classnames(
|
<div
|
||||||
"poll-option",
|
className={classnames("poll-option", {
|
||||||
{
|
["checked"]: props.checked,
|
||||||
["checked"]: props.checked,
|
})}
|
||||||
|
style={{
|
||||||
|
"--percentage": `${props.percentage}%`,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{props.checked && (
|
||||||
|
<motion.div
|
||||||
|
className="percentage-indicator"
|
||||||
|
animate={{ width: `${props.percentage}%` }}
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
transition={{ ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
}
|
<div className="poll-option-content">
|
||||||
)}
|
{props.checked && createIconRender("FaCheck")}
|
||||||
style={{
|
|
||||||
"--percentage": `${props.percentage}%`
|
|
||||||
}}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
props.checked && <motion.div
|
|
||||||
className="percentage-indicator"
|
|
||||||
animate={{ width: `${props.percentage}%` }}
|
|
||||||
initial={{ width: 0 }}
|
|
||||||
transition={{ ease: "easeOut" }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="poll-option-content">
|
{props.showPercentage && (
|
||||||
{
|
<span>{Math.floor(props.percentage)}%</span>
|
||||||
props.checked && createIconRender("FaCheck")
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
<span>{props.option.label}</span>
|
||||||
props.showPercentage && <span>
|
</div>
|
||||||
{Math.floor(props.percentage)}%
|
</div>
|
||||||
</span>
|
)
|
||||||
}
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{props.option.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Poll = (props) => {
|
const Poll = (props) => {
|
||||||
const { editMode, onClose, formRef } = props
|
const { editMode, onClose, formRef } = props
|
||||||
|
|
||||||
const [options, setOptions] = React.useState(props.options ?? [])
|
const [options, setOptions] = React.useState(props.options ?? [])
|
||||||
const [hasVoted, setHasVoted] = React.useState(false)
|
const [hasVoted, setHasVoted] = React.useState(false)
|
||||||
const [totalVotes, setTotalVotes] = React.useState(0)
|
const [totalVotes, setTotalVotes] = React.useState(0)
|
||||||
|
|
||||||
useWsEvents({
|
useWsEvents(
|
||||||
"post.poll.vote": (data) => {
|
{
|
||||||
const { post_id, option_id, user_id, previous_option_id } = data
|
"post.poll.vote": (data) => {
|
||||||
|
const { post_id, option_id, user_id, previous_option_id } = data
|
||||||
|
|
||||||
if (post_id !== props.post_id) {
|
if (post_id !== props.post_id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`U[${user_id}] vote to option [${option_id}]`)
|
console.debug(`U[${user_id}] vote to option [${option_id}]`)
|
||||||
|
|
||||||
setOptions((prev) => {
|
setOptions((prev) => {
|
||||||
prev = prev.map((option) => {
|
prev = prev.map((option) => {
|
||||||
return option
|
return option
|
||||||
})
|
})
|
||||||
|
|
||||||
if (user_id === app.userData._id) {
|
if (user_id === app.userData._id) {
|
||||||
// remove all `voted` properties
|
// remove all `voted` properties
|
||||||
prev = prev.map((option) => {
|
prev = prev.map((option) => {
|
||||||
delete option.voted
|
delete option.voted
|
||||||
|
|
||||||
option.voted = option.id === option_id
|
option.voted = option.id === option_id
|
||||||
|
|
||||||
return option
|
return option
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previous_option_id) {
|
if (previous_option_id) {
|
||||||
const previousOptionIndex = prev.findIndex((option) => option.id === previous_option_id)
|
const previousOptionIndex = prev.findIndex(
|
||||||
|
(option) => option.id === previous_option_id,
|
||||||
|
)
|
||||||
|
|
||||||
if (previousOptionIndex !== -1) {
|
if (previousOptionIndex !== -1) {
|
||||||
prev[previousOptionIndex].count = prev[previousOptionIndex].count - 1
|
prev[previousOptionIndex].count =
|
||||||
}
|
prev[previousOptionIndex].count - 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (option_id) {
|
if (option_id) {
|
||||||
const newOptionIndex = prev.findIndex((option) => option.id === option_id)
|
const newOptionIndex = prev.findIndex(
|
||||||
|
(option) => option.id === option_id,
|
||||||
|
)
|
||||||
|
|
||||||
if (newOptionIndex !== -1) {
|
if (newOptionIndex !== -1) {
|
||||||
prev[newOptionIndex].count += 1
|
prev[newOptionIndex].count += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
socketName: "posts"
|
{
|
||||||
})
|
socketName: "posts",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async function onVote(id) {
|
async function onVote(id) {
|
||||||
console.debug(`Voting poll option`, {
|
console.debug(`Voting poll option`, {
|
||||||
option_id: id,
|
option_id: id,
|
||||||
post_id: props.post_id,
|
post_id: props.post_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
await PostModel.votePoll({
|
await PostModel.votePoll({
|
||||||
post_id: props.post_id,
|
post_id: props.post_id,
|
||||||
option_id: id,
|
option_id: id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (options) {
|
if (options) {
|
||||||
const totalVotes = options.reduce((sum, option) => {
|
const totalVotes = options.reduce((sum, option) => {
|
||||||
return sum + option.count
|
return sum + option.count
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
setTotalVotes(totalVotes)
|
setTotalVotes(totalVotes)
|
||||||
|
|
||||||
const hasVoted = options.some((option) => {
|
const hasVoted = options.some((option) => {
|
||||||
return option.voted
|
return option.voted
|
||||||
})
|
})
|
||||||
|
|
||||||
setHasVoted(hasVoted)
|
setHasVoted(hasVoted)
|
||||||
}
|
}
|
||||||
}, [options])
|
}, [options])
|
||||||
|
|
||||||
return <div className="poll">
|
return (
|
||||||
{
|
<div className="poll">
|
||||||
!editMode && options.map((option, index) => {
|
{!editMode &&
|
||||||
const percentage = totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
|
options.map((option, index) => {
|
||||||
|
const percentage =
|
||||||
|
totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
|
||||||
|
|
||||||
return <PollOption
|
return (
|
||||||
key={index}
|
<PollOption
|
||||||
option={option}
|
key={index}
|
||||||
onClick={onVote}
|
option={option}
|
||||||
checked={option.voted}
|
onClick={onVote}
|
||||||
percentage={percentage}
|
checked={option.voted}
|
||||||
showPercentage={hasVoted}
|
percentage={percentage}
|
||||||
/>
|
showPercentage={hasVoted}
|
||||||
})
|
/>
|
||||||
}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{
|
{editMode && (
|
||||||
editMode && <antd.Form
|
<antd.Form
|
||||||
name="post-poll"
|
name="post-poll"
|
||||||
className="post-poll-edit"
|
className="post-poll-edit"
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
options: options
|
options: options,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<antd.Form.List
|
<antd.Form.List name="options">
|
||||||
name="options"
|
{(fields, { add, remove }) => {
|
||||||
>
|
return (
|
||||||
{(fields, { add, remove }) => {
|
<>
|
||||||
return <>
|
{fields.map((field, index) => {
|
||||||
{
|
return (
|
||||||
fields.map((field, index) => {
|
<div
|
||||||
return <div
|
key={field.key}
|
||||||
key={field.key}
|
className="post-poll-edit-option"
|
||||||
className="post-poll-edit-option"
|
>
|
||||||
>
|
<antd.Form.Item
|
||||||
<antd.Form.Item
|
{...field}
|
||||||
{...field}
|
name={[field.name, "label"]}
|
||||||
name={[field.name, "label"]}
|
>
|
||||||
>
|
<antd.Input placeholder="Type a option" />
|
||||||
<antd.Input
|
</antd.Form.Item>
|
||||||
placeholder="Type a option"
|
|
||||||
/>
|
|
||||||
</antd.Form.Item>
|
|
||||||
|
|
||||||
{
|
{fields.length > 1 && (
|
||||||
fields.length > 1 && <antd.Button
|
<antd.Button
|
||||||
onClick={() => remove(field.name)}
|
onClick={() =>
|
||||||
icon={createIconRender("MdRemove")}
|
remove(field.name)
|
||||||
/>
|
}
|
||||||
}
|
icon={createIconRender(
|
||||||
</div>
|
"MdRemove",
|
||||||
})
|
)}
|
||||||
}
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
onClick={() => add()}
|
onClick={() => add()}
|
||||||
icon={createIconRender("PlusOutlined")}
|
icon={createIconRender("PlusOutlined")}
|
||||||
>
|
>
|
||||||
Add Option
|
Add Option
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
</>
|
</>
|
||||||
}}
|
)
|
||||||
</antd.Form.List>
|
}}
|
||||||
</antd.Form>
|
</antd.Form.List>
|
||||||
}
|
</antd.Form>
|
||||||
|
)}
|
||||||
|
|
||||||
{
|
{editMode && (
|
||||||
editMode && <div className="poll-edit-actions">
|
<div className="poll-edit-actions">
|
||||||
<antd.Button
|
<antd.Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
icon={createIconRender("CloseOutlined")}
|
icon={createIconRender("CloseOutlined")}
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Poll
|
export default Poll
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Skeleton, Button } from "antd"
|
import { Skeleton, Button } from "antd"
|
||||||
import Plyr from "plyr-react"
|
|
||||||
import mimetypes from "mime"
|
import mimetypes from "mime"
|
||||||
import BearCarousel from "bear-react-carousel"
|
import BearCarousel from "bear-react-carousel"
|
||||||
|
|
||||||
@ -9,167 +8,176 @@ import ImageViewer from "@components/ImageViewer"
|
|||||||
import ContentFailed from "../contentFailed"
|
import ContentFailed from "../contentFailed"
|
||||||
|
|
||||||
import "bear-react-carousel/dist/index.css"
|
import "bear-react-carousel/dist/index.css"
|
||||||
import "plyr-react/dist/plyr.css"
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const renderDebug = localStorage.getItem("render_debug") === "true"
|
const renderDebug = localStorage.getItem("render_debug") === "true"
|
||||||
|
|
||||||
const Attachment = React.memo((props) => {
|
const Attachment = React.memo((props) => {
|
||||||
const [loaded, setLoaded] = React.useState(false)
|
const [loaded, setLoaded] = React.useState(false)
|
||||||
|
|
||||||
const [mimeType, setMimeType] = React.useState(null)
|
const [mimeType, setMimeType] = React.useState(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { url, id } = props.attachment
|
const { url, id } = props.attachment
|
||||||
|
|
||||||
const onDoubleClickAttachment = (e) => {
|
const onDoubleClickAttachment = (e) => {
|
||||||
if (mimeType.split("/")[0] === "image") {
|
if (mimeType.split("/")[0] === "image") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
app.controls.openFullImageViewer(url)
|
app.controls.openFullImageViewer(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMediaType = async () => {
|
const getMediaType = async () => {
|
||||||
let extension = null
|
let extension = null
|
||||||
|
|
||||||
// get media type by parsing the url
|
// get media type by parsing the url
|
||||||
const mediaExtname = /\.([a-zA-Z0-9]+)$/.exec(url)
|
const mediaExtname = /\.([a-zA-Z0-9]+)$/.exec(url)
|
||||||
|
|
||||||
if (mediaExtname) {
|
if (mediaExtname) {
|
||||||
extension = mediaExtname[1]
|
extension = mediaExtname[1]
|
||||||
} else {
|
} else {
|
||||||
// try to get media by creating requesting the url
|
// try to get media by creating requesting the url
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
})
|
})
|
||||||
|
|
||||||
extension = response.headers.get("content-type").split("/")[1]
|
extension = response.headers.get("content-type").split("/")[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension = extension.toLowerCase()
|
extension = extension.toLowerCase()
|
||||||
|
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
setLoaded(true)
|
setLoaded(true)
|
||||||
|
|
||||||
console.error("Failed to get media type", url, extension)
|
console.error("Failed to get media type", url, extension)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeType = mimetypes.getType(extension)
|
const mimeType = mimetypes.getType(extension)
|
||||||
|
|
||||||
setMimeType(mimeType)
|
setMimeType(mimeType)
|
||||||
|
|
||||||
setLoaded(true)
|
setLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMedia = () => {
|
const renderMedia = () => {
|
||||||
if (!mimeType) {
|
if (!mimeType) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mimeType.split("/")[0]) {
|
switch (mimeType.split("/")[0]) {
|
||||||
case "image": {
|
case "image": {
|
||||||
return <ImageViewer src={url} />
|
return <ImageViewer src={url} />
|
||||||
}
|
}
|
||||||
case "video": {
|
case "video": {
|
||||||
return <video controls>
|
return (
|
||||||
<source src={url} type={mimeType} />
|
<video controls>
|
||||||
</video>
|
<source src={url} type={mimeType} />
|
||||||
}
|
</video>
|
||||||
case "audio": {
|
)
|
||||||
return <audio controls>
|
}
|
||||||
<source src={url} type={mimeType} />
|
case "audio": {
|
||||||
</audio>
|
return (
|
||||||
}
|
<audio controls>
|
||||||
default: {
|
<source src={url} type={mimeType} />
|
||||||
return <h4>
|
</audio>
|
||||||
Unsupported media type [{mimeType}]
|
)
|
||||||
</h4>
|
}
|
||||||
}
|
default: {
|
||||||
}
|
return <h4>Unsupported media type [{mimeType}]</h4>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
getMediaType()
|
getMediaType()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return <Skeleton active />
|
return <Skeleton active />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loaded && !mimeType) {
|
if (loaded && !mimeType) {
|
||||||
return <ContentFailed />
|
return <ContentFailed />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
key={props.index}
|
<div
|
||||||
id={id}
|
key={props.index}
|
||||||
className="attachment"
|
id={id}
|
||||||
onDoubleClick={onDoubleClickAttachment}
|
className="attachment"
|
||||||
>
|
onDoubleClick={onDoubleClickAttachment}
|
||||||
{renderMedia()}
|
>
|
||||||
</div>
|
{renderMedia()}
|
||||||
} catch (error) {
|
</div>
|
||||||
console.error(error)
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
return <ContentFailed />
|
return <ContentFailed />
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default React.memo((props) => {
|
export default React.memo((props) => {
|
||||||
const [controller, setController] = React.useState()
|
const [controller, setController] = React.useState()
|
||||||
const [carouselState, setCarouselState] = React.useState()
|
const [carouselState, setCarouselState] = React.useState()
|
||||||
const [nsfwAccepted, setNsfwAccepted] = React.useState(false)
|
const [nsfwAccepted, setNsfwAccepted] = React.useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// get attachment index from query string
|
// get attachment index from query string
|
||||||
const attachmentIndex = parseInt(new URLSearchParams(window.location.search).get("attachment"))
|
const attachmentIndex = parseInt(
|
||||||
|
new URLSearchParams(window.location.search).get("attachment"),
|
||||||
|
)
|
||||||
|
|
||||||
if (attachmentIndex) {
|
if (attachmentIndex) {
|
||||||
controller?.slideToPage(attachmentIndex)
|
controller?.slideToPage(attachmentIndex)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className="post_attachments">
|
return (
|
||||||
{
|
<div className="post_attachments">
|
||||||
props.flags && props.flags.includes("nsfw") && !nsfwAccepted &&
|
{props.flags && props.flags.includes("nsfw") && !nsfwAccepted && (
|
||||||
<div className="nsfw_alert">
|
<div className="nsfw_alert">
|
||||||
<h2>
|
<h2>This post may contain sensitive content.</h2>
|
||||||
This post may contain sensitive content.
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<Button onClick={() => setNsfwAccepted(true)}>
|
<Button onClick={() => setNsfwAccepted(true)}>
|
||||||
Show anyways
|
Show anyways
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{
|
{props.attachments?.length > 0 && (
|
||||||
props.attachments?.length > 0 && <BearCarousel
|
<BearCarousel
|
||||||
data={props.attachments.map((attachment, index) => {
|
data={props.attachments.map((attachment, index) => {
|
||||||
if (typeof attachment !== "object") {
|
if (typeof attachment !== "object") {
|
||||||
attachment = {
|
attachment = {
|
||||||
url: attachment,
|
url: attachment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: index,
|
key: index,
|
||||||
children: <React.Fragment key={index}>
|
children: (
|
||||||
<Attachment index={index} attachment={attachment} />
|
<React.Fragment key={index}>
|
||||||
</React.Fragment>
|
<Attachment
|
||||||
}
|
index={index}
|
||||||
})}
|
attachment={attachment}
|
||||||
isEnableNavButton
|
/>
|
||||||
isEnableMouseMove
|
</React.Fragment>
|
||||||
isEnablePagination
|
),
|
||||||
setController={setController}
|
}
|
||||||
onSlideChange={setCarouselState}
|
})}
|
||||||
isDebug={renderDebug}
|
isEnableNavButton
|
||||||
/>
|
isEnableMouseMove
|
||||||
}
|
isEnablePagination
|
||||||
</div>
|
setController={setController}
|
||||||
})
|
onSlideChange={setCarouselState}
|
||||||
|
isDebug={renderDebug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import Plyr from "plyr-react"
|
import ReactPlayer from "react-player/lazy"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "motion/react"
|
||||||
|
|
||||||
import Poll from "@components/Poll"
|
import Poll from "@components/Poll"
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
@ -14,249 +14,288 @@ import PostAttachments from "./components/attachments"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const articleAnimationProps = {
|
const articleAnimationProps = {
|
||||||
layout: true,
|
layout: true,
|
||||||
initial: { y: -100, opacity: 0 },
|
initial: { y: -100, opacity: 0 },
|
||||||
animate: { y: 0, opacity: 1, },
|
animate: { y: 0, opacity: 1 },
|
||||||
exit: { scale: 0, opacity: 0 },
|
exit: { scale: 0, opacity: 0 },
|
||||||
transition: { duration: 0.1, },
|
transition: { duration: 0.1 },
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageRegexs = [
|
const messageRegexs = [
|
||||||
{
|
{
|
||||||
regex: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*/g,
|
regex: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*/g,
|
||||||
fn: (key, result) => {
|
fn: (key, result) => {
|
||||||
return <Plyr source={{
|
return <ReactPlayer url={result[1]} controls />
|
||||||
type: "video",
|
},
|
||||||
sources: [{
|
},
|
||||||
src: result[1],
|
{
|
||||||
provider: "youtube",
|
regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
|
||||||
}],
|
fn: (key, result) => {
|
||||||
}} />
|
return (
|
||||||
}
|
<a
|
||||||
},
|
key={key}
|
||||||
{
|
href={result[1]}
|
||||||
regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
|
target="_blank"
|
||||||
fn: (key, result) => {
|
rel="noopener noreferrer"
|
||||||
return <a key={key} href={result[1]} target="_blank" rel="noopener noreferrer">{result[1]}</a>
|
>
|
||||||
}
|
{result[1]}
|
||||||
},
|
</a>
|
||||||
{
|
)
|
||||||
regex: /(@[a-zA-Z0-9_]+)/gi,
|
},
|
||||||
fn: (key, result) => {
|
},
|
||||||
return <a key={key} onClick={() => window.app.location.push(`/@${result[1].substr(1)}`)}>{result[1]}</a>
|
{
|
||||||
}
|
regex: /(@[a-zA-Z0-9_]+)/gi,
|
||||||
},
|
fn: (key, result) => {
|
||||||
{
|
return (
|
||||||
regex: /#[a-zA-Z0-9_]+/gi,
|
<a
|
||||||
fn: (key, result) => {
|
key={key}
|
||||||
return <a key={key} onClick={() => window.app.location.push(`/trending/${result[0].substr(1)}`)}>{result[0]}</a>
|
onClick={() =>
|
||||||
}
|
window.app.location.push(`/@${result[1].substr(1)}`)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
{result[1]}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex: /#[a-zA-Z0-9_]+/gi,
|
||||||
|
fn: (key, result) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={key}
|
||||||
|
onClick={() =>
|
||||||
|
window.app.location.push(
|
||||||
|
`/trending/${result[0].substr(1)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{result[0]}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default class PostCard extends React.PureComponent {
|
export default class PostCard extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
data: this.props.data,
|
data: this.props.data,
|
||||||
|
|
||||||
countLikes: this.props.data.countLikes ?? 0,
|
countLikes: this.props.data.countLikes ?? 0,
|
||||||
countReplies: this.props.data.countComments ?? 0,
|
countReplies: this.props.data.countComments ?? 0,
|
||||||
|
|
||||||
hasLiked: this.props.data.isLiked ?? false,
|
hasLiked: this.props.data.isLiked ?? false,
|
||||||
hasSaved: this.props.data.isSaved ?? false,
|
hasSaved: this.props.data.isSaved ?? false,
|
||||||
hasReplies: this.props.data.hasReplies ?? false,
|
hasReplies: this.props.data.hasReplies ?? false,
|
||||||
|
|
||||||
open: this.props.defaultOpened ?? false,
|
open: this.props.defaultOpened ?? false,
|
||||||
|
|
||||||
isNsfw: this.props.data.flags?.includes("nsfw") ?? false,
|
isNsfw: this.props.data.flags?.includes("nsfw") ?? false,
|
||||||
nsfwAccepted: false,
|
nsfwAccepted: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDataUpdate = (data) => {
|
handleDataUpdate = (data) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
data: data,
|
data: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubleClick = async () => {
|
onDoubleClick = async () => {
|
||||||
if (typeof this.props.events.onDoubleClick !== "function") {
|
if (typeof this.props.events.onDoubleClick !== "function") {
|
||||||
console.warn("onDoubleClick event is not a function")
|
console.warn("onDoubleClick event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onDoubleClick(this.state.data)
|
return await this.props.events.onDoubleClick(this.state.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickDelete = async () => {
|
onClickDelete = async () => {
|
||||||
if (typeof this.props.events.onClickDelete !== "function") {
|
if (typeof this.props.events.onClickDelete !== "function") {
|
||||||
console.warn("onClickDelete event is not a function")
|
console.warn("onClickDelete event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickDelete(this.state.data)
|
return await this.props.events.onClickDelete(this.state.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickLike = async () => {
|
onClickLike = async () => {
|
||||||
if (typeof this.props.events.onClickLike !== "function") {
|
if (typeof this.props.events.onClickLike !== "function") {
|
||||||
console.warn("onClickLike event is not a function")
|
console.warn("onClickLike event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionResult = await this.props.events.onClickLike(this.state.data)
|
const actionResult = await this.props.events.onClickLike(
|
||||||
|
this.state.data,
|
||||||
|
)
|
||||||
|
|
||||||
if (actionResult) {
|
if (actionResult) {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasLiked: actionResult.liked,
|
hasLiked: actionResult.liked,
|
||||||
countLikes: actionResult.count,
|
countLikes: actionResult.count,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return actionResult
|
return actionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickSave = async () => {
|
onClickSave = async () => {
|
||||||
if (typeof this.props.events.onClickSave !== "function") {
|
if (typeof this.props.events.onClickSave !== "function") {
|
||||||
console.warn("onClickSave event is not a function")
|
console.warn("onClickSave event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionResult = await this.props.events.onClickSave(this.state.data)
|
const actionResult = await this.props.events.onClickSave(
|
||||||
|
this.state.data,
|
||||||
|
)
|
||||||
|
|
||||||
if (actionResult) {
|
if (actionResult) {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasSaved: actionResult.saved,
|
hasSaved: actionResult.saved,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return actionResult
|
return actionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickEdit = async () => {
|
onClickEdit = async () => {
|
||||||
if (typeof this.props.events.onClickEdit !== "function") {
|
if (typeof this.props.events.onClickEdit !== "function") {
|
||||||
console.warn("onClickEdit event is not a function")
|
console.warn("onClickEdit event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickEdit(this.state.data)
|
return await this.props.events.onClickEdit(this.state.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickReply = async () => {
|
onClickReply = async () => {
|
||||||
if (typeof this.props.events.onClickReply !== "function") {
|
if (typeof this.props.events.onClickReply !== "function") {
|
||||||
console.warn("onClickReply event is not a function")
|
console.warn("onClickReply event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickReply(this.state.data)
|
return await this.props.events.onClickReply(this.state.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = (prevProps) => {
|
componentDidUpdate = (prevProps) => {
|
||||||
if (prevProps.data !== this.props.data) {
|
if (prevProps.data !== this.props.data) {
|
||||||
this.setState({
|
this.setState({
|
||||||
data: this.props.data,
|
data: this.props.data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch = (error, info) => {
|
componentDidCatch = (error, info) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
return <div className="postCard error">
|
return (
|
||||||
<h1>
|
<div className="postCard error">
|
||||||
<Icons.FiAlertTriangle />
|
<h1>
|
||||||
<span>Cannot render this post</span>
|
<Icons.FiAlertTriangle />
|
||||||
<span>
|
<span>Cannot render this post</span>
|
||||||
Maybe this version of the app is outdated or is not supported yet
|
<span>
|
||||||
</span>
|
Maybe this version of the app is outdated or is not
|
||||||
</h1>
|
supported yet
|
||||||
</div>
|
</span>
|
||||||
}
|
</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
app.cores.api.listenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts")
|
app.cores.api.listenEvent(
|
||||||
}
|
`post.update.${this.state.data._id}`,
|
||||||
|
this.handleDataUpdate,
|
||||||
|
"posts",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
app.cores.api.unlistenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts")
|
app.cores.api.unlistenEvent(
|
||||||
}
|
`post.update.${this.state.data._id}`,
|
||||||
|
this.handleDataUpdate,
|
||||||
|
"posts",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <motion.article
|
return (
|
||||||
className={classnames(
|
<motion.article
|
||||||
"post_card",
|
className={classnames("post_card", {
|
||||||
{
|
["open"]: this.state.open,
|
||||||
["open"]: this.state.open,
|
})}
|
||||||
}
|
id={this.state.data._id}
|
||||||
)}
|
style={this.props.style}
|
||||||
id={this.state.data._id}
|
{...articleAnimationProps}
|
||||||
style={this.props.style}
|
>
|
||||||
{...articleAnimationProps}
|
<div
|
||||||
>
|
className="post_card_content"
|
||||||
<div
|
context-menu={"post-card"}
|
||||||
className="post_card_content"
|
user-id={this.state.data.user_id}
|
||||||
context-menu={"post-card"}
|
>
|
||||||
user-id={this.state.data.user_id}
|
<PostHeader
|
||||||
>
|
postData={this.state.data}
|
||||||
<PostHeader
|
onDoubleClick={this.onDoubleClick}
|
||||||
postData={this.state.data}
|
disableReplyTag={this.props.disableReplyTag}
|
||||||
onDoubleClick={this.onDoubleClick}
|
/>
|
||||||
disableReplyTag={this.props.disableReplyTag}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="post_content"
|
id="post_content"
|
||||||
className={classnames(
|
className={classnames("post_content")}
|
||||||
"post_content",
|
>
|
||||||
)}
|
<div className="message">
|
||||||
>
|
{processString(messageRegexs)(
|
||||||
<div className="message">
|
this.state.data.message ?? "",
|
||||||
{
|
)}
|
||||||
processString(messageRegexs)(this.state.data.message ?? "")
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
{!this.props.disableAttachments &&
|
||||||
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments
|
this.state.data.attachments &&
|
||||||
attachments={this.state.data.attachments}
|
this.state.data.attachments.length > 0 && (
|
||||||
flags={this.state.data.flags}
|
<PostAttachments
|
||||||
/>
|
attachments={this.state.data.attachments}
|
||||||
}
|
flags={this.state.data.flags}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{
|
{this.state.data.poll_options && (
|
||||||
this.state.data.poll_options && <Poll
|
<Poll
|
||||||
post_id={this.state.data._id}
|
post_id={this.state.data._id}
|
||||||
options={this.state.data.poll_options}
|
options={this.state.data.poll_options}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostActions
|
<PostActions
|
||||||
post={this.state.data}
|
post={this.state.data}
|
||||||
user_id={this.state.data.user_id}
|
user_id={this.state.data.user_id}
|
||||||
|
likesCount={this.state.countLikes}
|
||||||
|
repliesCount={this.state.countReplies}
|
||||||
|
defaultLiked={this.state.hasLiked}
|
||||||
|
defaultSaved={this.state.hasSaved}
|
||||||
|
PP
|
||||||
|
actions={{
|
||||||
|
onClickLike: this.onClickLike,
|
||||||
|
onClickEdit: this.onClickEdit,
|
||||||
|
onClickDelete: this.onClickDelete,
|
||||||
|
onClickSave: this.onClickSave,
|
||||||
|
onClickReply: this.onClickReply,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
likesCount={this.state.countLikes}
|
{!this.props.disableHasReplies &&
|
||||||
repliesCount={this.state.countReplies}
|
!!this.state.hasReplies && (
|
||||||
|
<div
|
||||||
defaultLiked={this.state.hasLiked}
|
className="post-card-has_replies"
|
||||||
defaultSaved={this.state.hasSaved}
|
onClick={() =>
|
||||||
PP
|
app.navigation.goToPost(this.state.data._id)
|
||||||
actions={{
|
}
|
||||||
onClickLike: this.onClickLike,
|
>
|
||||||
onClickEdit: this.onClickEdit,
|
<span>
|
||||||
onClickDelete: this.onClickDelete,
|
View {this.state.hasReplies} replies
|
||||||
onClickSave: this.onClickSave,
|
</span>
|
||||||
onClickReply: this.onClickReply,
|
</div>
|
||||||
}}
|
)}
|
||||||
/>
|
</div>
|
||||||
|
</motion.article>
|
||||||
{
|
)
|
||||||
!this.props.disableHasReplies && !!this.state.hasReplies && <div
|
}
|
||||||
className="post-card-has_replies"
|
}
|
||||||
onClick={() => app.navigation.goToPost(this.state.data._id)}
|
|
||||||
>
|
|
||||||
<span>View {this.state.hasReplies} replies</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</motion.article>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import { AnimatePresence } from "framer-motion"
|
import { AnimatePresence } from "motion/react"
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
|
|
||||||
import PostCard from "@components/PostCard"
|
import PostCard from "@components/PostCard"
|
||||||
@ -11,474 +11,499 @@ import PostModel from "@models/post"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const LoadingComponent = () => {
|
const LoadingComponent = () => {
|
||||||
return <div className="post_card">
|
return (
|
||||||
<antd.Skeleton
|
<div className="post_card">
|
||||||
avatar
|
<antd.Skeleton
|
||||||
style={{
|
avatar
|
||||||
width: "100%"
|
style={{
|
||||||
}}
|
width: "100%",
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoResultComponent = () => {
|
const NoResultComponent = () => {
|
||||||
return <antd.Empty
|
return (
|
||||||
description="No more post here"
|
<antd.Empty
|
||||||
style={{
|
description="No more post here"
|
||||||
width: "100%"
|
style={{
|
||||||
}}
|
width: "100%",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeToComponent = {
|
const typeToComponent = {
|
||||||
"post": (args) => <PostCard {...args} />,
|
post: (args) => <PostCard {...args} />,
|
||||||
//"playlist": (args) => <PlaylistTimelineEntry {...args} />,
|
//"playlist": (args) => <PlaylistTimelineEntry {...args} />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Entry = React.memo((props) => {
|
const Entry = React.memo((props) => {
|
||||||
const { data } = props
|
const { data } = props
|
||||||
|
|
||||||
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
return React.createElement(
|
||||||
key: data._id,
|
typeToComponent[data.type ?? "post"] ?? PostCard,
|
||||||
data: data,
|
{
|
||||||
disableReplyTag: props.disableReplyTag,
|
key: data._id,
|
||||||
disableHasReplies: props.disableHasReplies,
|
data: data,
|
||||||
events: {
|
disableReplyTag: props.disableReplyTag,
|
||||||
onClickLike: props.onLikePost,
|
disableHasReplies: props.disableHasReplies,
|
||||||
onClickSave: props.onSavePost,
|
events: {
|
||||||
onClickDelete: props.onDeletePost,
|
onClickLike: props.onLikePost,
|
||||||
onClickEdit: props.onEditPost,
|
onClickSave: props.onSavePost,
|
||||||
onClickReply: props.onReplyPost,
|
onClickDelete: props.onDeletePost,
|
||||||
onDoubleClick: props.onDoubleClick,
|
onClickEdit: props.onEditPost,
|
||||||
},
|
onClickReply: props.onReplyPost,
|
||||||
})
|
onDoubleClick: props.onDoubleClick,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostList = React.forwardRef((props, ref) => {
|
const PostList = React.forwardRef((props, ref) => {
|
||||||
return <LoadMore
|
return (
|
||||||
ref={ref}
|
<LoadMore
|
||||||
className="post-list"
|
ref={ref}
|
||||||
loadingComponent={LoadingComponent}
|
className="post-list"
|
||||||
noResultComponent={NoResultComponent}
|
loadingComponent={LoadingComponent}
|
||||||
hasMore={props.hasMore}
|
noResultComponent={NoResultComponent}
|
||||||
fetching={props.loading}
|
hasMore={props.hasMore}
|
||||||
onBottom={props.onLoadMore}
|
fetching={props.loading}
|
||||||
>
|
onBottom={props.onLoadMore}
|
||||||
{
|
>
|
||||||
!props.realtimeUpdates && !app.isMobile && <div className="resume_btn_wrapper">
|
{!props.realtimeUpdates && !app.isMobile && (
|
||||||
<antd.Button
|
<div className="resume_btn_wrapper">
|
||||||
type="primary"
|
<antd.Button
|
||||||
shape="round"
|
type="primary"
|
||||||
onClick={props.onResumeRealtimeUpdates}
|
shape="round"
|
||||||
loading={props.resumingLoading}
|
onClick={props.onResumeRealtimeUpdates}
|
||||||
icon={<Icons.FiSyncOutlined />}
|
loading={props.resumingLoading}
|
||||||
>
|
icon={<Icons.FiSyncOutlined />}
|
||||||
Resume
|
>
|
||||||
</antd.Button>
|
Resume
|
||||||
</div>
|
</antd.Button>
|
||||||
}
|
</div>
|
||||||
|
)}
|
||||||
<AnimatePresence>
|
|
||||||
{
|
|
||||||
props.list.map((data) => {
|
|
||||||
return <Entry
|
|
||||||
key={data._id}
|
|
||||||
data={data}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</AnimatePresence>
|
|
||||||
</LoadMore>
|
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{props.list.map((data) => {
|
||||||
|
return <Entry key={data._id} data={data} {...props} />
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</LoadMore>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export class PostsListsComponent extends React.Component {
|
export class PostsListsComponent extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
openPost: null,
|
openPost: null,
|
||||||
|
|
||||||
loading: false,
|
loading: false,
|
||||||
resumingLoading: false,
|
resumingLoading: false,
|
||||||
initialLoading: true,
|
initialLoading: true,
|
||||||
scrollingToTop: false,
|
scrollingToTop: false,
|
||||||
|
|
||||||
topVisible: true,
|
topVisible: true,
|
||||||
|
|
||||||
realtimeUpdates: true,
|
realtimeUpdates: true,
|
||||||
|
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
list: this.props.list ?? [],
|
list: this.props.list ?? [],
|
||||||
}
|
}
|
||||||
|
|
||||||
parentRef = this.props.innerRef
|
parentRef = this.props.innerRef
|
||||||
listRef = React.createRef()
|
listRef = React.createRef()
|
||||||
|
|
||||||
timelineWsEvents = {
|
timelineWsEvents = {
|
||||||
"feed.new": (data) => {
|
"feed.new": (data) => {
|
||||||
console.log("New feed => ", data)
|
console.log("New feed => ", data)
|
||||||
|
|
||||||
if (!this.state.realtimeUpdates) {
|
if (!this.state.realtimeUpdates) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
list: [data, ...this.state.list],
|
list: [data, ...this.state.list],
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
"post.new": (data) => {
|
"post.new": (data) => {
|
||||||
console.log("New post => ", data)
|
console.log("New post => ", data)
|
||||||
|
|
||||||
if (!this.state.realtimeUpdates) {
|
if (!this.state.realtimeUpdates) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
list: [data, ...this.state.list],
|
list: [data, ...this.state.list],
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
"post.delete": (id) => {
|
"post.delete": (id) => {
|
||||||
console.log("Deleted post => ", id)
|
console.log("Deleted post => ", id)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
list: this.state.list.filter((post) => {
|
list: this.state.list.filter((post) => {
|
||||||
return post._id !== id
|
return post._id !== id
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoad = async (fn, params = {}) => {
|
handleLoad = async (fn, params = {}) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
trim: this.state.list.length,
|
trim: this.state.list.length,
|
||||||
limit: app.cores.settings.get("feed_max_fetch"),
|
limit: app.cores.settings.get("feed_max_fetch"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.loadFromModelProps) {
|
if (this.props.loadFromModelProps) {
|
||||||
payload = {
|
payload = {
|
||||||
...payload,
|
...payload,
|
||||||
...this.props.loadFromModelProps,
|
...this.props.loadFromModelProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.replace) {
|
if (params.replace) {
|
||||||
payload.trim = 0
|
payload.trim = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fn(payload).catch((err) => {
|
const result = await fn(payload).catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
||||||
app.message.error("Failed to load more posts")
|
app.message.error("Failed to load more posts")
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("Loaded posts => ", result)
|
console.log("Loaded posts => ", result)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
return this.setState({
|
return this.setState({
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.replace) {
|
if (params.replace) {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: result,
|
list: result,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: [...this.state.list, ...result],
|
list: [...this.state.list, ...result],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addPost = (post) => {
|
addPost = (post) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: [post, ...this.state.list],
|
list: [post, ...this.state.list],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
removePost = (id) => {
|
removePost = (id) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: this.state.list.filter((post) => {
|
list: this.state.list.filter((post) => {
|
||||||
return post._id !== id
|
return post._id !== id
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_hacks = {
|
_hacks = {
|
||||||
addPost: this.addPost,
|
addPost: this.addPost,
|
||||||
removePost: this.removePost,
|
removePost: this.removePost,
|
||||||
addRandomPost: () => {
|
addRandomPost: () => {
|
||||||
const randomId = Math.random().toString(36).substring(7)
|
const randomId = Math.random().toString(36).substring(7)
|
||||||
|
|
||||||
this.addPost({
|
this.addPost({
|
||||||
_id: randomId,
|
_id: randomId,
|
||||||
message: `Random post ${randomId}`,
|
message: `Random post ${randomId}`,
|
||||||
user: {
|
user: {
|
||||||
_id: randomId,
|
_id: randomId,
|
||||||
username: "random user",
|
username: "random user",
|
||||||
avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
|
avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
listRef: this.listRef,
|
listRef: this.listRef,
|
||||||
}
|
}
|
||||||
|
|
||||||
onResumeRealtimeUpdates = async () => {
|
onResumeRealtimeUpdates = async () => {
|
||||||
console.log("Resuming realtime updates")
|
console.log("Resuming realtime updates")
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
resumingLoading: true,
|
resumingLoading: true,
|
||||||
scrollingToTop: true,
|
scrollingToTop: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.listRef.current.scrollTo({
|
this.listRef.current.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
})
|
})
|
||||||
|
|
||||||
// reload posts
|
// reload posts
|
||||||
await this.handleLoad(this.props.loadFromModel, {
|
await this.handleLoad(this.props.loadFromModel, {
|
||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
realtimeUpdates: true,
|
realtimeUpdates: true,
|
||||||
resumingLoading: false,
|
resumingLoading: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onScrollList = (e) => {
|
onScrollList = (e) => {
|
||||||
const { scrollTop } = e.target
|
const { scrollTop } = e.target
|
||||||
|
|
||||||
if (this.state.scrollingToTop && scrollTop === 0) {
|
if (this.state.scrollingToTop && scrollTop === 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollingToTop: false,
|
scrollingToTop: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollTop > 200) {
|
if (scrollTop > 200) {
|
||||||
if (this.state.topVisible) {
|
if (this.state.topVisible) {
|
||||||
this.setState({
|
this.setState({
|
||||||
topVisible: false,
|
topVisible: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (typeof this.props.onTopVisibility === "function") {
|
if (typeof this.props.onTopVisibility === "function") {
|
||||||
this.props.onTopVisibility(false)
|
this.props.onTopVisibility(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.props.realtime || this.state.resumingLoading || this.state.scrollingToTop) {
|
if (
|
||||||
return null
|
!this.props.realtime ||
|
||||||
}
|
this.state.resumingLoading ||
|
||||||
|
this.state.scrollingToTop
|
||||||
this.setState({
|
) {
|
||||||
realtimeUpdates: false,
|
return null
|
||||||
})
|
}
|
||||||
} else {
|
|
||||||
if (!this.state.topVisible) {
|
this.setState({
|
||||||
this.setState({
|
realtimeUpdates: false,
|
||||||
topVisible: true,
|
})
|
||||||
})
|
} else {
|
||||||
|
if (!this.state.topVisible) {
|
||||||
if (typeof this.props.onTopVisibility === "function") {
|
this.setState({
|
||||||
this.props.onTopVisibility(true)
|
topVisible: true,
|
||||||
}
|
})
|
||||||
|
|
||||||
// if (this.props.realtime || !this.state.realtimeUpdates && !this.state.resumingLoading && scrollTop < 5) {
|
if (typeof this.props.onTopVisibility === "function") {
|
||||||
// this.onResumeRealtimeUpdates()
|
this.props.onTopVisibility(true)
|
||||||
// }
|
}
|
||||||
}
|
|
||||||
}
|
// if (this.props.realtime || !this.state.realtimeUpdates && !this.state.resumingLoading && scrollTop < 5) {
|
||||||
}
|
// this.onResumeRealtimeUpdates()
|
||||||
|
// }
|
||||||
componentDidMount = async () => {
|
}
|
||||||
if (typeof this.props.loadFromModel === "function") {
|
}
|
||||||
await this.handleLoad(this.props.loadFromModel)
|
}
|
||||||
}
|
|
||||||
|
componentDidMount = async () => {
|
||||||
this.setState({
|
if (typeof this.props.loadFromModel === "function") {
|
||||||
initialLoading: false,
|
await this.handleLoad(this.props.loadFromModel)
|
||||||
})
|
}
|
||||||
|
|
||||||
if (this.props.watchTimeline) {
|
this.setState({
|
||||||
if (!Array.isArray(this.props.watchTimeline)) {
|
initialLoading: false,
|
||||||
console.error("watchTimeline prop must be an array")
|
})
|
||||||
} else {
|
|
||||||
this.props.watchTimeline.forEach((event) => {
|
if (this.props.watchTimeline) {
|
||||||
if (typeof this.timelineWsEvents[event] !== "function") {
|
if (!Array.isArray(this.props.watchTimeline)) {
|
||||||
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
console.error("watchTimeline prop must be an array")
|
||||||
}
|
} else {
|
||||||
|
this.props.watchTimeline.forEach((event) => {
|
||||||
app.cores.api.listenEvent(event, this.timelineWsEvents[event], "posts")
|
if (typeof this.timelineWsEvents[event] !== "function") {
|
||||||
})
|
console.error(
|
||||||
}
|
`The event "${event}" is not defined in the timelineWsEvents object`,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
if (this.listRef && this.listRef.current) {
|
|
||||||
this.listRef.current.addEventListener("scroll", this.onScrollList)
|
app.cores.api.listenEvent(
|
||||||
}
|
event,
|
||||||
|
this.timelineWsEvents[event],
|
||||||
window._hacks = this._hacks
|
"posts",
|
||||||
}
|
)
|
||||||
|
})
|
||||||
componentWillUnmount = async () => {
|
}
|
||||||
if (this.props.watchTimeline) {
|
}
|
||||||
if (!Array.isArray(this.props.watchTimeline)) {
|
|
||||||
console.error("watchTimeline prop must be an array")
|
if (this.listRef && this.listRef.current) {
|
||||||
} else {
|
this.listRef.current.addEventListener("scroll", this.onScrollList)
|
||||||
this.props.watchTimeline.forEach((event) => {
|
}
|
||||||
if (typeof this.timelineWsEvents[event] !== "function") {
|
|
||||||
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
window._hacks = this._hacks
|
||||||
}
|
}
|
||||||
|
|
||||||
app.cores.api.unlistenEvent(event, this.timelineWsEvents[event], "posts")
|
componentWillUnmount = async () => {
|
||||||
})
|
if (this.props.watchTimeline) {
|
||||||
}
|
if (!Array.isArray(this.props.watchTimeline)) {
|
||||||
}
|
console.error("watchTimeline prop must be an array")
|
||||||
|
} else {
|
||||||
if (this.listRef && this.listRef.current) {
|
this.props.watchTimeline.forEach((event) => {
|
||||||
this.listRef.current.removeEventListener("scroll", this.onScrollList)
|
if (typeof this.timelineWsEvents[event] !== "function") {
|
||||||
}
|
console.error(
|
||||||
|
`The event "${event}" is not defined in the timelineWsEvents object`,
|
||||||
window._hacks = null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = async (prevProps, prevState) => {
|
app.cores.api.unlistenEvent(
|
||||||
if (prevProps.list !== this.props.list) {
|
event,
|
||||||
this.setState({
|
this.timelineWsEvents[event],
|
||||||
list: this.props.list,
|
"posts",
|
||||||
})
|
)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
onLikePost = async (data) => {
|
|
||||||
let result = await PostModel.toggleLike({ post_id: data._id }).catch(() => {
|
if (this.listRef && this.listRef.current) {
|
||||||
antd.message.error("Failed to like post")
|
this.listRef.current.removeEventListener(
|
||||||
|
"scroll",
|
||||||
return false
|
this.onScrollList,
|
||||||
})
|
)
|
||||||
|
}
|
||||||
return result
|
|
||||||
}
|
window._hacks = null
|
||||||
|
}
|
||||||
onSavePost = async (data) => {
|
|
||||||
let result = await PostModel.toggleSave({ post_id: data._id }).catch(() => {
|
componentDidUpdate = async (prevProps, prevState) => {
|
||||||
antd.message.error("Failed to save post")
|
if (prevProps.list !== this.props.list) {
|
||||||
|
this.setState({
|
||||||
return false
|
list: this.props.list,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return result
|
}
|
||||||
}
|
|
||||||
|
onLikePost = async (data) => {
|
||||||
onEditPost = (data) => {
|
let result = await PostModel.toggleLike({ post_id: data._id }).catch(
|
||||||
app.controls.openPostCreator({
|
() => {
|
||||||
edit_post: data._id,
|
antd.message.error("Failed to like post")
|
||||||
})
|
|
||||||
}
|
return false
|
||||||
|
},
|
||||||
onReplyPost = (data) => {
|
)
|
||||||
app.controls.openPostCreator({
|
|
||||||
reply_to: data._id,
|
return result
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
onSavePost = async (data) => {
|
||||||
onDoubleClickPost = (data) => {
|
let result = await PostModel.toggleSave({ post_id: data._id }).catch(
|
||||||
app.navigation.goToPost(data._id)
|
() => {
|
||||||
}
|
antd.message.error("Failed to save post")
|
||||||
|
|
||||||
onDeletePost = async (data) => {
|
return false
|
||||||
antd.Modal.confirm({
|
},
|
||||||
title: "Are you sure you want to delete this post?",
|
)
|
||||||
content: "This action is irreversible",
|
|
||||||
okText: "Yes",
|
return result
|
||||||
okType: "danger",
|
}
|
||||||
cancelText: "No",
|
|
||||||
onOk: async () => {
|
onEditPost = (data) => {
|
||||||
await PostModel.deletePost({ post_id: data._id }).catch(() => {
|
app.controls.openPostCreator({
|
||||||
antd.message.error("Failed to delete post")
|
edit_post: data._id,
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
onReplyPost = (data) => {
|
||||||
|
app.controls.openPostCreator({
|
||||||
ontoggleOpen = (to, data) => {
|
reply_to: data._id,
|
||||||
if (typeof this.props.onOpenPost === "function") {
|
})
|
||||||
this.props.onOpenPost(to, data)
|
}
|
||||||
}
|
|
||||||
}
|
onDoubleClickPost = (data) => {
|
||||||
|
app.navigation.goToPost(data._id)
|
||||||
onLoadMore = async () => {
|
}
|
||||||
if (typeof this.props.onLoadMore === "function") {
|
|
||||||
return this.handleLoad(this.props.onLoadMore)
|
onDeletePost = async (data) => {
|
||||||
} else if (this.props.loadFromModel) {
|
antd.Modal.confirm({
|
||||||
return this.handleLoad(this.props.loadFromModel)
|
title: "Are you sure you want to delete this post?",
|
||||||
}
|
content: "This action is irreversible",
|
||||||
}
|
okText: "Yes",
|
||||||
|
okType: "danger",
|
||||||
render() {
|
cancelText: "No",
|
||||||
if (this.state.initialLoading) {
|
onOk: async () => {
|
||||||
return <antd.Skeleton active />
|
await PostModel.deletePost({ post_id: data._id }).catch(() => {
|
||||||
}
|
antd.message.error("Failed to delete post")
|
||||||
|
})
|
||||||
if (this.state.list.length === 0) {
|
},
|
||||||
if (typeof this.props.emptyListRender === "function") {
|
})
|
||||||
return React.createElement(this.props.emptyListRender)
|
}
|
||||||
}
|
|
||||||
|
ontoggleOpen = (to, data) => {
|
||||||
return <div className="no_more_posts">
|
if (typeof this.props.onOpenPost === "function") {
|
||||||
<antd.Empty />
|
this.props.onOpenPost(to, data)
|
||||||
<h1>Whoa, nothing on here...</h1>
|
}
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
|
onLoadMore = async () => {
|
||||||
const PostListProps = {
|
if (typeof this.props.onLoadMore === "function") {
|
||||||
list: this.state.list,
|
return this.handleLoad(this.props.onLoadMore)
|
||||||
|
} else if (this.props.loadFromModel) {
|
||||||
disableReplyTag: this.props.disableReplyTag,
|
return this.handleLoad(this.props.loadFromModel)
|
||||||
disableHasReplies: this.props.disableHasReplies,
|
}
|
||||||
|
}
|
||||||
onLikePost: this.onLikePost,
|
|
||||||
onSavePost: this.onSavePost,
|
render() {
|
||||||
onDeletePost: this.onDeletePost,
|
if (this.state.initialLoading) {
|
||||||
onEditPost: this.onEditPost,
|
return <antd.Skeleton active />
|
||||||
onReplyPost: this.onReplyPost,
|
}
|
||||||
onDoubleClick: this.onDoubleClickPost,
|
|
||||||
|
if (this.state.list.length === 0) {
|
||||||
onLoadMore: this.onLoadMore,
|
if (typeof this.props.emptyListRender === "function") {
|
||||||
hasMore: this.state.hasMore,
|
return React.createElement(this.props.emptyListRender)
|
||||||
loading: this.state.loading,
|
}
|
||||||
|
|
||||||
realtimeUpdates: this.state.realtimeUpdates,
|
return (
|
||||||
resumingLoading: this.state.resumingLoading,
|
<div className="no_more_posts">
|
||||||
onResumeRealtimeUpdates: this.onResumeRealtimeUpdates,
|
<antd.Empty />
|
||||||
}
|
<h1>Whoa, nothing on here...</h1>
|
||||||
|
</div>
|
||||||
if (app.isMobile) {
|
)
|
||||||
return <PostList
|
}
|
||||||
ref={this.listRef}
|
|
||||||
{...PostListProps}
|
const PostListProps = {
|
||||||
/>
|
list: this.state.list,
|
||||||
}
|
|
||||||
|
disableReplyTag: this.props.disableReplyTag,
|
||||||
return <div className="post-list_wrapper">
|
disableHasReplies: this.props.disableHasReplies,
|
||||||
<PostList
|
|
||||||
ref={this.listRef}
|
onLikePost: this.onLikePost,
|
||||||
{...PostListProps}
|
onSavePost: this.onSavePost,
|
||||||
/>
|
onDeletePost: this.onDeletePost,
|
||||||
</div>
|
onEditPost: this.onEditPost,
|
||||||
}
|
onReplyPost: this.onReplyPost,
|
||||||
|
onDoubleClick: this.onDoubleClickPost,
|
||||||
|
|
||||||
|
onLoadMore: this.onLoadMore,
|
||||||
|
hasMore: this.state.hasMore,
|
||||||
|
loading: this.state.loading,
|
||||||
|
|
||||||
|
realtimeUpdates: this.state.realtimeUpdates,
|
||||||
|
resumingLoading: this.state.resumingLoading,
|
||||||
|
onResumeRealtimeUpdates: this.onResumeRealtimeUpdates,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.isMobile) {
|
||||||
|
return <PostList ref={this.listRef} {...PostListProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="post-list_wrapper">
|
||||||
|
<PostList ref={this.listRef} {...PostListProps} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.forwardRef((props, ref) => <PostsListsComponent innerRef={ref} {...props} />)
|
export default React.forwardRef((props, ref) => (
|
||||||
|
<PostsListsComponent innerRef={ref} {...props} />
|
||||||
|
))
|
||||||
|
@ -1,86 +1,84 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
import { createIconRender } from "@components/Icons"
|
import { createIconRender } from "@components/Icons"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const ContextMenu = (props) => {
|
const ContextMenu = (props) => {
|
||||||
const [visible, setVisible] = React.useState(true)
|
const [visible, setVisible] = React.useState(true)
|
||||||
const { items = [], cords, clickedComponent, ctx } = props
|
const { items = [], cords, clickedComponent, ctx } = props
|
||||||
|
|
||||||
async function onClose() {
|
async function onClose() {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
props.unregisterOnClose(onClose)
|
props.unregisterOnClose(onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
props.registerOnClose(onClose)
|
props.registerOnClose(onClose)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleItemClick = async (item) => {
|
const handleItemClick = async (item) => {
|
||||||
if (typeof item.action === "function") {
|
if (typeof item.action === "function") {
|
||||||
await item.action(clickedComponent, ctx)
|
await item.action(clickedComponent, ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderItems = () => {
|
const renderItems = () => {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return <div>
|
return (
|
||||||
<p>No items</p>
|
<div>
|
||||||
</div>
|
<p>No items</p>
|
||||||
}
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return items.map((item, index) => {
|
return items.map((item, index) => {
|
||||||
if (item.type === "separator") {
|
if (item.type === "separator") {
|
||||||
return <div key={index} className="context-menu-separator" />
|
return <div key={index} className="context-menu-separator" />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
key={index}
|
<div
|
||||||
onClick={() => handleItemClick(item)}
|
key={index}
|
||||||
className="item"
|
onClick={() => handleItemClick(item)}
|
||||||
>
|
className="item"
|
||||||
<p className="label">
|
>
|
||||||
{item.label}
|
<p className="label">{item.label}</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
{
|
{item.description && (
|
||||||
item.description && <p className="description">
|
<p className="description">{item.description}</p>
|
||||||
{item.description}
|
)}
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{createIconRender(item.icon)}
|
||||||
createIconRender(item.icon)
|
</div>
|
||||||
}
|
)
|
||||||
</div>
|
})
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return <AnimatePresence>
|
return (
|
||||||
{
|
<AnimatePresence>
|
||||||
visible && <div
|
{visible && (
|
||||||
className="context-menu-wrapper"
|
<div
|
||||||
style={{
|
className="context-menu-wrapper"
|
||||||
top: cords.y,
|
style={{
|
||||||
left: cords.x,
|
top: cords.y,
|
||||||
}}
|
left: cords.x,
|
||||||
>
|
}}
|
||||||
<motion.div
|
>
|
||||||
className="context-menu"
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
className="context-menu"
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
exit={{ opacity: 0, scale: 0.3 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.05, ease: "easeInOut" }}
|
exit={{ opacity: 0, scale: 0.3 }}
|
||||||
>
|
transition={{ duration: 0.05, ease: "easeInOut" }}
|
||||||
{
|
>
|
||||||
renderItems()
|
{renderItems()}
|
||||||
}
|
</motion.div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
}
|
</AnimatePresence>
|
||||||
</AnimatePresence>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContextMenu
|
export default ContextMenu
|
||||||
|
@ -1,68 +1,67 @@
|
|||||||
import { Core } from "@ragestudio/vessel"
|
import { Core } from "@ragestudio/vessel"
|
||||||
import { Haptics } from "@capacitor/haptics"
|
// import { Haptics } from "@capacitor/haptics"
|
||||||
|
|
||||||
const vibrationPatterns = {
|
const vibrationPatterns = {
|
||||||
light: [10],
|
light: [10],
|
||||||
medium: [50],
|
medium: [50],
|
||||||
heavy: [80],
|
heavy: [80],
|
||||||
error: [100, 30, 100, 30, 100],
|
error: [100, 30, 100, 30, 100],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class HapticsCore extends Core {
|
export default class HapticsCore extends Core {
|
||||||
static namespace = "haptics"
|
static namespace = "haptics"
|
||||||
|
|
||||||
static dependencies = [
|
static dependencies = ["settings"]
|
||||||
"settings"
|
|
||||||
]
|
|
||||||
|
|
||||||
static isGlobalDisabled() {
|
static isGlobalDisabled() {
|
||||||
return app.cores.settings.is("haptics:enabled", false)
|
return app.cores.settings.is("haptics:enabled", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
if (window.navigator.userAgent === "capacitor") {
|
if (window.navigator.userAgent === "capacitor") {
|
||||||
navigator.vibrate = this.nativeVibrate
|
navigator.vibrate = this.nativeVibrate
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", this.handleClickEvent)
|
document.addEventListener("click", this.handleClickEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
public = {
|
public = {
|
||||||
isGlobalDisabled: HapticsCore.isGlobalDisabled,
|
//isGlobalDisabled: HapticsCore.isGlobalDisabled,
|
||||||
vibrate: this.vibrate.bind(this),
|
vibrate: this.vibrate.bind(this),
|
||||||
}
|
}
|
||||||
|
|
||||||
nativeVibrate = (pattern) => {
|
// nativeVibrate = (pattern) => {
|
||||||
if (!Array.isArray(pattern)) {
|
// if (!Array.isArray(pattern)) {
|
||||||
pattern = [pattern]
|
// pattern = [pattern]
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (let i = 0; i < pattern.length; i++) {
|
// for (let i = 0; i < pattern.length; i++) {
|
||||||
Haptics.vibrate({
|
// Haptics.vibrate({
|
||||||
duration: pattern[i],
|
// duration: pattern[i],
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
handleClickEvent = (event) => {
|
handleClickEvent = (event) => {
|
||||||
const button = event.target.closest("button") || event.target.closest(".ant-btn")
|
const button =
|
||||||
|
event.target.closest("button") || event.target.closest(".ant-btn")
|
||||||
|
|
||||||
if (button) {
|
if (button) {
|
||||||
this.vibrate("light")
|
this.vibrate("light")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vibrate(pattern = "light") {
|
vibrate(pattern = "light") {
|
||||||
const disabled = HapticsCore.isGlobalDisabled()
|
const disabled = HapticsCore.isGlobalDisabled()
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof pattern === "string") {
|
if (typeof pattern === "string") {
|
||||||
pattern = vibrationPatterns[pattern]
|
pattern = vibrationPatterns[pattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigator.vibrate(pattern)
|
return navigator.vibrate(pattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,48 @@
|
|||||||
import { Haptics } from "@capacitor/haptics"
|
//import { Haptics } from "@capacitor/haptics"
|
||||||
|
|
||||||
const NotfTypeToAudio = {
|
const NotfTypeToAudio = {
|
||||||
info: "notification",
|
info: "notification",
|
||||||
success: "notification",
|
success: "notification",
|
||||||
warning: "warn",
|
warning: "warn",
|
||||||
error: "error",
|
error: "error",
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationFeedback {
|
class NotificationFeedback {
|
||||||
static getSoundVolume = () => {
|
static getSoundVolume = () => {
|
||||||
return (window.app.cores.settings.get("sfx:notifications_volume") ?? 50) / 100
|
return (
|
||||||
}
|
(window.app.cores.settings.get("sfx:notifications_volume") ?? 50) /
|
||||||
|
100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
static playHaptic = async () => {
|
static playHaptic = async () => {
|
||||||
if (app.cores.settings.get("haptics:notifications_feedback")) {
|
if (app.cores.settings.get("haptics:notifications_feedback")) {
|
||||||
await Haptics.vibrate()
|
//await Haptics.vibrate()
|
||||||
}
|
//use navigator.vibrate
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static playAudio = (type) => {
|
static playAudio = (type) => {
|
||||||
if (app.cores.settings.get("sfx:notifications_feedback")) {
|
if (app.cores.settings.get("sfx:notifications_feedback")) {
|
||||||
if (typeof window.app.cores.sfx?.play === "function") {
|
if (typeof window.app.cores.sfx?.play === "function") {
|
||||||
window.app.cores.sfx.play(NotfTypeToAudio[type] ?? "notification", {
|
window.app.cores.sfx.play(
|
||||||
volume: NotificationFeedback.getSoundVolume(),
|
NotfTypeToAudio[type] ?? "notification",
|
||||||
})
|
{
|
||||||
}
|
volume: NotificationFeedback.getSoundVolume(),
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async feedback({ type = "notification", feedback = true }) {
|
static async feedback({ type = "notification", feedback = true }) {
|
||||||
if (!feedback) {
|
if (!feedback) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationFeedback.playHaptic(type)
|
NotificationFeedback.playHaptic(type)
|
||||||
NotificationFeedback.playAudio(type)
|
NotificationFeedback.playAudio(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationFeedback
|
export default NotificationFeedback
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
import { CapacitorMusicControls } from "capacitor-music-controls-plugin-v3"
|
|
||||||
|
|
||||||
export default class MediaSession {
|
|
||||||
initialize() {
|
|
||||||
CapacitorMusicControls.addListener("controlsNotification", (info) => {
|
|
||||||
console.log(info)
|
|
||||||
|
|
||||||
this.handleControlsEvent(info)
|
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener("controlsNotification", (event) => {
|
|
||||||
console.log(event)
|
|
||||||
|
|
||||||
const info = { message: event.message, position: 0 }
|
|
||||||
|
|
||||||
this.handleControlsEvent(info)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
update(manifest) {
|
|
||||||
try {
|
|
||||||
if ("mediaSession" in navigator) {
|
|
||||||
return navigator.mediaSession.metadata = new MediaMetadata({
|
|
||||||
title: manifest.title,
|
|
||||||
artist: manifest.artist,
|
|
||||||
album: manifest.album,
|
|
||||||
artwork: [
|
|
||||||
{
|
|
||||||
src: manifest.cover ?? manifest.thumbnail,
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/jpeg",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return CapacitorMusicControls.create({
|
|
||||||
track: manifest.title,
|
|
||||||
artist: manifest.artist,
|
|
||||||
album: manifest.album,
|
|
||||||
cover: manifest.cover,
|
|
||||||
|
|
||||||
hasPrev: false,
|
|
||||||
hasNext: false,
|
|
||||||
hasClose: true,
|
|
||||||
|
|
||||||
isPlaying: true,
|
|
||||||
dismissable: false,
|
|
||||||
|
|
||||||
playIcon: "media_play",
|
|
||||||
pauseIcon: "media_pause",
|
|
||||||
prevIcon: "media_prev",
|
|
||||||
nextIcon: "media_next",
|
|
||||||
closeIcon: "media_close",
|
|
||||||
notificationIcon: "notification"
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIsPlaying(to, timeElapsed = 0) {
|
|
||||||
try {
|
|
||||||
if ("mediaSession" in navigator) {
|
|
||||||
return navigator.mediaSession.playbackState = to ? "playing" : "paused"
|
|
||||||
}
|
|
||||||
|
|
||||||
return CapacitorMusicControls.updateIsPlaying({
|
|
||||||
isPlaying: to,
|
|
||||||
elapsed: timeElapsed,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if ("mediaSession" in navigator) {
|
|
||||||
navigator.mediaSession.playbackState = "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
this.active = false
|
|
||||||
|
|
||||||
return CapacitorMusicControls.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleControlsEvent(action) {
|
|
||||||
try {
|
|
||||||
const message = action.message
|
|
||||||
|
|
||||||
switch (message) {
|
|
||||||
case "music-controls-next": {
|
|
||||||
return app.cores.player.playback.next()
|
|
||||||
}
|
|
||||||
case "music-controls-previous": {
|
|
||||||
return app.cores.player.playback.previous()
|
|
||||||
}
|
|
||||||
case "music-controls-pause": {
|
|
||||||
return app.cores.player.playback.pause()
|
|
||||||
}
|
|
||||||
case "music-controls-play": {
|
|
||||||
return app.cores.player.playback.play()
|
|
||||||
}
|
|
||||||
case "music-controls-destroy": {
|
|
||||||
return app.cores.player.playback.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// External controls (iOS only)
|
|
||||||
case "music-controls-toggle-play-pause": {
|
|
||||||
return app.cores.player.playback.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headset events (Android only)
|
|
||||||
// All media button events are listed below
|
|
||||||
case "music-controls-media-button": {
|
|
||||||
return app.cores.player.playback.toggle()
|
|
||||||
}
|
|
||||||
case "music-controls-headset-unplugged": {
|
|
||||||
return app.cores.player.playback.pause()
|
|
||||||
}
|
|
||||||
case "music-controls-headset-plugged": {
|
|
||||||
return app.cores.player.playback.play()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,8 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import progressBar from "nprogress"
|
|
||||||
|
|
||||||
import Layouts from "@layouts"
|
import Layouts from "@layouts"
|
||||||
|
|
||||||
export default class Layout extends React.PureComponent {
|
export default class Layout extends React.PureComponent {
|
||||||
progressBar = progressBar.configure({ parent: "html", showSpinner: false })
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
layoutType: "default",
|
layoutType: "default",
|
||||||
renderError: null,
|
renderError: null,
|
||||||
@ -17,14 +14,18 @@ export default class Layout extends React.PureComponent {
|
|||||||
},
|
},
|
||||||
"layout.animations.fadeOut": () => {
|
"layout.animations.fadeOut": () => {
|
||||||
if (app.cores.settings.get("reduceAnimations")) {
|
if (app.cores.settings.get("reduceAnimations")) {
|
||||||
console.warn("Skipping fadeIn animation due to `reduceAnimations` setting")
|
console.warn(
|
||||||
|
"Skipping fadeIn animation due to `reduceAnimations` setting",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionLayer = document.getElementById("transitionLayer")
|
const transitionLayer = document.getElementById("transitionLayer")
|
||||||
|
|
||||||
if (!transitionLayer) {
|
if (!transitionLayer) {
|
||||||
console.warn("transitionLayer not found, no animation will be played")
|
console.warn(
|
||||||
|
"transitionLayer not found, no animation will be played",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,19 +33,23 @@ export default class Layout extends React.PureComponent {
|
|||||||
},
|
},
|
||||||
"layout.animations.fadeIn": () => {
|
"layout.animations.fadeIn": () => {
|
||||||
if (app.cores.settings.get("reduceAnimations")) {
|
if (app.cores.settings.get("reduceAnimations")) {
|
||||||
console.warn("Skipping fadeOut animation due to `reduceAnimations` setting")
|
console.warn(
|
||||||
|
"Skipping fadeOut animation due to `reduceAnimations` setting",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionLayer = document.getElementById("transitionLayer")
|
const transitionLayer = document.getElementById("transitionLayer")
|
||||||
|
|
||||||
if (!transitionLayer) {
|
if (!transitionLayer) {
|
||||||
console.warn("transitionLayer not found, no animation will be played")
|
console.warn(
|
||||||
|
"transitionLayer not found, no animation will be played",
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
transitionLayer.classList.remove("fade-opacity-leave")
|
transitionLayer.classList.remove("fade-opacity-leave")
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -58,7 +63,10 @@ export default class Layout extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (app.cores.settings.get("reduceAnimations")) {
|
if (app.cores.settings.get("reduceAnimations")) {
|
||||||
this.layoutInterface.toggleRootContainerClassname("reduce-animations", true)
|
this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"reduce-animations",
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.layoutInterface.toggleCenteredContent(true)
|
this.layoutInterface.toggleCenteredContent(true)
|
||||||
@ -75,7 +83,7 @@ export default class Layout extends React.PureComponent {
|
|||||||
this.setState({ renderError: { info, stack } })
|
this.setState({ renderError: { info, stack } })
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutInterface = window.app.layout = {
|
layoutInterface = (window.app.layout = {
|
||||||
set: (layout) => {
|
set: (layout) => {
|
||||||
if (typeof Layouts[layout] !== "function") {
|
if (typeof Layouts[layout] !== "function") {
|
||||||
return console.error("Layout not found")
|
return console.error("Layout not found")
|
||||||
@ -88,28 +96,52 @@ export default class Layout extends React.PureComponent {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
toggleCenteredContent: (to) => {
|
toggleCenteredContent: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("centered-content", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"centered-content",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleRootScaleEffect: (to) => {
|
toggleRootScaleEffect: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("root-scale-effect", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"root-scale-effect",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleMobileStyle: (to) => {
|
toggleMobileStyle: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("mobile", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"mobile",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleReducedAnimations: (to) => {
|
toggleReducedAnimations: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("reduce-animations", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"reduce-animations",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleTopBarSpacer: (to) => {
|
toggleTopBarSpacer: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("top-bar-spacer", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"top-bar-spacer",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleDisableTopLayoutPadding: (to) => {
|
toggleDisableTopLayoutPadding: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("disable-top-layout-padding", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"disable-top-layout-padding",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
togglePagePanelSpacer: (to) => {
|
togglePagePanelSpacer: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"page-panel-spacer",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleCompactMode: (to) => {
|
toggleCompactMode: (to) => {
|
||||||
return this.layoutInterface.toggleRootContainerClassname("compact-mode", to)
|
return this.layoutInterface.toggleRootContainerClassname(
|
||||||
|
"compact-mode",
|
||||||
|
to,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toggleRootContainerClassname: (classname, to) => {
|
toggleRootContainerClassname: (classname, to) => {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
@ -119,7 +151,10 @@ export default class Layout extends React.PureComponent {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
to = typeof to === "boolean" ? to : !root.classList.contains(classname)
|
to =
|
||||||
|
typeof to === "boolean"
|
||||||
|
? to
|
||||||
|
: !root.classList.contains(classname)
|
||||||
|
|
||||||
if (root.classList.contains(classname) === to) {
|
if (root.classList.contains(classname) === to) {
|
||||||
// ignore
|
// ignore
|
||||||
@ -154,8 +189,8 @@ export default class Layout extends React.PureComponent {
|
|||||||
...to,
|
...to,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let layoutType = this.state.layoutType
|
let layoutType = this.state.layoutType
|
||||||
@ -167,7 +202,10 @@ export default class Layout extends React.PureComponent {
|
|||||||
|
|
||||||
if (this.state.renderError) {
|
if (this.state.renderError) {
|
||||||
if (this.props.staticRenders?.RenderError) {
|
if (this.props.staticRenders?.RenderError) {
|
||||||
return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.renderError })
|
return React.createElement(
|
||||||
|
this.props.staticRenders?.RenderError,
|
||||||
|
{ error: this.state.renderError },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(this.state.renderError)
|
return JSON.stringify(this.state.renderError)
|
||||||
@ -176,11 +214,12 @@ export default class Layout extends React.PureComponent {
|
|||||||
const Layout = Layouts[layoutType]
|
const Layout = Layouts[layoutType]
|
||||||
|
|
||||||
if (!Layout) {
|
if (!Layout) {
|
||||||
return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`))
|
return app.eventBus.emit(
|
||||||
|
"runtime.crash",
|
||||||
|
new Error(`Layout type [${layoutType}] not found`),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Layout {...layoutComponentProps}>
|
return <Layout {...layoutComponentProps}>{this.props.children}</Layout>
|
||||||
{this.props.children}
|
|
||||||
</Layout>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Motion, spring } from "react-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
import { Icons, createIconRender } from "@components/Icons"
|
import { Icons, createIconRender } from "@components/Icons"
|
||||||
|
|
||||||
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/@mobile/quickNav"
|
import {
|
||||||
|
QuickNavMenuItems,
|
||||||
|
QuickNavMenu,
|
||||||
|
} from "@layouts/components/@mobile/quickNav"
|
||||||
|
|
||||||
import PlayerView from "@pages/@mobile-views/player"
|
import PlayerView from "@pages/@mobile-views/player"
|
||||||
import CreatorView from "@pages/@mobile-views/creator"
|
import CreatorView from "@pages/@mobile-views/creator"
|
||||||
@ -15,407 +18,434 @@ import CreatorView from "@pages/@mobile-views/creator"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const tourSteps = [
|
const tourSteps = [
|
||||||
{
|
{
|
||||||
title: "Quick nav",
|
title: "Quick nav",
|
||||||
description: "Tap & hold on the icon to open the navigation menu.",
|
description: "Tap & hold on the icon to open the navigation menu.",
|
||||||
placement: "top",
|
placement: "top",
|
||||||
refName: "navBtnRef",
|
refName: "navBtnRef",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Account button",
|
title: "Account button",
|
||||||
description: "Tap & hold on the account icon to open miscellaneous options.",
|
description:
|
||||||
placement: "top",
|
"Tap & hold on the account icon to open miscellaneous options.",
|
||||||
refName: "accountBtnRef",
|
placement: "top",
|
||||||
}
|
refName: "accountBtnRef",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const openPlayerView = () => {
|
const openPlayerView = () => {
|
||||||
app.layout.draggable.open("player", PlayerView)
|
app.layout.draggable.open("player", PlayerView)
|
||||||
}
|
}
|
||||||
const openCreator = () => {
|
const openCreator = () => {
|
||||||
app.layout.draggable.open("creator", CreatorView)
|
app.layout.draggable.open("creator", CreatorView)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlayerButton = (props) => {
|
const PlayerButton = (props) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
openPlayerView()
|
openPlayerView()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
className={classnames(
|
<div
|
||||||
"player_btn",
|
className={classnames("player_btn", {
|
||||||
{
|
bounce: props.playback === "playing",
|
||||||
"bounce": props.playback === "playing"
|
})}
|
||||||
}
|
style={{
|
||||||
)}
|
"--average-color": props.colorAnalysis?.rgba,
|
||||||
style={{
|
"--color": props.colorAnalysis?.isDark
|
||||||
"--average-color": props.colorAnalysis?.rgba,
|
? "var(--text-color-white)"
|
||||||
"--color": props.colorAnalysis?.isDark ? "var(--text-color-white)" : "var(--text-color-black)",
|
: "var(--text-color-black)",
|
||||||
}}
|
}}
|
||||||
onClick={openPlayerView}
|
onClick={openPlayerView}
|
||||||
>
|
>
|
||||||
{
|
{props.playback === "playing" ? (
|
||||||
props.playback === "playing" ? <Icons.MdMusicNote /> : <Icons.MdPause />
|
<Icons.MdMusicNote />
|
||||||
}
|
) : (
|
||||||
</div>
|
<Icons.MdPause />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountButton = React.forwardRef((props, ref) => {
|
const AccountButton = React.forwardRef((props, ref) => {
|
||||||
const user = app.userData
|
const user = app.userData
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return app.navigation.goAuth()
|
return app.navigation.goAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.navigation.goToAccount()
|
return app.navigation.goToAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHold = () => {
|
const handleHold = () => {
|
||||||
app.layout.draggable.actions({
|
app.layout.draggable.actions({
|
||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
key: "settings",
|
key: "settings",
|
||||||
icon: "FiSettings",
|
icon: "FiSettings",
|
||||||
label: "Settings",
|
label: "Settings",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
app.navigation.goToSettings()
|
app.navigation.goToSettings()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "account",
|
key: "account",
|
||||||
icon: "FiUser",
|
icon: "FiUser",
|
||||||
label: "Account",
|
label: "Account",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
app.navigation.goToAccount()
|
app.navigation.goToAccount()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "logout",
|
key: "logout",
|
||||||
icon: "FiLogOut",
|
icon: "FiLogOut",
|
||||||
label: "Logout",
|
label: "Logout",
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
app.eventBus.emit("app.logout_request")
|
app.eventBus.emit("app.logout_request")
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
key="account"
|
<div
|
||||||
id="account"
|
key="account"
|
||||||
className="item"
|
id="account"
|
||||||
ref={ref}
|
className="item"
|
||||||
onClick={handleClick}
|
ref={ref}
|
||||||
onContextMenu={handleHold}
|
onClick={handleClick}
|
||||||
context-menu="ignore"
|
onContextMenu={handleHold}
|
||||||
>
|
context-menu="ignore"
|
||||||
<div className="icon">
|
>
|
||||||
{
|
<div className="icon">
|
||||||
user ? <antd.Avatar shape="square" src={app.userData.avatar} /> : createIconRender("FiLogin")
|
{user ? (
|
||||||
}
|
<antd.Avatar shape="square" src={app.userData.avatar} />
|
||||||
</div>
|
) : (
|
||||||
</div>
|
createIconRender("FiLogin")
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export class BottomBar extends React.Component {
|
export class BottomBar extends React.Component {
|
||||||
static contextType = Context
|
static contextType = Context
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
visible: false,
|
visible: false,
|
||||||
quickNavVisible: false,
|
quickNavVisible: false,
|
||||||
render: null,
|
render: null,
|
||||||
tourOpen: false,
|
tourOpen: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
busEvents = {
|
busEvents = {
|
||||||
"runtime.crash": () => {
|
"runtime.crash": () => {
|
||||||
this.toggleVisibility(false)
|
this.toggleVisibility(false)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
navBtnRef = React.createRef()
|
navBtnRef = React.createRef()
|
||||||
accountBtnRef = React.createRef()
|
accountBtnRef = React.createRef()
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
this.interface = app.layout.bottom_bar = {
|
this.interface = app.layout.bottom_bar = {
|
||||||
toggleVisible: this.toggleVisibility,
|
toggleVisible: this.toggleVisibility,
|
||||||
isVisible: () => this.state.visible,
|
isVisible: () => this.state.visible,
|
||||||
render: (fragment) => {
|
render: (fragment) => {
|
||||||
this.setState({ render: fragment })
|
this.setState({ render: fragment })
|
||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
this.setState({ render: null })
|
this.setState({ render: null })
|
||||||
},
|
},
|
||||||
toggleTour: () => {
|
toggleTour: () => {
|
||||||
this.setState({ tourOpen: !this.state.tourOpen })
|
this.setState({ tourOpen: !this.state.tourOpen })
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({ visible: true })
|
this.setState({ visible: true })
|
||||||
}, 10)
|
}, 10)
|
||||||
|
|
||||||
// Register bus events
|
// Register bus events
|
||||||
Object.keys(this.busEvents).forEach((key) => {
|
Object.keys(this.busEvents).forEach((key) => {
|
||||||
app.eventBus.on(key, this.busEvents[key])
|
app.eventBus.on(key, this.busEvents[key])
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const isTourFinished = localStorage.getItem("mobile_tour")
|
const isTourFinished = localStorage.getItem("mobile_tour")
|
||||||
|
|
||||||
if (!isTourFinished) {
|
if (!isTourFinished) {
|
||||||
this.toggleTour(true)
|
this.toggleTour(true)
|
||||||
|
|
||||||
localStorage.setItem("mobile_tour", true)
|
localStorage.setItem("mobile_tour", true)
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
delete window.app.layout.bottom_bar
|
delete window.app.layout.bottom_bar
|
||||||
|
|
||||||
// Unregister bus events
|
// Unregister bus events
|
||||||
Object.keys(this.busEvents).forEach((key) => {
|
Object.keys(this.busEvents).forEach((key) => {
|
||||||
app.eventBus.off(key, this.busEvents[key])
|
app.eventBus.off(key, this.busEvents[key])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getTourSteps = () => {
|
getTourSteps = () => {
|
||||||
return tourSteps.map((step) => {
|
return tourSteps.map((step) => {
|
||||||
step.target = () => this[step.refName].current
|
step.target = () => this[step.refName].current
|
||||||
|
|
||||||
return step
|
return step
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVisibility = (to) => {
|
toggleVisibility = (to) => {
|
||||||
if (typeof to === "undefined") {
|
if (typeof to === "undefined") {
|
||||||
to = !this.state.visible
|
to = !this.state.visible
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ visible: to })
|
this.setState({ visible: to })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemClick = (item) => {
|
handleItemClick = (item) => {
|
||||||
if (item.dispatchEvent) {
|
if (item.dispatchEvent) {
|
||||||
app.eventBus.emit(item.dispatchEvent)
|
app.eventBus.emit(item.dispatchEvent)
|
||||||
} else if (item.location) {
|
} else if (item.location) {
|
||||||
app.location.push(item.location)
|
app.location.push(item.location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNavTouchStart = (e) => {
|
handleNavTouchStart = (e) => {
|
||||||
this._navTouchStart = setTimeout(() => {
|
this._navTouchStart = setTimeout(() => {
|
||||||
this.setState({ quickNavVisible: true })
|
this.setState({ quickNavVisible: true })
|
||||||
|
|
||||||
if (app.cores.haptics?.vibrate) {
|
if (app.cores.haptics?.vibrate) {
|
||||||
app.cores.haptics.vibrate(80)
|
app.cores.haptics.vibrate(80)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the timeout
|
// remove the timeout
|
||||||
this._navTouchStart = null
|
this._navTouchStart = null
|
||||||
}, 400)
|
}, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNavTouchEnd = (event) => {
|
handleNavTouchEnd = (event) => {
|
||||||
if (this._lastHovered) {
|
if (this._lastHovered) {
|
||||||
this._lastHovered.classList.remove("hover")
|
this._lastHovered.classList.remove("hover")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._navTouchStart) {
|
if (this._navTouchStart) {
|
||||||
clearTimeout(this._navTouchStart)
|
clearTimeout(this._navTouchStart)
|
||||||
|
|
||||||
this._navTouchStart = null
|
this._navTouchStart = null
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ quickNavVisible: false })
|
this.setState({ quickNavVisible: false })
|
||||||
|
|
||||||
// get cords of the touch
|
// get cords of the touch
|
||||||
const x = event.changedTouches[0].clientX
|
const x = event.changedTouches[0].clientX
|
||||||
const y = event.changedTouches[0].clientY
|
const y = event.changedTouches[0].clientY
|
||||||
|
|
||||||
// get the element at the touch
|
// get the element at the touch
|
||||||
const element = document.elementFromPoint(x, y)
|
const element = document.elementFromPoint(x, y)
|
||||||
|
|
||||||
// get the closest element with the attribute
|
// get the closest element with the attribute
|
||||||
const closest = element.closest(".quick-nav_item")
|
const closest = element.closest(".quick-nav_item")
|
||||||
|
|
||||||
if (!closest) {
|
if (!closest) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = QuickNavMenuItems.find((item) => {
|
const item = QuickNavMenuItems.find((item) => {
|
||||||
return item.id === closest.getAttribute("quicknav-item")
|
return item.id === closest.getAttribute("quicknav-item")
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.location) {
|
if (item.location) {
|
||||||
app.location.push(item.location)
|
app.location.push(item.location)
|
||||||
|
|
||||||
if (app.cores.haptics?.vibrate) {
|
if (app.cores.haptics?.vibrate) {
|
||||||
app.cores.haptics.vibrate([40, 80])
|
app.cores.haptics.vibrate([40, 80])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNavTouchMove = (event) => {
|
handleNavTouchMove = (event) => {
|
||||||
// check if the touch is hovering a quicknav item
|
// check if the touch is hovering a quicknav item
|
||||||
const x = event.changedTouches[0].clientX
|
const x = event.changedTouches[0].clientX
|
||||||
const y = event.changedTouches[0].clientY
|
const y = event.changedTouches[0].clientY
|
||||||
|
|
||||||
// get the element at the touch
|
// get the element at the touch
|
||||||
const element = document.elementFromPoint(x, y)
|
const element = document.elementFromPoint(x, y)
|
||||||
|
|
||||||
// get the closest element with the attribute
|
// get the closest element with the attribute
|
||||||
const closest = element.closest("[quicknav-item]")
|
const closest = element.closest("[quicknav-item]")
|
||||||
|
|
||||||
if (!closest) {
|
if (!closest) {
|
||||||
if (this._lastHovered) {
|
if (this._lastHovered) {
|
||||||
this._lastHovered.classList.remove("hover")
|
this._lastHovered.classList.remove("hover")
|
||||||
}
|
}
|
||||||
|
|
||||||
this._lastHovered = null
|
this._lastHovered = null
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._lastHovered !== closest) {
|
if (this._lastHovered !== closest) {
|
||||||
if (this._lastHovered) {
|
if (this._lastHovered) {
|
||||||
this._lastHovered.classList.remove("hover")
|
this._lastHovered.classList.remove("hover")
|
||||||
}
|
}
|
||||||
|
|
||||||
this._lastHovered = closest
|
this._lastHovered = closest
|
||||||
|
|
||||||
closest.classList.add("hover")
|
closest.classList.add("hover")
|
||||||
|
|
||||||
if (app.cores.haptics?.vibrate) {
|
if (app.cores.haptics?.vibrate) {
|
||||||
app.cores.haptics.vibrate(40)
|
app.cores.haptics.vibrate(40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTour = (to) => {
|
toggleTour = (to) => {
|
||||||
if (typeof to === "undefined") {
|
if (typeof to === "undefined") {
|
||||||
to = !this.state.tourOpen
|
to = !this.state.tourOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
tourOpen: to
|
tourOpen: to,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.render) {
|
if (this.state.render) {
|
||||||
return <div className="bottomBar">
|
return <div className="bottomBar">{this.state.render}</div>
|
||||||
{this.state.render}
|
}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightValue = this.state.visible ? Number(app.cores.style.getDefaultVar("bottom-bar-height").replace("px", "")) : 0
|
const heightValue = Number(
|
||||||
|
app.cores.style
|
||||||
|
.getDefaultVar("bottom-bar-height")
|
||||||
|
.replace("px", ""),
|
||||||
|
)
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{
|
<>
|
||||||
this.state.tourOpen && <antd.Tour
|
{this.state.tourOpen && (
|
||||||
open
|
<antd.Tour
|
||||||
steps={this.getTourSteps()}
|
open
|
||||||
onClose={() => this.setState({ tourOpen: false })}
|
steps={this.getTourSteps()}
|
||||||
/>
|
onClose={() => this.setState({ tourOpen: false })}
|
||||||
}
|
/>
|
||||||
<QuickNavMenu
|
)}
|
||||||
visible={this.state.quickNavVisible}
|
<QuickNavMenu visible={this.state.quickNavVisible} />
|
||||||
/>
|
|
||||||
|
|
||||||
<Motion style={{
|
<AnimatePresence>
|
||||||
y: spring(this.state.visible ? 0 : 300),
|
{this.state.visible && (
|
||||||
height: spring(heightValue)
|
<motion.div
|
||||||
}}>
|
className="bottomBar_wrapper"
|
||||||
{({ y, height }) =>
|
initial={{
|
||||||
<div className="bottomBar_wrapper">
|
height: 0,
|
||||||
<div
|
y: 300,
|
||||||
className="bottomBar"
|
}}
|
||||||
style={{
|
animate={{
|
||||||
WebkitTransform: `translateY(${y}px)`,
|
height: heightValue,
|
||||||
transform: `translateY(${y}px)`,
|
y: 0,
|
||||||
height: `${height}px`,
|
}}
|
||||||
}}
|
exit={{
|
||||||
>
|
height: 0,
|
||||||
<div className="items">
|
y: 300,
|
||||||
<div
|
}}
|
||||||
key="creator"
|
transition={{
|
||||||
id="creator"
|
type: "spring",
|
||||||
className={classnames("item", "primary")}
|
stiffness: 100,
|
||||||
onClick={openCreator}
|
damping: 20,
|
||||||
>
|
}}
|
||||||
<div className="icon">
|
>
|
||||||
{createIconRender("FiPlusCircle")}
|
<div className="bottomBar">
|
||||||
</div>
|
<div className="items">
|
||||||
</div>
|
<div
|
||||||
|
key="creator"
|
||||||
|
id="creator"
|
||||||
|
className={classnames(
|
||||||
|
"item",
|
||||||
|
"primary",
|
||||||
|
)}
|
||||||
|
onClick={openCreator}
|
||||||
|
>
|
||||||
|
<div className="icon">
|
||||||
|
{createIconRender("FiPlusCircle")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{this.context.track_manifest && (
|
||||||
this.context.track_manifest && <div
|
<div className="item">
|
||||||
className="item"
|
<PlayerButton
|
||||||
>
|
manifest={
|
||||||
<PlayerButton
|
this.context.track_manifest
|
||||||
manifest={this.context.track_manifest}
|
}
|
||||||
playback={this.context.playback_status}
|
playback={
|
||||||
colorAnalysis={this.context.track_manifest?.cover_analysis}
|
this.context.playback_status
|
||||||
/>
|
}
|
||||||
</div>
|
colorAnalysis={
|
||||||
}
|
this.context.track_manifest
|
||||||
|
?.cover_analysis
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
key="navigator"
|
key="navigator"
|
||||||
id="navigator"
|
id="navigator"
|
||||||
className="item"
|
className="item"
|
||||||
ref={this.navBtnRef}
|
ref={this.navBtnRef}
|
||||||
onClick={() => app.location.push("/")}
|
onClick={() => app.location.push("/")}
|
||||||
onTouchMove={this.handleNavTouchMove}
|
onTouchMove={this.handleNavTouchMove}
|
||||||
onTouchStart={this.handleNavTouchStart}
|
onTouchStart={this.handleNavTouchStart}
|
||||||
onTouchEnd={this.handleNavTouchEnd}
|
onTouchEnd={this.handleNavTouchEnd}
|
||||||
onTouchCancel={() => {
|
onTouchCancel={() => {
|
||||||
this.setState({ quickNavVisible: false })
|
this.setState({
|
||||||
}}
|
quickNavVisible: false,
|
||||||
>
|
})
|
||||||
<div className="icon">
|
}}
|
||||||
{createIconRender("FiHome")}
|
>
|
||||||
</div>
|
<div className="icon">
|
||||||
</div>
|
{createIconRender("FiHome")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
key="searcher"
|
key="searcher"
|
||||||
id="searcher"
|
id="searcher"
|
||||||
className="item"
|
className="item"
|
||||||
onClick={app.controls.openSearcher}
|
onClick={app.controls.openSearcher}
|
||||||
>
|
>
|
||||||
<div className="icon">
|
<div className="icon">
|
||||||
{createIconRender("FiSearch")}
|
{createIconRender("FiSearch")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountButton
|
<AccountButton ref={this.accountBtnRef} />
|
||||||
ref={this.accountBtnRef}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</AnimatePresence>
|
||||||
}
|
</>
|
||||||
</Motion>
|
)
|
||||||
</>
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
return <WithPlayerContext>
|
return (
|
||||||
<BottomBar
|
<WithPlayerContext>
|
||||||
{...props}
|
<BottomBar {...props} />
|
||||||
/>
|
</WithPlayerContext>
|
||||||
</WithPlayerContext>
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Motion, spring } from "react-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
import useLayoutInterface from "@hooks/useLayoutInterface"
|
import useLayoutInterface from "@hooks/useLayoutInterface"
|
||||||
import useDefaultVisibility from "@hooks/useDefaultVisibility"
|
import useDefaultVisibility from "@hooks/useDefaultVisibility"
|
||||||
@ -8,111 +8,125 @@ import useDefaultVisibility from "@hooks/useDefaultVisibility"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [visible, setVisible] = useDefaultVisibility()
|
const [visible, setVisible] = useDefaultVisibility()
|
||||||
const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] = React.useState(true)
|
const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] =
|
||||||
const [render, setRender] = React.useState(null)
|
React.useState(true)
|
||||||
|
const [render, setRender] = React.useState(null)
|
||||||
|
|
||||||
useLayoutInterface("top_bar", {
|
useLayoutInterface("top_bar", {
|
||||||
toggleVisibility: (to) => {
|
toggleVisibility: (to) => {
|
||||||
setVisible((prev) => {
|
setVisible((prev) => {
|
||||||
if (typeof to === undefined) {
|
if (typeof to === undefined) {
|
||||||
to = !prev
|
to = !prev
|
||||||
}
|
}
|
||||||
|
|
||||||
return to
|
return to
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
render: (component, options) => {
|
render: (component, options) => {
|
||||||
handleUpdateRender(component, options)
|
handleUpdateRender(component, options)
|
||||||
},
|
},
|
||||||
renderDefault: () => {
|
renderDefault: () => {
|
||||||
setRender(null)
|
setRender(null)
|
||||||
},
|
},
|
||||||
shouldUseTopBarSpacer: (to) => {
|
shouldUseTopBarSpacer: (to) => {
|
||||||
app.layout.toggleTopBarSpacer(to)
|
app.layout.toggleTopBarSpacer(to)
|
||||||
setShouldUseTopBarSpacer(to)
|
setShouldUseTopBarSpacer(to)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleUpdateRender = (...args) => {
|
const handleUpdateRender = (...args) => {
|
||||||
if (document.startViewTransition) {
|
if (document.startViewTransition) {
|
||||||
return document.startViewTransition(() => {
|
return document.startViewTransition(() => {
|
||||||
updateRender(...args)
|
updateRender(...args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateRender(...args)
|
return updateRender(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateRender = (component, options = {}) => {
|
const updateRender = (component, options = {}) => {
|
||||||
setRender({
|
setRender({
|
||||||
component,
|
component,
|
||||||
options
|
options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!shouldUseTopBarSpacer) {
|
if (!shouldUseTopBarSpacer) {
|
||||||
app.layout.togglePagePanelSpacer(true)
|
app.layout.togglePagePanelSpacer(true)
|
||||||
} else {
|
} else {
|
||||||
app.layout.togglePagePanelSpacer(false)
|
app.layout.togglePagePanelSpacer(false)
|
||||||
}
|
}
|
||||||
}, [shouldUseTopBarSpacer])
|
}, [shouldUseTopBarSpacer])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldUseTopBarSpacer) {
|
if (shouldUseTopBarSpacer) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
app.layout.toggleTopBarSpacer(true)
|
app.layout.toggleTopBarSpacer(true)
|
||||||
} else {
|
} else {
|
||||||
app.layout.toggleTopBarSpacer(false)
|
app.layout.toggleTopBarSpacer(false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
app.layout.togglePagePanelSpacer(true)
|
app.layout.togglePagePanelSpacer(true)
|
||||||
} else {
|
} else {
|
||||||
app.layout.togglePagePanelSpacer(false)
|
app.layout.togglePagePanelSpacer(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.layout.toggleTopBarSpacer(false)
|
app.layout.toggleTopBarSpacer(false)
|
||||||
}
|
}
|
||||||
}, [visible])
|
}, [visible])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (render) {
|
if (render) {
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
} else {
|
} else {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
}, [render])
|
}, [render])
|
||||||
|
|
||||||
const heightValue = visible ? Number(app.cores.style.getDefaultVar("top-bar-height").replace("px", "")) : 0
|
const heightValue = Number(
|
||||||
|
app.cores.style.getDefaultVar("top-bar-height").replace("px", ""),
|
||||||
|
)
|
||||||
|
|
||||||
return <Motion style={{
|
return (
|
||||||
y: spring(visible ? 0 : 300,),
|
<AnimatePresence>
|
||||||
height: spring(heightValue,),
|
{visible && (
|
||||||
}}>
|
<motion.div
|
||||||
{({ y, height }) => {
|
className="top-bar_wrapper"
|
||||||
return <>
|
animate={{
|
||||||
<div
|
y: 0,
|
||||||
className="top-bar_wrapper"
|
height: heightValue,
|
||||||
style={{
|
}}
|
||||||
WebkitTransform: `translateY(-${y}px)`,
|
initial={{
|
||||||
transform: `translateY(-${y}px)`,
|
y: -300,
|
||||||
height: `${height}px`,
|
height: 0,
|
||||||
}}
|
}}
|
||||||
>
|
exit={{
|
||||||
<div
|
y: -300,
|
||||||
className={classnames(
|
height: 0,
|
||||||
"top-bar",
|
}}
|
||||||
render?.options?.className,
|
transition={{
|
||||||
)}
|
type: "spring",
|
||||||
>
|
stiffness: 100,
|
||||||
{
|
damping: 20,
|
||||||
render?.component && React.cloneElement(render?.component, render?.options?.props ?? {})
|
}}
|
||||||
}
|
>
|
||||||
</div>
|
<div
|
||||||
</div>
|
className={classnames(
|
||||||
</>
|
"top-bar",
|
||||||
}}
|
render?.options?.className,
|
||||||
</Motion>
|
)}
|
||||||
}
|
>
|
||||||
|
{render?.component &&
|
||||||
|
React.cloneElement(
|
||||||
|
render?.component,
|
||||||
|
render?.options?.props ?? {},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import * as antd from "antd"
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ export class Drawer extends React.Component {
|
|||||||
this.toggleVisibility(false)
|
this.toggleVisibility(false)
|
||||||
|
|
||||||
this.props.controller.close(this.props.id, {
|
this.props.controller.close(this.props.id, {
|
||||||
transition: 150
|
transition: 150,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,31 +64,34 @@ export class Drawer extends React.Component {
|
|||||||
handleDone: this.handleDone,
|
handleDone: this.handleDone,
|
||||||
handleFail: this.handleFail,
|
handleFail: this.handleFail,
|
||||||
}
|
}
|
||||||
return <AnimatePresence>
|
return (
|
||||||
{
|
<AnimatePresence>
|
||||||
this.state.visible && <motion.div
|
{this.state.visible && (
|
||||||
key={this.props.id}
|
<motion.div
|
||||||
id={this.props.id}
|
key={this.props.id}
|
||||||
className="drawer"
|
id={this.props.id}
|
||||||
style={{
|
className="drawer"
|
||||||
...this.options.style,
|
style={{
|
||||||
}}
|
...this.options.style,
|
||||||
transformTemplate={transformTemplate}
|
}}
|
||||||
animate={{
|
transformTemplate={transformTemplate}
|
||||||
x: [-100, 0],
|
animate={{
|
||||||
opacity: [0, 1],
|
x: [-100, 0],
|
||||||
}}
|
opacity: [0, 1],
|
||||||
exit={{
|
}}
|
||||||
x: [0, -100],
|
exit={{
|
||||||
opacity: [1, 0],
|
x: [0, -100],
|
||||||
}}
|
opacity: [1, 0],
|
||||||
>
|
}}
|
||||||
{
|
>
|
||||||
React.createElement(this.props.children, componentProps)
|
{React.createElement(
|
||||||
}
|
this.props.children,
|
||||||
</motion.div>
|
componentProps,
|
||||||
}
|
)}
|
||||||
</AnimatePresence>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,10 +173,12 @@ export default class DrawerController extends React.Component {
|
|||||||
if (lastDrawer) {
|
if (lastDrawer) {
|
||||||
if (app.layout.modal && lastDrawer.options.confirmOnOutsideClick) {
|
if (app.layout.modal && lastDrawer.options.confirmOnOutsideClick) {
|
||||||
return app.layout.modal.confirm({
|
return app.layout.modal.confirm({
|
||||||
descriptionText: lastDrawer.options.confirmOnOutsideClickText ?? "Are you sure you want to close this drawer?",
|
descriptionText:
|
||||||
|
lastDrawer.options.confirmOnOutsideClickText ??
|
||||||
|
"Are you sure you want to close this drawer?",
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
lastDrawer.close()
|
lastDrawer.close()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,18 +206,12 @@ export default class DrawerController extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof addresses[id] === "undefined") {
|
if (typeof addresses[id] === "undefined") {
|
||||||
drawers.push(<Drawer
|
drawers.push(<Drawer key={id} {...instance} />)
|
||||||
key={id}
|
|
||||||
{...instance}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
addresses[id] = drawers.length - 1
|
addresses[id] = drawers.length - 1
|
||||||
refs[id] = instance.ref
|
refs[id] = instance.ref
|
||||||
} else {
|
} else {
|
||||||
drawers[addresses[id]] = <Drawer
|
drawers[addresses[id]] = <Drawer key={id} {...instance} />
|
||||||
key={id}
|
|
||||||
{...instance}
|
|
||||||
/>
|
|
||||||
refs[id] = instance.ref
|
refs[id] = instance.ref
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,37 +265,39 @@ export default class DrawerController extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return (
|
||||||
<AnimatePresence>
|
<>
|
||||||
{
|
|
||||||
this.state.maskVisible && <motion.div
|
|
||||||
className="drawers-mask"
|
|
||||||
onClick={() => this.closeLastDrawer()}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classnames(
|
|
||||||
"drawers-wrapper",
|
|
||||||
{
|
|
||||||
["hidden"]: !this.state.drawers.length,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{this.state.drawers}
|
{this.state.maskVisible && (
|
||||||
|
<motion.div
|
||||||
|
className="drawers-mask"
|
||||||
|
onClick={() => this.closeLastDrawer()}
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 20,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
|
||||||
</>
|
<div
|
||||||
|
className={classnames("drawers-wrapper", {
|
||||||
|
["hidden"]: !this.state.drawers.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AnimatePresence>{this.state.drawers}</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,64 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Motion, spring } from "react-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
import useLayoutInterface from "@hooks/useLayoutInterface"
|
import useLayoutInterface from "@hooks/useLayoutInterface"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [render, setRender] = React.useState(null)
|
const [render, setRender] = React.useState(null)
|
||||||
|
|
||||||
useLayoutInterface("header", {
|
useLayoutInterface("header", {
|
||||||
render: (component, options) => {
|
render: (component, options) => {
|
||||||
if (component === null) {
|
if (component === null) {
|
||||||
return setRender(null)
|
return setRender(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return setRender({
|
return setRender({
|
||||||
component,
|
component,
|
||||||
options
|
options,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (render) {
|
if (render) {
|
||||||
app.layout.toggleDisableTopLayoutPadding(true)
|
app.layout.toggleDisableTopLayoutPadding(true)
|
||||||
} else {
|
} else {
|
||||||
app.layout.toggleDisableTopLayoutPadding(false)
|
app.layout.toggleDisableTopLayoutPadding(false)
|
||||||
}
|
}
|
||||||
}, [render])
|
}, [render])
|
||||||
|
|
||||||
return <Motion
|
return (
|
||||||
style={{
|
<AnimatePresence>
|
||||||
y: spring(render ? 0 : 100,),
|
{render && (
|
||||||
}}
|
<motion.div
|
||||||
>
|
className="page_header_wrapper"
|
||||||
{({ y, height }) => {
|
animate={{
|
||||||
return <div
|
y: 0,
|
||||||
className={classnames(
|
}}
|
||||||
"page_header_wrapper",
|
initial={{
|
||||||
{
|
y: -100,
|
||||||
["hidden"]: !render,
|
}}
|
||||||
}
|
exit={{
|
||||||
)}
|
y: -100,
|
||||||
style={{
|
}}
|
||||||
WebkitTransform: `translateY(-${y}px)`,
|
transition={{
|
||||||
transform: `translateY(-${y}px)`,
|
type: "spring",
|
||||||
}}
|
stiffness: 100,
|
||||||
>
|
damping: 20,
|
||||||
<div
|
}}
|
||||||
className="page_header"
|
>
|
||||||
>
|
<div className="page_header">
|
||||||
{
|
{render?.component &&
|
||||||
render?.component && React.cloneElement(
|
React.cloneElement(
|
||||||
render?.component,
|
render?.component,
|
||||||
render?.options?.props ?? {}
|
render?.options?.props ?? {},
|
||||||
)
|
)}
|
||||||
}
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
)}
|
||||||
}}
|
</AnimatePresence>
|
||||||
</Motion>
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import React from "react"
|
|||||||
import config from "@config"
|
import config from "@config"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Translation } from "react-i18next"
|
import { Translation } from "react-i18next"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
import { Menu, Avatar, Dropdown, Tag } from "antd"
|
import { Menu, Avatar, Dropdown, Tag } from "antd"
|
||||||
|
|
||||||
import Drawer from "@layouts/components/drawer"
|
import Drawer from "@layouts/components/drawer"
|
||||||
|
@ -1,119 +1,133 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Motion, spring } from "react-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
import WidgetsWrapper from "@components/WidgetsWrapper"
|
import WidgetsWrapper from "@components/WidgetsWrapper"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default class ToolsBar extends React.Component {
|
export default class ToolsBar extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
visible: false,
|
visible: false,
|
||||||
renders: {
|
renders: {
|
||||||
top: [],
|
top: [],
|
||||||
bottom: [],
|
bottom: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
app.layout.tools_bar = this.interface
|
app.layout.tools_bar = this.interface
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
visible: true,
|
visible: true,
|
||||||
})
|
})
|
||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
delete app.layout.tools_bar
|
delete app.layout.tools_bar
|
||||||
}
|
}
|
||||||
|
|
||||||
interface = {
|
interface = {
|
||||||
toggleVisibility: (to) => {
|
toggleVisibility: (to) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
visible: to ?? !this.state.visible,
|
visible: to ?? !this.state.visible,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
attachRender: (id, component, props, { position = "bottom" } = {}) => {
|
attachRender: (id, component, props, { position = "bottom" } = {}) => {
|
||||||
this.setState((prev) => {
|
this.setState((prev) => {
|
||||||
prev.renders[position].push({
|
prev.renders[position].push({
|
||||||
id: id,
|
id: id,
|
||||||
component: component,
|
component: component,
|
||||||
props: props,
|
props: props,
|
||||||
})
|
})
|
||||||
|
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
|
|
||||||
return component
|
return component
|
||||||
},
|
},
|
||||||
detachRender: (id) => {
|
detachRender: (id) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
renders: {
|
renders: {
|
||||||
top: this.state.renders.top.filter((render) => render.id !== id),
|
top: this.state.renders.top.filter(
|
||||||
bottom: this.state.renders.bottom.filter((render) => render.id !== id),
|
(render) => render.id !== id,
|
||||||
},
|
),
|
||||||
})
|
bottom: this.state.renders.bottom.filter(
|
||||||
|
(render) => render.id !== id,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hasAnyRenders = this.state.renders.top.length > 0 || this.state.renders.bottom.length > 0
|
const hasAnyRenders =
|
||||||
|
this.state.renders.top.length > 0 ||
|
||||||
|
this.state.renders.bottom.length > 0
|
||||||
|
|
||||||
const isVisible = hasAnyRenders && this.state.visible
|
const isVisible = hasAnyRenders && this.state.visible
|
||||||
|
|
||||||
return <Motion
|
return (
|
||||||
style={{
|
<AnimatePresence>
|
||||||
x: spring(isVisible ? 0 : 100),
|
{isVisible && (
|
||||||
width: spring(isVisible ? 100 : 0),
|
<motion.div
|
||||||
}}
|
className={classnames("tools-bar-wrapper", {
|
||||||
>
|
["visible"]: isVisible,
|
||||||
{({ x, width }) => {
|
})}
|
||||||
return <div
|
animate={{
|
||||||
style={{
|
x: 0,
|
||||||
width: `${width}%`,
|
width: "100%",
|
||||||
transform: `translateX(${x}%)`,
|
}}
|
||||||
}}
|
initial={{
|
||||||
className={classnames(
|
x: 100,
|
||||||
"tools-bar-wrapper",
|
width: "0%",
|
||||||
{
|
}}
|
||||||
visible: isVisible,
|
exit={{
|
||||||
}
|
x: 100,
|
||||||
)}
|
width: "0%",
|
||||||
>
|
}}
|
||||||
<div
|
transition={{
|
||||||
id="tools_bar"
|
type: "spring",
|
||||||
className="tools-bar"
|
stiffness: 100,
|
||||||
>
|
damping: 20,
|
||||||
<div className="attached_renders top">
|
}}
|
||||||
{
|
>
|
||||||
this.state.renders.top.map((render, index) => {
|
<div id="tools_bar" className="tools-bar">
|
||||||
return React.createElement(render.component, {
|
<div className="attached_renders top">
|
||||||
...render.props,
|
{this.state.renders.top.map((render, index) => {
|
||||||
key: index,
|
return React.createElement(
|
||||||
})
|
render.component,
|
||||||
})
|
{
|
||||||
}
|
...render.props,
|
||||||
</div>
|
key: index,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<WidgetsWrapper />
|
<WidgetsWrapper />
|
||||||
|
|
||||||
<div className="attached_renders bottom">
|
<div className="attached_renders bottom">
|
||||||
{
|
{this.state.renders.bottom.map(
|
||||||
this.state.renders.bottom.map((render, index) => {
|
(render, index) => {
|
||||||
return React.createElement(render.component, {
|
return React.createElement(
|
||||||
...render.props,
|
render.component,
|
||||||
key: index,
|
{
|
||||||
})
|
...render.props,
|
||||||
})
|
key: index,
|
||||||
}
|
},
|
||||||
</div>
|
)
|
||||||
</div>
|
},
|
||||||
</div>
|
)}
|
||||||
}}
|
</div>
|
||||||
</Motion>
|
</div>
|
||||||
}
|
</motion.div>
|
||||||
}
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,79 +1,78 @@
|
|||||||
@import "@styles/vars.less";
|
@import "@styles/vars.less";
|
||||||
|
|
||||||
.tools-bar-wrapper {
|
.tools-bar-wrapper {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
z-index: 150;
|
z-index: 150;
|
||||||
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.visible) {
|
&:not(.visible) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tools-bar {
|
.tools-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
border-radius: @sidebar_borderRadius;
|
border-radius: @sidebar_borderRadius;
|
||||||
box-shadow: @card-shadow;
|
box-shadow: @card-shadow;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
flex: 0;
|
flex: 0;
|
||||||
|
|
||||||
.attached_renders {
|
.attached_renders {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
&.bottom {
|
&.bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
background-color: var(--background-color-primary);
|
background-color: var(--background-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import FollowButton from "@components/FollowButton"
|
import FollowButton from "@components/FollowButton"
|
||||||
@ -21,10 +21,10 @@ import FollowersTab from "./tabs/followers"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const TabsComponent = {
|
const TabsComponent = {
|
||||||
"posts": PostsTab,
|
posts: PostsTab,
|
||||||
"followers": FollowersTab,
|
followers: FollowersTab,
|
||||||
"details": DetailsTab,
|
details: DetailsTab,
|
||||||
"music": MusicTab,
|
music: MusicTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Account extends React.Component {
|
export default class Account extends React.Component {
|
||||||
@ -60,7 +60,7 @@ export default class Account extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user = await UserModel.data({
|
user = await UserModel.data({
|
||||||
username: requestedUser
|
username: requestedUser,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
@ -77,7 +77,9 @@ export default class Account extends React.Component {
|
|||||||
|
|
||||||
console.log(`Loaded User [${user.username}] >`, user)
|
console.log(`Loaded User [${user.username}] >`, user)
|
||||||
|
|
||||||
const followersResult = await FollowsModel.getFollowers(user._id).catch(() => false)
|
const followersResult = await FollowsModel.getFollowers(
|
||||||
|
user._id,
|
||||||
|
).catch(() => false)
|
||||||
|
|
||||||
if (followersResult) {
|
if (followersResult) {
|
||||||
followersCount = followersResult.count
|
followersCount = followersResult.count
|
||||||
@ -118,7 +120,10 @@ export default class Account extends React.Component {
|
|||||||
|
|
||||||
handlePageTransition = (key) => {
|
handlePageTransition = (key) => {
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
console.error("Cannot handle page transition. Invalid key, only valid passing string", key)
|
console.error(
|
||||||
|
"Cannot handle page transition. Invalid key, only valid passing string",
|
||||||
|
key,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +134,7 @@ export default class Account extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
tabActiveKey: key
|
tabActiveKey: key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,119 +142,119 @@ export default class Account extends React.Component {
|
|||||||
const user = this.state.user
|
const user = this.state.user
|
||||||
|
|
||||||
if (this.state.isNotExistent) {
|
if (this.state.isNotExistent) {
|
||||||
return <antd.Result
|
return (
|
||||||
status="404"
|
<antd.Result
|
||||||
title="This user does not exist, yet..."
|
status="404"
|
||||||
>
|
title="This user does not exist, yet..."
|
||||||
|
></antd.Result>
|
||||||
</antd.Result>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
id="profile"
|
<div
|
||||||
className={classnames(
|
id="profile"
|
||||||
"account-profile",
|
className={classnames("account-profile", {
|
||||||
{
|
|
||||||
["withCover"]: user.cover,
|
["withCover"]: user.cover,
|
||||||
}
|
})}
|
||||||
)}
|
>
|
||||||
>
|
{user.cover && (
|
||||||
{
|
<div
|
||||||
user.cover && <div
|
className={classnames("cover", {
|
||||||
className={classnames("cover", {
|
["expanded"]: this.state.coverExpanded,
|
||||||
["expanded"]: this.state.coverExpanded
|
})}
|
||||||
})}
|
style={{ backgroundImage: `url("${user.cover}")` }}
|
||||||
style={{ backgroundImage: `url("${user.cover}")` }}
|
onClick={() => this.toggleCoverExpanded()}
|
||||||
onClick={() => this.toggleCoverExpanded()}
|
id="profile-cover"
|
||||||
id="profile-cover"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="panels">
|
|
||||||
<div className="left-panel">
|
|
||||||
<UserCard
|
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="actions">
|
<div className="panels">
|
||||||
<FollowButton
|
<div className="left-panel">
|
||||||
count={this.state.followersCount}
|
<UserCard user={user} />
|
||||||
onClick={this.onClickFollow}
|
|
||||||
followed={this.state.following}
|
|
||||||
self={this.state.isSelf}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
<div className="actions">
|
||||||
!this.state.isSelf && <antd.Button
|
<FollowButton
|
||||||
icon={<Icons.MdMessage />}
|
count={this.state.followersCount}
|
||||||
onClick={() => app.location.push(`/messages/${user._id}`)}
|
onClick={this.onClickFollow}
|
||||||
|
followed={this.state.following}
|
||||||
|
self={this.state.isSelf}
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
|
{!this.state.isSelf && (
|
||||||
|
<antd.Button
|
||||||
|
icon={<Icons.MdMessage />}
|
||||||
|
onClick={() =>
|
||||||
|
app.location.push(
|
||||||
|
`/messages/${user._id}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="center-panel" ref={this.contentRef}>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.15,
|
||||||
|
}}
|
||||||
|
key={this.state.tabActiveKey}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.createElement(
|
||||||
|
TabsComponent[this.state.tabActiveKey],
|
||||||
|
{
|
||||||
|
onTopVisibility:
|
||||||
|
this.onPostListTopVisibility,
|
||||||
|
state: this.state,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="right-panel">
|
||||||
|
<antd.Menu
|
||||||
|
className="tabMenu"
|
||||||
|
mode={app.isMobile ? "horizontal" : "vertical"}
|
||||||
|
selectedKeys={[this.state.tabActiveKey]}
|
||||||
|
onClick={(e) => this.handlePageTransition(e.key)}
|
||||||
|
items={GenerateMenuItems([
|
||||||
|
{
|
||||||
|
id: "posts",
|
||||||
|
label: "Posts",
|
||||||
|
icon: "FiBookOpen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "music",
|
||||||
|
label: "Music",
|
||||||
|
icon: "MdAlbum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "followers",
|
||||||
|
label: "Followers",
|
||||||
|
icon: "FiUsers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "details",
|
||||||
|
label: "Details",
|
||||||
|
icon: "FiInfo",
|
||||||
|
},
|
||||||
|
])}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className="center-panel"
|
|
||||||
ref={this.contentRef}
|
|
||||||
>
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.15,
|
|
||||||
}}
|
|
||||||
key={this.state.tabActiveKey}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
React.createElement(TabsComponent[this.state.tabActiveKey], {
|
|
||||||
onTopVisibility: this.onPostListTopVisibility,
|
|
||||||
state: this.state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="right-panel">
|
|
||||||
<antd.Menu
|
|
||||||
className="tabMenu"
|
|
||||||
mode={app.isMobile ? "horizontal" : "vertical"}
|
|
||||||
selectedKeys={[this.state.tabActiveKey]}
|
|
||||||
onClick={(e) => this.handlePageTransition(e.key)}
|
|
||||||
items={GenerateMenuItems([
|
|
||||||
{
|
|
||||||
id: "posts",
|
|
||||||
label: "Posts",
|
|
||||||
icon: "FiBookOpen",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "music",
|
|
||||||
label: "Music",
|
|
||||||
icon: "MdAlbum",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "followers",
|
|
||||||
label: "Followers",
|
|
||||||
icon: "FiUsers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "details",
|
|
||||||
label: "Details",
|
|
||||||
icon: "FiInfo",
|
|
||||||
}
|
|
||||||
])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,153 +1,162 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Motion, spring } from "react-motion"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
|
|
||||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
const LyricsText = React.forwardRef((props, textRef) => {
|
const LyricsText = React.forwardRef((props, textRef) => {
|
||||||
const [playerState] = usePlayerStateContext()
|
const [playerState] = usePlayerStateContext()
|
||||||
|
|
||||||
const { lyrics } = props
|
const { lyrics } = props
|
||||||
|
|
||||||
const [syncInterval, setSyncInterval] = React.useState(null)
|
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||||
const [currentLineIndex, setCurrentLineIndex] = React.useState(0)
|
const [currentLineIndex, setCurrentLineIndex] = React.useState(0)
|
||||||
const [visible, setVisible] = React.useState(false)
|
const [visible, setVisible] = React.useState(false)
|
||||||
|
|
||||||
function syncPlayback() {
|
function syncPlayback() {
|
||||||
const currentTrackTime = app.cores.player.controls.seek() * 1000
|
const currentTrackTime = app.cores.player.controls.seek() * 1000
|
||||||
|
|
||||||
const lineIndex = lyrics.synced_lyrics.findIndex((line) => {
|
const lineIndex = lyrics.synced_lyrics.findIndex((line) => {
|
||||||
return currentTrackTime >= line.startTimeMs && currentTrackTime <= line.endTimeMs
|
return (
|
||||||
})
|
currentTrackTime >= line.startTimeMs &&
|
||||||
|
currentTrackTime <= line.endTimeMs
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if (lineIndex === -1) {
|
if (lineIndex === -1) {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const line = lyrics.synced_lyrics[lineIndex]
|
const line = lyrics.synced_lyrics[lineIndex]
|
||||||
|
|
||||||
setCurrentLineIndex(lineIndex)
|
setCurrentLineIndex(lineIndex)
|
||||||
|
|
||||||
if (line.break) {
|
if (line.break) {
|
||||||
return setVisible(false)
|
return setVisible(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.text) {
|
if (line.text) {
|
||||||
return setVisible(true)
|
return setVisible(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSyncInterval() {
|
function startSyncInterval() {
|
||||||
if (!lyrics || !lyrics.synced_lyrics) {
|
if (!lyrics || !lyrics.synced_lyrics) {
|
||||||
stopSyncInterval()
|
stopSyncInterval()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerState.playback_status !== "playing") {
|
if (playerState.playback_status !== "playing") {
|
||||||
stopSyncInterval()
|
stopSyncInterval()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (syncInterval) {
|
if (syncInterval) {
|
||||||
stopSyncInterval()
|
stopSyncInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
setSyncInterval(setInterval(syncPlayback, 100))
|
setSyncInterval(setInterval(syncPlayback, 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSyncInterval() {
|
function stopSyncInterval() {
|
||||||
clearInterval(syncInterval)
|
clearInterval(syncInterval)
|
||||||
setSyncInterval(null)
|
setSyncInterval(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
//* Handle when current line index change
|
//* Handle when current line index change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (currentLineIndex === 0) {
|
if (currentLineIndex === 0) {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
} else {
|
} else {
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
|
|
||||||
// find line element by id
|
// find line element by id
|
||||||
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`)
|
const lineElement = textRef.current.querySelector(
|
||||||
|
`#lyrics-line-${currentLineIndex}`,
|
||||||
|
)
|
||||||
|
|
||||||
// center scroll to current line
|
// center scroll to current line
|
||||||
if (lineElement) {
|
if (lineElement) {
|
||||||
lineElement.scrollIntoView({
|
lineElement.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "center",
|
block: "center",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// scroll to top
|
// scroll to top
|
||||||
textRef.current.scrollTop = 0
|
textRef.current.scrollTop = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [currentLineIndex])
|
}, [currentLineIndex])
|
||||||
|
|
||||||
//* Handle when playback status change
|
//* Handle when playback status change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
startSyncInterval()
|
startSyncInterval()
|
||||||
}, [playerState.playback_status])
|
}, [playerState.playback_status])
|
||||||
|
|
||||||
//* Handle when manifest object change, reset...
|
//* Handle when manifest object change, reset...
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
setCurrentLineIndex(0)
|
setCurrentLineIndex(0)
|
||||||
}, [playerState.track_manifest])
|
}, [playerState.track_manifest])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
startSyncInterval()
|
startSyncInterval()
|
||||||
}, [lyrics])
|
}, [lyrics])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(syncInterval)
|
clearInterval(syncInterval)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!lyrics?.synced_lyrics) {
|
if (!lyrics?.synced_lyrics) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
className="lyrics-text-wrapper"
|
<div className="lyrics-text-wrapper">
|
||||||
>
|
<AnimatePresence>
|
||||||
<Motion
|
{visible && (
|
||||||
style={{
|
<motion.div
|
||||||
opacity: spring(visible ? 1 : 0),
|
ref={textRef}
|
||||||
}}
|
className="lyrics-text"
|
||||||
>
|
animate={{
|
||||||
{({ opacity }) => {
|
opacity: 1,
|
||||||
return <div
|
}}
|
||||||
ref={textRef}
|
initial={{
|
||||||
className="lyrics-text"
|
opacity: 0,
|
||||||
style={{
|
}}
|
||||||
opacity
|
exit={{
|
||||||
}}
|
opacity: 0,
|
||||||
>
|
}}
|
||||||
{
|
transition={{
|
||||||
lyrics.synced_lyrics.map((line, index) => {
|
type: "spring",
|
||||||
return <p
|
stiffness: 100,
|
||||||
key={index}
|
damping: 20,
|
||||||
id={`lyrics-line-${index}`}
|
}}
|
||||||
className={classnames(
|
>
|
||||||
"line",
|
{lyrics.synced_lyrics.map((line, index) => {
|
||||||
{
|
return (
|
||||||
["current"]: currentLineIndex === index
|
<p
|
||||||
}
|
key={index}
|
||||||
)}
|
id={`lyrics-line-${index}`}
|
||||||
>
|
className={classnames("line", {
|
||||||
{line.text}
|
["current"]: currentLineIndex === index,
|
||||||
</p>
|
})}
|
||||||
})
|
>
|
||||||
}
|
{line.text}
|
||||||
</div>
|
</p>
|
||||||
}}
|
)
|
||||||
</Motion>
|
})}
|
||||||
</div>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default LyricsText
|
export default LyricsText
|
||||||
|
@ -10,281 +10,299 @@ import config from "@config"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const connectionsTooltipStrings = {
|
const connectionsTooltipStrings = {
|
||||||
secure: "This connection is secure",
|
secure: "This connection is secure",
|
||||||
insecure: "This connection is insecure, cause it's not using HTTPS protocol and the server cannot be verified on the trusted certificate authority.",
|
insecure:
|
||||||
warning: "This connection is secure but the server cannot be verified on the trusted certificate authority.",
|
"This connection is insecure, cause it's not using HTTPS protocol and the server cannot be verified on the trusted certificate authority.",
|
||||||
|
warning:
|
||||||
|
"This connection is secure but the server cannot be verified on the trusted certificate authority.",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "about",
|
id: "about",
|
||||||
icon: "FiInfo",
|
icon: "FiInfo",
|
||||||
label: "About",
|
label: "About",
|
||||||
group: "bottom",
|
group: "bottom",
|
||||||
render: () => {
|
render: () => {
|
||||||
const isProduction = import.meta.env.PROD
|
const isProduction = import.meta.env.PROD
|
||||||
|
|
||||||
const [serverManifest, setServerManifest] = React.useState(null)
|
const [serverManifest, setServerManifest] = React.useState(null)
|
||||||
const [serverOrigin, setServerOrigin] = React.useState(null)
|
const [serverOrigin, setServerOrigin] = React.useState(null)
|
||||||
const [secureConnection, setSecureConnection] = React.useState(false)
|
const [secureConnection, setSecureConnection] = React.useState(false)
|
||||||
const [capInfo, setCapInfo] = React.useState(null)
|
const [capInfo, setCapInfo] = React.useState(null)
|
||||||
|
|
||||||
const setCapacitorInfo = async () => {
|
const setCapacitorInfo = async () => {
|
||||||
if (Capacitor.Plugins.App) {
|
if (Capacitor) {
|
||||||
const info = await Capacitor.Plugins.App.getInfo()
|
if (Capacitor.Plugins.App) {
|
||||||
|
const info = await Capacitor.Plugins.App.getInfo()
|
||||||
|
|
||||||
setCapInfo(info)
|
setCapInfo(info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const checkServerVersion = async () => {
|
const checkServerVersion = async () => {
|
||||||
const serverManifest = await app.cores.api.customRequest()
|
const serverManifest = await app.cores.api.customRequest()
|
||||||
|
|
||||||
setServerManifest(serverManifest.data)
|
setServerManifest(serverManifest.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkServerOrigin = async () => {
|
const checkServerOrigin = async () => {
|
||||||
const instance = app.cores.api.client()
|
const instance = app.cores.api.client()
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
setServerOrigin(instance.mainOrigin)
|
setServerOrigin(instance.mainOrigin)
|
||||||
|
|
||||||
if (instance.mainOrigin.startsWith("https")) {
|
if (instance.mainOrigin.startsWith("https")) {
|
||||||
setSecureConnection(true)
|
setSecureConnection(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
checkServerVersion()
|
checkServerVersion()
|
||||||
checkServerOrigin()
|
checkServerOrigin()
|
||||||
|
|
||||||
setCapacitorInfo()
|
setCapacitorInfo()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className="about_app">
|
return (
|
||||||
<div className="header">
|
<div className="about_app">
|
||||||
<div className="branding">
|
<div className="header">
|
||||||
<div className="logo">
|
<div className="branding">
|
||||||
<img
|
<div className="logo">
|
||||||
src={config.logo.alt}
|
<img src={config.logo.alt} alt="Logo" />
|
||||||
alt="Logo"
|
</div>
|
||||||
/>
|
<div className="texts">
|
||||||
</div>
|
<div className="sitename-text">
|
||||||
<div className="texts">
|
<h2>{config.app.siteName}</h2>
|
||||||
<div className="sitename-text">
|
<antd.Tag>Beta</antd.Tag>
|
||||||
<h2>{config.app.siteName}</h2>
|
</div>
|
||||||
<antd.Tag>Beta</antd.Tag>
|
<span>{config.author}</span>
|
||||||
</div>
|
<span>
|
||||||
<span>{config.author}</span>
|
Licensed with{" "}
|
||||||
<span>Licensed with {config.package?.license ?? "unlicensed"} </span>
|
{config.package?.license ?? "unlicensed"}{" "}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="versions">
|
</div>
|
||||||
<antd.Tag><Icons.FiTag />v{window.app.version ?? "experimental"}</antd.Tag>
|
<div className="versions">
|
||||||
<antd.Tag color={isProduction ? "green" : "magenta"}>
|
<antd.Tag>
|
||||||
{isProduction ? <Icons.FiCheckCircle /> : <Icons.FiTriangle />}
|
<Icons.FiTag />v
|
||||||
{String(import.meta.env.MODE)}
|
{window.app.version ?? "experimental"}
|
||||||
</antd.Tag>
|
</antd.Tag>
|
||||||
</div>
|
<antd.Tag color={isProduction ? "green" : "magenta"}>
|
||||||
</div>
|
{isProduction ? (
|
||||||
|
<Icons.FiCheckCircle />
|
||||||
|
) : (
|
||||||
|
<Icons.FiTriangle />
|
||||||
|
)}
|
||||||
|
{String(import.meta.env.MODE)}
|
||||||
|
</antd.Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<div className="group_header">
|
<div className="group_header">
|
||||||
<h3><Icons.FiInfo />Server information</h3>
|
<h3>
|
||||||
</div>
|
<Icons.FiInfo />
|
||||||
|
Server information
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<h3><Icons.MdOutlineStream /> Origin</h3>
|
<h3>
|
||||||
|
<Icons.MdOutlineStream /> Origin
|
||||||
|
</h3>
|
||||||
|
|
||||||
<antd.Tooltip
|
<antd.Tooltip
|
||||||
title={secureConnection ? connectionsTooltipStrings.secure : connectionsTooltipStrings.insecure}
|
title={
|
||||||
>
|
secureConnection
|
||||||
<antd.Tag
|
? connectionsTooltipStrings.secure
|
||||||
color={secureConnection ? "green" : "red"}
|
: connectionsTooltipStrings.insecure
|
||||||
icon={secureConnection ? <Icons.MdHttps /> : <Icons.MdWarning />}
|
}
|
||||||
>
|
>
|
||||||
{
|
<antd.Tag
|
||||||
secureConnection ? " Secure connection" : "Insecure connection"
|
color={secureConnection ? "green" : "red"}
|
||||||
}
|
icon={
|
||||||
</antd.Tag>
|
secureConnection ? (
|
||||||
</antd.Tooltip>
|
<Icons.MdHttps />
|
||||||
</div>
|
) : (
|
||||||
|
<Icons.MdWarning />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{secureConnection
|
||||||
|
? " Secure connection"
|
||||||
|
: "Insecure connection"}
|
||||||
|
</antd.Tag>
|
||||||
|
</antd.Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
{serverOrigin ?? "Unknown"}
|
{serverOrigin ?? "Unknown"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<h3><Icons.MdOutlineMemory /> Instance Performance</h3>
|
<h3>
|
||||||
</div>
|
<Icons.MdOutlineMemory /> Instance Performance
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.5rem",
|
gap: "0.5rem",
|
||||||
fontSize: "1.4rem",
|
fontSize: "1.4rem",
|
||||||
justifyContent: "space-evenly",
|
justifyContent: "space-evenly",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LatencyIndicator
|
<LatencyIndicator type="http" />
|
||||||
type="http"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LatencyIndicator
|
<LatencyIndicator type="ws" />
|
||||||
type="ws"
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdBuild />
|
<Icons.MdBuild />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Version</p>
|
<p>Version</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
{serverManifest?.version ?? "Unknown"}
|
{serverManifest?.version ?? "Unknown"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<h3>Thanks to our sponsors</h3>
|
<h3>Thanks to our sponsors</h3>
|
||||||
<SponsorsList />
|
<SponsorsList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Platform</p>
|
<p>Platform</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">Web App</div>
|
||||||
{Capacitor.platform}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>React</p>
|
<p>React</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
{React.version ?? "Unknown"}
|
{React.version ?? "Unknown"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Engine</p>
|
<p>Engine</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
{app.__version ?? "Unknown"}
|
{app.__version ?? "Unknown"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Comty.js</p>
|
<p>Comty.js</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
{__comty_shared_state.version ?? "Unknown"}
|
{__comty_shared_state.version ?? "Unknown"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{capInfo && (
|
||||||
capInfo && <div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>App ID</p>
|
<p>App ID</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">{capInfo.id}</div>
|
||||||
{capInfo.id}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{capInfo && (
|
||||||
capInfo && <div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>App Build</p>
|
<p>App Build</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">{capInfo.build}</div>
|
||||||
{capInfo.build}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{capInfo && (
|
||||||
capInfo && <div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>App Version</p>
|
<p>App Version</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">{capInfo.version}</div>
|
||||||
{capInfo.version}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="inline_field">
|
<div className="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>View Open Source Licenses</p>
|
<p>View Open Source Licenses</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
<antd.Button
|
<antd.Button
|
||||||
icon={<Icons.MdOpenInNew />}
|
icon={<Icons.MdOpenInNew />}
|
||||||
onClick={() => app.location.push("/licenses")}
|
onClick={() => app.location.push("/licenses")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
@ -11,70 +11,46 @@ const oneYearInSeconds = 60 * 60 * 24 * 365
|
|||||||
const sslDirPath = path.join(__dirname, ".ssl")
|
const sslDirPath = path.join(__dirname, ".ssl")
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
plugins: [
|
plugins: [react()],
|
||||||
react(),
|
resolve: {
|
||||||
],
|
alias: aliases,
|
||||||
resolve: {
|
},
|
||||||
alias: aliases,
|
server: {
|
||||||
},
|
host: "0.0.0.0",
|
||||||
server: {
|
port: 8000,
|
||||||
host: "0.0.0.0",
|
fs: {
|
||||||
port: 8000,
|
allow: ["..", "../../"],
|
||||||
fs: {
|
},
|
||||||
allow: ["..", "../../"],
|
headers: {
|
||||||
},
|
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`,
|
||||||
headers: {
|
},
|
||||||
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`
|
proxy: {
|
||||||
},
|
"/api": {
|
||||||
proxy: {
|
target: backendUri,
|
||||||
"/api": {
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
target: backendUri,
|
hostRewrite: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
changeOrigin: true,
|
||||||
hostRewrite: true,
|
xfwd: true,
|
||||||
changeOrigin: true,
|
ws: true,
|
||||||
xfwd: true,
|
toProxy: true,
|
||||||
ws: true,
|
secure: false,
|
||||||
toProxy: true,
|
},
|
||||||
secure: false,
|
},
|
||||||
}
|
},
|
||||||
}
|
css: {
|
||||||
},
|
preprocessorOptions: {
|
||||||
css: {
|
less: {
|
||||||
preprocessorOptions: {
|
javascriptEnabled: true,
|
||||||
less: {
|
},
|
||||||
javascriptEnabled: true,
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
optimizeDeps: {
|
|
||||||
esbuildOptions: {
|
|
||||||
target: "esnext"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
target: "esnext",
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks(id) {
|
|
||||||
if (id.includes("node_modules")) {
|
|
||||||
return id.toString().split("node_modules/")[1].split("/")[0].toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
esbuild: {
|
|
||||||
supported: {
|
|
||||||
"top-level-await": true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(sslDirPath)) {
|
if (fs.existsSync(sslDirPath)) {
|
||||||
config.server.https = {
|
config.server.https = {
|
||||||
key: path.join(__dirname, ".ssl", "privkey.pem"),
|
key: path.join(__dirname, ".ssl", "privkey.pem"),
|
||||||
cert: path.join(__dirname, ".ssl", "cert.pem"),
|
cert: path.join(__dirname, ".ssl", "cert.pem"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig(config)
|
export default defineConfig(config)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user