mirror of
https://github.com/ragestudio/comty.git
synced 2025-07-11 10:14:16 +00:00
merge from local
This commit is contained in:
parent
f871dd3c83
commit
93638f0fa3
packages
app
config/context-menu
package.jsonpublic
src
App.jsxpatches.jsrouter.jsx
components
cores
contextMenu
notifications
player
remoteStorage
style
windows
hooks
layout.jsxlayouts
components
bottomBar
draggableDrawer
drawer
floatingStack
header
modals
quickNav
sidebar
sidedrawer
toolsBar
topBar
default
minimal
pages
account
lyrics
messages
music
post
settings/apparence
styles
utils
server
db_models
gateway
lib/handleWsAuth
package.jsonservices
chats
files/routes/upload/chunk
music/classes
41
packages/app/config/context-menu/default/index.js
Normal file
41
packages/app/config/context-menu/default/index.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
export default {
|
||||||
|
"default-context": (items) => {
|
||||||
|
const text = window.getSelection().toString()
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
items.push({
|
||||||
|
label: "Copy",
|
||||||
|
icon: "Copy",
|
||||||
|
action: (clickedItem, ctx) => {
|
||||||
|
copyToClipboard(text)
|
||||||
|
|
||||||
|
ctx.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: "Paste",
|
||||||
|
icon: "Clipboard",
|
||||||
|
action: (clickedItem, ctx) => {
|
||||||
|
app.message.error("This action is not supported by your browser")
|
||||||
|
|
||||||
|
ctx.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: "Report a bug",
|
||||||
|
icon: "AlertTriangle",
|
||||||
|
action: (clickedItem, ctx) => {
|
||||||
|
app.eventBus.emit("app.reportBug", {
|
||||||
|
clickedItem,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
@ -1,119 +0,0 @@
|
|||||||
import download from "@utils/download"
|
|
||||||
import copyToClipboard from "@utils/copyToClipboard"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"default-context": () => {
|
|
||||||
const items = []
|
|
||||||
|
|
||||||
const text = window.getSelection().toString()
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
items.push({
|
|
||||||
label: "Copy",
|
|
||||||
icon: "Copy",
|
|
||||||
action: (clickedItem, ctx) => {
|
|
||||||
copyToClipboard(text)
|
|
||||||
|
|
||||||
ctx.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
label: "Paste",
|
|
||||||
icon: "Clipboard",
|
|
||||||
action: (clickedItem, ctx) => {
|
|
||||||
app.message.error("This action is not supported by your browser")
|
|
||||||
|
|
||||||
ctx.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
label: "Report a bug",
|
|
||||||
icon: "AlertTriangle",
|
|
||||||
action: (clickedItem, ctx) => {
|
|
||||||
app.eventBus.emit("app.reportBug", {
|
|
||||||
clickedItem,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return items
|
|
||||||
},
|
|
||||||
"postCard-context": (parent, element, control) => {
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
label: "Copy ID",
|
|
||||||
icon: "Copy",
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(parent.id)
|
|
||||||
control.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Copy Link",
|
|
||||||
icon: "Link",
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(`${window.location.origin}/post/${parent.id}`)
|
|
||||||
control.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
let media = null
|
|
||||||
|
|
||||||
// if element div has `addition` class, search inside it for video or image
|
|
||||||
if (element.classList.contains("addition") || element.classList.contains("image-wrapper")) {
|
|
||||||
media = element.querySelector("video, img")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if element div has `plyr__poster` class, search outside it for video or image
|
|
||||||
if (element.classList.contains("plyr__poster")) {
|
|
||||||
console.log(element.parentElement)
|
|
||||||
media = element.parentElement.querySelector("video")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if media is found, and is a video, search for the source
|
|
||||||
if (media && media.tagName === "VIDEO") {
|
|
||||||
media = media.querySelector("source")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media) {
|
|
||||||
items.push({
|
|
||||||
type: "separator"
|
|
||||||
})
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
label: "Copy media URL",
|
|
||||||
icon: "Copy",
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(media.src)
|
|
||||||
control.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
label: "Open media in new tab",
|
|
||||||
icon: "ExternalLink",
|
|
||||||
action: () => {
|
|
||||||
window.open(media.src, "_blank")
|
|
||||||
control.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
label: "Download media",
|
|
||||||
icon: "Download",
|
|
||||||
action: () => {
|
|
||||||
download(media.src)
|
|
||||||
control.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
}
|
|
70
packages/app/config/context-menu/post/index.js
Normal file
70
packages/app/config/context-menu/post/index.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import copyToClipboard from "@utils/copyToClipboard"
|
||||||
|
import download from "@utils/download"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
"post-card": (items, parent, element, control) => {
|
||||||
|
items.push({
|
||||||
|
label: "Copy ID",
|
||||||
|
icon: "Copy",
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(parent.id)
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: "Copy Link",
|
||||||
|
icon: "Link",
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(`${window.location.origin}/post/${parent.id}`)
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let media = null
|
||||||
|
|
||||||
|
if (parent.querySelector(".attachment")) {
|
||||||
|
media = parent.querySelector(".attachment")
|
||||||
|
media = media.querySelector("video, img")
|
||||||
|
|
||||||
|
if (media.querySelector("source")) {
|
||||||
|
media = media.querySelector("source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media) {
|
||||||
|
items.push({
|
||||||
|
type: "separator",
|
||||||
|
})
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: "Copy media URL",
|
||||||
|
icon: "Copy",
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(media.src)
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: "Open media in new tab",
|
||||||
|
icon: "ExternalLink",
|
||||||
|
action: () => {
|
||||||
|
window.open(media.src, "_blank")
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: "Download media",
|
||||||
|
icon: "Download",
|
||||||
|
action: () => {
|
||||||
|
download(media.src)
|
||||||
|
control.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
@ -40,11 +40,12 @@
|
|||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
"@ragestudio/cordova-nfc": "^1.2.0",
|
"@ragestudio/cordova-nfc": "^1.2.0",
|
||||||
"@sentry/browser": "^7.64.0",
|
"@sentry/browser": "^7.64.0",
|
||||||
|
"@tanstack/react-virtual": "^3.5.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.6.4",
|
"antd": "^5.17.0",
|
||||||
"antd-mobile": "^5.31.0",
|
"antd-mobile": "^5.31.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.6.8",
|
||||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"capacitor-music-controls-plugin-v3": "^1.1.0",
|
"capacitor-music-controls-plugin-v3": "^1.1.0",
|
||||||
@ -65,7 +66,7 @@
|
|||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"lru-cache": "^10.0.0",
|
"lru-cache": "^10.0.0",
|
||||||
"luxon": "^3.0.4",
|
"luxon": "^3.0.4",
|
||||||
"million": "^2.5.4-beta.1",
|
"million": "^2.6.4",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
"mpegts.js": "^1.6.10",
|
"mpegts.js": "^1.6.10",
|
||||||
@ -95,61 +96,15 @@
|
|||||||
"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",
|
||||||
"ua-parser-js": "^1.0.36"
|
"ua-parser-js": "^1.0.36",
|
||||||
|
"vite": "^5.2.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^2.0.4",
|
"@capacitor/assets": "^2.0.4",
|
||||||
"@electron-forge/cli": "^6.0.0-beta.66",
|
|
||||||
"@electron-forge/maker-deb": "^6.0.0-beta.66",
|
|
||||||
"@electron-forge/maker-rpm": "^6.0.0-beta.66",
|
|
||||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.66",
|
|
||||||
"@electron-forge/maker-zip": "^6.0.0-beta.66",
|
|
||||||
"@electron-forge/plugin-vite": "^6.4.2",
|
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
|
||||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
|
||||||
"@types/jest": "^26.0.24",
|
|
||||||
"@types/node": "^16.4.10",
|
|
||||||
"@types/react": "^17.0.15",
|
|
||||||
"@types/react-dom": "^17.0.9",
|
|
||||||
"@types/react-router-config": "^5.0.3",
|
|
||||||
"@types/react-router-dom": "^5.1.8",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
|
||||||
"concurrently": "^7.4.0",
|
"concurrently": "^7.4.0",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^21.0.1",
|
|
||||||
"electron-builder": "^24.6.4",
|
|
||||||
"electron-is": "^3.0.0",
|
|
||||||
"electron-log": "^4.4.8",
|
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"forge": {
|
|
||||||
"packagerConfig": {},
|
|
||||||
"makers": [
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-squirrel",
|
|
||||||
"config": {
|
|
||||||
"name": "comty"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-zip",
|
|
||||||
"platforms": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-deb",
|
|
||||||
"config": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@electron-forge/maker-rpm",
|
|
||||||
"config": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
42
packages/app/public/dev-logo_alt.svg
Normal file
42
packages/app/public/dev-logo_alt.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="COMTY" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66.35 72">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #4b6fb5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-1, .cls-2, .cls-3, .cls-4 {
|
||||||
|
stroke-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #6f8cc4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #b7c5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #93a9d3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="ISO-COMPOSED">
|
||||||
|
<path id="C-BODY" class="cls-1" d="M64.43,19.59s-7.75,4.48-11.22,6.48c-.89.51-2.02.25-2.62-.59-4.54-6.28-13.01-9.53-21.82-6.06-4.07,1.6-7.38,4.73-9.1,8.75-5.63,13.16,3.87,25.83,16.34,25.83,4.31,0,8.26-1.51,11.36-4.04.81-.65,1.97-.62,2.71.11l9.92,9.93c.82.81.78,2.15-.08,2.92-9.71,8.63-24.13,12.09-38.92,6.04C9.38,64.21,0,49.7,0,37.14V2C0,.9.9,0,2.01,0h34c12.56,0,23.6,6.44,30.04,16.19.63.97.33,2.27-1.62,3.4Z"/>
|
||||||
|
<g id="C-MASK" class="cls-5">
|
||||||
|
<path class="cls-3" d="M18,0v18H0V2C0,.9.9,0,2.01,0h15.99Z"/>
|
||||||
|
<rect class="cls-4" x="18" y=".01" width="18" height="18"/>
|
||||||
|
<path class="cls-2" d="M54,4.84C48.77,1.81,42.71.05,36.24.01h-.24v18h18V4.84Z"/>
|
||||||
|
<rect class="cls-4" y="17.97" width="18" height="18"/>
|
||||||
|
<path class="cls-2" d="M18,35.97H0v1.18C0,42.71,1.85,48.65,4.91,53.97h13.09v-18Z"/>
|
||||||
|
<path class="cls-2" d="M21.23,25.16c.96-1.57,2.28-2.9,3.85-3.87,3.71-2.3,7.43-3.26,10.92-3.26v-.05h-18v18h.02c0-3.46.97-7.15,3.21-10.81Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path class="cls-1" d="M40.45,23.38c-10.75-3.5-20.54,6.29-17.04,17.04,1.25,3.84,4.3,6.88,8.13,8.13,10.75,3.5,20.54-6.29,17.04-17.04-1.25-3.84-4.3-6.88-8.13-8.13ZM29.64,42.33h0c-.95-.95-.95-2.48,0-3.42l9.31-9.31c.95-.95,2.48-.95,3.42,0h0c.95.95.95,2.48,0,3.42l-9.31,9.31c-.95.95-2.48.95-3.42,0Z"/>
|
||||||
|
</svg>
|
After (image error) Size: 1.7 KiB |
72
packages/app/public/dev-logo_full.svg
Normal file
72
packages/app/public/dev-logo_full.svg
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 27.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="COMTY" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 292.9 72" style="enable-background:new 0 0 292.9 72;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#4B6FB5;}
|
||||||
|
.st1{display:none;}
|
||||||
|
.st2{display:inline;fill:#FF6064;}
|
||||||
|
.st3{display:inline;fill:#FF8083;}
|
||||||
|
.st4{display:inline;fill:#FFA0A2;}
|
||||||
|
.st5{display:inline;fill:#FFBFC1;}
|
||||||
|
.st6{display:inline;}
|
||||||
|
.st7{opacity:0.65;fill:#FF7C8C;}
|
||||||
|
.st8{opacity:0.65;fill:#FF4971;}
|
||||||
|
.st9{opacity:0.65;fill:#FF4457;}
|
||||||
|
.st10{opacity:0.5;}
|
||||||
|
.st11{fill:#B7C5E1;}
|
||||||
|
.st12{fill:#93A9D3;}
|
||||||
|
.st13{fill:#6F8CC4;}
|
||||||
|
</style>
|
||||||
|
<path id="Y-BODY" class="st0" d="M262,72h-13.6c-1.1,0-2-0.9-2-2v-9.5c0-1.1,0.9-2,2-2h5.8c0.7,0,1.4-0.4,1.7-1l5.3-9.2
|
||||||
|
c0.4-0.6,0.4-1.4,0-2L246.7,21c-0.8-1.3,0.2-3,1.7-3h11c0.7,0,1.4,0.4,1.7,1l6.8,11.8c0.8,1.3,2.7,1.3,3.5,0l6.8-11.8
|
||||||
|
c0.4-0.6,1-1,1.7-1h11c1.5,0,2.5,1.7,1.7,3l-28.9,50C263.4,71.6,262.7,72,262,72z"/>
|
||||||
|
<path id="T-BODY" class="st0" d="M240,18h-7.2c-1.1,0-2-0.9-2-2V2c0-1.1-0.9-2-2-2h-9.5c-1.1,0-2,0.9-2,2v14c0,1.1-0.9,2-2,2H208
|
||||||
|
c-1.1,0-2,0.9-2,2v9.5c0,1.1,0.9,2,2,2h7.2c1.1,0,2,0.9,2,2V70c0,1.1,0.9,2,2,2h9.5c1.1,0,2-0.9,2-2V33.5c0-1.1,0.9-2,2-2h7.2
|
||||||
|
c1.1,0,2-0.9,2-2V20C242,18.9,241.1,18,240,18z"/>
|
||||||
|
<path id="M-BODY" class="st0" d="M201.5,40.5L201.5,40.5c0-12.4-10.1-22.5-22.5-22.5c-5.5,0-10.6,2-14.5,5.3c-0.7,0.6-1.8,0.6-2.6,0
|
||||||
|
c-3.9-3.3-9-5.3-14.5-5.3c-2.8,0-5.5,0.5-8,1.5c-0.5,0.2-1.1-0.2-1.1-0.7v0c0-0.4-0.3-0.8-0.8-0.8h-10.7c-1.1,0-2,0.9-2,2v50
|
||||||
|
c0,1.1,0.9,2,2,2h9.1h0.4h0.1c1.1,0,2-0.9,2-2V40.5c0-5,4-9,9-9c4.9,0,8.9,4,9,8.9V70c0,1.1,0.9,2,2,2h0.1h0.4h8.7h0.4h0
|
||||||
|
c1.1,0,2-0.9,2-2V40.5h0c0-5,4-9,9-9c4.9,0,9,4,9,8.9V70c0,1.1,0.9,2,2,2h0.1h0.4h9c1.1,0,2-0.9,2-2V40.5z"/>
|
||||||
|
<path id="O-BODY" class="st0" d="M93.4,18c-14.9,0-27,12.1-27,27s12.1,27,27,27s27-12.1,27-27S108.3,18,93.4,18z M93.4,58.5
|
||||||
|
c-7.5,0-13.5-6-13.5-13.5c0-7.5,6-13.5,13.5-13.5c7.5,0,13.5,6,13.5,13.5C106.9,52.4,100.8,58.5,93.4,58.5z"/>
|
||||||
|
<g id="Colors" class="st1">
|
||||||
|
<rect x="0.1" y="-27.5" class="st2" width="22.5" height="22.5"/>
|
||||||
|
<rect x="22.5" y="-27.5" class="st3" width="22.5" height="22.5"/>
|
||||||
|
<rect x="45" y="-27.5" class="st4" width="22.5" height="22.5"/>
|
||||||
|
<rect x="67.5" y="-27.5" class="st5" width="22.5" height="22.5"/>
|
||||||
|
</g>
|
||||||
|
<g id="old_x5F_iso" class="st1">
|
||||||
|
<path id="C_00000029753336008474061920000012106504942734039743_" class="st2" d="M129.4-23c-12.5,0-22-12.7-16.3-25.8
|
||||||
|
c1.7-4,5-7.1,9.1-8.7c8.8-3.5,17.3-0.2,21.8,6.1c0.6,0.8,1.7,1.1,2.6,0.6l11.2-6.5c1.9-1.1,2.2-2.4,1.6-3.4
|
||||||
|
c-6.4-9.7-17.5-16.2-30-16.2h-34c-1.1,0-2,0.9-2,2l0,35.1c0,12.6,9.4,27.1,21,31.8c14.8,6,29.2,2.6,38.9-6c0.9-0.8,0.9-2.1,0.1-2.9
|
||||||
|
l-9.9-9.9c-0.7-0.7-1.9-0.8-2.7-0.1C137.6-24.5,133.7-23,129.4-23z"/>
|
||||||
|
<g id="_x2D__00000003068151388543984340000009125836902913508246_" class="st6">
|
||||||
|
<path class="st7" d="M111.4-76.9H95.3c-1.1,0-1.9,0.9-1.9,2v16h18V-76.9z"/>
|
||||||
|
<rect x="111.4" y="-76.9" class="st8" width="18" height="18"/>
|
||||||
|
<path class="st9" d="M147.3-72.1c-5.2-3-11.3-4.8-17.8-4.8h-0.2v18h16c1.1,0,2-0.9,2-2V-72.1z"/>
|
||||||
|
<rect x="93.4" y="-59" class="st8" width="18" height="18"/>
|
||||||
|
<path class="st9" d="M111.4-41h-18v1.2c0,5.6,1.8,11.5,4.9,16.8h11.1c1.1,0,2-0.9,2-2V-41z"/>
|
||||||
|
<path class="st9" d="M113-48.8c1.7-4,5-7.1,9.1-8.7c2.5-1,4.9-1.4,7.2-1.4V-59h-18v18h0C111.4-43.5,111.9-46.2,113-48.8z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g id="ISO-COMPOSED_00000069363719166014630790000000862934890392767654_">
|
||||||
|
<path id="C-BODY_00000127745992915324767630000016197953627436916150_" class="st0" d="M64.4,19.6c0,0-7.8,4.5-11.2,6.5
|
||||||
|
c-0.9,0.5-2,0.3-2.6-0.6c-4.5-6.3-13-9.5-21.8-6.1c-4.1,1.6-7.4,4.7-9.1,8.8C14,41.3,23.5,54,36,54c4.3,0,8.3-1.5,11.4-4
|
||||||
|
c0.8-0.6,2-0.6,2.7,0.1L60,60c0.8,0.8,0.8,2.1-0.1,2.9C50.2,71.6,35.8,75,21,69C9.4,64.2,0,49.7,0,37.1V2c0-1.1,0.9-2,2-2h34
|
||||||
|
c12.6,0,23.6,6.4,30,16.2C66.7,17.2,66.4,18.5,64.4,19.6z"/>
|
||||||
|
<g id="C-MASK_00000045611387297072415010000003497810413074703294_" class="st10">
|
||||||
|
<path class="st11" d="M18,0v18H0V2c0-1.1,0.9-2,2-2H18z"/>
|
||||||
|
<rect x="18" y="0" class="st12" width="18" height="18"/>
|
||||||
|
<path class="st13" d="M54,4.8c-5.2-3-11.3-4.8-17.8-4.8H36v18h18V4.8z"/>
|
||||||
|
<rect y="18" class="st12" width="18" height="18"/>
|
||||||
|
<path class="st13" d="M18,36H0v1.2C0,42.7,1.8,48.7,4.9,54H18V36z"/>
|
||||||
|
<path class="st13" d="M21.2,25.2c1-1.6,2.3-2.9,3.8-3.9C28.8,19,32.5,18,36,18V18H18v18h0C18,32.5,19,28.8,21.2,25.2z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path class="st0" d="M40.5,23.4c-10.7-3.5-20.5,6.3-17,17c1.2,3.8,4.3,6.9,8.1,8.1c10.7,3.5,20.5-6.3,17-17
|
||||||
|
C47.3,27.7,44.3,24.6,40.5,23.4z M29.6,42.3L29.6,42.3c-0.9-0.9-0.9-2.5,0-3.4l9.3-9.3c0.9-0.9,2.5-0.9,3.4,0l0,0
|
||||||
|
c0.9,0.9,0.9,2.5,0,3.4l-9.3,9.3C32.1,43.3,30.6,43.3,29.6,42.3z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After (image error) Size: 4.6 KiB |
@ -28,7 +28,6 @@ import {
|
|||||||
NotificationsCenter,
|
NotificationsCenter,
|
||||||
PostCreator,
|
PostCreator,
|
||||||
} from "@components"
|
} from "@components"
|
||||||
import { DOMWindow } from "@components/RenderWindow"
|
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import DesktopTopBar from "@components/DesktopTopBar"
|
import DesktopTopBar from "@components/DesktopTopBar"
|
||||||
|
|
||||||
@ -40,7 +39,9 @@ import Splash from "./splash"
|
|||||||
|
|
||||||
import "@styles/index.less"
|
import "@styles/index.less"
|
||||||
|
|
||||||
CapacitorUpdater.notifyAppReady()
|
if (IS_MOBILE_HOST) {
|
||||||
|
CapacitorUpdater.notifyAppReady()
|
||||||
|
}
|
||||||
|
|
||||||
class ComtyApp extends React.Component {
|
class ComtyApp extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -52,7 +53,6 @@ class ComtyApp extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
desktopMode: false,
|
|
||||||
session: null,
|
session: null,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ class ComtyApp extends React.Component {
|
|||||||
window.app.version = config.package.version
|
window.app.version = config.package.version
|
||||||
window.app.confirm = antd.Modal.confirm
|
window.app.confirm = antd.Modal.confirm
|
||||||
window.app.message = antd.message
|
window.app.message = antd.message
|
||||||
window.app.isCapacitor = window.navigator.userAgent === "capacitor"
|
window.app.isCapacitor = IS_MOBILE_HOST
|
||||||
|
|
||||||
if (window.app.version !== window.localStorage.getItem("last_version")) {
|
if (window.app.version !== window.localStorage.getItem("last_version")) {
|
||||||
app.message.info(`Comty has been updated to version ${window.app.version}!`)
|
app.message.info(`Comty has been updated to version ${window.app.version}!`)
|
||||||
@ -157,16 +157,14 @@ class ComtyApp extends React.Component {
|
|||||||
framed: false
|
framed: false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
openMessages: () => {
|
||||||
|
app.location.push("/messages")
|
||||||
|
},
|
||||||
openFullImageViewer: (src) => {
|
openFullImageViewer: (src) => {
|
||||||
const win = new DOMWindow({
|
app.cores.window_mng.render("image_lightbox", <Lightbox
|
||||||
id: "fullImageViewer",
|
|
||||||
className: "fullImageViewer",
|
|
||||||
})
|
|
||||||
|
|
||||||
win.render(<Lightbox
|
|
||||||
small={src}
|
small={src}
|
||||||
large={src}
|
large={src}
|
||||||
onClose={() => win.remove()}
|
onClose={() => app.cores.window_mng.close("image_lightbox")}
|
||||||
hideDownload
|
hideDownload
|
||||||
showRotate
|
showRotate
|
||||||
/>)
|
/>)
|
||||||
@ -298,17 +296,6 @@ class ComtyApp extends React.Component {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
"app.no_session": async () => {
|
|
||||||
const location = window.location.pathname
|
|
||||||
|
|
||||||
if (location !== "/auth" && location !== "/register") {
|
|
||||||
antd.notification.info({
|
|
||||||
message: "You are not logged in, to use some features you will need to log in.",
|
|
||||||
btn: <antd.Button type="primary" onClick={() => app.controls.openLoginForm()}>Login</antd.Button>,
|
|
||||||
duration: 15,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"session.invalid": async (error) => {
|
"session.invalid": async (error) => {
|
||||||
const token = await SessionModel.token
|
const token = await SessionModel.token
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import * as lib3 from "react-icons/md"
|
|||||||
import * as lib4 from "react-icons/io"
|
import * as lib4 from "react-icons/io"
|
||||||
import * as lib5 from "react-icons/si"
|
import * as lib5 from "react-icons/si"
|
||||||
import * as lib6 from "react-icons/fa"
|
import * as lib6 from "react-icons/fa"
|
||||||
|
import * as lib7 from "react-icons/tb"
|
||||||
|
|
||||||
const marginedStyle = {
|
const marginedStyle = {
|
||||||
width: "1em",
|
width: "1em",
|
||||||
@ -42,6 +43,7 @@ export const Icons = {
|
|||||||
...lib4,
|
...lib4,
|
||||||
...lib5,
|
...lib5,
|
||||||
...lib6,
|
...lib6,
|
||||||
|
...lib7,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createIconRender(icon, props) {
|
export function createIconRender(icon, props) {
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
export { default as TopBar } from "./topBar"
|
|
||||||
export { default as BottomBar } from "./bottomBar"
|
|
||||||
export { default as Drawer } from "./drawer"
|
|
||||||
export { default as Sidebar } from "./sidebar"
|
|
||||||
export { default as Sidedrawer } from "./sidedrawer"
|
|
||||||
export { default as Modal } from "./modal"
|
|
||||||
export { default as Header } from "./header"
|
|
||||||
export { default as ToolsBar } from "./toolsBar"
|
|
@ -1,79 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import { Modal, Button } from "antd"
|
|
||||||
import { Icons } from "@components/Icons"
|
|
||||||
|
|
||||||
import "./index.less"
|
|
||||||
|
|
||||||
// TODO: Implement translucid mode
|
|
||||||
// TODO: Implement close on click mask (and Should remove the close button)
|
|
||||||
|
|
||||||
export default class AppModal extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.controller = app.ModalController = {
|
|
||||||
open: this.open,
|
|
||||||
close: this.close,
|
|
||||||
modalRef: this.modalRef,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
currentRender: null,
|
|
||||||
renderParams: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
modalRef = React.createRef()
|
|
||||||
|
|
||||||
open = (render, params = {}) => {
|
|
||||||
this.setState({
|
|
||||||
currentRender: render,
|
|
||||||
renderParams: params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
close = () => {
|
|
||||||
this.setState({
|
|
||||||
currentRender: null,
|
|
||||||
renderParams: {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModalClose = () => {
|
|
||||||
this.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
renderModal = () => {
|
|
||||||
return <div className="appModalWrapper">
|
|
||||||
<Button
|
|
||||||
icon={<Icons.X />}
|
|
||||||
className="closeButton"
|
|
||||||
onClick={this.handleModalClose}
|
|
||||||
shape="circle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="appModal" ref={this.modalRef}>
|
|
||||||
{
|
|
||||||
React.createElement(this.state.currentRender, {
|
|
||||||
...this.state.renderParams.props ?? {},
|
|
||||||
close: this.close,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Modal
|
|
||||||
open={this.state.currentRender}
|
|
||||||
maskClosable={this.state.renderParams.maskClosable ?? true}
|
|
||||||
modalRender={this.renderModal}
|
|
||||||
maskStyle={{
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
backdropFilter: "blur(5px)"
|
|
||||||
}}
|
|
||||||
destroyOnClose
|
|
||||||
centered
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
.appModalWrapper {
|
|
||||||
.closeButton {
|
|
||||||
background-color: var(--background-color-primary);
|
|
||||||
|
|
||||||
pointer-events: all;
|
|
||||||
|
|
||||||
font-size: 1.2rem;
|
|
||||||
|
|
||||||
transform: translate(-25px, -10px);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin: 0 !important;
|
|
||||||
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--background-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.appModal {
|
|
||||||
pointer-events: all;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
|
|
||||||
//min-height: 500px;
|
|
||||||
min-width: 600px;
|
|
||||||
|
|
||||||
padding: 30px;
|
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,8 +18,6 @@ export default React.forwardRef((props, ref) => {
|
|||||||
const insideViewportCb = (entries) => {
|
const insideViewportCb = (entries) => {
|
||||||
const { fetching, onBottom } = props
|
const { fetching, onBottom } = props
|
||||||
|
|
||||||
console.log("entries", entries)
|
|
||||||
|
|
||||||
entries.forEach(element => {
|
entries.forEach(element => {
|
||||||
if (element.intersectionRatio > 0 && !fetching) {
|
if (element.intersectionRatio > 0 && !fetching) {
|
||||||
onBottom()
|
onBottom()
|
||||||
@ -50,14 +48,14 @@ export default React.forwardRef((props, ref) => {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<lb style={{ clear: "both" }} />
|
<div style={{ clear: "both" }} />
|
||||||
|
|
||||||
<lb
|
<div
|
||||||
id="bottom"
|
id="bottom"
|
||||||
className="bottom"
|
className="bottom"
|
||||||
style={{ display: hasMore ? "block" : "none" }}
|
style={{ display: hasMore ? "block" : "none" }}
|
||||||
>
|
>
|
||||||
{loadingComponent && React.createElement(loadingComponent)}
|
{loadingComponent && React.createElement(loadingComponent)}
|
||||||
</lb>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
@ -21,19 +21,19 @@ import MusicModel from "@models/music"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const PlaylistTypeDecorators = {
|
const PlaylistTypeDecorators = {
|
||||||
"single": (props) => <span className="playlistType">
|
"single": () => <span className="playlistType">
|
||||||
<Icons.MdMusicNote />
|
<Icons.MdMusicNote />
|
||||||
Single
|
Single
|
||||||
</span>,
|
</span>,
|
||||||
"album": (props) => <span className="playlistType">
|
"album": () => <span className="playlistType">
|
||||||
<Icons.MdAlbum />
|
<Icons.MdAlbum />
|
||||||
Album
|
Album
|
||||||
</span>,
|
</span>,
|
||||||
"ep": (props) => <span className="playlistType">
|
"ep": () => <span className="playlistType">
|
||||||
<Icons.MdAlbum />
|
<Icons.MdAlbum />
|
||||||
EP
|
EP
|
||||||
</span>,
|
</span>,
|
||||||
"mix": (props) => <span className="playlistType">
|
"mix": () => <span className="playlistType">
|
||||||
<Icons.MdMusicNote />
|
<Icons.MdMusicNote />
|
||||||
Mix
|
Mix
|
||||||
</span>,
|
</span>,
|
||||||
@ -228,12 +228,14 @@ export default (props) => {
|
|||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playlistType = playlist.type?.toLowerCase() ?? "playlist"
|
||||||
|
|
||||||
return <PlaylistContext.Provider value={contextValues}>
|
return <PlaylistContext.Provider value={contextValues}>
|
||||||
<WithPlayerContext>
|
<WithPlayerContext>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"playlist_view",
|
"playlist_view",
|
||||||
props.type ?? playlist.type,
|
playlistType,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
||||||
@ -257,9 +259,9 @@ export default (props) => {
|
|||||||
|
|
||||||
<div className="play_info_statistics">
|
<div className="play_info_statistics">
|
||||||
{
|
{
|
||||||
playlist.type && PlaylistTypeDecorators[playlist.type] && <div className="play_info_statistics_item">
|
playlistType && PlaylistTypeDecorators[playlistType] && <div className="play_info_statistics_item">
|
||||||
{
|
{
|
||||||
PlaylistTypeDecorators[playlist.type]()
|
PlaylistTypeDecorators[playlistType]()
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,10 @@ export class PagePanelWithNavMenu extends React.Component {
|
|||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.tabs.length === 0) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
// slip the active tab by splitting on "."
|
// slip the active tab by splitting on "."
|
||||||
if (!this.state.activeTab) {
|
if (!this.state.activeTab) {
|
||||||
console.error("PagePanelWithNavMenu: activeTab is not defined")
|
console.error("PagePanelWithNavMenu: activeTab is not defined")
|
||||||
|
@ -64,7 +64,7 @@ export default (props) => {
|
|||||||
onClickReply,
|
onClickReply,
|
||||||
} = props.actions ?? {}
|
} = props.actions ?? {}
|
||||||
|
|
||||||
const genItems = () => {
|
const generateMoreMenuItems = () => {
|
||||||
let items = MoreActionsItems
|
let items = MoreActionsItems
|
||||||
|
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
@ -104,7 +104,7 @@ export default (props) => {
|
|||||||
<div className="action" id="more">
|
<div className="action" id="more">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: genItems(),
|
items: generateMoreMenuItems(),
|
||||||
onClick: handleDropdownClickItem,
|
onClick: handleDropdownClickItem,
|
||||||
}}
|
}}
|
||||||
trigger={["click"]}
|
trigger={["click"]}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
background-color: black;
|
background-color: var(--background-color-primary);
|
||||||
|
|
||||||
.bear-react-carousel__pagination-group {
|
.bear-react-carousel__pagination-group {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -12,6 +12,14 @@ import PostAttachments from "./components/attachments"
|
|||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
const articleAnimationProps = {
|
||||||
|
layout: true,
|
||||||
|
initial: { y: -100, opacity: 0 },
|
||||||
|
animate: { y: 0, opacity: 1, },
|
||||||
|
exit: { scale: 0, opacity: 0 },
|
||||||
|
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,
|
||||||
@ -164,26 +172,21 @@ export default class PostCard extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <motion.div
|
return <motion.article
|
||||||
initial={{ y: -100, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1, }}
|
|
||||||
exit={{ scale: 0, opacity: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.1,
|
|
||||||
}}
|
|
||||||
layout
|
|
||||||
key={this.props.index}
|
|
||||||
id={this.state.data._id}
|
|
||||||
post_id={this.state.data._id}
|
|
||||||
style={this.props.style}
|
|
||||||
user-id={this.state.data.user_id}
|
|
||||||
context-menu={"postCard-context"}
|
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"post_card",
|
"post_card",
|
||||||
{
|
{
|
||||||
["open"]: this.state.open,
|
["open"]: this.state.open,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
id={this.state.data._id}
|
||||||
|
style={this.props.style}
|
||||||
|
{...articleAnimationProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="post_card_content"
|
||||||
|
context-menu={"post-card"}
|
||||||
|
user-id={this.state.data.user_id}
|
||||||
>
|
>
|
||||||
<PostHeader
|
<PostHeader
|
||||||
postData={this.state.data}
|
postData={this.state.data}
|
||||||
@ -237,6 +240,8 @@ export default class PostCard extends React.PureComponent {
|
|||||||
<span>View {this.state.hasReplies} replies</span>
|
<span>View {this.state.hasReplies} replies</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
|
</motion.article>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -33,6 +33,13 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post_card_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.post_content {
|
.post_content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -1,233 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import ReactDOM from "react-dom"
|
|
||||||
import { Rnd } from "react-rnd"
|
|
||||||
import { Icons } from "@components/Icons"
|
|
||||||
|
|
||||||
import "./index.less"
|
|
||||||
|
|
||||||
class DOMWindow {
|
|
||||||
constructor(props = {}) {
|
|
||||||
this.props = props
|
|
||||||
|
|
||||||
this.id = this.props.id
|
|
||||||
this.key = 0
|
|
||||||
|
|
||||||
this.currentRender = null
|
|
||||||
this.root = document.getElementById("app_windows")
|
|
||||||
this.element = document.createElement("div")
|
|
||||||
|
|
||||||
this.element.setAttribute("id", this.id)
|
|
||||||
this.element.setAttribute("key", this.key)
|
|
||||||
this.element.setAttribute("classname", this.props.className)
|
|
||||||
|
|
||||||
// if props clickOutsideToClose is true, add event listener to close window
|
|
||||||
if (this.props.clickOutsideToClose) {
|
|
||||||
document.addEventListener("click", this.handleWrapperClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle root container
|
|
||||||
if (!this.root) {
|
|
||||||
this.root = document.createElement("div")
|
|
||||||
this.root.setAttribute("id", "app_windows")
|
|
||||||
|
|
||||||
document.body.append(this.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all windows opened has container
|
|
||||||
const rootNodes = this.root.childNodes
|
|
||||||
|
|
||||||
// ensure this window has last key from rootNode
|
|
||||||
if (rootNodes.length > 0) {
|
|
||||||
const lastChild = rootNodes[rootNodes.length - 1]
|
|
||||||
const lastChildKey = Number(lastChild.getAttribute("key"))
|
|
||||||
|
|
||||||
this.key = lastChildKey + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWrapperClick = (event) => {
|
|
||||||
if (!this.currentRender) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if click in not renderer fragment, close window
|
|
||||||
if (!this.element.contains(event.target)) {
|
|
||||||
this.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render = (fragment) => {
|
|
||||||
this.root.appendChild(this.element)
|
|
||||||
|
|
||||||
this.currentRender = fragment
|
|
||||||
|
|
||||||
return ReactDOM.render(fragment, this.element)
|
|
||||||
}
|
|
||||||
|
|
||||||
remove = () => {
|
|
||||||
this.root.removeChild(this.element)
|
|
||||||
this.currentRender = null
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy = () => {
|
|
||||||
this.element.remove()
|
|
||||||
this.currentRender = null
|
|
||||||
}
|
|
||||||
|
|
||||||
createDefaultWindow = (children, props) => {
|
|
||||||
return this.render(<DefaultWindowRender {...this.props} {...props} id={this.id} key={this.key} destroy={this.destroy} >
|
|
||||||
{children}
|
|
||||||
</DefaultWindowRender>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DefaultWindowRender extends React.Component {
|
|
||||||
state = {
|
|
||||||
actions: [],
|
|
||||||
dimensions: {
|
|
||||||
height: this.props.height ?? 600,
|
|
||||||
width: this.props.width ?? 400,
|
|
||||||
},
|
|
||||||
position: this.props.defaultPosition,
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
this.setDefaultActions()
|
|
||||||
|
|
||||||
if (typeof this.props.actions !== "undefined") {
|
|
||||||
if (Array.isArray(this.props.actions)) {
|
|
||||||
const actions = this.state.actions ?? []
|
|
||||||
|
|
||||||
this.props.actions.forEach((action) => {
|
|
||||||
actions.push(action)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({ actions })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.position) {
|
|
||||||
this.setState({ position: this.getCenterPosition() })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleVisibility(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleVisibility = (to) => {
|
|
||||||
this.setState({ visible: to ?? !this.state.visible })
|
|
||||||
}
|
|
||||||
|
|
||||||
getCenterPosition = () => {
|
|
||||||
const dimensions = this.state?.dimensions ?? {}
|
|
||||||
|
|
||||||
const windowHeight = dimensions.height ?? 600
|
|
||||||
const windowWidth = dimensions.width ?? 400
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: window.innerWidth / 2 - windowWidth / 2,
|
|
||||||
y: window.innerHeight / 2 - windowHeight / 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDefaultActions = () => {
|
|
||||||
const { actions } = this.state
|
|
||||||
|
|
||||||
actions.push({
|
|
||||||
key: "close",
|
|
||||||
render: () => <Icons.XCircle style={{ margin: 0, padding: 0 }} />,
|
|
||||||
onClick: () => {
|
|
||||||
this.props.destroy()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({ actions })
|
|
||||||
}
|
|
||||||
|
|
||||||
renderActions = () => {
|
|
||||||
const actions = this.state.actions
|
|
||||||
|
|
||||||
if (Array.isArray(actions)) {
|
|
||||||
return actions.map((action) => {
|
|
||||||
return (
|
|
||||||
<div key={action.key} onClick={action.onClick} {...action.props}>
|
|
||||||
{React.isValidElement(action.render) ? action.render : React.createElement(action.render)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
getComponentRender = () => {
|
|
||||||
return React.isValidElement(this.props.children)
|
|
||||||
? React.cloneElement(this.props.children, this.props.renderProps)
|
|
||||||
: React.createElement(this.props.children, this.props.renderProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { position, dimensions, visible } = this.state
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Rnd
|
|
||||||
default={{
|
|
||||||
...position,
|
|
||||||
...dimensions,
|
|
||||||
}}
|
|
||||||
onResize={(e, direction, ref, delta, position) => {
|
|
||||||
this.setState({
|
|
||||||
dimensions: {
|
|
||||||
width: ref.offsetWidth,
|
|
||||||
height: ref.offsetHeight,
|
|
||||||
},
|
|
||||||
position,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
minWidth={
|
|
||||||
this.props.minWidth
|
|
||||||
}
|
|
||||||
minHeight={
|
|
||||||
this.props.minHeight
|
|
||||||
}
|
|
||||||
enableResizing={
|
|
||||||
this.props.enableResizing ?? true
|
|
||||||
}
|
|
||||||
disableDragging={
|
|
||||||
this.props.disableDragging ?? false
|
|
||||||
}
|
|
||||||
dragHandleClassName={
|
|
||||||
this.props.dragHandleClassName ?? "window_topbar"
|
|
||||||
}
|
|
||||||
bounds={
|
|
||||||
this.props.bounds ?? "#root"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
this.props.useWrapper
|
|
||||||
? <div
|
|
||||||
style={{
|
|
||||||
height: dimensions.height,
|
|
||||||
width: dimensions.width,
|
|
||||||
}}
|
|
||||||
className="window_wrapper"
|
|
||||||
>
|
|
||||||
<div className="window_topbar">
|
|
||||||
<div className="title">{this.props.id}</div>
|
|
||||||
<div className="actions">{this.renderActions()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="window_body">{this.getComponentRender()}</div>
|
|
||||||
</div>
|
|
||||||
: this.props.children
|
|
||||||
}
|
|
||||||
</Rnd>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { DOMWindow, DefaultWindowRender }
|
|
@ -1,93 +0,0 @@
|
|||||||
@wrapper_background: rgba(255, 255, 255, 1);
|
|
||||||
|
|
||||||
@topbar_height: 30px;
|
|
||||||
@topbar_background: rgba(0, 0, 0, 0.4);
|
|
||||||
|
|
||||||
.window_wrapper {
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
background-color: @wrapper_background;
|
|
||||||
border: 1px solid rgba(161, 133, 133, 0.2);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.translucid {
|
|
||||||
border: unset;
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
--webkit-backdrop-filter: blur(10px);
|
|
||||||
filter: drop-shadow(8px 8px 10px rgba(0, 0, 0, 0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.window_topbar {
|
|
||||||
position: sticky;
|
|
||||||
z-index: 51;
|
|
||||||
background-color: @topbar_background;
|
|
||||||
|
|
||||||
height: @topbar_height;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin: 0 5px;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-left: 20px;
|
|
||||||
|
|
||||||
color: #fff - @topbar_background;
|
|
||||||
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: italic;
|
|
||||||
font-family: "JetBrains Mono", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: end;
|
|
||||||
|
|
||||||
color: #fff - @topbar_background;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
height: fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
line-height: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
> div:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.window_body {
|
|
||||||
z-index: 50;
|
|
||||||
|
|
||||||
padding: 10px 20px;
|
|
||||||
height: calc(100% - @topbar_height);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
overflow: overlay;
|
|
||||||
user-select: text !important;
|
|
||||||
--webkit-user-select: text !important;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
user-select: text !important;
|
|
||||||
--webkit-user-select: text !important;
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,12 +5,11 @@ import { Icons } from "@components/Icons"
|
|||||||
import WidgetItemPreview from "@components/WidgetItemPreview"
|
import WidgetItemPreview from "@components/WidgetItemPreview"
|
||||||
|
|
||||||
import useRequest from "comty.js/hooks/useRequest"
|
import useRequest from "comty.js/hooks/useRequest"
|
||||||
import WidgetModel from "comty.js/models/widget"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export const WidgetBrowser = (props) => {
|
export const WidgetBrowser = (props) => {
|
||||||
const [L_Widgets, R_Widgets, E_Widgets, M_Widgets] = useRequest(WidgetModel.browse)
|
const [L_Widgets, R_Widgets, E_Widgets, M_Widgets] = []
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = React.useState("")
|
const [searchValue, setSearchValue] = React.useState("")
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import * as Layout from "./Layout"
|
|
||||||
export { default as Footer } from "./Footer"
|
export { default as Footer } from "./Footer"
|
||||||
|
|
||||||
export { default as RenderError } from "./RenderError"
|
export { default as RenderError } from "./RenderError"
|
||||||
@ -41,8 +40,3 @@ export { default as UserBadges } from "./UserBadges"
|
|||||||
export { default as UserCard } from "./UserCard"
|
export { default as UserCard } from "./UserCard"
|
||||||
export { default as FollowersList } from "./FollowersList"
|
export { default as FollowersList } from "./FollowersList"
|
||||||
export { default as UserPreview } from "./UserPreview"
|
export { default as UserPreview } from "./UserPreview"
|
||||||
|
|
||||||
// OTHERS
|
|
||||||
export * as Window from "./RenderWindow"
|
|
||||||
|
|
||||||
export { Layout }
|
|
@ -1,5 +1,5 @@
|
|||||||
.contextMenu {
|
.contextMenu {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 100000;
|
z-index: 100000;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -8,7 +8,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
width: 200px;
|
width: 230px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
background-color: var(--background-color-primary);
|
background-color: var(--background-color-primary);
|
||||||
@ -16,11 +16,11 @@
|
|||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 7px;
|
||||||
|
|
||||||
font-family: "Recursive", sans-serif;
|
font-weight: 600;
|
||||||
|
font-family: var(--fontFamily);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -42,7 +42,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
padding: 10px 10px 10px 20px;
|
padding: 10px 10px 10px 15px;
|
||||||
|
|
||||||
transition: all 50ms ease-in-out;
|
transition: all 50ms ease-in-out;
|
||||||
|
|
||||||
@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
padding-left: 25px;
|
padding-left: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import Core from "evite/src/core"
|
import Core from "evite/src/core"
|
||||||
|
|
||||||
import { DOMWindow } from "@components/RenderWindow"
|
|
||||||
import ContextMenu from "./components/contextMenu"
|
import ContextMenu from "./components/contextMenu"
|
||||||
|
|
||||||
import InternalContexts from "@config/context-menu"
|
import DefaultContenxt from "@config/context-menu/default"
|
||||||
|
import PostCardContext from "@config/context-menu/post"
|
||||||
|
|
||||||
export default class ContextMenuCore extends Core {
|
export default class ContextMenuCore extends Core {
|
||||||
static namespace = "contextMenu"
|
static namespace = "contextMenu"
|
||||||
@ -15,13 +15,10 @@ export default class ContextMenuCore extends Core {
|
|||||||
registerContext: this.registerContext.bind(this),
|
registerContext: this.registerContext.bind(this),
|
||||||
}
|
}
|
||||||
|
|
||||||
contexts = Object()
|
contexts = {
|
||||||
|
...DefaultContenxt,
|
||||||
DOMWindow = new DOMWindow({
|
...PostCardContext,
|
||||||
id: "contextMenu",
|
}
|
||||||
className: "contextMenuWrapper",
|
|
||||||
clickOutsideToClose: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
if (app.isMobile) {
|
if (app.isMobile) {
|
||||||
@ -65,27 +62,26 @@ export default class ContextMenuCore extends Core {
|
|||||||
contexts.push("default-context")
|
contexts.push("default-context")
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const context of contexts) {
|
for await (const [index, context] of contexts.entries()) {
|
||||||
let contextObject = this.contexts[context] || InternalContexts[context]
|
let contextObject = this.contexts[context]
|
||||||
|
|
||||||
|
if (!contextObject) {
|
||||||
|
this.console.warn(`Context ${context} not found`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof contextObject === "function") {
|
if (typeof contextObject === "function") {
|
||||||
contextObject = await contextObject(parentElement, element, {
|
contextObject = await contextObject(items, parentElement, element, {
|
||||||
close: this.hide,
|
close: this.hide,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// push divider
|
// push divider
|
||||||
if (items.length > 0) {
|
if (contexts.length > 0 && index !== contexts.length - 1) {
|
||||||
items.push({
|
items.push({
|
||||||
type: "separator"
|
type: "separator"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(contextObject)) {
|
|
||||||
items.push(...contextObject)
|
|
||||||
} else {
|
|
||||||
items.push(contextObject)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fullfill each item with a correspondent index if missing declared
|
// fullfill each item with a correspondent index if missing declared
|
||||||
@ -142,10 +138,13 @@ export default class ContextMenuCore extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show(payload) {
|
show(payload) {
|
||||||
this.DOMWindow.render(React.createElement(ContextMenu, payload))
|
app.cores.window_mng.render("context-menu", React.createElement(ContextMenu, payload), {
|
||||||
|
createOrUpdate: true,
|
||||||
|
closeOnClickOutside: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.DOMWindow.remove()
|
app.cores.window_mng.close("context-menu")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ class NotificationFeedback {
|
|||||||
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 (options = {}) => {
|
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()
|
||||||
}
|
}
|
||||||
@ -28,7 +28,11 @@ class NotificationFeedback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async feedback(type) {
|
static async feedback({ type = "notification", feedback = true }) {
|
||||||
|
if (!feedback) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
NotificationFeedback.playHaptic(type)
|
NotificationFeedback.playHaptic(type)
|
||||||
NotificationFeedback.playAudio(type)
|
NotificationFeedback.playAudio(type)
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,15 @@ export default class NotificationCore extends Core {
|
|||||||
|
|
||||||
public = {
|
public = {
|
||||||
new: this.new,
|
new: this.new,
|
||||||
|
close: this.close,
|
||||||
}
|
}
|
||||||
|
|
||||||
async new(notification, options = {}) {
|
async new(notification) {
|
||||||
NotificationUI.notify(notification, options)
|
NotificationUI.notify(notification)
|
||||||
NotificationFeedback.feedback(options.type)
|
NotificationFeedback.feedback(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(id) {
|
||||||
|
NotificationUI.close(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,12 +5,7 @@ import { notification as Notf, Space, Button } from "antd"
|
|||||||
import { Icons, createIconRender } from "@components/Icons"
|
import { Icons, createIconRender } from "@components/Icons"
|
||||||
|
|
||||||
class NotificationUI {
|
class NotificationUI {
|
||||||
static async notify(
|
static async notify(notification) {
|
||||||
notification,
|
|
||||||
options = {
|
|
||||||
type: "info"
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (typeof notification === "string") {
|
if (typeof notification === "string") {
|
||||||
notification = {
|
notification = {
|
||||||
title: "New notification",
|
title: "New notification",
|
||||||
@ -19,8 +14,8 @@ class NotificationUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notfObj = {
|
const notfObj = {
|
||||||
duration: options.duration ?? 4,
|
duration: typeof notification.duration === "undefined" ? 4 : notification.duration,
|
||||||
key: options.key ?? Date.now(),
|
key: notification.key ?? Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.title) {
|
if (notification.title) {
|
||||||
@ -73,11 +68,11 @@ class NotificationUI {
|
|||||||
notfObj.icon = React.isValidElement(notification.icon) ? notification.icon : (createIconRender(notification.icon) ?? <Icons.Bell />)
|
notfObj.icon = React.isValidElement(notification.icon) ? notification.icon : (createIconRender(notification.icon) ?? <Icons.Bell />)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(options.actions)) {
|
if (Array.isArray(notification.actions)) {
|
||||||
notfObj.btn = (
|
notfObj.btn = (
|
||||||
<Space>
|
<Space>
|
||||||
{
|
{
|
||||||
options.actions.map((action, index) => {
|
notification.actions.map((action, index) => {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (typeof action.onClick === "function") {
|
if (typeof action.onClick === "function") {
|
||||||
action.onClick()
|
action.onClick()
|
||||||
@ -101,11 +96,24 @@ class NotificationUI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof Notf[options.type] !== "function") {
|
if (typeof notification.closable) {
|
||||||
options.type = "info"
|
notfObj.closable = notification.closable
|
||||||
}
|
}
|
||||||
|
|
||||||
return Notf[options.type](notfObj)
|
if (notification.type === "loading") {
|
||||||
|
notification.type = "open"
|
||||||
|
notfObj.icon = <Icons.LoadingOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Notf[notification.type] !== "function") {
|
||||||
|
notification.type = "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notf[notification.type](notfObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
static close(key) {
|
||||||
|
Notf.destroy(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +110,8 @@ export default class Player extends Core {
|
|||||||
set: (target, prop, value) => {
|
set: (target, prop, value) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
gradualFadeMs: Player.gradualFadeMs,
|
||||||
}
|
}
|
||||||
|
|
||||||
internalEvents = {
|
internalEvents = {
|
||||||
@ -128,10 +129,6 @@ export default class Player extends Core {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
wsEvents = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
this.native_controls.initialize()
|
this.native_controls.initialize()
|
||||||
|
|
||||||
@ -695,6 +692,10 @@ export default class Player extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async resumePlayback() {
|
async resumePlayback() {
|
||||||
|
if (!this.state.playback_status === "playing") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
if (!this.track_instance) {
|
if (!this.track_instance) {
|
||||||
this.console.error("No audio instance")
|
this.console.error("No audio instance")
|
||||||
|
@ -32,8 +32,7 @@ export default class RemoteStorage extends Core {
|
|||||||
const fn = async () => new Promise((resolve, reject) => {
|
const fn = async () => new Promise((resolve, reject) => {
|
||||||
const uploader = new ChunkedUpload({
|
const uploader = new ChunkedUpload({
|
||||||
endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
|
endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
|
||||||
// TODO: get chunk size from settings
|
splitChunkSize: 5 * 1024 * 1024,
|
||||||
splitChunkSize: 5 * 1024 * 1024, // 5MB in bytes
|
|
||||||
file: file,
|
file: file,
|
||||||
service: service,
|
service: service,
|
||||||
})
|
})
|
||||||
|
@ -239,8 +239,6 @@ export default class StyleCore extends Core {
|
|||||||
app.eventBus.emit("style.update", {
|
app.eventBus.emit("style.update", {
|
||||||
...this.public.mutation,
|
...this.public.mutation,
|
||||||
})
|
})
|
||||||
|
|
||||||
ConfigProvider.config({ theme: this.public.mutation })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyVariant(variant = (this.public.theme.defaultVariant ?? "light")) {
|
applyVariant(variant = (this.public.theme.defaultVariant ?? "light")) {
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default React.createContext({
|
||||||
|
title: null,
|
||||||
|
close: () => { },
|
||||||
|
updatePosition: () => { },
|
||||||
|
updateDimensions: () => { },
|
||||||
|
updateTitle: () => { },
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
dimensions: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
})
|
@ -0,0 +1,196 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Rnd } from "react-rnd"
|
||||||
|
|
||||||
|
import { Icons } from "@components/Icons"
|
||||||
|
|
||||||
|
import WindowContext from "./context"
|
||||||
|
|
||||||
|
export default class DefaultWindowRender extends React.Component {
|
||||||
|
static contextType = WindowContext
|
||||||
|
|
||||||
|
ref = React.createRef()
|
||||||
|
|
||||||
|
state = {
|
||||||
|
renderError: false,
|
||||||
|
title: null,
|
||||||
|
actions: [],
|
||||||
|
dimensions: {
|
||||||
|
height: this.props.height ?? 600,
|
||||||
|
width: this.props.width ?? 400,
|
||||||
|
},
|
||||||
|
position: this.props.defaultPosition,
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.setDefaultActions()
|
||||||
|
|
||||||
|
if (typeof this.props.actions !== "undefined") {
|
||||||
|
if (Array.isArray(this.props.actions)) {
|
||||||
|
const actions = this.state.actions ?? []
|
||||||
|
|
||||||
|
this.props.actions.forEach((action) => {
|
||||||
|
actions.push(action)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({ actions })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.position) {
|
||||||
|
this.setState({ position: this.getCenterPosition() })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleVisibility(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch = (error) => {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
renderError: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTitle = (title) => {
|
||||||
|
this.setState({ title })
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDimensions = (dimensions = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}) => {
|
||||||
|
this.ref.current?.updateSize({
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition = (position = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}) => {
|
||||||
|
this.ref.current?.updatePosition({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleVisibility = (to) => {
|
||||||
|
this.setState({ visible: to ?? !this.state.visible })
|
||||||
|
}
|
||||||
|
|
||||||
|
getCenterPosition = () => {
|
||||||
|
const dimensions = this.state?.dimensions ?? {}
|
||||||
|
|
||||||
|
const windowHeight = dimensions.height ?? 600
|
||||||
|
const windowWidth = dimensions.width ?? 400
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: window.innerWidth / 2 - windowWidth / 2,
|
||||||
|
y: window.innerHeight / 2 - windowHeight / 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultActions = () => {
|
||||||
|
const { actions } = this.state
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
key: "close",
|
||||||
|
render: () => <Icons.XCircle style={{ margin: 0, padding: 0 }} />,
|
||||||
|
onClick: () => {
|
||||||
|
this.props.close()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({ actions })
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActions = () => {
|
||||||
|
const actions = this.state.actions
|
||||||
|
|
||||||
|
if (Array.isArray(actions)) {
|
||||||
|
return actions.map((action) => {
|
||||||
|
return (
|
||||||
|
<div key={action.key} onClick={action.onClick} {...action.props}>
|
||||||
|
{React.isValidElement(action.render) ? action.render : React.createElement(action.render)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentRender = () => {
|
||||||
|
const ctx = {
|
||||||
|
...this.props,
|
||||||
|
updateTitle: this.updateTitle,
|
||||||
|
updateDimensions: this.updateDimensions,
|
||||||
|
updatePosition: this.updatePosition,
|
||||||
|
close: this.props.close,
|
||||||
|
position: this.state.position,
|
||||||
|
dimensions: this.state.dimensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <WindowContext.Provider value={ctx}>
|
||||||
|
{
|
||||||
|
React.isValidElement(this.props.children)
|
||||||
|
? React.cloneElement(this.props.children, ctx)
|
||||||
|
: React.createElement(this.props.children, ctx)
|
||||||
|
}
|
||||||
|
</WindowContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { position, dimensions, visible } = this.state
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Rnd
|
||||||
|
ref={this.ref}
|
||||||
|
default={{
|
||||||
|
...position,
|
||||||
|
...dimensions,
|
||||||
|
}}
|
||||||
|
minWidth={
|
||||||
|
this.props.minWidth
|
||||||
|
}
|
||||||
|
minHeight={
|
||||||
|
this.props.minHeight
|
||||||
|
}
|
||||||
|
enableResizing={
|
||||||
|
this.props.enableResizing ?? true
|
||||||
|
}
|
||||||
|
disableDragging={
|
||||||
|
this.props.disableDragging ?? false
|
||||||
|
}
|
||||||
|
dragHandleClassName={
|
||||||
|
this.props.dragHandleClassName ?? "window_topbar"
|
||||||
|
}
|
||||||
|
bounds={
|
||||||
|
this.props.bounds ?? "#root"
|
||||||
|
}
|
||||||
|
className="window_wrapper"
|
||||||
|
>
|
||||||
|
<div className="window_topbar">
|
||||||
|
<div className="title">{this.state.title}</div>
|
||||||
|
<div className="actions">{this.renderActions()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window_body">
|
||||||
|
{
|
||||||
|
this.state.renderError && <div className="render_error">
|
||||||
|
<h1>Render Error</h1>
|
||||||
|
<code>{this.state.renderError.message}</code>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!this.state.renderError && this.getComponentRender()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Rnd>
|
||||||
|
}
|
||||||
|
}
|
115
packages/app/src/cores/windows/index.less
Normal file
115
packages/app/src/cores/windows/index.less
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
@topbar_height: 30px;
|
||||||
|
@topbar_background: var(--background-color-accent);
|
||||||
|
|
||||||
|
.window_wrapper {
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
background-color: var(--background-color-primary);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
|
||||||
|
//filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.5));
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window_topbar {
|
||||||
|
position: sticky;
|
||||||
|
|
||||||
|
z-index: 51;
|
||||||
|
|
||||||
|
background-color: @topbar_background;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
height: @topbar_height;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
margin: 0 5px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
>div {
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
margin-right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
|
width: fit-content;
|
||||||
|
line-height: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
>div:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.window_body {
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
padding: 10px 20px;
|
||||||
|
height: calc(100% - @topbar_height);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
overflow: overlay;
|
||||||
|
user-select: text !important;
|
||||||
|
--webkit-user-select: text !important;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
>div {
|
||||||
|
user-select: text !important;
|
||||||
|
--webkit-user-select: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.render_error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
151
packages/app/src/cores/windows/windows.core.jsx
Normal file
151
packages/app/src/cores/windows/windows.core.jsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import React from "react"
|
||||||
|
import Core from "evite/src/core"
|
||||||
|
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
|
||||||
|
import DefaultWindow from "./components/defaultWindow"
|
||||||
|
|
||||||
|
import DefaultWindowContext from "./components/defaultWindow/context"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class WindowManager extends Core {
|
||||||
|
static namespace = "window_mng"
|
||||||
|
|
||||||
|
static idMount = "windows"
|
||||||
|
|
||||||
|
root = null
|
||||||
|
|
||||||
|
windows = []
|
||||||
|
|
||||||
|
public = {
|
||||||
|
close: this.close.bind(this),
|
||||||
|
render: this.render.bind(this),
|
||||||
|
}
|
||||||
|
|
||||||
|
async onInitialize() {
|
||||||
|
this.root = document.createElement("div")
|
||||||
|
|
||||||
|
this.root.setAttribute("id", this.constructor.idMount)
|
||||||
|
|
||||||
|
document.body.append(this.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWrapperClick = (id, event) => {
|
||||||
|
const element = this.root.querySelector(`#${id}`)
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
if (!element.contains(event.target)) {
|
||||||
|
this.close(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element with the specified id and appends it to the root element.
|
||||||
|
* If the element already exists and createOrUpdate option is false, it will throw an error.
|
||||||
|
* If the element already exists and createOrUpdate option is true, it will update the element.
|
||||||
|
* If useFrame option is true, it wraps the fragment with a DefaultWindow component before rendering.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the element to create or update.
|
||||||
|
* @param {ReactElement} fragment - The React element to render inside the created element.
|
||||||
|
* @param {Object} options - The options for creating or updating the element.
|
||||||
|
* @param {boolean} options.useFrame - Specifies whether to wrap the fragment with a DefaultWindow component.
|
||||||
|
* @param {boolean} options.createOrUpdate - Specifies whether to create a new element or update an existing one.
|
||||||
|
* @return {HTMLElement} The created or updated element.
|
||||||
|
*/
|
||||||
|
render(
|
||||||
|
id,
|
||||||
|
fragment,
|
||||||
|
{
|
||||||
|
useFrame = false,
|
||||||
|
createOrUpdate = false,
|
||||||
|
closeOnClickOutside = false,
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
let element = document.createElement("div")
|
||||||
|
let node = null
|
||||||
|
let win = null
|
||||||
|
|
||||||
|
// check if window already exist
|
||||||
|
if (this.root.querySelector(`#${id}`) && !createOrUpdate) {
|
||||||
|
const newId = `${id}_${Date.now()}`
|
||||||
|
|
||||||
|
this.console.warn(`Window ${id} already exist, overwritting id to ${newId}.\nYou can use {createOrUpdate = true} option to force refresh render of window`)
|
||||||
|
|
||||||
|
id = newId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.root.querySelector(`#${id}`) && createOrUpdate) {
|
||||||
|
element = document.getElementById(id)
|
||||||
|
|
||||||
|
win = this.windows.find((_node) => {
|
||||||
|
return _node.id === id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (win) {
|
||||||
|
node = win.node
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.setAttribute("id", id)
|
||||||
|
|
||||||
|
this.root.appendChild(element)
|
||||||
|
|
||||||
|
node = createRoot(element)
|
||||||
|
|
||||||
|
win = {
|
||||||
|
id: id,
|
||||||
|
node: node,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windows.push(win)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useFrame) {
|
||||||
|
fragment = <DefaultWindow>
|
||||||
|
{fragment}
|
||||||
|
</DefaultWindow>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeOnClickOutside) {
|
||||||
|
document.addEventListener("click", (e) => this.handleWrapperClick(id, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
node.render(React.cloneElement(fragment, {
|
||||||
|
close: () => {
|
||||||
|
this.close(id)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the window with the given ID.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the window to be closed.
|
||||||
|
* @return {boolean} Returns true if the window was successfully closed, false otherwise.
|
||||||
|
*/
|
||||||
|
close(id) {
|
||||||
|
const element = document.getElementById(id)
|
||||||
|
|
||||||
|
const win = this.windows.find((node) => {
|
||||||
|
return node.id === id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
this.console.warn(`Window ${id} not found`)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
win.node.unmount()
|
||||||
|
this.root.removeChild(element)
|
||||||
|
|
||||||
|
this.windows = this.windows.filter((node) => {
|
||||||
|
return node.id !== id
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
98
packages/app/src/hooks/useChat/index.js
Normal file
98
packages/app/src/hooks/useChat/index.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default (to_user_id) => {
|
||||||
|
const [socket, setSocket] = React.useState(null)
|
||||||
|
const [messages, setMessages] = React.useState([])
|
||||||
|
const [scroller, setScroller] = React.useState(null)
|
||||||
|
|
||||||
|
const [isLocalTyping, setIsLocalTyping] = React.useState(false)
|
||||||
|
const [isRemoteTyping, setIsRemoteTyping] = React.useState(false)
|
||||||
|
|
||||||
|
const [timeoutOffTypingEvent, setTimeoutOffTypingEvent] = React.useState(null)
|
||||||
|
|
||||||
|
async function sendMessage(message) {
|
||||||
|
emitTypingEvent(false)
|
||||||
|
|
||||||
|
await socket.emit("chat:send:message", {
|
||||||
|
to_user_id: to_user_id,
|
||||||
|
content: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function emitTypingEvent(to) {
|
||||||
|
if (isLocalTyping === true && to === true) {
|
||||||
|
return debouncedOffTypingEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
await socket.emit("chat:state:typing", {
|
||||||
|
to_user_id: to_user_id,
|
||||||
|
is_typing: to,
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsLocalTyping(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function debouncedOffTypingEvent() {
|
||||||
|
if (timeoutOffTypingEvent) {
|
||||||
|
clearTimeout(timeoutOffTypingEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeoutOffTypingEvent(setTimeout(() => {
|
||||||
|
emitTypingEvent(false)
|
||||||
|
}, 5000))
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenEvents = {
|
||||||
|
"chat:receive:message": (message) => {
|
||||||
|
setMessages((messages) => {
|
||||||
|
return [
|
||||||
|
...messages,
|
||||||
|
message
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"chat:state:typing": (state) => {
|
||||||
|
setIsRemoteTyping(state.is_typing)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (scroller?.current) {
|
||||||
|
const paddingBottom = scroller.current.style.paddingBottom.replace("px", "")
|
||||||
|
|
||||||
|
scroller.current?.scrollTo({
|
||||||
|
top: scroller.current.scrollHeight + paddingBottom,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const targetSocket = app.cores.api.client().sockets.chats
|
||||||
|
|
||||||
|
setSocket(targetSocket)
|
||||||
|
|
||||||
|
for (const [event, handler] of Object.entries(listenEvents)) {
|
||||||
|
targetSocket.on(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const [event, handler] of Object.entries(listenEvents)) {
|
||||||
|
targetSocket.off(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutOffTypingEvent) {
|
||||||
|
clearTimeout(timeoutOffTypingEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendMessage,
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
setScroller,
|
||||||
|
emitTypingEvent,
|
||||||
|
isRemoteTyping,
|
||||||
|
}
|
||||||
|
}
|
70
packages/app/src/hooks/useTextRoom/index.jsx
Normal file
70
packages/app/src/hooks/useTextRoom/index.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { EventEmitter } from "@foxify/events"
|
||||||
|
|
||||||
|
function useTextRoom(route, options = {
|
||||||
|
persistent: false,
|
||||||
|
}) {
|
||||||
|
const eventEmitter = new EventEmitter()
|
||||||
|
|
||||||
|
const [lines, setLines] = React.useState([])
|
||||||
|
|
||||||
|
const socket = app.cores.api.client().sockets.chats
|
||||||
|
|
||||||
|
function pushToLines(line) {
|
||||||
|
setLines((lines) => {
|
||||||
|
return [
|
||||||
|
...lines,
|
||||||
|
line,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteLine(message) {
|
||||||
|
setLines((lines) => {
|
||||||
|
return lines.filter((line) => line._id !== message._id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(payload) {
|
||||||
|
socket.emit("room:send:message", {
|
||||||
|
...payload,
|
||||||
|
route: route,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketEvents = {
|
||||||
|
"room:message": (message) => {
|
||||||
|
eventEmitter.emit("room:message", message)
|
||||||
|
pushToLines(message)
|
||||||
|
},
|
||||||
|
"room:delete:message": (message) => {
|
||||||
|
eventEmitter.emit("room:delete:message", message)
|
||||||
|
deleteLine(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
socket.emit("join:room", {
|
||||||
|
...options,
|
||||||
|
room: route,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [event, handler] of Object.entries(socketEvents)) {
|
||||||
|
socket.on(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.emit("leave:room", {
|
||||||
|
room: route,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [event, handler] of Object.entries(socketEvents)) {
|
||||||
|
socket.off(event, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return [send, lines, setLines, eventEmitter]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTextRoom
|
@ -44,14 +44,7 @@ export default class Layout extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transitionLayer.classList.remove("fade-opacity-leave")
|
transitionLayer.classList.remove("fade-opacity-leave")
|
||||||
},
|
}
|
||||||
"router.navigate": async (path, options) => {
|
|
||||||
this.progressBar.start()
|
|
||||||
|
|
||||||
await this.makePageTransition(options)
|
|
||||||
|
|
||||||
this.progressBar.done()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -82,33 +75,6 @@ export default class Layout extends React.PureComponent {
|
|||||||
this.setState({ renderError: { info, stack } })
|
this.setState({ renderError: { info, stack } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async makePageTransition(options = {}) {
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
return document.startViewTransition(async () => {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, options.state?.transitionDelay ?? 250)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const content_layout = document.getElementById("content_layout")
|
|
||||||
|
|
||||||
if (!content_layout) {
|
|
||||||
console.warn("content_layout not found, no animation will be played")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
content_layout.classList.add("fade-transverse-leave")
|
|
||||||
|
|
||||||
return await new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve()
|
|
||||||
content_layout.classList.remove("fade-transverse-leave")
|
|
||||||
}, options.state?.transitionDelay ?? 250)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutInterface = window.app.layout = {
|
layoutInterface = window.app.layout = {
|
||||||
set: (layout) => {
|
set: (layout) => {
|
||||||
if (typeof Layouts[layout] !== "function") {
|
if (typeof Layouts[layout] !== "function") {
|
||||||
|
@ -8,7 +8,7 @@ import { Icons, createIconRender } from "@components/Icons"
|
|||||||
|
|
||||||
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
import { QuickNavMenuItems, QuickNavMenu } from "@components/Layout/quickNav"
|
import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/quickNav"
|
||||||
|
|
||||||
import PlayerView from "@pages/@mobile-views/player"
|
import PlayerView from "@pages/@mobile-views/player"
|
||||||
import CreatorView from "@pages/@mobile-views/creator"
|
import CreatorView from "@pages/@mobile-views/creator"
|
@ -1,132 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import classnames from "classnames"
|
|
||||||
|
|
||||||
import { DOMWindow } from "@components/RenderWindow"
|
|
||||||
|
|
||||||
import "./index.less"
|
|
||||||
|
|
||||||
class FloatingStackItem extends React.PureComponent {
|
|
||||||
state = {
|
|
||||||
renderError: null
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error, info) {
|
|
||||||
console.log(error, info)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
renderError: error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.renderError) {
|
|
||||||
return <div className="floating_stack_item">
|
|
||||||
<h1>Render Error</h1>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="floating_stack_item" key={this.props.id} id={this.props.id}>
|
|
||||||
<React.Fragment>
|
|
||||||
{this.props.children}
|
|
||||||
</React.Fragment>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class FloatingStack extends React.Component {
|
|
||||||
state = {
|
|
||||||
renders: [],
|
|
||||||
globalVisibility: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
public = {
|
|
||||||
add: (id, render) => {
|
|
||||||
try {
|
|
||||||
if (!id) {
|
|
||||||
console.error(`FloatingStack: id is required`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!render) {
|
|
||||||
console.error(`FloatingStack: render is required`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.renders.find((item) => item.id === id)) {
|
|
||||||
console.error(`FloatingStack: id ${id} already exists`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
renders: [
|
|
||||||
...this.state.renders,
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
render: React.createElement(render),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
return render
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remove: (id) => {
|
|
||||||
this.setState({
|
|
||||||
renders: this.state.renders.filter((item) => {
|
|
||||||
return item.id !== id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
toggleGlobalVisibility: (to) => {
|
|
||||||
if (typeof to !== "boolean") {
|
|
||||||
to = !this.state.globalVisibility
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
globalVisibility: to,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.app.layout.floatingStack = this.public
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.app.layout.floatingStack = null
|
|
||||||
delete window.app.layout.floatingStack
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
className={classnames(
|
|
||||||
"floating_stack",
|
|
||||||
{
|
|
||||||
["hidden"]: !this.state.globalVisibility,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
this.state.renders.map((item) => {
|
|
||||||
return <FloatingStackItem id={item.id}>
|
|
||||||
{item.render}
|
|
||||||
</FloatingStackItem>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createWithDom = () => {
|
|
||||||
const dom = new DOMWindow({
|
|
||||||
id: "FloatingStack",
|
|
||||||
})
|
|
||||||
|
|
||||||
dom.render(<FloatingStack />)
|
|
||||||
|
|
||||||
return dom
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
.floating_stack {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 300;
|
|
||||||
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
max-width: 450px;
|
|
||||||
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
margin: 20px;
|
|
||||||
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating_stack_item {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
@ -12,7 +12,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.page_header {
|
.page_header {
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
margin: 10px 0 20px 0;
|
margin: 10px 0 20px 0;
|
||||||
|
|
||||||
padding: 5px;
|
padding: 5px;
|
@ -3,7 +3,6 @@ import { Modal as AntdModal } from "antd"
|
|||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
|
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import { DOMWindow } from "@components/RenderWindow"
|
|
||||||
|
|
||||||
import useLayoutInterface from "@hooks/useLayoutInterface"
|
import useLayoutInterface from "@hooks/useLayoutInterface"
|
||||||
|
|
||||||
@ -118,8 +117,6 @@ export default () => {
|
|||||||
render,
|
render,
|
||||||
{
|
{
|
||||||
framed = true,
|
framed = true,
|
||||||
frameContentStyle = null,
|
|
||||||
includeCloseButton = false,
|
|
||||||
|
|
||||||
confirmOnOutsideClick = false,
|
confirmOnOutsideClick = false,
|
||||||
confirmOnClickTitle,
|
confirmOnClickTitle,
|
||||||
@ -129,20 +126,13 @@ export default () => {
|
|||||||
props,
|
props,
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
const win = new DOMWindow({
|
app.cores.window_mng.render(id, <Modal
|
||||||
id: id,
|
|
||||||
className: className,
|
|
||||||
})
|
|
||||||
|
|
||||||
win.render(<Modal
|
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
win={win}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
win.destroy()
|
app.cores.window_mng.close(id)
|
||||||
}}
|
}}
|
||||||
includeCloseButton={includeCloseButton}
|
|
||||||
framed={framed}
|
framed={framed}
|
||||||
frameContentStyle={frameContentStyle}
|
className={className}
|
||||||
confirmOnOutsideClick={confirmOnOutsideClick}
|
confirmOnOutsideClick={confirmOnOutsideClick}
|
||||||
confirmOnClickTitle={confirmOnClickTitle}
|
confirmOnClickTitle={confirmOnClickTitle}
|
||||||
confirmOnClickContent={confirmOnClickContent}
|
confirmOnClickContent={confirmOnClickContent}
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: rgba(var(--bg_color_6), 0.5);
|
background-color: rgba(var(--bg_color_6), 0.1);
|
||||||
|
|
||||||
backdrop-filter: blur(@modal_background_blur);
|
backdrop-filter: blur(@modal_background_blur);
|
||||||
-webkit-backdrop-filter: blur(@modal_background_blur);
|
-webkit-backdrop-filter: blur(@modal_background_blur);
|
||||||
|
@ -31,6 +31,9 @@ const onClickHandlers = {
|
|||||||
search: () => {
|
search: () => {
|
||||||
window.app.controls.openSearcher()
|
window.app.controls.openSearcher()
|
||||||
},
|
},
|
||||||
|
messages: () => {
|
||||||
|
window.app.controls.openMessages()
|
||||||
|
},
|
||||||
create: () => {
|
create: () => {
|
||||||
window.app.controls.openCreator()
|
window.app.controls.openCreator()
|
||||||
},
|
},
|
||||||
@ -63,6 +66,37 @@ const generateTopItems = (extra = []) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BottomMenuDefaultItems = [
|
||||||
|
{
|
||||||
|
key: "search",
|
||||||
|
label: <Translation>
|
||||||
|
{(t) => t("Search")}
|
||||||
|
</Translation>,
|
||||||
|
icon: <Icons.Search />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "messages",
|
||||||
|
label: <Translation>
|
||||||
|
{(t) => t("Messages")}
|
||||||
|
</Translation>,
|
||||||
|
icon: <Icons.MessageCircle />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "notifications",
|
||||||
|
label: <Translation>
|
||||||
|
{(t) => t("Notifications")}
|
||||||
|
</Translation>,
|
||||||
|
icon: <Icons.Bell />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "settings",
|
||||||
|
label: <Translation>
|
||||||
|
{(t) => t("Settings")}
|
||||||
|
</Translation>,
|
||||||
|
icon: <Icons.Settings />,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const ActionMenuItems = [
|
const ActionMenuItems = [
|
||||||
{
|
{
|
||||||
key: "account",
|
key: "account",
|
||||||
@ -268,7 +302,7 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
if (e.item.props.ignoreClick) {
|
if (e.item.props.ignore_click === "true") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,11 +365,48 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBottomItems = () => {
|
||||||
|
const items = [
|
||||||
|
...BottomMenuDefaultItems,
|
||||||
|
...this.state.bottomItems,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (app.userData) {
|
||||||
|
items.push({
|
||||||
|
key: "account",
|
||||||
|
ignore_click: "true",
|
||||||
|
className: "user_avatar",
|
||||||
|
label: <Dropdown
|
||||||
|
menu={{
|
||||||
|
items: ActionMenuItems,
|
||||||
|
onClick: this.onClickDropdownItem
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
placement="top"
|
||||||
|
trigger={["click"]}
|
||||||
|
>
|
||||||
|
<Avatar shape="square" src={app.userData?.avatar} />
|
||||||
|
</Dropdown>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app.userData) {
|
||||||
|
items.push({
|
||||||
|
key: "login",
|
||||||
|
label: <Translation>
|
||||||
|
{t => t("Login")}
|
||||||
|
</Translation>,
|
||||||
|
icon: <Icons.LogIn />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const defaultSelectedKey = window.location.pathname.replace("/", "")
|
const defaultSelectedKey = window.location.pathname.replace("/", "")
|
||||||
|
|
||||||
return <>
|
return <Motion style={{
|
||||||
<Motion style={{
|
|
||||||
x: spring(!this.state.visible ? 100 : 0),
|
x: spring(!this.state.visible ? 100 : 0),
|
||||||
}}>
|
}}>
|
||||||
{({ x }) => {
|
{({ x }) => {
|
||||||
@ -398,77 +469,13 @@ export default class Sidebar extends React.Component {
|
|||||||
selectable={false}
|
selectable={false}
|
||||||
mode="inline"
|
mode="inline"
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
>
|
items={this.getBottomItems()}
|
||||||
{
|
|
||||||
this.state.bottomItems.map((item) => {
|
|
||||||
if (item.noContainer) {
|
|
||||||
return React.createElement(item.children, item.childrenProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Menu.Item
|
/>
|
||||||
key={item.id}
|
|
||||||
className="extra_bottom_item"
|
|
||||||
icon={createIconRender(item.icon)}
|
|
||||||
disabled={item.disabled ?? false}
|
|
||||||
{...item.containerProps}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
React.createElement(item.children, item.childrenProps)
|
|
||||||
}
|
|
||||||
</Menu.Item>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<Menu.Item key="search" icon={<Icons.Search />} >
|
|
||||||
<Translation>
|
|
||||||
{(t) => t("Search")}
|
|
||||||
</Translation>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="notifications" icon={<Icons.Bell />}>
|
|
||||||
<Translation>
|
|
||||||
{t => t("Notifications")}
|
|
||||||
</Translation>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="settings" icon={<Icons.Settings />}>
|
|
||||||
<Translation>
|
|
||||||
{t => t("Settings")}
|
|
||||||
</Translation>
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
{
|
|
||||||
app.userData && <Dropdown
|
|
||||||
menu={{
|
|
||||||
items: ActionMenuItems,
|
|
||||||
onClick: this.onClickDropdownItem
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
placement="top"
|
|
||||||
trigger={["click"]}
|
|
||||||
//onOpenChange={this.onDropdownOpenChange}
|
|
||||||
>
|
|
||||||
<Menu.Item
|
|
||||||
key="account"
|
|
||||||
className="user_avatar"
|
|
||||||
ignoreClick
|
|
||||||
onDoubleClick={onClickHandlers.account}
|
|
||||||
>
|
|
||||||
<Avatar shape="square" src={app.userData?.avatar} />
|
|
||||||
</Menu.Item>
|
|
||||||
</Dropdown>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!app.userData && <Menu.Item key="login" icon={<Icons.LogIn />}>
|
|
||||||
<Translation>
|
|
||||||
{t => t("Login")}
|
|
||||||
</Translation>
|
|
||||||
</Menu.Item>
|
|
||||||
}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}}
|
}}
|
||||||
</Motion>
|
</Motion>
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -173,14 +173,26 @@
|
|||||||
|
|
||||||
&.user_avatar {
|
&.user_avatar {
|
||||||
.ant-menu-title-content {
|
.ant-menu-title-content {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
|
.ant-dropdown-trigger {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
@ -6,7 +6,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
max-width: 20vw;
|
max-width: 420px;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
|
|
||||||
height: 100vh;
|
height: 100vh;
|
@ -2,32 +2,20 @@ import React from "react"
|
|||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Layout } from "antd"
|
import { Layout } from "antd"
|
||||||
|
|
||||||
import {
|
import Sidebar from "@layouts/components/sidebar"
|
||||||
Sidebar,
|
import Drawer from "@layouts/components/drawer"
|
||||||
Drawer,
|
import Sidedrawer from "@layouts/components/sidedrawer"
|
||||||
Sidedrawer,
|
import BottomBar from "@layouts/components/bottomBar"
|
||||||
BottomBar,
|
import TopBar from "@layouts/components/topBar"
|
||||||
TopBar,
|
import ToolsBar from "@layouts/components/toolsBar"
|
||||||
ToolsBar,
|
import Header from "@layouts/components/header"
|
||||||
Header,
|
import InitializeModalsController from "@layouts/components/modals"
|
||||||
} from "@components/Layout"
|
|
||||||
|
|
||||||
import BackgroundDecorator from "@components/BackgroundDecorator"
|
import BackgroundDecorator from "@components/BackgroundDecorator"
|
||||||
|
|
||||||
import { createWithDom as FloatingStack } from "../components/floatingStack"
|
|
||||||
import InitializeModalsController from "../components/modals"
|
|
||||||
|
|
||||||
const DesktopLayout = (props) => {
|
const DesktopLayout = (props) => {
|
||||||
InitializeModalsController()
|
InitializeModalsController()
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const floatingStack = FloatingStack()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
floatingStack.remove()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<BackgroundDecorator />
|
<BackgroundDecorator />
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ import React from "react"
|
|||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
|
|
||||||
import { Drawer, Sidedrawer } from "@components/Layout"
|
import Drawer from "@layouts/components/drawer"
|
||||||
|
import Sidedrawer from "@layouts/components/sidedrawer"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
return <antd.Layout className={classnames("app_layout")} style={{ height: "100%" }}>
|
return <antd.Layout className={classnames("app_layout")} style={{ height: "100%" }}>
|
||||||
|
@ -94,7 +94,6 @@ export default class Account extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onClickFollow = async () => {
|
onClickFollow = async () => {
|
||||||
const result = await FollowsModel.toggleFollow({
|
const result = await FollowsModel.toggleFollow({
|
||||||
user_id: this.state.user._id,
|
user_id: this.state.user._id,
|
||||||
@ -183,6 +182,13 @@ export default class Account extends React.Component {
|
|||||||
followed={this.state.following}
|
followed={this.state.following}
|
||||||
self={this.state.isSelf}
|
self={this.state.isSelf}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!this.state.isSelf && <antd.Button
|
||||||
|
icon={<Icons.MdMessage />}
|
||||||
|
onClick={() => app.location.push(`/messages/${user._id}`)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -108,7 +108,9 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
width: 20vw;
|
width: 20vw;
|
||||||
|
196
packages/app/src/pages/lyrics/components/controller/index.jsx
Normal file
196
packages/app/src/pages/lyrics/components/controller/index.jsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Tag } from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
|
import Marquee from "react-fast-marquee"
|
||||||
|
|
||||||
|
import { Icons } from "@components/Icons"
|
||||||
|
import Controls from "@components/Player/Controls"
|
||||||
|
|
||||||
|
import { Context } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
|
function isOverflown(element) {
|
||||||
|
if (!element) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderArtist = (props) => {
|
||||||
|
const { artist } = props
|
||||||
|
|
||||||
|
if (!artist) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(artist)) {
|
||||||
|
return <h3>{artist.join(",")}</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <h3>{artist}</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderAlbum = (props) => {
|
||||||
|
const { album } = props
|
||||||
|
|
||||||
|
if (!album) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(album)) {
|
||||||
|
return <h3>{album.join(",")}</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <h3>{album}</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlayerController = React.forwardRef((props, ref) => {
|
||||||
|
const context = React.useContext(Context)
|
||||||
|
|
||||||
|
const titleRef = React.useRef()
|
||||||
|
|
||||||
|
const [titleIsOverflown, setTitleIsOverflown] = React.useState(false)
|
||||||
|
|
||||||
|
const [currentTime, setCurrentTime] = React.useState(0)
|
||||||
|
const [trackDuration, setTrackDuration] = React.useState(0)
|
||||||
|
const [draggingTime, setDraggingTime] = React.useState(false)
|
||||||
|
const [currentDragWidth, setCurrentDragWidth] = React.useState(0)
|
||||||
|
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||||
|
|
||||||
|
async function onDragEnd(seekTime) {
|
||||||
|
setDraggingTime(false)
|
||||||
|
|
||||||
|
app.cores.player.seek(seekTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPlayback() {
|
||||||
|
if (!context.track_manifest) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTrackTime = app.cores.player.seek()
|
||||||
|
|
||||||
|
setCurrentTime(currentTrackTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
//* Handle when playback status change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (context.playback_status === "playing") {
|
||||||
|
setSyncInterval(setInterval(syncPlayback, 1000))
|
||||||
|
} else {
|
||||||
|
if (syncInterval) {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [context.playback_status])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setTitleIsOverflown(isOverflown(titleRef.current))
|
||||||
|
setTrackDuration(app.cores.player.duration())
|
||||||
|
}, [context.track_manifest])
|
||||||
|
|
||||||
|
const isStopped = context.playback_status === "stopped"
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="lyrics-player-controller-wrapper"
|
||||||
|
>
|
||||||
|
<div className="lyrics-player-controller">
|
||||||
|
<div className="lyrics-player-controller-info">
|
||||||
|
<div className="lyrics-player-controller-info-title">
|
||||||
|
{
|
||||||
|
<h4
|
||||||
|
ref={titleRef}
|
||||||
|
className={classnames(
|
||||||
|
"lyrics-player-controller-info-title-text",
|
||||||
|
{
|
||||||
|
["overflown"]: titleIsOverflown,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
context.playback_status === "stopped" ? "Nothing is playing" : <>
|
||||||
|
{context.track_manifest?.title ?? "Nothing is playing"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
titleIsOverflown && <Marquee
|
||||||
|
//gradient
|
||||||
|
//gradientColor={bgColor}
|
||||||
|
//gradientWidth={20}
|
||||||
|
play={!isStopped}
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
{
|
||||||
|
isStopped ?
|
||||||
|
"Nothing is playing" :
|
||||||
|
<>
|
||||||
|
{context.track_manifest?.title ?? "Untitled"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
</Marquee>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lyrics-player-controller-info-details">
|
||||||
|
<RenderArtist artist={context.track_manifest?.artists} />
|
||||||
|
-
|
||||||
|
<RenderAlbum album={context.track_manifest?.album} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controls />
|
||||||
|
|
||||||
|
<div className="lyrics-player-controller-progress-wrapper">
|
||||||
|
<div
|
||||||
|
className="lyrics-player-controller-progress"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
setDraggingTime(true)
|
||||||
|
}}
|
||||||
|
onMouseUp={(e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const seekTime = trackDuration * (e.clientX - rect.left) / rect.width
|
||||||
|
|
||||||
|
onDragEnd(seekTime)
|
||||||
|
}}
|
||||||
|
onMouseMove={(e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const atWidth = (e.clientX - rect.left) / rect.width * 100
|
||||||
|
|
||||||
|
setCurrentDragWidth(atWidth)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="lyrics-player-controller-progress-bar"
|
||||||
|
style={{
|
||||||
|
width: `${draggingTime ? currentDragWidth : ((currentTime / trackDuration) * 100)}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lyrics-player-controller-tags">
|
||||||
|
{
|
||||||
|
context.track_manifest?.metadata.lossless && <Tag
|
||||||
|
color="geekblue"
|
||||||
|
icon={<Icons.TbWaveSine />}
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
|
Lossless
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
context.track_manifest?.explicit && <Tag
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
|
Explicit
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PlayerController
|
147
packages/app/src/pages/lyrics/components/text/index.jsx
Normal file
147
packages/app/src/pages/lyrics/components/text/index.jsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React from "react"
|
||||||
|
import classnames from "classnames"
|
||||||
|
import { Motion, spring } from "react-motion"
|
||||||
|
|
||||||
|
import { Context } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
|
const LyricsText = React.forwardRef((props, textRef) => {
|
||||||
|
const context = React.useContext(Context)
|
||||||
|
|
||||||
|
const { lyrics } = props
|
||||||
|
|
||||||
|
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||||
|
const [currentLineIndex, setCurrentLineIndex] = React.useState(0)
|
||||||
|
const [visible, setVisible] = React.useState(false)
|
||||||
|
|
||||||
|
function syncPlayback() {
|
||||||
|
if (!lyrics) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTrackTime = app.cores.player.seek() * 1000
|
||||||
|
|
||||||
|
const lineIndex = lyrics.lrc.findIndex((line) => {
|
||||||
|
return currentTrackTime >= line.startTimeMs && currentTrackTime <= line.endTimeMs
|
||||||
|
})
|
||||||
|
|
||||||
|
if (lineIndex === -1) {
|
||||||
|
if (!visible) {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = lyrics.lrc[lineIndex]
|
||||||
|
|
||||||
|
setCurrentLineIndex(lineIndex)
|
||||||
|
|
||||||
|
if (line.break) {
|
||||||
|
return setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.text) {
|
||||||
|
return setVisible(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSyncInterval() {
|
||||||
|
setSyncInterval(setInterval(syncPlayback, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
//* Handle when current line index change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (currentLineIndex === 0) {
|
||||||
|
setVisible(false)
|
||||||
|
} else {
|
||||||
|
// find line element by id
|
||||||
|
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`)
|
||||||
|
|
||||||
|
// center scroll to current line
|
||||||
|
if (lineElement) {
|
||||||
|
lineElement.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentLineIndex])
|
||||||
|
|
||||||
|
//* Handle when playback status change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (lyrics) {
|
||||||
|
if (typeof lyrics?.lrc !== "undefined") {
|
||||||
|
if (context.playback_status === "playing") {
|
||||||
|
startSyncInterval()
|
||||||
|
} else {
|
||||||
|
if (syncInterval) {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
}
|
||||||
|
} startSyncInterval()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [context.playback_status])
|
||||||
|
|
||||||
|
//* Handle when lyrics object change
|
||||||
|
React.useEffect(() => {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
|
||||||
|
if (lyrics) {
|
||||||
|
if (typeof lyrics?.lrc !== "undefined") {
|
||||||
|
if (context.playback_status === "playing") {
|
||||||
|
startSyncInterval()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lyrics])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!lyrics?.lrc) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="lyrics-text-wrapper"
|
||||||
|
>
|
||||||
|
<Motion
|
||||||
|
style={{
|
||||||
|
opacity: spring(visible ? 1 : 0),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ opacity }) => {
|
||||||
|
return <div
|
||||||
|
ref={textRef}
|
||||||
|
className="lyrics-text"
|
||||||
|
style={{
|
||||||
|
opacity
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
lyrics.lrc.map((line, index) => {
|
||||||
|
return <p
|
||||||
|
key={index}
|
||||||
|
id={`lyrics-line-${index}`}
|
||||||
|
className={classnames(
|
||||||
|
"line",
|
||||||
|
{
|
||||||
|
["current"]: currentLineIndex === index
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{line.text}
|
||||||
|
</p>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LyricsText
|
166
packages/app/src/pages/lyrics/components/video/index.jsx
Normal file
166
packages/app/src/pages/lyrics/components/video/index.jsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import { Context } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
|
const maxLatencyInMs = 55
|
||||||
|
|
||||||
|
const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||||
|
const context = React.useContext(Context)
|
||||||
|
|
||||||
|
const { lyrics } = props
|
||||||
|
|
||||||
|
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||||
|
const [syncingVideo, setSyncingVideo] = React.useState(false)
|
||||||
|
const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0)
|
||||||
|
|
||||||
|
async function seekVideoToSyncAudio() {
|
||||||
|
if (lyrics) {
|
||||||
|
if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") {
|
||||||
|
const currentTrackTime = app.cores.player.seek()
|
||||||
|
|
||||||
|
setSyncingVideo(true)
|
||||||
|
|
||||||
|
videoRef.current.currentTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPlayback() {
|
||||||
|
if (!lyrics) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if `sync_audio_at_ms` is present, it means the video must be synced with audio
|
||||||
|
if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") {
|
||||||
|
const currentTrackTime = app.cores.player.seek()
|
||||||
|
const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000)
|
||||||
|
|
||||||
|
//console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`)
|
||||||
|
|
||||||
|
const maxOffset = maxLatencyInMs / 1000
|
||||||
|
const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime)
|
||||||
|
|
||||||
|
setCurrentVideoLatency(currentVideoTimeDiff)
|
||||||
|
|
||||||
|
if (syncingVideo === true) {
|
||||||
|
console.log(`Syncing video...`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVideoTimeDiff > maxOffset) {
|
||||||
|
console.warn(`Video offset exceeds`, maxOffset)
|
||||||
|
seekVideoToSyncAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSyncInterval() {
|
||||||
|
setSyncInterval(setInterval(syncPlayback, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
videoRef.current.addEventListener("seeked", (event) => {
|
||||||
|
setSyncingVideo(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// videoRef.current.addEventListener("error", (event) => {
|
||||||
|
// console.log("Failed to load", event)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// videoRef.current.addEventListener("ended", (event) => {
|
||||||
|
// console.log("Video ended", event)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// videoRef.current.addEventListener("stalled", (event) => {
|
||||||
|
// console.log("Failed to fetch data, but trying")
|
||||||
|
// })
|
||||||
|
|
||||||
|
// videoRef.current.addEventListener("waiting", (event) => {
|
||||||
|
// console.log("Waiting for data...")
|
||||||
|
// })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
//* Handle when playback status change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof lyrics?.sync_audio_at_ms !== "undefined") {
|
||||||
|
if (context.playback_status === "playing") {
|
||||||
|
videoRef.current.play()
|
||||||
|
|
||||||
|
setSyncInterval(setInterval(syncPlayback, 500))
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause()
|
||||||
|
|
||||||
|
if (syncInterval) {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [context.playback_status])
|
||||||
|
|
||||||
|
//* Handle when lyrics object change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (lyrics) {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
setCurrentVideoLatency(0)
|
||||||
|
setSyncingVideo(false)
|
||||||
|
|
||||||
|
if (lyrics.video_source) {
|
||||||
|
videoRef.current.src = lyrics.video_source
|
||||||
|
|
||||||
|
videoRef.current.load()
|
||||||
|
|
||||||
|
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
|
||||||
|
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
|
||||||
|
|
||||||
|
if (context.playback_status === "playing") {
|
||||||
|
videoRef.current.play()
|
||||||
|
startSyncInterval()
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = app.cores.player.seek()
|
||||||
|
|
||||||
|
if (currentTime > 0) {
|
||||||
|
seekVideoToSyncAudio()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
videoRef.current.loop = true
|
||||||
|
videoRef.current.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lyrics])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(syncInterval)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="videoDebugOverlay">
|
||||||
|
<div>
|
||||||
|
<p>Maximun latency</p>
|
||||||
|
<p>{maxLatencyInMs}ms</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Video Latency</p>
|
||||||
|
<p>{(currentVideoLatency * 1000).toFixed(2)}ms</p>
|
||||||
|
</div>
|
||||||
|
{syncingVideo ? <p>Syncing video...</p> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<video
|
||||||
|
className="lyrics-video"
|
||||||
|
ref={videoRef}
|
||||||
|
controls={false}
|
||||||
|
muted
|
||||||
|
preload="auto"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LyricsVideo
|
758
packages/app/src/pages/lyrics/index.jsx
Executable file → Normal file
758
packages/app/src/pages/lyrics/index.jsx
Executable file → Normal file
@ -1,750 +1,76 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import Marquee from "react-fast-marquee"
|
|
||||||
|
|
||||||
import Image from "@components/Image"
|
|
||||||
import Controls from "@components/Player/Controls"
|
|
||||||
|
|
||||||
|
import useMaxScreen from "@utils/useMaxScreen"
|
||||||
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
||||||
|
|
||||||
import request from "comty.js/handlers/request"
|
import MusicService from "@models/music"
|
||||||
|
|
||||||
|
import PlayerController from "./components/controller"
|
||||||
|
import LyricsVideo from "./components/video"
|
||||||
|
import LyricsText from "./components/text"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
function composeRgbValues(values) {
|
const EnchancedLyrics = (props) => {
|
||||||
let value = ""
|
const context = React.useContext(Context)
|
||||||
|
const [lyrics, setLyrics] = React.useState(null)
|
||||||
|
|
||||||
// only get the first 3 values
|
const videoRef = React.useRef()
|
||||||
for (let i = 0; i < 3; i++) {
|
const textRef = React.useRef()
|
||||||
// if last value, don't add comma
|
|
||||||
if (i === 2) {
|
|
||||||
value += `${values[i]}`
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
value += `${values[i]}, `
|
async function loadLyrics(track_id) {
|
||||||
}
|
const result = await MusicService.getTrackLyrics(track_id)
|
||||||
|
|
||||||
return value
|
if (result) {
|
||||||
}
|
setLyrics(result)
|
||||||
|
|
||||||
function calculateLineTime(line) {
|
|
||||||
if (!line) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return line.endTimeMs - line.startTimeMs
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOverflown(element) {
|
|
||||||
if (!element) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlayerController extends React.Component {
|
|
||||||
state = {
|
|
||||||
colorAnalysis: null,
|
|
||||||
currentDragWidth: 0,
|
|
||||||
titleOverflown: false,
|
|
||||||
|
|
||||||
currentDuration: 0,
|
|
||||||
currentTime: 0,
|
|
||||||
|
|
||||||
currentPlaying: app.cores.player.state["track_manifest"],
|
|
||||||
loading: app.cores.player.state["loading"] ?? false,
|
|
||||||
playbackStatus: app.cores.player.state["playback_status"] ?? "stopped",
|
|
||||||
|
|
||||||
audioMuted: app.cores.player.state["muted"] ?? false,
|
|
||||||
volume: app.cores.player.state["volume"],
|
|
||||||
|
|
||||||
syncModeLocked: app.cores.player.state["control_locked"] ?? false,
|
|
||||||
syncMode: app.cores.player.state["sync_mode"],
|
|
||||||
}
|
|
||||||
|
|
||||||
events = {
|
|
||||||
"player.seeked": (seekTime) => {
|
|
||||||
this.setState({
|
|
||||||
currentTime: seekTime,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"player.state.update:playback_status": (data) => {
|
|
||||||
this.setState({ playbackStatus: data })
|
|
||||||
},
|
|
||||||
"player.state.update:track_manifest": (data) => {
|
|
||||||
this.setState({ titleOverflown: false })
|
|
||||||
|
|
||||||
this.setState({ currentPlaying: data })
|
|
||||||
},
|
|
||||||
"player.state.update:control_locked": (to) => {
|
|
||||||
this.setState({ syncModeLocked: to })
|
|
||||||
},
|
|
||||||
"player.state.update:sync_mode": (to) => {
|
|
||||||
this.setState({ syncMode: to })
|
|
||||||
},
|
|
||||||
"player.state.update:muted": (data) => {
|
|
||||||
this.setState({ audioMuted: data })
|
|
||||||
},
|
|
||||||
"player.state.update:volume": (data) => {
|
|
||||||
this.setState({ audioVolume: data })
|
|
||||||
},
|
|
||||||
"player.state.update:loading": (data) => {
|
|
||||||
this.setState({ loading: data })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
titleRef = React.createRef()
|
|
||||||
|
|
||||||
startSync() {
|
|
||||||
// create a interval to get state from player
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncInterval = setInterval(() => {
|
|
||||||
const time = app.cores.player.seek()
|
|
||||||
const duration = app.cores.player.duration()
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
currentDuration: duration,
|
|
||||||
currentTime: time,
|
|
||||||
colorAnalysis: app.cores.player.state.track_manifest?.metadata.cover_analysis,
|
|
||||||
})
|
|
||||||
|
|
||||||
const titleOverflown = isOverflown(this.titleRef.current)
|
|
||||||
|
|
||||||
this.setState({ titleOverflown: titleOverflown })
|
|
||||||
}, 800)
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickPreviousButton = () => {
|
|
||||||
app.cores.player.playback.previous()
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickNextButton = () => {
|
|
||||||
app.cores.player.playback.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicktogglePlayButton = () => {
|
|
||||||
if (this.state?.playbackStatus === "playing") {
|
|
||||||
app.cores.player.playback.pause()
|
|
||||||
} else {
|
|
||||||
app.cores.player.playback.play()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVolume = (value) => {
|
useMaxScreen()
|
||||||
app.cores.player.volume(value)
|
|
||||||
|
//* Handle when context change track_manifest
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (context.track_manifest) {
|
||||||
|
loadLyrics(context.track_manifest._id)
|
||||||
}
|
}
|
||||||
|
}, [context.track_manifest])
|
||||||
|
|
||||||
toggleMute = () => {
|
//* Handle when lyrics data change
|
||||||
app.cores.player.toggleMute()
|
React.useEffect(() => {
|
||||||
}
|
console.log(lyrics)
|
||||||
|
}, [lyrics])
|
||||||
|
|
||||||
componentDidMount() {
|
return <div
|
||||||
for (const event in this.events) {
|
|
||||||
app.eventBus.on(event, this.events[event])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
for (const event in this.events) {
|
|
||||||
app.eventBus.off(event, this.events[event])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDragEnd = (seekTime) => {
|
|
||||||
this.setState({
|
|
||||||
currentDragWidth: 0,
|
|
||||||
dragging: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
app.cores.player.seek(seekTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
//const bgColor = RGBStringToValues(getComputedStyle(document.documentElement).getPropertyValue("--background-color-accent-values"))
|
|
||||||
|
|
||||||
return <div className="player_controller_wrapper">
|
|
||||||
<div
|
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"player_controller",
|
"lyrics",
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="player_controller_cover">
|
|
||||||
<Image
|
|
||||||
src={this.state.currentPlaying?.cover ?? this.state.currentPlaying?.thumbnail ?? "/assets/no_song.png"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="player_controller_left">
|
|
||||||
<div className="player_controller_info">
|
|
||||||
<div className="player_controller_info_title">
|
|
||||||
{
|
{
|
||||||
<h4
|
["stopped"]: context.playback_status !== "playing",
|
||||||
ref={this.titleRef}
|
|
||||||
className={classnames(
|
|
||||||
"player_controller_info_title_text",
|
|
||||||
{
|
|
||||||
["overflown"]: this.state.titleOverflown,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{
|
<LyricsVideo
|
||||||
this.state.plabackState === "stopped" ? "Nothing is playing" : <>
|
ref={videoRef}
|
||||||
{this.state.currentPlaying?.title ?? "Nothing is playing"}
|
lyrics={lyrics}
|
||||||
</>
|
/>
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
}
|
|
||||||
|
|
||||||
{this.state.titleOverflown &&
|
<LyricsText
|
||||||
<Marquee
|
ref={textRef}
|
||||||
//gradient
|
lyrics={lyrics}
|
||||||
//gradientColor={bgColor}
|
/>
|
||||||
//gradientWidth={20}
|
|
||||||
play={this.state.plabackState !== "stopped"}
|
<PlayerController
|
||||||
>
|
|
||||||
<h4>
|
|
||||||
{
|
|
||||||
this.state.plabackState === "stopped" ? "Nothing is playing" : <>
|
|
||||||
{this.state.currentPlaying?.title ?? "Nothing is playing"}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
</Marquee>}
|
|
||||||
</div>
|
|
||||||
<div className="player_controller_info_artist">
|
|
||||||
{
|
|
||||||
(this.state.currentPlaying?.metadata?.artist ?? this.state.currentPlaying?.artist) && <>
|
|
||||||
<h3>
|
|
||||||
{this.state.currentPlaying?.metadata?.artist ?? this.state.currentPlaying?.artist ?? "Unknown"}
|
|
||||||
</h3>
|
|
||||||
{
|
|
||||||
(this.state.currentPlaying?.metadata?.album ?? this.state.currentPlaying?.album) && <>
|
|
||||||
<span> - </span>
|
|
||||||
<h3>
|
|
||||||
{this.state.currentPlaying?.metadata?.album ?? this.state.currentPlaying?.album ?? "Unknown"}
|
|
||||||
</h3>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Controls
|
|
||||||
className="player_controller_controls"
|
|
||||||
controls={{
|
|
||||||
previous: this.onClickPreviousButton,
|
|
||||||
toggle: this.onClicktogglePlayButton,
|
|
||||||
next: this.onClickNextButton,
|
|
||||||
}}
|
|
||||||
syncModeLocked={this.state.syncModeLocked}
|
|
||||||
playbackStatus={this.state.playbackStatus}
|
|
||||||
loading={this.state.loading}
|
|
||||||
audioVolume={this.state.audioVolume}
|
|
||||||
audioMuted={this.state.audioMuted}
|
|
||||||
onVolumeUpdate={this.updateVolume}
|
|
||||||
onMuteUpdate={this.toggleMute}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="player_controller_progress_wrapper">
|
|
||||||
<div
|
|
||||||
className="player_controller_progress"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
this.setState({
|
|
||||||
dragging: true,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onMouseUp={(e) => {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const seekTime = this.state.currentDuration * (e.clientX - rect.left) / rect.width
|
|
||||||
|
|
||||||
this.onDragEnd(seekTime)
|
|
||||||
}}
|
|
||||||
onMouseMove={(e) => {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const atWidth = (e.clientX - rect.left) / rect.width * 100
|
|
||||||
|
|
||||||
this.setState({ currentDragWidth: atWidth })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="player_controller_progress_bar"
|
|
||||||
style={{
|
|
||||||
width: `${this.state.dragging ? this.state.currentDragWidth : this.state.currentTime / this.state.currentDuration * 100}%`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props) => {
|
const EnchancedLyricsPage = (props) => {
|
||||||
return <WithPlayerContext>
|
return <WithPlayerContext>
|
||||||
<SyncLyrics
|
<EnchancedLyrics
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</WithPlayerContext>
|
</WithPlayerContext>
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncLyrics extends React.Component {
|
export default EnchancedLyricsPage
|
||||||
static contextType = Context
|
|
||||||
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
notAvailable: false,
|
|
||||||
|
|
||||||
currentManifest: null,
|
|
||||||
currentStatus: null,
|
|
||||||
|
|
||||||
canvas_url: null,
|
|
||||||
lyrics: null,
|
|
||||||
currentLine: null,
|
|
||||||
|
|
||||||
colorAnalysis: null,
|
|
||||||
|
|
||||||
classnames: {
|
|
||||||
"cinematic-mode": false,
|
|
||||||
"centered-player": false,
|
|
||||||
"video-canvas-enabled": false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visualizerRef = React.createRef()
|
|
||||||
|
|
||||||
videoCanvasRef = React.createRef()
|
|
||||||
|
|
||||||
coverCanvasRef = React.createRef()
|
|
||||||
|
|
||||||
events = {
|
|
||||||
"player.state.update:track_manifest": (currentManifest) => {
|
|
||||||
console.log(currentManifest)
|
|
||||||
this.setState({ currentManifest })
|
|
||||||
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
document.startViewTransition(this.loadLyrics)
|
|
||||||
} else {
|
|
||||||
this.loadLyrics()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"player.state.update:playback_status": (currentStatus) => {
|
|
||||||
this.setState({ currentStatus })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleClassName = (className, to) => {
|
|
||||||
if (typeof to === "undefined") {
|
|
||||||
to = !this.state.classnames[className]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to) {
|
|
||||||
if (this.state.classnames[className] === true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
//app.message.info("Toogling on " + className)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
classnames: {
|
|
||||||
...this.state.classnames,
|
|
||||||
[className]: true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
if (this.state.classnames[className] === false) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
//app.message.info("Toogling off " + className)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
classnames: {
|
|
||||||
...this.state.classnames,
|
|
||||||
[className]: false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleVideoCanvas = (to) => {
|
|
||||||
return this.toggleClassName("video-canvas-enabled", to)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleCenteredControllerMode = (to) => {
|
|
||||||
return this.toggleClassName("centered-player", to)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleCinematicMode = (to) => {
|
|
||||||
return this.toggleClassName("cinematic-mode", to)
|
|
||||||
}
|
|
||||||
|
|
||||||
isCurrentLine = (line) => {
|
|
||||||
if (!this.state.currentLine) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.currentLine.startTimeMs === line.startTimeMs
|
|
||||||
}
|
|
||||||
|
|
||||||
loadLyrics = async () => {
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.context.track_manifest) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
loading: true,
|
|
||||||
notAvailable: false,
|
|
||||||
lyrics: null,
|
|
||||||
currentLine: null,
|
|
||||||
canvas_url: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const api = app.cores.api.instance().instances.music
|
|
||||||
|
|
||||||
let response = await request({
|
|
||||||
instance: api,
|
|
||||||
method: "get",
|
|
||||||
url: `/lyrics/${this.state.currentManifest._id}`,
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
notAvailable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
let data = response.data ?? {
|
|
||||||
lines: [],
|
|
||||||
syncType: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(this.state.currentManifest)
|
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
if (data.lines.length > 0 && data.syncType === "LINE_SYNCED") {
|
|
||||||
data.lines = data.lines.map((line, index) => {
|
|
||||||
const ref = React.createRef()
|
|
||||||
|
|
||||||
line.ref = ref
|
|
||||||
|
|
||||||
line.startTimeMs = Number(line.startTimeMs)
|
|
||||||
|
|
||||||
const nextLine = data.lines[index + 1]
|
|
||||||
|
|
||||||
// calculate end time
|
|
||||||
line.endTimeMs = nextLine ? Number(nextLine.startTimeMs) : Math.floor(app.cores.player.duration() * 1000)
|
|
||||||
|
|
||||||
return line
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.canvas_url) {
|
|
||||||
//app.message.info("Video canvas loaded")
|
|
||||||
console.log(`[SyncLyrics] Video canvas loaded`)
|
|
||||||
|
|
||||||
this.toggleVideoCanvas(true)
|
|
||||||
} else {
|
|
||||||
//app.message.info("No video canvas available for this song")
|
|
||||||
console.log(`[SyncLyrics] No video canvas available for this song`)
|
|
||||||
|
|
||||||
this.toggleVideoCanvas(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if has no lyrics or are unsynced, toggle cinematic mode off and center controller
|
|
||||||
if (data.lines.length === 0 || data.syncType !== "LINE_SYNCED") {
|
|
||||||
//app.message.info("No lyrics available for this song")
|
|
||||||
|
|
||||||
console.log(`[SyncLyrics] No lyrics available for this song, sync type [${data.syncType}]`)
|
|
||||||
|
|
||||||
this.toggleCinematicMode(false)
|
|
||||||
this.toggleCenteredControllerMode(true)
|
|
||||||
} else {
|
|
||||||
//app.message.info("Lyrics loaded, starting sync...")
|
|
||||||
console.log(`[SyncLyrics] Starting sync with type [${data.syncType}]`)
|
|
||||||
|
|
||||||
this.toggleCenteredControllerMode(false)
|
|
||||||
this.startLyricsSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// transform times
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
syncType: data.syncType,
|
|
||||||
canvas_url: data.canvas_url ?? null,
|
|
||||||
lyrics: data.lines,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
startLyricsSync = () => {
|
|
||||||
// create interval to sync lyrics
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to top
|
|
||||||
this.visualizerRef.current.scrollTop = 0
|
|
||||||
|
|
||||||
this.syncInterval = setInterval(() => {
|
|
||||||
if (!this.state.lyrics || !Array.isArray(this.state.lyrics) || this.state.lyrics.length === 0 || !this.state.lyrics[0]) {
|
|
||||||
console.warn(`Clearing interval because lyrics is not found or lyrics is empty, probably because memory leak or unmounted component`)
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const time = app.cores.player.seek()
|
|
||||||
|
|
||||||
// transform audio seek time to lyrics time (ms from start) // remove decimals
|
|
||||||
const transformedTime = Math.floor(time * 1000)
|
|
||||||
|
|
||||||
const hasStartedFirst = transformedTime >= this.state.lyrics[0].startTimeMs
|
|
||||||
|
|
||||||
if (!hasStartedFirst) {
|
|
||||||
if (this.state.canvas_url) {
|
|
||||||
this.toggleCinematicMode(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the closest line to the transformed time
|
|
||||||
const line = this.state.lyrics.find((line) => {
|
|
||||||
// match the closest line to the transformed time
|
|
||||||
return transformedTime >= line.startTimeMs && transformedTime <= line.endTimeMs
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!line || !line.ref) {
|
|
||||||
console.warn(`Clearing interval because cannot find line to sync or line REF is not found, probably because memory leak or unmounted component`)
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line) {
|
|
||||||
if (this.isCurrentLine(line)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// set current line
|
|
||||||
this.setState({
|
|
||||||
currentLine: line,
|
|
||||||
})
|
|
||||||
|
|
||||||
//console.log(line)
|
|
||||||
|
|
||||||
if (!line.ref.current) {
|
|
||||||
console.log(line)
|
|
||||||
console.warn(`Clearing interval because line CURRENT ref is not found, probably because memory leak or unmounted component`)
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visualizerRef.current.scrollTo({
|
|
||||||
top: line.ref.current.offsetTop - (this.visualizerRef.current.offsetHeight / 2),
|
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.state.canvas_url) {
|
|
||||||
if (line.words === "♪" || line.words === "♫" || line.words === " " || line.words === "") {
|
|
||||||
//console.log(`[SyncLyrics] Toogling cinematic mode on because line is empty`)
|
|
||||||
|
|
||||||
this.toggleCinematicMode(true)
|
|
||||||
} else {
|
|
||||||
//console.log(`[SyncLyrics] Toogling cinematic mode off because line is not empty`)
|
|
||||||
|
|
||||||
this.toggleCinematicMode(false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.state.classnames["cinematic-mode"] === true) {
|
|
||||||
this.toggleCinematicMode(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
// register player events
|
|
||||||
for (const [event, callback] of Object.entries(this.events)) {
|
|
||||||
app.eventBus.on(event, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get current playback status and time
|
|
||||||
const {
|
|
||||||
track_manifest,
|
|
||||||
playback_status,
|
|
||||||
} = app.cores.player.state
|
|
||||||
|
|
||||||
await this.setState({
|
|
||||||
currentManifest: track_manifest,
|
|
||||||
currentStatus: playback_status,
|
|
||||||
colorAnalysis: track_manifest.cover_analysis,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (app.layout.sidebar) {
|
|
||||||
app.controls.toggleUIVisibility(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.layout.toggleCenteredContent(false)
|
|
||||||
|
|
||||||
app.cores.style.compactMode(true)
|
|
||||||
app.cores.style.applyVariant("dark")
|
|
||||||
|
|
||||||
// // request full screen to browser
|
|
||||||
// if (document.fullscreenEnabled) {
|
|
||||||
// document.documentElement.requestFullscreen()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // listen when user exit full screen to exit cinematic mode
|
|
||||||
// document.addEventListener("fullscreenchange", () => {
|
|
||||||
// if (!document.fullscreenElement) {
|
|
||||||
// app.location.back()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
window._hacks = {
|
|
||||||
toggleVideoCanvas: this.toggleVideoCanvas,
|
|
||||||
toggleCinematicMode: this.toggleCinematicMode,
|
|
||||||
toggleCenteredControllerMode: this.toggleCenteredControllerMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.loadLyrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// unregister player events
|
|
||||||
for (const [event, callback] of Object.entries(this.events)) {
|
|
||||||
app.eventBus.off(event, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear sync interval
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete window._hacks
|
|
||||||
|
|
||||||
if (app.layout.sidebar) {
|
|
||||||
app.controls.toggleUIVisibility(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.cores.style.compactMode(false)
|
|
||||||
app.cores.style.applyInitialVariant()
|
|
||||||
|
|
||||||
// // exit full screen
|
|
||||||
// if (document.fullscreenEnabled) {
|
|
||||||
// document.exitFullscreen()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLines() {
|
|
||||||
if (!this.state.lyrics || this.state.notAvailable || this.state.syncType !== "LINE_SYNCED") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.lyrics.map((line, index) => {
|
|
||||||
return <div
|
|
||||||
ref={line.ref}
|
|
||||||
className={classnames(
|
|
||||||
"lyrics_viewer_lines_line",
|
|
||||||
{
|
|
||||||
["current"]: this.isCurrentLine(line)
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
id={line.startTimeMs}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
{line.words}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
ref={this.visualizerRef}
|
|
||||||
className={classnames(
|
|
||||||
"lyrics_viewer",
|
|
||||||
{
|
|
||||||
["text_dark"]: this.state.colorAnalysis?.isDark ?? false,
|
|
||||||
...Object.entries(this.state.classnames).reduce((acc, [key, value]) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[key]: value,
|
|
||||||
}
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
"--predominant-color": this.state.colorAnalysis?.hex ?? "unset",
|
|
||||||
"--predominant-color-rgb-values": this.state.colorAnalysis?.value ? composeRgbValues(this.state.colorAnalysis?.value) : [0, 0, 0],
|
|
||||||
"--line-time": `${calculateLineTime(this.state.currentLine)}ms`,
|
|
||||||
"--line-animation-play-state": this.state.currentStatus === "playing" ? "running" : "paused",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="lyrics_viewer_mask"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="lyrics_viewer_video_canvas"
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
src={this.state.canvas_url}
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
controls={false}
|
|
||||||
ref={this.videoCanvasRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="lyrics_viewer_cover"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={this.state.currentManifest?.cover ?? this.state.currentManifest?.thumbnail ?? "/assets/no_song.png"}
|
|
||||||
ref={this.coverRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PlayerController />
|
|
||||||
|
|
||||||
<div className="lyrics_viewer_content">
|
|
||||||
<div className="lyrics_viewer_lines">
|
|
||||||
{
|
|
||||||
this.renderLines()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
564
packages/app/src/pages/lyrics/index.less
Executable file → Normal file
564
packages/app/src/pages/lyrics/index.less
Executable file → Normal file
@ -1,405 +1,130 @@
|
|||||||
@enabled-video-canvas-opacity: 0.4;
|
.lyrics {
|
||||||
// in px
|
position: relative;
|
||||||
@cover-width: 150px;
|
|
||||||
@left-panel-width: 300px;
|
z-index: 100;
|
||||||
|
|
||||||
.lyrics_viewer {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
isolation: isolate;
|
&.stopped {
|
||||||
|
.lyrics-video {
|
||||||
|
filter: blur(6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//align-items: center;
|
.lyrics-video {
|
||||||
|
z-index: 105;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
padding: 50px 0;
|
object-fit: cover;
|
||||||
|
|
||||||
overflow-y: hidden;
|
transition: all 150ms ease-out;
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
background-color: rgba(var(--predominant-color-rgb-values), 0.8);
|
|
||||||
background:
|
|
||||||
linear-gradient(20deg, rgba(var(--predominant-color-rgb-values), 0.8), rgba(var(--predominant-color-rgb-values), 0.2)),
|
|
||||||
url(https://grainy-gradients.vercel.app/noise.svg);
|
|
||||||
|
|
||||||
//background-size: 1%;
|
|
||||||
background-position: center;
|
|
||||||
|
|
||||||
&.video-canvas-enabled {
|
|
||||||
background-color: rgba(var(--predominant-color-rgb-values), 1);
|
|
||||||
|
|
||||||
.lyrics_viewer_video_canvas {
|
|
||||||
video {
|
|
||||||
opacity: @enabled-video-canvas-opacity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.centered-player {
|
.lyrics-text-wrapper {
|
||||||
.lyrics_viewer_cover {
|
z-index: 110;
|
||||||
width: 100vw;
|
position: fixed;
|
||||||
|
|
||||||
height: 80vh; //fallback
|
bottom: 0;
|
||||||
height: 80dvh;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
bottom: 20vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_wrapper {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
width: 100%;
|
padding: 60px;
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
align-items: center;
|
.lyrics-text {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 600px;
|
||||||
|
height: 200px;
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
background-color: rgba(var(--background-color-accent-values), 0.6);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
|
||||||
|
.line {
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
opacity: 0.1;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
.player_controller {
|
&.current {
|
||||||
margin-top: 40%;
|
|
||||||
|
|
||||||
max-width: 50vw;
|
|
||||||
max-height: 50vh;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
//height: 100%;
|
|
||||||
|
|
||||||
border-radius: 18px;
|
|
||||||
|
|
||||||
gap: 0;
|
|
||||||
|
|
||||||
padding: 20px 40px;
|
|
||||||
|
|
||||||
.player_controller_left {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_cover {
|
|
||||||
width: 0px;
|
|
||||||
|
|
||||||
min-width: 0px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
min-width: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_info {
|
|
||||||
.player_controller_info_title {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cinematic-mode {
|
|
||||||
.lyrics_viewer_mask {
|
|
||||||
backdrop-filter: blur(0px);
|
|
||||||
-webkit-backdrop-filter: blur(0px)
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics_viewer_video_canvas {
|
|
||||||
video {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyrics_viewer_content {
|
|
||||||
.lyrics_viewer_lines {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.text_dark {
|
|
||||||
.lyrics_viewer_content {
|
|
||||||
.lyrics_viewer_lines {
|
|
||||||
.lyrics_viewer_lines_line {
|
|
||||||
color: var(--text-color-white);
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--text-color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics_viewer_mask {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
z-index: 200;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
backdrop-filter: blur(21px);
|
|
||||||
-webkit-backdrop-filter: blur(21px)
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics_viewer_video_canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
//height: 100dvh;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
object-fit: cover;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyrics_viewer_cover {
|
.lyrics-player-controller-wrapper {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
|
z-index: 115;
|
||||||
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
right: 0;
|
||||||
|
|
||||||
z-index: 250;
|
padding: 60px;
|
||||||
|
|
||||||
display: flex;
|
.lyrics-player-controller {
|
||||||
flex-direction: row;
|
position: relative;
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 25vw;
|
|
||||||
height: 25vw;
|
|
||||||
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 500px;
|
|
||||||
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics_viewer_content {
|
|
||||||
z-index: 250;
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
.lyrics_viewer_lines {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 90%;
|
gap: 10px;
|
||||||
|
|
||||||
margin: auto;
|
width: 300px;
|
||||||
|
|
||||||
font-family: "Space Grotesk", sans-serif;
|
padding: 30px;
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
&::after,
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
//height: 50dvh;
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics_viewer_lines_line {
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
z-index: 250;
|
|
||||||
|
|
||||||
text-wrap: balance;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
margin: 20px 0;
|
|
||||||
font-size: 2rem;
|
|
||||||
|
|
||||||
animation: spacing-letters var(--line-time) ease-in-out forwards;
|
|
||||||
animation-play-state: var(--line-animation-play-state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spacing-letters {
|
|
||||||
0% {
|
|
||||||
letter-spacing: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
margin: 50px;
|
|
||||||
|
|
||||||
z-index: 350;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
.marquee-container {
|
|
||||||
gap: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
min-width: 350px;
|
|
||||||
max-width: 500px;
|
|
||||||
|
|
||||||
height: 220px;
|
|
||||||
|
|
||||||
background-color: rgba(var(--background-color-accent-values), 0.4);
|
|
||||||
// background:
|
|
||||||
// linear-gradient(20deg, rgba(var(--background-color-accent-values), 0.8), transparent),
|
|
||||||
// url(https://grainy-gradients.vercel.app/noise.svg);
|
|
||||||
|
|
||||||
-webkit-backdrop-filter: blur(21px);
|
|
||||||
backdrop-filter: blur(21px);
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
gap: 20px;
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
|
||||||
color: var(--text-color);
|
background-color: rgba(var(--background-color-accent-values), 0.8);
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.player_controller_controls {
|
gap: 20px;
|
||||||
height: 8vh;
|
|
||||||
max-height: 100px;
|
.player-controls {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player_controller_progress_wrapper {
|
.lyrics-player-controller-tags {
|
||||||
bottom: 7px;
|
opacity: 1;
|
||||||
|
|
||||||
.player_controller_progress {
|
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|
||||||
width: 90%;
|
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player_controller_cover {
|
.lyrics-player-controller-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: @cover-width;
|
|
||||||
min-width: @cover-width;
|
|
||||||
max-width: @cover-width;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
object-fit: cover;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_left {
|
|
||||||
flex: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: @left-panel-width;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
.player_controller_info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
//align-items: flex-start;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
.player_controller_info_title {
|
.lyrics-player-controller-info-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
@ -411,7 +136,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player_controller_info_title_text {
|
.lyrics-player-controller-title-text {
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
width: 90%;
|
width: 90%;
|
||||||
@ -428,7 +153,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player_controller_info_artist {
|
.lyrics-player-controller-info-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
@ -437,120 +162,99 @@
|
|||||||
gap: 7px;
|
gap: 7px;
|
||||||
|
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
|
// do not wrap text
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-controls {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0px;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player_controller_controls {
|
.lyrics-player-controller-progress-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.lyrics-player-controller-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
background-color: rgba(var(--background-color-accent-values), 0.8);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.lyrics-player-controller-progress-bar {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-player-controller-progress-bar {
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-player-controller-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
height: 0px;
|
height: 0px;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.playButton {
|
.videoDebugOverlay {
|
||||||
position: relative;
|
position: fixed;
|
||||||
|
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
z-index: 115;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.loadCircle {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
z-index: 330;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
|
|
||||||
transform: scale(1.5);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke: var(--text-color);
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_progress_wrapper {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.player_controller_progress {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
height: 5px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
.player_controller_progress_bar {
|
background-color: rgba(var(--background-color-accent-values), 0.8);
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background-color: var(--background-color-contrast);
|
width: 200px;
|
||||||
|
height: fit-content;
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bottom-to-top {
|
|
||||||
0% {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
bottom: 20vh;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
161
packages/app/src/pages/messages/[to_user_id]/index.jsx
Normal file
161
packages/app/src/pages/messages/[to_user_id]/index.jsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
import UserPreview from "@components/UserPreview"
|
||||||
|
|
||||||
|
import useChat from "@hooks/useChat"
|
||||||
|
import ChatsService from "@models/chats"
|
||||||
|
|
||||||
|
import lodash from "lodash"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const ChatPage = (props) => {
|
||||||
|
const { to_user_id } = props.params
|
||||||
|
|
||||||
|
const messagesRef = React.useRef()
|
||||||
|
|
||||||
|
const [isOnBottomView, setIsOnBottomView] = React.useState(true)
|
||||||
|
const [currentText, setCurrentText] = React.useState("")
|
||||||
|
|
||||||
|
const [L_History, R_History, E_History, M_History] = app.cores.api.useRequest(ChatsService.getChatHistory, to_user_id)
|
||||||
|
|
||||||
|
const {
|
||||||
|
sendMessage,
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
setScroller,
|
||||||
|
emitTypingEvent,
|
||||||
|
isRemoteTyping,
|
||||||
|
} = useChat(to_user_id)
|
||||||
|
|
||||||
|
async function submitMessage() {
|
||||||
|
if (!currentText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendMessage(currentText)
|
||||||
|
|
||||||
|
setCurrentText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onInputChange(e) {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
setCurrentText(value)
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
emitTypingEvent(false)
|
||||||
|
} {
|
||||||
|
emitTypingEvent(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (R_History) {
|
||||||
|
setMessages(R_History.list)
|
||||||
|
// scroll to bottom
|
||||||
|
messagesRef.current?.scrollTo({
|
||||||
|
top: messagesRef.current.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [R_History])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isOnBottomView === true) {
|
||||||
|
setScroller(messagesRef)
|
||||||
|
} else {
|
||||||
|
setScroller(null)
|
||||||
|
}
|
||||||
|
}, [isOnBottomView])
|
||||||
|
|
||||||
|
if (E_History) {
|
||||||
|
return <antd.Result
|
||||||
|
status="warning"
|
||||||
|
title="Error"
|
||||||
|
subTitle={E_History.message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (L_History) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="chat-page"
|
||||||
|
>
|
||||||
|
<div className="chat-page-header">
|
||||||
|
<UserPreview
|
||||||
|
user_id={to_user_id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
isRemoteTyping && <p>Typing...</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
"chat-page-messages",
|
||||||
|
{
|
||||||
|
["empty"]: messages.length === 0
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
ref={messagesRef}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
messages.length === 0 && <antd.Empty />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
messages.map((line, index) => {
|
||||||
|
return <div
|
||||||
|
key={index}
|
||||||
|
className={classnames(
|
||||||
|
"chat-page-line-wrapper",
|
||||||
|
{
|
||||||
|
["self"]: line.user._id === app.userData._id
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="chat-page-line">
|
||||||
|
<div
|
||||||
|
className="chat-page-line-avatar"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={line.user.avatar}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{line.user.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="chat-page-line-text"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{line.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="chat-page-input-wrapper">
|
||||||
|
<div className="chat-page-input">
|
||||||
|
<antd.Input
|
||||||
|
placeholder="Enter message"
|
||||||
|
value={currentText}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onPressEnter={submitMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatPage
|
108
packages/app/src/pages/messages/[to_user_id]/index.less
Normal file
108
packages/app/src/pages/messages/[to_user_id]/index.less
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
.chat-page {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 90vh;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.chat-page-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-page-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
height: 80vh;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
|
||||||
|
overflow-y: overlay;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-page-line-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&.self {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-page-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--background-color-primary);
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.chat-page-line-avatar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-page-line-text {
|
||||||
|
p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-break: break-all;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-page-input-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.chat-page-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
packages/app/src/pages/messages/index.jsx
Normal file
13
packages/app/src/pages/messages/index.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const MessagesPage = (props) => {
|
||||||
|
return <div
|
||||||
|
className="messages-page"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessagesPage
|
0
packages/app/src/pages/messages/index.less
Normal file
0
packages/app/src/pages/messages/index.less
Normal file
@ -7,10 +7,10 @@ import MusicService from "@models/music"
|
|||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const TrackPage = (props) => {
|
const Item = (props) => {
|
||||||
const { id } = props.params
|
const { type, id } = props.params
|
||||||
|
|
||||||
const [loading, result, error, makeRequest] = app.cores.api.useRequest(MusicService.getTrackData, id)
|
const [loading, result, error, makeRequest] = app.cores.api.useRequest(MusicService.getReleaseData, id)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <antd.Result
|
return <antd.Result
|
||||||
@ -26,15 +26,11 @@ const TrackPage = (props) => {
|
|||||||
|
|
||||||
return <div className="track-page">
|
return <div className="track-page">
|
||||||
<PlaylistView
|
<PlaylistView
|
||||||
playlist={{
|
playlist={result}
|
||||||
title: result.title,
|
|
||||||
cover: result.cover_url,
|
|
||||||
list: [result]
|
|
||||||
}}
|
|
||||||
centered={app.isMobile}
|
centered={app.isMobile}
|
||||||
hasMore={false}
|
hasMore={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TrackPage
|
export default Item
|
3
packages/app/src/pages/music/[type]/[id]/index.less
Normal file
3
packages/app/src/pages/music/[type]/[id]/index.less
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.track-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
.track-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
@ -3,7 +3,6 @@ import * as antd from "antd"
|
|||||||
import { FloatingPanel } from "antd-mobile"
|
import { FloatingPanel } from "antd-mobile"
|
||||||
|
|
||||||
import PostCard from "@components/PostCard"
|
import PostCard from "@components/PostCard"
|
||||||
import CommentsCard from "@components/CommentsCard"
|
|
||||||
|
|
||||||
import Post from "@models/post"
|
import Post from "@models/post"
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ export default (props) => {
|
|||||||
<PostCard data={data} fullmode />
|
<PostCard data={data} fullmode />
|
||||||
|
|
||||||
<FloatingPanel anchors={floatingPanelAnchors}>
|
<FloatingPanel anchors={floatingPanelAnchors}>
|
||||||
<CommentsCard post_id={post_id} />
|
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// Patch global prototypes
|
// Patch global prototypes
|
||||||
import { Buffer } from "buffer"
|
import { Buffer } from "buffer"
|
||||||
|
|
||||||
|
globalThis.IS_MOBILE_HOST = window.navigator.userAgent === "capacitor"
|
||||||
|
|
||||||
window.Buffer = Buffer
|
window.Buffer = Buffer
|
||||||
|
|
||||||
Array.prototype.findAndUpdateObject = function (discriminator, obj) {
|
Array.prototype.findAndUpdateObject = function (discriminator, obj) {
|
||||||
|
@ -50,6 +50,7 @@ const generateRoutes = () => {
|
|||||||
.replace(/\/src\/pages|index|\.mobile|\.jsx$/g, "")
|
.replace(/\/src\/pages|index|\.mobile|\.jsx$/g, "")
|
||||||
.replace(/\/src\/pages|index|\.mobile|\.tsx$/g, "")
|
.replace(/\/src\/pages|index|\.mobile|\.tsx$/g, "")
|
||||||
|
|
||||||
|
path = path.replace(/\[([a-z]+)\]/g, ":$1")
|
||||||
path = path.replace(/\[\.{3}.+\]/, "*").replace(/\[(.+)\]/, ":$1")
|
path = path.replace(/\[\.{3}.+\]/, "*").replace(/\[(.+)\]/, ":$1")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -59,15 +60,8 @@ const generateRoutes = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePageElementWrapper(route, element, bindProps) {
|
function findRouteDeclaration(route) {
|
||||||
return React.createElement((props) => {
|
return routesDeclaration.find((layout) => {
|
||||||
const params = useParams()
|
|
||||||
const url = new URL(window.location)
|
|
||||||
const query = new Proxy(url, {
|
|
||||||
get: (target, prop) => target.searchParams.get(prop),
|
|
||||||
})
|
|
||||||
|
|
||||||
const routeDeclaration = routesDeclaration.find((layout) => {
|
|
||||||
const routePath = layout.path.replace(/\*/g, ".*").replace(/!/g, "^")
|
const routePath = layout.path.replace(/\*/g, ".*").replace(/!/g, "^")
|
||||||
|
|
||||||
return new RegExp(routePath).test(route)
|
return new RegExp(routePath).test(route)
|
||||||
@ -75,68 +69,79 @@ function generatePageElementWrapper(route, element, bindProps) {
|
|||||||
path: route,
|
path: route,
|
||||||
useLayout: "default",
|
useLayout: "default",
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
route = route.replace(/\?.+$/, "").replace(/\/{2,}/g, "/")
|
function isAuthenticated() {
|
||||||
route = route.replace(/\/$/, "")
|
return !!app.userData
|
||||||
|
}
|
||||||
|
|
||||||
if (routeDeclaration) {
|
function handleRouteDeclaration(declaration) {
|
||||||
if (!bindProps.user && (window.location.pathname !== config.app?.authPath)) {
|
React.useEffect(() => {
|
||||||
if (!routeDeclaration.public) {
|
if (declaration) {
|
||||||
|
// if not authenticated and is not in public route, redirect
|
||||||
|
if (!isAuthenticated() && !declaration.public && (window.location.pathname !== config.app?.authPath)) {
|
||||||
if (typeof window.app.location.push === "function") {
|
if (typeof window.app.location.push === "function") {
|
||||||
window.app.location.push(config.app?.authPath ?? "/login")
|
window.app.location.push(config.app?.authPath ?? "/login")
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
|
|
||||||
|
app.cores.notifications.new({
|
||||||
|
title: "Please login to use this feature.",
|
||||||
|
duration: 15,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
window.location.href = config.app?.authPath ?? "/login"
|
window.location.href = config.app?.authPath ?? "/login"
|
||||||
|
|
||||||
return <div />
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (declaration.useLayout) {
|
||||||
|
app.layout.set(declaration.useLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routeDeclaration.useLayout) {
|
if (typeof declaration.centeredContent !== "undefined") {
|
||||||
app.layout.set(routeDeclaration.useLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof routeDeclaration.centeredContent !== "undefined") {
|
|
||||||
let finalBool = null
|
let finalBool = null
|
||||||
|
|
||||||
if (typeof routeDeclaration.centeredContent === "boolean") {
|
if (typeof declaration.centeredContent === "boolean") {
|
||||||
finalBool = routeDeclaration.centeredContent
|
finalBool = declaration.centeredContent
|
||||||
} else {
|
} else {
|
||||||
if (app.isMobile) {
|
if (app.isMobile) {
|
||||||
finalBool = routeDeclaration.centeredContent?.mobile ?? null
|
finalBool = declaration.centeredContent?.mobile ?? null
|
||||||
} else {
|
} else {
|
||||||
finalBool = routeDeclaration.centeredContent?.desktop ?? null
|
finalBool = declaration.centeredContent?.desktop ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.layout.toggleCenteredContent(finalBool)
|
app.layout.toggleCenteredContent(finalBool)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof routeDeclaration.useTitle !== "undefined") {
|
if (typeof declaration.useTitle !== "undefined") {
|
||||||
if (typeof routeDeclaration.useTitle === "function") {
|
if (typeof declaration.useTitle === "function") {
|
||||||
routeDeclaration.useTitle = routeDeclaration.useTitle(route, params)
|
declaration.useTitle = declaration.useTitle(path, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = `${routeDeclaration.useTitle} - ${config.app.siteName}`
|
document.title = `${declaration.useTitle} - ${config.app.siteName}`
|
||||||
} else {
|
} else {
|
||||||
document.title = config.app.siteName
|
document.title = config.app.siteName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof routeDeclaration?.mobileTopBarSpacer === "boolean" && app.isMobile) {
|
|
||||||
app.layout.toggleTopBarSpacer(routeDeclaration.mobileTopBarSpacer)
|
|
||||||
} else {
|
|
||||||
app.layout.toggleTopBarSpacer(false)
|
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePageElementWrapper(path, element, props, declaration) {
|
||||||
|
return React.createElement((props) => {
|
||||||
|
const params = useParams()
|
||||||
|
const url = new URL(window.location)
|
||||||
|
const query = new Proxy(url, {
|
||||||
|
get: (target, prop) => target.searchParams.get(prop),
|
||||||
|
})
|
||||||
|
|
||||||
|
handleRouteDeclaration(declaration)
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
loadable(element, {
|
loadable(element, {
|
||||||
fallback: React.createElement(bindProps.staticRenders?.PageLoad || DefaultLoadingRender),
|
fallback: React.createElement(props.staticRenders?.PageLoad || DefaultLoadingRender),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
...bindProps,
|
...props,
|
||||||
url: url,
|
url: url,
|
||||||
params: params,
|
params: params,
|
||||||
query: query,
|
query: query,
|
||||||
@ -160,36 +165,22 @@ const NavigationController = (props) => {
|
|||||||
state = {}
|
state = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionDuration = app.cores.style.getValue("page-transition-duration") ?? "250ms"
|
|
||||||
|
|
||||||
state.transitionDelay = Number(transitionDuration.replace("ms", ""))
|
|
||||||
|
|
||||||
app.eventBus.emit("router.navigate", to, {
|
app.eventBus.emit("router.navigate", to, {
|
||||||
state,
|
state,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.location.last = window.location
|
app.location.last = window.location
|
||||||
|
|
||||||
if (state.transitionDelay >= 100) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, state.transitionDelay))
|
|
||||||
}
|
|
||||||
|
|
||||||
return navigate(to, {
|
return navigate(to, {
|
||||||
state
|
state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function backLocation() {
|
async function backLocation() {
|
||||||
const transitionDuration = app.cores.style.getValue("page-transition-duration") ?? "250ms"
|
|
||||||
|
|
||||||
app.eventBus.emit("router.navigate")
|
app.eventBus.emit("router.navigate")
|
||||||
|
|
||||||
app.location.last = window.location
|
app.location.last = window.location
|
||||||
|
|
||||||
if (transitionDuration >= 100) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, transitionDuration))
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.history.back()
|
return window.history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,10 +211,12 @@ export const PageRender = React.memo((props) => {
|
|||||||
return <Routes>
|
return <Routes>
|
||||||
{
|
{
|
||||||
routes.map((route, index) => {
|
routes.map((route, index) => {
|
||||||
|
const declaration = findRouteDeclaration(route.path)
|
||||||
|
|
||||||
return <Route
|
return <Route
|
||||||
key={index}
|
key={index}
|
||||||
path={route.path}
|
path={route.path}
|
||||||
element={generatePageElementWrapper(route.path, route.element, props)}
|
element={generatePageElementWrapper(route.path, route.element, props, declaration)}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
@ -106,7 +106,8 @@ export default {
|
|||||||
id: "style.uiFontScale",
|
id: "style.uiFontScale",
|
||||||
group: "aspect",
|
group: "aspect",
|
||||||
component: "Slider",
|
component: "Slider",
|
||||||
title: "UI font scale",
|
icon: "MdFormatSize",
|
||||||
|
title: "Font scale",
|
||||||
description: "Change the font scale of the application.",
|
description: "Change the font scale of the application.",
|
||||||
props: {
|
props: {
|
||||||
min: 1,
|
min: 1,
|
||||||
@ -133,7 +134,7 @@ export default {
|
|||||||
group: "aspect",
|
group: "aspect",
|
||||||
component: "Select",
|
component: "Select",
|
||||||
icon: "MdOutlineFontDownload",
|
icon: "MdOutlineFontDownload",
|
||||||
title: "UI font",
|
title: "Font family",
|
||||||
description: "Change the font of the application.",
|
description: "Change the font of the application.",
|
||||||
props: {
|
props: {
|
||||||
style: {
|
style: {
|
||||||
@ -178,18 +179,7 @@ export default {
|
|||||||
},
|
},
|
||||||
storaged: false,
|
storaged: false,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: "style.parallaxBackground",
|
|
||||||
// group: "aspect",
|
|
||||||
// component: "Switch",
|
|
||||||
// icon: "MdOutline3DRotation",
|
|
||||||
// title: "Parallax background",
|
|
||||||
// description: "Create a parallax effect on the background.",
|
|
||||||
// dependsOn: {
|
|
||||||
// "style.backgroundImage": true
|
|
||||||
// },
|
|
||||||
// storaged: true,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "style.backgroundImage",
|
id: "style.backgroundImage",
|
||||||
group: "aspect",
|
group: "aspect",
|
||||||
@ -225,27 +215,6 @@ export default {
|
|||||||
},
|
},
|
||||||
storaged: false,
|
storaged: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "style.backgroundPattern",
|
|
||||||
group: "aspect",
|
|
||||||
icon: "MdGrid4X4",
|
|
||||||
component: loadable(() => import("../components/backgroundSelector")),
|
|
||||||
title: "Background pattern",
|
|
||||||
description: "Change background pattern of the application.",
|
|
||||||
extraActions: [
|
|
||||||
{
|
|
||||||
id: "remove",
|
|
||||||
icon: "Delete",
|
|
||||||
title: "Remove",
|
|
||||||
onClick: () => {
|
|
||||||
app.cores.style.modify({
|
|
||||||
backgroundSVG: "unset"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
storaged: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "style.backgroundBlur",
|
id: "style.backgroundBlur",
|
||||||
group: "aspect",
|
group: "aspect",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@buttonsBorderRadius: 9px;
|
@import "./vars.less";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--adm-color-background: var(--background-color-primary);
|
--adm-color-background: var(--background-color-primary);
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
/* Default */
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
|
|
||||||
|
|
||||||
/* PostMessage */
|
|
||||||
@import url('https://fonts.googleapis.com/css?family=Poppins:300,300i,500,500i,700');
|
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css?family=Alata&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css?family=Kulim+Park&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Recursive:wght@300;400;500;600;700;800;900&display=swap');
|
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
|
||||||
|
|
||||||
@import url('https://fonts.cdnfonts.com/css/mona-sans');
|
|
||||||
@import url('https://fonts.cdnfonts.com/css/hubot-sans');
|
|
10
packages/app/src/styles/fonts.less
Executable file
10
packages/app/src/styles/fonts.less
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
/* Selectable fonts for users */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
|
||||||
|
|
||||||
|
/* Required secondary fonts */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600&display=swap');
|
||||||
|
|
||||||
|
/* Disabled fonts */
|
||||||
|
/* @import url('https://fonts.googleapis.com/css2?family=Recursive:wght@300;400;500;600;700;800;900&display=swap'); */
|
@ -1,6 +1,6 @@
|
|||||||
@import "@styles/animations.less";
|
@import "@styles/animations.less";
|
||||||
@import "@styles/vars.less";
|
@import "@styles/vars.less";
|
||||||
@import "@styles/fonts.css";
|
@import "@styles/fonts.less";
|
||||||
@import "@styles/fixments.less";
|
@import "@styles/fixments.less";
|
||||||
@import "@styles/mobile.less";
|
@import "@styles/mobile.less";
|
||||||
@import "@styles/splash.less";
|
@import "@styles/splash.less";
|
||||||
|
@ -20,5 +20,4 @@
|
|||||||
@bottomBar_iconSize: 45px;
|
@bottomBar_iconSize: 45px;
|
||||||
@topBar_height: 52px;
|
@topBar_height: 52px;
|
||||||
|
|
||||||
@modal_background_blur: 10px;
|
@modal_background_blur: 2px;
|
||||||
|
|
@ -1,35 +1,53 @@
|
|||||||
export default (uri, filename) => {
|
import axios from "axios"
|
||||||
app.message.info("Downloading media...")
|
import mime from "mime"
|
||||||
|
|
||||||
fetch(uri, {
|
export default async (uri) => {
|
||||||
method: "GET",
|
const key = `download-${uri}`
|
||||||
|
console.log(`[UTIL] Downloading ${uri}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
app.cores.notifications.new({
|
||||||
|
key: key,
|
||||||
|
title: "Downloading",
|
||||||
|
duration: 0,
|
||||||
|
type: "loading",
|
||||||
|
closable: false,
|
||||||
|
feedback: false,
|
||||||
})
|
})
|
||||||
.then((response) => response.blob())
|
|
||||||
.then((blob) => {
|
|
||||||
if (!filename) {
|
|
||||||
filename = uri.split("/").pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create blob link to download
|
const metadata = await axios({
|
||||||
const url = window.URL.createObjectURL(new Blob([blob]))
|
method: "HEAD",
|
||||||
|
url: uri,
|
||||||
|
})
|
||||||
|
|
||||||
|
const extension = mime.getExtension(metadata.headers["content-type"])
|
||||||
|
const filename = `${metadata.headers["x-amz-meta-file-hash"]}.${extension}`
|
||||||
|
|
||||||
|
const content = await axios({
|
||||||
|
method: "GET",
|
||||||
|
url: uri,
|
||||||
|
responseType: "blob",
|
||||||
|
})
|
||||||
|
|
||||||
|
const file = new File([content.data], filename, {
|
||||||
|
name: filename,
|
||||||
|
type: metadata.headers["content-type"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
|
||||||
const link = document.createElement("a")
|
const link = document.createElement("a")
|
||||||
|
|
||||||
link.href = url
|
link.href = url
|
||||||
|
link.download = file.name
|
||||||
link.setAttribute("download", filename)
|
|
||||||
|
|
||||||
// Append to html link element page
|
|
||||||
document.body.appendChild(link)
|
|
||||||
|
|
||||||
// Start download
|
|
||||||
link.click()
|
link.click()
|
||||||
|
|
||||||
// Clean up and remove the link
|
setTimeout(() => {
|
||||||
link.parentNode.removeChild(link)
|
app.cores.notifications.close(key)
|
||||||
})
|
}, 1000)
|
||||||
.catch((error) => {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
app.message.error("Failed to download media")
|
|
||||||
})
|
app.cores.notifications.close(key)
|
||||||
|
}
|
||||||
}
|
}
|
24
packages/app/src/utils/useMaxScreen/index.js
Normal file
24
packages/app/src/utils/useMaxScreen/index.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const enterPlayerAnimation = () => {
|
||||||
|
app.cores.style.applyVariant("dark")
|
||||||
|
app.cores.style.compactMode(true)
|
||||||
|
app.layout.toggleCenteredContent(false)
|
||||||
|
app.controls.toggleUIVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitPlayerAnimation = () => {
|
||||||
|
app.cores.style.applyInitialVariant()
|
||||||
|
app.cores.style.compactMode(false)
|
||||||
|
app.controls.toggleUIVisibility(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
enterPlayerAnimation()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
exitPlayerAnimation()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
11
packages/server/db_models/chatMessage/index.js
Normal file
11
packages/server/db_models/chatMessage/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
name: "ChatMessage",
|
||||||
|
collection: "chats_messages",
|
||||||
|
schema: {
|
||||||
|
type: { type: String, required: true },
|
||||||
|
from_user_id: { type: String, required: true },
|
||||||
|
to_user_id: { type: String, required: true },
|
||||||
|
content: { type: String, required: true },
|
||||||
|
created_at: { type: Date, required: true },
|
||||||
|
}
|
||||||
|
}
|
13
packages/server/db_models/musicLyrics/index.js
Normal file
13
packages/server/db_models/musicLyrics/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
name: "TrackLyric",
|
||||||
|
collection: "tracks_lyrics",
|
||||||
|
schema: {
|
||||||
|
track_id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lrc: {
|
||||||
|
type: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
packages/server/db_models/recentChat/index.js
Normal file
9
packages/server/db_models/recentChat/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
name: "RecentChat",
|
||||||
|
collection: "recent_chats",
|
||||||
|
schema: {
|
||||||
|
type: { type: String, required: true },
|
||||||
|
user_id: { type: String, required: true },
|
||||||
|
chat_id: { type: String, required: true },
|
||||||
|
}
|
||||||
|
}
|
@ -189,6 +189,7 @@ export default class Gateway {
|
|||||||
},
|
},
|
||||||
onReload: async ({ id, service, cwd, }) => {
|
onReload: async ({ id, service, cwd, }) => {
|
||||||
console.log(`[onReload] ${id} ${service}`)
|
console.log(`[onReload] ${id} ${service}`)
|
||||||
|
|
||||||
let instance = this.instancePool.find((instance) => instance.id === id)
|
let instance = this.instancePool.find((instance) => instance.id === id)
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
@ -209,7 +210,7 @@ export default class Gateway {
|
|||||||
// try to unregister from proxy
|
// try to unregister from proxy
|
||||||
this.proxy.unregisterAllFromService(id)
|
this.proxy.unregisterAllFromService(id)
|
||||||
|
|
||||||
instance.instance.kill()
|
await instance.instance.kill("SIGINT")
|
||||||
|
|
||||||
instance.instance = await spawnService({
|
instance.instance = await spawnService({
|
||||||
id,
|
id,
|
||||||
|
@ -13,7 +13,7 @@ export default async (socket, token, err) => {
|
|||||||
return err(`auth:token_invalid`)
|
return err(`auth:token_invalid`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = await User.findById(validation.data.user_id).catch((err) => {
|
let userData = await User.findById(validation.data.user_id).catch((err) => {
|
||||||
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
|
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@ -23,6 +23,9 @@ export default async (socket, token, err) => {
|
|||||||
return err(`auth:user_failed`)
|
return err(`auth:user_failed`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userData = userData.toObject()
|
||||||
|
userData._id = userData._id.toString()
|
||||||
|
|
||||||
socket.userData = userData
|
socket.userData = userData
|
||||||
socket.token = token
|
socket.token = token
|
||||||
socket.session = validation.data
|
socket.session = validation.data
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"linebridge": "^0.18.1",
|
"linebridge": "^0.18.1",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
|
"nodejs-snowflake": "^2.0.1",
|
||||||
"signal-exit": "^4.1.0",
|
"signal-exit": "^4.1.0",
|
||||||
"spinnies": "^0.5.1",
|
"spinnies": "^0.5.1",
|
||||||
"tree-kill": "^1.2.2"
|
"tree-kill": "^1.2.2"
|
||||||
|
@ -1,259 +0,0 @@
|
|||||||
import socketio from "socket.io"
|
|
||||||
|
|
||||||
import withWsAuth from "@middlewares/withWsAuth"
|
|
||||||
|
|
||||||
function generateFnHandler(fn, socket) {
|
|
||||||
return async (...args) => {
|
|
||||||
if (typeof socket === "undefined") {
|
|
||||||
socket = arguments[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fn(socket, ...args)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[HANDLER_ERROR] ${error.message} >`, error.stack)
|
|
||||||
|
|
||||||
if (typeof socket.emit !== "function") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return socket.emit("error", {
|
|
||||||
message: error.message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Room {
|
|
||||||
constructor(io, roomName) {
|
|
||||||
if (!io) {
|
|
||||||
throw new Error("io is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
this.io = io
|
|
||||||
this.roomName = roomName
|
|
||||||
}
|
|
||||||
|
|
||||||
connections = []
|
|
||||||
|
|
||||||
limitations = {
|
|
||||||
maxMessageLength: 540,
|
|
||||||
}
|
|
||||||
|
|
||||||
events = {
|
|
||||||
"room:send:message": (socket, payload) => {
|
|
||||||
let { message } = payload
|
|
||||||
|
|
||||||
if (!message || typeof message !== "string") {
|
|
||||||
return socket.emit("error", {
|
|
||||||
message: "Invalid message",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.length > this.limitations.maxMessageLength) {
|
|
||||||
message = message.substring(0, this.limitations.maxMessageLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.io.to(this.roomName).emit("room:recive:message", {
|
|
||||||
timestamp: payload.timestamp ?? Date.now(),
|
|
||||||
content: String(message),
|
|
||||||
user: {
|
|
||||||
user_id: socket.userData._id,
|
|
||||||
username: socket.userData.username,
|
|
||||||
fullName: socket.userData.fullName,
|
|
||||||
avatar: socket.userData.avatar,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
join = (socket) => {
|
|
||||||
if (socket.connectedRoom) {
|
|
||||||
console.warn(`[${socket.id}][@${socket.userData.username}] already connected to room ${socket.connectedRoom}`)
|
|
||||||
|
|
||||||
this.leave(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.connectedRoom = this.roomName
|
|
||||||
|
|
||||||
// join room
|
|
||||||
socket.join(this.roomName)
|
|
||||||
|
|
||||||
// add to connections
|
|
||||||
this.connections.push(socket)
|
|
||||||
|
|
||||||
// emit to self
|
|
||||||
socket.emit("room:joined", {
|
|
||||||
room: this.roomName,
|
|
||||||
limitations: this.limitations,
|
|
||||||
connectedUsers: this.connections.map((socket_conn) => {
|
|
||||||
return socket_conn.userData._id
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
// emit to others
|
|
||||||
this.io.to(this.roomName).emit("room:user:joined", {
|
|
||||||
user: {
|
|
||||||
user_id: socket.userData._id,
|
|
||||||
username: socket.userData.username,
|
|
||||||
fullName: socket.userData.fullName,
|
|
||||||
avatar: socket.userData.avatar,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const [event, fn] of Object.entries(this.events)) {
|
|
||||||
const handler = generateFnHandler(fn, socket)
|
|
||||||
|
|
||||||
if (!Array.isArray(socket.handlers)) {
|
|
||||||
socket.handlers = []
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.handlers.push([event, handler])
|
|
||||||
|
|
||||||
socket.on(event, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[${socket.id}][@${socket.userData.username}] joined room ${this.roomName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
leave = (socket) => {
|
|
||||||
if (!socket.connectedRoom) {
|
|
||||||
console.warn(`[${socket.id}][@${socket.userData.username}] not connected to any room`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket.connectedRoom !== this.roomName) {
|
|
||||||
console.warn(`[${socket.id}][@${socket.userData.username}] not connected to room ${this.roomName}, cannot leave`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.leave(this.roomName)
|
|
||||||
|
|
||||||
this.connections.splice(this.connections.indexOf(socket), 1)
|
|
||||||
|
|
||||||
socket.emit("room:left", {
|
|
||||||
room: this.roomName,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.io.to(this.roomName).emit("room:user:left", {
|
|
||||||
user: {
|
|
||||||
user_id: socket.userData._id,
|
|
||||||
username: socket.userData.username,
|
|
||||||
fullName: socket.userData.fullName,
|
|
||||||
avatar: socket.userData.avatar,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const [event, handler] of socket.handlers) {
|
|
||||||
socket.off(event, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[${socket.id}][@${socket.userData.username}] left room ${this.roomName}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RoomsController {
|
|
||||||
constructor(io) {
|
|
||||||
if (!io) {
|
|
||||||
throw new Error("io is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
this.io = io
|
|
||||||
}
|
|
||||||
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
checkRoomExists = (roomName) => {
|
|
||||||
return this.rooms.some((room) => room.roomName === roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoom = async (roomName) => {
|
|
||||||
if (this.checkRoomExists(roomName)) {
|
|
||||||
throw new Error(`Room ${roomName} already exists`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = new Room(this.io, roomName)
|
|
||||||
|
|
||||||
this.rooms.push(room)
|
|
||||||
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
|
|
||||||
connectSocketToRoom = async (socket, roomName) => {
|
|
||||||
if (!this.checkRoomExists(roomName)) {
|
|
||||||
await this.createRoom(roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = this.rooms.find((room) => room.roomName === roomName)
|
|
||||||
|
|
||||||
return room.join(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectSocketFromRoom = async (socket, roomName) => {
|
|
||||||
if (!this.checkRoomExists(roomName)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = this.rooms.find((room) => room.roomName === roomName)
|
|
||||||
|
|
||||||
return room.leave(socket)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ChatServer {
|
|
||||||
constructor(server) {
|
|
||||||
this.io = socketio(server, {
|
|
||||||
cors: {
|
|
||||||
origin: "*",
|
|
||||||
methods: ["GET", "POST"],
|
|
||||||
credentials: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (global.ioAdapter) {
|
|
||||||
this.io.adapter(global.ioAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.RoomsController = new RoomsController(this.io)
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionPool = []
|
|
||||||
|
|
||||||
events = {
|
|
||||||
"connection": (socket) => {
|
|
||||||
console.log(`[${socket.id}][${socket.userData.username}] connected to hub.`)
|
|
||||||
|
|
||||||
this.connectionPool.push(socket)
|
|
||||||
|
|
||||||
socket.on("disconnect", () => this.events.disconnect)
|
|
||||||
|
|
||||||
// Rooms
|
|
||||||
socket.on("join:room", (data) => this.RoomsController.connectSocketToRoom(socket, data.room))
|
|
||||||
socket.on("leave:room", (data) => this.RoomsController.disconnectSocketFromRoom(socket, data?.room ?? socket.connectedRoom))
|
|
||||||
},
|
|
||||||
"disconnect": (socket) => {
|
|
||||||
console.log(`[${socket.id}][@${socket.userData.username}] disconnected to hub.`)
|
|
||||||
|
|
||||||
if (socket.connectedRoom) {
|
|
||||||
this.Rooms.leave(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove from connection pool
|
|
||||||
this.connectionPool = this.connectionPool.filter((client) => client.id !== socket.id)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize = async () => {
|
|
||||||
this.io.use(withWsAuth)
|
|
||||||
|
|
||||||
Object.entries(this.events).forEach(([event, handler]) => {
|
|
||||||
this.io.on(event, (socket) => {
|
|
||||||
try {
|
|
||||||
handler(socket)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,8 +9,8 @@ import SharedMiddlewares from "@shared-middlewares"
|
|||||||
class API extends Server {
|
class API extends Server {
|
||||||
static refName = "chats"
|
static refName = "chats"
|
||||||
static useEngine = "hyper-express"
|
static useEngine = "hyper-express"
|
||||||
static wsRoutesPath = `${__dirname}/ws_routes`
|
|
||||||
static routesPath = `${__dirname}/routes`
|
static routesPath = `${__dirname}/routes`
|
||||||
|
static wsRoutesPath = `${__dirname}/routes_ws`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3004
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3004
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
@ -44,7 +44,11 @@ class API extends Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
this.contexts.rooms = new RoomsController(this.engine.io)
|
if (!this.engine.ws) {
|
||||||
|
throw new Error(`Engine WS not found!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contexts.rooms = new RoomsController(this.engine.ws.io)
|
||||||
|
|
||||||
await this.contexts.db.initialize()
|
await this.contexts.db.initialize()
|
||||||
await this.contexts.redis.initialize()
|
await this.contexts.redis.initialize()
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import buildFunctionHandler from "@utils/buildFunctionHandler"
|
import buildFunctionHandler from "@utils/buildFunctionHandler"
|
||||||
|
import { Snowflake } from "nodejs-snowflake"
|
||||||
|
|
||||||
|
import { ChatMessage } from "@db_models"
|
||||||
|
|
||||||
export default class Room {
|
export default class Room {
|
||||||
constructor(io, roomID, options) {
|
constructor(io, roomID, options) {
|
||||||
@ -27,10 +30,10 @@ export default class Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
roomEvents = {
|
roomEvents = {
|
||||||
"room:change:owner": (client, payload) => {
|
"room:change:owner": async (client, payload) => {
|
||||||
throw new OperationError(500, "Not implemented")
|
throw new OperationError(500, "Not implemented")
|
||||||
},
|
},
|
||||||
"room:send:message": (client, payload) => {
|
"room:send:message": async (client, payload) => {
|
||||||
console.log(`[${this.roomID}] [@${client.userData.username}] sent message >`, payload)
|
console.log(`[${this.roomID}] [@${client.userData.username}] sent message >`, payload)
|
||||||
|
|
||||||
let { message } = payload
|
let { message } = payload
|
||||||
@ -43,7 +46,12 @@ export default class Room {
|
|||||||
message = message.substring(0, this.limitations.maxMessageLength)
|
message = message.substring(0, this.limitations.maxMessageLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const created_at = new Date().getTime()
|
||||||
|
|
||||||
|
const id = `msg:${client.userData._id}:${created_at}`
|
||||||
|
|
||||||
this.handlers.broadcastToMembers("room:message", {
|
this.handlers.broadcastToMembers("room:message", {
|
||||||
|
_id: id,
|
||||||
timestamp: payload.timestamp ?? Date.now(),
|
timestamp: payload.timestamp ?? Date.now(),
|
||||||
content: String(message),
|
content: String(message),
|
||||||
user: {
|
user: {
|
||||||
@ -53,6 +61,33 @@ export default class Room {
|
|||||||
avatar: client.userData.avatar,
|
avatar: client.userData.avatar,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (payload.route) {
|
||||||
|
const routeValues = payload.route.split(":")
|
||||||
|
|
||||||
|
console.log(routeValues)
|
||||||
|
|
||||||
|
if (routeValues.length > 0) {
|
||||||
|
const [type, to_id] = routeValues
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "user": {
|
||||||
|
const doc = await ChatMessage.create({
|
||||||
|
type: type,
|
||||||
|
from_user_id: client.userData._id,
|
||||||
|
to_user_id: to_id,
|
||||||
|
content: message,
|
||||||
|
created_at: created_at,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import { User, ChatMessage } from "@db_models"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
middlewares: ["withAuthentication"],
|
||||||
|
fn: async (req) => {
|
||||||
|
const { limit = 50, offset = 0, order = "asc" } = req.query
|
||||||
|
|
||||||
|
const id = req.params.chat_id
|
||||||
|
|
||||||
|
const [from_user_id, to_user_id] = [req.auth.session.user_id, id]
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
from_user_id: {
|
||||||
|
$in: [
|
||||||
|
from_user_id,
|
||||||
|
to_user_id
|
||||||
|
]
|
||||||
|
},
|
||||||
|
to_user_id: {
|
||||||
|
$in: [
|
||||||
|
from_user_id,
|
||||||
|
to_user_id
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_datas = await User.find({
|
||||||
|
_id: [
|
||||||
|
from_user_id,
|
||||||
|
to_user_id
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
user_datas = user_datas.map((user) => {
|
||||||
|
user = user.toObject()
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
_id: 0,
|
||||||
|
username: "Deleted User",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user._id = user._id.toString()
|
||||||
|
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
|
||||||
|
let history = await ChatMessage.find(query)
|
||||||
|
.sort({ created_at: order === "desc" ? -1 : 1 })
|
||||||
|
.skip(offset)
|
||||||
|
.limit(limit)
|
||||||
|
|
||||||
|
history = history.map(async (item) => {
|
||||||
|
item = item.toObject()
|
||||||
|
|
||||||
|
item.user = user_datas.find((user) => {
|
||||||
|
return user._id === item.from_user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
history = await Promise.all(history)
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: await ChatMessage.count(query),
|
||||||
|
offset: offset,
|
||||||
|
limit: limit,
|
||||||
|
order: order,
|
||||||
|
list: history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { ChatMessage } from "@db_models"
|
||||||
|
|
||||||
|
export default async (socket, payload, engine) => {
|
||||||
|
const created_at = new Date().getTime()
|
||||||
|
|
||||||
|
const [from_user_id, to_user_id] = [socket.userData._id, payload.to_user_id]
|
||||||
|
|
||||||
|
const targetSocket = await engine.find.socketByUserId(payload.to_user_id)
|
||||||
|
|
||||||
|
const wsMessageObj = {
|
||||||
|
...payload,
|
||||||
|
created_at: created_at,
|
||||||
|
user: socket.userData,
|
||||||
|
_id: `msg:${from_user_id}:${created_at}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await ChatMessage.create({
|
||||||
|
type: "user",
|
||||||
|
from_user_id: from_user_id,
|
||||||
|
to_user_id: to_user_id,
|
||||||
|
content: payload.content,
|
||||||
|
created_at: created_at,
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.emit("chat:receive:message", wsMessageObj)
|
||||||
|
|
||||||
|
if (targetSocket.emit) {
|
||||||
|
await targetSocket.emit("chat:receive:message", wsMessageObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
export default async (socket, payload, engine) => {
|
||||||
|
const from_user_id = socket.userData._id
|
||||||
|
const { to_user_id, is_typing } = payload
|
||||||
|
|
||||||
|
const targetSocket = await engine.find.socketByUserId(to_user_id)
|
||||||
|
|
||||||
|
if (targetSocket) {
|
||||||
|
await targetSocket.emit("chat:state:typing", {
|
||||||
|
is_typing: is_typing
|
||||||
|
})
|
||||||
|
|
||||||
|
// socket.pendingFunctions.push("chats:state:typing")
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// socket.emit("chats:state:typing", {
|
||||||
|
// is_typing: false,
|
||||||
|
// })
|
||||||
|
// }, 5000)
|
||||||
|
}
|
||||||
|
}
|
@ -11,16 +11,32 @@ export default {
|
|||||||
"withAuthentication",
|
"withAuthentication",
|
||||||
],
|
],
|
||||||
fn: async (req, res) => {
|
fn: async (req, res) => {
|
||||||
const providerType = req.headers["provider-type"]
|
|
||||||
|
|
||||||
const userPath = path.join(this.default.contexts.cache.constructor.cachePath, req.auth.session.user_id)
|
const userPath = path.join(this.default.contexts.cache.constructor.cachePath, req.auth.session.user_id)
|
||||||
|
|
||||||
const tmpPath = path.resolve(userPath)
|
const tmpPath = path.resolve(userPath)
|
||||||
|
|
||||||
let build = await ChunkFileUpload(req, {
|
const limits = {
|
||||||
tmpDir: tmpPath,
|
|
||||||
maxFileSize: parseInt(this.default.contexts.limits.maxFileSizeInMB) * 1024 * 1024,
|
maxFileSize: parseInt(this.default.contexts.limits.maxFileSizeInMB) * 1024 * 1024,
|
||||||
maxChunkSize: parseInt(this.default.contexts.limits.maxChunkSizeInMB) * 1024 * 1024,
|
maxChunkSize: parseInt(this.default.contexts.limits.maxChunkSizeInMB) * 1024 * 1024,
|
||||||
|
useCompression: true,
|
||||||
|
useProvider: "standard",
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await req.auth.user()
|
||||||
|
|
||||||
|
if (user.roles.includes("admin")) {
|
||||||
|
// maxFileSize for admins 100GB
|
||||||
|
limits.maxFileSize = 100 * 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
// optional compression for admins
|
||||||
|
limits.useCompression = req.headers["use-compression"] ?? false
|
||||||
|
|
||||||
|
limits.useProvider = req.headers["provider-type"] ?? "b2"
|
||||||
|
}
|
||||||
|
|
||||||
|
let build = await ChunkFileUpload(req, {
|
||||||
|
tmpDir: tmpPath,
|
||||||
|
...limits,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
throw new OperationError(err.code, err.message)
|
throw new OperationError(err.code, err.message)
|
||||||
})
|
})
|
||||||
@ -32,8 +48,8 @@ export default {
|
|||||||
const result = await RemoteUpload({
|
const result = await RemoteUpload({
|
||||||
parentDir: req.auth.session.user_id,
|
parentDir: req.auth.session.user_id,
|
||||||
source: build.filePath,
|
source: build.filePath,
|
||||||
service: providerType,
|
service: limits.useProvider,
|
||||||
useCompression: req.headers["use-compression"] ?? true,
|
useCompression: limits.useCompression,
|
||||||
cachePath: tmpPath,
|
cachePath: tmpPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ const AllowedUpdateFields = [
|
|||||||
"artist",
|
"artist",
|
||||||
"type",
|
"type",
|
||||||
"public",
|
"public",
|
||||||
|
"list",
|
||||||
]
|
]
|
||||||
|
|
||||||
export default class Release {
|
export default class Release {
|
||||||
@ -19,7 +20,7 @@ export default class Release {
|
|||||||
explicit: payload.explicit,
|
explicit: payload.explicit,
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
public: payload.public,
|
public: payload.public,
|
||||||
items: payload.items,
|
list: payload.list,
|
||||||
public: payload.public,
|
public: payload.public,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -6,12 +6,22 @@ import axios from "axios"
|
|||||||
export default async (payload = {}) => {
|
export default async (payload = {}) => {
|
||||||
requiredFields(["title", "source", "user_id"], payload)
|
requiredFields(["title", "source", "user_id"], payload)
|
||||||
|
|
||||||
const { data: stream, headers } = await axios({
|
let stream = null
|
||||||
|
let headers = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceStream = await axios({
|
||||||
url: payload.source,
|
url: payload.source,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
responseType: "stream",
|
responseType: "stream",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
stream = sourceStream.data
|
||||||
|
headers = sourceStream.headers
|
||||||
|
} catch (error) {
|
||||||
|
throw new OperationError(500, `Failed to process fetching source: ${error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
const fileMetadata = await MusicMetadata.parseStream(stream, {
|
const fileMetadata = await MusicMetadata.parseStream(stream, {
|
||||||
mimeType: headers["content-type"],
|
mimeType: headers["content-type"],
|
||||||
})
|
})
|
||||||
|
@ -20,7 +20,9 @@ export default async (track_id, { limit = 50, offset = 0 } = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const track = await Track.findById(track_id).catch(() => null)
|
const track = await Track.findOne({
|
||||||
|
_id: track_id
|
||||||
|
})
|
||||||
|
|
||||||
if (!track) {
|
if (!track) {
|
||||||
throw new OperationError(404, "Track not found")
|
throw new OperationError(404, "Track not found")
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user