init commit

This commit is contained in:
rainbownapkin 2021-12-06 19:56:40 -05:00
parent ae639426d0
commit 7a491681cc
257 changed files with 95524 additions and 80 deletions

View file

@ -0,0 +1,88 @@
extends layout.pug
block content
if !loggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-danger.messagebox.center
strong Authorization Required
p You must be <a href="/login">logged in</a> to view this page.
else
.col-lg-6.col-md-6
h3 My Channels
if deleteChannelError
.alert.alert-danger.center.messagebox
strong Channel Deletion Failed
p= deleteChannelError
if channels.length == 0
.center
strong You haven't registered any channels
else
table.table.table-bordered
thead
tr
th Channel
tbody
for c in channels
tr
th
form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="delete_channel")
input(type="hidden", name="name", value=c.name)
button.btn.btn-xs.btn-danger(type="submit") Delete
span.glyphicon.glyphicon-trash
a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name
.col-lg-6.col-md-6
h3 Register a new channel
if newChannelError
.alert.alert-danger.messagebox.center
strong Channel Registration Failed
p= newChannelError
form(action="/account/channels", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="new_channel")
.form-group
label.control-label(for="channelname") Channel URL
.input-group
span.input-group-addon #{baseUrl}/#{channelPath}/
input#channelname.form-control(type="text", name="name", maxlength="30", onkeyup="checkChannel()")
p#validate_channel.text-danger.pull-right
button#register.btn.btn-primary.btn-block(type="submit") Register
append footer
script(type='text/javascript').
function checkChannel(){
function nameIsInvalid(id){
if(/\s/.test(id)){
return 'Channel URL may not contain spaces';
}
if(id === ''){
return 'Channel URL must not be empty';
}
if(!/^[\w-]{1,30}$/.test(id)){
return 'Channel URL may only consist of a-z, A-Z, 0-9, - and _';
}
return false;
}
var box = $("#channelname");
var value = box.val();
var lastkey = Date.now();
box.data("lastkey", lastkey);
setTimeout(function () {
if (box.data("lastkey") !== lastkey || box.val() !== value) {
return;
}
if(nameIsInvalid(value)){
$('#validate_channel').text(nameIsInvalid(value))
.parent().addClass('has-error').removeClass('has-success');
$('#register').addClass('disabled');
} else {
$('#validate_channel').text('')
.parent().addClass('has-success').removeClass('has-error');
$('#register').removeClass('disabled');
}
}, 200);
}

View file

@ -0,0 +1,51 @@
extends layout.pug
block content
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
if internalError
h2 Error
p
| Your account deletion request could not be processed due to an internal
| error. Please try again later and ask an administrator for assistance
| if the problem persists.
else if !loggedIn
h2 Authentication Required
p
| You must&nbsp;
a(href="/login") log in
| &nbsp; before requesting deletion of your account.
else if authFailed
h2 Authentication failed
p= reason
else if channelCount > 0
h2 Delete Account
p
| Your account cannot be deleted because you have one or more channels
| registered. In order to delete your account, you must first&nbsp;
a(href="/account/channels") delete them
| &nbsp;or ask an administrator to transfer ownership of these channels
| to another account.
else
h2 Delete Account
p
strong Submitting this form will initiate permanent deletion of your account.&nbsp;
| After 7 days, your account will be permanently deleted and unrecoverable.
| During this time, you will not be able to log in, but you can ask an
| administrator to restore your account if the deletion was requested in error.
| Please confirm your password to continue.
form(action="/account/delete", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group(class=wrongPassword ? "has-error" : "")
label.control-label(for="password") Password
input#password.form-control(type="password", name="password")
if wrongPassword
p.text-danger.
Password was incorrect
.checkbox
label
input#confirm-delete(type="checkbox", name="confirmed")
| I acknowledge that by submitting this request, my account will be permanently deleted unrecoverably
if missingConfirmation
p.text-danger.
You must check the box to confirm you want to delete your account
button.btn.btn-danger.btn-block(type="submit") Delete Account

View file

@ -0,0 +1,11 @@
extends layout.pug
block content
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
h2 Account Deleted
p.
Your account has been flagged for deletion. After 7 days, your user data
will be premanently deleted from the database. During this time, you will
not be able to log in, but you can ask an administrator for help if your
deletion request was in error. After 7 days, your account will no longer
be recoverable.

View file

@ -0,0 +1,83 @@
extends layout.pug
block content
if !loggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-danger.messagebox.center
strong Authorization Required
p You must be <a href="/login">logged in</a> to view this page.
else
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
if successMessage
.alert.alert-success.center
p= successMessage
else if errorMessage
.alert.alert-danger.center
p= errorMessage
h3 Change Password
form(action="/account/edit", method="post", onsubmit="return validatePasswordChange()")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="change_password")
.form-group
label.control-label(for="username") Username
input#username.form-control(type="text", name="name", value=loginName, disabled=true)
.form-group
label.control-label(for="oldpassword") Current Password
input#oldpassword.form-control(type="password", name="oldpassword")
.form-group
label.control-label(for="newpassword") New Password
input#newpassword.form-control(type="password", name="newpassword")
.form-group
label.control-label(for="newpassword_confirm") Confirm New Password
input#newpassword_confirm.form-control(type="password", name="newpassword_confirm")
button#changepassbtn.btn.btn-danger.btn-block(type="submit") Change Password
hr
h3 Change Email
form(action="/account/edit", method="post", onsubmit="return submitEmail()")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="change_email")
.form-group
label.control-label(for="username2") Username
input#username2.form-control(type="text", name="name", value=loginName, disabled=true)
.form-group
label.control-label(for="password2") Password
input#password2.form-control(type="password", name="password")
.form-group
label.control-label(for="email") New Email
input#email.form-control(type="email", name="email")
button#changeemailbtn.btn.btn-danger.btn-block(type="submit") Change Email
append footer
script(type="text/javascript").
function validatePasswordChange() {
var pw = $("#newpassword").val();
var pwc = $("#newpassword_confirm").val();
$("#passwordempty").remove();
$("#passwordmismatch").remove();
if (pw === '') {
$("#newpassword").parent().addClass("has-error");
$("<p/>").addClass("text-danger")
.attr("id", "passwordempty")
.text("Password must not be empty")
.insertAfter($("#newpassword"));
return false;
} else {
if (pw !== pwc) {
$("#newpassword_confirm").parent().addClass("has-error");
$("#newpassword").parent().addClass("has-error");
$("<p/>").addClass("text-danger")
.attr("id", "passwordmismatch")
.text("Passwords do not match")
.insertAfter($("#newpassword_confirm"));
return false;
} else {
$("#username").attr("disabled", false);
return true;
}
}
}
function submitEmail() {
$("#username2").attr("disabled", false);
return true;
}

View file

@ -0,0 +1,16 @@
extends layout.pug
block content
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
h3 Recover Password
if recovered
.alert.alert-success.center.messagebox
strong Your password has been changed
p Your account has been assigned the temporary password <code>#{recoverPw}</code>. You may now use this password to log in and choose a new password by visiting the <a href="/account/edit">change password/email</a> page.
else if confirm
form(role="form", method="POST")
button.btn.btn-primary.btn-block(type="submit") Click here to reset password
else
.alert.alert-danger.center.messagebox
strong Password recovery failed
p= recoverErr

View file

@ -0,0 +1,22 @@
extends layout.pug
block content
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
h3 Reset Password
if reset
.alert.alert-success.center.messagebox
strong Password reset request sent
p Please check #{resetEmail} for your recovery link.
else if resetErr
.alert.alert-danger.center.messagebox
strong Error
p= resetErr
form(action="/account/passwordreset", method="post", role="form")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group
label.control-label(for="username") Username
input#username.form-control(type="text", name="name")
.form-group
label.control-label(for="email") Email address
input#email.form-control(type="email", name="email")
button.btn.btn-primary.btn-block(type="submit") Send reset request

View file

@ -0,0 +1,62 @@
extends layout.pug
block content
if !loggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-danger.messagebox.center
strong Authorization Required
p You must be <a href="/login">logged in</a> to view this page.
else
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
h3 Profile
if profileError
.alert.alert-danger.center.messagebox
strong Profile Error
p= profileError
.profile-box.linewrap(style="position: inherit; z-index: auto;")
img.profile-image(src=profileImage)
strong= loginName
p= profileText
h3 Edit Profile
form(action="/account/profile", method="post", role="form")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group
label.control-label(for="profileimage") Image
input#profileimage.form-control(type="text", name="image", maxlength="255")
.form-group
label.control-label(for="profiletext") Text
textarea#profiletext.form-control(cols="10", name="text", maxlength="255")= profileText
button.btn.btn-primary.btn-block(type="submit") Save
append footer
script(type="text/javascript").
var $profileImage = $("#profileimage");
$profileImage.val("#{profileImage}");
var hasError = false;
function validateImage() {
var value = $profileImage.val().trim();
$profileImage.val(value);
if (!/^$|^https:/.test(value)) {
hasError = true;
$profileImage.parent().addClass("has-error");
var $error = $("#profileimage-error");
if ($error.length === 0) {
$error = $("<p/>")
.attr({ id: "profileimage-error" })
.addClass("text-danger")
.html("Profile image must be a URL beginning with <code>https://</code>")
.insertAfter($profileImage);
}
} else {
hasError = false;
$profileImage.parent().removeClass("has-error");
$("#profileimage-error").remove();
}
}
$("form").submit(function (event) {
validateImage();
if (hasError) {
event.preventDefault();
}
});

131
templates/acp.pug Normal file
View file

@ -0,0 +1,131 @@
doctype html
html(lang="en")
head
include head
+head()
link(rel="stylesheet", type="text/css", href="/css/acp.css")
body
#wrap
nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation")
include nav
+navheader()
#nav-collapsible.collapse.navbar-collapse
ul.nav.navbar-nav
+navdefaultlinks()
li#nav-acp-section.dropdown
a#nav-acp-dd-toggle.dropdown-toggle(data-toggle="dropdown", href="javascript:void(0)") Menu
span.caret
ul.dropdown-menu
+navloginlogout()
section#mainpage
.container
.row
#acp-logview.acp-panel.col-md-12(style="display: none")
h3 Log Viewer
.input-group
div.input-group-btn
button#acp-syslog-btn.btn.btn-default Syslog
button#acp-errlog-btn.btn.btn-default Error log
button#acp-httplog-btn.btn.btn-default HTTP log
input#acp-chanlog-name.form-control(type="text", placeholder="Channel name")
pre#acp-log
#acp-announcements.acp-panel.col-md-6.col-md-offset-3(style="display: none")
h3 Announcements
h3 New Announcement
div
form.form-horizontal(action="javascript:void(0)", role="form")
.form-group
label.control-label.col-sm-2(for="acp-announce-title") Title
.col-sm-10
input#acp-announce-title.form-control(type="text")
.form-group
label.control-label.col-sm-2(for="acp-announce-content") Text
.col-sm-10
textarea#acp-announce-content.form-control(type="text", rows="10")
.form-group
.col-sm-10.col-sm-offset-2
button#acp-announce-submit.btn.btn-primary Announce
#acp-global-bans.acp-panel.col-md-12(style="display: none")
h3 Global Bans
table.table.table-striped.table-bordered
thead
tr
th
th IP Address
th Note
h3 New Global Ban
div(style="max-width: 50%")
form.form-horizontal(action="javascript:void(0)", role="form")
.form-group
label.control-label.col-sm-3(for="acp-gban-ip") IP Address
.col-sm-9
input#acp-gban-ip.form-control(type="text")
.form-group
label.control-label.col-sm-3(for="acp-gban-note") Note
.col-sm-9
input#acp-gban-note.form-control(type="text")
.form-group
.col-sm-9.col-sm-offset-3
button#acp-gban-submit.btn.btn-danger Add ban
#acp-user-lookup.acp-panel.col-md-12(style="display: none")
h3 Users
.input-group(style="max-width: 50%")
input#acp-ulookup-query.form-control(type="text")
span.input-group-btn
button#acp-ulookup-btn-name.btn.btn-default(data-field="name") Search Name
span.input-group-btn
button#acp-ulookup-btn-email.btn.btn-default(data-field="email") Search Email
table.table.table-bordered.table-striped(style="margin-top: 20px")
thead
tr
th.sort(data-key="id") ID
th.sort(data-key="name") Name
th.sort(data-key="global_rank") Rank
th.sort(data-key="email") Email
th Actions
#acp-channel-lookup.acp-panel.col-md-12(style="display: none")
h3 Channels
form.form-inline(action="javascript:void(0)", role="form")
.form-group
input#acp-clookup-value.form-control(type="text", placeholder="Name")
.form-group
select#acp-clookup-field.form-control
option(value="name") Channel Name
option(value="owner") Channel Owner
button#acp-clookup-submit.btn.btn-default Search
table.table.table-bordered.table-striped(style="margin-top: 20px")
thead
tr
th.sort(data-key="id") ID
th.sort(data-key="name") Name
th.sort(data-key="owner") Owner
th.sort(data-key="last_loaded") Last Loaded
th.sort(data-key="owner_last_seen") Owner Last Seen
th Control
#acp-loaded-channels.acp-panel.col-md-12(style="display: none")
h3 Loaded Channels
button#acp-lchannels-refresh.btn.btn-default Refresh
table.table.table-bordered.table-striped(style="margin-top: 20px")
thead
tr
th Title
th Usercount
th Now Playing
th Registered
th Public
th Control
#acp-eventlog.acp-panel.col-md-12(style="display: none")
h3 Event Log
strong Filter event types
select#acp-eventlog-filter.form-control(multiple="multiple", style="max-width: 25%")
button#acp-eventlog-refresh.btn.btn-default Refresh
pre#acp-eventlog-text
include footer
+footer()
script(src=sioSource)
script(type="text/javascript").
window.IO_SERVERS = !{ioServers};
script(src="/js/util.js")
script(src="/js/paginator.js")
script(src="/js/acp.js")

262
templates/channel.pug Normal file
View file

@ -0,0 +1,262 @@
doctype html
html(lang="en")
head
include head
+head()
link(href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css", rel="stylesheet")
link(rel="stylesheet", href="/css/video-js.css")
link(rel="stylesheet", href="/css/videojs-resolution-switcher.css")
body
#wrap
nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation")
include nav
+navheader()
#nav-collapsible.collapse.navbar-collapse
ul.nav.navbar-nav
+navdefaultlinks()
li: a(href="javascript:void(0)", onclick="javascript:showUserOptions()") Options
li: a#showchansettings(href="javascript:void(0)", onclick="javascript:showChannelSettings()") Channel Settings
li.dropdown
a.dropdown-toggle(href="#", data-toggle="dropdown") Layout
b.caret
ul.dropdown-menu
li: a(href="#" onclick="javascript:chatOnly()") Chat Only
li: a(href="#" onclick="javascript:removeVideo(event)") Remove Video
+navsuperadmin(true)
+navloginlogout()
section#mainpage
.container
#motdrow.row
.col-lg-12.col-md-12
#motdwrap.well
button#togglemotd.close.pull-right(type="button")
span.glyphicon.glyphicon-minus
#motd
.clear
#announcements.row
#drinkbarwrap.row
#drinkbar.col-lg-12.col-md-12
h1#drinkcount
#main.row
#chatwrap.col-lg-5.col-md-5
#chatheader
i#userlisttoggle.glyphicon.glyphicon-chevron-down.pull-left.pointer(title="Show/Hide Userlist")
span#usercount.pointer Not Connected
span#modflair.label.label-default.pull-right.pointer Name Color
#userlist
#messagebuffer.linewrap
form(action="javascript:void(0)")
input#chatline.form-control(type="text", maxlength="320", style="display: none")
#guestlogin.input-group
span.input-group-addon Guest login
input#guestname.form-control(type="text", placeholder="Name")
#videowrap.col-lg-7.col-md-7
p#videowrap-header
span#resize-video-smaller.glyphicon.glyphicon-minus.pointer(title="Make the video smaller")
span#resize-video-larger.glyphicon.glyphicon-plus.pointer(title="Make the video larger")
span#currenttitle Nothing Playing
.embed-responsive.embed-responsive-16by9
#ytapiplayer.embed-responsive-item
#controlsrow.row
#leftcontrols.col-lg-5.col-md-5
button#newpollbtn.btn.btn-sm.btn-default New Poll
button#emotelistbtn.btn.btn-sm.btn-default Emote List
#rightcontrols.col-lg-7.col-md-7
#plcontrol.btn-group
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
span.glyphicon.glyphicon-search
button#showmediaurl.btn.btn-sm.btn-default(title="Add video from URL", data-toggle="collapse", data-target="#addfromurl")
span.glyphicon.glyphicon-plus
button#showcustomembed.btn.btn-sm.btn-default(title="Embed a custom frame", data-toggle="collapse", data-target="#customembed")
span.glyphicon.glyphicon-th-large
button#showplaylistmanager.btn.btn-sm.btn-default(title="Manage playlists", data-toggle="collapse", data-target="#playlistmanager")
span.glyphicon.glyphicon-list
button#clearplaylist.btn.btn-sm.btn-default(title="Clear the playlist")
span.glyphicon.glyphicon-trash
button#shuffleplaylist.btn.btn-sm.btn-default(title="Shuffle the playlist")
span.glyphicon.glyphicon-sort
button#qlockbtn.btn.btn-sm.btn-danger(title="Playlist locked")
span.glyphicon.glyphicon-lock
#videocontrols.btn-group.pull-right
button#mediarefresh.btn.btn-sm.btn-default(title="Reload the video player")
span.glyphicon.glyphicon-retweet
button#fullscreenbtn.btn.btn-sm.btn-default(title="Make the video player fullscreen")
span.glyphicon.glyphicon-fullscreen
button#getplaylist.btn.btn-sm.btn-default(title="Retrieve playlist links")
span.glyphicon.glyphicon-link
button#voteskip.btn.btn-sm.btn-default(title="Voteskip")
span.glyphicon.glyphicon-step-forward
#playlistrow.row
#leftpane.col-lg-5.col-md-5
#leftpane-inner.row
#pollwrap.col-lg-12.col-md-12
#playlistmanagerwrap.col-lg-12.col-md-12
#rightpane.col-lg-7.col-md-7
#rightpane-inner.row
#searchcontrol.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#library_query.form-control(type="text", placeholder="Search query")
span.input-group-btn
button#library_search.btn.btn-default Library
span.input-group-btn
button#youtube_search.btn.btn-default YouTube
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
ul#library.videolist.col-lg-12.col-md-12
#addfromurl.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#mediaurl.form-control(type="text", placeholder="Media URL")
span.input-group-btn
button#queue_next.btn.btn-default Queue next
span.input-group-btn
button#queue_end.btn.btn-default Queue last
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
div#addfromurl-queue
#customembed.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#customembed-title.form-control(type="text", placeholder="Title (optional)")
span.input-group-btn
button#ce_queue_next.btn.btn-default Queue next
span.input-group-btn
button#ce_queue_end.btn.btn-default Queue last
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
| Paste the embed code below and click Next or At End.
| Acceptable embed codes are <code>&lt;iframe&gt;</code> and <code>&lt;object&gt;</code> tags. <strong>CUSTOM EMBEDS CANNOT BE SYNCHRONIZED.</strong>
textarea#customembed-content.input-block-level.form-control(rows="3")
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
.input-group
input#userpl_name.form-control(type="text", placeholder="Playlist Name")
span.input-group-btn
button#userpl_save.btn.btn-default Save
.checkbox
label
input.add-temp(type="checkbox")
| Add as temporary
ul#userpl_list.videolist
#queuefail.col-lg-12.col-md-12
.vertical-spacer
.col-lg-12.col-md-12
ul#queue.videolist
#plmeta
span#plcount 0 items
span#pllength 00:00:00
#resizewrap.row
.col-lg-5.col-md-5
#videowidth.col-lg-7.col-md-7
#sitefooter
include pagefooter
#useroptions.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
.modal-dialog
.modal-content
.modal-header
button.close(data-dismiss="modal", aria-hidden="true") &times;
h4 User Preferences
ul.nav.nav-tabs
li: a(href="#us-general", data-toggle="tab") General
li: a(href="#us-playback", data-toggle="tab") Playback
li: a(href="#us-chat", data-toggle="tab") Chat
li: a(href="#us-scriptcontrol", data-toggle="tab") Script Access
li: a(href="#us-mod", data-toggle="tab", style="") Moderator
.modal-body
.tab-content
include useroptions
+us-general()
+us-playback()
+us-chat()
+us-scripts()
+us-mod()
.modal-footer
button.btn.btn-primary(type="button", data-dismiss="modal", onclick="javascript:saveUserOptions()") Save
button.btn.btn-default(type="button", data-dismiss="modal") Close
#emotelist.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
.modal-dialog.modal-dialog-nonfluid
.modal-content
.modal-header
button.close(data-dismiss="modal", aria-hidden="true") &times;
h4 Emote List
.modal-body
.pull-left
input.emotelist-search.form-control(type="text", placeholder="Search")
.pull-right
.checkbox
label
input.emotelist-alphabetical(type="checkbox")
| Sort alphabetically
.emotelist-paginator-container
table.emotelist-table
tbody
.modal-footer
#channeloptions.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
.modal-dialog
.modal-content
.modal-header
button.close(data-dismiss="modal", aria-hidden="true") &times;
h4 Channel Settings
ul.nav.nav-tabs
li.active: a(href="#cs-miscoptions", data-toggle="tab") General Settings
li: a(href="#cs-adminoptions", data-toggle="tab") Admin Settings
li.dropdown
a#cs-edit-dd-toggle(href="#", data-toggle="dropdown") Edit
span.caret
ul.dropdown-menu
li: a(href="#cs-chatfilters", data-toggle="tab", onclick="javascript:socket.emit('requestChatFilters')") Chat Filters
li: a(href="#cs-emotes", data-toggle="tab") Emotes
li: a(href="#cs-motdeditor", data-toggle="tab", tabindex="-1") MOTD
li: a(href="#cs-csseditor", data-toggle="tab", tabindex="-1") CSS
li: a(href="#cs-jseditor", data-toggle="tab", tabindex="-1") Javascript
li: a(href="#cs-permedit", data-toggle="tab", tabindex="-1") Permissions
li: a(href="#cs-chanranks", data-toggle="tab", tabindex="-1", onclick="javascript:socket.emit('requestChannelRanks')") Moderators
li: a(href="#cs-banlist", data-toggle="tab", tabindex="-1", onclick="javascript:socket.emit('requestBanlist')") Ban list
li: a(href="#cs-chanlog", data-toggle="tab", onclick="javascript:socket.emit('readChanLog')") Log
.modal-body
.tab-content
include channeloptions
+miscoptions()
+adminoptions()
+motdeditor()
+csseditor()
+jseditor()
+banlist()
+recentjoins()
+chanranks()
+chatfilters()
+emotes()
+chanlog()
+permeditor()
.modal-footer
button.btn.btn-default(type="button", data-dismiss="modal") Close
#pmbar
include footer
+footer()
script(id="socketio-js", src=sioSource)
script(src="/js/data.js")
script(src="/js/util.js")
script(src="/js/tabcomplete.js")
script(src="/js/player.js")
script(src="/js/paginator.js")
script(src="/js/ui.js")
script(src="/js/callbacks.js")
script(defer, src="https://www.youtube.com/iframe_api")
script(defer, src="https://api.dmcdn.net/all.js")
script(defer, src="https://player.vimeo.com/api/player.js")
script(defer, src="/js/sc.js")
script(defer, src="/js/video.js")
script(defer, src="/js/videojs-contrib-hls.min.js")
script(defer, src="/js/videojs-resolution-switcher.js")
script(defer, src="/js/playerjs-0.0.12.js")
script(defer, src="/js/dash.all.min.js")
script(defer, src="/js/videojs-dash.js")
script(defer, src="https://player.twitch.tv/js/embed/v1.js")

View file

@ -0,0 +1,228 @@
mixin lcheckbox(id, label)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
.checkbox
input(type="checkbox", id=id)
mixin rcheckbox(id, label)
.form-group
.col-sm-8.col-sm-offset-4
.checkbox
label(for=id)
input(type="checkbox", id=id)
= label
mixin textbox(id, label, placeholder)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
if placeholder
input.form-control(id=id, type="text", placeholder=placeholder)
else
input.form-control(id=id, type="text")
mixin lcheckbox-auto(id, label)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
.checkbox
input.cs-checkbox(type="checkbox", id=id)
mixin rcheckbox-auto(id, label)
.form-group
.col-sm-8.col-sm-offset-4
.checkbox
label(for=id)
input.cs-checkbox(type="checkbox", id=id)
= label
mixin textbox-auto(id, label, placeholder)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
if placeholder
input.form-control.cs-textbox(id=id, type="text", placeholder=placeholder)
else
input.form-control.cs-textbox(id=id, type="text")
mixin textbox-timeinput-auto(id, label, placeholder)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
if placeholder
input.form-control.cs-textbox-timeinput(id=id, type="text", placeholder=placeholder)
else
input.form-control.cs-textbox-timeinput(id=id, type="text")
mixin miscoptions
#cs-miscoptions.tab-pane.active
h4 General Settings
form.form-horizontal(action="javascript:void(0)")
+rcheckbox-auto("cs-allow_voteskip", "Allow voteskip")
+rcheckbox-auto("cs-allow_dupes", "Allow duplicate videos on the playlist")
+textbox-auto("cs-voteskip_ratio", "Voteskip ratio", "0.5")
+textbox-auto("cs-maxlength", "Max video length", "HH:MM:SS")
+textbox-timeinput-auto("cs-playlist_max_duration_per_user", "Max total queue time per user", "HH:MM:SS")
+textbox-auto("cs-afk_timeout", "Auto-AFK Delay", "0 (disabled)")
.form-group
.col-sm-offset-4
h4 Chat Settings
form.form-horizontal(action="javascript:void(0)")
+rcheckbox-auto("cs-enable_link_regex", "Convert URLs in chat to links")
+rcheckbox-auto("cs-chat_antiflood", "Throttle chat")
+textbox-auto("cs-chat_antiflood_burst", "# of messages allowed before throttling")
+textbox-auto("cs-chat_antiflood_sustained", "# of messages (after burst) allowed per second")
+textbox-timeinput-auto("cs-new_user_chat_delay", "Delay before new accounts can chat", "0")
.form-group
.col-sm-8.col-sm-offset-4
span.text-info Restrictions to new accounts can be disabled by setting the delay to 0.
+textbox-timeinput-auto("cs-new_user_chat_link_delay", "Delay before new accounts can post links in chat", "0")
.form-group
.col-sm-8.col-sm-offset-4
span.text-info Changes are automatically saved.
mixin adminoptions
#cs-adminoptions.tab-pane
h4 Admin-Only Settings
form.form-horizontal(action="javascript:void(0)")
- var defname = `CyTube - /${channelPath}/${channelName}`
+textbox-auto("cs-pagetitle", "Page title", defname)
+textbox-auto("cs-password", "Password", "leave blank to disable")
+textbox-auto("cs-externalcss", "External CSS", "Stylesheet URL")
+textbox-auto("cs-externaljs", "External Javascript", "Script URL")
+rcheckbox-auto("cs-show_public", "List channel publicly")
+rcheckbox-auto("cs-torbanned", "Block connections from Tor")
+rcheckbox-auto("cs-block_anonymous_users", "Block anonymous users")
+rcheckbox-auto("cs-allow_ascii_control", "Allow ASCII control characters (e.g. newlines)")
+textbox-auto("cs-playlist_max_per_user", "Maximum # of videos per user")
.form-group
.col-sm-8.col-sm-offset-4
span.text-info Set to 0 for no limit
.form-group
.col-sm-8.col-sm-offset-4
span.text-info Changes are automatically saved.
mixin motdeditor
#cs-motdeditor.tab-pane
h4 MOTD editor
p The MOTD can be formatted using a subset of HTML. Tags which attempt to execute Javascript will be removed.
textarea.form-control#cs-motdtext(rows="10")
button.btn.btn-primary#cs-motdsubmit Save MOTD
mixin csseditor
#cs-csseditor.tab-pane
h4 CSS editor
p Maximum size 20KB. If more space is required, use the External CSS option under General Settings to link to an externally hosted stylesheet.
textarea.form-control#cs-csstext(rows="10")
button.btn.btn-primary#cs-csssubmit Save CSS
mixin jseditor
#cs-jseditor.tab-pane
h4 JS editor
p Maximum size 20KB. If more space is required, use the External JS option under General Settings to link to an externally hosted stylesheet.
textarea.form-control#cs-jstext(rows="10")
button.btn.btn-primary#cs-jssubmit Save JS
mixin banlist
#cs-banlist.tab-pane
h4 Ban list
table.table.table-striped
thead
tr
th Unban
th IP
th Name
th Banned by
mixin recentjoins
#cs-recentjoins.tab-pane
h4 Recent connections
table.table.table-striped
thead
tr
th Name
th Aliases
th Time
mixin chanranks
#cs-chanranks.tab-pane
h4 Moderator List
form.form-inline(action="javascript:void(0)", role="form")
.input-group
input#cs-chanranks-name.form-control(type="text", placeholder="Name")
span.input-group-btn
button#cs-chanranks-mod.btn.btn-success +Mod
button#cs-chanranks-adm.btn.btn-info +Admin
button#cs-chanranks-owner.btn.btn-info +Owner
table.table.table-striped
thead
tr
th Name
th Rank
mixin chatfilters
#cs-chatfilters.tab-pane
h4 Chat Filters
form.form-horizontal(action="javascript:void(0)", role="form")
+textbox("cs-chatfilters-newname", "Filter name")
+textbox("cs-chatfilters-newregex", "Filter regex")
.form-group
label.control-label.col-sm-4(for="cs-chatfilters-newflags") Flags
.col-sm-8
input#cs-chatfilters-newflags.form-control.cs-textbox(type="text", value="g")
+textbox("cs-chatfilters-newreplace", "Replacement")
.form-group
.col-sm-8.col-sm-offset-4
button#cs-chatfilters-newsubmit.btn.btn-primary Create Filter
table.table.table-striped.table-condensed
thead
tr
th Control
th Name
th Active
button#cs-chatfilters-export.btn.btn-default Export filter list
button#cs-chatfilters-import.btn.btn-default Import filter list
textarea#cs-chatfilters-exporttext.form-control(rows="5")
mixin emotes
#cs-emotes.tab-pane
h4 Emotes
form.form-horizontal(action="javascript:void(0)", role="form")
+textbox("cs-emotes-newname", "Emote name")
+textbox("cs-emotes-newimage", "Emote image")
.form-group
.col-sm-8.col-sm-offset-4
button#cs-emotes-newsubmit.btn.btn-primary Create Emote
form.form-inline
.form-group
input.emotelist-search.form-control(type="text", placeholder="Search")
.form-group
.checkbox
label
input.emotelist-alphabetical(type="checkbox")
| Sort alphabetically
.emotelist-paginator-container
table.emotelist-table.table.table-striped.table-condensed
thead
tr
th Delete
th Name
th Image
tbody
button#cs-emotes-export.btn.btn-default Export emote list
button#cs-emotes-import.btn.btn-default Import emote list
textarea#cs-emotes-exporttext.form-control(rows="5")
mixin chanlog
#cs-chanlog.tab-pane
h4 Channel Log
strong Filter Log:
select#cs-chanlog-filter.form-control(multiple="multiple")
pre#cs-chanlog-text
button.btn.btn-default#cs-chanlog-refresh Refresh
mixin permeditor
#cs-permedit.tab-pane

34
templates/contact.pug Normal file
View file

@ -0,0 +1,34 @@
extends layout.pug
mixin email(e, k)
button.btn.btn-xs.btn-default(onclick="showEmail(this, '"+e+"', '"+k+"')") Show Email
block content
.col-md-8.col-md-offset-2
h1 Contact
h3 Email
if contacts.length == 0
p No contacts listed.
else
each contact in contacts
strong= contact.name
p.text-muted= contact.title
+email(contact.email, contact.emkey)
br
hr
append footer
script(type="text/javascript").
function showEmail(btn, email, key) {
email = unescape(email);
key = unescape(key);
var dest = new Array(email.length);
for (var i = 0; i < email.length; i++) {
dest[i] = String.fromCharCode(email.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
email = dest.join("");
$("<a/>").attr("href", "mailto:" + email)
.text(email)
.insertBefore(btn);
$(btn).remove();
}

15
templates/csrferror.pug Normal file
View file

@ -0,0 +1,15 @@
extends layout.pug
block content
.col-md-12
.alert.alert-danger
h1 Invalid Session
p Your browser attempted to submit form data to <code>#{path}</code> with an invalid authentication token. This may be because:
ul
li Your session has expired
li Your request was missing the authentication token
li A malicious user has attempted to tamper with your session
li Your browser does not support cookies, or they are not enabled
| If the problem persists, please contact an administrator.
if referer
a(href=referer) Return to previous page

10
templates/footer.pug Normal file
View file

@ -0,0 +1,10 @@
mixin footer
footer#footer
.container
p.text-muted.credit.
ourfore.st is hosted on fore.st, a fork of <a href="https://github.com/calzoneman/sync" target="_blank" rel="noreferrer noopener">CyTube</a> built for the TTN community. It's source code is available <a href="https://gitlab.com/SovietBear/fore.st" target="_blank" rel="noreferrer noopener">here</a>.
script(src="/js/jquery-1.11.0.min.js")
// Must be included before jQuery-UI since jQuery-UI overrides jQuery.fn.button
// I should really abandon this crap one day
script(src="/js/jquery-ui.js")
script(src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js")

View file

@ -0,0 +1,47 @@
extends layout.pug
block content
.col-md-8.col-md-offset-2
h1 Google Drive Userscript
h2 Disclaimer
.alert.alert-danger.messagebox
strong Unsupported
p.
This functionality is provided <strong>as-is</strong> for backwards
compatibility for existing users for whom it already is known to work.
There are many reasons, known and unknown, for which it may
<strong>not</strong> work for you; please note the staff in CyTube
support channels cannot provide any troubleshooting assistance and you
will be asked to simply use a different video provider.
p.
This functionality was originally added so that users could share their
own personal videos stored in their Drive. No support whatsoever will
be provided to users attempting to use it to circumvent copyright
restrictions on third-party video hosts.
h2 How It Works
p.
The userscript is a short script that you can install using a browser
extension such as Greasemonkey or Tampermonkey that runs on the page
and provides additional functionality needed to play Google Drive
videos.
h2 Installation
ul
li
strong Chrome
| &mdash;Install <a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo" target="_blank">Tampermonkey</a>.
li
strong Firefox
| &mdash;Install <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/" target="_blank">Tampermonkey</a>
| or <a href="https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/" target="_blank">Greasemonkey</a>.
li
strong Other Browsers
| &mdash;Install the appropriate userscript plugin for your browser.
| Tampermonkey supports many browsers besides Chrome.
p.
Once you have installed the userscript manager addon for your browser,
you can <a href="/js/cytube-google-drive.user.js" target="_blank">
install the userscript</a>. If this link 404s, it means the administrator
of this server hasn't generated it yet.
p.
You can find a guide with screenshots of the installation process
<a href="https://github.com/calzoneman/sync/wiki/Google-Drive-Userscript-Installation-Guide" target="_blank">on GitHub</a>.

27
templates/head.pug Normal file
View file

@ -0,0 +1,27 @@
- var DEFAULT_THEME = "/css/themes/fore.st.css";
mixin head()
meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(name="description", content=siteDescription)
title= siteTitle
link(href="/css/sticky-footer-navbar.css", rel="stylesheet")
link(href="/css/cytube.css", rel="stylesheet")
link(id="usertheme", href=DEFAULT_THEME, rel="stylesheet")
if channelName
script(type="text/javascript").
var DEFAULT_THEME = '#{DEFAULT_THEME}';
var CHANNELPATH = '#{channelPath}';
var CHANNELNAME = '#{channelName}';
else
script(type="text/javascript").
var DEFAULT_THEME = '#{DEFAULT_THEME}';
var CHANNELPATH = '#{channelPath}';
script(src="/js/theme.js")
//[if lt IE 9]
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
//[endif]

23
templates/httperror.pug Normal file
View file

@ -0,0 +1,23 @@
extends layout.pug
mixin notfound()
h1 Not Found
p The page you were looking for doesn't seem to exist. Please check that you typed the URL correctly.
if message
p Reason: #{message}
mixin forbidden()
h1 Forbidden
p You don't have permission to access <code>#{path}</code>
mixin genericerror()
h1 Oops
p Your request could not be processed. Status code: <code>#{status}</code>, message: <code>#{message}</code>
block content
.col-md-12
.alert.alert-danger
if status == 404
+notfound()
else if status == 403
+forbidden()
else
+genericerror()

25
templates/index.pug Normal file
View file

@ -0,0 +1,25 @@
extends layout.pug
block content
.col-lg-9.col-md-9
h3 Public Channels
table.table.table-bordered.table-striped
thead
th Channel
th # Connected
th Now Playing
tbody
each chan in channels
tr
td: a(href=`/${channelPath}/${chan.name}`) #{chan.pagetitle} (#{chan.name})
td= chan.usercount
td= chan.mediatitle
.col-lg-3.col-md-3
append footer
script(type="text/javascript").
$("#channelname").keydown(function (ev) {
if (ev.keyCode === 13) {
location.href = "/#{channelPath}/" + $("#channelname").val();
}
});

22
templates/layout.pug Normal file
View file

@ -0,0 +1,22 @@
doctype html
html(lang="en")
head
block head
include head
+head()
body
#wrap
nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation")
include nav
+navheader()
#nav-collapsible.collapse.navbar-collapse
ul.nav.navbar-nav
+navdefaultlinks()
+navsuperadmin(false)
+navloginlogout()
section#mainpage
.container
block content
block footer
include footer
+footer()

39
templates/login.pug Normal file
View file

@ -0,0 +1,39 @@
extends layout.pug
block content
if wasAlreadyLoggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-info.messagebox.center
h3(style="margin: 5px auto") Logged in as #{loginName}
else if !loggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
if loginError
.alert.alert-danger.messagebox.center
strong Login Failed
p= loginError
h2 Login
form(role="form", action="/login", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
if redirect
input(type="hidden", name="dest", value=redirect)
.form-group
label(for="username") Username
input#username.form-control(type="text", name="name")
.form-group
label(for="password") Password
input#password.form-control(type="password", name="password")
a(href="/account/passwordreset") Forgot password?
.form-group
.checkbox
label
input(type="checkbox", name="remember")
| Remember me
button.btn.btn-success.btn-block(type="submit") Login
else
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-success.messagebox.center
strong Login Successful
p Logged in as #{loginName}
if redirect
br
a(href=redirect) Return to previous page

9
templates/logout.pug Normal file
View file

@ -0,0 +1,9 @@
extends layout.pug
block content
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-info.center.messagebox
strong Logged out
p
if redirect
a(href=redirect) Return to previous page

67
templates/nav.pug Normal file
View file

@ -0,0 +1,67 @@
mixin navheader()
.navbar-header
button.navbar-toggle(type="button", data-toggle="collapse", data-target="#nav-collapsible")
span.icon-bar
span.icon-bar
span.icon-bar
a.navbar-brand(href="/")= siteTitle
mixin navdefaultlinks()
li
a(href="/") Home
li.dropdown
a.dropdown-toggle(href="#", data-toggle="dropdown") Account
b.caret
ul.dropdown-menu
if loggedIn
li: a(href="javascript:$('#logoutform').submit();") Log out
li.divider
li: a(href="/account/channels") Channels
li: a(href="/account/profile") Profile
li: a(href="/account/edit") Change Password/Email
li: a(href="/account/delete") Delete Account
else
li: a(href="/login") Login
li: a(href="/register") Register
mixin navsuperadmin(newTab)
if superadmin
if newTab
li: a(href="/acp", target="_blank") ACP
else
li: a(href="/acp") ACP
mixin navloginlogout()
if loggedIn
+navlogoutform()
else
+navloginform()
mixin navloginform()
.visible-lg
form#loginform.navbar-form.navbar-right(action="/login", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group
input#username.form-control(type="text", name="name", placeholder="Username")
.form-group
input#password.form-control(type="password", name="password", placeholder="Password")
.form-group
.checkbox
label
input(type="checkbox", name="remember")
span.navbar-text-nofloat Remember me
button#login.btn.btn-default(type="submit") Login
.visible-md
p#loginform.navbar-text.pull-right
a#login.navbar-link(href="/login") Log in
span &nbsp;&middot;&nbsp;
a#register.navbar-link(href="/register") Register
mixin navlogoutform()
form#logoutform.navbar-text.pull-right(action="/logout", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
span#welcome Welcome, #{loginName}
span &nbsp;&middot;&nbsp;
input#logout.navbar-link(type="submit", value="Log out")

0
templates/pagefooter.pug Normal file
View file

121
templates/register.pug Normal file
View file

@ -0,0 +1,121 @@
extends layout.pug
block content
if loggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-danger.messagebox.center
strong Already logged in
p.
You are already logged in. If you intend to register a new account, please <a href="/logout?redirect=/register">Logout</a> first.
// TODO Link to My Account page
else if !registered
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
if registerError
.alert.alert-danger.messagebox.center
strong Registration Failed
p= registerError
h2 Register
form(role="form", action="/register", method="post", onsubmit="return verify()")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group
label.control-label(for="username") Username
input#register-username.form-control(type="text", name="name", onkeyup="checkUsername()", maxlength="20")
p#validate_username.text-danger.pull-right
.form-group
label.control-label(for="password") Password
input#register-password.form-control(type="password", name="password", onkeyup="checkPasswords()")
p#validate_password.text-danger.pull-right
.form-group
label.control-label(for="password_confirm") Confirm Password
input#register-password-confirm.form-control(type="password", onkeyup="checkPasswords()")
p#validate_confirm.text-danger.pull-right
.form-group
label.control-label(for="email") Email (optional)
input#register-email.form-control(type="email", name="email")
p#validate_email.text-danger.pull-right
p
| Providing an email address is optional and will allow you to recover your account via email if you forget your password.
strong &nbsp;&nbsp;If you do not provide an email address, you will not be able to recover a lost account!
if hCaptchaSiteKey
noscript
.text-danger This website requires JavaScript in order to display a CAPTCHA.
.form-group
div.h-captcha(data-sitekey=hCaptchaSiteKey)
button#registerbtn.btn.btn-success.btn-block(type="submit") Register
else
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-success.messagebox.center
strong Registration Successful
p Thanks for registering, #{registerName}! Now you can <a href="/login">Login</a> to use your account.
append footer
if hCaptchaSiteKey
script(src="https://hcaptcha.com/1/api.js" async defer)
script(type="text/javascript").
function verify() {
var valid = checkUsername();
valid = checkPasswords() && valid;
valid = checkEmail() && valid;
return valid;
}
function checkUsername() {
function stateError(text){
target.parent()
.addClass("has-error")
.removeClass("has-success");
$("#validate_username").text(text);
}
var target = $("#register-username");
var name = target.val();
if (name === "") {
stateError('Username must not be empty')
return false;
} else if (!(/^[-\w\u00c0-\u00ff]{1,20}$/).test(name)) {
stateError("Username must consist of 1-20 characters" +
" a-Z, A-Z, 0-9, -, or _.");
return false;
} else {
target.parent()
.removeClass("has-error")
.addClass("has-success");
$("#validate_username").text('');
}
}
function checkPasswords() {
function stateError(text, target, validator){
target.parent()
.addClass("has-error")
.removeClass("has-success");
$(`#${validator}`).text(text);
}
var target = $("#register-password");
var target2 = $("#register-password-confirm");
var pw = target.val();
var pwc = target2.val();
$("#validate_password").text('');
$("#validate_confirm").text('');
if (pw === "") {
stateError('Password must not be empty', target, 'validate_password')
return false;
} else {
target.parent()
.removeClass("has-error")
.addClass("has-success");
if (pw !== pwc) {
stateError('Passwords do not match', target2, 'validate_confirm')
return false;
} else {
target2.parent()
.removeClass("has-error")
.addClass("has-success");
}
}
}
function checkEmail() {
var email = $("#register-email").val();
if (email.trim() === "") {
return confirm("Are you sure you want to register without setting a recovery email address? If you lose the password, or if your account is compromised, you WILL NOT be able to recover it.");
}
return true;
}

142
templates/useroptions.pug Normal file
View file

@ -0,0 +1,142 @@
mixin lcheckbox(id, label)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
.checkbox
input(type="checkbox", id=id)
mixin rcheckbox(id, label)
.form-group
.col-sm-8.col-sm-offset-4
.checkbox
label(for=id)
input(type="checkbox", id=id)
= label
mixin textbox(id, label, placeholder)
.form-group
label.control-label.col-sm-4(for=id)= label
.col-sm-8
if placeholder
input.form-control(id=id, type="text", placeholder=placeholder)
else
input.form-control(id=id, type="text")
mixin us-general
#us-general.tab-pane
h4 General Preferences
form.form-horizontal(action="javascript:void(0)")
.form-group
label.control-label.col-sm-4(for="#us-theme") Theme
.col-sm-8
select#us-theme.form-control
option(value="/css/themes/light.css") Light
option(value="/css/themes/bootstrap-theme.min.css") Bootstrap
option(value="/css/themes/slate.css") Slate
option(value="/css/themes/cyborg.css") Cyborg
option(value="/css/themes/modern.css") Modern
.form-group
label.control-label.col-sm-4(for="#us-layout") Layout
.col-sm-8
select#us-layout.form-control
option(value="default") Compact
option(value="synchtube") Synchtube (flipped)
option(value="fluid") Fluid
option(value="synchtube-fluid") Synchtube + Fluid
option(value="hd") HD
.col-sm-4
.col-sm-8
p.text-danger Changing layouts may require refreshing to take effect.
+rcheckbox("us-no-channelcss", "Ignore Channel CSS")
+rcheckbox("us-no-channeljs", "Ignore Channel Javascript")
.clear
mixin us-scripts
#us-scriptcontrol.tab-pane
h4 Script Access
table.table
thead
tr
th Channel
th Type
th Preference
th Clear
mixin us-playback
#us-playback.tab-pane
h4 Playback Preferences
form.form-horizontal(action="javascript:void(0)")
+rcheckbox("us-synch", "Synchronize video playback")
+textbox("us-synch-accuracy", "Synch threshold (seconds)", "2")
+rcheckbox("us-wmode-transparent", "Set wmode=transparent")
.form-group
.col-sm-4
.col-sm-8
p.text-info Setting <code>wmode=transparent</code> allows objects to be displayed above the video player, but may cause performance issues on some systems.
+rcheckbox("us-hidevideo", "Remove the video player")
+rcheckbox("us-playlistbuttons", "Hide playlist buttons by default")
+rcheckbox("us-oldbtns", "Old style playlist buttons")
.form-group
label.control-label.col-sm-4(for="#us-default-quality") Quality Preference
.col-sm-8
select#us-default-quality.form-control
option(value="auto") Auto
option(value="240") 240p
option(value="360") 360p
option(value="480") 480p
option(value="720") 720p
option(value="1080") 1080p
option(value="best") Highest Available
.form-group
.col-sm-4
.col-sm-8
p.text-info Due to technical changes on YouTube's side, the CyTube quality preference can no longer be automatically applied on YouTube videos. See <a href="https://github.com/calzoneman/sync/issues/726" rel="noopener noreferer" target="_blank">this GitHub issue</a> for details.
mixin us-chat
#us-chat.tab-pane
h4 Chat Preferences
form.form-horizontal(action="javascript:void(0)")
+rcheckbox("us-chat-timestamp", "Show timestamps in chat")
+rcheckbox("us-sort-rank", "Sort userlist by rank")
+rcheckbox("us-sort-afk", "Sort AFKers to bottom")
.col-sm-4
.col-sm-8
p.text-info The following 3 options apply to how and when you will be notified if a new chat message is received while CyTube is not the active window.
.form-group
label.control-label.col-sm-4(for="#us-blink-title") Blink page title on new messages
.col-sm-8
select#us-blink-title.form-control
option(value="never") Never
option(value="onlyping") Only when I am mentioned or PMed
option(value="always") Always
.form-group
label.control-label.col-sm-4(for="#us-ping-sound") Notification sound on new messages
.col-sm-8
select#us-ping-sound.form-control
option(value="never") Never
option(value="onlyping") Only when I am mentioned or PMed
option(value="always") Always
.form-group
label.control-label.col-sm-4(for="#us-notifications") Desktop notifications on new messages
.col-sm-8
select#us-notifications.form-control
option(value="never") Never
option(value="onlyping") Only when I am mentioned or PMed
option(value="always") Always
+rcheckbox("us-sendbtn", "Add a send button to chat")
+rcheckbox("us-no-emotes", "Disable chat emotes")
+rcheckbox("us-strip-image", "Remove images from chat")
.form-group
label.control-label.col-sm-4(for="#us-chat-tab-method") Tab completion method
.col-sm-8
select#us-chat-tab-method.form-control
option(value="Cycle options") Cycle options
option(value="Longest unique match") Longest unique match
mixin us-mod
#us-mod.tab-pane
h4 Moderator Preferences
form.form-horizontal(action="javascript:void(0)")
+rcheckbox("us-modflair", "Show name color")
+rcheckbox("us-shadowchat", "Show shadowmuted messages")
+rcheckbox("us-show-ip-in-tooltip", "Show IP addresses in profile tooltip")