Commits

Mathias Köhler committed 1f94739

Style, History and Playlist changes

  • Participants
  • Parent commits eac6f71

Comments (0)

Files changed (30)

 
 @manager.command
 def clean():
-    db = StrictRedis()
-    db.delete("connected_count")
-    db.delete("connected")
     db.delete("playlist")
 
 
 @manager.command
 def runserver():
+    db = StrictRedis()
+    db.set("user_connections", 0)
+    db.set("connections", 0)
+    db.delete("connected")
     SocketIOServer(('0.0.0.0', 5000), app,
         namespace="socket.io",  policy_server=False).serve_forever()
 

File wetube/__init__.py

 
 from forms import RegisterForm, LoginForm
 from .session_interface import RedisSessionInterface
-from .utils import login_required, logged_in
 
 
 app = Flask(__name__)
 from .websockets import WetubeSocket
 
 
-@app.context_processor
-def inject_user():
-    logged_in = "user" in session and session["user"] is not None
-    user =  db.hgetall("users:%s" % session["user"]) if logged_in else None
-    return dict(user=user, logged_in=logged_in)
-
-
-@app.route('/login', methods=['GET', 'POST'])
-def login():
-    form = LoginForm(request.form)
-    if form.validate_on_submit():
-        session["user"] = form.username.data
-        return form.redirect('index')
-    return render_template('user/login.html', login_form=form,
-                           register_form=RegisterForm())
-
-
 @app.route("/")
-@login_required
 def index():
     return render_template('index.html')
 
 
-@app.route('/logout')
-def logout():
-    if "user" in session:
-        del session["user"]
-    return redirect(url_for('login'))
-
-
-@app.route("/register", methods=["POST"])
-def register():
-    form = RegisterForm(request.form)
-    if form.validate_on_submit():
-        name = form.username.data
-        key = "users:%s" % name
-
-        pipe = db.pipeline()
-        pipe.hset(key, "hash", hasher.encrypt(form.password.data))
-        pipe.hset(key, "email", form.email.data)
-        pipe.hset(key, "name", name)
-        pipe.hset(key, "role", "user")
-        pipe.sadd("users", name)
-        pipe.execute()
-
-        session["user"] = name
-        return redirect(url_for("index"))
-    return render_template("user/login.html", login_form=LoginForm(),
-                           register_form=form)
-
-
 @app.route('/socket.io/<path:remaining>')
 def socketio(remaining):
     try:
-        socketio_manage(request.environ, {'/app': WetubeSocket})
+        socketio_manage(request.environ, {'/app': WetubeSocket},
+                        request=request)
     except:
         app.logger.error("Exception while handling socketio connection",
                          exc_info=True)

File wetube/assets/css/main.css

 html, body { background: #1b1b1b; position: relative; height: 100%; overflow: hidden; }
 
 /* line 12, ../sass/main.sass */
-header { width: 100%; height: 55px; background: #272727; color: white; z-index: 3; }
+header { width: 100%; height: 55px; background: #272727; color: #cccccc; z-index: 3; }
 /* line 19, ../sass/main.sass */
-header h1, header h2, header h3 { width: 40%; font-size: 1.3em; font-family: Arial; padding: 17px; float: left; color: #cccccc; }
+header > h1, header > h2, header > h3 { width: 40%; font-size: 1.3em; font-family: Arial; padding: 17px; float: left; color: white; }
 /* line 26, ../sass/main.sass */
-header h1:first-child, header h2:first-child, header h3:first-child { font-size: 1.5em; color: white; }
-/* line 30, ../sass/main.sass */
+header > h1:first-child, header > h2:first-child, header > h3:first-child { font-size: 1.5em; }
+/* line 28, ../sass/main.sass */
 header nav { height: 100%; }
-/* line 32, ../sass/main.sass */
-header nav > ul { float: right; text-align: right; width: auto; padding-right: 10px; height: 100%; }
-/* line 38, ../sass/main.sass */
-header nav > ul > li { position: relative; height: 15px; padding: 20px 5px 20px 5px; display: inline-block; float: left; }
-/* line 44, ../sass/main.sass */
-header nav > ul > li:hover { color: #ff3333; }
-/* line 46, ../sass/main.sass */
-header .userlist { position: absolute; top: 100%; right: 50%; color: #cccccc; -webkit-transform: translate(50%, 0); -moz-transform: translate(50%, 0); -ms-transform: translate(50%, 0); -o-transform: translate(50%, 0); transform: translate(50%, 0); z-index: 1; background: #333333; border: 6px solid #222222; border-top: 0; padding: 10px; text-align: left; -webkit-transition: 0.2s ease-out all; -moz-transition: 0.2s ease-out all; -o-transition: 0.2s ease-out all; transition: 0.2s ease-out all; opacity: 1; }
-/* line 60, ../sass/main.sass */
-header .userlist.ng-hide { opacity: 0; }
-/* line 62, ../sass/main.sass */
-header .userlist.ng-hide-remove, header .userlist.ng-hide-add { display: block !important; }
+/* line 31, ../sass/main.sass */
+header nav [class^="icon-"]:before, header nav [class*=" icon-"]:before { vertical-align: -20%; font-size: 24px; padding-right: 5px; }
+/* line 35, ../sass/main.sass */
+header nav > ul { float: right; text-align: right; width: auto; padding-right: 20px; height: 100%; }
+/* line 41, ../sass/main.sass */
+header nav > ul > li { position: relative; height: 25px; padding: 15px; padding-left: 25px; display: inline-block; float: left; }
+/* line 48, ../sass/main.sass */
+header .cmenu { position: absolute; top: 100%; left: 50%; min-width: 210px; z-index: 1; -webkit-transform: translate(-50%, 0); -moz-transform: translate(-50%, 0); -ms-transform: translate(-50%, 0); -o-transform: translate(-50%, 0); transform: translate(-50%, 0); -webkit-transition: 0.3s ease-in all; -moz-transition: 0.3s ease-in all; -o-transition: 0.3s ease-in all; transition: 0.3s ease-in all; overflow: hidden; color: #cccccc; background: #333333; border: 6px solid #222222; text-align: left; padding: 15px; border-top: 0; opacity: 1; }
 /* line 64, ../sass/main.sass */
-header .userlist li { padding: 5px; }
-/* line 67, ../sass/main.sass */
-header a { color: white; text-decoration: none; }
+header .cmenu.ng-hide { opacity: 0; }
+/* line 66, ../sass/main.sass */
+header .cmenu.ng-hide-remove, header .cmenu.ng-hide-add { display: block !important; }
+/* line 68, ../sass/main.sass */
+header .cmenu li { padding: 5px; }
 /* line 70, ../sass/main.sass */
-header a:hover { color: #ff3333; }
+header .cmenu.signup { position: absolute; left: auto; right: -20px; min-width: 400px; min-height: 240px; -webkit-transform: translate(0, 0); -moz-transform: translate(0, 0); -ms-transform: translate(0, 0); -o-transform: translate(0, 0); transform: translate(0, 0); }
+/* line 77, ../sass/main.sass */
+header .cmenu.signup form { position: absolute; bottom: 15px; left: 15px; border: 3px solid transparent; right: 15px; }
+/* line 83, ../sass/main.sass */
+header .cmenu.signup form.ng-dirty.ng-valid { border: 3px solid #227522; }
+/* line 85, ../sass/main.sass */
+header .cmenu.signup form.ng-dirty.ng-invalid { border: 3px solid #752222; background: #752222; }
+/* line 88, ../sass/main.sass */
+header .cmenu.signup form span { display: block; padding: 3px 3px 6px 3px; }
+/* line 91, ../sass/main.sass */
+header .cmenu.signup input { display: block; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 100%; }
+/* line 95, ../sass/main.sass */
+header .cmenu.signup button { float: left; display: block; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 50%; border-top: 0; background: #666666; color: black; }
+/* line 103, ../sass/main.sass */
+header .cmenu.signup button:last-child { border-left: 0; }
+/* line 105, ../sass/main.sass */
+header .cmenu.signup h2 { font-family: Arial; font-size: 1.6em; margin-bottom: 0.4em; }
+/* line 109, ../sass/main.sass */
+header .cmenu.signup p { margin-bottom: 1em; }
+/* line 111, ../sass/main.sass */
+header a { color: #dddddd; text-decoration: none; }
 
-/* line 73, ../sass/main.sass */
+/* line 115, ../sass/main.sass */
 #party { position: absolute; top: 55px; bottom: 0; right: 0; left: 0; }
 
-/* line 80, ../sass/main.sass */
-#video { width: 50%; height: 100%; position: relative; float: left; background: #131313; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; }
-/* line 88, ../sass/main.sass */
-#video #playlist { list-style: none; height: -webkit-calc(100vh - 28.125vw - 55px); height: -moz-calc(100vh - 28.125vw - 55px); height: -ms-calc(100vh - 28.125vw - 55px); height: -o-calc(100vh - 28.125vw - 55px); height: calc(100vh - 28.125vw - 55px); overflow: auto; }
-/* line 11, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/typography/lists/_bullets.scss */
-#video #playlist li { list-style-image: none; list-style-type: none; margin-left: 0; }
-/* line 92, ../sass/main.sass */
-#video #playlist li { position: relative; color: #bbbbbb; padding: 8px; }
-/* line 96, ../sass/main.sass */
-#video #playlist li:not(:last-child) { border-bottom: 1px solid #050505; -webkit-box-shadow: #222222 0px 1px 0px; -moz-box-shadow: #222222 0px 1px 0px; box-shadow: #222222 0px 1px 0px; }
-/* line 99, ../sass/main.sass */
-#video #playlist span.title { position: absolute; left: 105px; top: 50%; width: -webkit-calc(50vw - 160px); width: -moz-calc(50vw - 160px); width: -ms-calc(50vw - 160px); width: -o-calc(50vw - 160px); width: calc(50vw - 160px); -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
-/* line 105, ../sass/main.sass */
-#video #playlist span.duration { position: absolute; top: 50%; right: 10px; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
-/* line 110, ../sass/main.sass */
-#video #playlist .crop { margin-right: 10px; height: 47.8125px; width: 85px; overflow: hidden; }
-/* line 118, ../sass/main.sass */
-#video #playlist .crop img { width: 85px; margin-top: -7.96875px; margin-bottom: -7.96875px; }
 /* line 122, ../sass/main.sass */
-#video form { margin-left: 66px; height: 42px; }
-/* line 125, ../sass/main.sass */
-#video input { position: absolute; top: 50%; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
-/* line 129, ../sass/main.sass */
-#video input[type="submit"] { right: 10px; }
-/* line 131, ../sass/main.sass */
-#video input:not([type="submit"]) { width: -webkit-calc(50vw - 200px); width: -moz-calc(50vw - 200px); width: -ms-calc(50vw - 200px); width: -o-calc(50vw - 200px); width: calc(50vw - 200px); }
-
-/* line 134, ../sass/main.sass */
-#videowrapper { height: 28.125vw; position: relative; background: black; }
-
-/* line 139, ../sass/main.sass */
-#chat { float: right; height: 100%; width: 50%; color: #bbbbbb; background: #191919; line-height: 1.5; font-family: Arial; position: relative; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; }
-/* line 149, ../sass/main.sass */
-#chat ul { position: absolute; left: 0; right: 0; bottom: 55px; padding: 20px; }
-/* line 155, ../sass/main.sass */
-#chat li { margin: 5px; padding: 2px; width: -webkit-calc(100% - 20px); width: -moz-calc(100% - 20px); width: -ms-calc(100% - 20px); width: -o-calc(100% - 20px); width: calc(100% - 20px); display: inline-block; position: relative; }
-/* line 161, ../sass/main.sass */
-#chat li .user { padding-right: 10px; font-weight: bold; color: #777777; display: block; float: left; height: 100%; }
-/* line 162, ../sass/main.sass */
+#videowrapper { width: -webkit-calc(50% - 14px); width: -moz-calc(50% - 14px); width: -ms-calc(50% - 14px); width: -o-calc(50% - 14px); width: calc(50% - 14px); border-style: solid; border-color: #222222; border-width: 0 7px 0 7px; position: relative; float: left; background: #131313; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; }
+/* line 132, ../sass/main.sass */
+#videowrapper #playlistwrapper { padding-bottom: -webkit-calc(100vh - 56.25% - 55px); padding-bottom: -moz-calc(100vh - 56.25% - 55px); padding-bottom: -ms-calc(100vh - 56.25% - 55px); padding-bottom: -o-calc(100vh - 56.25% - 55px); padding-bottom: calc(100vh - 56.25% - 55px); overflow: hidden; position: relative; height: 0; }
+/* line 138, ../sass/main.sass */
+#videowrapper #playlist { list-style: none; height: -webkit-calc(100vh - 28.125vw - 55px); height: -moz-calc(100vh - 28.125vw - 55px); height: -ms-calc(100vh - 28.125vw - 55px); height: -o-calc(100vh - 28.125vw - 55px); height: calc(100vh - 28.125vw - 55px); position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: auto; }
+/* line 11, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/typography/lists/_bullets.scss */
+#videowrapper #playlist li { list-style-image: none; list-style-type: none; margin-left: 0; }
+/* line 147, ../sass/main.sass */
+#videowrapper #playlist li { position: relative; color: #bbbbbb; padding: 8px; }
+/* line 151, ../sass/main.sass */
+#videowrapper #playlist li:not(:last-child) { border-bottom: 1px solid #050505; -webkit-box-shadow: #222222 0px 1px 0px; -moz-box-shadow: #222222 0px 1px 0px; box-shadow: #222222 0px 1px 0px; }
+/* line 154, ../sass/main.sass */
+#videowrapper #playlist span.title { position: absolute; left: 105px; top: 50%; width: -webkit-calc(50vw - 230px); width: -moz-calc(50vw - 230px); width: -ms-calc(50vw - 230px); width: -o-calc(50vw - 230px); width: calc(50vw - 230px); -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 160, ../sass/main.sass */
+#videowrapper #playlist span.duration { position: absolute; top: 50%; right: 10px; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 165, ../sass/main.sass */
+#videowrapper #playlist .crop { margin-right: 10px; height: 47.8125px; width: 85px; overflow: hidden; }
+/* line 173, ../sass/main.sass */
+#videowrapper #playlist .crop img { width: 85px; margin-top: -7.96875px; margin-bottom: -7.96875px; }
+/* line 177, ../sass/main.sass */
+#videowrapper #playlist span[class^="icon-"] { position: absolute; right: 80px; top: 50%; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 182, ../sass/main.sass */
+#videowrapper form { height: 48px; }
+/* line 184, ../sass/main.sass */
+#videowrapper input { position: absolute; top: 50%; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 188, ../sass/main.sass */
+#videowrapper input[type="submit"] { right: 10px; }
+/* line 190, ../sass/main.sass */
+#videowrapper input:not([type="submit"]) { width: -webkit-calc(50vw - 137px); width: -moz-calc(50vw - 137px); width: -ms-calc(50vw - 137px); width: -o-calc(50vw - 137px); width: calc(50vw - 137px); }
+
+/* line 193, ../sass/main.sass */
+#video { padding-bottom: 56.25%; height: 0; position: relative; background: black; overflow: hidden; }
+/* line 199, ../sass/main.sass */
+#video iframe, #video embed { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }
+
+/* line 206, ../sass/main.sass */
+#chat { float: right; height: 100%; overflow: hidden; width: 50%; color: #bbbbbb; background: #191919; line-height: 1.5; font-family: Arial; position: relative; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; }
+/* line 217, ../sass/main.sass */
+#chat #messages { position: absolute; left: 0; right: 0; bottom: 55px; padding: 20px; }
+/* line 223, ../sass/main.sass */
+#chat ul { display: table; }
+/* line 225, ../sass/main.sass */
+#chat li { margin: 5px; padding: 2px; width: -webkit-calc(100% - 20px); width: -moz-calc(100% - 20px); width: -ms-calc(100% - 20px); width: -o-calc(100% - 20px); width: calc(100% - 20px); display: table-row; position: relative; }
+/* line 231, ../sass/main.sass */
+#chat li .user { padding-right: 10px; display: table-cell; text-align: right; font-weight: bold; height: 100%; white-space: nowrap; }
+/* line 232, ../sass/main.sass */
 #chat li .user:after { content: " >"; }
-/* line 170, ../sass/main.sass */
+/* line 240, ../sass/main.sass */
+#chat li .msg { display: table-cell; }
+/* line 242, ../sass/main.sass */
 #chat form { height: 35px; position: absolute; bottom: 0px; background: #272727; width: 100%; padding: 10px; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; }
-/* line 178, ../sass/main.sass */
-#chat input { background: #494949; border: 1px solid #222222; color: black; }
-/* line 183, ../sass/main.sass */
-#chat input:not([type="submit"]) { width: -webkit-calc(100% - 120px); width: -moz-calc(100% - 120px); width: -ms-calc(100% - 120px); width: -o-calc(100% - 120px); width: calc(100% - 120px); }
+/* line 250, ../sass/main.sass */
+#chat input, #chat button { border: 1px solid #111111; background-color: #353535; }
+/* line 253, ../sass/main.sass */
+#chat input:not([type="submit"]), #chat button:not([type="submit"]) { margin-right: 5px; width: -webkit-calc(100% - 125px); width: -moz-calc(100% - 125px); width: -ms-calc(100% - 125px); width: -o-calc(100% - 125px); width: calc(100% - 125px); }
 
-/* line 188, ../sass/main.sass */
-input { padding: 7px; font-size: 0.8em; margin: 0; border: 0; background: #222222; outline: none; color: #bbbbbb; border: 1px solid #080808; -webkit-box-shadow: #222222 0px 0px 3px; -moz-box-shadow: #222222 0px 0px 3px; box-shadow: #222222 0px 0px 3px; }
-/* line 198, ../sass/main.sass */
-input[type="submit"]::-moz-focus-inner { padding: 0; border: 0; }
+/* line 259, ../sass/main.sass */
+.admin-role { font-family: Arial; font-weight: bold; color: #bb6666; }
+
+/* line 264, ../sass/main.sass */
+.mod-role { font-family: Arial; font-weight: bold; color: #6666bb; }
+
+/* line 269, ../sass/main.sass */
+.user-role { font-family: Arial; color: #bbbbbb; }
 
-/* line 204, ../sass/main.sass */
+/* line 273, ../sass/main.sass */
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; }
+
+/* line 277, ../sass/main.sass */
+[class^="icon-"]:before, [class*=" icon-"]:before { color: #bbbbbb; }
+/* line 279, ../sass/main.sass */
+[class^="icon-"]:not(.icon-view), [class*=" icon-"]:not(.icon-view) { cursor: pointer; }
+/* line 281, ../sass/main.sass */
+[class^="icon-"]:not(.icon-view):hover:before, [class*=" icon-"]:not(.icon-view):hover:before { color: #ff4444; }
+
+/* line 288, ../sass/main.sass */
+input, button { padding: 7px; font-size: 0.8em; margin: 0; border: 0; background: #222222; outline: none; color: #bbbbbb; border: 1px solid #080808; -webkit-box-shadow: #222222 0px 0px 3px; -moz-box-shadow: #222222 0px 0px 3px; box-shadow: #222222 0px 0px 3px; }
+/* line 298, ../sass/main.sass */
+input[type="submit"]::-moz-focus-inner, button[type="submit"]::-moz-focus-inner { padding: 0; border: 0; }
+
+/* line 304, ../sass/main.sass */
 ::-webkit-scrollbar { width: 12px; height: 12px; }
 
-/* line 208, ../sass/main.sass */
+/* line 308, ../sass/main.sass */
 ::-webkit-scrollbar-track { background: #444444; }
 
-/* line 211, ../sass/main.sass */
+/* line 311, ../sass/main.sass */
 ::-webkit-scrollbar-thumb { background: #090909; }
+
+@font-face { font-family: "wetube_icons"; src: url("../fonts/wetube_icons.eot?q4ianb"); src: url("../fonts/wetube_icons.eot?#iefixq4ianb") format("embedded-opentype"), url("../fonts/wetube_icons.woff?q4ianb") format("woff"), url("../fonts/wetube_icons.ttf?q4ianb") format("truetype"), url("../fonts/wetube_icons.svg?q4ianb#wetube_icons") format("svg"); font-weight: normal; font-style: normal; }
+
+/* line 328, ../sass/main.sass */
+[class^="icon-"], [class*=" icon-"] { font-family: "wetube_icons"; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
+
+/* line 339, ../sass/main.sass */
+.icon-video:before { content: "\e600"; }
+
+/* line 341, ../sass/main.sass */
+.icon-add:before { content: "\e601"; }
+
+/* line 343, ../sass/main.sass */
+.icon-remove:before { content: "\e602"; }
+
+/* line 345, ../sass/main.sass */
+.icon-backspace:before { content: "\e603"; }
+
+/* line 347, ../sass/main.sass */
+.icon-blocked:before { content: "\e604"; }
+
+/* line 349, ../sass/main.sass */
+.icon-view:before { content: "\e605"; }
+
+/* line 351, ../sass/main.sass */
+.icon-users:before { content: "\e606"; }
+
+/* line 353, ../sass/main.sass */
+.icon-bubble:before { content: "\e608"; }
+
+/* line 355, ../sass/main.sass */
+.icon-history:before { content: "\e609"; }
+
+/* line 357, ../sass/main.sass */
+.icon-login:before { content: "\e379"; }
+
+/* line 359, ../sass/main.sass */
+.icon-logout:before { content: "\e37a"; }
+
+/* line 361, ../sass/main.sass */
+.icon-github:before { content: "\e4b8"; }
+
+/* line 363, ../sass/main.sass */
+.icon-reload:before { content: "\e3a8"; }
+
+/* line 365, ../sass/main.sass */
+.icon-shuffle:before { content: "\e3aa"; }
+
+/* line 367, ../sass/main.sass */
+.icon-vote:before { content: "\e310"; }
+
+/* line 369, ../sass/main.sass */
+.icon-forward:before { content: "\e389"; }
+
+/* line 371, ../sass/main.sass */
+.icon-settings:before { content: "\e1e2"; }
+
+/* line 373, ../sass/main.sass */
+.icon-spinner:before { content: "\e1a8"; }
+
+/* line 375, ../sass/main.sass */
+.icon-play:before { content: "\e385"; }
+
+/* line 377, ../sass/main.sass */
+.icon-pause:before { content: "\e386"; }
+
+/* line 379, ../sass/main.sass */
+.icon-eject:before { content: "\e38e"; }
+
+/* line 381, ../sass/main.sass */
+.icon-link:before { content: "\e47c"; }

File wetube/assets/js/app.js

 
 angular.module("wetube.directives", [
   "wetube.directives.player",
-  "wetube.directives.userlist"
+  "wetube.directives.ensureUnique",
+  "wetube.directives.userlist",
+  "wetube.directives.playlist",
+  "wetube.directives.videoform"
 ]);
 
 angular.module("wetube.filters", [
-  "wetube.filters.duration"
+  "wetube.filters.duration",
+  "wetube.filters.reverse"
 ]);
 
 angular.module("wetube.controllers", [
   "wetube.controllers.UserController",
   "wetube.controllers.ChatController",
   "wetube.controllers.PlaylistController",
-  "wetube.controllers.VideoController"  
+  "wetube.controllers.VideoController",
+  "wetube.controllers.HistoryController"
 ]);
 
 
     "wetube.filters"
   ])
   .run(function(socket, sync_time) {
-    socket.emit("join")
     sync_time.initiate()
   });
 

File wetube/assets/js/controllers/HistoryController.js

+angular.module("wetube.controllers.HistoryController", [
+    "wetube.services.socket"
+  ])
+  .controller('HistoryController', function($scope, socket) {
+    $scope.history = [];
+
+    socket.on("history", function(videos) {
+      $scope.history = videos
+    });
+    socket.on("playback", function(data) {
+      if ($scope.current_video) {
+        $scope.history.push(angular.copy($scope.current_video));
+        if ($scope.history > 20) {
+          $scope.history.splice(0, 1);
+        }
+      }
+      $scope.current_video = data;
+    });
+  });

File wetube/assets/js/controllers/PlaylistController.js

     "wetube.services.socket"
   ])
   .controller('PlaylistController', function($scope, socket) {
+    $scope.playlist = [];
+    $scope.history = [];
+
     socket.on("playlist", function(data) {
+      $scope.showAddVideo = undefined;
       $scope.playlist = data;
     });
 
-    $scope.addVideo = function() {
-      var url = $scope.video_input;
-      socket.emit("add_video", url);
-      $scope.video_input = "";
+    socket.on("history", function(videos) {
+      $scope.history = videos
+    });
+    socket.on("playback", function(data) {
+      if ($scope.current_video) {
+        $scope.history.push(angular.copy($scope.current_video));
+        if ($scope.history > 20) {
+          $scope.history.splice(0, 1);
+        }
+      }
+      $scope.current_video = data;
+    });
+
+    socket.on("remove", function(gid) {
+      index = $scope.playlist.map(function(video, index) {
+        if (video.gid === gid) {
+          return index;
+        }
+      }).filter(isFinite)[0];
+      console.log("Remove: " + index);
+      $scope.playlist.splice(index, 1);
+    });
+    socket.on("append", function(new_video, after) {
+      index = $scope.playlist.map(function(video, index) {
+        if (video.gid === after) {
+          return index;
+        }
+      }).filter(isFinite)[0] + 1;
+      console.log("Append after: " + index);
+      $scope.playlist.splice(index, 0, new_video);
+    });
+
+    $scope.input = {};
+    $scope.addVideo = function(after) {
+      var url = $scope.input.video;
+      console.log(url);
+      $scope.input.video = "";
+      $scope.showAddVideo = undefined;
+      socket.emit("add_video", {url: url, after: after});
+    };
+    $scope.removeVideo = function($event, gid) {
+      console.log(gid);
+      $event.stopPropagation();
+      socket.emit("remove_video", gid);
+      console.log("hahaha");
+    };
+
+    $scope.toggleAddVideo = function(gid) {
+      $scope.input.video = "";
+      if ($scope.showAddVideo === gid) {
+        $scope.showAddVideo = undefined;
+      } else {
+        $scope.showAddVideo = gid;
+      }
     };
   });

File wetube/assets/js/controllers/UserController.js

 angular.module("wetube.controllers.UserController", [
     "wetube.services.socket"  
   ])
-  .controller('UserController', function($scope, socket) {
-    $scope.showUserlist = false;
-    $scope.toggleUserlist = function() {
-      $scope.showUserlist = !$scope.showUserlist;
+  .controller('UserController', function($rootScope, $scope, socket) {
+    $scope.login = function() {
+      navigator.id.request();
+    };
+    $scope.logout = function() {
+      navigator.id.logout();
     }
-    socket.on('connected', function(data) {
+    navigator.id.watch({
+      loggedInUser: null,
+      onlogin: function(assertion) {
+        socket.emit("signin", assertion);
+      },
+      onlogout: function() {
+        socket.emit("logout");
+        window.location.reload();
+      }
+    });
+    socket.on("login_successful", function(user) {
+      $scope.showSignUp = false;
+      user.isMod = function() {
+        return this.role === 'mod' || this.role === 'admin';
+      }
+      $rootScope.user = user;
+      socket.emit("join");
+    });
+
+    socket.on('users', function(data) {
       $scope.users = data;
     });
+    socket.on('views', function(data) {
+      $scope.views = data;
+    });
     socket.on('user_leaved', function(leaved_user) {
       $scope.users = $scope.users.filter(function(user) {
         return user.name !== leaved_user.name;
     socket.on('user_joined', function(user) {
       $scope.users.push(user);
     });
+
+    $scope.input = {}
+    $scope.dialog = false;
+    $scope.submitSignUp = function() {
+      socket.emit("signup", $scope.input.name);
+      $scope.dialog = false;
+    };
+    var dialogToggle = function(name) {
+      return function() {
+        if ($scope.dialog !== name && $scope.dialog !== "signup") {
+          $scope.dialog = name
+        } else if ($scope.dialog === name) {
+          $scope.dialog = false;
+        }
+        
+      }
+    };
+    socket.on("signup_needed", function() {
+      $scope.dialog = "signup";
+    });
+    $scope.toggleUserlist = dialogToggle('userlist');
+    $scope.toggleHistory = dialogToggle('history');
   });

File wetube/assets/js/controllers/VideoController.js

     "wetube.services.socket",
     "wetube.services.youtube"  
   ])
-  .controller('VideoController', function($scope, socket, youtube) {
+  .controller('VideoController', function($rootScope, $scope, socket, youtube) {
+    $rootScope.events = [];
     socket.on("playback", function(data) {
-      youtube.load(data.id, data.started)
+      youtube.load(data.id, data.started);
+      data.now = (new Date()).getTime();
+      $rootScope.events.push(data);
     }); 
   });

File wetube/assets/js/directives/ensureUnique.js

+angular.module('wetube.directives.ensureUnique', [
+    "wetube.services.socket"
+  ])
+  .directive('ensureUnique', function(socket) {
+    return {
+      require: 'ngModel',
+      link: function(scope, ele, attrs, c) {
+        c.$parsers.unshift(function(value) {
+          socket.emit("checkname", value);
+          c.$setValidity('unique', true);
+          return value;
+        });
+        socket.on("checkname", function(valid) {
+          c.$setValidity('unique', valid);
+        });
+      }
+    }
+  });

File wetube/assets/js/directives/playlist.js

+angular.module("wetube.directives.playlist", [])
+  .directive('playlist', function(youtube) {
+    return {
+      restrict: 'AE',
+      templateUrl: "static/templates/playlist.html",
+      replace: true
+    };
+  });

File wetube/assets/js/directives/videoform.js

+angular.module("wetube.directives.videoform", [
+    "wetube.services.socket"  
+  ])
+  .directive('videoform', function(youtube) {
+    return {
+      restrict: 'AE',
+      templateUrl: "static/templates/videoform.html",
+      replace: true
+    };
+  });

File wetube/assets/js/filters/reverse.js

+angular.module("wetube.filters.reverse", [])
+  .filter('reverse', function() {
+    return function(data) {
+      if (!angular.isArray(data)) {
+        return data;
+      }
+      return data.slice().reverse();
+    };
+  });

File wetube/assets/js/services/sync_time.js

     "wetube.services.socket"
   ])
   .service('sync_time', function ($rootScope, $interval, socket) {
-    var round_trip, response_diff, time_diff_queue = [];
+    var round_trip, response_diff, request_time, time_diff_queue = [];
 
     this.time_diff = 0;
     this.latency = 0;
     $rootScope.latency = this.latency;
 
     socket.on('sync_time', function(request_diff, response_time) {
-      response_diff = (new Date()).getTime() - response_time;
+      now = (new Date()).getTime();
+      response_diff = now - response_time;
 
-      round_trip = Math.ceil((request_diff + response_diff) / 2);
+      round_trip = (now - request_time);
+      offset = ((response_time - request_time) + (response_time - now)) / 2;
 
-      time_diff_queue.push(response_diff - round_trip);
+      time_diff_queue.push(offset);
       if (time_diff_queue.length > 100) {
         time_diff_queue.shift();
       }
 
       var index = Math.ceil(time_diff_queue.length / 2) - 1;
-      this.time_diff = time_diff_queue.slice(0).sort(
+      this.time_diff = Math.floor(time_diff_queue.slice(0).sort(
         function (a, b) {
           return a - b;
         }
-      )[index];
+      )[index]);
       this.latency = round_trip;
       $rootScope.time_diff = this.time_diff;
       $rootScope.latency = this.latency;
     });
 
+    $interval(function () {
+      $rootScope.time = (new Date()).getTime() + $rootScope.time_diff;
+      $rootScope.now = (new Date()).getTime();
+    }, 300);
+
     this.initiate = function() {
-      socket.emit('sync_time', (new Date()).getTime());
+      request_time = (new Date()).getTime();
+      socket.emit('sync_time', request_time);
       $interval(function() {
-        socket.emit('sync_time', (new Date()).getTime());
+        request_time = (new Date()).getTime();
+        socket.emit('sync_time', request_time);
       }, 1000); 
     };
   });

File wetube/assets/js/services/youtube.js

       },
       create_player: function() {
         if (!service.ready) {
-          $timeout(this.create_player, 100);
+          $timeout(this.create_player, 200);
         }
         if (service.id && service.video) {
           $log.info('Create player!');

File wetube/assets/sass/main.sass

   width: 100%
   height: 55px
   background: #272727
-  color: #fff
+  color: #ccc
   z-index: 3
 
-  #{headings(1,3)}
+  #{nest("&>", headings(1,3))}
     width: 40%
     font-size: 1.3em
     font-family: Arial
     padding: 17px
     float: left
-    color: #ccc
+    color: #fff
     &:first-child
       font-size: 1.5em
-      color: #fff
-
   nav
     height: 100%
+    [class^="icon-"], [class*=" icon-"]
+      &:before
+        vertical-align: -20%
+        font-size: 24px
+        padding-right: 5px
     &> ul
       float: right
       text-align: right
       width: auto
-      padding-right: 10px
+      padding-right: 20px
       height: 100%
       &> li
         position: relative
-        height: 15px
-        padding: 20px 5px 20px 5px
+        height: 25px
+        padding: 15px
+        padding-left: 25px
         display: inline-block
         float: left
-        &:hover
-          color: #f33
-  .userlist
+  .cmenu
     position: absolute
     top: 100%
-    right: 50%
-    color: #ccc
-    +translate(50%, 0)
+    left: 50%
+    min-width: 210px
     z-index: 1
+    +translate(-50%, 0)
+    +transition(0.3s ease-in all)
+    overflow: hidden
+    color: #ccc
     background: #333
     border: 6px solid #222
-    border-top: 0
-    padding: 10px
     text-align: left
-    +transition(0.2s ease-out all)
+    padding: 15px
+    border-top: 0
     opacity: 1
     &.ng-hide
       opacity: 0
       display: block !important
     li
       padding: 5px
-
+  .cmenu.signup
+    position: absolute
+    left: auto
+    right: -20px
+    min-width: 400px
+    min-height: 240px
+    +translate(0, 0)
+    form
+      position: absolute
+      bottom: 15px
+      left: 15px
+      border: 3px solid transparent
+      right: 15px
+      &.ng-dirty.ng-valid
+        border: 3px solid #227522
+      &.ng-dirty.ng-invalid
+        border: 3px solid #752222
+        background: #752222
+      span
+        display: block
+        padding: 3px 3px 6px 3px
+    input
+      display: block
+      +box-sizing(border-box)
+      width: 100%
+    button
+      float: left
+      display: block
+      +box-sizing(border-box)
+      width: 50%
+      border-top: 0
+      background: #666
+      color: #000
+      &:last-child
+        border-left: 0
+    h2
+      font-family: Arial
+      font-size: 1.6em
+      margin-bottom: 0.4em
+    p
+      margin-bottom: 1em
   a
-    color: #fff
+    color: #ddd
     text-decoration: none
-    &:hover
-      color: #f33
 
 #party
   position: absolute
   right: 0
   left: 0
 
-#video
-  width: 50%
-  height: 100%
+#videowrapper
+  +experimental-value(width, calc(50% - 14px))
+  border-style: solid
+  border-color: #222
+  border-width: 0 7px 0 7px
   position: relative
   float: left
   background: #131313
   +box-shadow(#353535 0 -1px 0, #111 0 -2px 0)
 
+  #playlistwrapper
+    +experimental-value(padding-bottom, calc(100vh - 56.25% - 55px))
+    overflow: hidden
+    position: relative
+    height: 0
+
   #playlist
     +no-bullets
     +experimental-value(height, calc(100vh - 28.125vw - 55px))
+    position: absolute
+    top: 0
+    left: 0
+    width: 100%
+    height: 100%
     overflow: auto
     li
       position: relative
       position: absolute
       left: 105px
       top: 50%
-      +experimental-value(width, calc(50vw - 160px))
+      +experimental-value(width, calc(50vw - 230px))
       +translate(0, -50%)
     span.duration
       position: absolute
         width: $width
         margin-top: -$crop/2
         margin-bottom: -$crop/2
+    span[class^="icon-"]
+      position: absolute
+      right: 80px
+      top: 50%
+      +translate(0, -50%)
   form
-    margin-left: 66px
-    height: 42px
+    height: 48px
   input
     position: absolute
     top: 50%
   input[type="submit"]
     right: 10px
   input:not([type="submit"])
-    +experimental-value(width, calc(50vw - 200px))
+    +experimental-value(width, calc(50vw - 137px))
 
-#videowrapper
-  height: 28.125vw
+#video
+  padding-bottom: 56.25%
+  height: 0
   position: relative
   background: #000
+  overflow: hidden
+  iframe, embed
+    position: absolute
+    top: 0
+    left: 0
+    height: 100%
+    width: 100%
 
 #chat
   float: right
   height: 100%
+  overflow: hidden
   width: 50%
   color: #bbb
   background: #191919
   font-family: Arial
   position: relative
   +box-shadow(#353535 0 -1px 0, #111 0 -2px 0, #111 -1px 0 0)
-  ul
+  #messages
     position: absolute
     left: 0
     right: 0
     bottom: 55px
     padding: 20px
+  ul
+    display: table
   li
     margin: 5px
     padding: 2px
     +experimental-value(width, calc(100% - 20px))
-    display: inline-block
+    display: table-row
     position: relative
     .user
       &:after
         content: " >"
       padding-right: 10px
+      display: table-cell
+      text-align: right
       font-weight: bold
-      color: #777
-      display: block
-      float: left
       height: 100%
+      white-space: nowrap
+    .msg
+      display: table-cell
   form
     height: 35px
     position: absolute
     width: 100%
     padding: 10px
     +box-shadow(#353535 0 -1px 0, #111 0 -2px 0)
-  input
-    background: #494949
-    border: 1px solid #222
-    color: #000
-
+  input, button
+    border: 1px solid #111
+    background-color: #353535
     &:not([type="submit"])
-      +experimental-value(width, calc(100% - 120px))
+      margin-right: 5px
+      +experimental-value(width, calc(100% - 125px))
+
+// Classes
+
+.admin-role
+  font-family: Arial
+  font-weight: bold
+  color: #b66
+
+.mod-role
+  font-family: Arial
+  font-weight: bold
+  color: #66b
+
+.user-role
+  font-family: Arial
+  color: #bbb
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak
+  display: none !important
+
+[class^="icon-"], [class*=" icon-"]
+  &:before
+    color: #bbb
+  &:not(.icon-view)
+    cursor: pointer
+    &:hover:before
+      color: #f44
+
+
 
 // Common Tags
 
-input
+input, button
   padding: 7px
   font-size: 0.8em
   margin: 0
 
 ::-webkit-scrollbar-thumb
   background: #090909
+
+
+// Fonts
+
+@font-face
+  font-family: 'wetube_icons'
+  src: url('../fonts/wetube_icons.eot?q4ianb')
+  $fonts1: url('../fonts/wetube_icons.eot?#iefixq4ianb') format('embedded-opentype')
+  $fonts2: url('../fonts/wetube_icons.woff?q4ianb') format('woff')
+  $fonts3: url('../fonts/wetube_icons.ttf?q4ianb') format('truetype')
+  $fonts4: url('../fonts/wetube_icons.svg?q4ianb#wetube_icons') format('svg')
+  src: $fonts1, $fonts2, $fonts3, $fonts4
+  font-weight: normal
+  font-style: normal
+
+[class^="icon-"], [class*=" icon-"]
+  font-family: 'wetube_icons'
+  speak: none
+  font-style: normal
+  font-weight: normal
+  font-variant: normal
+  text-transform: none
+  line-height: 1
+  -webkit-font-smoothing: antialiased
+  -moz-osx-font-smoothing: grayscale
+
+.icon-video:before
+  content: "\e600"
+.icon-add:before
+  content: "\e601"
+.icon-remove:before
+  content: "\e602"
+.icon-backspace:before
+  content: "\e603"
+.icon-blocked:before
+  content: "\e604"
+.icon-view:before
+  content: "\e605"
+.icon-users:before
+  content: "\e606"
+.icon-bubble:before
+  content: "\e608"
+.icon-history:before
+  content: "\e609"
+.icon-login:before
+  content: "\e379"
+.icon-logout:before
+  content: "\e37a"
+.icon-github:before
+  content: "\e4b8"
+.icon-reload:before
+  content: "\e3a8"
+.icon-shuffle:before
+  content: "\e3aa"
+.icon-vote:before
+  content: "\e310"
+.icon-forward:before
+  content: "\e389"
+.icon-settings:before
+  content: "\e1e2"
+.icon-spinner:before
+  content: "\e1a8"
+.icon-play:before
+  content: "\e385"
+.icon-pause:before
+  content: "\e386"
+.icon-eject:before
+  content: "\e38e"
+.icon-link:before
+  content: "\e47c"

File wetube/forms.py

-# -*- coding: utf8 -*-
-
-from uuid import UUID, uuid4
-
-from flask import redirect, request, url_for, session
-from urlparse import urlparse, urljoin
-from wtforms import Form, TextField, PasswordField, HiddenField
-from wtforms.fields.html5 import EmailField
-from wtforms.validators import InputRequired, EqualTo, Optional, Email
-from wtforms.csrf.core import CSRF
-from passlib.hash import pbkdf2_sha512 as hasher
-
-from .utils import compress_uuid, decompress_uuid
-
-
-def get_redirect_target():
-    next = request.args.get('next')
-    if next and is_safe_url(next):
-        return next
-
-
-def is_safe_url(target):
-    ref_url = urlparse(request.host_url)
-    test_url = urlparse(urljoin(request.host_url, target))
-    return test_url.scheme in ('http', 'https') and \
-           ref_url.netloc == test_url.netloc
-
-
-class UUID_CSRF(CSRF):
-
-    def setup_form(self, form):
-        return super(UUID_CSRF, self).setup_form(form)
-
-    def generate_csrf_token(self, csrf_token):
-        if "csrf" not in session:
-            session["csrf"] = compress_uuid(uuid4())
-        return session["csrf"]
-
-    def validate_csrf_token(self, form, field):
-        token = session["csrf"]
-        del session["csrf"]
-        if field.data != token:
-            raise ValueError('Invalid CSRF')
-
-
-class BaseForm(Form):
-    class Meta:
-        csrf = True
-        csrf_class = UUID_CSRF
-
-    def validate_on_submit(self):
-        submitted = request and request.method in ("PUT", "POST")
-        return submitted and self.validate()
-
-
-class RedirectForm(BaseForm):
-    next = HiddenField()
-
-    def __init__(self, *args, **kwargs):
-        Form.__init__(self, *args, **kwargs)
-        if not self.next.data:
-            self.next.data = get_redirect_target()
-
-    def redirect(self, endpoint='index', **values):
-        if self.next.data and is_safe_url(self.next.data):
-            return redirect(self.next.data)
-        return redirect(url_for(endpoint, **values))
-
-
-class UserForm(BaseForm):
-    username = TextField(u'Username', validators=[InputRequired()])
-    password = PasswordField(u'Passwort', validators=[InputRequired()])
-
-
-class LoginForm(RedirectForm, UserForm):
-
-    def validate(self):
-        from . import db
-        rv = Form.validate(self)
-        if not rv:
-            return False
-
-        user = db.hgetall("users:%s" % self.username.data)
-        if not user:
-            self.username.errors.append(u'Unbekannter Username')
-            return False
-
-        passw = self.password.data
-        if not ("hash" in user or hasher.verify(passw, user["hash"])):
-            self.password.errors.append(u'Falsches Passwort')
-            return False
-
-        self.user = user
-        return True
-
-
-class RegisterForm(UserForm):
-    email = EmailField(u'Email', validators=[Optional(),Email()])
-
-    def validate(self):
-        from . import db
-        rv = Form.validate(self)
-        if not rv:
-            return False
-
-        if db.exists("users:%s" % self.username.data):
-            self.username.errors.append(u"Name bereits vergeben")
-            return False
-
-        return True

File wetube/static/css/main.css

-html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:400;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block}html,body{background:#1b1b1b;position:relative;height:100%;overflow:hidden}header{width:100%;height:55px;background:#272727;color:#fff;z-index:3}header h1,header h2,header h3{width:40%;font-size:1.3em;font-family:Arial;padding:17px;float:left;color:#ccc}header h1:first-child,header h2:first-child,header h3:first-child{font-size:1.5em;color:#fff}header nav{height:100%}header nav>ul{float:right;text-align:right;width:auto;padding-right:10px;height:100%}header nav>ul>li{position:relative;height:15px;padding:20px 5px;display:inline-block;float:left}header nav>ul>li:hover{color:#f33}header .userlist{position:absolute;top:100%;right:50%;color:#ccc;-webkit-transform:translate(50%,0);-moz-transform:translate(50%,0);-ms-transform:translate(50%,0);-o-transform:translate(50%,0);transform:translate(50%,0);z-index:1;background:#333;border:6px solid #222;border-top:0;padding:10px;text-align:left;-webkit-transition:.2s ease-out all;-moz-transition:.2s ease-out all;-o-transition:.2s ease-out all;transition:.2s ease-out all;opacity:1}header .userlist.ng-hide{opacity:0}header .userlist.ng-hide-remove,header .userlist.ng-hide-add{display:block!important}header .userlist li{padding:5px}header a{color:#fff;text-decoration:none}header a:hover{color:#f33}#party{position:absolute;top:55px;bottom:0;right:0;left:0}#video{width:50%;height:100%;position:relative;float:left;background:#131313;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0}#video #playlist{list-style:none;height:-webkit-calc(100vh - 28.125vw - 55px);height:-moz-calc(100vh - 28.125vw - 55px);height:-ms-calc(100vh - 28.125vw - 55px);height:-o-calc(100vh - 28.125vw - 55px);height:calc(100vh - 28.125vw - 55px);overflow:auto}#video #playlist li{list-style-image:none;list-style-type:none;margin-left:0}#video #playlist li{position:relative;color:#bbb;padding:8px}#video #playlist li:not(:last-child){border-bottom:1px solid #050505;-webkit-box-shadow:#222 0 1px 0;-moz-box-shadow:#222 0 1px 0;box-shadow:#222 0 1px 0}#video #playlist span.title{position:absolute;left:105px;top:50%;width:-webkit-calc(50vw - 160px);width:-moz-calc(50vw - 160px);width:-ms-calc(50vw - 160px);width:-o-calc(50vw - 160px);width:calc(50vw - 160px);-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#video #playlist span.duration{position:absolute;top:50%;right:10px;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#video #playlist .crop{margin-right:10px;height:47.8125px;width:85px;overflow:hidden}#video #playlist .crop img{width:85px;margin-top:-7.96875px;margin-bottom:-7.96875px}#video form{margin-left:66px;height:42px}#video input{position:absolute;top:50%;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#video input[type=submit]{right:10px}#video input:not([type=submit]){width:-webkit-calc(50vw - 200px);width:-moz-calc(50vw - 200px);width:-ms-calc(50vw - 200px);width:-o-calc(50vw - 200px);width:calc(50vw - 200px)}#videowrapper{height:28.125vw;position:relative;background:#000}#chat{float:right;height:100%;width:50%;color:#bbb;background:#191919;line-height:1.5;font-family:Arial;position:relative;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0}#chat ul{position:absolute;left:0;right:0;bottom:55px;padding:20px}#chat li{margin:5px;padding:2px;width:-webkit-calc(100% - 20px);width:-moz-calc(100% - 20px);width:-ms-calc(100% - 20px);width:-o-calc(100% - 20px);width:calc(100% - 20px);display:inline-block;position:relative}#chat li .user{padding-right:10px;font-weight:700;color:#777;display:block;float:left;height:100%}#chat li .user:after{content:" >"}#chat form{height:35px;position:absolute;bottom:0;background:#272727;width:100%;padding:10px;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0}#chat input{background:#494949;border:1px solid #222;color:#000}#chat input:not([type=submit]){width:-webkit-calc(100% - 120px);width:-moz-calc(100% - 120px);width:-ms-calc(100% - 120px);width:-o-calc(100% - 120px);width:calc(100% - 120px)}input{padding:7px;font-size:.8em;margin:0;border:0;background:#222;outline:0;color:#bbb;border:1px solid #080808;-webkit-box-shadow:#222 0 0 3px;-moz-box-shadow:#222 0 0 3px;box-shadow:#222 0 0 3px}input[type=submit]::-moz-focus-inner{padding:0;border:0}::-webkit-scrollbar{width:12px;height:12px}::-webkit-scrollbar-track{background:#444}::-webkit-scrollbar-thumb{background:#090909}
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:400;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block}html,body{background:#1b1b1b;position:relative;height:100%;overflow:hidden}header{width:100%;height:55px;background:#272727;color:#ccc;z-index:3}header>h1,header>h2,header>h3{width:40%;font-size:1.3em;font-family:Arial;padding:17px;float:left;color:#fff}header>h1:first-child,header>h2:first-child,header>h3:first-child{font-size:1.5em}header nav{height:100%}header nav [class^=icon-]:before,header nav [class*=" icon-"]:before{vertical-align:-20%;font-size:24px;padding-right:5px}header nav>ul{float:right;text-align:right;width:auto;padding-right:20px;height:100%}header nav>ul>li{position:relative;height:25px;padding:15px;padding-left:25px;display:inline-block;float:left}header .cmenu{position:absolute;top:100%;left:50%;min-width:210px;z-index:1;-webkit-transform:translate(-50%,0);-moz-transform:translate(-50%,0);-ms-transform:translate(-50%,0);-o-transform:translate(-50%,0);transform:translate(-50%,0);-webkit-transition:.3s ease-in all;-moz-transition:.3s ease-in all;-o-transition:.3s ease-in all;transition:.3s ease-in all;overflow:hidden;color:#ccc;background:#333;border:6px solid #222;text-align:left;padding:15px;border-top:0;opacity:1}header .cmenu.ng-hide{opacity:0}header .cmenu.ng-hide-remove,header .cmenu.ng-hide-add{display:block!important}header .cmenu li{padding:5px}header .cmenu.signup{position:absolute;left:auto;right:-20px;min-width:400px;min-height:240px;-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}header .cmenu.signup form{position:absolute;bottom:15px;left:15px;border:3px solid transparent;right:15px}header .cmenu.signup form.ng-dirty.ng-valid{border:3px solid #227522}header .cmenu.signup form.ng-dirty.ng-invalid{border:3px solid #752222;background:#752222}header .cmenu.signup form span{display:block;padding:3px 3px 6px}header .cmenu.signup input{display:block;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:100%}header .cmenu.signup button{float:left;display:block;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:50%;border-top:0;background:#666;color:#000}header .cmenu.signup button:last-child{border-left:0}header .cmenu.signup h2{font-family:Arial;font-size:1.6em;margin-bottom:.4em}header .cmenu.signup p{margin-bottom:1em}header a{color:#ddd;text-decoration:none}#party{position:absolute;top:55px;bottom:0;right:0;left:0}#videowrapper{width:-webkit-calc(50% - 14px);width:-moz-calc(50% - 14px);width:-ms-calc(50% - 14px);width:-o-calc(50% - 14px);width:calc(50% - 14px);border-style:solid;border-color:#222;border-width:0 7px;position:relative;float:left;background:#131313;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0}#videowrapper #playlistwrapper{padding-bottom:-webkit-calc(100vh - 56.25% - 55px);padding-bottom:-moz-calc(100vh - 56.25% - 55px);padding-bottom:-ms-calc(100vh - 56.25% - 55px);padding-bottom:-o-calc(100vh - 56.25% - 55px);padding-bottom:calc(100vh - 56.25% - 55px);overflow:hidden;position:relative;height:0}#videowrapper #playlist{list-style:none;height:-webkit-calc(100vh - 28.125vw - 55px);height:-moz-calc(100vh - 28.125vw - 55px);height:-ms-calc(100vh - 28.125vw - 55px);height:-o-calc(100vh - 28.125vw - 55px);height:calc(100vh - 28.125vw - 55px);position:absolute;top:0;left:0;width:100%;height:100%;overflow:auto}#videowrapper #playlist li{list-style-image:none;list-style-type:none;margin-left:0}#videowrapper #playlist li{position:relative;color:#bbb;padding:8px}#videowrapper #playlist li:not(:last-child){border-bottom:1px solid #050505;-webkit-box-shadow:#222 0 1px 0;-moz-box-shadow:#222 0 1px 0;box-shadow:#222 0 1px 0}#videowrapper #playlist span.title{position:absolute;left:105px;top:50%;width:-webkit-calc(50vw - 230px);width:-moz-calc(50vw - 230px);width:-ms-calc(50vw - 230px);width:-o-calc(50vw - 230px);width:calc(50vw - 230px);-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#videowrapper #playlist span.duration{position:absolute;top:50%;right:10px;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#videowrapper #playlist .crop{margin-right:10px;height:47.8125px;width:85px;overflow:hidden}#videowrapper #playlist .crop img{width:85px;margin-top:-7.96875px;margin-bottom:-7.96875px}#videowrapper #playlist span[class^=icon-]{position:absolute;right:80px;top:50%;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#videowrapper form{height:48px}#videowrapper input{position:absolute;top:50%;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#videowrapper input[type=submit]{right:10px}#videowrapper input:not([type=submit]){width:-webkit-calc(50vw - 137px);width:-moz-calc(50vw - 137px);width:-ms-calc(50vw - 137px);width:-o-calc(50vw - 137px);width:calc(50vw - 137px)}#video{padding-bottom:56.25%;height:0;position:relative;background:#000;overflow:hidden}#video iframe,#video embed{position:absolute;top:0;left:0;height:100%;width:100%}#chat{float:right;height:100%;overflow:hidden;width:50%;color:#bbb;background:#191919;line-height:1.5;font-family:Arial;position:relative;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0}#chat #messages{position:absolute;left:0;right:0;bottom:55px;padding:20px}#chat ul{display:table}#chat li{margin:5px;padding:2px;width:-webkit-calc(100% - 20px);width:-moz-calc(100% - 20px);width:-ms-calc(100% - 20px);width:-o-calc(100% - 20px);width:calc(100% - 20px);display:table-row;position:relative}#chat li .user{padding-right:10px;display:table-cell;text-align:right;font-weight:700;height:100%;white-space:nowrap}#chat li .user:after{content:" >"}#chat li .msg{display:table-cell}#chat form{height:35px;position:absolute;bottom:0;background:#272727;width:100%;padding:10px;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0}#chat input,#chat button{border:1px solid #111;background-color:#353535}#chat input:not([type=submit]),#chat button:not([type=submit]){margin-right:5px;width:-webkit-calc(100% - 125px);width:-moz-calc(100% - 125px);width:-ms-calc(100% - 125px);width:-o-calc(100% - 125px);width:calc(100% - 125px)}.admin-role{font-family:Arial;font-weight:700;color:#b66}.mod-role{font-family:Arial;font-weight:700;color:#66b}.user-role{font-family:Arial;color:#bbb}[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none!important}[class^=icon-]:before,[class*=" icon-"]:before{color:#bbb}[class^=icon-]:not(.icon-view),[class*=" icon-"]:not(.icon-view){cursor:pointer}[class^=icon-]:not(.icon-view):hover:before,[class*=" icon-"]:not(.icon-view):hover:before{color:#f44}input,button{padding:7px;font-size:.8em;margin:0;border:0;background:#222;outline:0;color:#bbb;border:1px solid #080808;-webkit-box-shadow:#222 0 0 3px;-moz-box-shadow:#222 0 0 3px;box-shadow:#222 0 0 3px}input[type=submit]::-moz-focus-inner,button[type=submit]::-moz-focus-inner{padding:0;border:0}::-webkit-scrollbar{width:12px;height:12px}::-webkit-scrollbar-track{background:#444}::-webkit-scrollbar-thumb{background:#090909}@font-face{font-family:wetube_icons;src:url(../fonts/wetube_icons.eot?q4ianb);src:url(../fonts/wetube_icons.eot?#iefixq4ianb) format("embedded-opentype"),url(../fonts/wetube_icons.woff?q4ianb) format("woff"),url(../fonts/wetube_icons.ttf?q4ianb) format("truetype"),url(../fonts/wetube_icons.svg?q4ianb#wetube_icons) format("svg");font-weight:400;font-style:normal}[class^=icon-],[class*=" icon-"]{font-family:wetube_icons;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-video:before{content:"\e600"}.icon-add:before{content:"\e601"}.icon-remove:before{content:"\e602"}.icon-backspace:before{content:"\e603"}.icon-blocked:before{content:"\e604"}.icon-view:before{content:"\e605"}.icon-users:before{content:"\e606"}.icon-bubble:before{content:"\e608"}.icon-history:before{content:"\e609"}.icon-login:before{content:"\e379"}.icon-logout:before{content:"\e37a"}.icon-github:before{content:"\e4b8"}.icon-reload:before{content:"\e3a8"}.icon-shuffle:before{content:"\e3aa"}.icon-vote:before{content:"\e310"}.icon-forward:before{content:"\e389"}.icon-settings:before{content:"\e1e2"}.icon-spinner:before{content:"\e1a8"}.icon-play:before{content:"\e385"}.icon-pause:before{content:"\e386"}.icon-eject:before{content:"\e38e"}.icon-link:before{content:"\e47c"}

File wetube/static/fonts/wetube_icons.eot

Binary file added.

File wetube/static/fonts/wetube_icons.svg

+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by IcoMoon</metadata>
+<defs>
+<font id="wetube_icons" horiz-adv-x="512">
+<font-face units-per-em="512" ascent="480" descent="-32" />
+<missing-glyph horiz-adv-x="512" />
+<glyph unicode="&#x20;" d="" horiz-adv-x="256" />
+<glyph unicode="&#xe1a8;" d="M512 224c-0.639 33.431-7.892 66.758-21.288 97.231-13.352 30.5-32.731 58.129-56.521 80.96-23.776 22.848-51.972 40.91-82.492 52.826-30.502 11.962-63.298 17.676-95.699 16.983-32.405-0.641-64.666-7.687-94.167-20.678-29.524-12.948-56.271-31.735-78.367-54.788-22.112-23.041-39.58-50.354-51.093-79.899-11.557-29.531-17.064-61.26-16.373-92.635 0.643-31.38 7.482-62.574 20.067-91.103 12.544-28.55 30.738-54.414 53.055-75.774 22.305-21.377 48.736-38.252 77.307-49.36 28.559-11.152 59.223-16.451 89.571-15.763 30.354 0.645 60.481 7.277 88.038 19.457 27.575 12.141 52.558 29.74 73.183 51.322 20.641 21.57 36.922 47.118 47.627 74.715 6.517 16.729 10.94 34.2 13.271 51.899 0.623-0.036 1.249-0.060 1.881-0.060 17.673 0 32 14.326 32 32 0 0.898-0.047 1.786-0.119 2.666h0.119zM461.153 139.026c-11.736-26.601-28.742-50.7-49.589-70.59-20.835-19.905-45.5-35.593-72.122-45.895-26.614-10.339-55.145-15.226-83.442-14.541-28.302 0.649-56.298 6.868-81.91 18.237-25.625 11.333-48.842 27.745-67.997 47.856-19.169 20.099-34.264 43.882-44.161 69.529-9.935 25.642-14.614 53.107-13.932 80.378 0.651 27.276 6.664 54.206 17.627 78.845 10.929 24.65 26.749 46.985 46.123 65.405 19.365 18.434 42.265 32.935 66.937 42.428 24.669 9.53 51.068 14.003 77.313 13.322 26.25-0.653 52.114-6.459 75.781-17.017 23.676-10.525 45.128-25.751 62.812-44.391 17.698-18.629 31.605-40.647 40.695-64.344 9.124-23.696 13.391-49.029 12.712-74.248h0.119c-0.072-0.88-0.119-1.768-0.119-2.666 0-16.506 12.496-30.087 28.543-31.812-3.112-17.411-8.265-34.409-15.39-50.496z" />
+<glyph unicode="&#xe1e2;" d="M316.576-32h-121.153l-12.202 73.212c-1.679 0.669-3.35 1.36-5.010 2.075l-60.396-43.141-85.669 85.667 43.058 60.281c-0.738 1.707-1.453 3.425-2.142 5.152l-73.062 12.179v121.152l72.868 12.144c0.711 1.795 1.45 3.582 2.213 5.355l-42.935 60.108 85.669 85.669 59.991-42.851c1.821 0.788 3.654 1.549 5.499 2.282l12.118 72.716h121.153l12.119-72.715c1.845-0.733 3.679-1.494 5.499-2.282l59.991 42.851 85.668-85.669-42.936-60.107c0.765-1.774 1.503-3.561 2.214-5.357l72.869-12.144v-121.152l-73.062-12.178c-0.689-1.728-1.403-3.445-2.142-5.151l43.058-60.281-85.668-85.668-60.397 43.141c-1.661-0.714-3.331-1.405-5.011-2.074l-12.202-73.214zM227.615 6h56.77l10.617 63.705 10.894 3.6c7.31 2.416 14.506 5.396 21.389 8.856l10.246 5.151 52.548-37.533 40.143 40.143-37.479 52.472 5.184 10.26c3.493 6.913 6.502 14.146 8.941 21.499l3.608 10.874 63.524 10.588v56.771l-63.402 10.567-3.589 10.92c-2.438 7.423-5.454 14.721-8.963 21.69l-5.158 10.25 37.333 52.265-40.143 40.144-52.191-37.279-10.262 5.192c-6.998 3.541-14.333 6.584-21.801 9.048l-10.902 3.596-10.537 63.221h-56.77l-10.537-63.222-10.902-3.596c-7.465-2.463-14.799-5.507-21.799-9.049l-10.262-5.192-52.192 37.28-40.144-40.144 37.333-52.266-5.158-10.249c-3.508-6.971-6.523-14.268-8.962-21.69l-3.588-10.921-63.404-10.566v-56.77l63.526-10.589 3.607-10.876c2.437-7.346 5.445-14.578 8.941-21.496l5.185-10.26-37.481-52.474 40.144-40.142 52.549 37.534 10.247-5.154c6.877-3.459 14.073-6.438 21.385-8.855l10.894-3.6 10.618-63.703zM272 288h-32c-26.4 0-48-21.6-48-48v-32c0-26.4 21.6-48 48-48h32c26.4 0 48 21.6 48 48v32c0 26.4-21.6 48-48 48zM288 208c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16v-32z" />
+<glyph unicode="&#xe310;" d="M496 144c0 16.458-8.978 30.977-22.654 39.624 4.222 7.146 6.654 15.476 6.654 24.376 0 26.51-21.49 48-48 48h-93.904c11.228 40.455 16.904 82.296 16.904 125 0 36.944-30.056 67-67 67-36.943 0-67-30.056-67-67 0-51.986-19.361-91.975-57.545-118.855-10.5-7.392-22.371-13.743-35.455-19.033v44.888h-128v-320h128v31.499c29.945-1.966 44.484-9.461 58.595-16.747 14.044-7.253 28.566-14.752 53.405-14.752h160c26.51 0 48 21.49 48 48 0 6.249-1.207 12.213-3.38 17.688 20.385 5.543 35.38 24.171 35.38 46.312 0 8.9-2.432 17.23-6.654 24.376 13.676 8.647 22.654 23.166 22.654 39.624zM48 0c-8.836 0-16 7.163-16 16s7.164 16 16 16 16-7.163 16-16-7.164-16-16-16zM444 128h-28v-32h16c8.822 0 16-7.178 16-16s-7.178-16-16-16h-32v-32c8.822 0 16-7.178 16-16s-7.178-16-16-16h-160c-17.064 0-26.149 4.691-38.723 11.185-15.874 8.196-35.193 18.16-73.277 20.364v171.111c21.468 7.214 40.714 16.715 57.329 28.412 48.196 33.928 73.671 85.772 73.671 149.928 0 15.991 13.010 29 29 29s29-13.009 29-29c0-42.83-6.207-84.706-18.48-125-3.099-11.509-6.033-22.913-8.012-32h141.492c8.822 0 16-7.178 16-16s-7.178-16-16-16h-16v-32h28c10.841 0 20-7.327 20-16s-9.159-16-20-16z" />
+<glyph unicode="&#xe379;" d="M448 448h-352c-17.6 0-32-14.4-32-32v-128h64v96h288v-352h-288v96h-64v-128c0-17.6 14.399-32 32-32h352c17.6 0 32 14.4 32 32v416c0 17.6-14.4 32-32 32zM192 64l144 144-144 144v-96h-192v-96h192z" />
+<glyph unicode="&#xe37a;" d="M496 208l-144 144v-96h-192v-96h192v-96zM416 384v32c0 17.6-14.4 32-32 32h-352c-17.6 0-32-14.4-32-32v-416c0-17.6 14.399-32 32-32h352c17.6 0 32 14.4 32 32v32h-352v352h352z" />
+<glyph unicode="&#xe385;" d="M96 416l320-192-320-192z" />
+<glyph unicode="&#xe386;" d="M64 416h160v-384h-160zM288 416h160v-384h-160z" />
+<glyph unicode="&#xe389;" d="M256 48v160l-160-160v352l160-160v160l176-176z" />
+<glyph unicode="&#xe38e;" d="M0 96h512v-64h-512zM256 416l256-256h-512z" />
+<glyph unicode="&#xe3a8;" d="M437.011 405.010c-46.326 46.328-110.318 74.99-181.011 74.99-109.744 0-203.345-69.064-239.749-166.094l59.938-22.477c27.302 72.773 97.503 124.571 179.811 124.571 53.020 0 101.010-21.5 135.753-56.247l-71.753-71.753h192v192l-74.989-74.99zM256 32c-53.020 0-101.013 21.496-135.756 56.244l71.756 71.756h-192v-192l74.997 74.997c46.323-46.331 110.309-74.997 181.003-74.997 109.745 0 203.346 69.064 239.75 166.094l-59.938 22.477c-27.302-72.773-97.503-124.571-179.812-124.571z" />
+<glyph unicode="&#xe3aa;" d="M512 352l-128 128v-96c-65.386 0-115.376-15.604-152.825-47.704-2.625-2.25-5.142-4.55-7.581-6.887 13.76-19.082 24.358-38.758 33.886-57.545 24.161 29.201 59.027 48.136 126.52 48.136v-192c-108.223 0-132.563 48.68-163.378 110.311-17.153 34.306-34.89 69.78-67.796 97.985-37.45 32.1-87.44 47.704-152.826 47.704v-64c108.223 0 132.563-48.68 163.378-110.311 17.153-34.306 34.89-69.78 67.796-97.985 37.45-32.1 87.441-47.704 152.826-47.704v-96l128 128-128 128 128 128zM0 128v-64c65.386 0 115.375 15.604 152.825 47.704 2.625 2.249 5.142 4.55 7.581 6.888-13.76 19.081-24.359 38.758-33.886 57.545-24.16-29.201-59.026-48.137-126.52-48.137z" />
+<glyph unicode="&#xe47c;" d="M352 192l64 64v-256h-384v384h256l-64-64h-128v-256h256zM480 448v-176l-65.372 65.372-177.373-177.372h-45.255v45.256l177.372 177.372-65.372 65.372z" />
+<glyph unicode="&#xe4b8;" d="M0 201.795c0-23.199 2.17-44.19 6.511-62.967 4.339-18.777 10.348-35.092 18.026-48.946 7.678-13.854 17.442-26.039 29.293-36.554 11.85-10.516 24.703-19.112 38.556-25.788 13.853-6.675 29.668-12.099 47.444-16.273 17.776-4.173 35.928-7.094 54.455-8.764 18.527-1.669 38.89-2.503 61.089-2.503 22.366 0 42.814 0.834 61.34 2.503 18.527 1.67 36.721 4.592 54.58 8.764 17.859 4.172 33.756 9.596 47.694 16.273 13.938 6.677 26.873 15.272 38.808 25.788 11.935 10.515 21.783 22.702 29.543 36.554 7.76 13.852 13.811 30.168 18.151 48.946 4.34 18.778 6.51 39.768 6.51 62.967 0 41.394-13.854 77.197-41.559 107.408 1.502 4.006 2.879 8.554 4.131 13.645 1.252 5.091 2.42 12.351 3.505 21.782 1.085 9.431 0.668 20.321-1.252 32.673-1.919 12.352-5.466 24.953-10.642 37.806l-3.755 0.751c-2.671 0.5-7.053 0.375-13.145-0.376-6.092-0.751-13.186-2.253-21.281-4.507-8.095-2.253-18.527-6.593-31.296-13.019-12.769-6.426-26.247-14.479-40.435-24.16-24.368 6.676-57.834 10.015-100.396 10.015-42.396 0-75.778-3.339-100.147-10.015-14.188 9.681-27.75 17.734-40.685 24.16-12.935 6.426-23.242 10.766-30.92 13.019-7.677 2.254-14.855 3.714-21.531 4.382-6.677 0.668-10.892 0.876-12.644 0.626-1.752-0.25-3.13-0.543-4.131-0.876-5.174-12.853-8.721-25.453-10.64-37.806-1.919-12.352-2.337-23.243-1.252-32.673 1.085-9.43 2.254-16.691 3.505-21.782 1.252-5.091 2.629-9.639 4.131-13.645-27.707-30.211-41.561-66.013-41.561-107.408zM62.842 138.953c0 24.035 10.933 46.068 32.798 66.097 6.509 6.010 14.104 10.557 22.783 13.646 8.679 3.088 18.485 4.84 29.418 5.258 10.933 0.417 21.406 0.334 31.421-0.251 10.014-0.584 22.366-1.377 37.054-2.378 14.688-1.002 27.374-1.502 38.056-1.502 10.683 0 23.368 0.5 38.056 1.502 14.689 1.001 27.039 1.794 37.055 2.378 10.015 0.585 20.487 0.668 31.421 0.251 10.933-0.418 20.738-2.17 29.419-5.258 8.678-3.088 16.272-7.635 22.782-13.646 21.865-19.697 32.799-41.728 32.799-66.097 0-14.356-1.795-27.081-5.384-38.182-3.589-11.1-8.179-20.405-13.771-27.915-5.592-7.51-13.352-13.895-23.284-19.153-9.931-5.258-19.611-9.305-29.042-12.144-9.431-2.837-21.533-5.049-36.304-6.635-14.773-1.586-27.958-2.546-39.559-2.879-11.6-0.334-26.33-0.501-44.189-0.501-17.859 0-32.589 0.167-44.189 0.501-11.6 0.333-24.787 1.293-39.558 2.879-14.771 1.586-26.872 3.798-36.303 6.635-9.43 2.839-19.111 6.887-29.042 12.144-9.931 5.257-17.693 11.641-23.284 19.153-5.591 7.511-10.182 16.815-13.77 27.915-3.589 11.101-5.383 23.828-5.383 38.182zM320 144c0 26.51 14.327 48 32 48s32-21.49 32-48c0-26.51-14.327-48-32-48-17.673 0-32 21.49-32 48zM128 144c0 26.51 14.327 48 32 48s32-21.49 32-48c0-26.51-14.327-48-32-48-17.673 0-32 21.49-32 48z" />
+<glyph unicode="&#xe600;" d="M490.594 399.946c-71.816 10.325-151.166 16.054-234.593 16.054-83.43 0-162.778-5.729-234.597-16.054-13.765-53.863-21.404-113.375-21.404-175.946 0-62.57 7.639-122.083 21.404-175.945 71.819-10.326 151.168-16.055 234.597-16.055 83.427 0 162.776 5.729 234.593 16.055 13.766 53.862 21.406 113.375 21.406 175.945 0 62.571-7.64 122.083-21.406 175.946zM192.001 128v192l160-96-160-96z" />
+<glyph unicode="&#xe601;" d="M256 480c-141.385 0-256-114.615-256-256s114.615-256 256-256 256 114.615 256 256-114.615 256-256 256zM256 32c-106.039 0-192 85.961-192 192s85.961 192 192 192c106.039 0 192-85.961 192-192 0-106.039-85.961-192-192-192zM384 192h-96v-96h-64v96h-96v64h96v96h64v-96h96z" />
+<glyph unicode="&#xe602;" d="M256 480c-141.385 0-256-114.615-256-256s114.615-256 256-256 256 114.615 256 256-114.615 256-256 256zM256 32c-106.039 0-192 85.961-192 192s85.961 192 192 192c106.039 0 192-85.961 192-192 0-106.039-85.961-192-192-192zM352 173.256v-45.256h-45.256l-50.744 50.744-50.745-50.744h-45.255v45.256l50.745 50.744-50.745 50.745v45.255h45.255l50.745-50.745 50.744 50.745h45.256v-45.255l-50.744-50.745z" />
+<glyph unicode="&#xe603;" d="M512 112l-64 208h-245.49l-112-112 112-112h245.49v224zM512 384v0-352h-336l-176 176 176 176h336zM370.744 288h45.256v-45.256l-34.744-34.744 34.744-34.744v-45.256h-45.256l-34.744 34.744-34.744-34.744h-45.256v45.255l34.745 34.745-34.745 34.745v45.255h45.255l34.745-34.745z" />
+<glyph unicode="&#xe604;" d="M0 224c0-141.385 114.615-256 256-256 141.386 0 256 114.615 256 256s-114.614 256-256 256c-141.385 0-256-114.615-256-256zM448 224c0-36.618-10.256-70.84-28.044-99.956l-263.911 263.912c29.115 17.789 63.337 28.044 99.955 28.044 106.038 0 192-85.961 192-192zM64 224c0 36.618 10.256 70.839 28.045 99.956l263.911-263.912c-29.117-17.789-63.338-28.044-99.956-28.044-106.038 0-192 85.961-192 192z" />
+<glyph unicode="&#xe605;" d="M256 70.5c-74.095 0-140.032 36.302-172.742 57.941-21.921 14.502-42.094 31.101-56.803 46.74-17.801 18.927-26.455 34.895-26.455 48.818 0 13.924 8.654 29.892 26.456 48.819 14.709 15.639 34.882 32.238 56.803 46.74 32.709 21.639 98.647 57.942 172.741 57.942 74.094 0 140.031-36.303 172.741-57.942 21.92-14.502 42.093-31.101 56.803-46.74 17.801-18.926 26.455-34.894 26.456-48.815-0.001-13.927-8.655-29.895-26.456-48.822-14.709-15.639-34.882-32.237-56.803-46.739-32.71-21.64-98.646-57.942-172.741-57.942zM48.817 224c5.511-10.143 28.255-34.967 67.025-59.47 27.2-17.19 81.577-46.029 140.158-46.029 58.582 0 112.959 28.839 140.158 46.030 38.77 24.503 61.514 49.326 67.024 59.469-5.512 10.143-28.255 34.966-67.025 59.47-27.199 17.191-81.576 46.030-140.157 46.030s-112.958-28.839-140.157-46.030c-38.77-24.503-61.515-49.327-67.026-59.47zM192 224c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64-35.346 0-64 28.654-64 64z" />
+<glyph unicode="&#xe606;" d="M96 288c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64-35.346 0-64 28.654-64 64zM288 384c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64-35.346 0-64 28.654-64 64zM160 192c-88.365 0-160-85.961-160-192h48c0 20.471 3.312 40.247 9.844 58.779 6.155 17.461 14.866 33.023 25.894 46.257 20.939 25.126 48.022 38.964 76.262 38.964s55.324-13.838 76.263-38.964c11.027-13.233 19.739-28.796 25.894-46.257 6.532-18.532 9.843-38.308 9.843-58.779h48c0 106.039-71.634 192-160 192zM352 288c-47.088 0-89.423-24.413-118.701-63.264 3.598-1.559 7.168-3.228 10.705-5.022 10.877-5.521 21.24-12.047 31.037-19.535 0.233 0.285 0.462 0.575 0.697 0.857 20.939 25.126 48.023 38.964 76.262 38.964 28.24 0 55.324-13.838 76.263-38.964 11.027-13.233 19.739-28.796 25.894-46.257 6.532-18.532 9.843-38.308 9.843-58.779h48c0 106.039-71.634 192-160 192z" />
+<glyph unicode="&#xe608;" d="M96 352h320v-32h-320zM96 288h256v-32h-256zM96 224h128v-32h-128zM464 448h-416c-26.4 0-48-21.6-48-48v-256c0-26.4 21.6-48 48-48h80v-128l153.6 128h182.4c26.4 0 48 21.6 48 48v256c0 26.4-21.6 48-48 48zM448 160h-189.571l-98.429-87.357v87.357h-96v224h384v-224z" />
+<glyph unicode="&#xe609;" d="M256 352v-160h128v32h-96v128zM288 448c-123.712 0-224-100.288-224-224h-64l80-96 80 96h-64c0 106.039 85.961 192 192 192s192-85.961 192-192c0-106.039-85.961-192-192-192-10.904 0-21.592 0.923-32 2.67v-32.385c10.453-1.495 21.134-2.285 32-2.285 123.712 0 224 100.288 224 224s-100.288 224-224 224z" />
+</font></defs></svg>

File wetube/static/fonts/wetube_icons.ttf

Binary file added.

File wetube/static/fonts/wetube_icons.woff

Binary file added.

File wetube/static/js/dist.js

-angular.module("wetube.services",["wetube.services.socket","wetube.services.youtube","wetube.services.sync_time"]),angular.module("wetube.directives",["wetube.directives.player","wetube.directives.userlist"]),angular.module("wetube.filters",["wetube.filters.duration"]),angular.module("wetube.controllers",["wetube.controllers.UserController","wetube.controllers.ChatController","wetube.controllers.PlaylistController","wetube.controllers.VideoController"]),angular.module("wetube",["ngAnimate","wetube.controllers","wetube.services","wetube.directives","wetube.filters"]).run(["socket","sync_time",function(e,t){e.emit("join"),t.initiate()}]);
+angular.module("wetube.services",["wetube.services.socket","wetube.services.youtube","wetube.services.sync_time"]),angular.module("wetube.directives",["wetube.directives.player","wetube.directives.ensureUnique","wetube.directives.userlist","wetube.directives.playlist","wetube.directives.videoform"]),angular.module("wetube.filters",["wetube.filters.duration","wetube.filters.reverse"]),angular.module("wetube.controllers",["wetube.controllers.UserController","wetube.controllers.ChatController","wetube.controllers.PlaylistController","wetube.controllers.VideoController","wetube.controllers.HistoryController"]),angular.module("wetube",["ngAnimate","wetube.controllers","wetube.services","wetube.directives","wetube.filters"]).run(["socket","sync_time",function(e,t){t.initiate()}]);
 angular.module("wetube.controllers.ChatController",["wetube.services.socket"]).controller("ChatController",["$scope","socket","$log",function(e,s){e.messages=[],s.on("message",function(s){e.messages.push(s)}),e.addMessage=function(){var o=e.message_input;s.emit("message",o),e.message_input=""}}]);
-angular.module("wetube.controllers.PlaylistController",["wetube.services.socket"]).controller("PlaylistController",["$scope","socket",function(o,e){e.on("playlist",function(e){o.playlist=e}),o.addVideo=function(){var l=o.video_input;e.emit("add_video",l),o.video_input=""}}]);
-angular.module("wetube.controllers.UserController",["wetube.services.socket"]).controller("UserController",["$scope","socket",function(e,s){e.showUserlist=!1,e.toggleUserlist=function(){e.showUserlist=!e.showUserlist},s.on("connected",function(s){e.users=s}),s.on("user_leaved",function(s){e.users=e.users.filter(function(e){return e.name!==s.name})}),s.on("user_joined",function(s){e.users.push(s)})}]);
-angular.module("wetube.controllers.VideoController",["wetube.services.socket","wetube.services.youtube"]).controller("VideoController",["$scope","socket","youtube",function(e,o,t){o.on("playback",function(e){t.load(e.id,e.started)})}]);
+angular.module("wetube.controllers.HistoryController",["wetube.services.socket"]).controller("HistoryController",["$scope","socket",function(o,r){o.history=[],r.on("history",function(r){o.history=r}),r.on("playback",function(r){o.current_video&&(o.history.push(angular.copy(o.current_video)),o.history>20&&o.history.splice(0,1)),o.current_video=r})}]);
+angular.module("wetube.controllers.PlaylistController",["wetube.services.socket"]).controller("PlaylistController",["$scope","socket",function(o,i){o.playlist=[],o.history=[],i.on("playlist",function(i){o.showAddVideo=void 0,o.playlist=i}),i.on("history",function(i){o.history=i}),i.on("playback",function(i){o.current_video&&(o.history.push(angular.copy(o.current_video)),o.history>20&&o.history.splice(0,1)),o.current_video=i}),i.on("remove",function(i){index=o.playlist.map(function(o,e){return o.gid===i?e:void 0}).filter(isFinite)[0],console.log("Remove: "+index),o.playlist.splice(index,1)}),i.on("append",function(i,e){index=o.playlist.map(function(o,i){return o.gid===e?i:void 0}).filter(isFinite)[0]+1,console.log("Append after: "+index),o.playlist.splice(index,0,i)}),o.input={},o.addVideo=function(e){var n=o.input.video;console.log(n),o.input.video="",o.showAddVideo=void 0,i.emit("add_video",{url:n,after:e})},o.removeVideo=function(o,e){console.log(e),o.stopPropagation(),i.emit("remove_video",e),console.log("hahaha")},o.toggleAddVideo=function(i){o.input.video="",o.showAddVideo=o.showAddVideo===i?void 0:i}}]);
+angular.module("wetube.controllers.UserController",["wetube.services.socket"]).controller("UserController",["$rootScope","$scope","socket",function(o,n,i){n.login=function(){navigator.id.request()},n.logout=function(){navigator.id.logout()},navigator.id.watch({loggedInUser:null,onlogin:function(o){i.emit("signin",o)},onlogout:function(){i.emit("logout"),window.location.reload()}}),i.on("login_successful",function(e){n.showSignUp=!1,e.isMod=function(){return"mod"===this.role||"admin"===this.role},o.user=e,i.emit("join")}),i.on("users",function(o){n.users=o}),i.on("views",function(o){n.views=o}),i.on("user_leaved",function(o){n.users=n.users.filter(function(n){return n.name!==o.name})}),i.on("user_joined",function(o){n.users.push(o)}),n.input={},n.dialog=!1,n.submitSignUp=function(){i.emit("signup",n.input.name),n.dialog=!1};var e=function(o){return function(){n.dialog!==o&&"signup"!==n.dialog?n.dialog=o:n.dialog===o&&(n.dialog=!1)}};i.on("signup_needed",function(){n.dialog="signup"}),n.toggleUserlist=e("userlist"),n.toggleHistory=e("history")}]);
+angular.module("wetube.controllers.VideoController",["wetube.services.socket","wetube.services.youtube"]).controller("VideoController",["$rootScope","$scope","socket","youtube",function(e,o,t,n){e.events=[],t.on("playback",function(o){n.load(o.id,o.started),o.now=(new Date).getTime(),e.events.push(o)})}]);
+angular.module("wetube.directives.ensureUnique",["wetube.services.socket"]).directive("ensureUnique",["socket",function(e){return{require:"ngModel",link:function(n,i,u,t){t.$parsers.unshift(function(n){return e.emit("checkname",n),t.$setValidity("unique",!0),n}),e.on("checkname",function(e){t.$setValidity("unique",e)})}}}]);
 angular.module("wetube.directives.player",["wetube.services.youtube"]).directive("player",["youtube",function(e){return{restrict:"AE",link:function(t,u){e.bind(u[0])}}}]);
+angular.module("wetube.directives.playlist",[]).directive("playlist",["youtube",function(){return{restrict:"AE",templateUrl:"static/templates/playlist.html",replace:!0}}]);
 angular.module("wetube.directives.userlist",[]).directive("userlist",["youtube",function(){return{restrict:"AE",templateUrl:"static/templates/userlist.html",replace:!0}}]);
+angular.module("wetube.directives.videoform",["wetube.services.socket"]).directive("videoform",["youtube",function(){return{restrict:"AE",templateUrl:"static/templates/videoform.html",replace:!0}}]);
 angular.module("wetube.filters.duration",[]).filter("duration",function(){return function(e){return angular.isNumber(Math.round(e/1e3))&&(e=new Date(Math.abs(e))),new Date(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())}});
+angular.module("wetube.filters.reverse",[]).filter("reverse",function(){return function(e){return angular.isArray(e)?e.slice().reverse():e}});
 angular.module("wetube.services.socket",[]).factory("socket",["$rootScope",function(n){var o=io.connect("/app");return{on:function(t,c){o.on(t,function(){var t=arguments;n.$apply(function(){c.apply(o,t)})})},emit:function(t,c,e){o.emit(t,c,function(){var t=arguments;n.$apply(function(){e&&e.apply(o,t)})})}}}]);
-angular.module("wetube.services.sync_time",["wetube.services.socket"]).service("sync_time",["$rootScope","$interval","socket",function(e,t,i){var n,c,s=[];this.time_diff=0,this.latency=0,e.time_diff=this.time_diff,e.latency=this.latency,i.on("sync_time",function(t,i){c=(new Date).getTime()-i,n=Math.ceil((t+c)/2),s.push(c-n),s.length>100&&s.shift();var f=Math.ceil(s.length/2)-1;this.time_diff=s.slice(0).sort(function(e,t){return e-t})[f],this.latency=n,e.time_diff=this.time_diff,e.latency=this.latency}),this.initiate=function(){i.emit("sync_time",(new Date).getTime()),t(function(){i.emit("sync_time",(new Date).getTime())},1e3)}}]);
-angular.module("wetube.services.youtube",[]).factory("youtube",["$rootScope","$window","$timeout","$interval","$log","sync_time",function(e,t,r,a,i,n){var d=e.$new(!0);return d.ready=!1,d.create=!1,d.player=null,d.id=null,d.video=null,d.started=null,t.onYouTubeIframeAPIReady=function(){i.info("Youtube API is ready"),d.ready=!0},a(function(){if(d.player&&d.created){var e=(new Date).getTime()-n.time_diff,t=(e-d.started)/1e3,r=t-d.player.getCurrentTime();(r>2||-2>r)&&d.player.seekTo(t)}},200),{bind:function(e){i.info("Bind youtube to player element"),e.id="youtube_player",d.id=e.id},load:function(e,t){if(d.video=e,d.started=t,d.player&&d.created){var r=(new Date).getTime()-n.time_diff,a=(r-t)/1e3;d.player.loadVideoById(e,a)}else this.create_player()},create_player:function(){d.ready||r(this.create_player,100),d.id&&d.video&&(i.info("Create player!"),d.player&&d.player.destroy(),d.player=new YT.Player(d.id,{videoId:d.video,width:"100%",height:"100%",playerVars:{modestbranding:1},events:{onReady:function(){d.created=!0;var e=(new Date).getTime()-n.time_diff,t=(e-d.started)/1e3;d.player.seekTo(t,!0)}}}))},player:function(){return d.player}}}]);
+angular.module("wetube.services.sync_time",["wetube.services.socket"]).service("sync_time",["$rootScope","$interval","socket",function(e,t,i){var n,s,f,c=[];this.time_diff=0,this.latency=0,e.time_diff=this.time_diff,e.latency=this.latency,i.on("sync_time",function(t,i){now=(new Date).getTime(),s=now-i,n=now-f,offset=(i-f+(i-now))/2,c.push(offset),c.length>100&&c.shift();var o=Math.ceil(c.length/2)-1;this.time_diff=Math.floor(c.slice(0).sort(function(e,t){return e-t})[o]),this.latency=n,e.time_diff=this.time_diff,e.latency=this.latency}),t(function(){e.time=(new Date).getTime()+e.time_diff,e.now=(new Date).getTime()},300),this.initiate=function(){f=(new Date).getTime(),i.emit("sync_time",f),t(function(){f=(new Date).getTime(),i.emit("sync_time",f)},1e3)}}]);
+angular.module("wetube.services.youtube",[]).factory("youtube",["$rootScope","$window","$timeout","$interval","$log","sync_time",function(e,t,r,a,i,n){var d=e.$new(!0);return d.ready=!1,d.create=!1,d.player=null,d.id=null,d.video=null,d.started=null,t.onYouTubeIframeAPIReady=function(){i.info("Youtube API is ready"),d.ready=!0},a(function(){if(d.player&&d.created){var e=(new Date).getTime()-n.time_diff,t=(e-d.started)/1e3,r=t-d.player.getCurrentTime();(r>2||-2>r)&&d.player.seekTo(t)}},200),{bind:function(e){i.info("Bind youtube to player element"),e.id="youtube_player",d.id=e.id},load:function(e,t){if(d.video=e,d.started=t,d.player&&d.created){var r=(new Date).getTime()-n.time_diff,a=(r-t)/1e3;d.player.loadVideoById(e,a)}else this.create_player()},create_player:function(){d.ready||r(this.create_player,200),d.id&&d.video&&(i.info("Create player!"),d.player&&d.player.destroy(),d.player=new YT.Player(d.id,{videoId:d.video,width:"100%",height:"100%",playerVars:{modestbranding:1},events:{onReady:function(){d.created=!0;var e=(new Date).getTime()-n.time_diff,t=(e-d.started)/1e3;d.player.seekTo(t,!0)}}}))},player:function(){return d.player}}}]);

File wetube/static/templates/playlist.html

+<ul id=playlist>
+  <li ng-cloak ng-repeat-start="video in playlist track by video.gid" ng-click="toggleAddVideo(video.gid)">
+    <div class=crop>
+      <img ng-src="{{ video.thumbnail }}" />
+    </div>
+    <span class=title>{{ video.title }}</span>
+    <span ng-cloak ng-if="user.isMod()" ng-click="removeVideo($event, video.gid)" class="icon-remove"></span>
+    <span class=duration>{{ video.duration | duration | date:'H:mm:ss'  }}</span>
+  </li>
+  <videoform ng-repeat-end ng-if="video.gid === showAddVideo && user.isMod()"></videoform>
+  <videoform ng-if="playlist.length < 1"></videoform>
+</ul>

File wetube/static/templates/userlist.html

-<ul class="userlist" ng-show="showUserlist">
-  <li ng-repeat="user in users">{{ user.name }}</li>
-</ul>
+<div class="cmenu" ng-show="dialog == 'userlist'" ng-cloak>
+  <ul>
+    <li ng-repeat="user in users | orderBy:['role','name']"
+        ng-class="[user.role + '-role']">
+      {{ user.name }}
+    </li>
+    <li ng-if="!users.length">Keine User eingeloggt</li>
+  </ul>
+</div>

File wetube/static/templates/videoform.html

+<li>
+  <form ng-submit="addVideo(video.gid)">
+    <input type="text" ng-model="input.video" placeholder="Youtube Video URL">
+    <input type="submit" value="Hinzufügen">
+  </form>
+</li>

File wetube/templates/base.html

-{%- from 'macros/helpers.html' import link -%}
 <!doctype html>
 <html ng-app="wetube">
 <meta charset=utf-8>
+<meta http-equiv="X-UA-Compatible" content="IE=Edge">
 <link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/main.css') }}">
 
 <title>{% block title %}Wetube{% endblock %}</title>
   <h1>{{ self.title() }}</h1>
 
   {% block header %}
+  {%- raw %}
   <nav>
-    <ul>
-      {%- if logged_in %}
-      <li>{{ link("logout", "Logout") }}</li>
-      {%- else %}
-      <li>{{ link("login", "Login/Register") }}</li>
-      {%- endif %}
+    <ul ng-controller="UserController">
+      <li ng-cloak ng-if="views">
+        <span class="icon-view">{{ views }}</span>
+      </li>
+      <li ng-click="toggleUserlist()" ng-cloak ng-if="users !== undefined" ng-class="{active: showUserlist}">
+        <span class="icon-users">{{ users.length }}</span>
+        <userlist></userlist>
+      </li>
+      <li ng-click="toggleHistory()" ng-controller="HistoryController">
+        <span class="icon-history"></span>
+        <div ng-cloak class="cmenu" ng-show="dialog == 'history'">
+          <ul>
+            <li ng-repeat="played_video in history | reverse">{{ played_video.title }}</li>
+          </ul>
+        </div>
+      </li>
+      <li>
+        <span class="icon-settings"></span>
+      </li>
+      <li ng-cloak ng-if="user" ng-click="logout()">
+        <span class="icon-logout"></span>
+      </li>
+      <li ng-if="!user">
+        <span class="icon-login" ng-click="login()"></span>
+        <div ng-cloak class="signup cmenu" ng-if="dialog == 'signup'">
+          <h2>Registrieren</h2>
+          <p>Jetzt muss du dir nur noch einen freien Spitznamen aussuchen, damit die Leute im Chat wissen, mit wem Sie schreiben</p>
+          <p>Sobald du drauf lostippst, bekommst du angezeigt angezeigt, ob der Name frei ist.</p>
+          <form name="form" ng-submit="submitSignUp()">
+            <span ng-show="form.username.$error.minlength || form.username.$error.maxlength">
+              Der Name muss zwischen 3 und 15 Zeichen lang sein
+            </span>
+            <span ng-show="form.username.$error.unique">
+              Der Name wird bereits von jemandem verwendet
+            </span>
+            <span ng-show="form.username.$error.pattern">
+              Du kannst nur alphanumerische Zeichen, sowie "-" und "_" zum trennen, verwenden. Beispiel: "mein_name-2"
+            </span>
+            <input name="username" type="text" ng-model="input.name" placeholder="Spitzname" required
+                   ng-minlength=3 ng-maxlength=15 ng-pattern="/^[a-zA-Z0-9_-]+$/" ensure-unique>
+            <button ng-disabled="form.$invalid" type="submit">Registrieren</button>
+            <button>Abbrechen</button>
+          </form>
+        </div>
+      </li>
     </ul>
   </nav>
+  {%- endraw %}
   {% endblock %}
 </header>
 
 <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.js"></script>
 <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular-animate.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js"></script>
+<script src="https://login.persona.org/include.js"></script>
 <script src="//www.youtube.com/iframe_api"></script>
 <script src="{{ url_for('static', filename='js/dist.js') }}"></script>
 </html>

File wetube/templates/index.html

 
 {% block title %}{{ super() }}{% endblock %}
 
-{% block header %}
-  <nav>
-    <ul>
-      {%- raw %}
-      <li ng-controller="UserController" ng-click="toggleUserlist()">{{ users.length }} Users online
-        <userlist></userlist>
-      </li>
-      <li><stats></stats></li>
-      {%- endraw %}
-      <li>{{ link("logout", "Logout") }}</li>
-    </ul>
-  </nav>
-{% endblock %}
-
 {% block content -%}
   <section id=party>
     {%- raw %}
-    <section id="video" ng-controller="VideoController">
-      <div id="videowrapper">
+    <section id="videowrapper" ng-controller="VideoController">
+      <div id="video">
         <player></player>
       </div>
-      <section ng-controller="PlaylistController">
-        <ul id=playlist>
-          <li ng-repeat="video in playlist">
-            <div class=crop><img ng-src="{{ video.thumbnail }}" /></div>
-            <span class=title>{{ video.title }}</span>
-            <span class=duration>{{ video.duration | duration | date:'H:mm:ss'  }}</span>
-          </li>
-          <li>
-            <form ng-submit="addVideo()">
-              <input type="text" ng-model="video_input" placeholder="Youtube Video URL">
-              <input type="submit" value="Hinzufügen">
-            </form>
-          </li>
-        </ul>
+      <section id="playlistwrapper" ng-controller="PlaylistController">
+        <playlist></playlist>
       </section>
     </section>
 
     <section id="chat" ng-controller="ChatController">
       <div id=messages>
         <ul>
-          <li ng-repeat="message in messages">
-            <span class=user>{{ message.user }}</span>{{ message.msg }}
+          <li ng-cloak ng-repeat="message in messages">
+            <span ng-class="[message.user.role + '-role']" class=user>{{ message.user.name }}</span>
+		        <span class="msg">{{ message.msg }}</span>
           </li>
         </ul>
       </div>

File wetube/utils.py

 _string_inc_re = re.compile(r'(\d+)$')
 
 
-def increment_string(string):
-    """Increment a string by one:
-
-    >>> increment_string(u'test')
-    u'test2'
-    >>> increment_string(u'test2')
-    u'test3'
-    """
-    match = _string_inc_re.search(string)
-    if match is None:
-        return string + u'2'
-    return string[:match.start()] + unicode(int(match.group(1)) + 1)
-
-
-def slugify(text, delim=u'-'):
-    """Generates an ASCII-only slug."""
-    result = []
-    for word in _punct_re.split(text.lower()):
-        word = long_encode(word)[0] # codec does not work in pypy?!
-        if word:
-            result.append(word)
-    return unicode(delim.join(result))
-
-
 def compress_uuid(uuid):
     characters = string.letters + string.digits
     uuid_int = getattr(uuid, 'int') or uuid
     return UUID(int=number)
 
 
-def logged_in():
-    if "user" in session and session["user"] is not None:
-        db = StrictRedis()
-        user = db.hgetall("users:%s" % session["user"])
-        return bool(user)
-
-
-def login_required(func):
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        if logged_in():
-            return func(*args, **kwargs)
-        return redirect(url_for('login'))
-    return wrapper
-
-
 def extract(d, keys):
     return dict((k, d[k]) for k in keys if k in d)

File wetube/websockets.py

 #!/usr/bin/env python
 # encoding: utf-8
 
+import re
 import math
 from gevent import monkey; monkey.patch_all()
 from time import time
 from socketio.namespace import BaseNamespace
 from socketio.mixins import RoomsMixin, BroadcastMixin
 
-from flask import request
+import requests
 from redis import StrictRedis
 from . import app
 from .utils import extract
 
 
-class WetubeSocket(BaseNamespace, BroadcastMixin):
+class Pubsub(object):
 
-    def __init__(self, *args, **kwargs):
-        self.db = StrictRedis()
-        self.r = self.db.pubsub()
-        self.listen = {}
-        super(WetubeSocket, self).__init__(*args, **kwargs)
-        self.session["user"] = None
+    def __init__(self):
+        self._publish_attr = False
+        self._db = StrictRedis()
+        self._r = self._db.pubsub()
+        self._callbacks = {}
+        self._publish_attr = True
 
-    def get_initial_acl(self):
-        return set(['recv_connect'])
+    def subscribe(self, event, func=None):
+        if event not in self._callbacks:
+            if func:
+                self._callbacks[event] = func
+            self._r.subscribe(event)
+
+    def listener(self):
+        for event in self._r.listen():
+            channel = event["channel"]
+            if event["type"] == "message":
+                data = loads(event["data"])
+                object.__setattr__(self, channel, data)
+                if channel in self._callbacks:
+                    func = self._callbacks[channel]
+                    func(data)
+
+    def __getattr__(self, name):
+        if name in self._r.channels:
+            return None
+        raise AttributeError
+
+    def __setattr__(self, name, value):
+        object.__setattr__(self, name, value)
+        if getattr(self, '_publish_attr', None):
+            if name == "_publish_attr":
+                return
+            self._db.publish(name, dumps(value))
+            self._r.subscribe(name)
+
+
+class SendMixin(object):
 
     def send_connected(self, data=None):
         users = []
-        usernames = self.db.smembers("connected")
-        for name in usernames:
-            user = self.db.hgetall("users:%s" % name)
+        emails = self.db.smembers("connected")
+        for email in emails:
+            user = self.db.hgetall("users:%s" % email)
             users.append(extract(user, ('name', 'role')))
-        self.emit("connected", users or [])
+        self.emit("users", users or [])
+
+    def send_connections(self, data=None):
+        if not data:
+            data = self.db.get("connections")
+        self.emit("views", int(data))
+
+    def send_user_connections(self, data=None):
+        self.send_connections()
+
+    def send_joined_user(self, data):
+        data = extract(data, ('name', 'role'))
+        self.emit("user_joined", data)
+
+    def send_leaved_user(self, data):
+        data = extract(data, ('name', 'role'))
+        self.emit("user_leaved", data)
 
     def send_playlist(self, data=None):
         videos = []
-        ids = self.db.lrange('playlist', 1, -1)
-        for id in ids:
-            videos.append(self.db.hgetall('video:%s' % id))
+        gids = self.db.lrange('playlist', 1, -1)
+        for gid in gids:
+            videos.append(self.db.hgetall('video:%s' % gid))
         if videos:
             self.emit('playlist', videos)
 
     def send_playback(self, data=None):
-        id = self.db.lindex('playlist', 0)
-        if not id:
+        gid = self.db.lindex('playlist', 0)
+        if not gid:
             return
-        video = self.db.hgetall('video:%s' % id)
+        video = self.db.hgetall('video:%s' % gid)
         self.emit("playback", video)
 
-    def send_message(self, data=None):
-        print "loool"
-        self.emit('message', loads(data))
+    def send_history(self, data=None):
+        videos = []
+        gids = self.db.lrange('history', 1, 10)
+        for gid in gids:
+            videos.append(self.db.hgetall('video:%s' % gid))
+        if videos:
+            self.emit('history', videos)
 
-    def subscribe(self, event, func):
-        if event not in self.listen:
-            self.listen[event] = func
-            self.r.subscribe(event)
+    def send_append(self, data):
+        self.emit("append", data["video"], data["after"])
 
-    def listener(self):
-        for event in self.r.listen():
-            channel = event["channel"]
-            if channel in self.listen and event["type"] == "message":
-                func = self.listen[channel]
-                func(event["data"])
+    def send_remove(self, data):
+        self.emit("remove", data)
 
-    def on_sync_time(self, client_time):
-        server_time = int(math.ceil(time() * 1000))
-        difference = server_time - client_time
+    def send_message(self, data):
+        self.emit('message', data)
 
-        key = "users:%s" % self.session["user"]
-        self.emit('sync_time', difference, server_time)
 
-    def on_join(self, data):
-        if self.allowed_methods:
-            self.allowed_methods.update(('on_message', ))
+class WetubeSocket(BaseNamespace, SendMixin):
+
+    def __init__(self, *args, **kwargs):
+        self.db = StrictRedis()
+        self.pb = Pubsub()
+        super(WetubeSocket, self).__init__(*args, **kwargs)
+        self.session["user"] = None
+
+    def get_initial_acl(self):
+        return set(['recv_connect'])
+
+    def recv_connect(self):
+        self.send_connected()
+        self.send_history()
+        self.send_playlist()
+        self.send_playback()
+
+        send_keys = lambda x: x.startswith('send_')
+        funcs = filter(send_keys, SendMixin.__dict__.keys())
+        events = map(lambda x: x[5:], funcs)
+        for event, func in zip(events, funcs):
+            self.pb.subscribe(event, getattr(self, func))
+        self.spawn(self.pb.listener)
+
+        self.allowed_methods.update(('on_signin', 'on_sync_time'))
+        self.pb.connections = self.db.incr("connections")
 
-        connected = self.db.smembers("connected")
-        if self.session["user"] in connected:
+    def recv_disconnect(self):
+        user_connections = []
+        email = self.session.get('user', None)
+        if email:
+            sockets = self.socket.server.sockets.iteritems()
+            users = map(lambda x: x[1].session.get("user", None), sockets)
+            user_connections = len(filter(lambda x: x == email, users))
+
+        self.pb.connections = self.db.decr("connections")
+        if user_connections:
+            self.pb.user_connections = self.db.decr("user_connections")
+            self.db.srem("connected", email)
+
+            user = self.db.hgetall("users:%s" % email)
+            user = extract(user, ('name', 'role'))
+            self.pb.leaved_user = user
+        self.disconnect(silent=True)
+
+    def on_signin(self, assertion):
+        api_url = "https://verifier.login.persona.org/verify"
+        params = {
+            'assertion': assertion,
+            'audience': "http://127.0.0.1:5000"
+        }
+        result = requests.post(api_url, params=params)
+        data = result.json()
+        if data["status"] == "okay":
+            email = data["email"]
+            key = "users:%s" % email
+            self.session["user"] = email
+            if not self.db.hexists(key, "role"):
+                self.db.hmset(key, {"email": email, "role": "user"})
+            user = self.db.hgetall(key)
+            if not user.get("name", None):
+                self.emit("signup_needed")
+                self.allowed_methods.update(('on_signup', 'on_checkname'))
+            else:
+                self.emit("login_successful", user)
+                self.allowed_methods.update(('on_join', ))
+        else:
+            self.emit("login_failed", data["reason"])
+
+    def on_checkname(self, name):
+        available = not self.db.sismember("users", name)
+        self.emit("checkname", available)
+
+    def on_signup(self, name):
+        if not re.match(r"^[\w-]{3,15}$", name):
             return
+        email = self.session["user"]
+        if not self.db.sismember("users", name):
+            self.db.hset("users:%s" % email, "name", name)
+            user = self.db.hgetall("users:%s" % email)
+            self.db.sadd("users", name)
+            self.emit("login_successful", user)
+            self.allowed_methods.update(('on_join', ))
 
-        self.db.incr("connected_count")
-        self.db.sadd("connected", self.session["user"])
+    def on_join(self, data):
+        if self.allowed_methods:
+            self.allowed_methods.update(('on_message', ))
 
         key = "users:%s" % self.session["user"]
         user = self.db.hgetall(key)
         user = extract(user, ('name', 'role'))
-        self.db.publish("user_joined", dumps(user))
-        self.broadcast_event("user_joined", user)
-
-    def on_add_video(self, url):
-        self.db.publish("new_video_url", url)
+        if not self.db.sismember("connected", self.session["user"]):
+            self.pb.joined_user = user
 
-    def on_message(self, msg):
-        print "lol"
-        data = {'user': self.session["user"], 'msg': msg}
-        self.db.publish("new_message", dumps(data))
-
-    def recv_connect(self):
-        with app.app_context():
-            with app.request_context(self.environ):
-                sid = request.cookies.get('session')
-        try:
-            data = self.db.hgetall('session:' + sid)
-            self.session["user"] = data["user"]
-        except (TypeError, KeyError):
-            self.disconnect()
-            return
+        self.db.sadd("connected", self.session["user"])
+        self.pb.user_connections = self.db.incr("user_connections")
 
-        self.allowed_methods.update(('on_sync_time', 'on_join'))
         key = "users:%s" % self.session["user"]
         user = self.db.hgetall(key)
-        if 'role' in user and user['role'] == 'mod':
+        if 'role' in user and user['role'] in ('mod', 'admin'):
             self.lift_acl_restrictions()
 
-        self.send_connected()
-        self.send_playlist()
-        self.send_playback()
+    def on_add_video(self, data):
+        self.pb.add_video_url = data
 
-        self.subscribe("playlist_changed", self.send_playlist)
-        self.subscribe("playback_changed", self.send_playback)
-        self.subscribe("new_message", self.send_message)
-        self.spawn(self.listener)
+    def on_remove_video(self, video):
+        print video
+        self.db.lrem("playlist", 0, video)
+        self.pb.remove = video
 
-    def recv_disconnect(self):
-        username = self.session['user']
-        sockets = self.socket.server.sockets.iteritems()
-        users = map(lambda x: x[1].session["user"], sockets)
-        user_connections = len(filter(lambda x: x == username, users))
+    def on_message(self, msg):
+        email = self.session["user"]
+        user = self.db.hgetall("users:%s" % email)
+        user = extract(user, ("name", "role"))
+        data = {'user': user, 'msg': msg}
+        self.pb.message = data
 
-        if user_connections < 2:
-            self.db.decr("connected_count")
-            self.db.srem("connected", username)
+    def on_sync_time(self, client_time):
+        server_time = int(math.ceil(time() * 1000))
+        difference = server_time - client_time
 
-            user = self.db.hgetall("users:%s" % username)
-            user = extract(user, ('name', 'role'))
-            self.db.publish("user_leaved", dumps(user))
-            self.broadcast_event_not_me("user_leaved", user)
-        self.disconnect(silent=True)
+        key = "users:%s" % self.session["user"]
+        self.emit('sync_time', difference, server_time)

File wetube/workers.py

 from redis import StrictRedis
 from isodate import parse_duration
 
+from uuid import uuid4
+from utils import compress_uuid
+
 
 def wait_till(event):
     db = StrictRedis()
     r = db.pubsub()
     r.subscribe(event)
     for e in r.listen():
-        print "hmm"
         if e['type'] == 'message':
             r.unsubscribe()
             data = e['data']
     return data
 
 
-def rotate_worker():
-    log = Logger('Rotate')
-    log.info("Start worker")
-    db = StrictRedis()
+class HistoryWorker(gevent.Greenlet):
 
-    id = db.lindex('playlist', 0)
-    if not id:
-        log.info('Wait until a video is in playlist')
-        id = loads(wait_till('video_added'))
-    video = db.hgetall('video:%s' % id)
+    def __init__(self):
+        gevent.Greenlet.__init__(self)
+        self.db = StrictRedis()
+        self.log = Logger("History")
+        self.r = self.db.pubsub()
+        self.r.subscribe("playback")
 
-    while True:
-        # Wait until somebody is connected
-        connected = int(db.get('connected_count'))
-        if not connected:
-            log.info('Wait until a user is connected')
-            wait_till('user_joined')
+    def _run(self):
+        for event in self.r.listen():
+            if event["type"] == 'message':
+                video = loads(event['data'])
+                self.log.info("Add '{}'", video['title'])
+                self.db.lpush("history", video['gid'])
 
-        now = int(time() * 1000)
-        started = int(video['started'])
-        duration = int(video['duration'])
-        if now > (started + duration):
-            # Sort the current playlist
-            db.lpop('playlist')
-            db.rpush('playlist', id)
-
-            # Get current playlist and set start
-            # time of the first (current) video
-            id = db.lindex('playlist', 0)