Merge commit 'fd7425a2cf312b787b52843a142e22eb965314d2' into fix-android-backAction
13
.github/workflows/changelog.yml
vendored
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
name: Create Changelogs
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
jobs:
|
|
||||||
create_changelog:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Create Changelogs
|
|
||||||
uses: heineiuo/create-changelogs@v0.2.8
|
|
18
.github/workflows/trello_issue.yml
vendored
@ -1,18 +0,0 @@
|
|||||||
name: Trello Issue List
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
env:
|
|
||||||
TRELLO_KEY: ${{ secrets.TRELLO_KEY }}
|
|
||||||
TRELLO_TOKEN: ${{ secrets.TRELLO_TOKEN }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
issue_send:
|
|
||||||
name: Send Issue to Trello
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Runs trello manage
|
|
||||||
uses: sisodiya2421/trello-manage@master
|
|
||||||
with:
|
|
||||||
repo-name: Comty Development
|
|
||||||
trello-username: ${{ secrets.TRELLO_USERNAME }}
|
|
17
.github/workflows/validate.yml
vendored
@ -1,17 +0,0 @@
|
|||||||
name: Validate code
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm test
|
|
@ -22,5 +22,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^7.5.0"
|
"concurrently": "^7.5.0"
|
||||||
},
|
},
|
||||||
"version": "0.31.1"
|
"version": "0.33.0"
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,11 @@
|
|||||||
"language": "en",
|
"language": "en",
|
||||||
"sidebarKeys": [
|
"sidebarKeys": [
|
||||||
"home",
|
"home",
|
||||||
|
"tv",
|
||||||
|
"music",
|
||||||
|
"events",
|
||||||
|
"groups",
|
||||||
"marketplace",
|
"marketplace",
|
||||||
"groups"
|
"dev"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -14,10 +14,31 @@
|
|||||||
"reachable": true
|
"reachable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "saved",
|
"id": "events",
|
||||||
"path": "/saved",
|
"path": "/events",
|
||||||
"title": "Saved",
|
"title": "Events",
|
||||||
"icon": "Archive",
|
"icon": "MdLocalActivity",
|
||||||
|
"reachable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tv",
|
||||||
|
"path": "/tv",
|
||||||
|
"title": "Tv",
|
||||||
|
"icon": "Tv",
|
||||||
|
"reachable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music",
|
||||||
|
"path": "/music",
|
||||||
|
"title": "Music",
|
||||||
|
"icon": "MdMusicVideo",
|
||||||
|
"reachable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "groups",
|
||||||
|
"path": "/groups",
|
||||||
|
"title": "Groups",
|
||||||
|
"icon": "Users",
|
||||||
"reachable": true
|
"reachable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,23 +49,10 @@
|
|||||||
"reachable": true
|
"reachable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "streams",
|
"id": "dev",
|
||||||
"path": "/streams",
|
"path": "/dev",
|
||||||
"title": "Streams",
|
"title": "Development",
|
||||||
"icon": "Tv",
|
"icon": "MdOutlineCode",
|
||||||
"reachable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "streaming_control",
|
|
||||||
"path": "/streaming_control",
|
|
||||||
"title": "Streaming Control",
|
|
||||||
"icon": "Video"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "groups",
|
|
||||||
"path": "/groups",
|
|
||||||
"title": "Groups",
|
|
||||||
"icon": "Users",
|
|
||||||
"reachable": true
|
"reachable": true
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "comty",
|
"name": "comty",
|
||||||
"version": "0.31.1",
|
"version": "0.33.0",
|
||||||
"license": "LGPL-2.1",
|
"license": "LGPL-2.1",
|
||||||
"main": "electron/main",
|
"main": "electron/main",
|
||||||
"author": "RageStudio",
|
"author": "RageStudio",
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
@ -85,7 +85,10 @@ const steps = [
|
|||||||
setValidCharacters(hasValidCharacters(username))
|
setValidCharacters(hasValidCharacters(username))
|
||||||
|
|
||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
if (!validCharacters) return
|
if (!validCharacters) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const request = await app.api.customRequest("main", {
|
const request = await app.api.customRequest("main", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -120,7 +123,7 @@ const steps = [
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
onPressEnter={submit}
|
onPressEnter={submit}
|
||||||
placeholder="@newuser"
|
placeholder="newuser"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={handleUpdate}
|
onChange={handleUpdate}
|
||||||
status={username.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
|
status={username.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
|
||||||
|
@ -27,6 +27,12 @@ export default class Livestream {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getCategories() {
|
||||||
|
const request = await Livestream.bridge.get.streamingCategories()
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
static async getStreamInfo(payload) {
|
static async getStreamInfo(payload) {
|
||||||
let { username } = payload ?? {}
|
let { username } = payload ?? {}
|
||||||
|
|
||||||
|
@ -6,36 +6,10 @@ import { Icons, createIconRender } from "components/Icons"
|
|||||||
|
|
||||||
import { HashtagTrendings, FeaturedEventsAnnouncements, ConnectedFriends } from "components"
|
import { HashtagTrendings, FeaturedEventsAnnouncements, ConnectedFriends } from "components"
|
||||||
|
|
||||||
import FeedBrowser from "./components/feed"
|
import Tabs from "./tabs"
|
||||||
import ExploreBrowser from "./components/explore"
|
|
||||||
import LivestreamsBrowser from "./components/livestreams"
|
|
||||||
import SavedPostsBrowser from "./components/savedPosts"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const Tabs = {
|
|
||||||
"feed": {
|
|
||||||
title: "Feed",
|
|
||||||
icon: "Rss",
|
|
||||||
component: FeedBrowser
|
|
||||||
},
|
|
||||||
"explore": {
|
|
||||||
title: "Explore",
|
|
||||||
icon: "Search",
|
|
||||||
component: ExploreBrowser
|
|
||||||
},
|
|
||||||
"savedPosts": {
|
|
||||||
title: "Saved posts",
|
|
||||||
icon: "Bookmark",
|
|
||||||
component: SavedPostsBrowser
|
|
||||||
},
|
|
||||||
"livestreams": {
|
|
||||||
title: "Livestreams",
|
|
||||||
icon: "Tv",
|
|
||||||
component: LivestreamsBrowser
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Dashboard extends React.Component {
|
export default class Dashboard extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
activeTab: this.props.match.params.type ?? "feed"
|
activeTab: this.props.match.params.type ?? "feed"
|
||||||
@ -80,7 +54,7 @@ export default class Dashboard extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="dashboard">
|
return <div className="postingDashboard">
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -2,40 +2,10 @@ import React from "react"
|
|||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
|
|
||||||
import { Icons, createIconRender } from "components/Icons"
|
import Tabs from "./tabs"
|
||||||
|
|
||||||
import { HashtagTrendings, FeaturedEventsAnnouncements, ConnectedFriends } from "components"
|
|
||||||
|
|
||||||
import FeedBrowser from "./components/feed"
|
|
||||||
import ExploreBrowser from "./components/explore"
|
|
||||||
import LivestreamsBrowser from "./components/livestreams"
|
|
||||||
import SavedPostsBrowser from "./components/savedPosts"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const Tabs = {
|
|
||||||
"feed": {
|
|
||||||
title: "Feed",
|
|
||||||
icon: "Rss",
|
|
||||||
component: FeedBrowser
|
|
||||||
},
|
|
||||||
"explore": {
|
|
||||||
title: "Explore",
|
|
||||||
icon: "Search",
|
|
||||||
component: ExploreBrowser
|
|
||||||
},
|
|
||||||
"savedPosts": {
|
|
||||||
title: "Saved posts",
|
|
||||||
icon: "Bookmark",
|
|
||||||
component: SavedPostsBrowser
|
|
||||||
},
|
|
||||||
"livestreams": {
|
|
||||||
title: "Livestreams",
|
|
||||||
icon: "Tv",
|
|
||||||
component: LivestreamsBrowser
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Dashboard extends React.Component {
|
export default class Dashboard extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
activeTab: this.props.match.params.type ?? "feed"
|
activeTab: this.props.match.params.type ?? "feed"
|
||||||
@ -80,7 +50,7 @@ export default class Dashboard extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="dashboard">
|
return <div className="postingDashboard">
|
||||||
<div
|
<div
|
||||||
ref={this.primaryPanelRef}
|
ref={this.primaryPanelRef}
|
||||||
className={classnames("panel", "fade-opacity-active")}
|
className={classnames("panel", "fade-opacity-active")}
|
||||||
|
14
packages/app/src/pages/home/components/trendings/index.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Result } from "antd"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
return <div className="trendingsBrowser">
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="Not implemented"
|
||||||
|
subTitle="Sorry, but this feature is not implemented yet."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
.trendingsBrowser {}
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
app.setLocation("home/feed")
|
app.setLocation("/home/feed")
|
||||||
|
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
.dashboard {
|
.postingDashboard {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
grid-template-columns: 10vw 1fr 0.5fr;
|
grid-template-columns: 10vw 1fr 0.5fr;
|
||||||
|
27
packages/app/src/pages/home/tabs.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import FeedTab from "./components/feed"
|
||||||
|
import ExploreTab from "./components/explore"
|
||||||
|
import TrendingsTab from "./components/trendings"
|
||||||
|
import SavedPostsTab from "./components/savedPosts"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
"feed": {
|
||||||
|
title: "Feed",
|
||||||
|
icon: "Rss",
|
||||||
|
component: FeedTab
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
title: "Explore",
|
||||||
|
icon: "Search",
|
||||||
|
component: ExploreTab
|
||||||
|
},
|
||||||
|
"trendings": {
|
||||||
|
title: "Trendings",
|
||||||
|
icon: "TrendingUp",
|
||||||
|
component: TrendingsTab
|
||||||
|
},
|
||||||
|
"savedPosts": {
|
||||||
|
title: "Saved posts",
|
||||||
|
icon: "Bookmark",
|
||||||
|
component: SavedPostsTab
|
||||||
|
}
|
||||||
|
}
|
87
packages/app/src/pages/tv/[type].jsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
|
||||||
|
import Tabs from "./tabs"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class TVDashboard extends React.Component {
|
||||||
|
state = {
|
||||||
|
activeTab: this.props.match.params.type ?? "feed"
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryPanelRef = React.createRef()
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
app.eventBus.emit("style.compactMode", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActiveTab() {
|
||||||
|
const tab = Tabs[this.state.activeTab]
|
||||||
|
|
||||||
|
if (!tab) {
|
||||||
|
return <antd.Result
|
||||||
|
status="404"
|
||||||
|
title="404"
|
||||||
|
subTitle="Sorry, the tab you visited does not exist."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createElement(tab.component)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabChange = (key) => {
|
||||||
|
if (this.state.activeTab === key) return
|
||||||
|
|
||||||
|
// set to primary panel fade-opacity-leave class
|
||||||
|
this.primaryPanelRef.current.classList.add("fade-opacity-leave")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ activeTab: key })
|
||||||
|
// update location
|
||||||
|
app.history.replace(key)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
// remove fade-opacity-leave class after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.primaryPanelRef.current.classList.remove("fade-opacity-leave")
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="tvDashboard">
|
||||||
|
<div
|
||||||
|
ref={this.primaryPanelRef}
|
||||||
|
className={classnames("panel", "fade-opacity-active")}
|
||||||
|
>
|
||||||
|
{this.renderActiveTab()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="panel">
|
||||||
|
<div className="card" id="browserType">
|
||||||
|
<h2><Icons.Tv /> TV</h2>
|
||||||
|
<antd.Menu
|
||||||
|
mode="inline"
|
||||||
|
selectedKeys={[this.state.activeTab]}
|
||||||
|
activeKey={this.state.activeTab}
|
||||||
|
onClick={({ key }) => this.handleTabChange(key)}
|
||||||
|
>
|
||||||
|
{Object.keys(Tabs).map((key) => {
|
||||||
|
const tab = Tabs[key]
|
||||||
|
|
||||||
|
return <antd.Menu.Item
|
||||||
|
key={key}
|
||||||
|
icon={createIconRender(tab.icon)}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</antd.Menu.Item>
|
||||||
|
})}
|
||||||
|
</antd.Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import * as antd from "antd"
|
|||||||
|
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
import Livestream from "../../models/livestream"
|
import Livestream from "../../../../models/livestream"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
@ -32,6 +32,131 @@ const StreamingKeyView = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LivestreamsCategoriesSelector = (props) => {
|
||||||
|
const [categories, setCategories] = React.useState([])
|
||||||
|
const [loading, setLoading] = React.useState(true)
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const categories = await Livestream.getCategories().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
app.message.error("Failed to load categories")
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Loaded categories >`, categories)
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (categories) {
|
||||||
|
setCategories(categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <antd.Select
|
||||||
|
placeholder="Select a category"
|
||||||
|
defaultValue={props.defaultValue}
|
||||||
|
onChange={(value) => props.updateStreamInfo("category", value)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
categories.map((category) => {
|
||||||
|
return <antd.Select.Option value={category?.key ?? "unknown"}>{category?.label ?? "No category"}</antd.Select.Option>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</antd.Select>
|
||||||
|
}
|
||||||
|
|
||||||
|
const StreamInfoEditor = (props) => {
|
||||||
|
const [streamInfo, setStreamInfo] = React.useState(props.defaultStreamInfo ?? {})
|
||||||
|
|
||||||
|
const updateStreamInfo = (key, value) => {
|
||||||
|
setStreamInfo({
|
||||||
|
...streamInfo,
|
||||||
|
[key]: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveStreamInfo = async () => {
|
||||||
|
if (typeof props.onSave === "function") {
|
||||||
|
return await props.onSave(streamInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// peform default save
|
||||||
|
const result = await Livestream.updateLivestreamInfo(streamInfo).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
app.message.error("Failed to update stream info")
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
app.message.success("Stream info updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.onSaveComplete === "function") {
|
||||||
|
await props.onSaveComplete(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="streamInfoEditor">
|
||||||
|
<div className="field">
|
||||||
|
<span>
|
||||||
|
<Icons.MdTitle />Title
|
||||||
|
</span>
|
||||||
|
<div className="value">
|
||||||
|
<antd.Input
|
||||||
|
placeholder="Stream title"
|
||||||
|
value={streamInfo.title}
|
||||||
|
onChange={(e) => updateStreamInfo("title", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<span>
|
||||||
|
<Icons.MdTextFields /> Description
|
||||||
|
</span>
|
||||||
|
<div className="value">
|
||||||
|
<antd.Input
|
||||||
|
placeholder="Stream description"
|
||||||
|
value={streamInfo.description}
|
||||||
|
onChange={(e) => updateStreamInfo("description", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<span>
|
||||||
|
<Icons.MdCategory /> Category
|
||||||
|
</span>
|
||||||
|
<div className="value">
|
||||||
|
<LivestreamsCategoriesSelector
|
||||||
|
defaultValue={streamInfo.category.key}
|
||||||
|
updateStreamInfo={updateStreamInfo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<antd.Button
|
||||||
|
type="primary"
|
||||||
|
onClick={saveStreamInfo}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</antd.Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [streamInfo, setStreamInfo] = React.useState({})
|
const [streamInfo, setStreamInfo] = React.useState({})
|
||||||
const [addresses, setAddresses] = React.useState({})
|
const [addresses, setAddresses] = React.useState({})
|
||||||
@ -39,6 +164,19 @@ export default (props) => {
|
|||||||
const [isConnected, setIsConnected] = React.useState(false)
|
const [isConnected, setIsConnected] = React.useState(false)
|
||||||
const [streamingKey, setStreamingKey] = React.useState(null)
|
const [streamingKey, setStreamingKey] = React.useState(null)
|
||||||
|
|
||||||
|
const onClickEditInfo = () => {
|
||||||
|
app.ModalController.open(() => <StreamInfoEditor
|
||||||
|
defaultStreamInfo={streamInfo}
|
||||||
|
onSaveComplete={(result) => {
|
||||||
|
if (result) {
|
||||||
|
app.ModalController.close()
|
||||||
|
|
||||||
|
fetchStreamInfo()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
const regenerateStreamingKey = async () => {
|
const regenerateStreamingKey = async () => {
|
||||||
antd.Modal.confirm({
|
antd.Modal.confirm({
|
||||||
title: "Regenerate streaming key",
|
title: "Regenerate streaming key",
|
||||||
@ -125,15 +263,35 @@ export default (props) => {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="description">
|
||||||
|
<span>
|
||||||
|
Description
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{streamInfo?.description ?? "No description"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="category">
|
<div className="category">
|
||||||
<span>
|
<span>
|
||||||
Category
|
Category
|
||||||
</span>
|
</span>
|
||||||
<h4>
|
<h4>
|
||||||
{streamInfo?.category ?? "No category"}
|
{streamInfo?.category?.label ?? "No category"}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<antd.Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Icons.Edit2 />}
|
||||||
|
onClick={onClickEditInfo}
|
||||||
|
>
|
||||||
|
Edit info
|
||||||
|
</antd.Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="config">
|
<div className="config">
|
@ -2,13 +2,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
height: 20vh;
|
height: fit-content;
|
||||||
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
@ -21,7 +23,7 @@
|
|||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 400px;
|
width: 300px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -38,6 +40,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@ -47,6 +50,7 @@
|
|||||||
|
|
||||||
.config {
|
.config {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
|
|
||||||
@ -83,7 +87,7 @@
|
|||||||
.title {
|
.title {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@ -114,8 +118,29 @@
|
|||||||
div {
|
div {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamInfoEditor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,11 +1,10 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import Livestream from "models/livestream"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
|
||||||
import { UserPreview } from "components"
|
import { UserPreview } from "components"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
import Livestream from "../../../../models/livestream"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const LivestreamItem = (props) => {
|
const LivestreamItem = (props) => {
|
||||||
@ -34,7 +33,7 @@ const LivestreamItem = (props) => {
|
|||||||
<h2>{livestream.info?.description ?? "No description"}</h2>
|
<h2>{livestream.info?.description ?? "No description"}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="livestream_category">
|
<div className="livestream_category">
|
||||||
{livestream.info?.catagory ?? "No category"}
|
{livestream.info?.category?.label ?? "No category"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -71,10 +70,6 @@ export default (props) => {
|
|||||||
app.setLocation(`/live/${livestream.username}`)
|
app.setLocation(`/live/${livestream.username}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickControlPanel = () => {
|
|
||||||
app.setLocation("/live_control")
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderList = () => {
|
const renderList = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
@ -105,19 +100,10 @@ export default (props) => {
|
|||||||
<span>Livestreams</span>
|
<span>Livestreams</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel">
|
|
||||||
<antd.Button
|
|
||||||
icon={<Icons.Settings />}
|
|
||||||
onClick={onClickControlPanel}
|
|
||||||
>
|
|
||||||
Control Panel
|
|
||||||
</antd.Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="livestream_list">
|
<div className="livestream_list">
|
||||||
{renderList()}
|
{renderList()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
14
packages/app/src/pages/tv/components/feed/index.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Result } from "antd"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
return <div className="livestreamsFeed">
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="Not implemented"
|
||||||
|
subTitle="Sorry, but this feature is not implemented yet."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
1
packages/app/src/pages/tv/components/feed/index.less
Normal file
@ -0,0 +1 @@
|
|||||||
|
.livestreamsFeed {}
|
7
packages/app/src/pages/tv/index.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
app.setLocation("/tv/feed")
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
}
|
47
packages/app/src/pages/tv/index.less
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.tvDashboard {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: 3fr 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-column-gap: 10px;
|
||||||
|
grid-row-gap: 0px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
padding-left: 30px;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
min-width: 20vw;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu {
|
||||||
|
svg {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
packages/app/src/pages/tv/tabs.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import FeedTab from "./components/feed"
|
||||||
|
import ExploreTab from "./components/explore"
|
||||||
|
import ControlPanelTab from "./components/controlPanel"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
"feed": {
|
||||||
|
title: "Feed",
|
||||||
|
icon: "Rss",
|
||||||
|
component: FeedTab
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
title: "Explore",
|
||||||
|
icon: "Search",
|
||||||
|
component: ExploreTab
|
||||||
|
},
|
||||||
|
"controlPanel": {
|
||||||
|
title: "Control Panel",
|
||||||
|
icon: "Settings",
|
||||||
|
component: ControlPanelTab
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@comty/server",
|
"name": "@comty/server",
|
||||||
"version": "0.31.1",
|
"version": "0.33.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "corenode-cli build",
|
"build": "corenode-cli build",
|
||||||
|
@ -3,19 +3,17 @@ import { nanoid } from "nanoid"
|
|||||||
import lodash from "lodash"
|
import lodash from "lodash"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
|
|
||||||
import { User, StreamingKey, StreamingInfo } from "../../models"
|
import { Schematized } from "../../lib"
|
||||||
|
import { User, StreamingKey, StreamingInfo, StreamingCategory } from "../../models"
|
||||||
|
|
||||||
const streamingIngestServer = process.env.STREAMING_INGEST_SERVER
|
const streamingIngestServer = process.env.STREAMING_INGEST_SERVER ?? ""
|
||||||
const streamingServerAPIAddress = process.env.STREAMING_API_SERVER
|
const streamingServerAPIAddress = process.env.STREAMING_API_SERVER ?? ""
|
||||||
const streamingServerAPIProtocol = streamingServerAPIAddress.startsWith("https") ? "https" : "http"
|
|
||||||
|
|
||||||
const streamingServerAPIUri = `${streamingServerAPIProtocol}://${streamingServerAPIAddress.split("://")[1]}`
|
const streamingServerAPIUri = `${streamingServerAPIAddress.startsWith("https") ? "https" : "http"}://${streamingServerAPIAddress.split("://")[1]}`
|
||||||
|
|
||||||
const FILTER_KEYS = ["stream"]
|
const FILTER_KEYS = ["stream"]
|
||||||
|
|
||||||
export default class StreamingController extends Controller {
|
export default class StreamingController extends Controller {
|
||||||
streamings = []
|
|
||||||
|
|
||||||
methods = {
|
methods = {
|
||||||
genereteKey: async (user_id) => {
|
genereteKey: async (user_id) => {
|
||||||
// this will generate a new key for the user
|
// this will generate a new key for the user
|
||||||
@ -34,78 +32,61 @@ export default class StreamingController extends Controller {
|
|||||||
|
|
||||||
return streamingKey
|
return streamingKey
|
||||||
},
|
},
|
||||||
regenerateStreamingList: async () => {
|
fetchStreams: async () => {
|
||||||
// fetch all streams from api
|
// fetch all streams from api
|
||||||
let streams = await axios.get(`${streamingServerAPIUri}/api/v1/streams`).catch((err) => {
|
let { data } = await axios.get(`${streamingServerAPIUri}/api/v1/streams`).catch((err) => {
|
||||||
console.log(err)
|
console.error(err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
if (streams) {
|
let streamings = []
|
||||||
streams = streams.data.streams
|
|
||||||
|
|
||||||
// FIXME: this method is not totally async
|
if (!data) return streamings
|
||||||
streams.forEach((stream) => {
|
|
||||||
// check if the stream is already in the list
|
|
||||||
const streamInList = this.streamings.find((s) => s.stream === stream.name)
|
|
||||||
|
|
||||||
if (!streamInList) {
|
streamings = data.streams
|
||||||
// if not, add it
|
|
||||||
this.methods.pushToLocalList({
|
streamings = streamings.map(async (stream) => {
|
||||||
stream: stream.name,
|
stream = await this.methods.generateStreamFromStreamkey(stream.name)
|
||||||
app: stream.app,
|
|
||||||
}).catch((err) => {
|
let info = await StreamingInfo.findOne({
|
||||||
// sorry for you
|
user_id: stream.user_id
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
},
|
|
||||||
pushToLocalList: async (payload) => {
|
|
||||||
const { stream, app } = payload
|
|
||||||
|
|
||||||
const username = app.split("/")[1]
|
if (info) {
|
||||||
const user_id = await User.findOne({ username }).then((user) => user._id)
|
stream.info = info.toObject()
|
||||||
|
|
||||||
const streamingKey = await StreamingKey.findOne({
|
stream.info.category = await StreamingCategory.findOne({
|
||||||
key: stream
|
key: stream.info.category
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!streamingKey) {
|
streamings = await Promise.all(streamings)
|
||||||
throw new Error("Invalid streaming key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username !== streamingKey.username) {
|
return streamings.map((stream) => {
|
||||||
throw new Error("Invalid streaming key for this username")
|
return lodash.omit(stream, FILTER_KEYS)
|
||||||
}
|
})
|
||||||
|
},
|
||||||
|
generateStreamFromStreamkey: async (streamKey) => {
|
||||||
|
// generate a stream from a streamkey
|
||||||
|
const streamingKey = await StreamingKey.findOne({
|
||||||
|
key: streamKey
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!streamingKey) return false
|
||||||
|
|
||||||
const streaming = {
|
const streaming = {
|
||||||
stream,
|
user_id: streamingKey.user_id,
|
||||||
user_id: user_id.toString(),
|
|
||||||
username: streamingKey.username,
|
username: streamingKey.username,
|
||||||
sources: {
|
sources: {
|
||||||
rtmp: `${streamingIngestServer}/live/${username}`,
|
rtmp: `${streamingIngestServer}/live/${streamingKey.username}`,
|
||||||
hls: `${streamingServerAPIAddress}/live/${username}/src.m3u8`,
|
hls: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.m3u8`,
|
||||||
flv: `${streamingServerAPIAddress}/live/${username}/src.flv`,
|
flv: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.flv`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.streamings.push(streaming)
|
|
||||||
|
|
||||||
return streaming
|
|
||||||
},
|
|
||||||
removeFromLocalList: async (payload) => {
|
|
||||||
const { stream } = payload
|
|
||||||
|
|
||||||
// remove from streamings array
|
|
||||||
const streaming = this.streamings.find((streaming) => streaming.stream === stream)
|
|
||||||
|
|
||||||
if (!streaming) {
|
|
||||||
throw new Error("Stream not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
this.streamings = this.streamings.filter((streaming) => streaming.stream !== stream)
|
|
||||||
|
|
||||||
return streaming
|
return streaming
|
||||||
},
|
},
|
||||||
handleInfoUpdate: async (payload) => {
|
handleInfoUpdate: async (payload) => {
|
||||||
@ -147,28 +128,15 @@ export default class StreamingController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get = {
|
get = {
|
||||||
|
"/streaming/categories": async (req, res) => {
|
||||||
|
const categories = await StreamingCategory.find()
|
||||||
|
|
||||||
|
return res.json(categories)
|
||||||
|
},
|
||||||
"/streams": async (req, res) => {
|
"/streams": async (req, res) => {
|
||||||
await this.methods.regenerateStreamingList()
|
const remoteStreams = await this.methods.fetchStreams()
|
||||||
|
|
||||||
let data = this.streamings.map((stream) => {
|
return res.json(remoteStreams)
|
||||||
return lodash.omit(stream, FILTER_KEYS)
|
|
||||||
})
|
|
||||||
|
|
||||||
data = data.map(async (stream) => {
|
|
||||||
let info = await StreamingInfo.findOne({
|
|
||||||
user_id: stream.user_id
|
|
||||||
})
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
stream.info = info.toObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream
|
|
||||||
})
|
|
||||||
|
|
||||||
data = await Promise.all(data)
|
|
||||||
|
|
||||||
return res.json(data)
|
|
||||||
},
|
},
|
||||||
"/stream/info": {
|
"/stream/info": {
|
||||||
middleware: ["withAuthentication"],
|
middleware: ["withAuthentication"],
|
||||||
@ -189,11 +157,32 @@ export default class StreamingController extends Controller {
|
|||||||
user_id = user_id["_id"].toString()
|
user_id = user_id["_id"].toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await StreamingInfo.findOne({
|
let info = await StreamingInfo.findOne({
|
||||||
user_id,
|
user_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(info)
|
if (!info) {
|
||||||
|
info = new StreamingInfo({
|
||||||
|
user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
await info.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await StreamingCategory.findOne({
|
||||||
|
key: info.category
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
return {}
|
||||||
|
}) ?? {}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
...info.toObject(),
|
||||||
|
["category"]: {
|
||||||
|
key: category?.key ?? "unknown",
|
||||||
|
label: category?.label ?? "Unknown",
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/streaming/addresses": {
|
"/streaming/addresses": {
|
||||||
@ -218,8 +207,10 @@ export default class StreamingController extends Controller {
|
|||||||
"/streaming/:username": async (req, res) => {
|
"/streaming/:username": async (req, res) => {
|
||||||
const { username } = req.params
|
const { username } = req.params
|
||||||
|
|
||||||
|
const streamings = await this.methods.fetchStreams()
|
||||||
|
|
||||||
// search on this.streamings
|
// search on this.streamings
|
||||||
const streaming = this.streamings.find((streaming) => streaming.username === username)
|
const streaming = streamings.find((streaming) => streaming.username === username)
|
||||||
|
|
||||||
if (streaming) {
|
if (streaming) {
|
||||||
return res.json(lodash.omit(streaming, FILTER_KEYS))
|
return res.json(lodash.omit(streaming, FILTER_KEYS))
|
||||||
@ -257,6 +248,36 @@ export default class StreamingController extends Controller {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
put = {
|
||||||
|
"/streaming/category": {
|
||||||
|
middlewares: ["withAuthentication", "onlyAdmin"],
|
||||||
|
fn: Schematized({
|
||||||
|
required: ["key", "label"]
|
||||||
|
}, async (req, res) => {
|
||||||
|
const { key, label } = req.selection
|
||||||
|
|
||||||
|
const existingCategory = await StreamingCategory.findOne({
|
||||||
|
key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingCategory) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Category already exists"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = new StreamingCategory({
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
|
||||||
|
await category.save()
|
||||||
|
|
||||||
|
return res.json(category)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
post = {
|
post = {
|
||||||
"/streaming/update_info": {
|
"/streaming/update_info": {
|
||||||
middlewares: ["withAuthentication"],
|
middlewares: ["withAuthentication"],
|
||||||
@ -285,25 +306,22 @@ export default class StreamingController extends Controller {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/streaming/publish": async (req, res) => {
|
"/streaming/publish": async (req, res) => {
|
||||||
const { app, stream, tcUrl } = req.body
|
const { stream } = req.body
|
||||||
|
|
||||||
|
const streaming = await this.methods.generateStreamFromStreamkey(stream).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
const streaming = await this.methods.pushToLocalList({
|
|
||||||
app,
|
|
||||||
stream,
|
|
||||||
tcUrl
|
|
||||||
}).catch((err) => {
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
code: 1,
|
error: `Cannot generate stream: ${err.message}`,
|
||||||
error: err.message
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return false
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (streaming) {
|
if (streaming) {
|
||||||
global.wsInterface.io.emit(`streaming.new`, {
|
global.wsInterface.io.emit(`streaming.new`, streaming)
|
||||||
username: streaming.username,
|
|
||||||
})
|
global.wsInterface.io.emit(`streaming.new.${streaming.username}`, streaming)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
code: 0,
|
code: 0,
|
||||||
@ -314,21 +332,16 @@ export default class StreamingController extends Controller {
|
|||||||
"/streaming/unpublish": async (req, res) => {
|
"/streaming/unpublish": async (req, res) => {
|
||||||
const { stream } = req.body
|
const { stream } = req.body
|
||||||
|
|
||||||
const streaming = await this.methods.removeFromLocalList({
|
const streaming = await this.methods.generateStreamFromStreamkey(stream).catch((err) => {
|
||||||
stream
|
console.error(err)
|
||||||
}).catch((err) => {
|
|
||||||
res.status(500).json({
|
|
||||||
code: 2,
|
|
||||||
status: err.message
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (streaming) {
|
if (streaming) {
|
||||||
global.wsInterface.io.emit(`streaming.end`, {
|
global.wsInterface.io.emit(`streaming.end`, streaming)
|
||||||
username: streaming.username,
|
|
||||||
})
|
global.wsInterface.io.emit(`streaming.end.${streaming.username}`, streaming)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
code: 0,
|
code: 0,
|
||||||
|
@ -37,6 +37,7 @@ export const Playlist = mongoose.model("Playlist", schemas.Playlist, "playlists"
|
|||||||
// streamings
|
// streamings
|
||||||
export const StreamingKey = mongoose.model("StreamingKey", schemas.StreamingKey, "streamingKeys")
|
export const StreamingKey = mongoose.model("StreamingKey", schemas.StreamingKey, "streamingKeys")
|
||||||
export const StreamingInfo = mongoose.model("StreamingInfo", schemas.StreamingInfo, "streamingInfos")
|
export const StreamingInfo = mongoose.model("StreamingInfo", schemas.StreamingInfo, "streamingInfos")
|
||||||
|
export const StreamingCategory = mongoose.model("StreamingCategory", schemas.StreamingCategory, "streamingCategories")
|
||||||
|
|
||||||
// others
|
// others
|
||||||
export const FeaturedWallpaper = mongoose.model("FeaturedWallpaper", schemas.FeaturedWallpaper, "featuredWallpapers")
|
export const FeaturedWallpaper = mongoose.model("FeaturedWallpaper", schemas.FeaturedWallpaper, "featuredWallpapers")
|
||||||
|
@ -18,4 +18,5 @@ export { default as FeaturedWallpaper } from "./featuredWallpaper"
|
|||||||
export { default as FeaturedEvent } from "./featuredEvent"
|
export { default as FeaturedEvent } from "./featuredEvent"
|
||||||
|
|
||||||
export { default as StreamingKey } from "./streamingKey"
|
export { default as StreamingKey } from "./streamingKey"
|
||||||
export { default as StreamingInfo } from "./streamingInfo"
|
export { default as StreamingInfo } from "./streamingInfo"
|
||||||
|
export { default as StreamingCategory } from "./streamingCategory"
|
10
packages/server/src/schemas/streamingCategory/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
key: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wrapper",
|
"name": "wrapper",
|
||||||
"version": "0.31.1",
|
"version": "0.33.0",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|