New feature: featured links
This commit is contained in:
parent
4dda891389
commit
f82214a6f5
82
app.js
82
app.js
@ -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)
|
||||
})
|
||||
|
||||
|
@ -13,4 +13,3 @@
|
||||
[OAuth2]
|
||||
ClientID = 1
|
||||
ClientSecret = "hackme"
|
||||
|
||||
|
10
migrations/002_link.sql
Normal file
10
migrations/002_link.sql
Normal 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;
|
@ -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;
|
||||
}
|
||||
|
@ -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, '<').replace(/\>/g, '>')
|
||||
$('#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
|
||||
|
103
src/player.js
103
src/player.js
@ -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()
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user