New feature: featured links

This commit is contained in:
Evert Prants 2019-10-23 13:43:31 +03:00
parent 4dda891389
commit f82214a6f5
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 288 additions and 5 deletions

82
app.js
View File

@ -11,6 +11,7 @@ const fs = require('fs')
const uuid = require('uuid/v4')
const redis = require('redis')
const connectSession = require('connect-redis')
const URL = require('url')
require('express-async-errors')
@ -289,10 +290,17 @@ app.use(async function (req, res, next) {
res.locals.session = { uuid: req.session.login, username: req.session.username }
if (!cache.streamers[req.session.login]) {
let db = await dbPromise
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.session.login)
if (streamer) cache.streamers[req.session.login] = streamer
}
if (cache.streamers[req.session.login]) {
req.isStreamer = true
return next()
}
next()
})
@ -350,6 +358,69 @@ app.get('/dashboard/data', async (req, res) => {
})
})
// Get links
app.get('/dashboard/link', async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
let user = req.session.login
let db = await dbPromise
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
res.jsonp(links)
})
// Add link URL
app.post('/dashboard/link', async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
let user = req.session.login
let name = req.body.name
let url = req.body.url
if (name == null || url == null) return res.jsonp({ error: 'Missing parameters!' })
if (name.length > 120) return res.jsonp({ error: 'Only 120 characters are allowed in the name.' })
if (name.indexOf('<') !== -1 || name.indexOf('>') !== -1) return res.jsonp({ error: 'HTML tags are forbidden!' })
// Validate URL
try {
URL.parse(url)
} catch (e) {
return res.jsonp({ error: 'Invalid URL!' })
}
// Checks
let db = await dbPromise
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
if (links.length > 10) return res.jsonp({ error: 'You can currently only add up to 10 links!' })
let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', url, user)
if (link) return res.jsonp({ error: 'This URL already exists!' })
// Add
await db.run('INSERT INTO link (name,url,uuid) VALUES (?,?,?)', name, url, user)
res.jsonp({ success: true })
})
// Remove link URL
app.post('/dashboard/link/delete', async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
let user = req.session.login
if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' })
// Check
let db = await dbPromise
let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', req.body.url, user)
if (!link) {
link = await db.get('SELECT * FROM link WHERE name = ? AND uuid = ?', req.body.name, user)
}
if (!link) return res.jsonp({ error: 'Invalid link parameter!' })
// Delete
await db.run('DELETE FROM link WHERE id = ?', link.id)
res.jsonp({ success: true })
})
// Player
app.get('/watch/:name', (req, res) => {
res.render('player.html', { name: req.params.name, server: streamServer })
@ -363,11 +434,20 @@ app.get('/player/:name', (req, res) => {
app.get('/api/channel/:name', async (req, res) => {
let name = req.params.name
let db = await dbPromise
let data = await db.get('SELECT name,live_at,last_stream FROM channels WHERE name=?', name)
let data = await db.get('SELECT user_uuid,name,live_at,last_stream FROM channels WHERE name=?', name)
if (!data) return res.jsonp({ error: 'No such channel!' })
let links = await db.all('SELECT * FROM link WHERE uuid = ?', data.user_uuid)
data.live = data.live_at != null
data.live_at = new Date(parseInt(data.live_at))
data.last_stream = new Date(parseInt(data.last_stream))
data.links = links || []
for (let i in data.links) {
delete data.links[i].id
delete data.links[i].uuid
}
res.jsonp(data)
})

View File

@ -13,4 +13,3 @@
[OAuth2]
ClientID = 1
ClientSecret = "hackme"

10
migrations/002_link.sql Normal file
View File

@ -0,0 +1,10 @@
-- Up
CREATE TABLE link (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL,
name TEXT NOT NULL,
uuid TEXT NOT NULL
);
-- Down
DROP TABLE link;

View File

@ -118,3 +118,16 @@ body {
.seekview:hover > .seeker {
display: inline-block;
}
.overlay-list {
background-color: hsla(0, 0%, 12%, 0.75);
position: absolute;
}
.overlay-list.links a {
padding: 10px;
color: #fff;
font-size: 1.1em;
display: block;
width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -28,6 +28,29 @@ function recursiveStats (table, subtable) {
}
}
function updateLinkList (k) {
if (k && k.error) return alert(k.error)
$.get('/dashboard/link', function (res) {
if (res.error) return
$('#link-list').html('<tbody></tbody>')
$('#link-list tbody').append('<tr><th>Name</th><th>URL</th><th>Action</th></tr>')
for (let i in res) {
let p = res[i]
p.name = p.name.replace(/\</g, '&lt;').replace(/\>/g, '&gt;')
$('#link-list tbody').append('<tr data-url="' + p.url + '"><td>' + p.name +'</td><td>' +
'<a href="' + p.url +'" target="_blank" rel="nofollow">' +
p.url + '</a></td><td><a href="#" class="delete-link">Remove</a></td></tr>')
}
$('.delete-link').click(function (e) {
e.preventDefault()
let pr = $(this).parent().parent().attr('data-url')
$.post('/dashboard/link/delete', { url: pr }, updateLinkList)
})
})
}
function dashboard (k) {
$.get('/dashboard/data', function (res) {
if (res.error) {
@ -46,12 +69,46 @@ function dashboard (k) {
$('#show_key').html(k)
})
$('.go-page').click(function (e) {
let el = $(this)
$('.go-page').removeClass('active')
el.addClass('active')
$('.page:visible').fadeOut(function () {
$('#page-' + el.attr('data-page')).fadeIn()
})
})
$('#add-link').submit(function (e) {
e.preventDefault()
let name = $('input[name="name"]').val()
let url = $('input[name="url"]').val()
if (name.length > 120) return alert('Only 120 characters are allowed in the name.')
$.post('/dashboard/link', { name, url }, function () {
$('input[name="name"]').val('')
$('input[name="url"]').val('')
updateLinkList()
})
})
setInterval(function () {
$.get('/dashboard/stats', function (res) {
if (res.error) return
recursiveStats(res, '')
})
}, 5000)
updateLinkList()
let hash = window.location.hash
if (hash.indexOf('#') === 0) hash = hash.substr(1)
if ($('#page-' + hash).length) {
$('.go-page').removeClass('active')
$('.go-page[data-page="' + hash + '"').addClass('active')
$('.page:visible').hide()
$('.page#page-' + hash).show()
}
}
export default dashboard

View File

@ -16,6 +16,8 @@ let bigbtn = overlay.querySelector('.bigplaybtn')
let volumebar = overlay.querySelector('#volume_seek')
let volumeseek = volumebar.querySelector('.seeker')
let volumeseekInner = volumeseek.querySelector('.seekbar')
let links
let linksList
// Variables
let hls
@ -251,6 +253,100 @@ if (Hls.isSupported()) {
alert('Your browser does not support HLS streaming!')
}
// helper function to get an element's exact position
function getPosition(el) {
let xPosition = 0
let yPosition = 0
while (el) {
if (el.tagName == 'BODY') {
// deal with browser quirks with body/window/document and page scroll
let xScrollPos = el.scrollLeft || document.documentElement.scrollLeft
let yScrollPos = el.scrollTop || document.documentElement.scrollTop
xPosition += (el.offsetLeft - xScrollPos + el.clientLeft)
yPosition += (el.offsetTop - yScrollPos + el.clientTop)
} else {
xPosition += (el.offsetLeft - el.scrollLeft + el.clientLeft)
yPosition += (el.offsetTop - el.scrollTop + el.clientTop)
}
el = el.offsetParent
}
return {
x: xPosition,
y: yPosition
}
}
function hideOnClickOutside (element) {
const isVisible = (element) => {
return element.style.display === 'block'
}
const outsideClickListener = event => {
if ((!element.contains(event.target) && isVisible(element))
&& (event.target !== links && !links.contains(event.target))) {
element.style.display = 'none'
removeClickListener()
}
}
const removeClickListener = () => {
document.removeEventListener('click', outsideClickListener)
}
document.addEventListener('click', outsideClickListener)
}
function updateLinks (srcs) {
if (srcs.length === 0) {
if (links) links.style.display = 'none'
return
}
if (!links) {
links = document.createElement('div')
links.className = 'right button'
links.id = 'links'
links.innerHTML = '<i class="fa fa-link" aria-hidden="true"></i>'
overlay.getElementsByClassName('controls')[0].insertBefore(links, fullscreenbtn)
linksList = document.createElement('div')
linksList.className = 'links overlay-list'
linksList.style = 'display: none;'
overlay.appendChild(linksList)
links.addEventListener('click', function (e) {
e.preventDefault()
if (linksList.style.display === 'block') {
linksList.style = 'display: none;'
return
}
hideOnClickOutside(linksList)
let pos = getPosition(links)
linksList.style = 'display: block;'
pos.x -= linksList.offsetWidth - links.offsetWidth / 2
pos.y -= linksList.offsetHeight
linksList.style = 'display: block; left: ' + pos.x + 'px; top: ' + pos.y + 'px;'
})
}
links.style.display = 'block'
linksList.innerHTML = ''
for (let i in srcs) {
let link = srcs[i]
let el = document.createElement('a')
el.href = link.url
el.innerText = link.name
el.target = '_blank'
linksList.appendChild(el)
}
}
function getStreamStatus () {
GET('/api/channel/' + STREAM_NAME).then((data) => {
let jd = JSON.parse(data)
@ -259,6 +355,8 @@ function getStreamStatus () {
return alert(jd.error)
}
if (jd.links) updateLinks(jd.links)
if (jd.live && !vidReady) loadSource()
liveStatus(jd.live)
}, (e) => {
@ -278,6 +376,11 @@ document.addEventListener('mozfullscreenchange', exitHandler, false)
document.addEventListener('fullscreenchange', exitHandler, false)
document.addEventListener('MSFullscreenChange', exitHandler, false)
window.addEventListener('resize', function () {
if (!linksList || linksList.style.display !== 'block') return
linksList.style = 'display: none;'
})
vid.addEventListener('timeupdate', updateTime, false)
getStreamStatus()

View File

@ -4,6 +4,7 @@
<script type="text/javascript">
window.STREAM_KEY = "{{ stream }}"
</script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/dist/css/dashboard.css">
<meta charset="utf-8">
@ -25,16 +26,22 @@
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<a class="nav-link active go-page" href="#dashboard" data-page="dashboard">
<i class="fa fa-home" aria-hidden="true"></i>
Dashboard <span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link go-page" href="#links" data-page="links">
<i class="fa fa-link" aria-hidden="true"></i>
Manage Links
</a>
</li>
</ul>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" id="page-dashboard">
<main role="main" class="page col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" id="page-dashboard">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
</div>
@ -125,6 +132,20 @@
</tbody>
</table>
</main>
<main role="main" class="page col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" id="page-links" style="display: none;">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">My Links</h1>
</div>
<p>These links will be automatically available from under the player. You can use it to link your social medias, chats or personal website.</p>
<table class="list table" id="link-list"></table>
<p>Link name can only contain 120 characters and forbids the use of HTML tags.</p>
<form id="add-link">
<input placeholder="Name" id="add-link-name" name="name" />
<input placeholder="URL" id="add-link-url" name="url" />
<input type="submit" value="Add URL">
</form>
</main>
</div>
</div>
<script src="/dist/main.bundle.js"></script>