c&&1===a.x&&(a=new THREE.Vector2(a.x-1,a.y));0===b.x&&0===b.z&&(a=new THREE.Vector2(c/2/Math.PI+.5,
+a.y));return a.clone()}THREE.Geometry.call(this);this.type="PolyhedronGeometry";this.parameters={vertices:a,indices:b,radius:c,detail:d};c=c||1;d=d||0;for(var k=this,n=0,p=a.length;nr&&(.2>d&&(b[0].x+=1),.2>a&&(b[1].x+=1),.2>q&&(b[2].x+=1));n=0;for(p=this.vertices.length;n
c.y?this.quaternion.set(1,0,0,0):(a.set(c.z,0,-c.x).normalize(),b=Math.acos(c.y),this.quaternion.setFromAxisAngle(a,b))}}();
+THREE.ArrowHelper.prototype.setLength=function(a,b,c){void 0===b&&(b=.2*a);void 0===c&&(c=.2*b);this.line.scale.set(1,a,1);this.line.updateMatrix();this.cone.scale.set(c,b,c);this.cone.position.y=a;this.cone.updateMatrix()};THREE.ArrowHelper.prototype.setColor=function(a){this.line.material.color.set(a);this.cone.material.color.set(a)};
+THREE.BoxHelper=function(a){var b=new THREE.BufferGeometry;b.addAttribute("position",new THREE.BufferAttribute(new Float32Array(72),3));THREE.Line.call(this,b,new THREE.LineBasicMaterial({color:16776960}),THREE.LinePieces);void 0!==a&&this.update(a)};THREE.BoxHelper.prototype=Object.create(THREE.Line.prototype);
+THREE.BoxHelper.prototype.update=function(a){var b=a.geometry;null===b.boundingBox&&b.computeBoundingBox();var c=b.boundingBox.min,b=b.boundingBox.max,d=this.geometry.attributes.position.array;d[0]=b.x;d[1]=b.y;d[2]=b.z;d[3]=c.x;d[4]=b.y;d[5]=b.z;d[6]=c.x;d[7]=b.y;d[8]=b.z;d[9]=c.x;d[10]=c.y;d[11]=b.z;d[12]=c.x;d[13]=c.y;d[14]=b.z;d[15]=b.x;d[16]=c.y;d[17]=b.z;d[18]=b.x;d[19]=c.y;d[20]=b.z;d[21]=b.x;d[22]=b.y;d[23]=b.z;d[24]=b.x;d[25]=b.y;d[26]=c.z;d[27]=c.x;d[28]=b.y;d[29]=c.z;d[30]=c.x;d[31]=b.y;
+d[32]=c.z;d[33]=c.x;d[34]=c.y;d[35]=c.z;d[36]=c.x;d[37]=c.y;d[38]=c.z;d[39]=b.x;d[40]=c.y;d[41]=c.z;d[42]=b.x;d[43]=c.y;d[44]=c.z;d[45]=b.x;d[46]=b.y;d[47]=c.z;d[48]=b.x;d[49]=b.y;d[50]=b.z;d[51]=b.x;d[52]=b.y;d[53]=c.z;d[54]=c.x;d[55]=b.y;d[56]=b.z;d[57]=c.x;d[58]=b.y;d[59]=c.z;d[60]=c.x;d[61]=c.y;d[62]=b.z;d[63]=c.x;d[64]=c.y;d[65]=c.z;d[66]=b.x;d[67]=c.y;d[68]=b.z;d[69]=b.x;d[70]=c.y;d[71]=c.z;this.geometry.attributes.position.needsUpdate=!0;this.geometry.computeBoundingSphere();this.matrix=a.matrixWorld;
+this.matrixAutoUpdate=!1};THREE.BoundingBoxHelper=function(a,b){var c=void 0!==b?b:8947848;this.object=a;this.box=new THREE.Box3;THREE.Mesh.call(this,new THREE.BoxGeometry(1,1,1),new THREE.MeshBasicMaterial({color:c,wireframe:!0}))};THREE.BoundingBoxHelper.prototype=Object.create(THREE.Mesh.prototype);THREE.BoundingBoxHelper.prototype.update=function(){this.box.setFromObject(this.object);this.box.size(this.scale);this.box.center(this.position)};
+THREE.CameraHelper=function(a){function b(a,b,d){c(a,d);c(b,d)}function c(a,b){d.vertices.push(new THREE.Vector3);d.colors.push(new THREE.Color(b));void 0===f[a]&&(f[a]=[]);f[a].push(d.vertices.length-1)}var d=new THREE.Geometry,e=new THREE.LineBasicMaterial({color:16777215,vertexColors:THREE.FaceColors}),f={};b("n1","n2",16755200);b("n2","n4",16755200);b("n4","n3",16755200);b("n3","n1",16755200);b("f1","f2",16755200);b("f2","f4",16755200);b("f4","f3",16755200);b("f3","f1",16755200);b("n1","f1",16755200);
+b("n2","f2",16755200);b("n3","f3",16755200);b("n4","f4",16755200);b("p","n1",16711680);b("p","n2",16711680);b("p","n3",16711680);b("p","n4",16711680);b("u1","u2",43775);b("u2","u3",43775);b("u3","u1",43775);b("c","t",16777215);b("p","c",3355443);b("cn1","cn2",3355443);b("cn3","cn4",3355443);b("cf1","cf2",3355443);b("cf3","cf4",3355443);THREE.Line.call(this,d,e,THREE.LinePieces);this.camera=a;this.matrix=a.matrixWorld;this.matrixAutoUpdate=!1;this.pointMap=f;this.update()};
+THREE.CameraHelper.prototype=Object.create(THREE.Line.prototype);
+THREE.CameraHelper.prototype.update=function(){var a,b,c=new THREE.Vector3,d=new THREE.Camera,e=function(e,g,h,k){c.set(g,h,k).unproject(d);e=b[e];if(void 0!==e)for(g=0,h=e.length;gt;t++){d[0]=r[g[t]];d[1]=r[g[(t+1)%3]];d.sort(f);var s=d.toString();void 0===e[s]?(e[s]={vert1:d[0],vert2:d[1],face1:q,face2:void 0},p++):e[s].face2=q}d=new Float32Array(6*p);f=0;for(s in e)if(g=e[s],void 0===g.face2||
+.9999>k[g.face1].normal.dot(k[g.face2].normal))p=n[g.vert1],d[f++]=p.x,d[f++]=p.y,d[f++]=p.z,p=n[g.vert2],d[f++]=p.x,d[f++]=p.y,d[f++]=p.z;h.addAttribute("position",new THREE.BufferAttribute(d,3));THREE.Line.call(this,h,new THREE.LineBasicMaterial({color:c}),THREE.LinePieces);this.matrix=a.matrixWorld;this.matrixAutoUpdate=!1};THREE.EdgesHelper.prototype=Object.create(THREE.Line.prototype);
+THREE.FaceNormalsHelper=function(a,b,c,d){this.object=a;this.size=void 0!==b?b:1;a=void 0!==c?c:16776960;d=void 0!==d?d:1;b=new THREE.Geometry;c=0;for(var e=this.object.geometry.faces.length;cb;b++)a.faces[b].color=this.colors[4>b?0:1];b=new THREE.MeshBasicMaterial({vertexColors:THREE.FaceColors,wireframe:!0});this.lightSphere=new THREE.Mesh(a,b);this.add(this.lightSphere);
+this.update()};THREE.HemisphereLightHelper.prototype=Object.create(THREE.Object3D.prototype);THREE.HemisphereLightHelper.prototype.dispose=function(){this.lightSphere.geometry.dispose();this.lightSphere.material.dispose()};
+THREE.HemisphereLightHelper.prototype.update=function(){var a=new THREE.Vector3;return function(){this.colors[0].copy(this.light.color).multiplyScalar(this.light.intensity);this.colors[1].copy(this.light.groundColor).multiplyScalar(this.light.intensity);this.lightSphere.lookAt(a.setFromMatrixPosition(this.light.matrixWorld).negate());this.lightSphere.geometry.colorsNeedUpdate=!0}}();
+THREE.PointLightHelper=function(a,b){this.light=a;this.light.updateMatrixWorld();var c=new THREE.SphereGeometry(b,4,2),d=new THREE.MeshBasicMaterial({wireframe:!0,fog:!1});d.color.copy(this.light.color).multiplyScalar(this.light.intensity);THREE.Mesh.call(this,c,d);this.matrix=this.light.matrixWorld;this.matrixAutoUpdate=!1};THREE.PointLightHelper.prototype=Object.create(THREE.Mesh.prototype);THREE.PointLightHelper.prototype.dispose=function(){this.geometry.dispose();this.material.dispose()};
+THREE.PointLightHelper.prototype.update=function(){this.material.color.copy(this.light.color).multiplyScalar(this.light.intensity)};
+THREE.SkeletonHelper=function(a){this.bones=this.getBoneList(a);for(var b=new THREE.Geometry,c=0;cs;s++){d[0]=t[g[s]];d[1]=t[g[(s+1)%3]];d.sort(f);var u=d.toString();void 0===e[u]&&(q[2*p]=d[0],q[2*p+1]=d[1],e[u]=!0,p++)}d=new Float32Array(6*p);m=0;for(r=p;ms;s++)p=
+k[q[2*m+s]],g=6*m+3*s,d[g+0]=p.x,d[g+1]=p.y,d[g+2]=p.z;h.addAttribute("position",new THREE.BufferAttribute(d,3))}else if(a.geometry instanceof THREE.BufferGeometry){if(void 0!==a.geometry.attributes.index){k=a.geometry.attributes.position.array;r=a.geometry.attributes.index.array;n=a.geometry.drawcalls;p=0;0===n.length&&(n=[{count:r.length,index:0,start:0}]);for(var q=new Uint32Array(2*r.length),t=0,v=n.length;ts;s++)d[0]=
+g+r[m+s],d[1]=g+r[m+(s+1)%3],d.sort(f),u=d.toString(),void 0===e[u]&&(q[2*p]=d[0],q[2*p+1]=d[1],e[u]=!0,p++);d=new Float32Array(6*p);m=0;for(r=p;ms;s++)g=6*m+3*s,p=3*q[2*m+s],d[g+0]=k[p],d[g+1]=k[p+1],d[g+2]=k[p+2]}else for(k=a.geometry.attributes.position.array,p=k.length/3,q=p/3,d=new Float32Array(6*p),m=0,r=q;ms;s++)g=18*m+6*s,q=9*m+3*s,d[g+0]=k[q],d[g+1]=k[q+1],d[g+2]=k[q+2],p=9*m+(s+1)%3*3,d[g+3]=k[p],d[g+4]=k[p+1],d[g+5]=k[p+2];h.addAttribute("position",new THREE.BufferAttribute(d,
+3))}THREE.Line.call(this,h,new THREE.LineBasicMaterial({color:c}),THREE.LinePieces);this.matrix=a.matrixWorld;this.matrixAutoUpdate=!1};THREE.WireframeHelper.prototype=Object.create(THREE.Line.prototype);THREE.ImmediateRenderObject=function(){THREE.Object3D.call(this);this.render=function(a){}};THREE.ImmediateRenderObject.prototype=Object.create(THREE.Object3D.prototype);
+THREE.MorphBlendMesh=function(a,b){THREE.Mesh.call(this,a,b);this.animationsMap={};this.animationsList=[];var c=this.geometry.morphTargets.length;this.createAnimation("__default",0,c-1,c/1);this.setAnimationWeight("__default",1)};THREE.MorphBlendMesh.prototype=Object.create(THREE.Mesh.prototype);
+THREE.MorphBlendMesh.prototype.createAnimation=function(a,b,c,d){b={startFrame:b,endFrame:c,length:c-b+1,fps:d,duration:(c-b)/d,lastFrame:0,currentFrame:0,active:!1,time:0,direction:1,weight:1,directionBackwards:!1,mirroredLoop:!1};this.animationsMap[a]=b;this.animationsList.push(b)};
+THREE.MorphBlendMesh.prototype.autoCreateAnimations=function(a){for(var b=/([a-z]+)_?(\d+)/,c,d={},e=this.geometry,f=0,g=e.morphTargets.length;fh.end&&(h.end=f);c||(c=k)}}for(k in d)h=d[k],this.createAnimation(k,h.start,h.end,a);this.firstAnimation=c};
+THREE.MorphBlendMesh.prototype.setAnimationDirectionForward=function(a){if(a=this.animationsMap[a])a.direction=1,a.directionBackwards=!1};THREE.MorphBlendMesh.prototype.setAnimationDirectionBackward=function(a){if(a=this.animationsMap[a])a.direction=-1,a.directionBackwards=!0};THREE.MorphBlendMesh.prototype.setAnimationFPS=function(a,b){var c=this.animationsMap[a];c&&(c.fps=b,c.duration=(c.end-c.start)/c.fps)};
+THREE.MorphBlendMesh.prototype.setAnimationDuration=function(a,b){var c=this.animationsMap[a];c&&(c.duration=b,c.fps=(c.end-c.start)/c.duration)};THREE.MorphBlendMesh.prototype.setAnimationWeight=function(a,b){var c=this.animationsMap[a];c&&(c.weight=b)};THREE.MorphBlendMesh.prototype.setAnimationTime=function(a,b){var c=this.animationsMap[a];c&&(c.time=b)};THREE.MorphBlendMesh.prototype.getAnimationTime=function(a){var b=0;if(a=this.animationsMap[a])b=a.time;return b};
+THREE.MorphBlendMesh.prototype.getAnimationDuration=function(a){var b=-1;if(a=this.animationsMap[a])b=a.duration;return b};THREE.MorphBlendMesh.prototype.playAnimation=function(a){var b=this.animationsMap[a];b?(b.time=0,b.active=!0):console.warn("animation["+a+"] undefined")};THREE.MorphBlendMesh.prototype.stopAnimation=function(a){if(a=this.animationsMap[a])a.active=!1};
+THREE.MorphBlendMesh.prototype.update=function(a){for(var b=0,c=this.animationsList.length;bd.duration||0>d.time)d.direction*=-1,d.time>d.duration&&(d.time=d.duration,d.directionBackwards=!0),0>d.time&&(d.time=0,d.directionBackwards=!1)}else d.time%=d.duration,0>d.time&&(d.time+=d.duration);var f=d.startFrame+THREE.Math.clamp(Math.floor(d.time/e),0,d.length-1),g=d.weight;
+f!==d.currentFrame&&(this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*g,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);this.morphTargetInfluences[d.currentFrame]=e*g;this.morphTargetInfluences[d.lastFrame]=(1-e)*g}}};
diff --git a/plugins/Sidebar/media-globe/world.jpg b/plugins/Sidebar/media-globe/world.jpg
new file mode 100644
index 00000000..222bd939
Binary files /dev/null and b/plugins/Sidebar/media-globe/world.jpg differ
diff --git a/plugins/Sidebar/media/Class.coffee b/plugins/Sidebar/media/Class.coffee
new file mode 100644
index 00000000..d62ab25c
--- /dev/null
+++ b/plugins/Sidebar/media/Class.coffee
@@ -0,0 +1,23 @@
+class Class
+ trace: true
+
+ log: (args...) ->
+ return unless @trace
+ return if typeof console is 'undefined'
+ args.unshift("[#{@.constructor.name}]")
+ console.log(args...)
+ @
+
+ logStart: (name, args...) ->
+ return unless @trace
+ @logtimers or= {}
+ @logtimers[name] = +(new Date)
+ @log "#{name}", args..., "(started)" if args.length > 0
+ @
+
+ logEnd: (name, args...) ->
+ ms = +(new Date)-@logtimers[name]
+ @log "#{name}", args..., "(Done in #{ms}ms)"
+ @
+
+window.Class = Class
\ No newline at end of file
diff --git a/plugins/Sidebar/media/Internals.coffee b/plugins/Sidebar/media/Internals.coffee
new file mode 100644
index 00000000..484ecdb7
--- /dev/null
+++ b/plugins/Sidebar/media/Internals.coffee
@@ -0,0 +1,60 @@
+class Internals extends Class
+ constructor: (@sidebar) ->
+ @tag = null
+ @opened = false
+ if window.top.location.hash == "#internals"
+ setTimeout (=> @open()), 10
+
+ createHtmltag: ->
+ @when_loaded = $.Deferred()
+ if not @container
+ @container = $("""
+
+
+ """)
+ @container.appendTo(document.body)
+ @tag = @container.find(".internals")
+
+ open: =>
+ @createHtmltag()
+ @sidebar.fixbutton_targety = @sidebar.page_height
+ @stopDragY()
+
+ onOpened: =>
+ @sidebar.onClosed()
+ @log "onOpened"
+
+ onClosed: =>
+ $(document.body).removeClass("body-internals")
+
+ stopDragY: =>
+ # Animate sidebar and iframe
+ if @sidebar.fixbutton_targety == @sidebar.fixbutton_inity
+ # Closed
+ targety = 0
+ @opened = false
+ else
+ # Opened
+ targety = @sidebar.fixbutton_targety - @sidebar.fixbutton_inity
+ @onOpened()
+ @opened = true
+
+ # Revent sidebar transitions
+ if @tag
+ @tag.css("transition", "0.5s ease-out")
+ @tag.css("transform", "translateY(#{targety}px)").one transitionEnd, =>
+ @tag.css("transition", "")
+ if not @opened
+ @log "cleanup"
+ # Revert body transformations
+ @log "stopdrag", "opened:", @opened, targety
+ if not @opened
+ @onClosed()
+
+window.Internals = Internals
\ No newline at end of file
diff --git a/plugins/Sidebar/media/Internals.css b/plugins/Sidebar/media/Internals.css
new file mode 100644
index 00000000..36b2489e
--- /dev/null
+++ b/plugins/Sidebar/media/Internals.css
@@ -0,0 +1,17 @@
+.internals-container { width: 100%; z-index: 998; position: absolute; top: -100vh; }
+.internals { background-color: #EEE; height: 100vh; transform: translateY(0px); }
+.internals-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; }
+
+.internals .mynode {
+ border: 0.5px solid #aaa; width: 50px; height: 50px; transform: rotateZ(45deg); margin-top: -25px; margin-left: -25px;
+ opacity: 1; display: inline-block; background-color: #EEE; z-index: 9; position: absolute; outline: 5px solid #EEE;
+}
+.internals .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; }
+.internals .peer { left: 0px; top: 0px; position: absolute; }
+.internals .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; }
+.internals .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; transition: all 0.3s }
+.internals .peer .icon:hover:before { opacity: 1; transition: none }
+.internals .peer .line {
+ width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px;
+ transform: rotateZ(334deg); transform-origin: bottom left;
+}
\ No newline at end of file
diff --git a/plugins/Sidebar/media/Menu.coffee b/plugins/Sidebar/media/Menu.coffee
new file mode 100644
index 00000000..3e19fd9f
--- /dev/null
+++ b/plugins/Sidebar/media/Menu.coffee
@@ -0,0 +1,49 @@
+class Menu
+ constructor: (@button) ->
+ @elem = $(".menu.template").clone().removeClass("template")
+ @elem.appendTo("body")
+ @items = []
+
+ show: ->
+ if window.visible_menu and window.visible_menu.button[0] == @button[0] # Same menu visible then hide it
+ window.visible_menu.hide()
+ @hide()
+ else
+ button_pos = @button.offset()
+ left = button_pos.left
+ @elem.css({"top": button_pos.top+@button.outerHeight(), "left": left})
+ @button.addClass("menu-active")
+ @elem.addClass("visible")
+ if @elem.position().left + @elem.width() + 20 > window.innerWidth
+ @elem.css("left", window.innerWidth - @elem.width() - 20)
+ if window.visible_menu then window.visible_menu.hide()
+ window.visible_menu = @
+
+
+ hide: ->
+ @elem.removeClass("visible")
+ @button.removeClass("menu-active")
+ window.visible_menu = null
+
+
+ addItem: (title, cb) ->
+ item = $(".menu-item.template", @elem).clone().removeClass("template")
+ item.html(title)
+ item.on "click", =>
+ if not cb(item)
+ @hide()
+ return false
+ item.appendTo(@elem)
+ @items.push item
+ return item
+
+
+ log: (args...) ->
+ console.log "[Menu]", args...
+
+window.Menu = Menu
+
+# Hide menu on outside click
+$("body").on "click", (e) ->
+ if window.visible_menu and e.target != window.visible_menu.button[0] and $(e.target).parent()[0] != window.visible_menu.elem[0]
+ window.visible_menu.hide()
diff --git a/plugins/Sidebar/media/Menu.css b/plugins/Sidebar/media/Menu.css
new file mode 100644
index 00000000..e2afa16e
--- /dev/null
+++ b/plugins/Sidebar/media/Menu.css
@@ -0,0 +1,19 @@
+.menu {
+ background-color: white; padding: 10px 0px; position: absolute; top: 0px; left: 0px; max-height: 0px; overflow: hidden; transform: translate(0px, -30px); pointer-events: none;
+ box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out;
+}
+.menu.visible { opacity: 1; max-height: 350px; transform: translate(0px, 0px); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; pointer-events: all }
+
+.menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; padding-left: 30px; }
+.menu-item-separator { margin-top: 5px; border-top: 1px solid #eee }
+
+.menu-item:hover { background-color: #F6F6F6; transition: none; color: inherit; border: none }
+.menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; transition: none }
+.menu-item.selected:before {
+ content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1);
+ font-weight: bold; position: absolute; margin-left: -17px; font-size: 12px; margin-top: 2px;
+}
+
+@media only screen and (max-width: 800px) {
+.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
+}
\ No newline at end of file
diff --git a/plugins/Sidebar/media/RateLimit.coffee b/plugins/Sidebar/media/RateLimit.coffee
new file mode 100644
index 00000000..17c67433
--- /dev/null
+++ b/plugins/Sidebar/media/RateLimit.coffee
@@ -0,0 +1,14 @@
+limits = {}
+call_after_interval = {}
+window.RateLimit = (interval, fn) ->
+ if not limits[fn]
+ call_after_interval[fn] = false
+ fn() # First call is not delayed
+ limits[fn] = setTimeout (->
+ if call_after_interval[fn]
+ fn()
+ delete limits[fn]
+ delete call_after_interval[fn]
+ ), interval
+ else # Called within iterval, delay the call
+ call_after_interval[fn] = true
diff --git a/plugins/Sidebar/media/Scrollable.js b/plugins/Sidebar/media/Scrollable.js
new file mode 100644
index 00000000..689a5719
--- /dev/null
+++ b/plugins/Sidebar/media/Scrollable.js
@@ -0,0 +1,91 @@
+/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
+
+window.initScrollable = function () {
+
+ var scrollContainer = document.querySelector('.scrollable'),
+ scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
+ scrollContent = document.querySelector('.scrollable .content'),
+ contentPosition = 0,
+ scrollerBeingDragged = false,
+ scroller,
+ topPosition,
+ scrollerHeight;
+
+ function calculateScrollerHeight() {
+ // *Calculation of how tall scroller should be
+ var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
+ if (visibleRatio == 1)
+ scroller.style.display = "none";
+ else
+ scroller.style.display = "block";
+ return visibleRatio * scrollContainer.offsetHeight;
+ }
+
+ function moveScroller(evt) {
+ // Move Scroll bar to top offset
+ var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
+ topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
+ scroller.style.top = topPosition + 'px';
+ }
+
+ function startDrag(evt) {
+ normalizedPosition = evt.pageY;
+ contentPosition = scrollContentWrapper.scrollTop;
+ scrollerBeingDragged = true;
+ window.addEventListener('mousemove', scrollBarScroll);
+ return false;
+ }
+
+ function stopDrag(evt) {
+ scrollerBeingDragged = false;
+ window.removeEventListener('mousemove', scrollBarScroll);
+ }
+
+ function scrollBarScroll(evt) {
+ if (scrollerBeingDragged === true) {
+ evt.preventDefault();
+ var mouseDifferential = evt.pageY - normalizedPosition;
+ var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
+ scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
+ }
+ }
+
+ function updateHeight() {
+ scrollerHeight = calculateScrollerHeight() - 10;
+ scroller.style.height = scrollerHeight + 'px';
+ }
+
+ function createScroller() {
+ // *Creates scroller element and appends to '.scrollable' div
+ // create scroller element
+ scroller = document.createElement("div");
+ scroller.className = 'scroller';
+
+ // determine how big scroller should be based on content
+ scrollerHeight = calculateScrollerHeight() - 10;
+
+ if (scrollerHeight / scrollContainer.offsetHeight < 1) {
+ // *If there is a need to have scroll bar based on content size
+ scroller.style.height = scrollerHeight + 'px';
+
+ // append scroller to scrollContainer div
+ scrollContainer.appendChild(scroller);
+
+ // show scroll path divot
+ scrollContainer.className += ' showScroll';
+
+ // attach related draggable listeners
+ scroller.addEventListener('mousedown', startDrag);
+ window.addEventListener('mouseup', stopDrag);
+ }
+
+ }
+
+ createScroller();
+
+
+ // *** Listeners ***
+ scrollContentWrapper.addEventListener('scroll', moveScroller);
+
+ return updateHeight;
+};
\ No newline at end of file
diff --git a/plugins/Sidebar/media/Scrollbable.css b/plugins/Sidebar/media/Scrollbable.css
new file mode 100644
index 00000000..6e3e0b6a
--- /dev/null
+++ b/plugins/Sidebar/media/Scrollbable.css
@@ -0,0 +1,44 @@
+.scrollable {
+ overflow: hidden;
+}
+
+.scrollable.showScroll::after {
+ position: absolute;
+ content: '';
+ top: 5%;
+ right: 7px;
+ height: 90%;
+ width: 3px;
+ background: rgba(224, 224, 255, .3);
+}
+
+.scrollable .content-wrapper {
+ width: 100%;
+ height: 100%;
+ padding-right: 50%;
+ overflow-y: scroll;
+}
+.scroller {
+ margin-top: 5px;
+ z-index: 5;
+ cursor: pointer;
+ position: absolute;
+ width: 7px;
+ border-radius: 5px;
+ background: #3A3A3A;
+ top: 0px;
+ left: 395px;
+ -webkit-transition: top .08s;
+ -moz-transition: top .08s;
+ -ms-transition: top .08s;
+ -o-transition: top .08s;
+ transition: top .08s;
+}
+.scroller {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee
new file mode 100644
index 00000000..938fe41b
--- /dev/null
+++ b/plugins/Sidebar/media/Sidebar.coffee
@@ -0,0 +1,617 @@
+class Sidebar extends Class
+ constructor: (@wrapper) ->
+ @tag = null
+ @container = null
+ @opened = false
+ @width = 410
+ @internals = new Internals(@)
+ @fixbutton = $(".fixbutton")
+ @fixbutton_addx = 0
+ @fixbutton_addy = 0
+ @fixbutton_initx = 0
+ @fixbutton_inity = 15
+ @fixbutton_targetx = 0
+ @move_lock = null
+ @page_width = $(window).width()
+ @page_height = $(window).height()
+ @frame = $("#inner-iframe")
+ @initFixbutton()
+ @dragStarted = 0
+ @globe = null
+ @preload_html = null
+
+ @original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original
+
+ # Start in opened state for debugging
+ if false
+ @startDrag()
+ @moved()
+ @fixbutton_targetx = @fixbutton_initx - @width
+ @stopDrag()
+
+
+ initFixbutton: ->
+
+ # Detect dragging
+ @fixbutton.on "mousedown touchstart", (e) =>
+ if e.button > 0 # Right or middle click
+ return
+ e.preventDefault()
+
+ # Disable previous listeners
+ @fixbutton.off "click touchend touchcancel"
+ @fixbutton.off "mousemove touchmove"
+
+ # Make sure its not a click
+ @dragStarted = (+ new Date)
+ @fixbutton.one "mousemove touchmove", (e) =>
+ mousex = e.pageX
+ mousey = e.pageY
+ if not mousex
+ mousex = e.originalEvent.touches[0].pageX
+ mousey = e.originalEvent.touches[0].pageY
+
+ @fixbutton_addx = @fixbutton.offset().left - mousex
+ @fixbutton_addy = @fixbutton.offset().top - mousey
+ @startDrag()
+ @fixbutton.parent().on "click touchend touchcancel", (e) =>
+ if (+ new Date) - @dragStarted < 100
+ window.top.location = @fixbutton.find(".fixbutton-bg").attr("href")
+ @stopDrag()
+ @resized()
+ $(window).on "resize", @resized
+
+ resized: =>
+ @page_width = $(window).width()
+ @page_height = $(window).height()
+ @fixbutton_initx = @page_width - 75 # Initial x position
+ if @opened
+ @fixbutton.css
+ left: @fixbutton_initx - @width
+ else
+ @fixbutton.css
+ left: @fixbutton_initx
+
+ # Start dragging the fixbutton
+ startDrag: ->
+ @move_lock = "x" # Temporary until internals not finished
+ @log "startDrag"
+ @fixbutton_targetx = @fixbutton_initx # Fallback x position
+
+ @fixbutton.addClass("dragging")
+
+ # Fullscreen drag bg to capture mouse events over iframe
+ $("").appendTo(document.body)
+
+ # IE position wrap fix
+ if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
+ @fixbutton.css("pointer-events", "none")
+
+ # Don't go to homepage
+ @fixbutton.one "click", (e) =>
+ @stopDrag()
+ @fixbutton.removeClass("dragging")
+ moved_x = Math.abs(@fixbutton.offset().left - @fixbutton_initx)
+ moved_y = Math.abs(@fixbutton.offset().top - @fixbutton_inity)
+ if moved_x > 5 or moved_y > 10
+ # If moved more than some pixel the button then don't go to homepage
+ e.preventDefault()
+
+ # Animate drag
+ @fixbutton.parents().on "mousemove touchmove", @animDrag
+ @fixbutton.parents().on "mousemove touchmove" ,@waitMove
+
+ # Stop dragging listener
+ @fixbutton.parents().one "mouseup touchend touchcancel", (e) =>
+ e.preventDefault()
+ @stopDrag()
+
+
+ # Wait for moving the fixbutton
+ waitMove: (e) =>
+ document.body.style.perspective = "1000px"
+ document.body.style.height = "100%"
+ document.body.style.willChange = "perspective"
+ document.documentElement.style.height = "100%"
+ #$(document.body).css("backface-visibility", "hidden").css("perspective", "1000px").css("height", "900px")
+ # $("iframe").css("backface-visibility", "hidden")
+
+ moved_x = Math.abs(parseInt(@fixbutton[0].style.left) - @fixbutton_targetx)
+ moved_y = Math.abs(parseInt(@fixbutton[0].style.top) - @fixbutton_targety)
+ if moved_x > 5 and (+ new Date) - @dragStarted + moved_x > 50
+ @moved("x")
+ @fixbutton.stop().animate {"top": @fixbutton_inity}, 1000
+ @fixbutton.parents().off "mousemove touchmove" ,@waitMove
+
+ else if moved_y > 5 and (+ new Date) - @dragStarted + moved_y > 50
+ @moved("y")
+ @fixbutton.parents().off "mousemove touchmove" ,@waitMove
+
+ moved: (direction) ->
+ @log "Moved", direction
+ @move_lock = direction
+ if direction == "y"
+ $(document.body).addClass("body-internals")
+ return @internals.createHtmltag()
+ @createHtmltag()
+ $(document.body).addClass("body-sidebar")
+ @container.on "mousedown touchend touchcancel", (e) =>
+ if e.target != e.currentTarget
+ return true
+ @log "closing"
+ if $(document.body).hasClass("body-sidebar")
+ @close()
+ return true
+
+ $(window).off "resize"
+ $(window).on "resize", =>
+ $(document.body).css "height", $(window).height()
+ @scrollable()
+ @resized()
+
+ # Override setsiteinfo to catch changes
+ @wrapper.setSiteInfo = (site_info) =>
+ @setSiteInfo(site_info)
+ @original_set_site_info.apply(@wrapper, arguments)
+
+ # Preload world.jpg
+ img = new Image();
+ img.src = "/uimedia/globe/world.jpg";
+
+ setSiteInfo: (site_info) ->
+ RateLimit 1500, =>
+ @updateHtmlTag()
+ RateLimit 30000, =>
+ @displayGlobe()
+
+ # Create the sidebar html tag
+ createHtmltag: ->
+ @when_loaded = $.Deferred()
+ if not @container
+ @container = $("""
+
+ """)
+ @container.appendTo(document.body)
+ @tag = @container.find(".sidebar")
+ @updateHtmlTag()
+ @scrollable = window.initScrollable()
+
+
+ updateHtmlTag: ->
+ if @preload_html
+ @setHtmlTag(@preload_html)
+ @preload_html = null
+ else
+ @wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
+
+ setHtmlTag: (res) =>
+ if @tag.find(".content").children().length == 0 # First update
+ @log "Creating content"
+ @container.addClass("loaded")
+ morphdom(@tag.find(".content")[0], ''+res+'
')
+ # @scrollable()
+ @when_loaded.resolve()
+
+ else # Not first update, patch the html to keep unchanged dom elements
+ morphdom @tag.find(".content")[0], ''+res+'
', {
+ onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state
+ if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
+ return false
+ else
+ return true
+ }
+
+ # Save and forgot privatekey for site signing
+ @tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) =>
+ @wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) =>
+ @wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) =>
+ @wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000
+ return false
+
+ @tag.find("#privatekey-forgot").off("click, touchend").on "click touchend", (e) =>
+ @wrapper.displayConfirm "Remove saved private key for this site?", "Forgot", (res) =>
+ if not res
+ return false
+ @wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) =>
+ @wrapper.notifications.add "privatekey", "done", "Saved private key removed", 5000
+ return false
+
+
+
+ animDrag: (e) =>
+ mousex = e.pageX
+ mousey = e.pageY
+ if not mousex and e.originalEvent.touches
+ mousex = e.originalEvent.touches[0].pageX
+ mousey = e.originalEvent.touches[0].pageY
+
+ overdrag = @fixbutton_initx - @width - mousex
+ if overdrag > 0 # Overdragged
+ overdrag_percent = 1 + overdrag/300
+ mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
+ targetx = @fixbutton_initx - mousex - @fixbutton_addx
+ targety = @fixbutton_inity - mousey - @fixbutton_addy
+
+ if @move_lock == "x"
+ targety = @fixbutton_inity
+ else if @move_lock == "y"
+ targetx = @fixbutton_initx
+
+ if not @move_lock or @move_lock == "x"
+ @fixbutton[0].style.left = (mousex + @fixbutton_addx) + "px"
+ if @tag
+ @tag[0].style.transform = "translateX(#{0 - targetx}px)"
+
+ if not @move_lock or @move_lock == "y"
+ @fixbutton[0].style.top = (mousey + @fixbutton_addy) + "px"
+ if @internals.tag
+ @internals.tag[0].style.transform = "translateY(#{0 - targety}px)"
+
+ #if @move_lock == "x"
+ # @fixbutton[0].style.left = "#{@fixbutton_targetx} px"
+ #@fixbutton[0].style.top = "#{@fixbutton_inity}px"
+ #if @move_lock == "y"
+ # @fixbutton[0].style.top = "#{@fixbutton_targety} px"
+
+ # Check if opened
+ if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
+ @fixbutton_targetx = @fixbutton_initx - @width # Make it opened
+ else
+ @fixbutton_targetx = @fixbutton_initx
+
+ if (not @internals.opened and 0 - targety > @page_height/10) or (@internals.opened and 0 - targety > @page_height*0.95)
+ @fixbutton_targety = @page_height - @fixbutton_inity - 50
+ else
+ @fixbutton_targety = @fixbutton_inity
+
+
+ # Stop dragging the fixbutton
+ stopDrag: ->
+ @fixbutton.parents().off "mousemove touchmove"
+ @fixbutton.off "mousemove touchmove"
+ @fixbutton.css("pointer-events", "")
+ $(".drag-bg").remove()
+ if not @fixbutton.hasClass("dragging")
+ return
+ @fixbutton.removeClass("dragging")
+
+ # Move back to initial position
+ if @fixbutton_targetx != @fixbutton.offset().left
+ # Animate fixbutton
+ if @move_lock == "y"
+ top = @fixbutton_targety
+ left = @fixbutton_initx
+ if @move_lock == "x"
+ top = @fixbutton_inity
+ left = @fixbutton_targetx
+ @fixbutton.stop().animate {"left": left, "top": top}, 500, "easeOutBack", =>
+ # Switch back to auto align
+ if @fixbutton_targetx == @fixbutton_initx # Closed
+ @fixbutton.css("left", "auto")
+ else # Opened
+ @fixbutton.css("left", left)
+
+ $(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status
+
+ @stopDragX()
+ @internals.stopDragY()
+ @move_lock = null
+
+ stopDragX: ->
+ # Animate sidebar and iframe
+ if @fixbutton_targetx == @fixbutton_initx or @move_lock == "y"
+ # Closed
+ targetx = 0
+ @opened = false
+ else
+ # Opened
+ targetx = @width
+ if @opened
+ @onOpened()
+ else
+ @when_loaded.done =>
+ @onOpened()
+ @opened = true
+
+ # Revent sidebar transitions
+ if @tag
+ @tag.css("transition", "0.4s ease-out")
+ @tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
+ @tag.css("transition", "")
+ if not @opened
+ @container.remove()
+ @container = null
+ if @tag
+ @tag.remove()
+ @tag = null
+
+ # Revert body transformations
+ @log "stopdrag", "opened:", @opened
+ if not @opened
+ @onClosed()
+
+
+ onOpened: ->
+ @log "Opened"
+ @scrollable()
+
+ # Re-calculate height when site admin opened or closed
+ @tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
+ setTimeout (=>
+ @scrollable()
+ ), 300
+
+ # Site limit button
+ @tag.find("#button-sitelimit").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
+ @updateHtmlTag()
+ return false
+
+ # Site autodownload limit button
+ @tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000
+ @updateHtmlTag()
+ return false
+
+ # Database reload
+ @tag.find("#button-dbreload").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "dbReload", [], =>
+ @wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
+ @updateHtmlTag()
+ return false
+
+ # Database rebuild
+ @tag.find("#button-dbrebuild").off("click touchend").on "click touchend", =>
+ @wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
+ @wrapper.ws.cmd "dbRebuild", [], =>
+ @wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
+ @updateHtmlTag()
+ return false
+
+ # Update site
+ @tag.find("#button-update").off("click touchend").on "click touchend", =>
+ @tag.find("#button-update").addClass("loading")
+ @wrapper.ws.cmd "siteUpdate", @wrapper.site_info.address, =>
+ @wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
+ @tag.find("#button-update").removeClass("loading")
+ return false
+
+ # Pause site
+ @tag.find("#button-pause").off("click touchend").on "click touchend", =>
+ @tag.find("#button-pause").addClass("hidden")
+ @wrapper.ws.cmd "sitePause", @wrapper.site_info.address
+ return false
+
+ # Resume site
+ @tag.find("#button-resume").off("click touchend").on "click touchend", =>
+ @tag.find("#button-resume").addClass("hidden")
+ @wrapper.ws.cmd "siteResume", @wrapper.site_info.address
+ return false
+
+ # Delete site
+ @tag.find("#button-delete").off("click touchend").on "click touchend", =>
+ @wrapper.displayConfirm "Are you sure?", ["Delete this site", "Blacklist"], (confirmed) =>
+ if confirmed == 1
+ @tag.find("#button-delete").addClass("loading")
+ @wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
+ document.location = $(".fixbutton-bg").attr("href")
+ else if confirmed == 2
+ @wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) =>
+ @tag.find("#button-delete").addClass("loading")
+ @wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason]
+ @wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
+ document.location = $(".fixbutton-bg").attr("href")
+
+
+ return false
+
+ # Owned checkbox
+ @tag.find("#checkbox-owned").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")]
+
+ # Owned checkbox
+ @tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
+
+ # Change identity button
+ @tag.find("#button-identity").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "certSelect"
+ return false
+
+ # Save settings
+ @tag.find("#button-settings").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "fileGet", "content.json", (res) =>
+ data = JSON.parse(res)
+ data["title"] = $("#settings-title").val()
+ data["description"] = $("#settings-description").val()
+ json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
+ @wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
+ if res != "ok" # fileWrite failed
+ @wrapper.notifications.add "file-write", "error", "File write error: #{res}"
+ else
+ @wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
+ if @wrapper.site_info.privatekey
+ @wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: "content.json", update_changed_files: true}
+ @updateHtmlTag()
+ return false
+
+
+ # Open site directory
+ @tag.find("#link-directory").off("click touchend").on "click touchend", =>
+ @wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address]
+ return false
+
+ # Copy site with peers
+ @tag.find("#link-copypeers").off("click touchend").on "click touchend", (e) =>
+ copy_text = e.currentTarget.href
+ handler = (e) =>
+ e.clipboardData.setData('text/plain', copy_text)
+ e.preventDefault()
+ @wrapper.notifications.add "copy", "done", "Site address with peers copied to your clipboard", 5000
+ document.removeEventListener('copy', handler, true)
+
+ document.addEventListener('copy', handler, true)
+ document.execCommand('copy')
+ return false
+
+ # Sign and publish content.json
+ $(document).on "click touchend", =>
+ @tag?.find("#button-sign-publish-menu").removeClass("visible")
+ @tag?.find(".contents + .flex").removeClass("sign-publish-flex")
+
+ @tag.find(".contents-content").off("click touchend").on "click touchend", (e) =>
+ $("#input-contents").val(e.currentTarget.innerText);
+ return false;
+
+ menu = new Menu(@tag.find("#menu-sign-publish"))
+ menu.elem.css("margin-top", "-130px") # Open upwards
+ menu.addItem "Sign", =>
+ inner_path = @tag.find("#input-contents").val()
+
+ @wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (res) =>
+ if @wrapper.site_info.privatekey
+ # Privatekey stored in users.json
+ @wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
+ else if @wrapper.site_info.auth_address in res.signers
+ # ZeroID or other ID provider
+ @wrapper.ws.cmd "siteSign", {privatekey: null, inner_path: inner_path, update_changed_files: true}, (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
+ else
+ # Ask the user for privatekey
+ @wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
+ @wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000
+
+ @tag.find(".contents + .flex").removeClass "active"
+ menu.hide()
+
+ menu.addItem "Publish", =>
+ inner_path = @tag.find("#input-contents").val()
+ @wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}
+
+ @tag.find(".contents + .flex").removeClass "active"
+ menu.hide()
+
+ @tag.find("#menu-sign-publish").off("click touchend").on "click touchend", =>
+ if window.visible_menu == menu
+ @tag.find(".contents + .flex").removeClass "active"
+ menu.hide()
+ else
+ @tag.find(".contents + .flex").addClass "active"
+ @tag.find(".content-wrapper").prop "scrollTop", 10000
+ menu.show()
+ return false
+
+ $("body").on "click", =>
+ if @tag
+ @tag.find(".contents + .flex").removeClass "active"
+
+ @tag.find("#button-sign-publish").off("click touchend").on "click touchend", =>
+ inner_path = @tag.find("#input-contents").val()
+
+ @wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (res) =>
+ if @wrapper.site_info.privatekey
+ # Privatekey stored in users.json
+ @wrapper.ws.cmd "sitePublish", {privatekey: "stored", inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
+ else if @wrapper.site_info.auth_address in res.signers
+ # ZeroID or other ID provider
+ @wrapper.ws.cmd "sitePublish", {privatekey: null, inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
+ else
+ # Ask the user for privatekey
+ @wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
+ @wrapper.ws.cmd "sitePublish", {privatekey: privatekey, inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
+ if res == "ok"
+ @wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
+
+ return false
+
+ # Close
+ @tag.find(".close").off("click touchend").on "click touchend", (e) =>
+ @close()
+ return false
+
+ @loadGlobe()
+
+ close: ->
+ @move_lock = "x"
+ @startDrag()
+ @stopDrag()
+
+
+ onClosed: ->
+ $(window).off "resize"
+ $(window).on "resize", @resized
+ $(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
+ if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-internals")
+ $(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd
+ @unloadGlobe()
+
+ # We dont need site info anymore
+ @wrapper.setSiteInfo = @original_set_site_info
+
+
+ loadGlobe: =>
+ console.log "loadGlobe", @tag.find(".globe")[0], @tag.find(".globe").hasClass("loading")
+ if @tag.find(".globe").hasClass("loading")
+ setTimeout (=>
+ if typeof(DAT) == "undefined" # Globe script not loaded, do it first
+ script_tag = $("
+