function makeAlert(title, text, klass, textOnly) {
if(!klass) {
klass = "alert-info";
}
var wrap = $("
").addClass("col-md-12");
var al = $("").addClass("alert")
.addClass(klass)
.appendTo(wrap);
textOnly ? al.text(text) : al.html(text) ;
$("
").prependTo(al);
$("").text(title).prependTo(al);
$("").addClass("close pull-right").html("×")
.on('click', function() {
al.hide("fade", function() {
wrap.remove();
});
})
.prependTo(al);
return wrap;
}
function formatURL(data) {
switch(data.type) {
case "yt":
return "https://youtube.com/watch?v=" + data.id;
case "vi":
return "https://vimeo.com/" + data.id;
case "dm":
return "https://dailymotion.com/video/" + data.id;
case "sc":
return data.id;
case "li":
const [account,event] = data.id.split(';');
return `https://livestream.com/accounts/${account}/events/${event}`;
case "tw":
return "https://twitch.tv/" + data.id;
case "rt":
return data.id;
case "gd":
return "https://docs.google.com/file/d/" + data.id;
case "fi":
return data.id;
case "hl":
return data.id;
case "sb":
return "https://streamable.com/" + data.id;
case "tc":
return "https://clips.twitch.tv/" + data.id;
case "cm":
return data.id;
case "cu":
return data.meta.embed.src;
case "pt":
if(data.meta.embed.onlyLong){
return `https://${data.meta.embed.domain}/videos/watch/${data.meta.embed.uuid}`;
} else {
return `https://${data.meta.embed.domain}/w/${data.meta.embed.short}`;
}
case "bc":
return `https://www.bitchute.com/video/${data.id}/`;
case "bn":
const [artist,track] = data.id.split(';');
return `https://${artist}.bandcamp.com/track/${track}`;
case "od":
const [user,video] = data.id.split(';');
return `https://odysee.com/@${user}/${video}`;
case "nv":
return `https://www.nicovideo.jp/watch/${data.id}`;
default:
return "#";
}
}
function findUserlistItem(name) {
var children = $("#userlist .userlist_item");
if(children.length == 0)
return null;
name = name.toLowerCase();
// WARNING: Incoming hax because of jQuery and bootstrap bullshit
var keys = Object.keys(children);
for(var k in keys) {
var i = keys[k];
if(isNaN(parseInt(i))) {
continue;
}
var child = children[i];
if($(child.children[1]).text().toLowerCase() == name)
return $(child);
}
return null;
}
function formatUserlistItem(div) {
var data = {
name: div.data("name") || "",
rank: div.data("rank"),
profile: div.data("profile") || { image: "", text: ""},
leader: div.data("leader") || false,
icon: div.data("icon") || false,
};
var name = $(div.children()[1]);
name.removeClass();
name.css("font-style", "");
name.addClass(getNameColor(data.rank));
div.find(".profile-box").remove();
var meta = div.data().meta || {}; // Not sure how this could happen.
if (meta.afk) {
div.addClass("userlist_afk");
} else {
div.removeClass("userlist_afk");
}
if (meta.muted) {
div.addClass("userlist_muted");
} else {
div.removeClass("userlist_muted");
}
if (meta.smuted) {
div.addClass("userlist_smuted");
} else {
div.removeClass("userlist_smuted");
}
var profile = null;
/*
* 2015-10-19
* Prevent rendering unnecessary duplicates of the profile box when
* a user's status changes.
*/
name.unbind("mouseenter");
name.unbind("mousemove");
name.unbind("mouseleave");
name.on('mouseenter', function(ev) {
if (profile)
profile.remove();
var top = ev.clientY + 5;
var horiz = ev.clientX;
profile = $("")
.addClass("profile-box linewrap")
.css("top", top + "px")
.appendTo(div);
if(data.profile.image) {
$("").addClass("profile-image")
.attr("src", data.profile.image)
.appendTo(profile);
}
$("").text(data.name).appendTo(profile);
var meta = div.data("meta") || {};
if (meta.ip && USEROPTS.show_ip_in_tooltip) {
$("
").appendTo(profile);
$("").text(meta.ip).appendTo(profile);
}
if (meta.aliases) {
$("
").appendTo(profile);
$("").text("aliases: " + meta.aliases.join(", ")).appendTo(profile);
}
$("
").css("margin-top", "5px").css("margin-bottom", "5px").appendTo(profile);
$("").text(data.profile.text).appendTo(profile);
if ($("body").hasClass("synchtube")) horiz -= profile.outerWidth();
profile.css("left", horiz + "px")
});
name.on('mousemove', function(ev) {
var top = ev.clientY + 5;
var horiz = ev.clientX;
if ($("body").hasClass("synchtube")) horiz -= profile.outerWidth();
profile.css("left", horiz + "px")
.css("top", top + "px");
});
name.on('mouseleave', function() {
profile.remove();
});
var icon = div.children()[0];
icon.innerHTML = "";
// denote current leader with a star
if(data.leader) {
$("").addClass("glyphicon glyphicon-star-empty").appendTo(icon);
}
if(div.data().meta.afk) {
name.css("font-style", "italic");
$("").addClass("glyphicon glyphicon-time").appendTo(icon);
}
if (data.icon) {
$("").addClass("glyphicon " + data.icon).prependTo(icon);
}
}
function getNameColor(rank) {
if(rank >= Rank.Siteadmin)
return "userlist_siteadmin";
else if(rank >= Rank.Admin)
return "userlist_owner";
else if(rank >= Rank.Moderator)
return "userlist_op";
else if(rank == Rank.Guest)
return "userlist_guest";
else
return "";
}
function addUserDropdown(entry) {
var name = entry.data("name"),
rank = entry.data("rank"),
leader = entry.data("leader"),
meta = entry.data("meta") || {};
entry.find(".user-dropdown").remove();
var menu = $("")
.addClass("user-dropdown")
.appendTo(entry)
.hide();
$("").text(name).appendTo(menu);
$("
").appendTo(menu);
var btngroup = $("").addClass("btn-group-vertical").appendTo(menu);
/* ignore button */
if (name !== CLIENT.name) {
var ignore = $("").addClass("btn btn-xs btn-default")
.appendTo(btngroup)
.on('click', function () {
if(IGNORED.indexOf(name) == -1) {
ignore.text("Unignore User");
IGNORED.push(name);
entry.addClass("userlist-ignored");
} else {
ignore.text("Ignore User");
IGNORED.splice(IGNORED.indexOf(name), 1);
entry.removeClass("userlist-ignored");
}
setOpt("ignorelist", IGNORED);
});
if(IGNORED.indexOf(name) == -1) {
entry.removeClass("userlist-ignored");
ignore.text("Ignore User");
} else {
entry.addClass("userlist-ignored");
ignore.text("Unignore User");
}
}
/* pm button */
if (name !== CLIENT.name) {
var pm = $("").addClass("btn btn-xs btn-default")
.text("Private Message")
.appendTo(btngroup)
.on('click', function () {
initPm(name).find(".panel-heading").click();
menu.hide();
});
}
/* give/remove leader (moderator+ only) */
if (hasPermission("leaderctl")) {
var ldr = $("").addClass("btn btn-xs btn-default")
.appendTo(btngroup);
if(leader) {
ldr.text("Remove Leader");
ldr.on('click', function () {
socket.emit("assignLeader", {
name: ""
});
});
} else {
ldr.text("Give Leader");
ldr.on('click', function () {
socket.emit("assignLeader", {
name: name
});
});
}
}
/* kick button */
if(hasPermission("kick")) {
$("").addClass("btn btn-xs btn-default")
.text("Kick")
.on('click', function () {
var reason = prompt("Enter kick reason (optional)");
if (reason === null) {
return;
}
socket.emit("chatMsg", {
msg: "/kick " + name + " " + reason,
meta: {}
});
})
.appendTo(btngroup);
}
/* mute buttons */
if (hasPermission("mute")) {
var mute = $("").addClass("btn btn-xs btn-default")
.text("Mute")
.on('click', function () {
socket.emit("chatMsg", {
msg: "/mute " + name,
meta: {}
});
})
.appendTo(btngroup);
var smute = $("").addClass("btn btn-xs btn-default")
.text("Shadow Mute")
.on('click', function () {
socket.emit("chatMsg", {
msg: "/smute " + name,
meta: {}
});
})
.appendTo(btngroup);
var unmute = $("").addClass("btn btn-xs btn-default")
.text("Unmute")
.on('click', function () {
socket.emit("chatMsg", {
msg: "/unmute " + name,
meta: {}
});
})
.appendTo(btngroup);
if (meta.muted) {
mute.hide();
smute.hide();
} else {
unmute.hide();
}
}
/* ban buttons */
if(hasPermission("ban")) {
$("").addClass("btn btn-xs btn-default")
.text("Name Ban")
.on('click', function () {
var reason = prompt("Enter ban reason (optional)");
if (reason === null) {
return;
}
socket.emit("chatMsg", {
msg: "/ban " + name + " " + reason,
meta: {}
});
})
.appendTo(btngroup);
$("").addClass("btn btn-xs btn-default")
.text("IP Ban")
.on('click', function () {
var reason = prompt("Enter ban reason (optional)");
if (reason === null) {
return;
}
socket.emit("chatMsg", {
msg: "/ipban " + name + " " + reason,
meta: {}
});
})
.appendTo(btngroup);
}
var showdd = function(ev) {
// Workaround for Chrome
if (ev.shiftKey) return true;
ev.preventDefault();
if(menu.css("display") == "none") {
$(".user-dropdown").hide();
$(document).bind("mouseup.userlist-ddown", function (e) {
if (menu.has(e.target).length === 0 &&
entry.parent().has(e.target).length === 0) {
menu.hide();
$(document).unbind("mouseup.userlist-ddown");
}
});
menu.show();
menu.css("top", entry.position().top);
} else {
menu.hide();
}
return false;
};
entry.contextmenu(showdd);
entry.click(showdd);
}
function calcUserBreakdown() {
var breakdown = {
"Site Admins": 0,
"Channel Admins": 0,
"Moderators": 0,
"Regular Users": 0,
"Guests": 0,
"Anonymous": 0,
"AFK": 0
};
var total = 0;
$("#userlist .userlist_item").each(function (index, item) {
var data = {
rank: $(item).data("rank")
};
if(data.rank >= 255)
breakdown["Site Admins"]++;
else if(data.rank >= 3)
breakdown["Channel Admins"]++;
else if(data.rank == 2)
breakdown["Moderators"]++;
else if(data.rank >= 1)
breakdown["Regular Users"]++;
else
breakdown["Guests"]++;
total++;
if($(item).data().meta.afk)
breakdown["AFK"]++;
});
breakdown["Anonymous"] = CHANNEL.usercount - total;
return breakdown;
}
function sortUserlist() {
var slice = Array.prototype.slice;
var list = slice.call($("#userlist .userlist_item"));
list.sort(function (a, b) {
var r1 = $(a).data("rank");
var r2 = $(b).data("rank");
var afk1 = $(a).find(".glyphicon-time").length > 0;
var afk2 = $(b).find(".glyphicon-time").length > 0;
var name1 = a.children[1].innerHTML.toLowerCase();
var name2 = b.children[1].innerHTML.toLowerCase();
if(USEROPTS.sort_afk) {
if(afk1 && !afk2)
return 1;
if(!afk1 && afk2)
return -1;
}
if(USEROPTS.sort_rank) {
if(r1 < r2)
return 1;
if(r1 > r2)
return -1;
}
return name1 === name2 ? 0 : (name1 < name2 ? -1 : 1);
});
list.forEach(function (item) {
$(item).detach();
});
list.forEach(function (item) {
$(item).appendTo($("#userlist"));
});
}
/* queue stuff */
function scrollQueue() {
var li = playlistFind(PL_CURRENT);
if(!li)
return;
li = $(li);
$("#queue").scrollTop(0);
var scroll = li.position().top - $("#queue").position().top;
$("#queue").scrollTop(scroll);
}
function makeQueueEntry(item, addbtns) {
var video = item.media;
var li = $("");
li.addClass("queue_entry");
li.addClass("pluid-" + item.uid);
li.data("uid", item.uid);
li.data("media", video);
li.data("temp", item.temp);
if(video.thumb) {
$("").attr("src", video.thumb.url)
.css("float", "left")
.css("clear", "both")
.appendTo(li);
}
var title = $("").addClass("qe_title").appendTo(li)
.text(video.title)
.attr("href", formatURL(video))
.attr("target", "_blank");
var time = $("").addClass("qe_time").appendTo(li);
time.text(video.duration);
var clear = $("").addClass("qe_clear").appendTo(li);
if(item.temp) {
li.addClass("queue_temp");
}
if(addbtns)
addQueueButtons(li);
return li;
}
function makeSearchEntry(video) {
var li = $("");
li.addClass("queue_entry");
li.data("media", video);
if(video.thumb) {
$("").attr("src", video.thumb.url)
.css("float", "left")
.css("clear", "both")
.appendTo(li);
}
var title = $("").addClass("qe_title").appendTo(li)
.text(video.title)
.attr("href", formatURL(video))
.attr("target", "_blank");
var time = $("").addClass("qe_time").appendTo(li);
time.text(video.duration);
var clear = $("").addClass("qe_clear").appendTo(li);
return li;
}
function addQueueButtons(li) {
li.find(".btn-group").remove();
var menu = $("").addClass("btn-group").appendTo(li);
// Play
if(hasPermission("playlistjump")) {
$("").addClass("btn btn-xs btn-default qbtn-play")
.html("Play")
.on('click', function() {
socket.emit("jumpTo", li.data("uid"));
})
.appendTo(menu);
}
// Queue next
if(hasPermission("playlistmove")) {
$("").addClass("btn btn-xs btn-default qbtn-next")
.html("Queue Next")
.on('click', function() {
socket.emit("moveMedia", {
from: li.data("uid"),
after: PL_CURRENT
});
})
.appendTo(menu);
}
// Temp/Untemp
if(hasPermission("settemp")) {
var tempstr = li.data("temp")?"Make Permanent":"Make Temporary";
$("").addClass("btn btn-xs btn-default qbtn-tmp")
.html("" + tempstr)
.on('click', function() {
socket.emit("setTemp", {
uid: li.data("uid"),
temp: !li.data("temp")
});
})
.appendTo(menu);
}
// Delete
if(hasPermission("playlistdelete")) {
$("").addClass("btn btn-xs btn-default qbtn-delete")
.html("Delete")
.on('click', function() {
socket.emit("delete", li.data("uid"));
})
.appendTo(menu);
}
if(USEROPTS.qbtn_hide && !USEROPTS.qbtn_idontlikechange
|| menu.find(".btn").length == 0)
menu.hide();
// I DON'T LIKE CHANGE
if(USEROPTS.qbtn_idontlikechange) {
menu.addClass("pull-left");
menu.detach().prependTo(li);
menu.find(".btn").each(function() {
// Clear icon
var icon = $(this).find(".glyphicon");
$(this).html("");
icon.appendTo(this);
});
menu.find(".qbtn-play").addClass("btn-success");
menu.find(".qbtn-delete").addClass("btn-danger");
}
else if(menu.find(".btn").length != 0) {
li.unbind("contextmenu");
li.contextmenu(function(ev) {
// Allow shift+click to open context menu
// (Chrome workaround, works by default on Firefox)
if (ev.shiftKey) return true;
ev.preventDefault();
if(menu.css("display") == "none")
menu.show("blind");
else
menu.hide("blind");
return false;
});
}
}
function rebuildPlaylist() {
var qli = $("#queue li");
if(qli.length == 0)
return;
REBUILDING = Math.random() + "";
var r = REBUILDING;
var i = 0;
qli.each(function() {
var li = $(this);
(function(i, r) {
setTimeout(function() {
// Stop if another rebuild is running
if(REBUILDING != r)
return;
addQueueButtons(li);
if(i == qli.length - 1) {
scrollQueue();
REBUILDING = false;
}
}, 10*i);
})(i, r);
i++;
});
}
/* menus */
/* user settings menu */
function showUserOptions() {
if (CLIENT.rank < 2) {
$("a[href='#us-mod']").parent().hide();
} else {
$("a[href='#us-mod']").parent().show();
}
$("#us-theme").val(USEROPTS.theme);
$("#us-layout").val(USEROPTS.layout);
$("#us-no-channelcss").prop("checked", USEROPTS.ignore_channelcss);
$("#us-no-channeljs").prop("checked", USEROPTS.ignore_channeljs);
$("#us-synch").prop("checked", USEROPTS.synch);
$("#us-synch-accuracy").val(USEROPTS.sync_accuracy);
$("#us-hidevideo").prop("checked", USEROPTS.hidevid);
$("#us-playlistbuttons").prop("checked", USEROPTS.qbtn_hide);
$("#us-oldbtns").prop("checked", USEROPTS.qbtn_idontlikechange);
$("#us-default-quality").val(USEROPTS.default_quality || "auto");
$("#us-peertube").prop("checked", USEROPTS.peertube_risk);
$("#us-chat-timestamp").prop("checked", USEROPTS.show_timestamps);
$("#us-sort-rank").prop("checked", USEROPTS.sort_rank);
$("#us-sort-afk").prop("checked", USEROPTS.sort_afk);
$("#us-blink-title").val(USEROPTS.blink_title);
$("#us-ping-sound").val(USEROPTS.boop);
$("#us-notifications").val(USEROPTS.notifications);
$("#us-sendbtn").prop("checked", USEROPTS.chatbtn);
$("#us-no-emotes").prop("checked", USEROPTS.no_emotes);
$("#us-strip-image").prop("checked", USEROPTS.strip_image);
$("#us-chat-tab-method").val(USEROPTS.chat_tab_method);
$("#us-modflair").prop("checked", USEROPTS.modhat);
$("#us-shadowchat").prop("checked", USEROPTS.show_shadowchat);
$("#us-show-ip-in-tooltip").prop("checked", USEROPTS.show_ip_in_tooltip);
formatScriptAccessPrefs();
$("a[href='#us-general']").trigger("click");
$("#useroptions").modal();
}
function saveUserOptions() {
USEROPTS.theme = $("#us-theme").val();
createCookie("cytube-theme", USEROPTS.theme, 1000);
USEROPTS.layout = $("#us-layout").val();
USEROPTS.ignore_channelcss = $("#us-no-channelcss").prop("checked");
USEROPTS.ignore_channeljs = $("#us-no-channeljs").prop("checked");
USEROPTS.show_ip_in_tooltip = $("#us-show-ip-in-tooltip").prop("checked");
USEROPTS.synch = $("#us-synch").prop("checked");
USEROPTS.sync_accuracy = parseFloat($("#us-synch-accuracy").val()) || 2;
USEROPTS.hidevid = $("#us-hidevideo").prop("checked");
USEROPTS.qbtn_hide = $("#us-playlistbuttons").prop("checked");
USEROPTS.qbtn_idontlikechange = $("#us-oldbtns").prop("checked");
USEROPTS.default_quality = $("#us-default-quality").val();
USEROPTS.peertube_risk = $("#us-peertube").prop("checked");
USEROPTS.show_timestamps = $("#us-chat-timestamp").prop("checked");
USEROPTS.sort_rank = $("#us-sort-rank").prop("checked");
USEROPTS.sort_afk = $("#us-sort-afk").prop("checked");
USEROPTS.blink_title = $("#us-blink-title").val();
USEROPTS.boop = $("#us-ping-sound").val();
USEROPTS.notifications = $("#us-notifications").val();
USEROPTS.chatbtn = $("#us-sendbtn").prop("checked");
USEROPTS.no_emotes = $("#us-no-emotes").prop("checked");
USEROPTS.strip_image = $("#us-strip-image").prop("checked");
USEROPTS.chat_tab_method = $("#us-chat-tab-method").val();
if (CLIENT.rank >= 2) {
USEROPTS.modhat = $("#us-modflair").prop("checked");
USEROPTS.show_shadowchat = $("#us-shadowchat").prop("checked");
}
storeOpts();
applyOpts();
}
function storeOpts() {
for(var key in USEROPTS) {
setOpt(key, USEROPTS[key]);
}
}
function applyOpts() {
if ($("#usertheme").attr("href") !== USEROPTS.theme) {
var old = $("#usertheme").attr("id", "usertheme_old");
var theme = USEROPTS.theme;
if (theme === "default") {
theme = DEFAULT_THEME;
}
$("").attr("rel", "stylesheet")
.attr("type", "text/css")
.attr("id", "usertheme")
.attr("href", theme)
.attr("onload", "$('#usertheme_old').remove()")
.appendTo($("head"));
fixWeirdButtonAlignmentIssue();
}
switch (USEROPTS.layout) {
case "synchtube-fluid":
fluidLayout();
case "synchtube":
synchtubeLayout();
break;
case "fluid":
fluidLayout();
break;
case "hd":
hdLayout();
break;
default:
compactLayout();
break;
}
if(USEROPTS.hidevid) {
removeVideo();
}
$("#chatbtn").remove();
if(USEROPTS.chatbtn) {
var btn = $("").addClass("btn btn-default btn-block")
.text("Send")
.attr("id", "chatbtn")
.appendTo($("#chatwrap"));
btn.on('click', function() {
if($("#chatline").val().trim()) {
socket.emit("chatMsg", {
msg: $("#chatline").val(),
meta: {}
});
$("#chatline").val("");
}
});
}
if (USEROPTS.modhat) {
$("#modflair").removeClass("label-default")
.addClass("label-success");
} else {
$("#modflair").removeClass("label-success")
.addClass("label-default");
}
if (USEROPTS.notifications !== "never") {
if ("Notification" in window) {
Notification.requestPermission().then(function(permission) {
if (permission !== "granted") {
USEROPTS.notifications = "never";
}
});
}
else {
USEROPTS.notifications = "never";
}
}
}
function parseTimeout(t) {
var m;
if (m = t.match(/^(\d+):(\d+):(\d+)$/)) {
// HH:MM:SS
return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10);
} else if (m = t.match(/^(\d+):(\d+)$/)) {
// MM:SS
return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
} else if (m = t.match(/^(\d+)$/)) {
// Seconds
return parseInt(m[1], 10);
} else {
throw new Error("Invalid timeout value '" + t + "'");
}
}
function showPollMenu() {
$("#pollwrap .poll-menu").remove();
var menu = $("").addClass("well poll-menu")
.prependTo($("#pollwrap"));
$("").addClass("btn btn-sm btn-danger pull-right")
.text("Cancel")
.appendTo(menu)
.on('click', function() {
menu.remove();
});
$("").text("Title").appendTo(menu);
var title = $("").addClass("form-control")
.attr("maxlength", "255")
.attr("type", "text")
.appendTo(menu);
$("").text("Timeout (optional)").appendTo(menu);
$("").text("If you specify a timeout, the poll will automatically " +
"be closed after that amount of time. You can either " +
"specify the number of seconds or use the format " +
"minutes:seconds. Examples: 90 (90 seconds), 5:30 " +
"(5 minutes, 30 seconds)")
.addClass("text-muted")
.appendTo(menu);
var timeout = $("").addClass("form-control")
.attr("type", "text")
.appendTo(menu);
var timeoutError = null;
var checkboxOuter = $("").addClass("checkbox").appendTo(menu);
var lbl = $("").text("Hide poll results until it closes")
.appendTo(checkboxOuter);
var hidden = $("").attr("type", "checkbox")
.prependTo(lbl);
var retainVotesOuter = $("").addClass("checkbox").appendTo(menu);
var retainVotesLbl = $("").text("Keep poll vote after user leaves")
.appendTo(retainVotesOuter);
var retainVotes = $("").attr("type", "checkbox")
.prependTo(retainVotesLbl);
$("").text("Options").appendTo(menu);
var addbtn = $("").addClass("btn btn-sm btn-default")
.text("Add Option")
.appendTo(menu);
function addOption() {
$("").addClass("form-control")
.attr("type", "text")
.attr("maxlength", "255")
.addClass("poll-menu-option")
.insertBefore(addbtn);
}
addbtn.click(addOption);
addOption();
addOption();
$("").addClass("btn btn-default btn-block")
.text("Open Poll")
.appendTo(menu)
.on('click', function() {
var t = timeout.val().trim();
if (t) {
try {
t = parseTimeout(t);
} catch (e) {
if (timeoutError) {
timeoutError.remove();
}
timeoutError = $("").addClass("text-danger").text(e.message);
timeoutError.insertAfter(timeout);
timeout.focus();
return;
}
} else {
t = undefined;
}
var opts = [];
menu.find(".poll-menu-option").each(function() {
if($(this).val() != "")
opts.push($(this).val());
});
socket.emit("newPoll", {
title: title.val(),
opts: opts,
obscured: hidden.prop("checked"),
retainVotes: retainVotes.prop("checked"),
timeout: t
}, function ack(result) {
if (result.error) {
modalAlert({
title: 'Error creating poll',
textContent: result.error.message
});
} else {
menu.remove();
}
});
});
}
function scrollChat() {
scrollAndIgnoreEvent($("#messagebuffer").prop("scrollHeight"));
$("#newmessages-indicator").remove();
}
function scrollAndIgnoreEvent(top) {
IGNORE_SCROLL_EVENT = true;
$("#messagebuffer").scrollTop(top);
}
function hasPermission(key) {
if(key.indexOf("playlist") == 0 && CHANNEL.openqueue) {
var key2 = "o" + key;
var v = CHANNEL.perms[key2];
if(typeof v == "number" && CLIENT.rank >= v) {
return true;
}
}
var v = CHANNEL.perms[key];
if(typeof v != "number") {
return false;
}
return CLIENT.rank >= v;
}
function setVisible(selector, bool) {
// I originally added this check because of a race condition
// Now it seems to work without but I don't trust it
if($(selector) && $(selector).attr("id") != selector.substring(1)) {
setTimeout(function() {
setVisible(selector, bool);
}, 100);
return;
}
var disp = bool ? "" : "none";
$(selector).css("display", disp);
}
function setParentVisible(selector, bool) {
var disp = bool ? "" : "none";
$(selector).parent().css("display", disp);
}
function handleModPermissions() {
$("#cs-chanranks-adm").attr("disabled", CLIENT.rank < 4);
$("#cs-chanranks-owner").attr("disabled", CLIENT.rank < 4);
/* update channel controls */
$("#cs-pagetitle").val(CHANNEL.opts.pagetitle);
$("#cs-pagetitle").attr("disabled", CLIENT.rank < 3);
$("#cs-externalcss").val(CHANNEL.opts.externalcss);
$("#cs-externalcss").attr("disabled", CLIENT.rank < 3);
$("#cs-externaljs").val(CHANNEL.opts.externaljs);
$("#cs-externaljs").attr("disabled", CLIENT.rank < 3);
$("#cs-chat_antiflood").prop("checked", CHANNEL.opts.chat_antiflood);
if ("chat_antiflood_params" in CHANNEL.opts) {
$("#cs-chat_antiflood_burst").val(CHANNEL.opts.chat_antiflood_params.burst);
$("#cs-chat_antiflood_sustained").val(CHANNEL.opts.chat_antiflood_params.sustained);
}
$("#cs-show_public").prop("checked", CHANNEL.opts.show_public);
$("#cs-show_public").attr("disabled", CLIENT.rank < 3);
$("#cs-password").val(CHANNEL.opts.password || "");
$("#cs-password").attr("disabled", CLIENT.rank < 3);
$("#cs-enable_link_regex").prop("checked", CHANNEL.opts.enable_link_regex);
$("#cs-afk_timeout").val(CHANNEL.opts.afk_timeout);
$("#cs-allow_voteskip").prop("checked", CHANNEL.opts.allow_voteskip);
$("#cs-voteskip_ratio").val(CHANNEL.opts.voteskip_ratio);
$("#cs-allow_dupes").prop("checked", CHANNEL.opts.allow_dupes);
$("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned);
$("#cs-block_anonymous_users").prop("checked", CHANNEL.opts.block_anonymous_users);
$("#cs-allow_ascii_control").prop("checked", CHANNEL.opts.allow_ascii_control);
$("#cs-playlist_max_per_user").val(CHANNEL.opts.playlist_max_per_user || 0);
$("#cs-playlist_max_duration_per_user").val(formatTime(CHANNEL.opts.playlist_max_duration_per_user));
$("#cs-new_user_chat_delay").val(formatTime(CHANNEL.opts.new_user_chat_delay || 0));
$("#cs-new_user_chat_link_delay").val(formatTime(CHANNEL.opts.new_user_chat_link_delay || 0));
$("#cs-maxlength").val(formatTime(CHANNEL.opts.maxlength));
$("#cs-csstext").val(CHANNEL.css);
$("#cs-jstext").val(CHANNEL.js);
$("#cs-motdtext").val(CHANNEL.motd);
setParentVisible("a[href='#cs-motdeditor']", hasPermission("motdedit"));
setParentVisible("a[href='#cs-permedit']", CLIENT.rank >= 3);
setParentVisible("a[href='#cs-banlist']", hasPermission("ban"));
setParentVisible("a[href='#cs-csseditor']", CLIENT.rank >= 3);
setParentVisible("a[href='#cs-jseditor']", CLIENT.rank >= 3);
setParentVisible("a[href='#cs-chatfilters']", hasPermission("filteredit"));
setParentVisible("a[href='#cs-emotes']", hasPermission("emoteedit"));
setParentVisible("a[href='#cs-chanranks']", CLIENT.rank >= 3);
setParentVisible("a[href='#cs-chanlog']", CLIENT.rank >= 3);
$("#cs-chatfilters-import").attr("disabled", !hasPermission("filterimport"));
$("#cs-emotes-import").attr("disabled", !hasPermission("filterimport"));
}
function handlePermissionChange() {
if(CLIENT.rank >= 2) {
handleModPermissions();
}
$("#qlockbtn").attr("disabled", !hasPermission("playlistlock"));
setVisible("#showchansettings", CLIENT.rank >= 2);
setVisible("#playlistmanagerwrap", CLIENT.rank >= 1);
setVisible("#modflair", CLIENT.rank >= 2);
setVisible("#guestlogin", CLIENT.rank < 0);
setVisible("#chatline", CLIENT.rank >= 0);
setVisible("#queue", hasPermission("seeplaylist"));
setVisible("#plmeta", hasPermission("seeplaylist"));
$("#getplaylist").attr("disabled", !hasPermission("seeplaylist"));
setVisible("#showplaylistmanager", hasPermission("seeplaylist"));
setVisible("#showmediaurl", hasPermission("playlistadd"));
setVisible("#showcustomembed", hasPermission("playlistaddcustom"));
$("#queue_next").attr("disabled", !hasPermission("playlistnext"));
if(hasPermission("playlistadd") ||
hasPermission("playlistmove") ||
hasPermission("playlistjump") ||
hasPermission("playlistdelete") ||
hasPermission("settemp")) {
if(USEROPTS.first_visit && $("#plonotification").length == 0) {
var al = makeAlert("Playlist Options", [
"From the Options menu, you can choose to automatically",
" hide the buttons on each entry (and show them when",
" you right click). You can also choose to use the old",
" style of playlist buttons.",
"
"].join(""))
.attr("id", "plonotification")
.insertAfter($("#queuefail"));
al.find(".close").remove();
$("").addClass("btn btn-primary")
.text("Dismiss")
.appendTo(al.find(".alert"))
.on('click', function() {
USEROPTS.first_visit = false;
storeOpts();
al.hide("fade", function() {
al.remove();
});
});
}
}
if(hasPermission("playlistmove")) {
$("#queue").sortable("enable");
$("#queue").addClass("queue_sortable");
}
else {
$("#queue").sortable("disable");
$("#queue").removeClass("queue_sortable");
}
setVisible("#clearplaylist", hasPermission("playlistclear"));
setVisible("#shuffleplaylist", hasPermission("playlistshuffle"));
if (!hasPermission("addnontemp")) {
$(".add-temp").prop("checked", true);
$(".add-temp").attr("disabled", true);
} else {
$(".add-temp").attr("disabled", false);
}
fixWeirdButtonAlignmentIssue();
setVisible("#newpollbtn", hasPermission("pollctl"));
$("#voteskip").attr("disabled", !hasPermission("voteskip") ||
!CHANNEL.opts.allow_voteskip);
$("#pollwrap .active").find(".btn-danger").remove();
if(hasPermission("pollctl")) {
var poll = $("#pollwrap .active");
if(poll.length > 0) {
$("").addClass("btn btn-danger pull-right")
.text("End Poll")
.insertAfter(poll.find(".close"))
.on('click', function() {
socket.emit("closePoll");
});
}
}
var poll = $("#pollwrap .active");
if(poll.length > 0) {
poll.find(".btn").attr("disabled", !hasPermission("pollvote"));
}
var users = $("#userlist").children();
for(var i = 0; i < users.length; i++) {
addUserDropdown($(users[i]));
}
$("#chatline").attr("disabled", !hasPermission("chat"));
if (!hasPermission("chat")) {
$("#chatline").attr("placeholder", "Chat permissions are restricted on this channel");
} else {
$("#chatline").attr("placeholder", "");
}
rebuildPlaylist();
}
function fixWeirdButtonAlignmentIssue() {
// Weird things happen to the alignment in chromium when I toggle visibility
// of the above buttons
// This fixes it?
var wtf = $("#videocontrols").removeClass("pull-right");
setTimeout(function () {
wtf.addClass("pull-right");
}, 1);
}
/* search stuff */
function clearSearchResults() {
$("#library").html("");
$("#search_clear").remove();
var p = $("#library").data("paginator");
if(p) {
p.paginator.html("");
}
}
function addLibraryButtons(li, item, source) {
var btns = $("").addClass("btn-group")
.addClass("pull-left")
.prependTo(li);
var id = item.id;
var type = item.type;
if(hasPermission("playlistadd")) {
if(hasPermission("playlistnext")) {
$("").addClass("btn btn-xs btn-default")
.text("Next")
.on('click', function() {
socket.emit("queue", {
id: id,
pos: "next",
type: type,
temp: $(".add-temp").prop("checked")
});
})
.appendTo(btns);
}
$("").addClass("btn btn-xs btn-default")
.text("End")
.on('click', function() {
socket.emit("queue", {
id: id,
pos: "end",
type: type,
temp: $(".add-temp").prop("checked")
});
})
.appendTo(btns);
}
if(hasPermission("deletefromchannellib") && source === "library") {
$("").addClass("btn btn-xs btn-danger")
.html("")
.on('click', function() {
socket.emit("uncache", {
id: id
});
li.hide("fade", function() {
li.remove();
});
})
.appendTo(btns);
}
}
/* queue stuff */
var AsyncQueue = function () {
this._q = [];
this._lock = false;
this._tm = 0;
};
AsyncQueue.prototype.next = function () {
if (this._q.length > 0) {
if (!this.lock())
return;
var item = this._q.shift();
var fn = item[0], tm = item[1];
this._tm = Date.now() + item[1];
fn(this);
}
};
AsyncQueue.prototype.lock = function () {
if (this._lock) {
if (this._tm > 0 && Date.now() > this._tm) {
this._tm = 0;
return true;
}
return false;
}
this._lock = true;
return true;
};
AsyncQueue.prototype.release = function () {
var self = this;
if (!self._lock)
return false;
self._lock = false;
self.next();
return true;
};
AsyncQueue.prototype.queue = function (fn) {
var self = this;
self._q.push([fn, 20000]);
self.next();
};
AsyncQueue.prototype.reset = function () {
this._q = [];
this._lock = false;
};
var PL_ACTION_QUEUE = new AsyncQueue();
// Because jQuery UI does weird things
// 2017-03-26: Does it really though? I have no idea if this is still needed.
function playlistFind(uid) {
var children = document.getElementById("queue").children;
for(var i in children) {
if(typeof children[i].className != "string")
continue;
if(children[i].className.split(" ").indexOf("pluid-" + uid) > 0)
return children[i];
}
return false;
}
function playlistMove(from, after, cb) {
var lifrom = $(".pluid-" + from);
if(lifrom.length == 0) {
cb(false);
return;
}
var q = $("#queue");
if(after === "prepend") {
lifrom.hide("blind", function() {
lifrom.detach();
lifrom.prependTo(q);
lifrom.show("blind", cb);
});
}
else if(after === "append") {
lifrom.hide("blind", function() {
lifrom.detach();
lifrom.appendTo(q);
lifrom.show("blind", cb);
});
}
else {
var liafter = $(".pluid-" + after);
if(liafter.length == 0) {
cb(false);
return;
}
lifrom.hide("blind", function() {
lifrom.detach();
lifrom.insertAfter(liafter);
lifrom.show("blind", cb);
});
}
}
function checkYP(id) {
if (!/^(PL[a-zA-Z0-9_-]{32}|PL[A-F0-9]{16}|OLA[a-zA-Z0-9_-]{38})$/.test(id)) {
throw new Error('Invalid YouTube Playlist ID. Note that only regular user-created playlists are supported.');
}
}
function parseMediaLink(url) {
function parseShortCode(url){
const type = url.slice(0,2);
let id = url.slice(3);
switch(type){
// So we still trim DailyMotion URLs
case 'dm':
return { type, id: id.match(/([^\?_]+)/)[1] };
// Raw files need to keep the query string
case 'fi':
case 'cm':
return { type, id };
case 'yp':
checkYP(id);
return { type, id };
// Generic for the rest.
default:
return { type, id: id.match(/([^\?]+)/)[1] };
}
}
const indeterminable = ()=>{
return new Error(
'Could not determine video type. ' +
'Check https://git.io/fjtOK for a list of supported media providers.'
);
}
const livestream = ()=>{
return new Error(
'This format of Livestream.com link is not supported. ' +
'You must use one that has the numeric account ID.'
);
}
if(typeof url != 'string') {
return {
id: null,
type: null
};
}
/* Shorthand URIs */
if(url.match(/^[a-z]{2}:/)){
return parseShortCode(url);
}
var data
try {
data = new URL(url);
} catch(err){
throw indeterminable();
}
if(data.protocol == 'rtmp:') {
return { type: 'rt', id: url };
}
if (data.pathname.match(/\.m3u8$/)) {
return { type: 'hl', id: url };
}
if (data.pathname.match(/\.json$/)) {
return { type: 'cm', id: url };
}
switch(data.hostname.replace('www.', '')){
case 'youtube.com':
if(data.pathname == '/watch'){
return { type: 'yt', id: data.searchParams.get('v') }
}
if(data.pathname.startsWith('/shorts/')){
return { type: 'yt', id: data.pathname.slice(8,19) }
}
if(data.pathname == '/playlist'){
checkYP(data.searchParams.get('list'));
return { type: 'yp', id: data.searchParams.get('list') }
}
case 'youtu.be':
return { type: 'yt', id: data.pathname.slice(1) }
case 'twitch.tv':
if(data.pathname.includes('/clip/')){
return { type: 'tc', id: data.pathname.split('/').pop() }
}
if(data.pathname.startsWith('/videos/')){
return { type: 'tv', id: `v${data.pathname.split('/').pop()}` }
}
return { type: 'tw', id: data.pathname.slice(1) }
case 'clips.twitch.tv':
return { type: 'tc', id: data.pathname.slice(1) }
case 'livestream.com':
if(data.pathname.startsWith('/accounts/')){
const pattern = new RegExp('/accounts/(?[0-9]+)/events/(?[0-9]+)');
const m = data.pathname.match(pattern);
if(m.groups.account && m.groups.event){
return { type: 'li', id: `${m.groups.account};${m.groups.event}` };
}
} else {
throw livestream();
}
case 'vimeo.com':
return { type: 'vi', id: data.pathname.slice(1) }
case 'dailymotion.com':
return { type: 'dm', id: data.pathname.slice('7') }
case 'soundcloud.com':
return { type: 'sc', id: url }
case 'streamable.com':
return { type: 'sb', id: data.pathname.slice(1) }
case 'docs.google.com':
case 'drive.google.com':
if(data.pathname.startsWith('/file/')){
return { type: 'gd', id: data.pathname.slice('8').split('/').shift() }
}
if(data.pathname == '/open'){
return { type: 'gd', id: data.searchParams.get('id') }
}
case 'bitchute.com':
if(data.pathname.startsWith('/video/')){
return { type: 'bc', id: `${data.pathname.slice(7).split('/').shift()}` }
}
case 'nicovideo.jp':
if(data.pathname.startsWith('/watch/')){
return { type: 'nv', id: `${data.pathname.slice(7).split('/').shift()}` }
}
case 'odysee.com':
const format = new RegExp('/@(?[^:]+)(?::\\w)?/(?