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": {
|
||||
"concurrently": "^7.5.0"
|
||||
},
|
||||
"version": "0.31.1"
|
||||
"version": "0.33.0"
|
||||
}
|
||||
|
@ -19,7 +19,11 @@
|
||||
"language": "en",
|
||||
"sidebarKeys": [
|
||||
"home",
|
||||
"tv",
|
||||
"music",
|
||||
"events",
|
||||
"groups",
|
||||
"marketplace",
|
||||
"groups"
|
||||
"dev"
|
||||
]
|
||||
}
|
@ -14,10 +14,31 @@
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "saved",
|
||||
"path": "/saved",
|
||||
"title": "Saved",
|
||||
"icon": "Archive",
|
||||
"id": "events",
|
||||
"path": "/events",
|
||||
"title": "Events",
|
||||
"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
|
||||
},
|
||||
{
|
||||
@ -28,23 +49,10 @@
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "streams",
|
||||
"path": "/streams",
|
||||
"title": "Streams",
|
||||
"icon": "Tv",
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "streaming_control",
|
||||
"path": "/streaming_control",
|
||||
"title": "Streaming Control",
|
||||
"icon": "Video"
|
||||
},
|
||||
{
|
||||
"id": "groups",
|
||||
"path": "/groups",
|
||||
"title": "Groups",
|
||||
"icon": "Users",
|
||||
"id": "dev",
|
||||
"path": "/dev",
|
||||
"title": "Development",
|
||||
"icon": "MdOutlineCode",
|
||||
"reachable": true
|
||||
}
|
||||
]
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "comty",
|
||||
"version": "0.31.1",
|
||||
"version": "0.33.0",
|
||||
"license": "LGPL-2.1",
|
||||
"main": "electron/main",
|
||||
"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))
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
if (!validCharacters) return
|
||||
if (!validCharacters) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const request = await app.api.customRequest("main", {
|
||||
method: "GET",
|
||||
@ -120,7 +123,7 @@ const steps = [
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
onPressEnter={submit}
|
||||
placeholder="@newuser"
|
||||
placeholder="newuser"
|
||||
value={username}
|
||||
onChange={handleUpdate}
|
||||
status={username.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
|
||||
|
@ -27,6 +27,12 @@ export default class Livestream {
|
||||
return data
|
||||
}
|
||||
|
||||
static async getCategories() {
|
||||
const request = await Livestream.bridge.get.streamingCategories()
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
static async getStreamInfo(payload) {
|
||||
let { username } = payload ?? {}
|
||||
|
||||
|
@ -6,36 +6,10 @@ import { Icons, createIconRender } from "components/Icons"
|
||||
|
||||
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 Tabs from "./tabs"
|
||||
|
||||
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 {
|
||||
state = {
|
||||
activeTab: this.props.match.params.type ?? "feed"
|
||||
@ -80,7 +54,7 @@ export default class Dashboard extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="dashboard">
|
||||
return <div className="postingDashboard">
|
||||
<div></div>
|
||||
|
||||
<div
|
||||
|
@ -2,40 +2,10 @@ import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import { Icons, createIconRender } from "components/Icons"
|
||||
|
||||
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 Tabs from "./tabs"
|
||||
|
||||
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 {
|
||||
state = {
|
||||
activeTab: this.props.match.params.type ?? "feed"
|
||||
@ -80,7 +50,7 @@ export default class Dashboard extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="dashboard">
|
||||
return <div className="postingDashboard">
|
||||
<div
|
||||
ref={this.primaryPanelRef}
|
||||
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"
|
||||
|
||||
export default () => {
|
||||
app.setLocation("home/feed")
|
||||
app.setLocation("/home/feed")
|
||||
|
||||
return <></>
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
.dashboard {
|
||||
.postingDashboard {
|
||||
display: grid;
|
||||
|
||||
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 Livestream from "../../models/livestream"
|
||||
import Livestream from "../../../../models/livestream"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
@ -32,6 +32,131 @@ const StreamingKeyView = (props) => {
|
||||
</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) => {
|
||||
const [streamInfo, setStreamInfo] = React.useState({})
|
||||
const [addresses, setAddresses] = React.useState({})
|
||||
@ -39,6 +164,19 @@ export default (props) => {
|
||||
const [isConnected, setIsConnected] = React.useState(false)
|
||||
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 () => {
|
||||
antd.Modal.confirm({
|
||||
title: "Regenerate streaming key",
|
||||
@ -125,15 +263,35 @@ export default (props) => {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="description">
|
||||
<span>
|
||||
Description
|
||||
</span>
|
||||
|
||||
<p>
|
||||
{streamInfo?.description ?? "No description"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="category">
|
||||
<span>
|
||||
Category
|
||||
</span>
|
||||
<h4>
|
||||
{streamInfo?.category ?? "No category"}
|
||||
{streamInfo?.category?.label ?? "No category"}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<antd.Button
|
||||
type="primary"
|
||||
icon={<Icons.Edit2 />}
|
||||
onClick={onClickEditInfo}
|
||||
>
|
||||
Edit info
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config">
|
@ -2,13 +2,15 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
height: 20vh;
|
||||
height: fit-content;
|
||||
|
||||
padding: 15px;
|
||||
|
||||
@ -21,7 +23,7 @@
|
||||
|
||||
.preview {
|
||||
height: 100%;
|
||||
max-width: 400px;
|
||||
width: 300px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@ -38,6 +40,7 @@
|
||||
flex-direction: column;
|
||||
|
||||
padding: 20px 0;
|
||||
width: 100%;
|
||||
|
||||
.status {
|
||||
margin-bottom: 20px;
|
||||
@ -47,6 +50,7 @@
|
||||
|
||||
.config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 0 40px;
|
||||
|
||||
@ -83,7 +87,7 @@
|
||||
.title {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@ -114,8 +118,29 @@
|
||||
div {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
align-items: 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 Livestream from "models/livestream"
|
||||
import * as antd from "antd"
|
||||
|
||||
import { UserPreview } from "components"
|
||||
import { Icons } from "components/Icons"
|
||||
|
||||
import Livestream from "../../../../models/livestream"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const LivestreamItem = (props) => {
|
||||
@ -34,7 +33,7 @@ const LivestreamItem = (props) => {
|
||||
<h2>{livestream.info?.description ?? "No description"}</h2>
|
||||
</div>
|
||||
<div className="livestream_category">
|
||||
{livestream.info?.catagory ?? "No category"}
|
||||
{livestream.info?.category?.label ?? "No category"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -71,10 +70,6 @@ export default (props) => {
|
||||
app.setLocation(`/live/${livestream.username}`)
|
||||
}
|
||||
|
||||
const onClickControlPanel = () => {
|
||||
app.setLocation("/live_control")
|
||||
}
|
||||
|
||||
const renderList = () => {
|
||||
if (loading) {
|
||||
return <antd.Skeleton active />
|
||||
@ -105,19 +100,10 @@ export default (props) => {
|
||||
<span>Livestreams</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<antd.Button
|
||||
icon={<Icons.Settings />}
|
||||
onClick={onClickControlPanel}
|
||||
>
|
||||
Control Panel
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="livestream_list">
|
||||
{renderList()}
|
||||
</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",
|
||||
"version": "0.31.1",
|
||||
"version": "0.33.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "corenode-cli build",
|
||||
|
@ -3,19 +3,17 @@ import { nanoid } from "nanoid"
|
||||
import lodash from "lodash"
|
||||
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 streamingServerAPIAddress = process.env.STREAMING_API_SERVER
|
||||
const streamingServerAPIProtocol = streamingServerAPIAddress.startsWith("https") ? "https" : "http"
|
||||
const streamingIngestServer = process.env.STREAMING_INGEST_SERVER ?? ""
|
||||
const streamingServerAPIAddress = process.env.STREAMING_API_SERVER ?? ""
|
||||
|
||||
const streamingServerAPIUri = `${streamingServerAPIProtocol}://${streamingServerAPIAddress.split("://")[1]}`
|
||||
const streamingServerAPIUri = `${streamingServerAPIAddress.startsWith("https") ? "https" : "http"}://${streamingServerAPIAddress.split("://")[1]}`
|
||||
|
||||
const FILTER_KEYS = ["stream"]
|
||||
|
||||
export default class StreamingController extends Controller {
|
||||
streamings = []
|
||||
|
||||
methods = {
|
||||
genereteKey: async (user_id) => {
|
||||
// this will generate a new key for the user
|
||||
@ -34,78 +32,61 @@ export default class StreamingController extends Controller {
|
||||
|
||||
return streamingKey
|
||||
},
|
||||
regenerateStreamingList: async () => {
|
||||
fetchStreams: async () => {
|
||||
// fetch all streams from api
|
||||
let streams = await axios.get(`${streamingServerAPIUri}/api/v1/streams`).catch((err) => {
|
||||
console.log(err)
|
||||
let { data } = await axios.get(`${streamingServerAPIUri}/api/v1/streams`).catch((err) => {
|
||||
console.error(err)
|
||||
return false
|
||||
})
|
||||
|
||||
if (streams) {
|
||||
streams = streams.data.streams
|
||||
let streamings = []
|
||||
|
||||
// FIXME: this method is not totally async
|
||||
streams.forEach((stream) => {
|
||||
// check if the stream is already in the list
|
||||
const streamInList = this.streamings.find((s) => s.stream === stream.name)
|
||||
if (!data) return streamings
|
||||
|
||||
if (!streamInList) {
|
||||
// if not, add it
|
||||
this.methods.pushToLocalList({
|
||||
stream: stream.name,
|
||||
app: stream.app,
|
||||
}).catch((err) => {
|
||||
// sorry for you
|
||||
})
|
||||
}
|
||||
streamings = data.streams
|
||||
|
||||
streamings = streamings.map(async (stream) => {
|
||||
stream = await this.methods.generateStreamFromStreamkey(stream.name)
|
||||
|
||||
let info = await StreamingInfo.findOne({
|
||||
user_id: stream.user_id
|
||||
})
|
||||
}
|
||||
},
|
||||
pushToLocalList: async (payload) => {
|
||||
const { stream, app } = payload
|
||||
|
||||
const username = app.split("/")[1]
|
||||
const user_id = await User.findOne({ username }).then((user) => user._id)
|
||||
if (info) {
|
||||
stream.info = info.toObject()
|
||||
|
||||
const streamingKey = await StreamingKey.findOne({
|
||||
key: stream
|
||||
stream.info.category = await StreamingCategory.findOne({
|
||||
key: stream.info.category
|
||||
})
|
||||
}
|
||||
|
||||
return stream
|
||||
})
|
||||
|
||||
if (!streamingKey) {
|
||||
throw new Error("Invalid streaming key")
|
||||
}
|
||||
streamings = await Promise.all(streamings)
|
||||
|
||||
if (username !== streamingKey.username) {
|
||||
throw new Error("Invalid streaming key for this username")
|
||||
}
|
||||
return streamings.map((stream) => {
|
||||
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 = {
|
||||
stream,
|
||||
user_id: user_id.toString(),
|
||||
user_id: streamingKey.user_id,
|
||||
username: streamingKey.username,
|
||||
sources: {
|
||||
rtmp: `${streamingIngestServer}/live/${username}`,
|
||||
hls: `${streamingServerAPIAddress}/live/${username}/src.m3u8`,
|
||||
flv: `${streamingServerAPIAddress}/live/${username}/src.flv`,
|
||||
rtmp: `${streamingIngestServer}/live/${streamingKey.username}`,
|
||||
hls: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.m3u8`,
|
||||
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
|
||||
},
|
||||
handleInfoUpdate: async (payload) => {
|
||||
@ -147,28 +128,15 @@ export default class StreamingController extends Controller {
|
||||
}
|
||||
|
||||
get = {
|
||||
"/streaming/categories": async (req, res) => {
|
||||
const categories = await StreamingCategory.find()
|
||||
|
||||
return res.json(categories)
|
||||
},
|
||||
"/streams": async (req, res) => {
|
||||
await this.methods.regenerateStreamingList()
|
||||
const remoteStreams = await this.methods.fetchStreams()
|
||||
|
||||
let data = this.streamings.map((stream) => {
|
||||
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)
|
||||
return res.json(remoteStreams)
|
||||
},
|
||||
"/stream/info": {
|
||||
middleware: ["withAuthentication"],
|
||||
@ -189,11 +157,32 @@ export default class StreamingController extends Controller {
|
||||
user_id = user_id["_id"].toString()
|
||||
}
|
||||
|
||||
const info = await StreamingInfo.findOne({
|
||||
let info = await StreamingInfo.findOne({
|
||||
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": {
|
||||
@ -218,8 +207,10 @@ export default class StreamingController extends Controller {
|
||||
"/streaming/:username": async (req, res) => {
|
||||
const { username } = req.params
|
||||
|
||||
const streamings = await this.methods.fetchStreams()
|
||||
|
||||
// search on this.streamings
|
||||
const streaming = this.streamings.find((streaming) => streaming.username === username)
|
||||
const streaming = streamings.find((streaming) => streaming.username === username)
|
||||
|
||||
if (streaming) {
|
||||
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 = {
|
||||
"/streaming/update_info": {
|
||||
middlewares: ["withAuthentication"],
|
||||
@ -285,25 +306,22 @@ export default class StreamingController extends Controller {
|
||||
}
|
||||
},
|
||||
"/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({
|
||||
code: 1,
|
||||
error: err.message
|
||||
error: `Cannot generate stream: ${err.message}`,
|
||||
})
|
||||
|
||||
return false
|
||||
return null
|
||||
})
|
||||
|
||||
if (streaming) {
|
||||
global.wsInterface.io.emit(`streaming.new`, {
|
||||
username: streaming.username,
|
||||
})
|
||||
global.wsInterface.io.emit(`streaming.new`, streaming)
|
||||
|
||||
global.wsInterface.io.emit(`streaming.new.${streaming.username}`, streaming)
|
||||
|
||||
return res.json({
|
||||
code: 0,
|
||||
@ -314,21 +332,16 @@ export default class StreamingController extends Controller {
|
||||
"/streaming/unpublish": async (req, res) => {
|
||||
const { stream } = req.body
|
||||
|
||||
const streaming = await this.methods.removeFromLocalList({
|
||||
stream
|
||||
}).catch((err) => {
|
||||
res.status(500).json({
|
||||
code: 2,
|
||||
status: err.message
|
||||
})
|
||||
const streaming = await this.methods.generateStreamFromStreamkey(stream).catch((err) => {
|
||||
console.error(err)
|
||||
|
||||
return false
|
||||
return null
|
||||
})
|
||||
|
||||
if (streaming) {
|
||||
global.wsInterface.io.emit(`streaming.end`, {
|
||||
username: streaming.username,
|
||||
})
|
||||
global.wsInterface.io.emit(`streaming.end`, streaming)
|
||||
|
||||
global.wsInterface.io.emit(`streaming.end.${streaming.username}`, streaming)
|
||||
|
||||
return res.json({
|
||||
code: 0,
|
||||
|
@ -37,6 +37,7 @@ export const Playlist = mongoose.model("Playlist", schemas.Playlist, "playlists"
|
||||
// streamings
|
||||
export const StreamingKey = mongoose.model("StreamingKey", schemas.StreamingKey, "streamingKeys")
|
||||
export const StreamingInfo = mongoose.model("StreamingInfo", schemas.StreamingInfo, "streamingInfos")
|
||||
export const StreamingCategory = mongoose.model("StreamingCategory", schemas.StreamingCategory, "streamingCategories")
|
||||
|
||||
// others
|
||||
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 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",
|
||||
"version": "0.31.1",
|
||||
"version": "0.33.0",
|
||||
"main": "./src/index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|