mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
f871dd3c83
commit
93638f0fa3
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 Width: | Height: | 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 Width: | Height: | 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,79 +172,76 @@ 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}
|
||||||
>
|
>
|
||||||
<PostHeader
|
|
||||||
postData={this.state.data}
|
|
||||||
onDoubleClick={this.onDoubleClick}
|
|
||||||
disableReplyTag={this.props.disableReplyTag}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="post_content"
|
className="post_card_content"
|
||||||
className={classnames(
|
context-menu={"post-card"}
|
||||||
"post_content",
|
user-id={this.state.data.user_id}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="message">
|
<PostHeader
|
||||||
|
postData={this.state.data}
|
||||||
|
onDoubleClick={this.onDoubleClick}
|
||||||
|
disableReplyTag={this.props.disableReplyTag}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="post_content"
|
||||||
|
className={classnames(
|
||||||
|
"post_content",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="message">
|
||||||
|
{
|
||||||
|
processString(messageRegexs)(this.state.data.message ?? "")
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
processString(messageRegexs)(this.state.data.message ?? "")
|
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments
|
||||||
|
attachments={this.state.data.attachments}
|
||||||
|
flags={this.state.data.flags}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PostActions
|
||||||
|
user_id={this.state.data.user_id}
|
||||||
|
|
||||||
|
likesCount={this.state.countLikes}
|
||||||
|
repliesCount={this.state.countReplies}
|
||||||
|
|
||||||
|
defaultLiked={this.state.hasLiked}
|
||||||
|
defaultSaved={this.state.hasSaved}
|
||||||
|
|
||||||
|
actions={{
|
||||||
|
onClickLike: this.onClickLike,
|
||||||
|
onClickEdit: this.onClickEdit,
|
||||||
|
onClickDelete: this.onClickDelete,
|
||||||
|
onClickSave: this.onClickSave,
|
||||||
|
onClickReply: this.onClickReply,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments
|
!this.props.disableHasReplies && !!this.state.hasReplies && <div
|
||||||
attachments={this.state.data.attachments}
|
className="post-card-has_replies"
|
||||||
flags={this.state.data.flags}
|
onClick={() => app.navigation.goToPost(this.state.data._id)}
|
||||||
/>
|
>
|
||||||
|
<span>View {this.state.hasReplies} replies</span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostActions
|
</motion.article>
|
||||||
user_id={this.state.data.user_id}
|
|
||||||
|
|
||||||
likesCount={this.state.countLikes}
|
|
||||||
repliesCount={this.state.countReplies}
|
|
||||||
|
|
||||||
defaultLiked={this.state.hasLiked}
|
|
||||||
defaultSaved={this.state.hasSaved}
|
|
||||||
|
|
||||||
actions={{
|
|
||||||
onClickLike: this.onClickLike,
|
|
||||||
onClickEdit: this.onClickEdit,
|
|
||||||
onClickDelete: this.onClickDelete,
|
|
||||||
onClickSave: this.onClickSave,
|
|
||||||
onClickReply: this.onClickReply,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
!this.props.disableHasReplies && !!this.state.hasReplies && <div
|
|
||||||
className="post-card-has_replies"
|
|
||||||
onClick={() => app.navigation.goToPost(this.state.data._id)}
|
|
||||||
>
|
|
||||||
<span>View {this.state.hasReplies} replies</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</motion.div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"
|
||||||
@ -40,9 +39,4 @@ export { default as PostCreator } from "./PostCreator"
|
|||||||
export { default as UserBadges } from "./UserBadges"
|
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}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@ -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,144 +365,117 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBottomItems = () => {
|
||||||
|
const items = [
|
||||||
|
...BottomMenuDefaultItems,
|
||||||
|
...this.state.bottomItems,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (app.userData) {
|
||||||
|
items.push({
|
||||||
|
key: "account",
|
||||||
|
ignore_click: "true",
|
||||||
|
className: "user_avatar",
|
||||||
|
label: <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 }) => {
|
return <div
|
||||||
return <div
|
className={classnames(
|
||||||
className={classnames(
|
"app_sidebar_wrapper",
|
||||||
"app_sidebar_wrapper",
|
|
||||||
{
|
|
||||||
visible: this.state.visible,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
transform: `translateX(-${x}%)`,
|
|
||||||
}}
|
|
||||||
onMouseEnter={this.onMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
>
|
|
||||||
{
|
{
|
||||||
window.__TAURI__ && navigator.platform.includes("Mac") && <div
|
visible: this.state.visible,
|
||||||
className="app_sidebar_tauri"
|
|
||||||
data-tauri-drag-region
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transform: `translateX(-${x}%)`,
|
||||||
|
}}
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
window.__TAURI__ && navigator.platform.includes("Mac") && <div
|
||||||
|
className="app_sidebar_tauri"
|
||||||
|
data-tauri-drag-region
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"app_sidebar",
|
"app_sidebar",
|
||||||
{
|
{
|
||||||
["expanded"]: this.state.visible && this.state.expanded,
|
["expanded"]: this.state.visible && this.state.expanded,
|
||||||
["hidden"]: !this.state.visible,
|
["hidden"]: !this.state.visible,
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ref={this.sidebarRef}
|
)
|
||||||
>
|
}
|
||||||
|
ref={this.sidebarRef}
|
||||||
|
>
|
||||||
|
|
||||||
<div className="app_sidebar_header">
|
<div className="app_sidebar_header">
|
||||||
<div className="app_sidebar_header_logo">
|
<div className="app_sidebar_header_logo">
|
||||||
<img src={config.logo?.alt} />
|
<img src={config.logo?.alt} />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div key="menu" className="app_sidebar_menu_wrapper">
|
|
||||||
<Menu
|
|
||||||
mode="inline"
|
|
||||||
onClick={this.handleClick}
|
|
||||||
defaultSelectedKeys={[defaultSelectedKey]}
|
|
||||||
items={this.state.topItems}
|
|
||||||
selectable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
key="bottom"
|
|
||||||
className={classnames(
|
|
||||||
"app_sidebar_menu_wrapper",
|
|
||||||
"bottom"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
selectable={false}
|
|
||||||
mode="inline"
|
|
||||||
onClick={this.handleClick}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
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 key="menu" className="app_sidebar_menu_wrapper">
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
onClick={this.handleClick}
|
||||||
|
defaultSelectedKeys={[defaultSelectedKey]}
|
||||||
|
items={this.state.topItems}
|
||||||
|
selectable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
key="bottom"
|
||||||
|
className={classnames(
|
||||||
|
"app_sidebar_menu_wrapper",
|
||||||
|
"bottom"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
selectable={false}
|
||||||
|
mode="inline"
|
||||||
|
onClick={this.handleClick}
|
||||||
|
items={this.getBottomItems()}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}}
|
</div>
|
||||||
</Motion>
|
}}
|
||||||
</>
|
</Motion>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -173,15 +173,27 @@
|
|||||||
|
|
||||||
&.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;
|
||||||
|
@ -17,14 +17,14 @@ const MainSelector = (props) => {
|
|||||||
<div className="actions">
|
<div className="actions">
|
||||||
{
|
{
|
||||||
app.userData && <antd.Button
|
app.userData && <antd.Button
|
||||||
type="default"
|
type="default"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
app.navigation.goMain()
|
app.navigation.goMain()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Continue as {app.userData.username}
|
Continue as {app.userData.username}
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
|
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
|
786
packages/app/src/pages/lyrics/index.jsx
Executable file → Normal file
786
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) {
|
async function loadLyrics(track_id) {
|
||||||
value += `${values[i]}`
|
const result = await MusicService.getTrackLyrics(track_id)
|
||||||
continue
|
|
||||||
|
if (result) {
|
||||||
|
setLyrics(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
value += `${values[i]}, `
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
useMaxScreen()
|
||||||
|
|
||||||
|
//* Handle when context change track_manifest
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (context.track_manifest) {
|
||||||
|
loadLyrics(context.track_manifest._id)
|
||||||
|
}
|
||||||
|
}, [context.track_manifest])
|
||||||
|
|
||||||
|
//* Handle when lyrics data change
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log(lyrics)
|
||||||
|
}, [lyrics])
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classnames(
|
||||||
|
"lyrics",
|
||||||
|
{
|
||||||
|
["stopped"]: context.playback_status !== "playing",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LyricsVideo
|
||||||
|
ref={videoRef}
|
||||||
|
lyrics={lyrics}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LyricsText
|
||||||
|
ref={textRef}
|
||||||
|
lyrics={lyrics}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PlayerController
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateLineTime(line) {
|
const EnchancedLyricsPage = (props) => {
|
||||||
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) => {
|
|
||||||
app.cores.player.volume(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMute = () => {
|
|
||||||
app.cores.player.toggleMute()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
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(
|
|
||||||
"player_controller",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
ref={this.titleRef}
|
|
||||||
className={classnames(
|
|
||||||
"player_controller_info_title_text",
|
|
||||||
{
|
|
||||||
["overflown"]: this.state.titleOverflown,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
this.state.plabackState === "stopped" ? "Nothing is playing" : <>
|
|
||||||
{this.state.currentPlaying?.title ?? "Nothing is playing"}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
}
|
|
||||||
|
|
||||||
{this.state.titleOverflown &&
|
|
||||||
<Marquee
|
|
||||||
//gradient
|
|
||||||
//gradientColor={bgColor}
|
|
||||||
//gradientWidth={20}
|
|
||||||
play={this.state.plabackState !== "stopped"}
|
|
||||||
>
|
|
||||||
<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 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) => {
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
544
packages/app/src/pages/lyrics/index.less
Executable file → Normal file
544
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 {
|
||||||
//align-items: center;
|
filter: blur(6px);
|
||||||
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
padding: 50px 0;
|
|
||||||
|
|
||||||
overflow-y: hidden;
|
|
||||||
|
|
||||||
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-video {
|
||||||
.lyrics_viewer_cover {
|
z-index: 105;
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
height: 80vh; //fallback
|
|
||||||
height: 80dvh;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
bottom: 20vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_wrapper {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.player_controller {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
position: absolute;
|
||||||
|
|
||||||
z-index: 200;
|
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
|
|
||||||
backdrop-filter: blur(21px);
|
object-fit: cover;
|
||||||
-webkit-backdrop-filter: blur(21px)
|
|
||||||
|
transition: all 150ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyrics_viewer_video_canvas {
|
.lyrics-text-wrapper {
|
||||||
position: absolute;
|
z-index: 110;
|
||||||
top: 0;
|
position: fixed;
|
||||||
|
|
||||||
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 {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
z-index: 250;
|
padding: 60px;
|
||||||
|
|
||||||
display: flex;
|
.lyrics-text {
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
width: 600px;
|
||||||
justify-content: center;
|
height: 200px;
|
||||||
|
|
||||||
opacity: 0;
|
padding: 20px;
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
width: 0px;
|
overflow: hidden;
|
||||||
height: 0px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
background-color: rgba(var(--background-color-accent-values), 0.6);
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 25vw;
|
|
||||||
height: 25vw;
|
|
||||||
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 500px;
|
|
||||||
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
backdrop-filter: blur(5px);
|
||||||
}
|
-webkit-backdrop-filter: blur(5px);
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics_viewer_content {
|
.line {
|
||||||
z-index: 250;
|
font-size: 2rem;
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
.lyrics_viewer_lines {
|
opacity: 0.1;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
margin: 0;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 90%;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
font-family: "Space Grotesk", sans-serif;
|
|
||||||
|
|
||||||
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 {
|
&.current {
|
||||||
margin: 20px 0;
|
opacity: 1;
|
||||||
font-size: 2rem;
|
|
||||||
|
|
||||||
animation: spacing-letters var(--line-time) ease-in-out forwards;
|
|
||||||
animation-play-state: var(--line-animation-play-state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spacing-letters {
|
.lyrics-player-controller-wrapper {
|
||||||
0% {
|
position: fixed;
|
||||||
letter-spacing: 0.3rem;
|
z-index: 115;
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
bottom: 0;
|
||||||
letter-spacing: 0;
|
right: 0;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_wrapper {
|
padding: 60px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
position: absolute;
|
.lyrics-player-controller {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
color: var(--text-color);
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.player_controller_controls {
|
|
||||||
height: 8vh;
|
|
||||||
max-height: 100px;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_progress_wrapper {
|
|
||||||
bottom: 7px;
|
|
||||||
|
|
||||||
.player_controller_progress {
|
|
||||||
height: 10px;
|
|
||||||
|
|
||||||
width: 90%;
|
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_cover {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: @cover-width;
|
gap: 10px;
|
||||||
min-width: @cover-width;
|
|
||||||
max-width: @cover-width;
|
|
||||||
|
|
||||||
height: 100%;
|
width: 300px;
|
||||||
|
|
||||||
img {
|
padding: 30px;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
object-fit: cover;
|
border-radius: 12px;
|
||||||
|
|
||||||
border-radius: 12px;
|
backdrop-filter: blur(5px);
|
||||||
}
|
-webkit-backdrop-filter: blur(5px);
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_left {
|
background-color: rgba(var(--background-color-accent-values), 0.8);
|
||||||
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;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
.player_controller_info {
|
&:hover {
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.player-controls {
|
||||||
|
opacity: 1;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-player-controller-tags {
|
||||||
|
opacity: 1;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-player-controller-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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_controller_controls {
|
.player-controls {
|
||||||
display: flex;
|
opacity: 0;
|
||||||
flex-direction: row;
|
height: 0px;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
align-items: center;
|
.lyrics-player-controller-progress-wrapper {
|
||||||
justify-content: center;
|
width: 100%;
|
||||||
|
|
||||||
gap: 8px;
|
.lyrics-player-controller-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
padding: 10px;
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
height: 0px;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
.playButton {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.loadCircle {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
z-index: 330;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
align-self: center;
|
transition: all 150ms ease-in-out;
|
||||||
justify-self: center;
|
|
||||||
|
|
||||||
transform: scale(1.5);
|
border-radius: 12px;
|
||||||
|
|
||||||
svg {
|
background-color: rgba(var(--background-color-accent-values), 0.8);
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
path {
|
&:hover {
|
||||||
stroke: var(--text-color);
|
.lyrics-player-controller-progress-bar {
|
||||||
stroke-width: 1;
|
height: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lyrics-player-controller-progress-bar {
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.player_controller_progress_wrapper {
|
.lyrics-player-controller-tags {
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.player_controller_progress {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
height: 5px;
|
justify-content: center;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
margin: auto;
|
width: 100%;
|
||||||
|
height: 0px;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
.player_controller_progress_bar {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background-color: var(--background-color-contrast);
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bottom-to-top {
|
.videoDebugOverlay {
|
||||||
0% {
|
position: fixed;
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
top: 20px;
|
||||||
bottom: 20vh;
|
right: 20px;
|
||||||
|
|
||||||
|
z-index: 115;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
background-color: rgba(var(--background-color-accent-values), 0.8);
|
||||||
|
|
||||||
|
width: 200px;
|
||||||
|
height: fit-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
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%;
|
|
||||||
}
|
|
@ -16,7 +16,7 @@ const PostPage = (props) => {
|
|||||||
const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
|
const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
|
||||||
post_id,
|
post_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <antd.Result
|
return <antd.Result
|
||||||
status="warning"
|
status="warning"
|
||||||
|
@ -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,7 +60,72 @@ const generateRoutes = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePageElementWrapper(route, element, bindProps) {
|
function findRouteDeclaration(route) {
|
||||||
|
return routesDeclaration.find((layout) => {
|
||||||
|
const routePath = layout.path.replace(/\*/g, ".*").replace(/!/g, "^")
|
||||||
|
|
||||||
|
return new RegExp(routePath).test(route)
|
||||||
|
}) ?? {
|
||||||
|
path: route,
|
||||||
|
useLayout: "default",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthenticated() {
|
||||||
|
return !!app.userData
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRouteDeclaration(declaration) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
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") {
|
||||||
|
window.app.location.push(config.app?.authPath ?? "/login")
|
||||||
|
|
||||||
|
app.cores.notifications.new({
|
||||||
|
title: "Please login to use this feature.",
|
||||||
|
duration: 15,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
window.location.href = config.app?.authPath ?? "/login"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (declaration.useLayout) {
|
||||||
|
app.layout.set(declaration.useLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof declaration.centeredContent !== "undefined") {
|
||||||
|
let finalBool = null
|
||||||
|
|
||||||
|
if (typeof declaration.centeredContent === "boolean") {
|
||||||
|
finalBool = declaration.centeredContent
|
||||||
|
} else {
|
||||||
|
if (app.isMobile) {
|
||||||
|
finalBool = declaration.centeredContent?.mobile ?? null
|
||||||
|
} else {
|
||||||
|
finalBool = declaration.centeredContent?.desktop ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.layout.toggleCenteredContent(finalBool)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof declaration.useTitle !== "undefined") {
|
||||||
|
if (typeof declaration.useTitle === "function") {
|
||||||
|
declaration.useTitle = declaration.useTitle(path, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = `${declaration.useTitle} - ${config.app.siteName}`
|
||||||
|
} else {
|
||||||
|
document.title = config.app.siteName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePageElementWrapper(path, element, props, declaration) {
|
||||||
return React.createElement((props) => {
|
return React.createElement((props) => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const url = new URL(window.location)
|
const url = new URL(window.location)
|
||||||
@ -67,76 +133,15 @@ function generatePageElementWrapper(route, element, bindProps) {
|
|||||||
get: (target, prop) => target.searchParams.get(prop),
|
get: (target, prop) => target.searchParams.get(prop),
|
||||||
})
|
})
|
||||||
|
|
||||||
const routeDeclaration = routesDeclaration.find((layout) => {
|
handleRouteDeclaration(declaration)
|
||||||
const routePath = layout.path.replace(/\*/g, ".*").replace(/!/g, "^")
|
|
||||||
|
|
||||||
return new RegExp(routePath).test(route)
|
|
||||||
}) ?? {
|
|
||||||
path: route,
|
|
||||||
useLayout: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
route = route.replace(/\?.+$/, "").replace(/\/{2,}/g, "/")
|
|
||||||
route = route.replace(/\/$/, "")
|
|
||||||
|
|
||||||
if (routeDeclaration) {
|
|
||||||
if (!bindProps.user && (window.location.pathname !== config.app?.authPath)) {
|
|
||||||
if (!routeDeclaration.public) {
|
|
||||||
if (typeof window.app.location.push === "function") {
|
|
||||||
window.app.location.push(config.app?.authPath ?? "/login")
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = config.app?.authPath ?? "/login"
|
|
||||||
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeDeclaration.useLayout) {
|
|
||||||
app.layout.set(routeDeclaration.useLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof routeDeclaration.centeredContent !== "undefined") {
|
|
||||||
let finalBool = null
|
|
||||||
|
|
||||||
if (typeof routeDeclaration.centeredContent === "boolean") {
|
|
||||||
finalBool = routeDeclaration.centeredContent
|
|
||||||
} else {
|
|
||||||
if (app.isMobile) {
|
|
||||||
finalBool = routeDeclaration.centeredContent?.mobile ?? null
|
|
||||||
} else {
|
|
||||||
finalBool = routeDeclaration.centeredContent?.desktop ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.layout.toggleCenteredContent(finalBool)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof routeDeclaration.useTitle !== "undefined") {
|
|
||||||
if (typeof routeDeclaration.useTitle === "function") {
|
|
||||||
routeDeclaration.useTitle = routeDeclaration.useTitle(route, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.title = `${routeDeclaration.useTitle} - ${config.app.siteName}`
|
|
||||||
} else {
|
|
||||||
document.title = config.app.siteName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof routeDeclaration?.mobileTopBarSpacer === "boolean" && app.isMobile) {
|
|
||||||
app.layout.toggleTopBarSpacer(routeDeclaration.mobileTopBarSpacer)
|
|
||||||
} else {
|
|
||||||
app.layout.toggleTopBarSpacer(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`)
|
||||||
.then((response) => response.blob())
|
|
||||||
.then((blob) => {
|
|
||||||
if (!filename) {
|
|
||||||
filename = uri.split("/").pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create blob link to download
|
try {
|
||||||
const url = window.URL.createObjectURL(new Blob([blob]))
|
app.cores.notifications.new({
|
||||||
|
key: key,
|
||||||
const link = document.createElement("a")
|
title: "Downloading",
|
||||||
|
duration: 0,
|
||||||
link.href = url
|
type: "loading",
|
||||||
|
closable: false,
|
||||||
link.setAttribute("download", filename)
|
feedback: false,
|
||||||
|
|
||||||
// Append to html link element page
|
|
||||||
document.body.appendChild(link)
|
|
||||||
|
|
||||||
// Start download
|
|
||||||
link.click()
|
|
||||||
|
|
||||||
// Clean up and remove the link
|
|
||||||
link.parentNode.removeChild(link)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
const metadata = await axios({
|
||||||
app.message.error("Failed to download media")
|
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")
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
link.download = file.name
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
app.cores.notifications.close(key)
|
||||||
|
}, 1000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
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,8 +44,12 @@ 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -48,7 +64,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
throw new OperationError(error.code ?? 500, error.message ?? "Failed to upload file")
|
throw new OperationError(error.code ?? 500, error.message ?? "Failed to upload file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ export default class Release {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async fullfillItemData(release) {
|
static async fullfillItemData(release) {
|
||||||
|
|
||||||
return release
|
return release
|
||||||
}
|
}
|
||||||
}
|
}
|
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