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
17892508e7
commit
00f6e34c53
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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,40 +30,36 @@ 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
|
||||||
|
if (videoRef.current === null || !lyrics || !lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined" || playerState.playback_status !== "playing") {
|
||||||
|
return stopSyncInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTrackTime = app.cores.player.controls.seek()
|
||||||
|
const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000)
|
||||||
|
|
||||||
|
//console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`)
|
||||||
|
|
||||||
|
const maxOffset = maxLatencyInMs / 1000
|
||||||
|
const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime)
|
||||||
|
|
||||||
|
setCurrentVideoLatency(currentVideoTimeDiff)
|
||||||
|
|
||||||
|
if (syncingVideo === true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// if `sync_audio_at_ms` is present, it means the video must be synced with audio
|
if (currentVideoTimeDiff > maxOffset) {
|
||||||
if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") {
|
seekVideoToSyncAudio()
|
||||||
if (!videoRef.current) {
|
|
||||||
clearInterval(syncInterval)
|
|
||||||
setSyncInterval(null)
|
|
||||||
setCurrentVideoLatency(0)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTrackTime = app.cores.player.controls.seek()
|
|
||||||
const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000)
|
|
||||||
|
|
||||||
//console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`)
|
|
||||||
|
|
||||||
const maxOffset = maxLatencyInMs / 1000
|
|
||||||
const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime)
|
|
||||||
|
|
||||||
setCurrentVideoLatency(currentVideoTimeDiff)
|
|
||||||
|
|
||||||
if (syncingVideo === true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentVideoTimeDiff > maxOffset) {
|
|
||||||
seekVideoToSyncAudio()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,45 +67,13 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
|||||||
setSyncInterval(setInterval(syncPlayback, 300))
|
setSyncInterval(setInterval(syncPlayback, 300))
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
function stopSyncInterval() {
|
||||||
videoRef.current.addEventListener("seeked", (event) => {
|
setSyncingVideo(false)
|
||||||
setSyncingVideo(false)
|
setSyncInterval(null)
|
||||||
})
|
clearInterval(syncInterval)
|
||||||
|
}
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [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 playback status change
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (initialLoading === false) {
|
||||||
|
console.log(`VIDEO:: Playback status changed to ${playerState.playback_status}`)
|
||||||
|
|
||||||
|
if (lyrics && lyrics.video_source) {
|
||||||
|
if (playerState.playback_status === "playing") {
|
||||||
|
videoRef.current.play()
|
||||||
|
startSyncInterval()
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause()
|
||||||
|
stopSyncInterval()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [playerState.playback_status])
|
||||||
|
|
||||||
//* Handle when lyrics object change
|
//* Handle when lyrics object change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
clearInterval(syncInterval)
|
|
||||||
setCurrentVideoLatency(0)
|
setCurrentVideoLatency(0)
|
||||||
setSyncingVideo(false)
|
stopSyncInterval()
|
||||||
|
|
||||||
if (lyrics) {
|
if (lyrics) {
|
||||||
if (lyrics.video_source) {
|
if (lyrics.video_source) {
|
||||||
|
console.log("Loading video source >", lyrics.video_source)
|
||||||
hls.current.loadSource(lyrics.video_source)
|
hls.current.loadSource(lyrics.video_source)
|
||||||
|
|
||||||
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
|
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
|
||||||
|
videoRef.current.loop = false
|
||||||
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
|
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
|
||||||
|
|
||||||
if (playerState.playback_status === "playing") {
|
startSyncInterval()
|
||||||
videoRef.current.play()
|
|
||||||
startSyncInterval()
|
|
||||||
} else {
|
|
||||||
videoRef.current.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = app.cores.player.controls.seek()
|
|
||||||
|
|
||||||
if (currentTime > 0) {
|
|
||||||
seekVideoToSyncAudio()
|
|
||||||
}
|
|
||||||
} 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()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
27
packages/app/src/pages/trending/[trending].jsx
Normal file
27
packages/app/src/pages/trending/[trending].jsx
Normal 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
|
18
packages/app/src/pages/trending/index.less
Normal file
18
packages/app/src/pages/trending/index.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
@ -7,55 +7,61 @@ 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() ?? [],
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
React.useEffect(() => {
|
||||||
return <div className="widgets-manager">
|
if (app.layout.tools_bar) {
|
||||||
<div className="widgets-manager-list">
|
app.layout.tools_bar.toggleVisibility(true)
|
||||||
{
|
}
|
||||||
Array.isArray(this.state.loadedWidgets) && this.state.loadedWidgets.map((manifest) => {
|
}, [])
|
||||||
return <React.Fragment>
|
|
||||||
<WidgetItemPreview
|
|
||||||
manifest={manifest}
|
|
||||||
onRemove={() => {
|
|
||||||
app.cores.widgets.uninstall(manifest._id)
|
|
||||||
}}
|
|
||||||
onInstall={() => {
|
|
||||||
app.cores.widgets.install(manifest._id)
|
|
||||||
}}
|
|
||||||
onUpdate={() => {
|
|
||||||
app.cores.widgets.install(manifest._id, {
|
|
||||||
update: true,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onChangeVisible={(visible) => {
|
|
||||||
app.cores.widgets.toggleVisibility(manifest._id, visible)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<div
|
return <div className="widgets-manager">
|
||||||
className="widget_load_list_item"
|
<h1>Widgets</h1>
|
||||||
style={{
|
|
||||||
display: "flex",
|
<div className="widgets-manager-list">
|
||||||
justifyContent: "center",
|
{
|
||||||
alignItems: "center",
|
Array.isArray(loadedWidgets) && loadedWidgets.map((manifest) => {
|
||||||
}}
|
return <React.Fragment>
|
||||||
|
<WidgetItemPreview
|
||||||
|
manifest={manifest}
|
||||||
|
onRemove={() => {
|
||||||
|
app.cores.widgets.uninstall(manifest._id)
|
||||||
|
}}
|
||||||
|
onInstall={() => {
|
||||||
|
app.cores.widgets.install(manifest._id)
|
||||||
|
}}
|
||||||
|
onUpdate={() => {
|
||||||
|
app.cores.widgets.install(manifest._id, {
|
||||||
|
update: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onChangeVisible={(visible) => {
|
||||||
|
app.cores.widgets.toggleVisibility(manifest._id, visible)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="widget_load_list_item"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<antd.Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Icons.FiPlus />}
|
||||||
|
onClick={() => { }}
|
||||||
>
|
>
|
||||||
<antd.Button
|
Install more
|
||||||
type="primary"
|
</antd.Button>
|
||||||
icon={<Icons.FiPlus />}
|
|
||||||
onClick={openWidgetsBrowserModal}
|
|
||||||
>
|
|
||||||
Install more
|
|
||||||
</antd.Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default WidgetsManager
|
@ -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>
|
|
||||||
},
|
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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) {
|
||||||
|
@ -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({
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export default class Extension {
|
||||||
|
static resolve = require("./methods/resolve").default
|
||||||
|
}
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
import { Extension } from "@db_models"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,10 +42,9 @@ 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
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
$gte: DateTime.local().minus({ days: maxDaysOld }).toISO()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.lean()
|
|
||||||
|
|
||||||
// get the hastag content
|
const trendings = await Post.aggregate([
|
||||||
posts = posts.map((post) => {
|
{
|
||||||
post.hashtags = post.message.match(/#[a-zA-Z0-9_]+/gi)
|
$match: {
|
||||||
|
message: { $regex: /#/gi },
|
||||||
post.hashtags = post.hashtags.map((hashtag) => {
|
created_at: { $gte: startDate }
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
hashtags: {
|
||||||
|
$regexFindAll: {
|
||||||
|
input: "$message",
|
||||||
|
regex: /#[a-zA-Z0-9_]+/g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ $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
|
|
||||||
}
|
}
|
44
packages/server/services/users/routes/users/search/get.js
Normal file
44
packages/server/services/users/routes/users/search/get.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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,50 +66,69 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
console.log("📦 Creating bundle...")
|
const packageJSON = require(path.resolve(rootPath, "package.json"))
|
||||||
|
|
||||||
await copyToTmp(rootPath)
|
// check if package.json has a main file
|
||||||
|
if (!packageJSON.main) {
|
||||||
|
console.error("🛑 package.json does not have a main file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await createBundle(`${tmpPath}/*`, bundlePath)
|
if (!fs.existsSync(path.resolve(rootPath, packageJSON.main))) {
|
||||||
|
console.error("🛑 main file not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await fs.promises.rm(tmpPath, { recursive: true, force: true })
|
console.log(packageJSON)
|
||||||
|
|
||||||
console.log("📦✅ Bundle created successfully")
|
console.log("📦 Creating bundle...")
|
||||||
|
|
||||||
console.log("🚚 Publishing bundle...")
|
await copySources(rootPath, buildPath)
|
||||||
|
await createBundle(`${buildPath}/*`, bundlePath)
|
||||||
|
|
||||||
const formData = new formdata()
|
console.log("📦✅ Bundle created successfully")
|
||||||
|
|
||||||
formData.append("bundle", fs.createReadStream(bundlePath))
|
console.log("🚚 Publishing bundle...")
|
||||||
|
|
||||||
const response = await axios({
|
const formData = new formdata()
|
||||||
method: "POST",
|
|
||||||
url: `${widgetsApi}/widgets/publish`,
|
|
||||||
headers: {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
data: formData,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("🛑 Error while publishing bundle \n\t", error.response?.data ?? error)
|
|
||||||
|
|
||||||
return false
|
formData.append("file", fs.createReadStream(bundlePath))
|
||||||
})
|
|
||||||
|
|
||||||
await fs.promises.rm(bundlePath, { recursive: true, force: true })
|
const response = await axios({
|
||||||
await fs.promises.rm(tmpPath, { recursive: true, force: true })
|
method: "PUT",
|
||||||
|
url: `${marketplaceAPIOrigin}/publish`,
|
||||||
|
headers: {
|
||||||
|
...formData.getHeaders(),
|
||||||
|
pkg: JSON.stringify(packageJSON),
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
data: formData,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("🛑 Error while publishing bundle \n\t", error.response?.data ?? error)
|
||||||
|
|
||||||
if (response) {
|
return false
|
||||||
console.log("🚚✅ Bundle published successfully! \n", response.data)
|
})
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
widgets/Clock/package.json
Normal file
6
widgets/Clock/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "clock",
|
||||||
|
"version": "1.0.6",
|
||||||
|
"description": "Display the current time",
|
||||||
|
"main": "./src/extension.js"
|
||||||
|
}
|
21
widgets/Clock/src/clock.jsx
Normal file
21
widgets/Clock/src/clock.jsx
Normal 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
|
45
widgets/Clock/src/extension.js
Normal file
45
widgets/Clock/src/extension.js
Normal 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!")
|
||||||
|
}
|
||||||
|
}
|
6
widgets/Clock/src/index.less
Normal file
6
widgets/Clock/src/index.less
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.clock {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: blue;
|
||||||
|
|
||||||
|
font-family: "DM Mono", monospace;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user