merge from local

This commit is contained in:
SrGooglo 2024-08-20 08:33:52 +00:00
parent 0252498d2b
commit 5436678fed
207 changed files with 1898 additions and 5347 deletions

View File

@ -12,9 +12,6 @@
"postdeploy": "node ./scripts/post-deploy.js",
"postinstall": "node ./scripts/post-install.js"
},
"workspaces": [
"packages/**"
],
"dependencies": {
"7zip-min": "1.4.3",
"axios": "^1.4.0",

17
packages/app/aliases.js Normal file
View File

@ -0,0 +1,17 @@
import path from "path"
export default {
"@": path.join(__dirname, "src"),
"@config": path.join(__dirname, "config"),
"@cores": path.join(__dirname, "src/cores"),
"@pages": path.join(__dirname, "src/pages"),
"@styles": path.join(__dirname, "src/styles"),
"@components": path.join(__dirname, "src/components"),
"@contexts": path.join(__dirname, "src/contexts"),
"@utils": path.join(__dirname, "src/utils"),
"@layouts": path.join(__dirname, "src/layouts"),
"@hooks": path.join(__dirname, "src/hooks"),
"@classes": path.join(__dirname, "src/classes"),
"@models": path.join(__dirname, "../../", "comty.js/src/models"),
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
}

View File

@ -5,7 +5,7 @@
"webDir": "dist",
"plugins": {
"CapacitorUpdater": {
"updateUrl": "https://api.comty.app/auto-update/mobile",
"updateUrl": "https://api.comty.app/repo/auto-update/mobile",
"resetWhenUpdate": true
}
}

View File

@ -8,6 +8,7 @@
"scripts": {
"build": "vite build",
"dev": "vite",
"dev:farm": "farm start",
"docker-compose:update_run": "docker-compose down && git pull && yarn build && docker-compose up -d --build",
"preview": "vite preview"
},
@ -15,7 +16,7 @@
"react": "^18.2.0"
},
"dependencies": {
"@ant-design/icons": "4.7.0",
"@ant-design/icons": "^5.4.0",
"@capacitor/android": "^5.0.5",
"@capacitor/app": "^5.0.3",
"@capacitor/cli": "^5.0.5",
@ -30,16 +31,24 @@
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "11.0.0",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@farmfe/cli": "^1.0.2",
"@farmfe/core": "^1.2.6",
"@farmfe/js-plugin-less": "^1.8.0",
"@farmfe/plugin-react": "^1.1.0",
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"@loadable/component": "5.15.2",
"@mui/material": "^5.11.9",
"@ragestudio/cordova-nfc": "^1.2.0",
"@sentry/browser": "^7.64.0",
"@stripe/react-stripe-js": "^2.7.3",
"@stripe/stripe-js": "^4.2.0",
"@tanstack/react-virtual": "^3.5.0",
"@tauri-apps/api": "^1.5.4",
"@tsmx/human-readable": "^1.0.7",
"antd": "^5.17.0",
"antd": "^5.20.1",
"antd-mobile": "^5.31.0",
"axios": "^1.6.8",
"bear-react-carousel": "^4.0.10-alpha.0",
@ -58,9 +67,7 @@
"js-cookie": "3.0.1",
"jsmediatags": "^3.9.7",
"less": "4.1.2",
"linebridge": "0.16.0",
"lottie-react": "^2.4.0",
"lru-cache": "^10.0.0",
"luxon": "^3.0.4",
"million": "^2.6.4",
"mime": "^3.0.0",
@ -71,9 +78,10 @@
"plyr": "^3.6.12",
"plyr-react": "^3.2.1",
"prop-types": "^15.8.1",
"react": "18.2.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-color": "2.19.3",
"react-confetti-explosion": "^2.1.2",
"react-countup": "^6.4.1",
"react-dom": "18.2.0",
"react-fast-marquee": "^1.3.5",
@ -87,6 +95,7 @@
"react-motion": "0.5.2",
"react-rnd": "10.3.5",
"react-router-dom": "^6.6.2",
"react-slot-counter": "^3.0.1",
"react-ticker": "^1.3.2",
"react-transition-group": "^4.4.5",
"react-useanimations": "^2.10.0",

View File

@ -106,7 +106,7 @@ class ComtyApp extends React.Component {
}
},
openLoginForm: async (options = {}) => {
app.layout.drawer.open("login", Login, {
app.layout.draggable.open("login", Login, {
defaultLocked: options.defaultLocked ?? false,
componentProps: {
sessionController: this.sessionController,
@ -134,7 +134,7 @@ class ComtyApp extends React.Component {
},
// Opens the notification window and sets up the UI for the notification to be displayed
openNotifications: () => {
window.app.layout.sidedrawer.open("notifications", NotificationsCenter, {
window.app.layout.drawer.open("notifications", NotificationsCenter, {
props: {
width: "fit-content",
},
@ -158,7 +158,7 @@ class ComtyApp extends React.Component {
})
},
openMessages: () => {
app.location.push("/messages")
app.location.push("/messages")
},
openFullImageViewer: (src) => {
app.cores.window_mng.render("image_lightbox", <Lightbox
@ -169,7 +169,6 @@ class ComtyApp extends React.Component {
showRotate
/>)
},
openPostCreator: (params) => {
app.layout.modal.open("post_creator", (props) => <PostCreator
{...props}

View File

@ -11,17 +11,15 @@ const CoverEditor = (props) => {
const [url, setUrl] = React.useState(value)
React.useEffect(() => {
setUrl(value)
}, [value])
React.useEffect(() => {
onChange(url)
}, [url])
React.useEffect(() => {
if (!url) {
if (!value) {
setUrl(defaultUrl)
} else {
setUrl(value)
}
}, [])

View File

@ -304,9 +304,10 @@ class Login extends React.Component {
</>
}
<antd.Input
placeholder="4 Digit MFA code"
onChange={(e) => this.onUpdateInput("mfa_code", e.target.value)}
<antd.Input.OTP
length={4}
formatter={(str) => str.toUpperCase()}
onChange={(code) => this.onUpdateInput("mfa_code", code)}
onPressEnter={this.nextStep}
/>
</antd.Form.Item>

View File

@ -5,6 +5,8 @@ import { Icons } from "@components/Icons"
import MusicModel from "@models/music"
import { DefaultReleaseEditorState, ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
import Tabs from "./tabs"
import "./index.less"
@ -14,11 +16,33 @@ const ReleaseEditor = (props) => {
const basicInfoRef = React.useRef()
const [loading, setLoading] = React.useState(true)
const [loadError, setLoadError] = React.useState(null)
const [globalState, setGlobalState] = React.useState(DefaultReleaseEditorState)
const [selectedTab, setSelectedTab] = React.useState("info")
const [L_Release, R_Release, E_Release, F_Release] = release_id !== "new" ? app.cores.api.useRequest(MusicModel.getReleaseData, release_id) : [false, false, false, false]
async function initialize() {
setLoading(true)
setLoadError(null)
if (release_id !== "new") {
try {
const releaseData = await MusicModel.getReleaseData(release_id)
setGlobalState({
...globalState,
...releaseData,
})
} catch (error) {
setLoadError(error)
}
}
setLoading(false)
}
async function handleSubmit() {
basicInfoRef.current.submit()
console.log("Submit >", globalState)
}
async function onFinish(values) {
@ -29,79 +53,88 @@ const ReleaseEditor = (props) => {
return true
}
if (E_Release) {
React.useEffect(() => {
initialize()
}, [])
if (loadError) {
return <antd.Result
status="warning"
title="Error"
subTitle={E_Release.message}
subTitle={loadError.message}
/>
}
if (L_Release) {
if (loading) {
return <antd.Skeleton active />
}
const Tab = Tabs.find(({ key }) => key === selectedTab)
return <div className="music-studio-release-editor">
<div className="music-studio-release-editor-menu">
<antd.Menu
onClick={(e) => setSelectedTab(e.key)}
selectedKeys={[selectedTab]}
items={Tabs}
mode="vertical"
/>
return <ReleaseEditorStateContext.Provider value={globalState}>
<div className="music-studio-release-editor">
<div className="music-studio-release-editor-menu">
<antd.Menu
onClick={(e) => setSelectedTab(e.key)}
selectedKeys={[selectedTab]}
items={Tabs}
mode="vertical"
/>
<div className="music-studio-release-editor-menu-actions">
<antd.Button
type="primary"
onClick={handleSubmit}
icon={<Icons.Save />}
disabled={L_Release || !canFinish()}
>
Save
</antd.Button>
{
release_id !== "new" ? <antd.Button
icon={<Icons.IoMdTrash />}
disabled={L_Release}
<div className="music-studio-release-editor-menu-actions">
<antd.Button
type="primary"
onClick={handleSubmit}
icon={<Icons.Save />}
disabled={loading || !canFinish()}
>
Delete
</antd.Button> : null
Save
</antd.Button>
{
release_id !== "new" ? <antd.Button
icon={<Icons.IoMdTrash />}
disabled={loading}
>
Delete
</antd.Button> : null
}
{
release_id !== "new" ? <antd.Button
icon={<Icons.MdLink />}
onClick={() => app.location.push(`/music/release/${globalState._id}`)}
>
Go to release
</antd.Button> : null
}
</div>
</div>
<div className="music-studio-release-editor-content">
{
!Tab && <antd.Result
status="error"
title="Error"
subTitle="Tab not found"
/>
}
{
release_id !== "new" ? <antd.Button
icon={<Icons.MdLink />}
onClick={() => app.location.push(`/music/release/${R_Release._id}`)}
>
Go to release
</antd.Button> : null
Tab && React.createElement(Tab.render, {
release: globalState,
onFinish: onFinish,
state: globalState,
setState: setGlobalState,
references: {
basic: basicInfoRef
}
})
}
</div>
</div>
<div className="music-studio-release-editor-content">
{
!Tab && <antd.Result
status="error"
title="Error"
subTitle="Tab not found"
/>
}
{
Tab && React.createElement(Tab.render, {
release: R_Release,
onFinish: onFinish,
references: {
basic: basicInfoRef
}
})
}
</div>
</div>
</ReleaseEditorStateContext.Provider>
}
export default ReleaseEditor

View File

@ -29,7 +29,16 @@ const ReleasesTypes = [
]
const BasicInformation = (props) => {
const { release, onFinish } = props
const { release, onFinish, setState, state } = props
async function onFormChange(change) {
setState((globalState) => {
return {
...globalState,
...change
}
})
}
return <div className="music-studio-release-editor-tab">
<h1>Release Information</h1>
@ -40,12 +49,13 @@ const BasicInformation = (props) => {
ref={props.references.basic}
onFinish={onFinish}
requiredMark={false}
onValuesChange={onFormChange}
>
<antd.Form.Item
label=""
name="cover"
rules={[{ required: true, message: "Input a cover for the release" }]}
initialValue={release?.cover}
initialValue={state?.cover}
>
<CoverEditor
defaultUrl="https://storage.ragestudio.net/comty-static-assets/default_song.png"
@ -70,7 +80,7 @@ const BasicInformation = (props) => {
label={<><Icons.MdMusicNote /> <span>Title</span></>}
name="title"
rules={[{ required: true, message: "Input a title for the release" }]}
initialValue={release?.title}
initialValue={state?.title}
>
<antd.Input
placeholder="Release title"
@ -83,7 +93,7 @@ const BasicInformation = (props) => {
label={<><Icons.MdAlbum /> <span>Type</span></>}
name="type"
rules={[{ required: true, message: "Select a type for the release" }]}
initialValue={release?.type}
initialValue={state?.type}
>
<antd.Select
placeholder="Release type"
@ -94,7 +104,7 @@ const BasicInformation = (props) => {
<antd.Form.Item
label={<><Icons.MdPublic /> <span>Public</span></>}
name="public"
initialValue={release?.public}
initialValue={state?.public}
>
<antd.Switch />
</antd.Form.Item>

View File

@ -0,0 +1,89 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { Draggable } from "react-beautiful-dnd"
import Image from "@components/Image"
import { Icons } from "@components/Icons"
import TrackEditor from "@components/MusicStudio/TrackEditor"
import "./index.less"
const TrackListItem = (props) => {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
const { track } = props
async function onClickEditTrack() {
app.layout.drawer.open("track_editor", TrackEditor, {
type: "drawer",
props: {
width: "600px",
headerStyle: {
display: "none",
}
},
componentProps: {
track,
onSave: (newTrackData) => {
console.log("Saving track", newTrackData)
},
},
})
}
return <Draggable
key={track._id}
draggableId={track._id}
index={props.index}
>
{
(provided, snapshot) => {
return <div
className={classnames(
"music-studio-release-editor-tracks-list-item",
{
["loading"]: loading,
["failed"]: !!error
}
)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className="music-studio-release-editor-tracks-list-item-index">
<span>{props.index + 1}</span>
</div>
<Image
src={track.cover}
style={{
width: 25,
height: 25,
borderRadius: 8,
}}
/>
<span>{track.title}</span>
<div className="music-studio-release-editor-tracks-list-item-actions">
<antd.Button
type="ghost"
icon={<Icons.Edit2 />}
onClick={onClickEditTrack}
/>
<div
{...provided.dragHandleProps}
className="music-studio-release-editor-tracks-list-item-dragger"
>
<Icons.MdDragIndicator />
</div>
</div>
</div>
}
}
</Draggable>
}
export default TrackListItem

View File

@ -0,0 +1,45 @@
.music-studio-release-editor-tracks-list-item {
position: relative;
display: flex;
flex-direction: row;
padding: 10px;
gap: 10px;
border-radius: 12px;
background-color: var(--background-color-accent);
.music-studio-release-editor-tracks-list-item-actions {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 5px;
svg {
margin: 0;
}
.music-studio-release-editor-tracks-list-item-dragger {
display: flex;
align-items: center;
justify-content: center;
svg {
font-size: 1rem;
}
}
}
}

View File

@ -0,0 +1,13 @@
import React from "react"
import { Icons } from "@components/Icons"
import "./index.less"
export default (props) => {
return <div className="music-studio-tracks-uploader-hint">
<Icons.MdPlaylistAdd />
<p>Upload your tracks</p>
<p>Drag and drop your tracks here or click this box to start uploading files.</p>
</div>
}

View File

@ -0,0 +1,4 @@
.music-studio-tracks-uploader-hint {
display: flex;
flex-direction: column;
}

View File

@ -1,163 +1,221 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"
import { DragDropContext, Droppable } from "react-beautiful-dnd"
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js"
import { Icons } from "@components/Icons"
import TrackEditor from "@components/MusicStudio/TrackEditor"
import TrackListItem from "./components/TrackListItem"
import UploadHint from "./components/UploadHint"
import "./index.less"
const UploadHint = (props) => {
return <div className="uploadHint">
<Icons.MdPlaylistAdd />
<p>Upload your tracks</p>
<p>Drag and drop your tracks here or click this box to start uploading files.</p>
</div>
async function uploadBinaryArrayToStorage(bin, args) {
const { format, data } = bin
const filenameExt = format.split("/")[1]
const filename = `cover.${filenameExt}`
const byteArray = new Uint8Array(data)
const blob = new Blob([byteArray], { type: data.type })
// create a file object
const file = new File([blob], filename, {
type: format,
})
return await app.cores.remoteStorage.uploadFile(file, args)
}
const TrackListItem = (props) => {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
class TrackManifest {
constructor(params) {
this.params = params
const { track } = props
return this
}
async function onClickEditTrack() {
app.layout.drawer.open("track_editor", TrackEditor, {
type: "drawer",
props: {
width: "600px",
headerStyle: {
display: "none",
}
},
componentProps: {
track,
onSave: (newTrackData) => {
console.log("Saving track", newTrackData)
cover = "https://storage.ragestudio.net/comty-static-assets/default_song.png"
title = "Untitled"
album = "Unknown"
artist = "Unknown"
source = null
async initialize() {
const metadata = await this.analyzeMetadata(this.params.file.originFileObj)
console.log(metadata)
if (metadata.tags) {
if (metadata.tags.title) {
this.title = metadata.tags.title
}
if (metadata.tags.artist) {
this.artist = metadata.tags.artist
}
if (metadata.tags.album) {
this.album = metadata.tags.album
}
if (metadata.tags.picture) {
const coverUpload = await uploadBinaryArrayToStorage(metadata.tags.picture)
this.cover = coverUpload.url
}
}
return this
}
analyzeMetadata = async (file) => {
return new Promise((resolve, reject) => {
jsmediatags.read(file, {
onSuccess: (data) => {
return resolve(data)
},
},
onError: (error) => {
return reject(error)
}
})
})
}
}
class TracksManager extends React.Component {
state = {
list: [],
pendingUploads: [],
}
componentDidMount() {
if (typeof this.props.list !== "undefined" && Array.isArray(this.props.list)) {
this.setState({
list: this.props.list
})
}
}
componentDidUpdate = (prevProps, prevState) => {
if (prevState.list !== this.state.list || prevState.pendingUploads !== this.state.pendingUploads) {
if (typeof this.props.onChangeState === "function") {
this.props.onChangeState(this.state)
}
}
}
findTrackByUid = (uid) => {
if (!uid) {
return false
}
return this.state.list.find((item) => item.uid === uid)
}
addTrackToList = (track) => {
if (!track) {
return false
}
this.setState({
list: [...this.state.list, track],
})
}
return <Draggable
key={track._id}
draggableId={track._id}
index={props.index}
>
{
(provided, snapshot) => {
return <div
className={classnames(
"music-studio-release-editor-tracks-list-item",
{
["loading"]: loading,
["failed"]: !!error
}
)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className="music-studio-release-editor-tracks-list-item-index">
<span>{props.index + 1}</span>
</div>
<span>{track.title}</span>
<div className="music-studio-release-editor-tracks-list-item-actions">
<antd.Button
type="ghost"
icon={<Icons.Edit2 />}
onClick={onClickEditTrack}
/>
<div
{...provided.dragHandleProps}
className="music-studio-release-editor-tracks-list-item-dragger"
>
<Icons.MdDragIndicator />
</div>
</div>
</div>
}
removeTrackByUid = (uid) => {
if (!uid) {
return false
}
</Draggable>
}
const ReleaseTracks = (props) => {
const { release } = props
const [list, setList] = React.useState(release.list ?? [])
const [pendingTracksUpload, setPendingTracksUpload] = React.useState([])
this.setState({
list: this.state.list.filter((item) => item.uid !== uid),
})
}
async function onTrackUploaderChange (change) {
addTrackUIDToPendingUploads = (uid) => {
if (!uid) {
return false
}
if (!this.state.pendingUploads.includes(uid)) {
this.setState({
pendingUploads: [...this.state.pendingUploads, uid],
})
}
}
removeTrackUIDFromPendingUploads = (uid) => {
if (!uid) {
return false
}
this.setState({
pendingUploads: this.state.pendingUploads.filter((item) => item !== uid),
})
}
handleUploaderStateChange = async (change) => {
switch (change.file.status) {
case "uploading": {
if (!pendingTracksUpload.includes(change.file.uid)) {
pendingTracksUpload.push(change.file.uid)
}
this.addTrackUIDToPendingUploads(change.file.uid)
setList((prev) => {
return [
...prev,
]
const trackManifest = new TrackManifest({
uid: change.file.uid,
file: change.file,
})
await trackManifest.initialize()
this.addTrackToList(trackManifest)
break
}
case "done": {
// remove pending file
this.setState({
pendingTracksUpload: this.state.pendingTracksUpload.filter((uid) => uid !== change.file.uid)
})
this.removeTrackUIDFromPendingUploads(change.file.uid)
// update file url in the track info
const track = this.state.trackList.find((file) => file.uid === change.file.uid)
const trackIndex = this.state.list.findIndex((item) => item.uid === uid)
if (track) {
track.source = change.file.response.url
track.status = "done"
if (trackIndex === -1) {
console.error(`Track with uid [${uid}] not found!`)
break
}
this.setState({
trackList: this.state.trackList
// update track list
this.setState((state) => {
state.list[trackIndex].source = change.file.response.url
return state
})
break
}
case "error": {
// remove pending file
this.handleTrackRemove(change.file.uid)
this.removeTrackUIDFromPendingUploads(change.file.uid)
// open a dialog to show the error and ask user to retry
antd.Modal.error({
title: "Upload failed",
content: "An error occurred while uploading the file. You want to retry?",
cancelText: "No",
okText: "Retry",
onOk: () => {
this.handleUploadTrack(change)
},
onCancel: () => {
this.handleTrackRemove(change.file.uid)
}
})
// remove from tracklist
await this.removeTrackByUid(change.file.uid)
}
case "removed": {
this.handleTrackRemove(change.file.uid)
// stop upload & delete from pending list and tracklist
await this.removeTrackByUid(change.file.uid)
}
default: {
break
}
}
}
async function handleUploadTrack (req) {
uploadToStorage = async (req) => {
const response = await app.cores.remoteStorage.uploadFile(req.file, {
onProgress: this.handleFileProgress,
service: "premium-cdn"
onProgress: this.handleTrackFileUploadProgress,
service: "b2"
}).catch((error) => {
console.error(error)
antd.message.error(error)
@ -172,38 +230,40 @@ const ReleaseTracks = (props) => {
}
}
async function onTrackDragEnd(result) {
console.log(result)
handleTrackFileUploadProgress = async (file, progress) => {
console.log(file, progress)
}
orderTrackList = (result) => {
if (!result.destination) {
return
}
setList((prev) => {
const trackList = [...prev]
this.setState((prev) => {
const trackList = [...prev.list]
const [removed] = trackList.splice(result.source.index, 1)
trackList.splice(result.destination.index, 0, removed)
return trackList
return {
list: trackList
}
})
}
return <div className="music-studio-release-editor-tab">
<h1>Tracks</h1>
<div>
render() {
return <div className="music-studio-release-editor-tracks">
<antd.Upload
className="uploader"
customRequest={handleUploadTrack}
onChange={onTrackUploaderChange}
className="music-studio-tracks-uploader"
onChange={this.handleUploaderStateChange}
customRequest={this.uploadToStorage}
showUploadList={false}
accept="audio/*"
multiple
>
{
list.length === 0 ?
this.state.list.length === 0 ?
<UploadHint /> : <antd.Button
className="uploadMoreButton"
icon={<Icons.Plus />}
@ -212,7 +272,7 @@ const ReleaseTracks = (props) => {
</antd.Upload>
<DragDropContext
onDragEnd={onTrackDragEnd}
onDragEnd={this.orderTrackList}
>
<Droppable
droppableId="droppable"
@ -224,13 +284,13 @@ const ReleaseTracks = (props) => {
className="music-studio-release-editor-tracks-list"
>
{
list.length === 0 && <antd.Result
this.state.list.length === 0 && <antd.Result
status="info"
title="No tracks"
/>
}
{
list.map((track, index) => {
this.state.list.map((track, index) => {
return <TrackListItem
index={index}
track={track}
@ -243,6 +303,25 @@ const ReleaseTracks = (props) => {
</Droppable>
</DragDropContext>
</div>
}
}
const ReleaseTracks = (props) => {
const { state, setState } = props
return <div className="music-studio-release-editor-tab">
<h1>Tracks</h1>
<TracksManager
_id={state._id}
list={state.list}
onChangeState={(managerState) => {
setState({
...state,
...managerState
})
}}
/>
</div>
}

View File

@ -3,50 +3,4 @@
flex-direction: column;
gap: 10px;
}
.music-studio-release-editor-tracks-list-item {
position: relative;
display: flex;
flex-direction: row;
padding: 10px;
gap: 10px;
border-radius: 12px;
background-color: var(--background-color-accent);
.music-studio-release-editor-tracks-list-item-actions {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 5px;
svg {
margin: 0;
}
.music-studio-release-editor-tracks-list-item-dragger {
display: flex;
align-items: center;
justify-content: center;
svg {
font-size: 1rem;
}
}
}
}

View File

@ -1,33 +1,102 @@
import React from "react"
import * as antd from "antd"
import classNames from "classnames"
import { createIconRender } from "@components/Icon"
import { createIconRender } from "@components/Icons"
import "./index.less"
const PollOption = (props) => {
return <div className="poll-option">
<div className="label">
{
createIconRender(props.option.icon)
}
const { option, editMode, onRemove } = props
<span>
{props.option.label}
return <div
className={classNames(
"poll-option",
{
["editable"]: !!editMode
}
)}
>
{
editMode && <antd.Input
placeholder="Option"
defaultValue={option.label}
/>
}
{
!editMode && <span>
{option.label}
</span>
</div>
}
{
editMode && <antd.Button
onClick={onRemove}
icon={createIconRender("CloseOutlined")}
size="small"
type="text"
/>
}
</div>
}
const Poll = (props) => {
const { editMode, onClose } = props
const [options, setOptions] = React.useState(props.options ?? [])
async function addOption() {
setOptions((prev) => {
return [
...prev,
{
label: null
}
]
})
}
async function removeOption(index) {
setOptions((prev) => {
return [
...prev.slice(0, index),
...prev.slice(index + 1)
]
})
}
return <div className="poll">
{
props.options.map((option) => {
options.map((option, index) => {
return <PollOption
key={option.id}
key={index}
option={option}
editMode={editMode}
onRemove={() => {
removeOption(index)
}}
/>
})
}
{
editMode && <div className="poll-edit-actions">
<antd.Button
onClick={addOption}
icon={createIconRender("PlusOutlined")}
>
Add Option
</antd.Button>
<antd.Button
onClick={onClose}
icon={createIconRender("CloseOutlined")}
size="small"
type="text"
/>
</div>
}
</div>
}

View File

@ -2,25 +2,59 @@
display: flex;
flex-direction: column;
width: 100%;
gap: 8px;
background-color: var(--background-color-primary);
border-radius: 12px;
border: 2px solid var(--border-color);
padding: 10px;
.poll-option {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
padding: 10px 20px;
gap: 10px;
padding: 5px;
width: 100%;
transition: all 150ms ease-in-out;
border-radius: 6px;
background-color: var(--background-color-accent);
border-radius: 12px;
cursor: pointer;
&:hover {
background-color: var(--background-color-accent-hover);
.ant-input {
background-color: transparent;
width: 100%;
border: 0;
color: var(--text-color);
&::placeholder {
color: var(--text-color);
opacity: 0.5;
}
}
}
.poll-edit-actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
}

View File

@ -4,10 +4,13 @@ import classnames from "classnames"
import { DateTime } from "luxon"
import lodash from "lodash"
import humanSize from "@tsmx/human-readable"
import PostLink from "@components/PostLink"
import { Icons } from "@components/Icons"
import Poll from "@components/Poll"
import clipboardEventFileToFile from "@utils/clipboardEventFileToFile"
import PostModel from "@models/post"
import "./index.less"
@ -26,6 +29,7 @@ export default class PostCreator extends React.Component {
postMessage: "",
postAttachments: [],
postPoll: null,
fileList: [],
postingPolicy: DEFAULT_POST_POLICY,
@ -451,6 +455,20 @@ export default class PostCreator extends React.Component {
dialog.click()
}
handleAddPoll = () => {
if (!this.state.postPoll) {
this.setState({
postPoll: []
})
}
}
handleDeletePoll = () => {
this.setState({
postPoll: null
})
}
componentDidMount = async () => {
if (this.props.edit_post) {
await this.setState({
@ -589,6 +607,14 @@ export default class PostCreator extends React.Component {
</antd.Upload.Dragger>
</div>
{
this.state.postPoll && <Poll
options={this.state.postPoll}
onClose={this.handleDeletePoll}
editMode
/>
}
<div className="actions">
<antd.Button
type="ghost"
@ -599,6 +625,7 @@ export default class PostCreator extends React.Component {
<antd.Button
type="ghost"
icon={<Icons.MdPoll />}
onClick={this.handleAddPoll}
/>
</div>
</div>

View File

@ -0,0 +1,15 @@
import React from "react"
export const DefaultReleaseEditorState = {
cover: null,
title: "Untitled",
type: "single",
public: false,
list: [],
pendingUploads: [],
}
export const ReleaseEditorStateContext = React.createContext(DefaultReleaseEditorState)
export default ReleaseEditorStateContext

View File

@ -0,0 +1,19 @@
import React from "react"
export default (to) => {
React.useEffect(() => {
if (typeof to !== "undefined") {
app.layout.tools_bar.toggleVisibility(to)
return () => {
app.layout.tools_bar.toggleVisibility(!!to)
}
}
app.layout.tools_bar.toggleVisibility(false)
return () => {
app.layout.tools_bar.toggleVisibility(true)
}
}, [])
}

View File

@ -2,7 +2,7 @@ import React from "react"
export default (namespace, ctx) => {
React.useEffect(() => {
if (app.layout["namespace"] === "object") {
if (app.layout[namespace] === "object") {
throw new Error(`Layout namespace [${namespace}] already exists`)
}

View File

@ -8,7 +8,7 @@ import { Icons, createIconRender } from "@components/Icons"
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/quickNav"
import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/@mobile/quickNav"
import PlayerView from "@pages/@mobile-views/player"
import CreatorView from "@pages/@mobile-views/creator"

View File

@ -9,6 +9,72 @@ import { css } from "@emotion/css"
import "./index.less"
// TODO: Finish me pleassse
export class DraggableDrawerController extends Component {
constructor(props) {
super(props)
this.interface = {
open: this.open,
close: this.close,
}
this.state = {
drawers: []
}
}
componentDidMount() {
app.layout.draggable = this.interface
}
open = (id, render, options = {}) => {
this.setState({
drawers: [
...this.state.drawers,
{
id: id,
locked: options.defaultLocked ?? false,
render: <DraggableDrawer
onRequestClose={this.close.bind(this, id)}
close={this.close.bind(this, id)}
open={true}
destroyOnClose={true}
{...options.props ?? {}}
>
{React.createElement(render)}
</DraggableDrawer>,
}
],
})
}
close = (id) => {
const drawerIndex = this.state.drawers.findIndex((drawer) => drawer.id === id)
if (drawerIndex === -1) {
console.error("Drawer not found")
return false
}
const drawer = this.state.drawers[drawerIndex]
if (drawer.locked === true){
return false
}
const drawers = this.state.drawers
drawers.splice(drawerIndex, 1)
this.setState({ drawers: drawers })
}
render() {
return this.state.drawers.map((drawer) => drawer.render)
}
}
export default class DraggableDrawer extends Component {
static propTypes = {
open: PropTypes.bool.isRequired,

View File

@ -1,7 +1,93 @@
import React from "react"
import { EventBus } from "evite"
import { Drawer as AntdDrawer } from "antd"
import DraggableDrawer from "../draggableDrawer"
import classnames from "classnames"
import { Motion, spring } from "react-motion"
import "./index.less"
export class Drawer extends React.Component {
options = this.props.options ?? {}
state = {
visible: false,
}
toggleVisibility = (to) => {
to = to ?? !this.state.visible
this.setState({ visible: to })
}
close = async () => {
if (typeof this.options.onClose === "function") {
this.options.onClose()
}
this.toggleVisibility(false)
this.props.controller.close(this.props.id, {
delay: 500
})
}
handleDone = (...context) => {
if (typeof this.options.onDone === "function") {
this.options.onDone(this, ...context)
}
}
handleFail = (...context) => {
if (typeof this.options.onFail === "function") {
this.options.onFail(this, ...context)
}
}
componentDidMount = async () => {
if (typeof this.props.controller === "undefined") {
throw new Error(`Cannot mount an drawer without an controller`)
}
if (typeof this.props.children === "undefined") {
throw new Error(`Empty component`)
}
this.toggleVisibility(true)
}
render() {
const componentProps = {
...this.options.componentProps,
close: this.close,
handleDone: this.handleDone,
handleFail: this.handleFail,
}
return <Motion
key={this.props.id}
style={{
x: spring(!this.state.visible ? 100 : 0),
opacity: spring(!this.state.visible ? 0 : 1),
}}
>
{({ x, opacity }) => {
return <div
key={this.props.id}
id={this.props.id}
className="drawer"
style={{
...this.options.style,
transform: `translateX(-${x}%)`,
opacity: opacity,
}}
>
{
React.createElement(this.props.children, componentProps)
}
</div>
}}
</Motion>
}
}
export default class DrawerController extends React.Component {
constructor(props) {
@ -11,19 +97,99 @@ export default class DrawerController extends React.Component {
addresses: {},
refs: {},
drawers: [],
maskVisible: false,
maskRender: false,
}
app.layout.drawer = {
this.interface = {
open: this.open,
close: this.close,
closeAll: this.closeAll,
drawersLength: () => this.state.drawers.length,
isMaskVisible: () => this.state.maskVisible,
}
}
sendEvent = (id, ...context) => {
const ref = this.state.refs[id]?.current
return ref.events.emit(...context)
componentDidMount = () => {
app.layout["drawer"] = this.interface
this.listenEscape()
}
componentWillUnmount = () => {
delete app.layout["drawer"]
this.unlistenEscape()
}
componentWillUpdate = (prevProps, prevState) => {
// is mask visible, hide sidebar with `app.layout.sidebar.toggleVisibility(false)`
if (prevState.maskVisible !== this.state.maskVisible) {
app.layout.sidebar.toggleVisibility(false)
} else if (prevState.maskRender !== this.state.maskRender) {
app.layout.sidebar.toggleVisibility(true)
}
}
listenEscape = () => {
document.addEventListener("keydown", this.handleEscKeyPress)
}
unlistenEscape = () => {
document.removeEventListener("keydown", this.handleEscKeyPress)
}
handleEscKeyPress = (event) => {
if (this.state.drawers.length === 0) {
return false
}
let isEscape = false
if ("key" in event) {
isEscape = event.key === "Escape" || event.key === "Esc"
} else {
isEscape = event.keyCode === 27
}
if (isEscape) {
this.closeLastDrawer()
}
}
getLastDrawer = () => {
return this.state.drawers[this.state.drawers.length - 1].ref.current
}
closeLastDrawer = () => {
const lastDrawer = this.getLastDrawer()
if (lastDrawer) {
lastDrawer.close()
}
}
toggleMaskVisibility = async (to) => {
to = to ?? !this.state.maskVisible
this.setState({
maskVisible: to,
})
if (to === true) {
this.setState({
maskRender: true
})
} else {
await new Promise((resolve) => {
setTimeout(resolve, 500)
})
this.setState({
maskRender: false
})
}
}
open = (id, component, options) => {
@ -46,33 +212,37 @@ export default class DrawerController extends React.Component {
addresses[id] = drawers.length - 1
refs[id] = instance.ref
} else {
const ref = refs[id].current
const isLocked = ref.state.locked
if (!isLocked) {
drawers[addresses[id]] = <Drawer {...instance} />
refs[id] = instance.ref
} else {
console.warn("Cannot update an locked drawer.")
}
drawers[addresses[id]] = <Drawer {...instance} />
refs[id] = instance.ref
}
this.setState({ refs, addresses, drawers })
this.setState({
refs,
addresses,
drawers,
})
this.toggleMaskVisibility(true)
}
close = (id) => {
close = async (id, { delay = 0 }) => {
let { addresses, drawers, refs } = this.state
const index = addresses[id]
const ref = this.state.refs[id]?.current
if (typeof ref === "undefined") {
return console.warn("This drawer not exists")
}
if (ref.state.locked && ref.state.visible) {
return console.warn("This drawer is locked and cannot be closed")
if (drawers.length === 1) {
this.toggleMaskVisibility(false)
}
if (delay > 0) {
await new Promise((resolve) => {
setTimeout(resolve, delay)
})
}
if (typeof drawers[index] !== "undefined") {
@ -82,7 +252,11 @@ export default class DrawerController extends React.Component {
delete addresses[id]
delete refs[id]
this.setState({ addresses, drawers })
this.setState({
refs,
addresses,
drawers,
})
}
closeAll = () => {
@ -92,129 +266,34 @@ export default class DrawerController extends React.Component {
}
render() {
return this.state.drawers
}
}
return <>
<Motion
style={{
opacity: spring(this.state.maskVisible ? 1 : 0),
}}
>
{({ opacity }) => {
return <div
className="drawers-mask"
onClick={() => this.closeLastDrawer()}
style={{
opacity,
display: this.state.maskRender ? "block" : "none",
}}
/>
}}
</Motion>
export class Drawer extends React.Component {
options = this.props.options ?? {}
events = new EventBus()
state = {
type: this.options.type ?? "right",
visible: true,
locked: false,
}
componentDidMount = async () => {
if (this.options.defaultLocked) {
this.setState({ locked: true })
}
if (typeof this.props.controller === "undefined") {
throw new Error(`Cannot mount an drawer without an controller`)
}
if (typeof this.props.children === "undefined") {
throw new Error(`Empty component`)
}
}
toggleVisibility = (to) => {
this.setState({ visible: to ?? !this.state.visible })
}
lock = async () => {
return await this.setState({ locked: true })
}
unlock = async () => {
return await this.setState({ locked: false })
}
close = async ({
unlock = false
} = {}) => {
return new Promise(async (resolve) => {
if (unlock) {
await this.setState({ locked: false })
}
if (this.state.locked && !unlock) {
return console.warn("Cannot close a locked drawer")
}
this.toggleVisibility(false)
this.events.emit("beforeClose")
setTimeout(() => {
if (typeof this.options.onClose === "function") {
this.options.onClose()
}
this.props.controller.close(this.props.id)
resolve()
}, 500)
})
}
sendEvent = (...context) => {
return this.props.controller.sendEvent(this.props.id, ...context)
}
handleDone = (...context) => {
if (typeof this.options.onDone === "function") {
this.options.onDone(this, ...context)
}
}
handleFail = (...context) => {
if (typeof this.options.onFail === "function") {
this.options.onFail(this, ...context)
}
}
render() {
const drawerProps = {
...this.options.props,
ref: this.props.ref,
key: this.props.id,
onRequestClose: this.close,
onClose: this.close,
open: this.state.visible,
containerElementClass: "drawer",
modalElementClass: "body",
destroyOnClose: true,
}
const componentProps = {
...this.options.componentProps,
locked: this.state.locked,
lock: this.lock,
unlock: this.unlock,
events: this.events,
close: this.close,
handleDone: this.handleDone,
handleFail: this.handleFail,
}
switch (this.options.type) {
case "drawer": {
return <AntdDrawer {...drawerProps}>
<div
className={classnames(
"drawers-wrapper",
{
React.createElement(this.props.children, componentProps)
["hidden"]: !this.state.drawers.length,
}
</AntdDrawer>
}
default: {
return <DraggableDrawer {...drawerProps}>
{
React.createElement(this.props.children, componentProps)
}
</DraggableDrawer>
}
}
)}
>
{this.state.drawers}
</div>
</>
}
}

View File

@ -0,0 +1,66 @@
@import "@styles/vars.less";
.drawers-wrapper {
position: fixed;
top: 0;
left: 0;
z-index: 500;
display: flex;
flex-direction: row;
padding: @sidebar_padding;
margin-left: calc(@sidebar_padding * 2);
height: 100dvh;
height: 100vh;
// &.hidden {
// display: none;
// }
}
.drawers-mask {
position: fixed;
top: 0;
left: 0;
z-index: 500;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
}
.drawer {
position: relative;
z-index: 550;
top: 0;
left: 0;
bottom: 0;
width: fit-content;
min-width: 320px;
height: 100%;
padding: 20px;
background-color: var(--background-color-accent);
border-radius: @sidebar_borderRadius;
box-shadow: @card-shadow;
border: 1px solid var(--sidebar-background-color);
overflow-x: hidden;
overflow-y: overlay;
}

View File

@ -1,118 +1,10 @@
import React from "react"
import { Modal as AntdModal } from "antd"
import classnames from "classnames"
import { Icons } from "@components/Icons"
import Modal from "./modal"
import useLayoutInterface from "@hooks/useLayoutInterface"
import "./index.less"
class Modal extends React.Component {
state = {
visible: false,
}
contentRef = React.createRef()
escTimeout = null
componentDidMount() {
setTimeout(() => {
this.setState({
visible: true,
})
}, 10)
document.addEventListener("keydown", this.handleEsc, false)
}
componentWillUnmount() {
document.removeEventListener("keydown", this.handleEsc, false)
}
close = () => {
this.setState({
visible: false,
})
setTimeout(() => {
if (typeof this.props.onClose === "function") {
this.props.onClose()
}
}, 250)
}
handleEsc = (e) => {
if (e.key === "Escape") {
if (this.escTimeout !== null) {
clearTimeout(this.escTimeout)
return this.close()
}
this.escTimeout = setTimeout(() => {
this.escTimeout = null
}, 250)
}
}
handleClickOutside = (e) => {
if (this.props.confirmOnOutsideClick) {
return AntdModal.confirm({
title: this.props.confirmOnClickTitle ?? "Are you sure?",
content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
onOk: () => {
this.close()
}
})
}
return this.close()
}
render() {
return <div
className={classnames(
"app_modal_wrapper",
{
["active"]: this.state.visible,
["framed"]: this.props.framed,
}
)}
>
<div
id="mask_trigger"
onTouchEnd={this.handleClickOutside}
onMouseDown={this.handleClickOutside}
/>
<div
className="app_modal_content"
ref={this.contentRef}
style={this.props.frameContentStyle}
>
{
this.props.includeCloseButton && <div
className="app_modal_close"
onClick={this.close}
>
<Icons.MdClose />
</div>
}
{
React.cloneElement(this.props.children, {
close: this.close
})
}
</div>
</div>
}
}
export default () => {
const modalRef = React.useRef()
function openModal(
function open(
id,
render,
{
@ -127,7 +19,6 @@ export default () => {
} = {}
) {
app.cores.window_mng.render(id, <Modal
ref={modalRef}
onClose={() => {
app.cores.window_mng.close(id)
}}
@ -143,13 +34,13 @@ export default () => {
</Modal>)
}
function closeModal() {
modalRef.current.close()
function close(id) {
app.cores.window_mng.close(id)
}
useLayoutInterface("modal", {
open: openModal,
close: closeModal,
open: open,
close: close,
})
return null

View File

@ -1,117 +0,0 @@
@import "@styles/vars.less";
.app_modal_wrapper {
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
transition: all 150ms ease-in-out;
#mask_trigger {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
&.framed {
.app_modal_content {
display: flex;
flex-direction: column;
padding: 30px;
background-color: var(--background-color-accent);
border-radius: 12px;
}
}
&.active {
background-color: rgba(var(--bg_color_6), 0.1);
backdrop-filter: blur(@modal_background_blur);
-webkit-backdrop-filter: blur(@modal_background_blur);
.app_modal_content {
opacity: 1;
transform: translateY(0);
}
}
.app_modal_content {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateY(100px);
height: fit-content;
width: 40vw;
max-width: 600px;
transition: all 150ms ease-in-out;
.app_modal_close {
position: sticky;
align-self: flex-end;
top: 0;
right: 0;
font-size: 1.5rem;
cursor: pointer;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 5px;
border-radius: 12px;
svg {
margin: 0;
color: var(--text-color);
}
}
// fixments
.postCreator {
box-shadow: @card-shadow;
-webkit-box-shadow: @card-shadow;
-moz-box-shadow: @card-shadow;
}
.searcher {
box-sizing: border-box;
width: 48vw;
height: 80vh;
}
}
}

View File

@ -0,0 +1,110 @@
import React from "react"
import { Modal as AntdModal } from "antd"
import classnames from "classnames"
import { Icons } from "@components/Icons"
import "./index.less"
class Modal extends React.Component {
state = {
visible: false,
}
contentRef = React.createRef()
escTimeout = null
componentDidMount() {
setTimeout(() => {
this.setState({
visible: true,
})
}, 10)
document.addEventListener("keydown", this.handleEsc, false)
}
componentWillUnmount() {
document.removeEventListener("keydown", this.handleEsc, false)
}
close = () => {
this.setState({
visible: false,
})
setTimeout(() => {
if (typeof this.props.onClose === "function") {
this.props.onClose()
}
}, 250)
}
handleEsc = (e) => {
if (e.key === "Escape") {
if (this.escTimeout !== null) {
clearTimeout(this.escTimeout)
return this.close()
}
this.escTimeout = setTimeout(() => {
this.escTimeout = null
}, 250)
}
}
handleClickOutside = (e) => {
if (this.props.confirmOnOutsideClick) {
return AntdModal.confirm({
title: this.props.confirmOnClickTitle ?? "Are you sure?",
content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
onOk: () => {
this.close()
}
})
}
return this.close()
}
render() {
return <div
className={classnames(
"app_modal_wrapper",
{
["active"]: this.state.visible,
["framed"]: this.props.framed,
}
)}
>
<div
id="mask_trigger"
onTouchEnd={this.handleClickOutside}
onMouseDown={this.handleClickOutside}
/>
<div
className="app_modal_content"
ref={this.contentRef}
style={this.props.frameContentStyle}
>
{
this.props.includeCloseButton && <div
className="app_modal_close"
onClick={this.close}
>
<Icons.MdClose />
</div>
}
{
React.cloneElement(this.props.children, {
close: this.close
})
}
</div>
</div>
}
}
export default Modal

View File

@ -0,0 +1,117 @@
@import "@styles/vars.less";
.app_modal_wrapper {
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
transition: all 150ms ease-in-out;
#mask_trigger {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
&.framed {
.app_modal_content {
display: flex;
flex-direction: column;
padding: 30px;
background-color: var(--background-color-accent);
border-radius: 12px;
}
}
&.active {
background-color: rgba(var(--bg_color_6), 0.1);
backdrop-filter: blur(@modal_background_blur);
-webkit-backdrop-filter: blur(@modal_background_blur);
.app_modal_content {
opacity: 1;
transform: translateY(0);
}
}
.app_modal_content {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateY(100px);
height: fit-content;
width: 40vw;
max-width: 600px;
transition: all 150ms ease-in-out;
.app_modal_close {
position: sticky;
align-self: flex-end;
top: 0;
right: 0;
font-size: 1.5rem;
cursor: pointer;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 5px;
border-radius: 12px;
svg {
margin: 0;
color: var(--text-color);
}
}
// fixments
.postCreator {
box-shadow: @card-shadow;
-webkit-box-shadow: @card-shadow;
-moz-box-shadow: @card-shadow;
}
.searcher {
box-sizing: border-box;
width: 48vw;
height: 80vh;
}
}
}

View File

@ -4,19 +4,69 @@ import classnames from "classnames"
import { Translation } from "react-i18next"
import { Motion, spring } from "react-motion"
import { Menu, Avatar, Dropdown } from "antd"
import Drawer from "@layouts/components/drawer"
import { Icons, createIconRender } from "@components/Icons"
import { GiLockedChest } from "react-icons/gi"
import sidebarItems from "@config/sidebar"
import "./index.less"
const builtInApps = [
{
key: "hb",
label: "Hotel",
icon: "MdGames",
location: "/apps/hb",
},
{
key: "pay",
label: "Pay",
icon: "MdPayment",
location: "/apps/pay",
},
{
key: "loots",
label: "Loots",
icon: <GiLockedChest />,
location: "/apps/loots",
}
]
const AppDrawer = (props) => {
return <div className="app-drawer">
<h1>Apps</h1>
{
builtInApps.map((item) => {
return <div
key={item.key}
className="app-drawer_item"
onClick={() => {
if (item.location) {
app.location.push(item.location)
}
props.close()
}}
>
<h3>{item.icon && createIconRender(item.icon)} {item.label}</h3>
</div>
})
}
</div>
}
const onClickHandlers = {
apps: () => {
app.layout.drawer.open("apps", AppDrawer)
},
addons: () => {
window.app.location.push("/addons")
window.app.location.push("/addons")
},
studio: () => {
window.app.location.push("/studio")
window.app.location.push("/studio")
},
settings: () => {
window.app.navigation.goToSettings()
@ -28,12 +78,12 @@ const onClickHandlers = {
window.app.controls.openSearcher()
},
messages: () => {
window.app.controls.openMessages()
window.app.controls.openMessages()
},
create: () => {
window.app.controls.openCreator()
},
account: () => {
profile: () => {
window.app.navigation.goToAccount()
},
login: () => {
@ -84,6 +134,13 @@ const BottomMenuDefaultItems = [
</Translation>,
icon: <Icons.Bell />,
},
{
key: "apps",
label: <Translation>
{(t) => t("Apps")}
</Translation>,
icon: <Icons.MdApps />,
},
{
key: "settings",
label: <Translation>
@ -258,15 +315,7 @@ export default class Sidebar extends React.Component {
}
events = {
"sidedrawers.visible": (has) => {
this.setState({
lockAutocollapse: has
})
if (!has && this.state.expanded) {
this.interface.toggleCollapse(false)
}
}
}
componentDidMount = async () => {
@ -316,7 +365,7 @@ export default class Sidebar extends React.Component {
return app.location.push(`/${item.path ?? e.key}`, 150)
}
onMouseEnter = () => {
onMouseEnter = (event) => {
if (!this.state.visible) return
if (window.app.cores.settings.is("sidebar.collapsable", false)) {
@ -327,10 +376,15 @@ export default class Sidebar extends React.Component {
return
}
// do nothing if is mask visible
if (app.layout.drawer.isMaskVisible()) {
return false
}
this.interface.toggleCollapse(true)
}
handleMouseLeave = () => {
handleMouseLeave = (event) => {
if (!this.state.visible) return
if (window.app.cores.settings.is("sidebar.collapsable", false)) return
@ -431,10 +485,12 @@ export default class Sidebar extends React.Component {
}
ref={this.sidebarRef}
>
<div className="app_sidebar_header">
<div className="app_sidebar_header_logo">
<img src={config.logo?.alt} />
<img
src={config.logo?.alt}
onClick={() => app.navigation.goMain()}
/>
</div>
</div>
@ -463,6 +519,8 @@ export default class Sidebar extends React.Component {
/>
</div>
</div>
<Drawer />
</div>
}}
</Motion>

View File

@ -32,6 +32,9 @@
.app_sidebar {
position: relative;
z-index: 1000;
display: flex;
flex-direction: column;
@ -80,6 +83,11 @@
padding: 0;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
.app_sidebar_header {
display: flex;
flex-direction: column;
@ -174,9 +182,9 @@
&.user_avatar {
.ant-menu-title-content {
width: 100%;
display: inline-flex;
align-items: flex-start;
justify-content: center;
@ -192,8 +200,6 @@
}
}
padding: 0 !important;
}
}

View File

@ -1,262 +0,0 @@
import React from "react"
import classnames from "classnames"
import { Motion, spring } from "react-motion"
import "./index.less"
export class Sidedrawer extends React.Component {
state = {
visible: false,
}
toggleVisibility = (to) => {
this.setState({ visible: to ?? !this.state.visible })
}
render() {
return <Motion style={{
x: spring(!this.state.visible ? 100 : 0),
opacity: spring(!this.state.visible ? 0 : 1),
}}>
{({ x, opacity }) => {
return <div
key={this.props.id}
id={this.props.id}
className={classnames(
"sidedrawer",
{
"first": this.props.first
}
)}
style={{
...this.props.style,
transform: `translateX(-${x}%)`,
opacity: opacity,
}}
>
{
React.createElement(this.props.children, {
...this.props.props,
close: this.props.close,
})
}
</div>
}}
</Motion>
}
}
export default class SidedrawerController extends React.Component {
constructor(props) {
super(props)
this.interface = app.layout.sidedrawer = {
open: this.open,
close: this.close,
closeAll: this.closeAll,
hasDrawers: this.state.drawers.length > 0,
toggleGlobalVisibility: () => {
this.setState({
globalVisible: !this.state.globalVisible,
})
}
}
}
state = {
globalVisible: true,
drawers: [],
lockedIds: [],
}
componentDidMount = () => {
this.listenEscape()
}
componentWillUnmount = () => {
this.unlistenEscape()
}
drawerIsLocked = (id) => {
return this.state.lockedIds.includes(id)
}
lockDrawerId = (id) => {
this.setState({
lockedIds: [...this.state.lockedIds, id],
})
}
unlockDrawer = (id) => {
this.setState({
lockedIds: this.state.lockedIds.filter(lockedId => lockedId !== id),
})
}
open = async (id, component, options = {}) => {
if (typeof id !== "string") {
options = component
component = id
id = component.key ?? component.id ?? Math.random().toString(36).substr(2, 9)
}
const drawers = this.state.drawers
// check if id is already in use
// but only if its allowed to be used multiple times
const existentDrawer = drawers.find((drawer) => drawer.props.id === id)
if (existentDrawer) {
if (!existentDrawer.props.allowMultiples) {
console.warn(`Sidedrawer with id "${id}" already exists.`)
return false
}
// fix id appending the corresponding array index at the end of the id
// ex ["A", "B", "C"] => ["A", "B", "C", "A-1"]
// to prevent id collisions
let index = 0
let newId = id
while (drawers.find(drawer => drawer.props.id === newId)) {
index++
newId = id + "-" + index
}
id = newId
}
const drawerProps = {
id: id,
allowMultiples: options.allowMultiples ?? false,
escClosable: options.escClosable ?? true,
first: drawers.length === 0,
style: {
zIndex: 100 - drawers.length,
},
ref: React.createRef(),
close: this.close,
lock: () => this.lockDrawerId(id),
unlock: () => this.unlockDrawer(id),
}
drawers.push(React.createElement(Sidedrawer, drawerProps, component))
if (options.lock) {
this.lockDrawerId(id)
}
await this.setState({
drawers,
})
setTimeout(() => {
this.toggleDrawerVisibility(id, true)
}, 10)
window.app.eventBus.emit("sidedrawer.open")
if (this.state.drawers.length > 0) {
app.eventBus.emit("sidedrawers.visible", true)
}
}
toggleDrawerVisibility = (id, to) => {
// find drawer
const drawer = this.state.drawers.find(drawer => drawer.props.id === id)
if (!drawer) {
console.warn(`Sidedrawer with id "${id}" does not exist.`)
return
}
if (!drawer.ref.current) {
console.warn(`Sidedrawer with id "${id}" has not valid ref.`)
return
}
return drawer.ref.current.toggleVisibility(to)
}
close = (id) => {
// if an id is provided filter by key
// else close the last opened drawer
let drawers = this.state.drawers
let drawerId = id ?? drawers[drawers.length - 1].props.id
// check if id is locked
if (this.drawerIsLocked(id)) {
console.warn(`Sidedrawer with id "${id}" is locked.`)
return false
}
// check if id exists
const drawer = drawers.find(drawer => drawer.props.id === drawerId)
if (!drawer) {
console.warn(`Sidedrawer with id "${id}" does not exist.`)
return false
}
// emit event
window.app.eventBus.emit("sidedrawer.close")
// toggleVisibility off
this.toggleDrawerVisibility(drawerId, false)
// await drawer transition
setTimeout(() => {
// remove drawer
drawers = drawers.filter(drawer => drawer.props.id !== drawerId)
this.setState({ drawers })
if (this.state.drawers.length === 0) {
app.eventBus.emit("sidedrawers.visible", false)
}
}, 500)
}
listenEscape = () => {
document.addEventListener("keydown", this.handleEscKeyPress)
}
unlistenEscape = () => {
document.removeEventListener("keydown", this.handleEscKeyPress)
}
handleEscKeyPress = (event) => {
// avoid handle keypress when is nothing to render
if (this.state.drawers.length === 0) {
return false
}
let isEscape = false
if ("key" in event) {
isEscape = event.key === "Escape" || event.key === "Esc"
} else {
isEscape = event.keyCode === 27
}
if (isEscape) {
// close the last opened drawer
this.close()
}
}
render() {
return <div
className={classnames(
"sidedrawers-wrapper",
{
["hidden"]: !this.state.drawers.length || this.state.globalVisible,
}
)}
>
{this.state.drawers}
</div>
}
}

View File

@ -1,48 +0,0 @@
@import "@styles/vars.less";
.sidedrawers-wrapper {
position: relative;
top: 0;
right: 0;
display: flex;
flex-direction: row;
padding: @sidebar_padding;
height: 100dvh;
height: 100vh;
.sidedrawer {
position: absolute;
z-index: 100;
top: 0;
left: 0;
bottom: 0;
margin-top: @sidebar_padding;
height: calc(100% - @sidebar_padding * 2);
width: auto;
min-width: 400px;
max-width: 15vw;
background-color: var(--sidedrawer-background-color);
border-radius: @sidebar_borderRadius;
padding: 20px;
overflow-x: hidden;
overflow-y: overlay;
box-shadow: @card-shadow;
}
&.hidden{
width: 0;
padding: 0;
}
}

View File

@ -3,26 +3,24 @@ import classnames from "classnames"
import { Layout } from "antd"
import Sidebar from "@layouts/components/sidebar"
import Drawer from "@layouts/components/drawer"
import Sidedrawer from "@layouts/components/sidedrawer"
import BottomBar from "@layouts/components/bottomBar"
import TopBar from "@layouts/components/topBar"
import ToolsBar from "@layouts/components/toolsBar"
import Header from "@layouts/components/header"
import InitializeModalsController from "@layouts/components/modals"
import Modals from "@layouts/components/modals"
// mobile components
import { DraggableDrawerController } from "@layouts/components/draggableDrawer"
import BottomBar from "@layouts/components/@mobile/bottomBar"
import TopBar from "@layouts/components/@mobile/topBar"
import BackgroundDecorator from "@components/BackgroundDecorator"
const DesktopLayout = (props) => {
InitializeModalsController()
return <>
<BackgroundDecorator />
<Modals />
<Layout id="app_layout" className="app_layout">
<Drawer />
<Sidebar />
<Sidedrawer />
<Layout.Content
id="content_layout"
@ -45,6 +43,7 @@ const DesktopLayout = (props) => {
const MobileLayout = (props) => {
return <Layout id="app_layout" className="app_layout">
<DraggableDrawerController />
<TopBar />
<Layout.Content
@ -61,7 +60,6 @@ const MobileLayout = (props) => {
</Layout.Content>
<BottomBar />
<Drawer />
</Layout>
}

View File

@ -1,14 +1,15 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { DraggableDrawerController } from "@layouts/components/draggableDrawer"
import Drawer from "@layouts/components/drawer"
import Sidedrawer from "@layouts/components/sidedrawer"
export default (props) => {
return <antd.Layout className={classnames("app_layout")} style={{ height: "100%" }}>
<Drawer />
<Sidedrawer />
<DraggableDrawerController />
<div id="transitionLayer" className="fade-transverse-active">
{React.cloneElement(props.children, props)}
</div>

View File

@ -1,31 +1,16 @@
import React from "react"
import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl"
import "./index.mobile.less"
export default (props) => {
const [wallpaperData, setWallpaperData] = React.useState(null)
const setRandomWallpaper = async () => {
const { data: featuredWallpapers } = await app.cores.api.customRequest({
method: "GET",
url: "/featured_wallpapers"
}).catch((err) => {
console.error(err)
return []
})
// get random wallpaper from array
const randomWallpaper = featuredWallpapers[Math.floor(Math.random() * featuredWallpapers.length)]
setWallpaperData(randomWallpaper)
}
const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
React.useEffect(() => {
if (app.userData) {
app.navigation.goMain()
} else {
setRandomWallpaper()
app.controls.openLoginForm({
defaultLocked: true,
})
@ -35,7 +20,7 @@ export default (props) => {
return <div className="loginPage">
<div
style={{
backgroundImage: `url(${wallpaperData?.url})`,
backgroundImage: `url(${randomWallpaperURL})`,
}}
className="wallpaper"
>

View File

@ -64,6 +64,7 @@ const PlayerController = React.forwardRef((props, ref) => {
setDraggingTime(false)
app.cores.player.seek(seekTime)
syncPlayback()
}
@ -91,7 +92,6 @@ const PlayerController = React.forwardRef((props, ref) => {
React.useEffect(() => {
setTitleIsOverflown(isOverflown(titleRef.current))
setTrackDuration(app.cores.player.duration())
console.log(context.track_manifest)
}, [context.track_manifest])
React.useEffect(() => {
@ -104,7 +104,7 @@ const PlayerController = React.forwardRef((props, ref) => {
className={classnames(
"lyrics-player-controller-wrapper",
{
["hidden"]: hide,
["hidden"]: props.lyrics?.video_source && hide,
}
)}
onMouseEnter={onMouseEnter}

View File

@ -25,10 +25,14 @@ const EnchancedLyrics = (props) => {
async function loadLyrics(track_id) {
const result = await MusicService.getTrackLyrics(track_id, {
preferTranslation: translationEnabled,
}).catch((err) => {
return null
})
if (result) {
setLyrics(result)
} else {
setLyrics(false)
}
}
@ -49,7 +53,7 @@ const EnchancedLyrics = (props) => {
//* Handle when context change track_manifest
React.useEffect(() => {
setLyrics(null)
if (context.track_manifest) {
loadLyrics(context.track_manifest._id)
}
@ -72,6 +76,20 @@ const EnchancedLyrics = (props) => {
}
)}
>
{
!lyrics?.video_source && <div
className="lyrics-background-wrapper"
>
<div
className="lyrics-background-cover"
>
<img
src={context.track_manifest.cover}
/>
</div>
</div>
}
<LyricsVideo
ref={videoRef}
lyrics={lyrics}

View File

@ -3,6 +3,9 @@
z-index: 100;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
@ -12,6 +15,36 @@
}
}
.lyrics-background-wrapper {
z-index: 100;
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
.lyrics-background-cover {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
padding: 80px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.lyrics-video {
z-index: 105;
position: absolute;

View File

@ -40,7 +40,6 @@ const ChatPage = (props) => {
isRemoteTyping,
} = useChat(to_user_id)
console.log(R_User)
async function submitMessage(e) {

View File

@ -1,7 +1,6 @@
import React from "react"
import * as antd from "antd"
import ChatsService from "@models/chats"
import TimeAgo from "@components/TimeAgo"

View File

@ -27,6 +27,13 @@ export default (props) => {
case "profile": {
return app.navigation.goToAccount(result.behavior.value)
}
case "random_list": {
const values = result.behavior.value.split(";")
const index = Math.floor(Math.random() * values.length)
return window.location.href = values[index]
}
}
}

View File

@ -1,7 +1,7 @@
import React from "react"
import * as antd from "antd"
import { version as linebridgeVersion } from "linebridge/package.json"
// import { version as linebridgeVersion } from "linebridge/package.json"
import { Icons } from "@components/Icons"
import LatencyIndicator from "@components/PerformanceIndicators/latency"
@ -157,6 +157,20 @@ export default {
</div>
<div className="group">
<div className="inline_field">
<div className="field_header">
<div className="field_icon">
<Icons.MdInfo />
</div>
<p>Platform</p>
</div>
<div className="field_value">
{Capacitor.platform}
</div>
</div>
<div className="inline_field">
<div className="field_header">
<div className="field_icon">
@ -177,21 +191,7 @@ export default {
<Icons.MdInfo />
</div>
<p>Linebridge Engine</p>
</div>
<div className="field_value">
{linebridgeVersion ?? globalThis._linebrige_version ?? "Unknown"}
</div>
</div>
<div className="inline_field">
<div className="field_header">
<div className="field_icon">
<Icons.MdInfo />
</div>
<p>Evite Framework</p>
<p>Engine</p>
</div>
<div className="field_value">
@ -205,7 +205,7 @@ export default {
<Icons.MdInfo />
</div>
<p>Comty.JS</p>
<p>Comty.js</p>
</div>
<div className="field_value">
@ -213,20 +213,6 @@ export default {
</div>
</div>
<div className="inline_field">
<div className="field_header">
<div className="field_icon">
<Icons.MdInfo />
</div>
<p>Platform</p>
</div>
<div className="field_value">
{Capacitor.platform}
</div>
</div>
{
capInfo && <div className="inline_field">
<div className="field_header">

View File

@ -0,0 +1,81 @@
import React from "react"
import * as antd from "antd"
import "./index.less"
const useGetMainOrigin = () => {
const [mainOrigin, setMainOrigin] = React.useState(null)
React.useEffect(() => {
const instance = app.cores.api.client()
if (instance) {
setMainOrigin(instance.mainOrigin)
}
return () => {
setMainOrigin(null)
}
}, [])
return mainOrigin
}
export default {
id: "api",
icon: "TbApi",
label: "API",
group: "advanced",
render: () => {
const mainOrigin = useGetMainOrigin()
const [keys, setKeys] = React.useState([])
return <div className="developer-settings">
<div className="card">
<h3>
Main Origin
</h3>
<p>
{mainOrigin}
</p>
</div>
<div className="card api_keys">
<div className="api_keys_header">
<div className="api_keys_header_title">
<h3>Your Keys</h3>
<p>Manage your API keys</p>
</div>
<antd.Button
type="primary"
>
Create new
</antd.Button>
</div>
<div className="api_keys_list">
{
keys.map((key) => {
return null
})
}
{
keys.length === 0 && <antd.Empty />
}
</div>
</div>
<div className="card">
<h3>Documentations</h3>
<div className="links">
<a>Comty CLI</a>
<a>Comty.JS for NodeJS</a>
<a>Comty Extensions SDK</a>
<a>Spectrum API</a>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,32 @@
.developer-settings {
display: flex;
flex-direction: column;
gap: 20px;
}
.api_keys {
display: flex;
flex-direction: column;
background-color: var(--background-color-accent);
padding: 10px;
border-radius: 12px;
.api_keys_header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.links {
display: flex;
flex-direction: column;
gap: 5px;
}

View File

@ -14,7 +14,6 @@ export default {
{
id: "style:variant_mode",
group: "aspect",
component: "Switch",
icon: "Moon",
title: "Theme",
description: "Change the theme of the application.",
@ -52,7 +51,7 @@ export default {
max: 1.2,
step: 0.01,
tooltip: {
formatter: (value) => `${value}x`
formatter: (value) => `x${value}`
}
},
defaultValue: () => {
@ -143,8 +142,6 @@ export default {
defaultValue: () => {
const value = app.cores.style.getVar("backgroundImage")
console.log(value)
return value ? value.replace(/url\(|\)/g, "") : ""
},
onUpdate: (value) => {

View File

@ -14,7 +14,7 @@ import "./index.less"
const FetchChangelogs = async () => {
const response = await app.cores.api.customRequest({
method: "GET",
url: `/release-notes`,
url: `/repo/releases-notes`,
})
return response.data

View File

@ -125,11 +125,7 @@ const ChangePasswordComponent = (props) => {
export default (props) => {
const onClick = () => {
if (app.isMobile) {
return app.layout.drawer.open("passwordChange", ChangePasswordComponent)
}
return app.layout.sidedrawer.open("passwordChange", ChangePasswordComponent)
return app.layout.drawer.open("passwordChange", ChangePasswordComponent)
}
return <antd.Button onClick={onClick}>

View File

@ -7,6 +7,8 @@
border: 1px var(--border-color) solid;
border-radius: 12px;
width: fit-content;
overflow: hidden;
.__setting_theme_variant_selector-variant {

View File

@ -60,7 +60,7 @@ export default {
group: "ui.sounds",
component: "Switch",
icon: "MdVolumeUp",
title: "UI effects",
title: "Effects",
description: "Enable the UI effects.",
mobile: false,
},
@ -70,8 +70,8 @@ export default {
group: "ui.sounds",
component: "Slider",
icon: "MdVolumeUp",
title: "UI volume",
description: "Set the volume of the app sounds.",
title: "Volume",
description: "Set the volume of the app UI sounds.",
props: {
tipFormatter: (value) => {
return `${value}%`
@ -108,7 +108,7 @@ export default {
group: "notifications",
component: "Slider",
icon: "MdVolumeUp",
title: "Sound Volume",
title: "Volume",
description: "Set the volume of the sound when a notification is received.",
props: {
tipFormatter: (value) => {

View File

@ -152,7 +152,7 @@ const TagItem = (props) => {
<antd.Button
icon={<Icons.MdDelete />}
danger
disabled
onClick={props.onDelete}
/>
</div>
</div>
@ -192,6 +192,25 @@ class OwnTags extends React.Component {
})
}
handleTagDelete = (tag) => {
antd.Modal.confirm({
title: "Are you sure you want to delete this tag?",
content: `This action cannot be undone.`,
onOk: async () => {
NFCModel.deleteTag(tag._id)
.then(() => {
app.message.success("Tag deleted")
this.loadData()
})
.catch((err) => {
console.error(err)
app.message.error(err.message)
return false
})
}
})
}
handleTagRead = async (error, tag) => {
if (error) {
console.error(error)
@ -252,6 +271,9 @@ class OwnTags extends React.Component {
tag
})
}}
onDelete={() => {
this.handleTagDelete(tag)
}}
/>
})
}

View File

@ -137,9 +137,9 @@ export default (props) => {
</antd.Select.Option>
<antd.Select.Option
value="post"
value="random_list"
>
Post
Random list
</antd.Select.Option>
</antd.Select>
</antd.Form.Item>

View File

@ -331,4 +331,17 @@ svg {
}
}
}
}
.card {
display: flex;
flex-direction: column;
background-color: var(--background-color-accent);
padding: 10px;
border-radius: 12px;
gap: 10px;
}

View File

@ -1,23 +1,10 @@
import path from "path"
import aliases from "./aliases"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
const aliases = {
"@": path.join(__dirname, "src"),
"@config": path.join(__dirname, "config"),
"@cores": path.join(__dirname, "src/cores"),
"@pages": path.join(__dirname, "src/pages"),
"@styles": path.join(__dirname, "src/styles"),
"@components": path.join(__dirname, "src/components"),
"@contexts": path.join(__dirname, "src/contexts"),
"@utils": path.join(__dirname, "src/utils"),
"@layouts": path.join(__dirname, "src/layouts"),
"@hooks": path.join(__dirname, "src/hooks"),
"@classes": path.join(__dirname, "src/classes"),
"@models": path.join(__dirname, "../../", "comty.js/src/models"),
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
}
const oneYearInSeconds = 60 * 60 * 24 * 365
export default defineConfig({
plugins: [
@ -35,7 +22,10 @@ export default defineConfig({
https: {
key: path.join(__dirname, "ssl", "privkey.pem"),
cert: path.join(__dirname, "ssl", "cert.pem"),
}
},
headers: {
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`
},
},
css: {
preprocessorOptions: {

View File

@ -0,0 +1,8 @@
./node_modules
./build
./dist
./ssl
# secrets
./api.production.env
./.env

15
packages/server/.swcrc Normal file
View File

@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/swcrc",
"exclude":[
"node_modules/minio/**",
"node_modules/@octokit/**"
],
"module": {
"type": "commonjs",
// These are defaults.
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
}
}

View File

@ -1,27 +1,36 @@
FROM node:16-bullseye-slim
FROM node:18-bookworm-slim
EXPOSE 9000
# Install dependencies
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt update
RUN apt install --no-install-recommends curl ffmpeg python yarn build-essential -y
RUN apt install -y --no-install-recommends build-essential
RUN apt install -y --no-install-recommends curl
RUN apt install -y --no-install-recommends ffmpeg
RUN apt install -y --no-install-recommends yarn
RUN apt install -y --no-install-recommends git
RUN apt install -y --no-install-recommends ssh
RUN apt install -y --no-install-recommends ca-certificates
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
# Create workdir
RUN mkdir -p /comty-server
WORKDIR /comty-server
WORKDIR /home/node/app
# Copy Files
COPY package.json ./
COPY . .
# Fix permissions
RUN chmod -R 777 /comty-server
RUN chown -R node:node /comty-server
# Set user to node
USER node
EXPOSE 3010
COPY package.json ./
COPY --chown=node:node . .
RUN chmod -R 777 /home/node/app
RUN export NODE_ENV=production
RUN yarn global add cross-env
RUN yarn install --production
RUN yarn build
# Install modules & rebuild for host
RUN npm install
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
CMD ["yarn", "run", "run:prod"]
# Start server
RUN export NODE_ENV=production
CMD ["npm", "run", "start:prod"]

View File

@ -0,0 +1,11 @@
services:
api:
container_name: comty-api
build: .
restart: unless-stopped
ports:
- "9000:9000"
env_file:
- ./api.production.env
volumes:
- ./ssl:/comty-server/ssl

View File

@ -5,7 +5,7 @@ import Spinnies from "spinnies"
import { Observable } from "@gullerya/object-observer"
import { dots as DefaultSpinner } from "spinnies/spinners.json"
import EventEmitter from "@foxify/events"
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
import IPCRouter from "linebridge/dist/server/classes/IPCRouter"
import chokidar from "chokidar"
import { onExit } from "signal-exit"
import chalk from "chalk"

View File

@ -1,5 +1,5 @@
import httpProxy from "http-proxy"
import defaults from "linebridge/src/server/defaults"
import defaults from "linebridge/dist/server/defaults"
import pkg from "../package.json"

View File

@ -1,36 +1,54 @@
{
"name": "@comty/server",
"version": "0.70.0",
"license": "MIT",
"workspaces": [
"services/*"
],
"private": true,
"scripts": {
"build": "hermes build",
"dev": "cross-env NODE_ENV=development hermes-node ./index.js",
"run:prod": "cross-env NODE_ENV=production node ./dist/index.js"
},
"dependencies": {
"@gullerya/object-observer": "^6.1.3",
"@infisical/sdk": "^2.1.8",
"@ragestudio/hermes": "^0.1.1",
"chalk": "4.1.2",
"dotenv": "^16.4.4",
"http-proxy": "^1.18.1",
"linebridge": "^0.18.1",
"module-alias": "^2.2.3",
"nodejs-snowflake": "^2.0.1",
"signal-exit": "^4.1.0",
"spinnies": "^0.5.1",
"tree-kill": "^1.2.2"
},
"devDependencies": {
"chai": "^5.1.0",
"cross-env": "^7.0.3",
"mocha": "^10.3.0"
},
"resolutions": {
"string-width": "4.2.3"
}
"name": "@comty/server",
"version": "0.70.0",
"license": "MIT",
"workspaces": [
"services/*"
],
"private": true,
"scripts": {
"start:prod": "cross-env NODE_ENV=production hermes-node ./index.js",
"dev": "cross-env NODE_ENV=development hermes-node ./index.js",
"build:bin": "cd build && pkg ./index.js"
},
"dependencies": {
"@gullerya/object-observer": "^6.1.3",
"@infisical/sdk": "^2.1.8",
"@ragestudio/hermes": "^0.1.1",
"axios": "^1.7.4",
"bcrypt": "^5.1.1",
"chalk": "4.1.2",
"comty.js": "^0.60.3",
"dotenv": "^16.4.4",
"http-proxy": "^1.18.1",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"linebridge": "^0.20.3",
"minio": "^8.0.1",
"module-alias": "^2.2.3",
"mongoose": "^8.5.3",
"nodejs-snowflake": "^2.0.1",
"qs": "^6.13.0",
"signal-exit": "^4.1.0",
"spinnies": "^0.5.1",
"tree-kill": "^1.2.2"
},
"devDependencies": {
"@swc-node/register": "^1.10.9",
"@swc/cli": "^0.3.12",
"@swc/core": "^1.4.11",
"chai": "^5.1.0",
"cross-env": "^7.0.3",
"mocha": "^10.3.0",
"pkg": "^5.8.1"
},
"resolutions": {
"string-width": "4.2.3"
},
"pkg": {
"targets": [
"node18-linux-arm64"
],
"outputPath": "dist"
}
}

View File

@ -1,4 +1,4 @@
import { Server } from "linebridge/src/server"
import { Server } from "linebridge/dist/server"
import DbManager from "@shared-classes/DbManager"
import SharedMiddlewares from "@shared-middlewares"

View File

@ -1,10 +1,4 @@
{
"name": "auth",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"proxy":{
"namespace": "/auth",
"port": 3020
}
"version": "1.0.0"
}

View File

@ -42,17 +42,18 @@ export default async (req, res) => {
if (mfaSession) {
if (!req.body.mfa_code) {
await mfaSession.delete()
await MFASession.deleteMany({ user_id: user._id })
} else {
if (mfaSession.expires_at < new Date().getTime()) {
await mfaSession.delete()
await MFASession.deleteMany({ user_id: user._id })
throw new OperationError(401, "MFA code expired, login again...")
}
if (mfaSession.code == req.body.mfa_code) {
codeVerified = true
await mfaSession.delete()
await MFASession.deleteMany({ user_id: user._id })
} else {
throw new OperationError(401, "Invalid MFA code, try again...")
}

View File

@ -0,0 +1,7 @@
export default {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
}
}

View File

@ -1,4 +1,4 @@
import { Server } from "linebridge/src/server"
import { Server } from "linebridge/dist/server"
import DbManager from "@shared-classes/DbManager"
import RedisClient from "@shared-classes/RedisClient"

View File

@ -1,25 +1,4 @@
{
"name": "chats",
"version": "0.60.2",
"license": "MIT",
"dependencies": {
"@foxify/events": "^2.1.0",
"axios": "^1.4.0",
"bcrypt": "5.0.1",
"comty.js": "^0.58.2",
"connect-mongo": "^4.6.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"jsonwebtoken": "8.5.1",
"linebridge": "0.15.12",
"luxon": "^3.0.4",
"minio": "^7.0.32",
"moment": "2.29.4",
"moment-timezone": "0.5.37",
"mongoose": "^6.9.0",
"morgan": "^1.10.0",
"redis": "^4.6.6",
"socket.io": "^4.5.4"
}
"version": "0.60.2"
}

View File

@ -64,7 +64,7 @@ export default {
history = await Promise.all(history)
return {
total: await ChatMessage.count(query),
total: await ChatMessage.countDocuments(query),
offset: offset,
limit: limit,
order: order,

View File

@ -28,8 +28,6 @@ export default async (socket, payload, engine) => {
const targetSocket = await engine.find.socketByUserId(payload.to_user_id)
console.log(targetSocket)
if (targetSocket) {
await targetSocket.emit("chat:receive:message", wsMessageObj)
}

View File

@ -1,4 +1,4 @@
import { Server } from "linebridge/src/server"
import { Server } from "linebridge/dist/server"
import nodemailer from "nodemailer"
import DbManager from "@shared-classes/DbManager"

View File

@ -2,12 +2,6 @@
"name": "ems",
"description": "External Messaging Service (SMS, EMAIL, PUSH)",
"version": "0.1.0",
"main": "index.js",
"license": "MIT",
"proxy": {
"path": "/ems",
"port": 3007
},
"dependencies": {
"handlebars": "^4.7.8",
"nodemailer": "^6.9.11",

View File

@ -1,4 +1,4 @@
import { Server } from "linebridge/src/server"
import { Server } from "linebridge/dist/server"
import B2 from "backblaze-b2"

View File

@ -1,31 +1,15 @@
{
"name": "files",
"version": "0.60.2",
"license": "MIT",
"dependencies": {
"@foxify/events": "^2.1.0",
"axios": "^1.4.0",
"backblaze-b2": "^1.7.0",
"bcrypt": "^5.1.0",
"busboy": "^1.6.0",
"comty.js": "^0.58.2",
"connect-mongo": "^4.6.0",
"content-range": "^2.0.2",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"fluent-ffmpeg": "^2.1.2",
"jsonwebtoken": "^9.0.0",
"linebridge": "0.15.12",
"luxon": "^3.0.4",
"merge-files": "^0.1.2",
"mime-types": "^2.1.35",
"sharp": "0.32.6",
"minio": "^7.0.32",
"moment": "^2.29.4",
"moment-timezone": "^0.5.40",
"mongoose": "^6.9.0",
"morgan": "^1.10.0",
"normalize-url": "^8.0.0",
"p-map": "4.0.0",
"p-queue": "^7.3.4",

View File

@ -5,6 +5,8 @@ import ChunkFileUpload from "@shared-classes/ChunkFileUpload"
import RemoteUpload from "@services/remoteUpload"
const availableProviders = ["b2", "standard"]
export default {
useContext: ["cache", "limits"],
middlewares: [
@ -34,6 +36,11 @@ export default {
limits.useProvider = req.headers["provider-type"] ?? "b2"
}
// check if provider is valid
if (!availableProviders.includes(limits.useProvider)) {
throw new OperationError(400, "Invalid provider")
}
let build = await ChunkFileUpload(req, {
tmpDir: tmpPath,
...limits,

View File

@ -1,58 +0,0 @@
import { User, Session, Post } from "@db_models"
export default {
method: "GET",
route: "/accounts_statistics",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
// get number of users registered,
const users = await User.count()
// calculate the last 5 days logins from diferent users
let last5D_logins = await Session.find({
date: {
$gte: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000),
$lte: new Date(),
}
})
const last5D_logins_counts = []
// filter from different users
last5D_logins.forEach((session) => {
if (!last5D_logins_counts.includes(session.user_id)) {
last5D_logins_counts.push(session.user_id)
}
})
// calculate active users within 1 week (using postings)
const active_1w_posts_users = await Post.count({
user_id: {
$in: last5D_logins_counts
},
created_at: {
$gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
$lte: new Date(),
}
})
// calculate total posts
const total_posts = await Post.count()
// calculate total post (1week)
const total_posts_1w = await Post.count({
created_at: {
$gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
$lte: new Date(),
}
})
return res.json({
accounts_registered: users,
last5D_logins: last5D_logins_counts.length,
active_1w_posts_users: active_1w_posts_users,
total_posts: total_posts,
total_posts_1w: total_posts_1w,
})
}
}

View File

@ -1,26 +0,0 @@
import { FeaturedWallpaper } from "@db_models"
export default {
method: "DELETE",
route: "/featured_wallpaper/:id",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const id = req.params.id
const wallpaper = await FeaturedWallpaper.findById(id)
if (!wallpaper) {
return res.status(404).json({
error: "Cannot find wallpaper"
})
}
await FeaturedWallpaper.deleteOne({
_id: id
})
return res.json({
done: true
})
}
}

View File

@ -1,39 +0,0 @@
import { FeaturedWallpaper } from "@db_models"
import momentTimezone from "moment-timezone"
export default {
method: "PUT",
route: "/featured_wallpaper",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const data = req.body.wallpaper
if (!data) {
return res.status(400).json({
error: "Invalid data"
})
}
// try to find if data._id exists, else create a new one
let wallpaper = null
if (data._id) {
wallpaper = await FeaturedWallpaper.findOne({
_id: data._id
})
} else {
wallpaper = new FeaturedWallpaper()
}
const current_timezone = momentTimezone.tz.guess()
wallpaper.active = data.active ?? wallpaper.active ?? true
wallpaper.date = data.date ?? momentTimezone.tz(Date.now(), current_timezone).format()
wallpaper.url = data.url ?? wallpaper.url
wallpaper.author = data.author ?? wallpaper.author
await wallpaper.save()
return res.json(wallpaper)
}
}

View File

@ -1,36 +0,0 @@
import { User } from "@db_models"
import bcrypt from "bcrypt"
export default {
method: "POST",
route: "/update_password/:user_id",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const { password } = req.body
if (!password) {
return res.status(400).json({ message: "Missing password" })
}
const { user_id } = req.params
const user = await User.findById(user_id).select("+password")
if (!user) {
return res.status(404).json({ message: "User not found" })
}
// hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
user.password = hash
await user.save()
return res.status(200).json({
status: "ok",
message: "Password updated successfully",
})
}
}

View File

@ -1,35 +0,0 @@
import { User } from "@db_models"
export default {
method: "POST",
route: "/update_data/:user_id",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const targetUserId = req.params.user_id
const user = await User.findById(targetUserId).catch((err) => {
return false
})
if (!user) {
return res.status(404).json({ error: "No user founded" })
}
const updateKeys = Object.keys(req.body.update)
updateKeys.forEach((key) => {
user[key] = req.body.update[key]
})
await user.save()
global.engine.ws.io.of("/").emit(`user.update`, {
...user.toObject(),
})
global.engine.ws.io.of("/").emit(`user.update.${targetUserId}`, {
...user.toObject(),
})
return res.json(user.toObject())
}
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class AdminController extends Controller {
static refName = "AdminController"
static useRoute = "/admin"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,46 +0,0 @@
import { Controller } from "linebridge/dist/server"
import { Octokit } from "@octokit/rest"
const octokit = new Octokit({})
const endpoints = {
post: {
"/mobile": {
fn: async (req, res) => {
if (!process.env.GITHUB_REPO) {
return res.status(400).json({
error: "GITHUB_REPO env variable not set"
})
}
const lastRelease = await octokit.repos.getLatestRelease({
owner: process.env.GITHUB_REPO.split("/")[0],
repo: process.env.GITHUB_REPO.split("/")[1]
})
const bundle = lastRelease.data.assets.find((asset) => asset.name === "mobile_dist.zip")
const version = lastRelease.data.tag_name
if (!bundle) {
return res.status(400).json({
error: "mobile asset not available",
version: version,
})
}
return res.json({
url: bundle.browser_download_url,
version: version,
})
}
}
}
}
export default class AutoUpdate extends Controller {
static refName = "AutoUpdate"
static useRoute = "/auto-update"
httpEndpoints = endpoints
}

View File

@ -1,21 +0,0 @@
import { Badge } from "@db_models"
export default {
method: "DELETE",
route: "/badge/:badge_id",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const badge = await Badge.findById(req.params.badge_id).catch((err) => {
res.status(500).json({ error: err })
return false
})
if (!badge) {
return res.status(404).json({ error: "No badge founded" })
}
badge.remove()
return res.json(badge)
}
}

View File

@ -1,29 +0,0 @@
import { Schematized } from "@lib"
import { Badge } from "@db_models"
export default {
method: "GET",
route: "/",
fn: Schematized({
select: ["_id", "name", "label"],
}, async (req, res) => {
let badges = []
if (req.selection._id) {
badges = await Badge.find({
_id: { $in: req.selection._id },
})
badges = badges.map(badge => badge.toObject())
} else {
badges = await Badge.find(req.selection).catch((err) => {
res.status(500).json({ error: err })
return false
})
}
if (badges) {
return res.json(badges)
}
})
}

View File

@ -1,21 +0,0 @@
import { User, Badge } from "@db_models"
export default {
method: "GET",
route: "/user/:user_id",
fn: async (req, res) => {
const user = await User.findOne({
_id: req.params.user_id,
})
if (!user) {
return res.status(404).json({ error: "User not found" })
}
const badges = await Badge.find({
name: { $in: user.badges },
})
return res.json(badges)
}
}

View File

@ -1,41 +0,0 @@
import { Badge, User } from "@db_models"
import { Schematized } from "@lib"
export default {
method: "POST",
route: "/badge/:badge_id/giveToUser",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: Schematized({
required: ["user_id"],
select: ["user_id"],
}, async (req, res) => {
const badge = await Badge.findById(req.params.badge_id).catch((err) => {
res.status(500).json({ error: err })
return false
})
if (!badge) {
return res.status(404).json({ error: "No badge founded" })
}
const user = await User.findById(req.selection.user_id).catch((err) => {
res.status(500).json({ error: err })
return false
})
if (!user) {
return res.status(404).json({ error: "No user founded" })
}
// check if user already have this badge
if (user.badges.includes(badge._id)) {
return res.status(409).json({ error: "User already have this badge" })
}
user.badges.push(badge._id.toString())
user.save()
return res.json(user)
})
}

View File

@ -1,27 +0,0 @@
import { Badge } from "@db_models"
import { Schematized } from "@lib"
export default {
method: "PUT",
route: "/",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: Schematized({
select: ["badge_id", "name", "label", "description", "icon", "color"],
}, async (req, res) => {
let badge = await Badge.findById(req.selection.badge_id).catch((err) => null)
if (!badge) {
badge = new Badge()
}
badge.name = req.selection.name || badge.name
badge.label = req.selection.label || badge.label
badge.description = req.selection.description || badge.description
badge.icon = req.selection.icon || badge.icon
badge.color = req.selection.color || badge.color
badge.save()
return res.json(badge)
})
}

View File

@ -1,41 +0,0 @@
import { Schematized } from "@lib"
import { Badge, User } from "@db_models"
export default {
method: "DELETE",
route: "/badge/:badge_id/removeFromUser",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: Schematized({
required: ["user_id"],
select: ["user_id"],
}, async (req, res) => {
const badge = await Badge.findById(req.params.badge_id).catch((err) => {
res.status(500).json({ error: err })
return false
})
if (!badge) {
return res.status(404).json({ error: "No badge founded" })
}
const user = await User.findById(req.selection.user_id).catch((err) => {
res.status(500).json({ error: err })
return false
})
if (!user) {
return res.status(404).json({ error: "No user founded" })
}
// check if user already have this badge
if (!user.badges.includes(badge._id)) {
return res.status(409).json({ error: "User don't have this badge" })
}
user.badges = user.badges.filter(b => b !== badge._id.toString())
user.save()
return res.json(user)
})
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class BadgesController extends Controller {
static refName = "BadgesController"
static useRoute = "/badge"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,63 +0,0 @@
import { Controller } from "linebridge/dist/server"
import { FeaturedEvent } from "@db_models"
import createFeaturedEvent from "./services/createFeaturedEvent"
// TODO: Migrate to new linebridge 0.15 endpoint classes instead of this
export default class FeaturedEventsController extends Controller {
httpEndpoints = {
get: {
"/featured_event/:id": async (req, res) => {
const { id } = req.params
const featuredEvent = await FeaturedEvent.findById(id)
return res.json(featuredEvent)
},
"/featured_events": async (req, res) => {
let query = {
expired: false
}
if (req.query.includeExpired) {
delete query.expired
}
const featuredEvents = await FeaturedEvent.find(query)
return res.json(featuredEvents)
}
},
post: {
"/featured_event": {
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const result = await createFeaturedEvent(req.body).catch((err) => {
res.status(500).json({
error: err.message
})
return null
})
if (result) {
return res.json(result)
}
}
}
},
delete: {
"/featured_event/:id": {
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const { id } = req.params
const featuredEvent = await FeaturedEvent.findByIdAndDelete(id)
return res.json(featuredEvent)
}
}
},
}
}

View File

@ -1,27 +0,0 @@
import { FeaturedEvent } from "@db_models"
export default async (payload) => {
const {
name,
category,
description,
dates,
location,
announcement,
customHeader,
} = payload
const featuredEvent = new FeaturedEvent({
name,
category,
description,
dates,
location,
announcement,
customHeader,
})
await featuredEvent.save()
return featuredEvent
}

View File

@ -1,11 +0,0 @@
export default {
method: "GET",
route: "/execution/:user_id",
fn: async (req, res) => {
let execution = {
}
return res.json(execution)
}
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class NFCController extends Controller {
static refName = "NFCController"
static useRoute = "/nfc"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,28 +0,0 @@
import { FeaturedWallpaper } from "@db_models"
export default {
method: "GET",
route: "/featured_wallpapers",
fn: async (req, res) => {
const { all } = req.query
const query = {
active: true
}
if (all) {
delete query.active
}
const featuredWallpapers = await FeaturedWallpaper.find(query)
.sort({ date: -1 })
.limit(all ? undefined : 10)
.catch(err => {
return res.status(500).json({
error: err.message
}).end()
})
return res.json(featuredWallpapers)
}
}

View File

@ -1,49 +0,0 @@
import { Octokit } from "@octokit/rest"
import axios from "axios"
const octokit = new Octokit({})
export default {
method: "GET",
route: "/release-notes",
fn: async (req, res) => {
if (!process.env.GITHUB_REPO) {
return res.status(400).json({
error: "GITHUB_REPO env variable not set"
})
}
const releasesNotes = []
// fetch the 3 latest releases
const releases = await octokit.repos.listReleases({
owner: process.env.GITHUB_REPO.split("/")[0],
repo: process.env.GITHUB_REPO.split("/")[1],
per_page: 3
})
for await (const release of releases.data) {
let changelogData = release.body
const bundle = release.assets.find((asset) => asset.name === "changelog.md")
if (bundle) {
const response = await axios.get(bundle.browser_download_url)
.catch(() => null)
if (response) {
changelogData = response.data
}
}
releasesNotes.push({
version: release.tag_name,
date: release.published_at,
body: changelogData,
isMd: bundle !== undefined
})
}
return res.json(releasesNotes)
}
}

View File

@ -1,27 +0,0 @@
import { ServerLimit } from "@db_models"
export default {
method: "GET",
route: "/global_server_limits/:limitkey",
fn: async (req, res) => {
const { limitkey } = req.params
const serverLimit = await ServerLimit.findOne({
key: limitkey,
active: true,
})
.catch(err => {
return res.status(500).json({
error: err.message
})
})
if (!serverLimit) {
return res.status(404).json({
error: "Server limit not found or inactive"
})
}
return res.json(serverLimit)
}
}

View File

@ -1,7 +0,0 @@
export default {
route: "/ping",
method: "GET",
fn: async (req, res) => {
return res.send("pong")
}
}

Some files were not shown because too many files have changed in this diff Show More