merge from local

This commit is contained in:
SrGooglo 2024-11-17 20:35:20 +00:00
parent 17892508e7
commit 00f6e34c53
45 changed files with 757 additions and 395 deletions

View File

@ -1,4 +1,13 @@
services: services:
app:
build: packages/app
restart: unless-stopped
networks:
- internal_network
ports:
- "3000:3000"
env_file:
- ./.env
api: api:
build: packages/server build: packages/server
restart: unless-stopped restart: unless-stopped

View File

@ -1,4 +1,4 @@
FROM node:16-alpine FROM node:20-alpine
RUN apk add git RUN apk add git
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

View File

@ -4,17 +4,6 @@
"label": "Search", "label": "Search",
"icon": "FiSearch" "icon": "FiSearch"
}, },
{
"id": "messages",
"label": "Messages",
"icon": "FiMessageCircle",
"path": "/messages"
},
{
"id": "notifications",
"label": "Notifications",
"icon": "FiBell"
},
{ {
"id": "settings", "id": "settings",
"label": "Settings", "label": "Settings",

View File

@ -45,7 +45,7 @@
"fast-average-color": "^9.2.0", "fast-average-color": "^9.2.0",
"framer-motion": "^10.12.17", "framer-motion": "^10.12.17",
"fuse.js": "6.5.3", "fuse.js": "6.5.3",
"hls.js": "^1.5.15", "hls.js": "^1.5.17",
"howler": "2.2.3", "howler": "2.2.3",
"i18next": "21.6.6", "i18next": "21.6.6",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",

View File

@ -75,17 +75,9 @@ const Attachment = React.memo((props) => {
return <ImageViewer src={url} /> return <ImageViewer src={url} />
} }
case "video": { case "video": {
return <Plyr return <video controls>
source={{ <source src={url} type={mimeType} />
type: "video", </video>
sources: [{
src: url,
}],
}}
options={{
controls: ["play", "progress", "current-time", "mute", "volume"],
}}
/>
} }
case "audio": { case "audio": {
return <audio controls> return <audio controls>

View File

@ -68,6 +68,7 @@
video { video {
height: 100%; height: 100%;
width: 100%;
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;

View File

@ -12,6 +12,7 @@ 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 SearchModel from "@models/search"
import "./index.less" import "./index.less"
@ -33,6 +34,8 @@ export default class PostCreator extends React.Component {
fileList: [], fileList: [],
postingPolicy: DEFAULT_POST_POLICY, postingPolicy: DEFAULT_POST_POLICY,
mentionsLoadedData: []
} }
pollRef = React.createRef() pollRef = React.createRef()
@ -59,14 +62,6 @@ export default class PostCreator extends React.Component {
}) })
} }
fetchUploadPolicy = async () => {
const policy = await PostModel.getPostingPolicy()
this.setState({
postingPolicy: policy
})
}
canSubmit = () => { canSubmit = () => {
const { postMessage, postAttachments, pending, postingPolicy } = this.state const { postMessage, postAttachments, pending, postingPolicy } = this.state
@ -83,9 +78,7 @@ export default class PostCreator extends React.Component {
return true return true
} }
debounceSubmit = lodash.debounce(() => this.submit(), 50) submit = lodash.debounce(async () => {
submit = async () => {
if (this.state.loading) { if (this.state.loading) {
return false return false
} }
@ -154,10 +147,9 @@ export default class PostCreator extends React.Component {
app.navigation.goToPost(this.props.reply_to) app.navigation.goToPost(this.props.reply_to)
} }
} }
} }, 50)
uploadFile = async (req) => { uploadFile = async (req) => {
// hide uploader
this.toggleUploaderVisibility(false) this.toggleUploaderVisibility(false)
const request = await app.cores.remoteStorage.uploadFile(req.file) const request = await app.cores.remoteStorage.uploadFile(req.file)
@ -265,18 +257,18 @@ export default class PostCreator extends React.Component {
} }
} }
onChangeMessageInput = (event) => { handleMessageInputChange = (inputText) => {
// if the fist character is a space or a whitespace remove it // if the fist character is a space or a whitespace remove it
if (event.target.value[0] === " " || event.target.value[0] === "\n") { if (inputText[0] === " " || inputText[0] === "\n") {
event.target.value = event.target.value.slice(1) inputText = inputText.slice(1)
} }
this.setState({ this.setState({
postMessage: event.target.value postMessage: inputText
}) })
} }
handleKeyDown = async (e) => { handleMessageInputKeydown = async (e) => {
// detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed // detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed
if (e.keyCode === 13 && !e.shiftKey) { if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault() e.preventDefault()
@ -290,6 +282,15 @@ export default class PostCreator extends React.Component {
} }
} }
handleOnMentionSearch = lodash.debounce(async (value) => {
const results = await SearchModel.userSearch(`username:${value}`)
this.setState({
mentionsLoadedData: results
})
}, 300)
updateFileList = (uid, newValue) => { updateFileList = (uid, newValue) => {
let updatedFileList = this.state.fileList let updatedFileList = this.state.fileList
@ -577,16 +578,28 @@ export default class PostCreator extends React.Component {
<img src={app.userData?.avatar} /> <img src={app.userData?.avatar} />
</div> </div>
<antd.Input.TextArea <antd.Mentions
placeholder="What are you thinking?" placeholder="What are you thinking?"
value={postMessage} value={postMessage}
autoSize={{ minRows: 3, maxRows: 6 }} autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={postingPolicy.maxMessageLength} maxLength={postingPolicy.maxMessageLength}
onChange={this.onChangeMessageInput} onChange={this.handleMessageInputChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleMessageInputKeydown}
disabled={loading} disabled={loading}
draggable={false} draggable={false}
prefix="@"
allowClear allowClear
options={this.state.mentionsLoadedData.map((item) => {
return {
key: item.id,
value: item.username,
label: <>
<antd.Avatar src={item.avatar} />
<span>{item.username}</span>
</>,
}
})}
onSearch={this.handleOnMentionSearch}
/> />
<div> <div>

View File

@ -21,6 +21,9 @@ const TrendingsCard = (props) => {
{ {
E_Trendings && <span>Something went wrong</span> E_Trendings && <span>Something went wrong</span>
} }
{
!L_Trendings && !E_Trendings && R_Trendings && R_Trendings.length === 0 && <span>No trendings</span>
}
{ {
!L_Trendings && !E_Trendings && R_Trendings && R_Trendings.map((trending, index) => { !L_Trendings && !E_Trendings && R_Trendings && R_Trendings.map((trending, index) => {
return <div return <div

View File

@ -273,7 +273,7 @@ export default class Player extends Core {
}) })
} }
async playbackMode(mode) { playbackMode(mode) {
if (typeof mode !== "string") { if (typeof mode !== "string") {
return this.state.playback_mode return this.state.playback_mode
} }
@ -289,7 +289,7 @@ export default class Player extends Core {
return mode return mode
} }
async stopPlayback() { stopPlayback() {
if (this.queue.currentItem) { if (this.queue.currentItem) {
this.queue.currentItem.stop() this.queue.currentItem.stop()
} }

View File

@ -131,7 +131,6 @@ export default class StyleCore extends Core {
mutateTheme: (...args) => this.mutateTheme(...args), mutateTheme: (...args) => this.mutateTheme(...args),
resetToDefault: () => this.resetToDefault(), resetToDefault: () => this.resetToDefault(),
toggleCompactMode: () => this.toggleCompactMode(),
} }
async onInitialize() { async onInitialize() {
@ -251,20 +250,6 @@ export default class StyleCore extends Core {
StyleCore.storagedModifications = this.public.mutation StyleCore.storagedModifications = this.public.mutation
} }
toggleCompactMode(value = !window.app.cores.settings.get("style.compactMode")) {
if (value === true) {
return this.applyStyles({
layoutMargin: 0,
layoutPadding: 0,
})
}
return this.applyStyles({
layoutMargin: this.getVar("layoutMargin"),
layoutPadding: this.getVar("layoutPadding"),
})
}
resetToDefault() { resetToDefault() {
store.remove(StyleCore.modificationStorageKey) store.remove(StyleCore.modificationStorageKey)

View File

@ -21,8 +21,6 @@ export default class WidgetsCore extends Core {
async onInitialize() { async onInitialize() {
try { try {
//await WidgetsCore.apiInstance()
const currentStore = this.getInstalled() const currentStore = this.getInstalled()
if (!Array.isArray(currentStore)) { if (!Array.isArray(currentStore)) {

View File

@ -3,14 +3,14 @@ import React from "react"
export default () => { export default () => {
const enterPlayerAnimation = () => { const enterPlayerAnimation = () => {
app.cores.style.applyTemporalVariant("dark") app.cores.style.applyTemporalVariant("dark")
app.cores.style.toggleCompactMode(true) app.layout.toggleCompactMode(true)
app.layout.toggleCenteredContent(false) app.layout.toggleCenteredContent(false)
app.controls.toggleUIVisibility(false) app.controls.toggleUIVisibility(false)
} }
const exitPlayerAnimation = () => { const exitPlayerAnimation = () => {
app.cores.style.applyVariant(app.cores.style.getStoragedVariantKey()) app.cores.style.applyVariant(app.cores.style.getStoragedVariantKey())
app.cores.style.toggleCompactMode(false) app.layout.toggleCompactMode(false)
app.layout.toggleCenteredContent(true) app.layout.toggleCenteredContent(true)
app.controls.toggleUIVisibility(true) app.controls.toggleUIVisibility(true)
} }

View File

@ -108,6 +108,9 @@ export default class Layout extends React.PureComponent {
togglePagePanelSpacer: (to) => { togglePagePanelSpacer: (to) => {
return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to) return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to)
}, },
toggleCompactMode: (to) => {
return this.layoutInterface.toggleRootContainerClassname("compact-mode", to)
},
toggleRootContainerClassname: (classname, to) => { toggleRootContainerClassname: (classname, to) => {
const root = document.documentElement const root = document.documentElement

View File

@ -61,10 +61,14 @@ export default class ToolsBar extends React.Component {
} }
render() { render() {
const hasAnyRenders = this.state.renders.top.length > 0 || this.state.renders.bottom.length > 0
const isVisible = hasAnyRenders && this.state.visible
return <Motion return <Motion
style={{ style={{
x: spring(this.state.visible ? 0 : 100), x: spring(isVisible ? 0 : 100),
width: spring(this.state.visible ? 100 : 0), width: spring(isVisible ? 100 : 0),
}} }}
> >
{({ x, width }) => { {({ x, width }) => {
@ -76,7 +80,7 @@ export default class ToolsBar extends React.Component {
className={classnames( className={classnames(
"tools-bar-wrapper", "tools-bar-wrapper",
{ {
visible: this.state.visible, visible: isVisible,
} }
)} )}
> >

View File

@ -18,34 +18,6 @@ function isOverflown(element) {
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth; return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
} }
const RenderArtist = (props) => {
const { artist } = props
if (!artist) {
return null
}
if (Array.isArray(artist)) {
return <h3>{artist.join(",")}</h3>
}
return <h3>{artist}</h3>
}
const RenderAlbum = (props) => {
const { album } = props
if (!album) {
return null
}
if (Array.isArray(album)) {
return <h3>{album.join(",")}</h3>
}
return <h3>{album}</h3>
}
const PlayerController = React.forwardRef((props, ref) => { const PlayerController = React.forwardRef((props, ref) => {
const [playerState] = usePlayerStateContext() const [playerState] = usePlayerStateContext()
@ -152,9 +124,7 @@ const PlayerController = React.forwardRef((props, ref) => {
</div> </div>
<div className="lyrics-player-controller-info-details"> <div className="lyrics-player-controller-info-details">
<RenderArtist artist={playerState.track_manifest?.artists} /> <span>{playerState.track_manifest?.artistStr}</span>
-
<RenderAlbum album={playerState.track_manifest?.album} />
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
const { lyrics } = props const { lyrics } = props
const [initialLoading, setInitialLoading] = React.useState(true)
const [syncInterval, setSyncInterval] = React.useState(null) const [syncInterval, setSyncInterval] = React.useState(null)
const [syncingVideo, setSyncingVideo] = React.useState(false) const [syncingVideo, setSyncingVideo] = React.useState(false)
const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0) const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0)
@ -29,21 +30,18 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
setSyncingVideo(true) setSyncingVideo(true)
videoRef.current.currentTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000 let newTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000
// dec some ms to ensure the video seeks correctly
newTime -= 5 / 1000
videoRef.current.currentTime = newTime
} }
async function syncPlayback() { async function syncPlayback() {
if (!lyrics) { // if something is wrong, stop syncing
return false if (videoRef.current === null || !lyrics || !lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined" || playerState.playback_status !== "playing") {
} return stopSyncInterval()
// if `sync_audio_at_ms` is present, it means the video must be synced with audio
if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") {
if (!videoRef.current) {
clearInterval(syncInterval)
setSyncInterval(null)
setCurrentVideoLatency(0)
return false
} }
const currentTrackTime = app.cores.player.controls.seek() const currentTrackTime = app.cores.player.controls.seek()
@ -64,51 +62,18 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
seekVideoToSyncAudio() seekVideoToSyncAudio()
} }
} }
}
function startSyncInterval() { function startSyncInterval() {
setSyncInterval(setInterval(syncPlayback, 300)) setSyncInterval(setInterval(syncPlayback, 300))
} }
React.useEffect(() => { function stopSyncInterval() {
videoRef.current.addEventListener("seeked", (event) => {
setSyncingVideo(false) setSyncingVideo(false)
}) setSyncInterval(null)
// videoRef.current.addEventListener("error", (event) => {
// console.log("Failed to load", event)
// })
// videoRef.current.addEventListener("ended", (event) => {
// console.log("Video ended", event)
// })
// videoRef.current.addEventListener("stalled", (event) => {
// console.log("Failed to fetch data, but trying")
// })
// videoRef.current.addEventListener("waiting", (event) => {
// console.log("Waiting for data...")
// })
}, [])
//* Handle when playback status change
React.useEffect(() => {
if (lyrics?.video_source && typeof lyrics?.sync_audio_at_ms !== "undefined") {
if (playerState.playback_status === "playing") {
videoRef.current.play()
setSyncInterval(setInterval(syncPlayback, 500))
} else {
videoRef.current.pause()
if (syncInterval) {
clearInterval(syncInterval) clearInterval(syncInterval)
} }
}
}
}, [playerState.playback_status])
//* handle when player is loading
React.useEffect(() => { React.useEffect(() => {
if (lyrics?.video_source && playerState.loading === true && playerState.playback_status === "playing") { if (lyrics?.video_source && playerState.loading === true && playerState.playback_status === "playing") {
videoRef.current.pause() videoRef.current.pause()
@ -119,50 +84,61 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
} }
}, [playerState.loading]) }, [playerState.loading])
//* Handle when lyrics object change //* Handle when playback status change
React.useEffect(() => { React.useEffect(() => {
clearInterval(syncInterval) if (initialLoading === false) {
setCurrentVideoLatency(0) console.log(`VIDEO:: Playback status changed to ${playerState.playback_status}`)
setSyncingVideo(false)
if (lyrics) {
if (lyrics.video_source) {
hls.current.loadSource(lyrics.video_source)
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
if (lyrics && lyrics.video_source) {
if (playerState.playback_status === "playing") { if (playerState.playback_status === "playing") {
videoRef.current.play() videoRef.current.play()
startSyncInterval() startSyncInterval()
} else { } else {
videoRef.current.pause() videoRef.current.pause()
stopSyncInterval()
} }
const currentTime = app.cores.player.controls.seek()
if (currentTime > 0) {
seekVideoToSyncAudio()
} }
}
}, [playerState.playback_status])
//* Handle when lyrics object change
React.useEffect(() => {
setCurrentVideoLatency(0)
stopSyncInterval()
if (lyrics) {
if (lyrics.video_source) {
console.log("Loading video source >", lyrics.video_source)
hls.current.loadSource(lyrics.video_source)
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
videoRef.current.loop = false
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
startSyncInterval()
} else { } else {
videoRef.current.loop = true videoRef.current.loop = true
videoRef.current.currentTime = 0
}
if (playerState.playback_status === "playing"){
videoRef.current.play() videoRef.current.play()
} }
} else {
videoRef.current
} }
} else {
videoRef.current
} }
setInitialLoading(false)
}, [lyrics]) }, [lyrics])
React.useEffect(() => { React.useEffect(() => {
clearInterval(syncInterval) videoRef.current.addEventListener("seeked", (event) => {
setSyncingVideo(false)
})
hls.current.attachMedia(videoRef.current) hls.current.attachMedia(videoRef.current)
return () => { return () => {
clearInterval(syncInterval) stopSyncInterval()
} }
}, []) }, [])

View File

@ -8,11 +8,15 @@ import PostsList from "@components/PostsList"
import PostService from "@models/post" import PostService from "@models/post"
import useCenteredContainer from "@hooks/useCenteredContainer"
import "./index.less" import "./index.less"
const PostPage = (props) => { const PostPage = (props) => {
const post_id = props.params.post_id const post_id = props.params.post_id
useCenteredContainer(true)
const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, { const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
post_id, post_id,
}) })

View File

@ -0,0 +1,27 @@
import React from "react"
import PostList from "@components/PostsList"
import PostModel from "@models/post"
import "./index.less"
const TrendingPage = (props) => {
const { trending } = props.params
return <div className="trending-page">
<div className="trending-page-header">
<h1>#{trending.toLowerCase()}</h1>
</div>
<div className="trending-page-content">
<PostList
loadFromModel={PostModel.getTrending}
loadFromModelProps={{
trending,
}}
/>
</div>
</div>
}
export default TrendingPage

View File

@ -0,0 +1,18 @@
.trending-page {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
.trending-page-header {
display: flex;
flex-direction: column;
}
.trending-page-content {
display: flex;
flex-direction: column;
}
}

View File

@ -257,14 +257,14 @@ export default class StreamViewer extends React.Component {
enterPlayerAnimation = () => { enterPlayerAnimation = () => {
app.cores.style.applyTemporalVariant("dark") app.cores.style.applyTemporalVariant("dark")
app.cores.style.toggleCompactMode(true) app.layout.toggleCompactMode(true)
app.layout.toggleCenteredContent(false) app.layout.toggleCenteredContent(false)
app.controls.toggleUIVisibility(false) app.controls.toggleUIVisibility(false)
} }
exitPlayerAnimation = () => { exitPlayerAnimation = () => {
app.cores.style.applyVariant(app.cores.style.currentVariantKey) app.cores.style.applyVariant(app.cores.style.currentVariantKey)
app.cores.style.toggleCompactMode(false) app.layout.toggleCompactMode(false)
app.layout.toggleCenteredContent(true) app.layout.toggleCenteredContent(true)
app.controls.toggleUIVisibility(true) app.controls.toggleUIVisibility(true)
} }

View File

@ -14,29 +14,4 @@ export default [
component: LivestreamsList, component: LivestreamsList,
disabled: true, disabled: true,
}, },
{
key: "controlPanel",
label: "Creator Panel",
icon: "MdSpaceDashboard",
children: [
{
key: "controlPanel.uploads",
label: "Uploads",
icon: "FiUpload",
disabled: true
},
{
key: "controlPanel.streaming_settings",
label: "Stream Configuration",
icon: "FiSettings",
disabled: true,
},
{
key: "controlPanel.dvr_settings",
label: "DVR",
icon: "MdFiberDvr",
disabled: true
}
]
}
] ]

View File

@ -7,16 +7,21 @@ import WidgetItemPreview from "@components/WidgetItemPreview"
import "./index.less" import "./index.less"
export default class WidgetsManager extends React.Component { const WidgetsManager = () => {
state = { const [loadedWidgets, setLoadedWidgets] = React.useState(app.cores.widgets.getInstalled() ?? [])
loadedWidgets: app.cores.widgets.getInstalled() ?? [],
} React.useEffect(() => {
if (app.layout.tools_bar) {
app.layout.tools_bar.toggleVisibility(true)
}
}, [])
render() {
return <div className="widgets-manager"> return <div className="widgets-manager">
<h1>Widgets</h1>
<div className="widgets-manager-list"> <div className="widgets-manager-list">
{ {
Array.isArray(this.state.loadedWidgets) && this.state.loadedWidgets.map((manifest) => { Array.isArray(loadedWidgets) && loadedWidgets.map((manifest) => {
return <React.Fragment> return <React.Fragment>
<WidgetItemPreview <WidgetItemPreview
manifest={manifest} manifest={manifest}
@ -50,12 +55,13 @@ export default class WidgetsManager extends React.Component {
<antd.Button <antd.Button
type="primary" type="primary"
icon={<Icons.FiPlus />} icon={<Icons.FiPlus />}
onClick={openWidgetsBrowserModal} onClick={() => { }}
> >
Install more Install more
</antd.Button> </antd.Button>
</div> </div>
</div> </div>
</div> </div>
}
} }
export default WidgetsManager

View File

@ -1,5 +1,3 @@
import React from "react"
import WidgetsManager from "../components/widgetsManager" import WidgetsManager from "../components/widgetsManager"
export default { export default {
@ -7,17 +5,5 @@ export default {
icon: "FiList", icon: "FiList",
label: "Widgets", label: "Widgets",
group: "app", group: "app",
render: () => { render: WidgetsManager
React.useEffect(() => {
if (app.layout.tools_bar) {
app.layout.tools_bar.toggleVisibility(true)
}
}, [])
return <div>
<h1>Widgets</h1>
<WidgetsManager />
</div>
},
} }

View File

@ -577,7 +577,8 @@
} }
// fix input // fix input
.ant-input { .ant-input,
.ant-mentions {
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
color: var(--text-color); color: var(--text-color);
@ -588,11 +589,37 @@
} }
} }
.ant-input-affix-wrapper { .ant-mentions-outlined {
background-color: var(--background-color-accent); background: transparent;
&:hover {
background: transparent;
background-color: var(--background-color-accent) !important;
}
&:focus {
background: transparent;
background-color: var(--background-color-accent) !important;
}
&:focus-within {
background: transparent;
background-color: var(--background-color-accent) !important;
}
}
.ant-input-affix-wrapper,
.ant-mentions-affix-wrapper {
background: transparent;
background-color: var(--background-color-accent);
color: var(--text-color); color: var(--text-color);
border: 0 !important;
:hover {
border: 0 !important;
}
::placeholder { ::placeholder {
color: var(--text-color); color: var(--text-color);
opacity: 0.5; opacity: 0.5;

View File

@ -83,6 +83,8 @@ a {
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
overflow-x: hidden;
&.electron { &.electron {
.ant-layout-sider { .ant-layout-sider {
padding-top: 0px; padding-top: 0px;
@ -167,6 +169,13 @@ html {
// } // }
// } // }
&.compact-mode {
.content_layout {
padding: 0 !important;
margin: 0 !important;
}
}
&.centered-content { &.centered-content {
.content_layout { .content_layout {
margin: 0 auto; margin: 0 auto;

View File

@ -2,13 +2,33 @@ export default {
name: "Extension", name: "Extension",
collection: "extensions", collection: "extensions",
schema: { schema: {
user_id: {
type: String,
required: true
},
assetsUrl: {
type: String,
required: true
},
srcUrl: {
type: String,
required: true
},
registryId: {
type: String,
required: true
},
packageUrl: {
type: String,
required: true
},
version: { version: {
type: String, type: String,
required: true required: true
}, },
title: { name: {
type: String, type: String,
default: "Untitled" default: "untitled"
}, },
description: { description: {
type: String, type: String,
@ -22,9 +42,5 @@ export default {
type: Date, type: Date,
required: true, required: true,
}, },
experimental: {
type: Boolean,
default: false
},
} }
} }

View File

@ -172,12 +172,14 @@ export default class Proxy {
if (!route) { if (!route) {
res.statusCode = 404 res.statusCode = 404
res.end(`
{ res.end(JSON.stringify({
"error": "404 Not found" error: "Gateway route not found",
} details: "The gateway route you are trying to access does not exist, maybe the service is down...",
`) namespace: namespace,
return }))
return null
} }
if (route.pathRewrite) { if (route.pathRewrite) {

View File

@ -1,8 +1,6 @@
import templates from "../templates" import templates from "../templates"
export default async (ctx, data) => { export default async (ctx, data) => {
console.log(`EMS Send account activation `, data)
const { user, activation_code } = data const { user, activation_code } = data
const result = await ctx.mailTransporter.sendMail({ const result = await ctx.mailTransporter.sendMail({

View File

@ -0,0 +1,3 @@
export default class Extension {
static resolve = require("./methods/resolve").default
}

View File

@ -0,0 +1,24 @@
import { Extension } from "@db_models"
export default async function resolve(payload) {
let { user_id, pkg } = payload
const [pkgName, pkgVersion] = pkg.split("@")
if (!pkgVersion) {
pkgVersion = "latest"
}
if (pkgVersion === "latest") {
return await Extension.findOne({
user_id,
name: pkgName,
}).sort({ version: -1 }).limit(1).exec()
}
return await Extension.findOne({
user_id,
name: pkgName,
version: pkgVersion,
})
}

View File

@ -1,6 +1,8 @@
import { Server } from "linebridge" import { Server } from "linebridge"
import B2 from "backblaze-b2"
import DbManager from "@shared-classes/DbManager" import DbManager from "@shared-classes/DbManager"
import CacheService from "@shared-classes/CacheService"
import SharedMiddlewares from "@shared-middlewares" import SharedMiddlewares from "@shared-middlewares"
@ -16,10 +18,21 @@ class API extends Server {
contexts = { contexts = {
db: new DbManager(), db: new DbManager(),
b2: new B2({
applicationKeyId: process.env.B2_KEY_ID,
applicationKey: process.env.B2_APP_KEY,
}),
cache: new CacheService({
fsram: false
}),
} }
async onInitialize() { async onInitialize() {
await this.contexts.db.initialize() await this.contexts.db.initialize()
await this.contexts.b2.authorize()
global.cache = this.contexts.cache
global.b2 = this.contexts.b2
} }
handleWsAuth = require("@shared-lib/handleWsAuth").default handleWsAuth = require("@shared-lib/handleWsAuth").default

View File

@ -2,8 +2,9 @@
"name": "marketplace", "name": "marketplace",
"version": "0.60.2", "version": "0.60.2",
"dependencies": { "dependencies": {
"7zip-min": "^1.4.4",
"@octokit/rest": "^19.0.7", "@octokit/rest": "^19.0.7",
"7zip-min": "^1.4.4",
"backblaze-b2": "^1.7.0",
"sucrase": "^3.32.0", "sucrase": "^3.32.0",
"uglify-js": "^3.17.4" "uglify-js": "^3.17.4"
} }

View File

@ -0,0 +1,10 @@
import ExtensionClass from "@classes/extension"
export default async (req) => {
const { user_id, pkg } = req.params
return await ExtensionClass.resolveManifest({
user_id,
pkg,
})
}

View File

@ -0,0 +1,12 @@
import ExtensionClass from "@classes/extension"
export default async (req, res) => {
const { user_id, pkg } = req.params
const manifest = await ExtensionClass.resolve({
user_id,
pkg,
})
return manifest
}

View File

@ -0,0 +1,133 @@
import { Extension } from "@db_models"
import fs from "node:fs"
import path from "node:path"
import sevenzip from "7zip-min"
async function uploadFolderToB2(bucketId, folderPath, b2Directory) {
try {
const uploadFiles = async (dir) => {
const files = fs.readdirSync(dir)
for (const file of files) {
const fullPath = path.join(dir, file)
const stats = fs.statSync(fullPath)
if (stats.isDirectory()) {
await uploadFiles(fullPath)
} else {
const fileData = fs.readFileSync(fullPath)
const b2FileName = path.join(b2Directory, path.relative(folderPath, fullPath)).replace(/\\/g, '/')
console.log(`Uploading ${b2FileName}...`)
const uploadUrl = await b2.getUploadUrl({
bucketId: bucketId,
})
await b2.uploadFile({
uploadUrl: uploadUrl.data.uploadUrl,
uploadAuthToken: uploadUrl.data.authorizationToken,
fileName: b2FileName,
data: fileData,
})
console.log(`Uploaded ${b2FileName}`)
}
}
}
await uploadFiles(folderPath)
console.log('All files uploaded successfully.')
} catch (error) {
console.error('Error uploading folder:', error)
}
}
export default {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
let { pkg } = req.headers
if (!pkg) {
throw new OperationError(400, "Missing package")
}
if (!req.auth) {
throw new OperationError(401, "Unauthorized")
}
pkg = JSON.parse(pkg)
const { user_id } = req.auth.session
const registryId = `${user_id}/${pkg.name}@${pkg.version}`
const s3Path = `${process.env.B2_BUCKET_ID}/extensions/${pkg.name}/${pkg.version}`
const assetsUrl = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${s3Path}`
const workPath = path.resolve(global.cache.constructor.cachePath, String(Date.now()), registryId)
const pkgPath = path.resolve(workPath, "pkg")
const bundlePath = path.resolve(workPath, "bundle.7z")
// console.log({
// user_id,
// pkg,
// registryId,
// s3Path,
// workPath,
// bundlePath
// })
let extensionRegistry = await Extension.findOne({
user_id: user_id,
registryId: registryId,
version: pkg.version
})
if (extensionRegistry) {
throw new OperationError(400, "Extension already exists")
}
try {
if (!fs.existsSync(workPath)) {
await fs.promises.mkdir(workPath, { recursive: true })
}
// read multipart form
await req.multipart(async (field) => {
await field.write(bundlePath)
})
await new Promise((resolve, reject) => {
sevenzip.unpack(bundlePath, pkgPath, (error) => {
if (error) {
fs.promises.rm(workPath, { recursive: true, force: true })
reject(error)
} else {
resolve()
}
})
})
await uploadFolderToB2(process.env.B2_BUCKET_ID, pkgPath, s3Path)
fs.promises.rm(workPath, { recursive: true, force: true })
extensionRegistry = await Extension.create({
user_id: user_id,
name: pkg.name,
version: pkg.version,
registryId: registryId,
assetsUrl: assetsUrl,
srcUrl: `${assetsUrl}/src`,
packageUrl: `${assetsUrl}/package.json`,
created_at: Date.now(),
})
return extensionRegistry
} catch (error) {
fs.promises.rm(workPath, { recursive: true, force: true })
throw error
}
}
}

View File

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

View File

@ -18,6 +18,13 @@ export default {
throw new OperationError(403, "Unauthorized") throw new OperationError(403, "Unauthorized")
} }
console.log(`Setting lyrics for track ${track_id} >`, {
track_id: track_id,
video_source: video_source,
lrc: lrc,
track: track,
})
let trackLyric = await TrackLyric.findOne({ let trackLyric = await TrackLyric.findOne({
track_id: track_id track_id: track_id
}).lean() }).lean()
@ -35,9 +42,8 @@ export default {
trackLyric.sync_audio_at = sync_audio_at trackLyric.sync_audio_at = sync_audio_at
} }
trackLyric = await TrackLyric.findOneAndUpdate( trackLyric = await TrackLyric.findOneAndUpdate({
{ track_id: track_id
_id: trackLyric._id
}, },
trackLyric trackLyric
) )

View File

@ -0,0 +1,25 @@
import { Post } from "@db_models"
import fullfill from "@classes/posts/methods/fullfill"
export default {
middlewares: ["withOptionalAuthentication"],
fn: async (req) => {
const { limit, trim } = req.query
let result = await Post.find({
message: {
$regex: new RegExp(`#${req.params.trending}`, "gi")
}
})
.sort({ created_at: -1 })
.skip(trim ?? 0)
.limit(limit ?? 20)
result = await fullfill({
posts: result,
for_user_id: req.auth.session.user_id,
})
return result
}
}

View File

@ -2,56 +2,44 @@ import { Post } from "@db_models"
import { DateTime } from "luxon" import { DateTime } from "luxon"
const maxDaysOld = 30 const maxDaysOld = 30
const maxHashtags = 5
export default async (req) => { export default async (req) => {
// fetch all posts that contain in message an #, with a maximun of 5 diferent hashtags // fetch all posts that contain in message an #, with a maximun of 5 diferent hashtags
let posts = await Post.find({ const startDate = DateTime.local().minus({ days: maxDaysOld }).toISO()
message: {
$regex: /#/gi const trendings = await Post.aggregate([
{
$match: {
message: { $regex: /#/gi },
created_at: { $gte: startDate }
}
}, },
created_at: { {
$gte: DateTime.local().minus({ days: maxDaysOld }).toISO() $project: {
hashtags: {
$regexFindAll: {
input: "$message",
regex: /#[a-zA-Z0-9_]+/g
} }
})
.lean()
// get the hastag content
posts = posts.map((post) => {
post.hashtags = post.message.match(/#[a-zA-Z0-9_]+/gi)
post.hashtags = post.hashtags.map((hashtag) => {
return hashtag.substring(1)
})
return post
})
// build trendings
let trendings = posts.reduce((acc, post) => {
post.hashtags.forEach((hashtag) => {
if (acc.find((trending) => trending.hashtag === hashtag)) {
acc = acc.map((trending) => {
if (trending.hashtag === hashtag) {
trending.count++
} }
return trending
})
} else {
acc.push({
hashtag,
count: 1
})
} }
}) },
{ $unwind: "$hashtags" },
{
$project: {
hashtag: { $substr: ["$hashtags.match", 1, -1] }
}
},
{
$group: {
_id: "$hashtag",
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } },
{ $limit: maxHashtags }
])
return acc return trendings.map(({ _id, count }) => ({ hashtag: _id, count }));
}, [])
// sort by count
trendings = trendings.sort((a, b) => {
return b.count - a.count
})
return trendings
} }

View File

@ -0,0 +1,44 @@
import { User } from "@db_models"
const ALLOWED_FIELDS = [
"username",
"publicName",
"id",
]
export default {
middlewares: ["withOptionalAuthentication"],
fn: async (req, res) => {
const { keywords, limit = 50 } = req.query
let filters = {}
if (keywords) {
keywords.split(";").forEach((pair) => {
const [field, value] = pair.split(":")
if (value === "" || value === " ") {
return
}
// Verifica que el campo esté en los permitidos y que tenga un valor
if (ALLOWED_FIELDS.includes(field) && value) {
// Si el campo es "id", se busca coincidencia exacta
if (field === "id") {
filters[field] = value
} else {
// Para otros campos, usa $regex para coincidencias parciales
filters[field] = { $regex: `\\b${value}`, $options: "i" }
}
}
})
}
console.log(filters)
let users = await User.find(filters)
.limit(limit)
return users
}
}

View File

@ -1,12 +1,11 @@
#!/usr/bin/env node
import path from "path" import path from "path"
import fs from "fs" import fs from "fs"
import axios from "axios" import axios from "axios"
import sevenzip from "7zip-min" import sevenzip from "7zip-min"
import formdata from "form-data" import formdata from "form-data"
const tmpPath = path.join(process.cwd(), ".tmp") const marketplaceAPIOrigin = "https://indev.comty.app/api/extensions"
const widgetsApi = "http://localhost:3040"
const token = process.argv[2] const token = process.argv[2]
const excludedFiles = [ const excludedFiles = [
@ -17,9 +16,18 @@ const excludedFiles = [
"/package-lock.json", "/package-lock.json",
] ]
async function copyToTmp(origin) { const rootPath = process.cwd()
const tmpPath = path.join(rootPath, ".tmp")
const buildPath = path.join(tmpPath, "build")
const bundlePath = path.join(tmpPath, "bundle.7z")
async function copySources(origin, to) {
const files = fs.readdirSync(origin) const files = fs.readdirSync(origin)
if (!fs.existsSync(to)) {
await fs.promises.mkdir(to, { recursive: true })
}
for (const file of files) { for (const file of files) {
const filePath = path.join(origin, file) const filePath = path.join(origin, file)
@ -33,14 +41,9 @@ async function copyToTmp(origin) {
} }
if (fs.lstatSync(filePath).isDirectory()) { if (fs.lstatSync(filePath).isDirectory()) {
await copyToTmp(filePath) await copySources(filePath, path.join(to, file))
} else { } else {
const fileContent = fs.readFileSync(filePath) await fs.promises.copyFile(filePath, path.join(to, file))
const relativePath = filePath.replace(process.cwd(), "")
const tmpFilePath = path.join(tmpPath, relativePath)
fs.mkdirSync(path.dirname(tmpFilePath), { recursive: true })
fs.writeFileSync(tmpFilePath, fileContent)
} }
} }
} }
@ -63,22 +66,37 @@ async function main() {
return return
} }
const rootPath = process.cwd()
// create a .tmp folder // create a .tmp folder
if (!fs.existsSync(tmpPath)) { if (fs.existsSync(tmpPath)) {
fs.mkdirSync(tmpPath) await fs.promises.rm(tmpPath, { recursive: true, force: true })
} }
const bundlePath = path.join(rootPath, "bundle.7z") try {
// try to read package.json
if (!fs.existsSync(path.resolve(rootPath, "package.json"))) {
console.error("🛑 package.json not found")
return
}
const packageJSON = require(path.resolve(rootPath, "package.json"))
// check if package.json has a main file
if (!packageJSON.main) {
console.error("🛑 package.json does not have a main file")
return
}
if (!fs.existsSync(path.resolve(rootPath, packageJSON.main))) {
console.error("🛑 main file not found")
return
}
console.log(packageJSON)
console.log("📦 Creating bundle...") console.log("📦 Creating bundle...")
await copyToTmp(rootPath) await copySources(rootPath, buildPath)
await createBundle(`${buildPath}/*`, bundlePath)
await createBundle(`${tmpPath}/*`, bundlePath)
await fs.promises.rm(tmpPath, { recursive: true, force: true })
console.log("📦✅ Bundle created successfully") console.log("📦✅ Bundle created successfully")
@ -86,13 +104,14 @@ async function main() {
const formData = new formdata() const formData = new formdata()
formData.append("bundle", fs.createReadStream(bundlePath)) formData.append("file", fs.createReadStream(bundlePath))
const response = await axios({ const response = await axios({
method: "POST", method: "PUT",
url: `${widgetsApi}/widgets/publish`, url: `${marketplaceAPIOrigin}/publish`,
headers: { headers: {
...formData.getHeaders(), ...formData.getHeaders(),
pkg: JSON.stringify(packageJSON),
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
data: formData, data: formData,
@ -102,12 +121,15 @@ async function main() {
return false return false
}) })
await fs.promises.rm(bundlePath, { recursive: true, force: true })
await fs.promises.rm(tmpPath, { recursive: true, force: true })
if (response) { if (response) {
console.log("🚚✅ Bundle published successfully! \n", response.data) console.log("🚚✅ Bundle published successfully! \n", response.data)
} }
await fs.promises.rm(tmpPath, { recursive: true, force: true })
} catch (error) {
console.error("🛑 Error while publishing bundle \n\t", error)
await fs.promises.rm(tmpPath, { recursive: true, force: true })
}
} }
main().catch(console.error) main().catch(console.error)

View File

@ -0,0 +1,6 @@
{
"name": "clock",
"version": "1.0.6",
"description": "Display the current time",
"main": "./src/extension.js"
}

View File

@ -0,0 +1,21 @@
import React from "react"
import "./index.less"
const Clock = () => {
const [time, setTime] = React.useState(new Date())
React.useEffect(() => {
const interval = setInterval(() => {
setTime(new Date())
}, 1000)
return () => clearInterval(interval)
}, [])
return <div className="clock">
{time.toLocaleTimeString()}
</div>
}
export default Clock

View File

@ -0,0 +1,45 @@
export default class Clock {
registerWidgets = [
{
name: "Clock",
description: "Display the current time",
component: () => import("./clock.jsx"),
}
]
registerPages = [
{
path: "/clock",
component: () => import("./clock.jsx"),
}
]
public = {
echo: (...str) => {
this.console.log(...str)
},
fib: (n) => {
let a = 0, b = 1
for (let i = 0; i < n; i++) {
let c = a + b
a = b
b = c
}
return a
}
}
events = {
"test": (data) => {
this.console.log("test")
if (data) {
this.console.log(data)
}
}
}
async onInitialize() {
this.console.log("Hi from the extension worker!")
}
}

View File

@ -0,0 +1,6 @@
.clock {
font-size: 2rem;
color: blue;
font-family: "DM Mono", monospace;
}