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",
|
"postdeploy": "node ./scripts/post-deploy.js",
|
||||||
"postinstall": "node ./scripts/post-install.js"
|
"postinstall": "node ./scripts/post-install.js"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
|
||||||
"packages/**"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-min": "1.4.3",
|
"7zip-min": "1.4.3",
|
||||||
"axios": "^1.4.0",
|
"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",
|
"webDir": "dist",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"CapacitorUpdater": {
|
"CapacitorUpdater": {
|
||||||
"updateUrl": "https://api.comty.app/auto-update/mobile",
|
"updateUrl": "https://api.comty.app/repo/auto-update/mobile",
|
||||||
"resetWhenUpdate": true
|
"resetWhenUpdate": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"dev:farm": "farm start",
|
||||||
"docker-compose:update_run": "docker-compose down && git pull && yarn build && docker-compose up -d --build",
|
"docker-compose:update_run": "docker-compose down && git pull && yarn build && docker-compose up -d --build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
@ -15,7 +16,7 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "4.7.0",
|
"@ant-design/icons": "^5.4.0",
|
||||||
"@capacitor/android": "^5.0.5",
|
"@capacitor/android": "^5.0.5",
|
||||||
"@capacitor/app": "^5.0.3",
|
"@capacitor/app": "^5.0.3",
|
||||||
"@capacitor/cli": "^5.0.5",
|
"@capacitor/cli": "^5.0.5",
|
||||||
@ -30,16 +31,24 @@
|
|||||||
"@dnd-kit/core": "^6.0.8",
|
"@dnd-kit/core": "^6.0.8",
|
||||||
"@dnd-kit/sortable": "^7.0.2",
|
"@dnd-kit/sortable": "^7.0.2",
|
||||||
"@emotion/css": "11.0.0",
|
"@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/ffmpeg": "^0.12.10",
|
||||||
"@ffmpeg/util": "^0.12.1",
|
"@ffmpeg/util": "^0.12.1",
|
||||||
"@loadable/component": "5.15.2",
|
"@loadable/component": "5.15.2",
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
"@ragestudio/cordova-nfc": "^1.2.0",
|
"@ragestudio/cordova-nfc": "^1.2.0",
|
||||||
"@sentry/browser": "^7.64.0",
|
"@sentry/browser": "^7.64.0",
|
||||||
|
"@stripe/react-stripe-js": "^2.7.3",
|
||||||
|
"@stripe/stripe-js": "^4.2.0",
|
||||||
"@tanstack/react-virtual": "^3.5.0",
|
"@tanstack/react-virtual": "^3.5.0",
|
||||||
"@tauri-apps/api": "^1.5.4",
|
"@tauri-apps/api": "^1.5.4",
|
||||||
"@tsmx/human-readable": "^1.0.7",
|
"@tsmx/human-readable": "^1.0.7",
|
||||||
"antd": "^5.17.0",
|
"antd": "^5.20.1",
|
||||||
"antd-mobile": "^5.31.0",
|
"antd-mobile": "^5.31.0",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||||
@ -58,9 +67,7 @@
|
|||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"jsmediatags": "^3.9.7",
|
"jsmediatags": "^3.9.7",
|
||||||
"less": "4.1.2",
|
"less": "4.1.2",
|
||||||
"linebridge": "0.16.0",
|
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"lru-cache": "^10.0.0",
|
|
||||||
"luxon": "^3.0.4",
|
"luxon": "^3.0.4",
|
||||||
"million": "^2.6.4",
|
"million": "^2.6.4",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
@ -71,9 +78,10 @@
|
|||||||
"plyr": "^3.6.12",
|
"plyr": "^3.6.12",
|
||||||
"plyr-react": "^3.2.1",
|
"plyr-react": "^3.2.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-color": "2.19.3",
|
"react-color": "2.19.3",
|
||||||
|
"react-confetti-explosion": "^2.1.2",
|
||||||
"react-countup": "^6.4.1",
|
"react-countup": "^6.4.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-fast-marquee": "^1.3.5",
|
"react-fast-marquee": "^1.3.5",
|
||||||
@ -87,6 +95,7 @@
|
|||||||
"react-motion": "0.5.2",
|
"react-motion": "0.5.2",
|
||||||
"react-rnd": "10.3.5",
|
"react-rnd": "10.3.5",
|
||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
|
"react-slot-counter": "^3.0.1",
|
||||||
"react-ticker": "^1.3.2",
|
"react-ticker": "^1.3.2",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-useanimations": "^2.10.0",
|
"react-useanimations": "^2.10.0",
|
||||||
|
@ -106,7 +106,7 @@ class ComtyApp extends React.Component {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
openLoginForm: async (options = {}) => {
|
openLoginForm: async (options = {}) => {
|
||||||
app.layout.drawer.open("login", Login, {
|
app.layout.draggable.open("login", Login, {
|
||||||
defaultLocked: options.defaultLocked ?? false,
|
defaultLocked: options.defaultLocked ?? false,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
sessionController: this.sessionController,
|
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
|
// Opens the notification window and sets up the UI for the notification to be displayed
|
||||||
openNotifications: () => {
|
openNotifications: () => {
|
||||||
window.app.layout.sidedrawer.open("notifications", NotificationsCenter, {
|
window.app.layout.drawer.open("notifications", NotificationsCenter, {
|
||||||
props: {
|
props: {
|
||||||
width: "fit-content",
|
width: "fit-content",
|
||||||
},
|
},
|
||||||
@ -158,7 +158,7 @@ class ComtyApp extends React.Component {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
openMessages: () => {
|
openMessages: () => {
|
||||||
app.location.push("/messages")
|
app.location.push("/messages")
|
||||||
},
|
},
|
||||||
openFullImageViewer: (src) => {
|
openFullImageViewer: (src) => {
|
||||||
app.cores.window_mng.render("image_lightbox", <Lightbox
|
app.cores.window_mng.render("image_lightbox", <Lightbox
|
||||||
@ -169,7 +169,6 @@ class ComtyApp extends React.Component {
|
|||||||
showRotate
|
showRotate
|
||||||
/>)
|
/>)
|
||||||
},
|
},
|
||||||
|
|
||||||
openPostCreator: (params) => {
|
openPostCreator: (params) => {
|
||||||
app.layout.modal.open("post_creator", (props) => <PostCreator
|
app.layout.modal.open("post_creator", (props) => <PostCreator
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -11,17 +11,15 @@ const CoverEditor = (props) => {
|
|||||||
|
|
||||||
const [url, setUrl] = React.useState(value)
|
const [url, setUrl] = React.useState(value)
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setUrl(value)
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
onChange(url)
|
onChange(url)
|
||||||
}, [url])
|
}, [url])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!url) {
|
if (!value) {
|
||||||
setUrl(defaultUrl)
|
setUrl(defaultUrl)
|
||||||
|
} else {
|
||||||
|
setUrl(value)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -304,9 +304,10 @@ class Login extends React.Component {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
<antd.Input
|
<antd.Input.OTP
|
||||||
placeholder="4 Digit MFA code"
|
length={4}
|
||||||
onChange={(e) => this.onUpdateInput("mfa_code", e.target.value)}
|
formatter={(str) => str.toUpperCase()}
|
||||||
|
onChange={(code) => this.onUpdateInput("mfa_code", code)}
|
||||||
onPressEnter={this.nextStep}
|
onPressEnter={this.nextStep}
|
||||||
/>
|
/>
|
||||||
</antd.Form.Item>
|
</antd.Form.Item>
|
||||||
|
@ -5,6 +5,8 @@ import { Icons } from "@components/Icons"
|
|||||||
|
|
||||||
import MusicModel from "@models/music"
|
import MusicModel from "@models/music"
|
||||||
|
|
||||||
|
import { DefaultReleaseEditorState, ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor"
|
||||||
|
|
||||||
import Tabs from "./tabs"
|
import Tabs from "./tabs"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
@ -14,11 +16,33 @@ const ReleaseEditor = (props) => {
|
|||||||
|
|
||||||
const basicInfoRef = React.useRef()
|
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 [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() {
|
async function handleSubmit() {
|
||||||
basicInfoRef.current.submit()
|
console.log("Submit >", globalState)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFinish(values) {
|
async function onFinish(values) {
|
||||||
@ -29,79 +53,88 @@ const ReleaseEditor = (props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E_Release) {
|
React.useEffect(() => {
|
||||||
|
initialize()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loadError) {
|
||||||
return <antd.Result
|
return <antd.Result
|
||||||
status="warning"
|
status="warning"
|
||||||
title="Error"
|
title="Error"
|
||||||
subTitle={E_Release.message}
|
subTitle={loadError.message}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (L_Release) {
|
if (loading) {
|
||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = Tabs.find(({ key }) => key === selectedTab)
|
const Tab = Tabs.find(({ key }) => key === selectedTab)
|
||||||
|
|
||||||
return <div className="music-studio-release-editor">
|
return <ReleaseEditorStateContext.Provider value={globalState}>
|
||||||
<div className="music-studio-release-editor-menu">
|
<div className="music-studio-release-editor">
|
||||||
<antd.Menu
|
<div className="music-studio-release-editor-menu">
|
||||||
onClick={(e) => setSelectedTab(e.key)}
|
<antd.Menu
|
||||||
selectedKeys={[selectedTab]}
|
onClick={(e) => setSelectedTab(e.key)}
|
||||||
items={Tabs}
|
selectedKeys={[selectedTab]}
|
||||||
mode="vertical"
|
items={Tabs}
|
||||||
/>
|
mode="vertical"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="music-studio-release-editor-menu-actions">
|
<div className="music-studio-release-editor-menu-actions">
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
icon={<Icons.Save />}
|
icon={<Icons.Save />}
|
||||||
disabled={L_Release || !canFinish()}
|
disabled={loading || !canFinish()}
|
||||||
>
|
|
||||||
Save
|
|
||||||
</antd.Button>
|
|
||||||
|
|
||||||
{
|
|
||||||
release_id !== "new" ? <antd.Button
|
|
||||||
icon={<Icons.IoMdTrash />}
|
|
||||||
disabled={L_Release}
|
|
||||||
>
|
>
|
||||||
Delete
|
Save
|
||||||
</antd.Button> : null
|
</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
|
Tab && React.createElement(Tab.render, {
|
||||||
icon={<Icons.MdLink />}
|
release: globalState,
|
||||||
onClick={() => app.location.push(`/music/release/${R_Release._id}`)}
|
onFinish: onFinish,
|
||||||
>
|
|
||||||
Go to release
|
state: globalState,
|
||||||
</antd.Button> : null
|
setState: setGlobalState,
|
||||||
|
|
||||||
|
references: {
|
||||||
|
basic: basicInfoRef
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ReleaseEditorStateContext.Provider>
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReleaseEditor
|
export default ReleaseEditor
|
@ -29,7 +29,16 @@ const ReleasesTypes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const BasicInformation = (props) => {
|
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">
|
return <div className="music-studio-release-editor-tab">
|
||||||
<h1>Release Information</h1>
|
<h1>Release Information</h1>
|
||||||
@ -40,12 +49,13 @@ const BasicInformation = (props) => {
|
|||||||
ref={props.references.basic}
|
ref={props.references.basic}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
requiredMark={false}
|
requiredMark={false}
|
||||||
|
onValuesChange={onFormChange}
|
||||||
>
|
>
|
||||||
<antd.Form.Item
|
<antd.Form.Item
|
||||||
label=""
|
label=""
|
||||||
name="cover"
|
name="cover"
|
||||||
rules={[{ required: true, message: "Input a cover for the release" }]}
|
rules={[{ required: true, message: "Input a cover for the release" }]}
|
||||||
initialValue={release?.cover}
|
initialValue={state?.cover}
|
||||||
>
|
>
|
||||||
<CoverEditor
|
<CoverEditor
|
||||||
defaultUrl="https://storage.ragestudio.net/comty-static-assets/default_song.png"
|
defaultUrl="https://storage.ragestudio.net/comty-static-assets/default_song.png"
|
||||||
@ -70,7 +80,7 @@ const BasicInformation = (props) => {
|
|||||||
label={<><Icons.MdMusicNote /> <span>Title</span></>}
|
label={<><Icons.MdMusicNote /> <span>Title</span></>}
|
||||||
name="title"
|
name="title"
|
||||||
rules={[{ required: true, message: "Input a title for the release" }]}
|
rules={[{ required: true, message: "Input a title for the release" }]}
|
||||||
initialValue={release?.title}
|
initialValue={state?.title}
|
||||||
>
|
>
|
||||||
<antd.Input
|
<antd.Input
|
||||||
placeholder="Release title"
|
placeholder="Release title"
|
||||||
@ -83,7 +93,7 @@ const BasicInformation = (props) => {
|
|||||||
label={<><Icons.MdAlbum /> <span>Type</span></>}
|
label={<><Icons.MdAlbum /> <span>Type</span></>}
|
||||||
name="type"
|
name="type"
|
||||||
rules={[{ required: true, message: "Select a type for the release" }]}
|
rules={[{ required: true, message: "Select a type for the release" }]}
|
||||||
initialValue={release?.type}
|
initialValue={state?.type}
|
||||||
>
|
>
|
||||||
<antd.Select
|
<antd.Select
|
||||||
placeholder="Release type"
|
placeholder="Release type"
|
||||||
@ -94,7 +104,7 @@ const BasicInformation = (props) => {
|
|||||||
<antd.Form.Item
|
<antd.Form.Item
|
||||||
label={<><Icons.MdPublic /> <span>Public</span></>}
|
label={<><Icons.MdPublic /> <span>Public</span></>}
|
||||||
name="public"
|
name="public"
|
||||||
initialValue={release?.public}
|
initialValue={state?.public}
|
||||||
>
|
>
|
||||||
<antd.Switch />
|
<antd.Switch />
|
||||||
</antd.Form.Item>
|
</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 React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
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 { Icons } from "@components/Icons"
|
||||||
import TrackEditor from "@components/MusicStudio/TrackEditor"
|
|
||||||
|
import TrackListItem from "./components/TrackListItem"
|
||||||
|
import UploadHint from "./components/UploadHint"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const UploadHint = (props) => {
|
async function uploadBinaryArrayToStorage(bin, args) {
|
||||||
return <div className="uploadHint">
|
const { format, data } = bin
|
||||||
<Icons.MdPlaylistAdd />
|
|
||||||
<p>Upload your tracks</p>
|
const filenameExt = format.split("/")[1]
|
||||||
<p>Drag and drop your tracks here or click this box to start uploading files.</p>
|
const filename = `cover.${filenameExt}`
|
||||||
</div>
|
|
||||||
|
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) => {
|
class TrackManifest {
|
||||||
const [loading, setLoading] = React.useState(false)
|
constructor(params) {
|
||||||
const [error, setError] = React.useState(null)
|
this.params = params
|
||||||
|
|
||||||
const { track } = props
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
async function onClickEditTrack() {
|
cover = "https://storage.ragestudio.net/comty-static-assets/default_song.png"
|
||||||
app.layout.drawer.open("track_editor", TrackEditor, {
|
|
||||||
type: "drawer",
|
title = "Untitled"
|
||||||
props: {
|
|
||||||
width: "600px",
|
album = "Unknown"
|
||||||
headerStyle: {
|
|
||||||
display: "none",
|
artist = "Unknown"
|
||||||
}
|
|
||||||
},
|
source = null
|
||||||
componentProps: {
|
|
||||||
track,
|
async initialize() {
|
||||||
onSave: (newTrackData) => {
|
const metadata = await this.analyzeMetadata(this.params.file.originFileObj)
|
||||||
console.log("Saving track", newTrackData)
|
|
||||||
|
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
|
removeTrackByUid = (uid) => {
|
||||||
key={track._id}
|
if (!uid) {
|
||||||
draggableId={track._id}
|
return false
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</Draggable>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReleaseTracks = (props) => {
|
|
||||||
const { release } = props
|
|
||||||
|
|
||||||
const [list, setList] = React.useState(release.list ?? [])
|
this.setState({
|
||||||
const [pendingTracksUpload, setPendingTracksUpload] = React.useState([])
|
list: this.state.list.filter((item) => item.uid !== uid),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function onTrackUploaderChange (change) {
|
addTrackUIDToPendingUploads = (uid) => {
|
||||||
|
if (!uid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.pendingUploads.includes(uid)) {
|
||||||
|
this.setState({
|
||||||
|
pendingUploads: [...this.state.pendingUploads, uid],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTrackUIDFromPendingUploads = (uid) => {
|
||||||
|
if (!uid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
pendingUploads: this.state.pendingUploads.filter((item) => item !== uid),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUploaderStateChange = async (change) => {
|
||||||
switch (change.file.status) {
|
switch (change.file.status) {
|
||||||
case "uploading": {
|
case "uploading": {
|
||||||
if (!pendingTracksUpload.includes(change.file.uid)) {
|
this.addTrackUIDToPendingUploads(change.file.uid)
|
||||||
pendingTracksUpload.push(change.file.uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
setList((prev) => {
|
const trackManifest = new TrackManifest({
|
||||||
return [
|
uid: change.file.uid,
|
||||||
...prev,
|
file: change.file,
|
||||||
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await trackManifest.initialize()
|
||||||
|
|
||||||
|
this.addTrackToList(trackManifest)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "done": {
|
case "done": {
|
||||||
// remove pending file
|
// remove pending file
|
||||||
this.setState({
|
this.removeTrackUIDFromPendingUploads(change.file.uid)
|
||||||
pendingTracksUpload: this.state.pendingTracksUpload.filter((uid) => uid !== change.file.uid)
|
|
||||||
})
|
|
||||||
|
|
||||||
// update file url in the track info
|
const trackIndex = this.state.list.findIndex((item) => item.uid === uid)
|
||||||
const track = this.state.trackList.find((file) => file.uid === change.file.uid)
|
|
||||||
|
|
||||||
if (track) {
|
if (trackIndex === -1) {
|
||||||
track.source = change.file.response.url
|
console.error(`Track with uid [${uid}] not found!`)
|
||||||
track.status = "done"
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
// update track list
|
||||||
trackList: this.state.trackList
|
this.setState((state) => {
|
||||||
|
state.list[trackIndex].source = change.file.response.url
|
||||||
|
|
||||||
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "error": {
|
case "error": {
|
||||||
// remove pending file
|
// 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
|
// remove from tracklist
|
||||||
antd.Modal.error({
|
await this.removeTrackByUid(change.file.uid)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
case "removed": {
|
case "removed": {
|
||||||
this.handleTrackRemove(change.file.uid)
|
// stop upload & delete from pending list and tracklist
|
||||||
|
await this.removeTrackByUid(change.file.uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUploadTrack (req) {
|
uploadToStorage = async (req) => {
|
||||||
const response = await app.cores.remoteStorage.uploadFile(req.file, {
|
const response = await app.cores.remoteStorage.uploadFile(req.file, {
|
||||||
onProgress: this.handleFileProgress,
|
onProgress: this.handleTrackFileUploadProgress,
|
||||||
service: "premium-cdn"
|
service: "b2"
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
antd.message.error(error)
|
antd.message.error(error)
|
||||||
@ -172,38 +230,40 @@ const ReleaseTracks = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTrackDragEnd(result) {
|
handleTrackFileUploadProgress = async (file, progress) => {
|
||||||
console.log(result)
|
console.log(file, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderTrackList = (result) => {
|
||||||
if (!result.destination) {
|
if (!result.destination) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setList((prev) => {
|
this.setState((prev) => {
|
||||||
const trackList = [...prev]
|
const trackList = [...prev.list]
|
||||||
|
|
||||||
const [removed] = trackList.splice(result.source.index, 1)
|
const [removed] = trackList.splice(result.source.index, 1)
|
||||||
|
|
||||||
trackList.splice(result.destination.index, 0, removed)
|
trackList.splice(result.destination.index, 0, removed)
|
||||||
|
|
||||||
return trackList
|
return {
|
||||||
|
list: trackList
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="music-studio-release-editor-tab">
|
render() {
|
||||||
<h1>Tracks</h1>
|
return <div className="music-studio-release-editor-tracks">
|
||||||
|
|
||||||
<div>
|
|
||||||
<antd.Upload
|
<antd.Upload
|
||||||
className="uploader"
|
className="music-studio-tracks-uploader"
|
||||||
customRequest={handleUploadTrack}
|
onChange={this.handleUploaderStateChange}
|
||||||
onChange={onTrackUploaderChange}
|
customRequest={this.uploadToStorage}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
accept="audio/*"
|
accept="audio/*"
|
||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
list.length === 0 ?
|
this.state.list.length === 0 ?
|
||||||
<UploadHint /> : <antd.Button
|
<UploadHint /> : <antd.Button
|
||||||
className="uploadMoreButton"
|
className="uploadMoreButton"
|
||||||
icon={<Icons.Plus />}
|
icon={<Icons.Plus />}
|
||||||
@ -212,7 +272,7 @@ const ReleaseTracks = (props) => {
|
|||||||
</antd.Upload>
|
</antd.Upload>
|
||||||
|
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={onTrackDragEnd}
|
onDragEnd={this.orderTrackList}
|
||||||
>
|
>
|
||||||
<Droppable
|
<Droppable
|
||||||
droppableId="droppable"
|
droppableId="droppable"
|
||||||
@ -224,13 +284,13 @@ const ReleaseTracks = (props) => {
|
|||||||
className="music-studio-release-editor-tracks-list"
|
className="music-studio-release-editor-tracks-list"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
list.length === 0 && <antd.Result
|
this.state.list.length === 0 && <antd.Result
|
||||||
status="info"
|
status="info"
|
||||||
title="No tracks"
|
title="No tracks"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
list.map((track, index) => {
|
this.state.list.map((track, index) => {
|
||||||
return <TrackListItem
|
return <TrackListItem
|
||||||
index={index}
|
index={index}
|
||||||
track={track}
|
track={track}
|
||||||
@ -243,6 +303,25 @@ const ReleaseTracks = (props) => {
|
|||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,50 +3,4 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
gap: 10px;
|
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 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"
|
import "./index.less"
|
||||||
|
|
||||||
const PollOption = (props) => {
|
const PollOption = (props) => {
|
||||||
return <div className="poll-option">
|
const { option, editMode, onRemove } = props
|
||||||
<div className="label">
|
|
||||||
{
|
|
||||||
createIconRender(props.option.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
<span>
|
return <div
|
||||||
{props.option.label}
|
className={classNames(
|
||||||
|
"poll-option",
|
||||||
|
{
|
||||||
|
["editable"]: !!editMode
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
editMode && <antd.Input
|
||||||
|
placeholder="Option"
|
||||||
|
defaultValue={option.label}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!editMode && <span>
|
||||||
|
{option.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
editMode && <antd.Button
|
||||||
|
onClick={onRemove}
|
||||||
|
icon={createIconRender("CloseOutlined")}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Poll = (props) => {
|
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">
|
return <div className="poll">
|
||||||
{
|
{
|
||||||
props.options.map((option) => {
|
options.map((option, index) => {
|
||||||
return <PollOption
|
return <PollOption
|
||||||
key={option.id}
|
key={index}
|
||||||
option={option}
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,25 +2,59 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
background-color: var(--background-color-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
.poll-option {
|
.poll-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
width: 100%;
|
align-items: center;
|
||||||
|
|
||||||
padding: 10px 20px;
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
.ant-input {
|
||||||
background-color: var(--background-color-accent-hover);
|
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 { DateTime } from "luxon"
|
||||||
import lodash from "lodash"
|
import lodash from "lodash"
|
||||||
import humanSize from "@tsmx/human-readable"
|
import humanSize from "@tsmx/human-readable"
|
||||||
|
|
||||||
import PostLink from "@components/PostLink"
|
import PostLink from "@components/PostLink"
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
|
import Poll from "@components/Poll"
|
||||||
|
|
||||||
import clipboardEventFileToFile from "@utils/clipboardEventFileToFile"
|
import clipboardEventFileToFile from "@utils/clipboardEventFileToFile"
|
||||||
|
|
||||||
import PostModel from "@models/post"
|
import PostModel from "@models/post"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
@ -26,6 +29,7 @@ export default class PostCreator extends React.Component {
|
|||||||
|
|
||||||
postMessage: "",
|
postMessage: "",
|
||||||
postAttachments: [],
|
postAttachments: [],
|
||||||
|
postPoll: null,
|
||||||
|
|
||||||
fileList: [],
|
fileList: [],
|
||||||
postingPolicy: DEFAULT_POST_POLICY,
|
postingPolicy: DEFAULT_POST_POLICY,
|
||||||
@ -451,6 +455,20 @@ export default class PostCreator extends React.Component {
|
|||||||
dialog.click()
|
dialog.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleAddPoll = () => {
|
||||||
|
if (!this.state.postPoll) {
|
||||||
|
this.setState({
|
||||||
|
postPoll: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeletePoll = () => {
|
||||||
|
this.setState({
|
||||||
|
postPoll: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount = async () => {
|
componentDidMount = async () => {
|
||||||
if (this.props.edit_post) {
|
if (this.props.edit_post) {
|
||||||
await this.setState({
|
await this.setState({
|
||||||
@ -589,6 +607,14 @@ export default class PostCreator extends React.Component {
|
|||||||
</antd.Upload.Dragger>
|
</antd.Upload.Dragger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.state.postPoll && <Poll
|
||||||
|
options={this.state.postPoll}
|
||||||
|
onClose={this.handleDeletePoll}
|
||||||
|
editMode
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
@ -599,6 +625,7 @@ export default class PostCreator extends React.Component {
|
|||||||
<antd.Button
|
<antd.Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
icon={<Icons.MdPoll />}
|
icon={<Icons.MdPoll />}
|
||||||
|
onClick={this.handleAddPoll}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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) => {
|
export default (namespace, ctx) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (app.layout["namespace"] === "object") {
|
if (app.layout[namespace] === "object") {
|
||||||
throw new Error(`Layout namespace [${namespace}] already exists`)
|
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 { 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 PlayerView from "@pages/@mobile-views/player"
|
||||||
import CreatorView from "@pages/@mobile-views/creator"
|
import CreatorView from "@pages/@mobile-views/creator"
|
@ -9,6 +9,72 @@ import { css } from "@emotion/css"
|
|||||||
|
|
||||||
import "./index.less"
|
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 {
|
export default class DraggableDrawer extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
open: PropTypes.bool.isRequired,
|
open: PropTypes.bool.isRequired,
|
||||||
|
@ -1,7 +1,93 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { EventBus } from "evite"
|
import classnames from "classnames"
|
||||||
import { Drawer as AntdDrawer } from "antd"
|
import { Motion, spring } from "react-motion"
|
||||||
import DraggableDrawer from "../draggableDrawer"
|
|
||||||
|
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 {
|
export default class DrawerController extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -11,19 +97,99 @@ export default class DrawerController extends React.Component {
|
|||||||
addresses: {},
|
addresses: {},
|
||||||
refs: {},
|
refs: {},
|
||||||
drawers: [],
|
drawers: [],
|
||||||
|
|
||||||
|
maskVisible: false,
|
||||||
|
maskRender: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
app.layout.drawer = {
|
this.interface = {
|
||||||
open: this.open,
|
open: this.open,
|
||||||
close: this.close,
|
close: this.close,
|
||||||
closeAll: this.closeAll,
|
closeAll: this.closeAll,
|
||||||
drawersLength: () => this.state.drawers.length,
|
drawersLength: () => this.state.drawers.length,
|
||||||
|
isMaskVisible: () => this.state.maskVisible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendEvent = (id, ...context) => {
|
componentDidMount = () => {
|
||||||
const ref = this.state.refs[id]?.current
|
app.layout["drawer"] = this.interface
|
||||||
return ref.events.emit(...context)
|
|
||||||
|
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) => {
|
open = (id, component, options) => {
|
||||||
@ -46,33 +212,37 @@ export default class DrawerController extends React.Component {
|
|||||||
addresses[id] = drawers.length - 1
|
addresses[id] = drawers.length - 1
|
||||||
refs[id] = instance.ref
|
refs[id] = instance.ref
|
||||||
} else {
|
} else {
|
||||||
const ref = refs[id].current
|
drawers[addresses[id]] = <Drawer {...instance} />
|
||||||
const isLocked = ref.state.locked
|
refs[id] = instance.ref
|
||||||
|
|
||||||
if (!isLocked) {
|
|
||||||
drawers[addresses[id]] = <Drawer {...instance} />
|
|
||||||
refs[id] = instance.ref
|
|
||||||
} else {
|
|
||||||
console.warn("Cannot update an locked drawer.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
let { addresses, drawers, refs } = this.state
|
||||||
|
|
||||||
const index = addresses[id]
|
const index = addresses[id]
|
||||||
|
|
||||||
const ref = this.state.refs[id]?.current
|
const ref = this.state.refs[id]?.current
|
||||||
|
|
||||||
if (typeof ref === "undefined") {
|
if (typeof ref === "undefined") {
|
||||||
return console.warn("This drawer not exists")
|
return console.warn("This drawer not exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref.state.locked && ref.state.visible) {
|
if (drawers.length === 1) {
|
||||||
return console.warn("This drawer is locked and cannot be closed")
|
this.toggleMaskVisibility(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delay)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof drawers[index] !== "undefined") {
|
if (typeof drawers[index] !== "undefined") {
|
||||||
@ -82,7 +252,11 @@ export default class DrawerController extends React.Component {
|
|||||||
delete addresses[id]
|
delete addresses[id]
|
||||||
delete refs[id]
|
delete refs[id]
|
||||||
|
|
||||||
this.setState({ addresses, drawers })
|
this.setState({
|
||||||
|
refs,
|
||||||
|
addresses,
|
||||||
|
drawers,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAll = () => {
|
closeAll = () => {
|
||||||
@ -92,129 +266,34 @@ export default class DrawerController extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.state.drawers
|
return <>
|
||||||
}
|
<Motion
|
||||||
}
|
style={{
|
||||||
|
opacity: spring(this.state.maskVisible ? 1 : 0),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ opacity }) => {
|
||||||
|
return <div
|
||||||
|
className="drawers-mask"
|
||||||
|
onClick={() => this.closeLastDrawer()}
|
||||||
|
style={{
|
||||||
|
opacity,
|
||||||
|
display: this.state.maskRender ? "block" : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}}
|
||||||
|
</Motion>
|
||||||
|
|
||||||
export class Drawer extends React.Component {
|
<div
|
||||||
options = this.props.options ?? {}
|
className={classnames(
|
||||||
|
"drawers-wrapper",
|
||||||
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)
|
["hidden"]: !this.state.drawers.length,
|
||||||
}
|
}
|
||||||
</AntdDrawer>
|
)}
|
||||||
}
|
>
|
||||||
|
{this.state.drawers}
|
||||||
default: {
|
</div>
|
||||||
return <DraggableDrawer {...drawerProps}>
|
</>
|
||||||
{
|
|
||||||
React.createElement(this.props.children, componentProps)
|
|
||||||
}
|
|
||||||
</DraggableDrawer>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
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 React from "react"
|
||||||
import { Modal as AntdModal } from "antd"
|
import Modal from "./modal"
|
||||||
import classnames from "classnames"
|
|
||||||
|
|
||||||
import { Icons } from "@components/Icons"
|
|
||||||
|
|
||||||
import useLayoutInterface from "@hooks/useLayoutInterface"
|
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 () => {
|
export default () => {
|
||||||
const modalRef = React.useRef()
|
function open(
|
||||||
|
|
||||||
function openModal(
|
|
||||||
id,
|
id,
|
||||||
render,
|
render,
|
||||||
{
|
{
|
||||||
@ -127,7 +19,6 @@ export default () => {
|
|||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
app.cores.window_mng.render(id, <Modal
|
app.cores.window_mng.render(id, <Modal
|
||||||
ref={modalRef}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
app.cores.window_mng.close(id)
|
app.cores.window_mng.close(id)
|
||||||
}}
|
}}
|
||||||
@ -143,13 +34,13 @@ export default () => {
|
|||||||
</Modal>)
|
</Modal>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function close(id) {
|
||||||
modalRef.current.close()
|
app.cores.window_mng.close(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutInterface("modal", {
|
useLayoutInterface("modal", {
|
||||||
open: openModal,
|
open: open,
|
||||||
close: closeModal,
|
close: close,
|
||||||
})
|
})
|
||||||
|
|
||||||
return null
|
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,19 +4,69 @@ import classnames from "classnames"
|
|||||||
import { Translation } from "react-i18next"
|
import { Translation } from "react-i18next"
|
||||||
import { Motion, spring } from "react-motion"
|
import { Motion, spring } from "react-motion"
|
||||||
import { Menu, Avatar, Dropdown } from "antd"
|
import { Menu, Avatar, Dropdown } from "antd"
|
||||||
|
import Drawer from "@layouts/components/drawer"
|
||||||
|
|
||||||
import { Icons, createIconRender } from "@components/Icons"
|
import { Icons, createIconRender } from "@components/Icons"
|
||||||
|
import { GiLockedChest } from "react-icons/gi"
|
||||||
|
|
||||||
import sidebarItems from "@config/sidebar"
|
import sidebarItems from "@config/sidebar"
|
||||||
|
|
||||||
import "./index.less"
|
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 = {
|
const onClickHandlers = {
|
||||||
|
apps: () => {
|
||||||
|
app.layout.drawer.open("apps", AppDrawer)
|
||||||
|
},
|
||||||
addons: () => {
|
addons: () => {
|
||||||
window.app.location.push("/addons")
|
window.app.location.push("/addons")
|
||||||
},
|
},
|
||||||
studio: () => {
|
studio: () => {
|
||||||
window.app.location.push("/studio")
|
window.app.location.push("/studio")
|
||||||
},
|
},
|
||||||
settings: () => {
|
settings: () => {
|
||||||
window.app.navigation.goToSettings()
|
window.app.navigation.goToSettings()
|
||||||
@ -28,12 +78,12 @@ const onClickHandlers = {
|
|||||||
window.app.controls.openSearcher()
|
window.app.controls.openSearcher()
|
||||||
},
|
},
|
||||||
messages: () => {
|
messages: () => {
|
||||||
window.app.controls.openMessages()
|
window.app.controls.openMessages()
|
||||||
},
|
},
|
||||||
create: () => {
|
create: () => {
|
||||||
window.app.controls.openCreator()
|
window.app.controls.openCreator()
|
||||||
},
|
},
|
||||||
account: () => {
|
profile: () => {
|
||||||
window.app.navigation.goToAccount()
|
window.app.navigation.goToAccount()
|
||||||
},
|
},
|
||||||
login: () => {
|
login: () => {
|
||||||
@ -84,6 +134,13 @@ const BottomMenuDefaultItems = [
|
|||||||
</Translation>,
|
</Translation>,
|
||||||
icon: <Icons.Bell />,
|
icon: <Icons.Bell />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "apps",
|
||||||
|
label: <Translation>
|
||||||
|
{(t) => t("Apps")}
|
||||||
|
</Translation>,
|
||||||
|
icon: <Icons.MdApps />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "settings",
|
key: "settings",
|
||||||
label: <Translation>
|
label: <Translation>
|
||||||
@ -258,15 +315,7 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
events = {
|
events = {
|
||||||
"sidedrawers.visible": (has) => {
|
|
||||||
this.setState({
|
|
||||||
lockAutocollapse: has
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!has && this.state.expanded) {
|
|
||||||
this.interface.toggleCollapse(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = async () => {
|
componentDidMount = async () => {
|
||||||
@ -316,7 +365,7 @@ export default class Sidebar extends React.Component {
|
|||||||
return app.location.push(`/${item.path ?? e.key}`, 150)
|
return app.location.push(`/${item.path ?? e.key}`, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseEnter = () => {
|
onMouseEnter = (event) => {
|
||||||
if (!this.state.visible) return
|
if (!this.state.visible) return
|
||||||
|
|
||||||
if (window.app.cores.settings.is("sidebar.collapsable", false)) {
|
if (window.app.cores.settings.is("sidebar.collapsable", false)) {
|
||||||
@ -327,10 +376,15 @@ export default class Sidebar extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do nothing if is mask visible
|
||||||
|
if (app.layout.drawer.isMaskVisible()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
this.interface.toggleCollapse(true)
|
this.interface.toggleCollapse(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseLeave = () => {
|
handleMouseLeave = (event) => {
|
||||||
if (!this.state.visible) return
|
if (!this.state.visible) return
|
||||||
|
|
||||||
if (window.app.cores.settings.is("sidebar.collapsable", false)) 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}
|
ref={this.sidebarRef}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="app_sidebar_header">
|
<div className="app_sidebar_header">
|
||||||
<div className="app_sidebar_header_logo">
|
<div className="app_sidebar_header_logo">
|
||||||
<img src={config.logo?.alt} />
|
<img
|
||||||
|
src={config.logo?.alt}
|
||||||
|
onClick={() => app.navigation.goMain()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -463,6 +519,8 @@ export default class Sidebar extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Drawer />
|
||||||
</div>
|
</div>
|
||||||
}}
|
}}
|
||||||
</Motion>
|
</Motion>
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
|
|
||||||
.app_sidebar {
|
.app_sidebar {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -80,6 +83,11 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.app_sidebar_header {
|
.app_sidebar_header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -174,9 +182,9 @@
|
|||||||
&.user_avatar {
|
&.user_avatar {
|
||||||
.ant-menu-title-content {
|
.ant-menu-title-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
@ -192,8 +200,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
padding: 0 !important;
|
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 { Layout } from "antd"
|
||||||
|
|
||||||
import Sidebar from "@layouts/components/sidebar"
|
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 ToolsBar from "@layouts/components/toolsBar"
|
||||||
import Header from "@layouts/components/header"
|
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"
|
import BackgroundDecorator from "@components/BackgroundDecorator"
|
||||||
|
|
||||||
const DesktopLayout = (props) => {
|
const DesktopLayout = (props) => {
|
||||||
InitializeModalsController()
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<BackgroundDecorator />
|
<BackgroundDecorator />
|
||||||
|
<Modals />
|
||||||
|
|
||||||
<Layout id="app_layout" className="app_layout">
|
<Layout id="app_layout" className="app_layout">
|
||||||
<Drawer />
|
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Sidedrawer />
|
|
||||||
|
|
||||||
<Layout.Content
|
<Layout.Content
|
||||||
id="content_layout"
|
id="content_layout"
|
||||||
@ -45,6 +43,7 @@ const DesktopLayout = (props) => {
|
|||||||
|
|
||||||
const MobileLayout = (props) => {
|
const MobileLayout = (props) => {
|
||||||
return <Layout id="app_layout" className="app_layout">
|
return <Layout id="app_layout" className="app_layout">
|
||||||
|
<DraggableDrawerController />
|
||||||
<TopBar />
|
<TopBar />
|
||||||
|
|
||||||
<Layout.Content
|
<Layout.Content
|
||||||
@ -61,7 +60,6 @@ const MobileLayout = (props) => {
|
|||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
|
|
||||||
<BottomBar />
|
<BottomBar />
|
||||||
<Drawer />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
|
import { DraggableDrawerController } from "@layouts/components/draggableDrawer"
|
||||||
|
|
||||||
import Drawer from "@layouts/components/drawer"
|
import Drawer from "@layouts/components/drawer"
|
||||||
import Sidedrawer from "@layouts/components/sidedrawer"
|
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
return <antd.Layout className={classnames("app_layout")} style={{ height: "100%" }}>
|
return <antd.Layout className={classnames("app_layout")} style={{ height: "100%" }}>
|
||||||
<Drawer />
|
<Drawer />
|
||||||
<Sidedrawer />
|
<DraggableDrawerController />
|
||||||
|
|
||||||
<div id="transitionLayer" className="fade-transverse-active">
|
<div id="transitionLayer" className="fade-transverse-active">
|
||||||
{React.cloneElement(props.children, props)}
|
{React.cloneElement(props.children, props)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,31 +1,16 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
|
import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl"
|
||||||
|
|
||||||
import "./index.mobile.less"
|
import "./index.mobile.less"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [wallpaperData, setWallpaperData] = React.useState(null)
|
const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (app.userData) {
|
if (app.userData) {
|
||||||
app.navigation.goMain()
|
app.navigation.goMain()
|
||||||
} else {
|
} else {
|
||||||
setRandomWallpaper()
|
|
||||||
|
|
||||||
app.controls.openLoginForm({
|
app.controls.openLoginForm({
|
||||||
defaultLocked: true,
|
defaultLocked: true,
|
||||||
})
|
})
|
||||||
@ -35,7 +20,7 @@ export default (props) => {
|
|||||||
return <div className="loginPage">
|
return <div className="loginPage">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${wallpaperData?.url})`,
|
backgroundImage: `url(${randomWallpaperURL})`,
|
||||||
}}
|
}}
|
||||||
className="wallpaper"
|
className="wallpaper"
|
||||||
>
|
>
|
||||||
|
@ -64,6 +64,7 @@ const PlayerController = React.forwardRef((props, ref) => {
|
|||||||
setDraggingTime(false)
|
setDraggingTime(false)
|
||||||
|
|
||||||
app.cores.player.seek(seekTime)
|
app.cores.player.seek(seekTime)
|
||||||
|
|
||||||
syncPlayback()
|
syncPlayback()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +92,6 @@ const PlayerController = React.forwardRef((props, ref) => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setTitleIsOverflown(isOverflown(titleRef.current))
|
setTitleIsOverflown(isOverflown(titleRef.current))
|
||||||
setTrackDuration(app.cores.player.duration())
|
setTrackDuration(app.cores.player.duration())
|
||||||
console.log(context.track_manifest)
|
|
||||||
}, [context.track_manifest])
|
}, [context.track_manifest])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -104,7 +104,7 @@ const PlayerController = React.forwardRef((props, ref) => {
|
|||||||
className={classnames(
|
className={classnames(
|
||||||
"lyrics-player-controller-wrapper",
|
"lyrics-player-controller-wrapper",
|
||||||
{
|
{
|
||||||
["hidden"]: hide,
|
["hidden"]: props.lyrics?.video_source && hide,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
|
@ -25,10 +25,14 @@ const EnchancedLyrics = (props) => {
|
|||||||
async function loadLyrics(track_id) {
|
async function loadLyrics(track_id) {
|
||||||
const result = await MusicService.getTrackLyrics(track_id, {
|
const result = await MusicService.getTrackLyrics(track_id, {
|
||||||
preferTranslation: translationEnabled,
|
preferTranslation: translationEnabled,
|
||||||
|
}).catch((err) => {
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
setLyrics(result)
|
setLyrics(result)
|
||||||
|
} else {
|
||||||
|
setLyrics(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +53,7 @@ const EnchancedLyrics = (props) => {
|
|||||||
//* Handle when context change track_manifest
|
//* Handle when context change track_manifest
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setLyrics(null)
|
setLyrics(null)
|
||||||
|
|
||||||
if (context.track_manifest) {
|
if (context.track_manifest) {
|
||||||
loadLyrics(context.track_manifest._id)
|
loadLyrics(context.track_manifest._id)
|
||||||
}
|
}
|
||||||
@ -72,6 +76,20 @@ const EnchancedLyrics = (props) => {
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
!lyrics?.video_source && <div
|
||||||
|
className="lyrics-background-wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="lyrics-background-cover"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={context.track_manifest.cover}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<LyricsVideo
|
<LyricsVideo
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
lyrics={lyrics}
|
lyrics={lyrics}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.lyrics-video {
|
||||||
z-index: 105;
|
z-index: 105;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -40,7 +40,6 @@ const ChatPage = (props) => {
|
|||||||
isRemoteTyping,
|
isRemoteTyping,
|
||||||
} = useChat(to_user_id)
|
} = useChat(to_user_id)
|
||||||
|
|
||||||
|
|
||||||
console.log(R_User)
|
console.log(R_User)
|
||||||
|
|
||||||
async function submitMessage(e) {
|
async function submitMessage(e) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
|
||||||
import ChatsService from "@models/chats"
|
import ChatsService from "@models/chats"
|
||||||
|
|
||||||
import TimeAgo from "@components/TimeAgo"
|
import TimeAgo from "@components/TimeAgo"
|
||||||
|
@ -27,6 +27,13 @@ export default (props) => {
|
|||||||
case "profile": {
|
case "profile": {
|
||||||
return app.navigation.goToAccount(result.behavior.value)
|
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 React from "react"
|
||||||
import * as antd from "antd"
|
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 { Icons } from "@components/Icons"
|
||||||
import LatencyIndicator from "@components/PerformanceIndicators/latency"
|
import LatencyIndicator from "@components/PerformanceIndicators/latency"
|
||||||
@ -157,6 +157,20 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="group">
|
<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="inline_field">
|
||||||
<div className="field_header">
|
<div className="field_header">
|
||||||
<div className="field_icon">
|
<div className="field_icon">
|
||||||
@ -177,21 +191,7 @@ export default {
|
|||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Linebridge Engine</p>
|
<p>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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
@ -205,7 +205,7 @@ export default {
|
|||||||
<Icons.MdInfo />
|
<Icons.MdInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Comty.JS</p>
|
<p>Comty.js</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
@ -213,20 +213,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
capInfo && <div className="inline_field">
|
||||||
<div className="field_header">
|
<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",
|
id: "style:variant_mode",
|
||||||
group: "aspect",
|
group: "aspect",
|
||||||
component: "Switch",
|
|
||||||
icon: "Moon",
|
icon: "Moon",
|
||||||
title: "Theme",
|
title: "Theme",
|
||||||
description: "Change the theme of the application.",
|
description: "Change the theme of the application.",
|
||||||
@ -52,7 +51,7 @@ export default {
|
|||||||
max: 1.2,
|
max: 1.2,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
formatter: (value) => `${value}x`
|
formatter: (value) => `x${value}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultValue: () => {
|
defaultValue: () => {
|
||||||
@ -143,8 +142,6 @@ export default {
|
|||||||
defaultValue: () => {
|
defaultValue: () => {
|
||||||
const value = app.cores.style.getVar("backgroundImage")
|
const value = app.cores.style.getVar("backgroundImage")
|
||||||
|
|
||||||
console.log(value)
|
|
||||||
|
|
||||||
return value ? value.replace(/url\(|\)/g, "") : ""
|
return value ? value.replace(/url\(|\)/g, "") : ""
|
||||||
},
|
},
|
||||||
onUpdate: (value) => {
|
onUpdate: (value) => {
|
||||||
|
@ -14,7 +14,7 @@ import "./index.less"
|
|||||||
const FetchChangelogs = async () => {
|
const FetchChangelogs = async () => {
|
||||||
const response = await app.cores.api.customRequest({
|
const response = await app.cores.api.customRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/release-notes`,
|
url: `/repo/releases-notes`,
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
|
@ -125,11 +125,7 @@ const ChangePasswordComponent = (props) => {
|
|||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (app.isMobile) {
|
return app.layout.drawer.open("passwordChange", ChangePasswordComponent)
|
||||||
return app.layout.drawer.open("passwordChange", ChangePasswordComponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.layout.sidedrawer.open("passwordChange", ChangePasswordComponent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <antd.Button onClick={onClick}>
|
return <antd.Button onClick={onClick}>
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
border: 1px var(--border-color) solid;
|
border: 1px var(--border-color) solid;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.__setting_theme_variant_selector-variant {
|
.__setting_theme_variant_selector-variant {
|
||||||
|
@ -60,7 +60,7 @@ export default {
|
|||||||
group: "ui.sounds",
|
group: "ui.sounds",
|
||||||
component: "Switch",
|
component: "Switch",
|
||||||
icon: "MdVolumeUp",
|
icon: "MdVolumeUp",
|
||||||
title: "UI effects",
|
title: "Effects",
|
||||||
description: "Enable the UI effects.",
|
description: "Enable the UI effects.",
|
||||||
mobile: false,
|
mobile: false,
|
||||||
},
|
},
|
||||||
@ -70,8 +70,8 @@ export default {
|
|||||||
group: "ui.sounds",
|
group: "ui.sounds",
|
||||||
component: "Slider",
|
component: "Slider",
|
||||||
icon: "MdVolumeUp",
|
icon: "MdVolumeUp",
|
||||||
title: "UI volume",
|
title: "Volume",
|
||||||
description: "Set the volume of the app sounds.",
|
description: "Set the volume of the app UI sounds.",
|
||||||
props: {
|
props: {
|
||||||
tipFormatter: (value) => {
|
tipFormatter: (value) => {
|
||||||
return `${value}%`
|
return `${value}%`
|
||||||
@ -108,7 +108,7 @@ export default {
|
|||||||
group: "notifications",
|
group: "notifications",
|
||||||
component: "Slider",
|
component: "Slider",
|
||||||
icon: "MdVolumeUp",
|
icon: "MdVolumeUp",
|
||||||
title: "Sound Volume",
|
title: "Volume",
|
||||||
description: "Set the volume of the sound when a notification is received.",
|
description: "Set the volume of the sound when a notification is received.",
|
||||||
props: {
|
props: {
|
||||||
tipFormatter: (value) => {
|
tipFormatter: (value) => {
|
||||||
|
@ -152,7 +152,7 @@ const TagItem = (props) => {
|
|||||||
<antd.Button
|
<antd.Button
|
||||||
icon={<Icons.MdDelete />}
|
icon={<Icons.MdDelete />}
|
||||||
danger
|
danger
|
||||||
disabled
|
onClick={props.onDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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) => {
|
handleTagRead = async (error, tag) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@ -252,6 +271,9 @@ class OwnTags extends React.Component {
|
|||||||
tag
|
tag
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
this.handleTagDelete(tag)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -137,9 +137,9 @@ export default (props) => {
|
|||||||
</antd.Select.Option>
|
</antd.Select.Option>
|
||||||
|
|
||||||
<antd.Select.Option
|
<antd.Select.Option
|
||||||
value="post"
|
value="random_list"
|
||||||
>
|
>
|
||||||
Post
|
Random list
|
||||||
</antd.Select.Option>
|
</antd.Select.Option>
|
||||||
</antd.Select>
|
</antd.Select>
|
||||||
</antd.Form.Item>
|
</antd.Form.Item>
|
||||||
|
@ -331,4 +331,17 @@ 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 path from "path"
|
||||||
|
import aliases from "./aliases"
|
||||||
|
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
|
|
||||||
const aliases = {
|
const oneYearInSeconds = 60 * 60 * 24 * 365
|
||||||
"@": 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"),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -35,7 +22,10 @@ export default defineConfig({
|
|||||||
https: {
|
https: {
|
||||||
key: path.join(__dirname, "ssl", "privkey.pem"),
|
key: path.join(__dirname, "ssl", "privkey.pem"),
|
||||||
cert: path.join(__dirname, "ssl", "cert.pem"),
|
cert: path.join(__dirname, "ssl", "cert.pem"),
|
||||||
}
|
},
|
||||||
|
headers: {
|
||||||
|
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
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 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 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
|
USER node
|
||||||
|
|
||||||
EXPOSE 3010
|
# Install modules & rebuild for host
|
||||||
|
RUN npm install
|
||||||
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
|
|
||||||
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
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 { Observable } from "@gullerya/object-observer"
|
||||||
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
||||||
import EventEmitter from "@foxify/events"
|
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 chokidar from "chokidar"
|
||||||
import { onExit } from "signal-exit"
|
import { onExit } from "signal-exit"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import httpProxy from "http-proxy"
|
import httpProxy from "http-proxy"
|
||||||
import defaults from "linebridge/src/server/defaults"
|
import defaults from "linebridge/dist/server/defaults"
|
||||||
|
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
|
|
||||||
|
@ -1,36 +1,54 @@
|
|||||||
{
|
{
|
||||||
"name": "@comty/server",
|
"name": "@comty/server",
|
||||||
"version": "0.70.0",
|
"version": "0.70.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"services/*"
|
"services/*"
|
||||||
],
|
],
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "hermes build",
|
"start:prod": "cross-env NODE_ENV=production hermes-node ./index.js",
|
||||||
"dev": "cross-env NODE_ENV=development hermes-node ./index.js",
|
"dev": "cross-env NODE_ENV=development hermes-node ./index.js",
|
||||||
"run:prod": "cross-env NODE_ENV=production node ./dist/index.js"
|
"build:bin": "cd build && pkg ./index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gullerya/object-observer": "^6.1.3",
|
"@gullerya/object-observer": "^6.1.3",
|
||||||
"@infisical/sdk": "^2.1.8",
|
"@infisical/sdk": "^2.1.8",
|
||||||
"@ragestudio/hermes": "^0.1.1",
|
"@ragestudio/hermes": "^0.1.1",
|
||||||
"chalk": "4.1.2",
|
"axios": "^1.7.4",
|
||||||
"dotenv": "^16.4.4",
|
"bcrypt": "^5.1.1",
|
||||||
"http-proxy": "^1.18.1",
|
"chalk": "4.1.2",
|
||||||
"linebridge": "^0.18.1",
|
"comty.js": "^0.60.3",
|
||||||
"module-alias": "^2.2.3",
|
"dotenv": "^16.4.4",
|
||||||
"nodejs-snowflake": "^2.0.1",
|
"http-proxy": "^1.18.1",
|
||||||
"signal-exit": "^4.1.0",
|
"ioredis": "^5.4.1",
|
||||||
"spinnies": "^0.5.1",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"tree-kill": "^1.2.2"
|
"linebridge": "^0.20.3",
|
||||||
},
|
"minio": "^8.0.1",
|
||||||
"devDependencies": {
|
"module-alias": "^2.2.3",
|
||||||
"chai": "^5.1.0",
|
"mongoose": "^8.5.3",
|
||||||
"cross-env": "^7.0.3",
|
"nodejs-snowflake": "^2.0.1",
|
||||||
"mocha": "^10.3.0"
|
"qs": "^6.13.0",
|
||||||
},
|
"signal-exit": "^4.1.0",
|
||||||
"resolutions": {
|
"spinnies": "^0.5.1",
|
||||||
"string-width": "4.2.3"
|
"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 DbManager from "@shared-classes/DbManager"
|
||||||
|
|
||||||
import SharedMiddlewares from "@shared-middlewares"
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "auth",
|
"name": "auth",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0"
|
||||||
"main": "index.js",
|
|
||||||
"license": "MIT",
|
|
||||||
"proxy":{
|
|
||||||
"namespace": "/auth",
|
|
||||||
"port": 3020
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -42,17 +42,18 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
if (mfaSession) {
|
if (mfaSession) {
|
||||||
if (!req.body.mfa_code) {
|
if (!req.body.mfa_code) {
|
||||||
await mfaSession.delete()
|
await MFASession.deleteMany({ user_id: user._id })
|
||||||
} else {
|
} else {
|
||||||
if (mfaSession.expires_at < new Date().getTime()) {
|
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...")
|
throw new OperationError(401, "MFA code expired, login again...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mfaSession.code == req.body.mfa_code) {
|
if (mfaSession.code == req.body.mfa_code) {
|
||||||
codeVerified = true
|
codeVerified = true
|
||||||
await mfaSession.delete()
|
|
||||||
|
await MFASession.deleteMany({ user_id: user._id })
|
||||||
} else {
|
} else {
|
||||||
throw new OperationError(401, "Invalid MFA code, try again...")
|
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 DbManager from "@shared-classes/DbManager"
|
||||||
import RedisClient from "@shared-classes/RedisClient"
|
import RedisClient from "@shared-classes/RedisClient"
|
||||||
|
@ -1,25 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "chats",
|
"name": "chats",
|
||||||
"version": "0.60.2",
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -64,7 +64,7 @@ export default {
|
|||||||
history = await Promise.all(history)
|
history = await Promise.all(history)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: await ChatMessage.count(query),
|
total: await ChatMessage.countDocuments(query),
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
order: order,
|
order: order,
|
||||||
|
@ -28,8 +28,6 @@ export default async (socket, payload, engine) => {
|
|||||||
|
|
||||||
const targetSocket = await engine.find.socketByUserId(payload.to_user_id)
|
const targetSocket = await engine.find.socketByUserId(payload.to_user_id)
|
||||||
|
|
||||||
console.log(targetSocket)
|
|
||||||
|
|
||||||
if (targetSocket) {
|
if (targetSocket) {
|
||||||
await targetSocket.emit("chat:receive:message", wsMessageObj)
|
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 nodemailer from "nodemailer"
|
||||||
import DbManager from "@shared-classes/DbManager"
|
import DbManager from "@shared-classes/DbManager"
|
||||||
|
|
||||||
|
@ -2,12 +2,6 @@
|
|||||||
"name": "ems",
|
"name": "ems",
|
||||||
"description": "External Messaging Service (SMS, EMAIL, PUSH)",
|
"description": "External Messaging Service (SMS, EMAIL, PUSH)",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"main": "index.js",
|
|
||||||
"license": "MIT",
|
|
||||||
"proxy": {
|
|
||||||
"path": "/ems",
|
|
||||||
"port": 3007
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"nodemailer": "^6.9.11",
|
"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"
|
import B2 from "backblaze-b2"
|
||||||
|
|
||||||
|
@ -1,31 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "files",
|
"name": "files",
|
||||||
"version": "0.60.2",
|
"version": "0.60.2",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@foxify/events": "^2.1.0",
|
|
||||||
"axios": "^1.4.0",
|
|
||||||
"backblaze-b2": "^1.7.0",
|
"backblaze-b2": "^1.7.0",
|
||||||
"bcrypt": "^5.1.0",
|
|
||||||
"busboy": "^1.6.0",
|
"busboy": "^1.6.0",
|
||||||
"comty.js": "^0.58.2",
|
|
||||||
"connect-mongo": "^4.6.0",
|
|
||||||
"content-range": "^2.0.2",
|
"content-range": "^2.0.2",
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.0.3",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"jsonwebtoken": "^9.0.0",
|
|
||||||
"linebridge": "0.15.12",
|
|
||||||
"luxon": "^3.0.4",
|
|
||||||
"merge-files": "^0.1.2",
|
"merge-files": "^0.1.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"minio": "^7.0.32",
|
"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",
|
"normalize-url": "^8.0.0",
|
||||||
"p-map": "4.0.0",
|
"p-map": "4.0.0",
|
||||||
"p-queue": "^7.3.4",
|
"p-queue": "^7.3.4",
|
||||||
|
@ -5,6 +5,8 @@ import ChunkFileUpload from "@shared-classes/ChunkFileUpload"
|
|||||||
|
|
||||||
import RemoteUpload from "@services/remoteUpload"
|
import RemoteUpload from "@services/remoteUpload"
|
||||||
|
|
||||||
|
const availableProviders = ["b2", "standard"]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
useContext: ["cache", "limits"],
|
useContext: ["cache", "limits"],
|
||||||
middlewares: [
|
middlewares: [
|
||||||
@ -34,6 +36,11 @@ export default {
|
|||||||
limits.useProvider = req.headers["provider-type"] ?? "b2"
|
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, {
|
let build = await ChunkFileUpload(req, {
|
||||||
tmpDir: tmpPath,
|
tmpDir: tmpPath,
|
||||||
...limits,
|
...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