Merge pull request #169 from ragestudio/dev

This commit is contained in:
srgooglo 2025-07-07 17:35:25 +02:00 committed by GitHub
commit d279264277
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 510 additions and 338 deletions

@ -1 +1 @@
Subproject commit 0dd24fee4a231dbb41d5b14ff92b67d8f14cb5a2 Subproject commit fb4d666576a937eaad4ea69f9e5a13778f06b3f5

View File

@ -1,6 +1,6 @@
{ {
"name": "@comty/app", "name": "@comty/app",
"version": "1.44.0@alpha", "version": "1.44.1@alpha",
"license": "ComtyLicense", "license": "ComtyLicense",
"main": "electron/main", "main": "electron/main",
"type": "module", "type": "module",
@ -34,7 +34,7 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0", "bear-react-carousel": "^4.0.10-alpha.0",
"classnames": "2.3.1", "classnames": "2.3.1",
"comty.js": "^0.68.0", "comty.js": "^0.68.1",
"d3": "^7.9.0", "d3": "^7.9.0",
"dashjs": "^5.0.3", "dashjs": "^5.0.3",
"dompurify": "^3.0.0", "dompurify": "^3.0.0",
@ -68,6 +68,7 @@
"react-player": "^2.16.0", "react-player": "^2.16.0",
"react-rnd": "^10.4.14", "react-rnd": "^10.4.14",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-turnstile": "^1.1.4",
"react-useanimations": "^2.10.0", "react-useanimations": "^2.10.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",

View File

@ -1,3 +1,11 @@
&.mobile {
.player-seek_bar {
.slider-container {
height: 10px;
}
}
}
.player-seek_bar { .player-seek_bar {
z-index: 330; z-index: 330;

View File

@ -40,9 +40,7 @@ export const usePlayerStateContext = (updater) => {
app.cores.player.eventBus().on("player.state.update", handleStateChange) app.cores.player.eventBus().on("player.state.update", handleStateChange)
return () => { return () => {
app.cores.player app.cores.player.eventBus().off("player.state.update", handleStateChange)
.eventBus()
.off("player.state.update", handleStateChange)
} }
}, []) }, [])
@ -56,9 +54,7 @@ export class WithPlayerContext extends React.Component {
events = { events = {
"player.state.update": async (state) => { "player.state.update": async (state) => {
if (state !== this.state) {
this.setState(state) this.setState(state)
}
}, },
} }

View File

@ -23,9 +23,7 @@ export default class Layout extends React.PureComponent {
const transitionLayer = document.getElementById("transitionLayer") const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) { if (!transitionLayer) {
console.warn( console.warn("transitionLayer not found, no animation will be played")
"transitionLayer not found, no animation will be played",
)
return false return false
} }
@ -42,9 +40,7 @@ export default class Layout extends React.PureComponent {
const transitionLayer = document.getElementById("transitionLayer") const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) { if (!transitionLayer) {
console.warn( console.warn("transitionLayer not found, no animation will be played")
"transitionLayer not found, no animation will be played",
)
return false return false
} }
@ -114,10 +110,7 @@ export default class Layout extends React.PureComponent {
) )
}, },
toggleMobileStyle: (to) => { toggleMobileStyle: (to) => {
return this.layoutInterface.toggleRootContainerClassname( return this.layoutInterface.toggleRootContainerClassname("mobile", to)
"mobile",
to,
)
}, },
toggleReducedAnimations: (to) => { toggleReducedAnimations: (to) => {
return this.layoutInterface.toggleRootContainerClassname( return this.layoutInterface.toggleRootContainerClassname(
@ -157,10 +150,7 @@ export default class Layout extends React.PureComponent {
return false return false
} }
to = to = typeof to === "boolean" ? to : !root.classList.contains(classname)
typeof to === "boolean"
? to
: !root.classList.contains(classname)
if (root.classList.contains(classname) === to) { if (root.classList.contains(classname) === to) {
// ignore // ignore
@ -208,10 +198,9 @@ export default class Layout extends React.PureComponent {
if (this.state.renderError) { if (this.state.renderError) {
if (this.props.staticRenders?.RenderError) { if (this.props.staticRenders?.RenderError) {
return React.createElement( return React.createElement(this.props.staticRenders?.RenderError, {
this.props.staticRenders?.RenderError, error: this.state.renderError,
{ error: this.state.renderError }, })
)
} }
return JSON.stringify(this.state.renderError) return JSON.stringify(this.state.renderError)

View File

@ -159,7 +159,10 @@ const AccountButton = React.forwardRef((props, ref) => {
> >
<div className="icon"> <div className="icon">
{user ? ( {user ? (
<antd.Avatar shape="square" src={app.userData.avatar} /> <antd.Avatar
shape="square"
src={app.userData.avatar}
/>
) : ( ) : (
createIconRender("FiLogin") createIconRender("FiLogin")
)} )}
@ -168,6 +171,8 @@ const AccountButton = React.forwardRef((props, ref) => {
) )
}) })
AccountButton.displayName = "AccountButton"
export class BottomBar extends React.Component { export class BottomBar extends React.Component {
static contextType = Context static contextType = Context
@ -366,9 +371,7 @@ export class BottomBar extends React.Component {
} }
const heightValue = Number( const heightValue = Number(
app.cores.style app.cores.style.getDefaultVar("bottom-bar-height").replace("px", ""),
.getDefaultVar("bottom-bar-height")
.replace("px", ""),
) )
return ( return (
@ -409,10 +412,7 @@ export class BottomBar extends React.Component {
<div <div
key="creator" key="creator"
id="creator" id="creator"
className={classnames( className={classnames("item", "primary")}
"item",
"primary",
)}
onClick={openCreator} onClick={openCreator}
> >
<div className="icon"> <div className="icon">
@ -441,9 +441,7 @@ export class BottomBar extends React.Component {
}) })
}} }}
> >
<div className="icon"> <div className="icon">{createIconRender("FiHome")}</div>
{createIconRender("FiHome")}
</div>
</div> </div>
<div <div
@ -452,9 +450,7 @@ export class BottomBar extends React.Component {
className="item" className="item"
onClick={app.controls.openSearcher} onClick={app.controls.openSearcher}
> >
<div className="icon"> <div className="icon">{createIconRender("FiSearch")}</div>
{createIconRender("FiSearch")}
</div>
</div> </div>
<AccountButton ref={this.accountBtnRef} /> <AccountButton ref={this.accountBtnRef} />

View File

@ -12,8 +12,9 @@ import UsernameStep from "./steps/username"
import PasswordStep from "./steps/password" import PasswordStep from "./steps/password"
import EmailStep from "./steps/email" import EmailStep from "./steps/email"
import TOSStep from "./steps/tos" import TOSStep from "./steps/tos"
import CaptchaStep from "./steps/captcha"
const steps = [UsernameStep, PasswordStep, EmailStep, TOSStep] const steps = [UsernameStep, PasswordStep, EmailStep, TOSStep, CaptchaStep]
const RegisterForm = (props) => { const RegisterForm = (props) => {
const [finishing, setFinishing] = React.useState(false) const [finishing, setFinishing] = React.useState(false)
@ -35,6 +36,7 @@ const RegisterForm = (props) => {
password: stepsValues.password, password: stepsValues.password,
email: stepsValues.email, email: stepsValues.email,
tos: stepsValues.tos, tos: stepsValues.tos,
captcha: stepsValues.captcha,
}).catch((err) => { }).catch((err) => {
setFinishSuccess(false) setFinishSuccess(false)
setFinishing(false) setFinishing(false)
@ -107,8 +109,8 @@ const RegisterForm = (props) => {
<> <>
<h1>👋 Hi! Nice to meet you</h1> <h1>👋 Hi! Nice to meet you</h1>
<p> <p>
Tell us some basic information to get started Tell us some basic information to get started creating your
creating your account. account.
</p> </p>
</> </>
)} )}
@ -116,8 +118,7 @@ const RegisterForm = (props) => {
{!finishSuccess && !finishing && step > 0 && ( {!finishSuccess && !finishing && step > 0 && (
<> <>
<h1> <h1>
{currentStepData?.icon && {currentStepData?.icon && createIconRender(currentStepData.icon)}
createIconRender(currentStepData.icon)}
{currentStepData?.title} {currentStepData?.title}
</h1> </h1>
@ -150,10 +151,7 @@ const RegisterForm = (props) => {
<div className="register_form_success"> <div className="register_form_success">
<Icons.CheckCircleOutlined /> <Icons.CheckCircleOutlined />
<h1>Welcome abord!</h1> <h1>Welcome abord!</h1>
<p> <p>One last step, we need you to login with your new account.</p>
One last step, we need you to login with your new
account.
</p>
<antd.Button <antd.Button
type="primary" type="primary"
@ -165,22 +163,21 @@ const RegisterForm = (props) => {
)} )}
{finishError && ( {finishError && (
<antd.Alert type="error" message={finishError.message} /> <antd.Alert
type="error"
message={finishError.message}
/>
)} )}
{!finishSuccess && !finishing && ( {!finishSuccess && !finishing && (
<div className="register_form_actions"> <div className="register_form_actions">
{step === 0 && ( {step === 0 && (
<antd.Button <antd.Button onClick={() => props.setActiveKey("selector")}>
onClick={() => props.setActiveKey("selector")}
>
Cancel Cancel
</antd.Button> </antd.Button>
)} )}
{step > 0 && ( {step > 0 && (
<antd.Button onClick={() => prevStep()}> <antd.Button onClick={() => prevStep()}>Back</antd.Button>
Back
</antd.Button>
)} )}
<antd.Button <antd.Button

View File

@ -0,0 +1,22 @@
import Turnstile from "react-turnstile"
const CaptchaStepComponent = (props) => {
return (
<Turnstile
sitekey={import.meta.env.VITE_TURNSTILE_SITEKEY}
onVerify={(token) => {
props.updateValue(token)
}}
/>
)
}
export default {
key: "captcha",
title: "Step 4",
icon: "FiLock",
description:
"We need you to prove that you are a human. Please enter the captcha below.",
required: true,
content: CaptchaStepComponent,
}

View File

@ -19,7 +19,9 @@ const EmailStepComponent = (props) => {
} }
const submit = () => { const submit = () => {
if (!isValid()) return if (!isValid()) {
return false
}
props.onPressEnter() props.onPressEnter()
} }
@ -40,11 +42,14 @@ const EmailStepComponent = (props) => {
setLoading(true) setLoading(true)
setValidFormat(checkIfIsEmail(email)) const isEmailValid = checkIfIsEmail(email)
setValidFormat(isEmailValid)
// check if email is available // check if email is available
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
if (!validFormat) return if (!isEmailValid) {
return false
}
const request = await AuthModel.availability({ email }).catch((error) => { const request = await AuthModel.availability({ email }).catch((error) => {
antd.message.error(`Cannot check email availability: ${error.message}`) antd.message.error(`Cannot check email availability: ${error.message}`)
@ -69,22 +74,33 @@ const EmailStepComponent = (props) => {
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [email]) }, [email])
return <div className="register_form_step_content"> return (
<div className="register_form_step_content">
<antd.Input <antd.Input
defaultValue={props.currentValue} defaultValue={props.currentValue}
placeholder="Email" placeholder="Email"
onPressEnter={submit} onPressEnter={submit}
onChange={handleUpdate} onChange={handleUpdate}
status={email.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")} status={
email.length == 0
? "default"
: loading
? "default"
: isValid()
? "success"
: "error"
}
/> />
</div> </div>
)
} }
export default { export default {
key: "email", key: "email",
title: "Step 3", title: "Step 3",
icon: "FiMail", icon: "FiMail",
description: "Enter a email for the account, it can be used to access to your account. \n Will not be shared with anyone else and not be used for marketing purposes.", description:
"Enter a email for the account, it can be used to access to your account. \n Will not be shared with anyone else and not be used for marketing purposes.",
required: true, required: true,
content: EmailStepComponent, content: EmailStepComponent,
} }

View File

@ -1,32 +1,31 @@
import React from "react"
import * as antd from "antd" import * as antd from "antd"
import MarkdownReader from "@components/MarkdownReader" import MarkdownReader from "@components/MarkdownReader"
import config from "@config" import config from "@config"
const FrameStyle = { const FrameStyle = {
"width": "60vw", width: "60vw",
"max-width": "60vw", "max-width": "60vw",
"height": "90vh", height: "90vh",
"max-height": "90vh", "max-height": "90vh",
"overflow": "overlay", overflow: "overlay",
"justify-content": "flex-start", "justify-content": "flex-start",
} }
const LegalDocumentsDecorators = { const LegalDocumentsDecorators = {
"terms": "Terms of Service", terms: "Terms of Service",
"privacy": "Privacy Policy", privacy: "Privacy Policy",
} }
function composeConfirmationCheckboxLabel(documents) { function composeConfirmationCheckboxLabel(documents) {
let labels = [ let labels = ["I have read and accept"]
"I have read and accept"
]
documents.forEach(([key, value], index) => { documents.forEach(([key, value], index) => {
const isLast = index === documents.length - 1 const isLast = index === documents.length - 1
labels.push(`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`) labels.push(
`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`,
)
}) })
return labels.join(" ") return labels.join(" ")
@ -35,29 +34,30 @@ function composeConfirmationCheckboxLabel(documents) {
const TermsOfServiceStepComponent = (props) => { const TermsOfServiceStepComponent = (props) => {
const legalDocuments = Object.entries(config.legal) const legalDocuments = Object.entries(config.legal)
return <div className="register_form_step_content"> return (
{ <div className="register_form_step_content">
Object.entries(config.legal).map(([key, value]) => { {Object.entries(config.legal).map(([key, value]) => {
if (!value) { if (!value) {
return null return null
} }
return <antd.Button return (
<antd.Button
key={key} key={key}
onClick={() => { onClick={() => {
app.layout.modal.open(key, MarkdownReader, { app.layout.modal.open(key, MarkdownReader, {
includeCloseButton: true, includeCloseButton: true,
frameContentStyle: FrameStyle, frameContentStyle: FrameStyle,
props: { props: {
url: value url: value,
} },
}) })
}} }}
> >
Read {LegalDocumentsDecorators[key] ?? `document (${key})`} Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
</antd.Button> </antd.Button>
}) )
} })}
<antd.Checkbox <antd.Checkbox
defaultChecked={props.currentValue} defaultChecked={props.currentValue}
@ -68,6 +68,7 @@ const TermsOfServiceStepComponent = (props) => {
{composeConfirmationCheckboxLabel(legalDocuments)} {composeConfirmationCheckboxLabel(legalDocuments)}
</antd.Checkbox> </antd.Checkbox>
</div> </div>
)
} }
export default { export default {

View File

@ -6,9 +6,14 @@ import { Icons } from "@components/Icons"
const MainSelector = (props) => { const MainSelector = (props) => {
return ( return (
<> <>
{!app.isMobile && (
<div className="content_header"> <div className="content_header">
<img src={config.logo.alt} className="logo" /> <img
src={config.logo.alt}
className="logo"
/>
</div> </div>
)}
<div className="actions"> <div className="actions">
{app.userData && ( {app.userData && (

View File

@ -1,32 +1,55 @@
import React from "react" import React from "react"
import config from "@config"
import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl" import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl"
import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey"
import RegisterForm from "./forms/register"
import MainSelector from "./forms/selector"
import RecoveryForm from "./forms/recovery"
const keyToComponents = {
selector: MainSelector,
register: RegisterForm,
recovery: RecoveryForm,
}
import "./index.mobile.less" import "./index.mobile.less"
export default (props) => { const AuthPage = (props) => {
const randomWallpaperURL = useRandomFeaturedWallpaperUrl() const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
React.useEffect(() => { defaultKey: "selector",
if (app.userData) { })
app.navigation.goMain()
} else {
app.auth.login()
}
}, [])
return ( return (
<div className="loginPage"> <div className="login-page">
<div <div
style={{ style={{
backgroundImage: `url(${randomWallpaperURL})`, backgroundImage: `url(${randomWallpaperURL})`,
}} }}
className="wallpaper" className="wallpaper"
> />
{/* <p>
{wallpaperData?.author ? wallpaperData.author : null} <div className="login-page-card">
</p> */} <div className="login-page-card__header">
<img
className="login-page-card__header__logo"
src={config.logo.alt}
/>
</div>
<div className="login-page-card__content">
{React.createElement(
keyToComponents[activeKey] ?? keyToComponents["selector"],
{
setActiveKey: setActiveKey,
},
)}
</div>
</div> </div>
</div> </div>
) )
} }
export default AuthPage

View File

@ -1,16 +1,24 @@
.loginPage { .login-page {
position: relative; position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
.wallpaper { .wallpaper {
position: absolute; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: -1;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
@ -19,4 +27,63 @@
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.login-page-card {
position: relative;
display: flex;
flex-direction: column;
width: 95%;
padding: 15px;
background-color: var(--background-color-primary);
border-radius: 24px;
gap: 30px;
&__header {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
&__logo {
width: 40px;
height: 40px;
}
}
&__content {
display: flex;
flex-direction: column;
.ant-btn {
height: fit-content;
padding: 7px 15px;
font-size: 0.8rem;
}
.actions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
}
.register_form {
position: unset;
.register_form_actions {
top: 0;
bottom: unset;
}
}
}
}
} }

View File

@ -104,6 +104,8 @@ const LyricsText = React.forwardRef((props, textRef) => {
React.useEffect(() => { React.useEffect(() => {
setVisible(false) setVisible(false)
setCurrentLineIndex(0) setCurrentLineIndex(0)
// set scroll top to 0
textRef.current.scrollTop = 0
}, [playerState.track_manifest]) }, [playerState.track_manifest])
React.useEffect(() => { React.useEffect(() => {

View File

@ -35,7 +35,6 @@ export default class API extends Server {
onExit() { onExit() {
this.queuesManager.cleanUp() this.queuesManager.cleanUp()
console.log("Jijija")
} }
} }

View File

@ -1,32 +1,52 @@
import bcrypt from "bcrypt" import bcrypt from "bcrypt"
import { User } from "@db_models" import { User } from "@db_models"
import requiredFields from "@shared-utils/requiredFields"
import Account from "@classes/account" import Account from "@classes/account"
import requiredFields from "@shared-utils/requiredFields"
import verifyTurnstileToken from "@utils/verifyTurnstileToken"
export default async (payload) => { export default async (payload) => {
requiredFields(["username", "password", "email"], payload) requiredFields(["username", "password", "email"], payload)
let { username, password, email, public_name, roles, avatar, accept_tos } = payload let {
username,
password,
email,
public_name,
roles,
avatar,
accept_tos,
captcha,
} = payload
if (ToBoolean(accept_tos) !== true) { if (ToBoolean(accept_tos) !== true) {
throw new OperationError(400, "You must accept the terms of service in order to create an account.") throw new OperationError(
400,
"You must accept the terms of service in order to create an account.",
)
}
if (!captcha) {
throw new OperationError(400, "Captcha token is required")
}
const turnstileResponse = await verifyTurnstileToken(captcha)
if (turnstileResponse.success !== true) {
throw new OperationError(400, "Invalid captcha token")
} }
await Account.usernameMeetPolicy(username) await Account.usernameMeetPolicy(username)
// check if username is already taken // check if username is already taken
const existentUser = await User const existentUser = await User.findOne({ username: username })
.findOne({ username: username })
if (existentUser) { if (existentUser) {
throw new OperationError(400, "User already exists") throw new OperationError(400, "User already exists")
} }
// check if the email is already in use // check if the email is already in use
const existentEmail = await User const existentEmail = await User.findOne({ email: email }).select("+email")
.findOne({ email: email })
.select("+email")
if (existentEmail) { if (existentEmail) {
throw new OperationError(400, "Email already in use") throw new OperationError(400, "Email already in use")
@ -35,14 +55,18 @@ export default async (payload) => {
await Account.passwordMeetPolicy(password) await Account.passwordMeetPolicy(password)
// hash the password // hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3)) const hash = bcrypt.hashSync(
password,
parseInt(process.env.BCRYPT_ROUNDS ?? 3),
)
let user = new User({ let user = new User({
username: username, username: username,
password: hash, password: hash,
email: email, email: email,
public_name: public_name, public_name: public_name,
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`, avatar:
avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles, roles: roles,
created_at: new Date().getTime(), created_at: new Date().getTime(),
accept_tos: accept_tos, accept_tos: accept_tos,

View File

@ -0,0 +1,26 @@
import axios from "axios"
export default async (token) => {
const secret = process.env.TURNSTILE_SECRET
if (!secret) {
throw new Error("Turnstile secret is not set")
}
let response = await axios({
url: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: {
secret: secret,
response: token,
},
}).catch((err) => {
console.error(err.response.data)
throw new Error("Turnstile verification failed")
})
return response.data
}