mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
0252498d2b
commit
5436678fed
@ -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
17
packages/app/aliases.js
Normal 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"),
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
},
|
||||
@ -169,7 +169,6 @@ class ComtyApp extends React.Component {
|
||||
showRotate
|
||||
/>)
|
||||
},
|
||||
|
||||
openPostCreator: (params) => {
|
||||
app.layout.modal.open("post_creator", (props) => <PostCreator
|
||||
{...props}
|
||||
|
@ -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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
@ -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>
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
.music-studio-tracks-uploader-hint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
@ -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
|
||||
this.setState({
|
||||
list: this.state.list.filter((item) => item.uid !== uid),
|
||||
})
|
||||
}
|
||||
|
||||
const [list, setList] = React.useState(release.list ?? [])
|
||||
const [pendingTracksUpload, setPendingTracksUpload] = React.useState([])
|
||||
addTrackUIDToPendingUploads = (uid) => {
|
||||
if (!uid) {
|
||||
return false
|
||||
}
|
||||
|
||||
async function onTrackUploaderChange (change) {
|
||||
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>
|
||||
}
|
||||
|
||||
|
@ -4,49 +4,3 @@
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
15
packages/app/src/contexts/MusicReleaseEditor/index.js
Normal file
15
packages/app/src/contexts/MusicReleaseEditor/index.js
Normal 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
|
19
packages/app/src/hooks/useHideToolsBar/index.js
Normal file
19
packages/app/src/hooks/useHideToolsBar/index.js
Normal 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)
|
||||
}
|
||||
}, [])
|
||||
}
|
@ -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`)
|
||||
}
|
||||
|
||||
|
@ -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"
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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}>
|
||||
{
|
||||
React.createElement(this.props.children, componentProps)
|
||||
}
|
||||
</AntdDrawer>
|
||||
}
|
||||
|
||||
default: {
|
||||
return <DraggableDrawer {...drawerProps}>
|
||||
{
|
||||
React.createElement(this.props.children, componentProps)
|
||||
}
|
||||
</DraggableDrawer>
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
<div
|
||||
className={classnames(
|
||||
"drawers-wrapper",
|
||||
{
|
||||
["hidden"]: !this.state.drawers.length,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{this.state.drawers}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
66
packages/app/src/layouts/components/drawer/index.less
Normal file
66
packages/app/src/layouts/components/drawer/index.less
Normal 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;
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
110
packages/app/src/layouts/components/modals/modal/index.jsx
Normal file
110
packages/app/src/layouts/components/modals/modal/index.jsx
Normal 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
|
117
packages/app/src/layouts/components/modals/modal/index.less
Normal file
117
packages/app/src/layouts/components/modals/modal/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,14 +4,64 @@ 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")
|
||||
},
|
||||
@ -33,7 +83,7 @@ const onClickHandlers = {
|
||||
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>
|
||||
|
@ -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;
|
||||
@ -192,8 +200,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -40,7 +40,6 @@ const ChatPage = (props) => {
|
||||
isRemoteTyping,
|
||||
} = useChat(to_user_id)
|
||||
|
||||
|
||||
console.log(R_User)
|
||||
|
||||
async function submitMessage(e) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
|
||||
import ChatsService from "@models/chats"
|
||||
|
||||
import TimeAgo from "@components/TimeAgo"
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
81
packages/app/src/settings/api/index.jsx
Normal file
81
packages/app/src/settings/api/index.jsx
Normal 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>
|
||||
}
|
||||
}
|
32
packages/app/src/settings/api/index.less
Normal file
32
packages/app/src/settings/api/index.less
Normal 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;
|
||||
}
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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}>
|
||||
|
@ -7,6 +7,8 @@
|
||||
border: 1px var(--border-color) solid;
|
||||
border-radius: 12px;
|
||||
|
||||
width: fit-content;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.__setting_theme_variant_selector-variant {
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -332,3 +332,16 @@ svg {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
gap: 10px;
|
||||
}
|
@ -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: {
|
||||
|
8
packages/server/.dockerignore
Normal file
8
packages/server/.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
./node_modules
|
||||
./build
|
||||
./dist
|
||||
./ssl
|
||||
|
||||
# secrets
|
||||
./api.production.env
|
||||
./.env
|
15
packages/server/.swcrc
Normal file
15
packages/server/.swcrc
Normal 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
|
||||
}
|
||||
}
|
@ -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"]
|
11
packages/server/docker-compose.yml
Normal file
11
packages/server/docker-compose.yml
Normal 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
|
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -1,10 +1,4 @@
|
||||
{
|
||||
"name": "auth",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"proxy":{
|
||||
"namespace": "/auth",
|
||||
"port": 3020
|
||||
}
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
@ -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...")
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
|
||||
export default {
|
||||
middlewares: ["withAuthentication"],
|
||||
fn: async (req, res) => {
|
||||
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Server } from "linebridge/src/server"
|
||||
import { Server } from "linebridge/dist/server"
|
||||
|
||||
import B2 from "backblaze-b2"
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export default {
|
||||
method: "GET",
|
||||
route: "/execution/:user_id",
|
||||
fn: async (req, res) => {
|
||||
let execution = {
|
||||
|
||||
}
|
||||
|
||||
return res.json(execution)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user