Merge branch 'master' into gh-pages

Conflicts:
	index.html
gh-pages
David Cormier 11 years ago
commit cf1459b469
  1. 31
      3rdparty/inflate.min.js
  2. 6
      3rdparty/inflate.min.js.map
  3. 2
      README.md
  4. 4
      bower.json
  5. 12
      css/glowingbear.css
  6. 88
      index.html
  7. 41
      js/filters.js
  8. 162
      js/glowingbear.js
  9. 7
      js/handlers.js
  10. 7
      js/inputbar.js
  11. 40
      js/localstorage.js
  12. 96
      js/models.js
  13. 26
      js/notifications.js
  14. 10
      js/plugin-directive.js
  15. 187
      js/plugins.js
  16. 68
      js/settings.js
  17. 2
      manifest.json
  18. 11
      manifest.webapp
  19. 4
      min.js
  20. 2
      min.map
  21. 4
      package.json
  22. 2
      test/karma.conf.js
  23. 65
      test/unit/filters.js
  24. 24
      test/unit/plugins.js

@ -1,16 +1,15 @@
/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';function m(b){throw b;}var n=void 0,r=this;function s(b,d){var a=b.split("."),c=r;!(a[0]in c)&&c.execScript&&c.execScript("var "+a[0]);for(var f;a.length&&(f=a.shift());)!a.length&&d!==n?c[f]=d:c=c[f]?c[f]:c[f]={}};var u="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array;function v(b){var d=b.length,a=0,c=Number.POSITIVE_INFINITY,f,e,g,h,k,l,q,p,t;for(p=0;p<d;++p)b[p]>a&&(a=b[p]),b[p]<c&&(c=b[p]);f=1<<a;e=new (u?Uint32Array:Array)(f);g=1;h=0;for(k=2;g<=a;){for(p=0;p<d;++p)if(b[p]===g){l=0;q=h;for(t=0;t<g;++t)l=l<<1|q&1,q>>=1;for(t=l;t<f;t+=k)e[t]=g<<16|p;++h}++g;h<<=1;k<<=1}return[e,a,c]};function w(b,d){this.g=[];this.h=32768;this.d=this.f=this.a=this.l=0;this.input=u?new Uint8Array(b):b;this.m=!1;this.i=x;this.r=!1;if(d||!(d={}))d.index&&(this.a=d.index),d.bufferSize&&(this.h=d.bufferSize),d.bufferType&&(this.i=d.bufferType),d.resize&&(this.r=d.resize);switch(this.i){case y:this.b=32768;this.c=new (u?Uint8Array:Array)(32768+this.h+258);break;case x:this.b=0;this.c=new (u?Uint8Array:Array)(this.h);this.e=this.z;this.n=this.v;this.j=this.w;break;default:m(Error("invalid inflate mode"))}}
var y=0,x=1,z={t:y,s:x};
w.prototype.k=function(){for(;!this.m;){var b=A(this,3);b&1&&(this.m=!0);b>>>=1;switch(b){case 0:var d=this.input,a=this.a,c=this.c,f=this.b,e=n,g=n,h=n,k=c.length,l=n;this.d=this.f=0;e=d[a++];e===n&&m(Error("invalid uncompressed block header: LEN (first byte)"));g=e;e=d[a++];e===n&&m(Error("invalid uncompressed block header: LEN (second byte)"));g|=e<<8;e=d[a++];e===n&&m(Error("invalid uncompressed block header: NLEN (first byte)"));h=e;e=d[a++];e===n&&m(Error("invalid uncompressed block header: NLEN (second byte)"));h|=
e<<8;g===~h&&m(Error("invalid uncompressed block header: length verify"));a+g>d.length&&m(Error("input buffer is broken"));switch(this.i){case y:for(;f+g>c.length;){l=k-f;g-=l;if(u)c.set(d.subarray(a,a+l),f),f+=l,a+=l;else for(;l--;)c[f++]=d[a++];this.b=f;c=this.e();f=this.b}break;case x:for(;f+g>c.length;)c=this.e({p:2});break;default:m(Error("invalid inflate mode"))}if(u)c.set(d.subarray(a,a+g),f),f+=g,a+=g;else for(;g--;)c[f++]=d[a++];this.a=a;this.b=f;this.c=c;break;case 1:this.j(B,C);break;case 2:aa(this);
break;default:m(Error("unknown BTYPE: "+b))}}return this.n()};
var D=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],E=u?new Uint16Array(D):D,F=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],G=u?new Uint16Array(F):F,H=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],I=u?new Uint8Array(H):H,J=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],K=u?new Uint16Array(J):J,L=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,
13],M=u?new Uint8Array(L):L,N=new (u?Uint8Array:Array)(288),O,P;O=0;for(P=N.length;O<P;++O)N[O]=143>=O?8:255>=O?9:279>=O?7:8;var B=v(N),Q=new (u?Uint8Array:Array)(30),R,S;R=0;for(S=Q.length;R<S;++R)Q[R]=5;var C=v(Q);function A(b,d){for(var a=b.f,c=b.d,f=b.input,e=b.a,g;c<d;)g=f[e++],g===n&&m(Error("input buffer is broken")),a|=g<<c,c+=8;g=a&(1<<d)-1;b.f=a>>>d;b.d=c-d;b.a=e;return g}
function T(b,d){for(var a=b.f,c=b.d,f=b.input,e=b.a,g=d[0],h=d[1],k,l,q;c<h;){k=f[e++];if(k===n)break;a|=k<<c;c+=8}l=g[a&(1<<h)-1];q=l>>>16;b.f=a>>q;b.d=c-q;b.a=e;return l&65535}
function aa(b){function d(a,b,c){var d,e,f,g;for(g=0;g<a;)switch(d=T(this,b),d){case 16:for(f=3+A(this,2);f--;)c[g++]=e;break;case 17:for(f=3+A(this,3);f--;)c[g++]=0;e=0;break;case 18:for(f=11+A(this,7);f--;)c[g++]=0;e=0;break;default:e=c[g++]=d}return c}var a=A(b,5)+257,c=A(b,5)+1,f=A(b,4)+4,e=new (u?Uint8Array:Array)(E.length),g,h,k,l;for(l=0;l<f;++l)e[E[l]]=A(b,3);g=v(e);h=new (u?Uint8Array:Array)(a);k=new (u?Uint8Array:Array)(c);b.j(v(d.call(b,a,g,h)),v(d.call(b,c,g,k)))}
w.prototype.j=function(b,d){var a=this.c,c=this.b;this.o=b;for(var f=a.length-258,e,g,h,k;256!==(e=T(this,b));)if(256>e)c>=f&&(this.b=c,a=this.e(),c=this.b),a[c++]=e;else{g=e-257;k=G[g];0<I[g]&&(k+=A(this,I[g]));e=T(this,d);h=K[e];0<M[e]&&(h+=A(this,M[e]));c>=f&&(this.b=c,a=this.e(),c=this.b);for(;k--;)a[c]=a[c++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=c};
w.prototype.w=function(b,d){var a=this.c,c=this.b;this.o=b;for(var f=a.length,e,g,h,k;256!==(e=T(this,b));)if(256>e)c>=f&&(a=this.e(),f=a.length),a[c++]=e;else{g=e-257;k=G[g];0<I[g]&&(k+=A(this,I[g]));e=T(this,d);h=K[e];0<M[e]&&(h+=A(this,M[e]));c+k>f&&(a=this.e(),f=a.length);for(;k--;)a[c]=a[c++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=c};
w.prototype.e=function(){var b=new (u?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,c,f=this.c;if(u)b.set(f.subarray(32768,b.length));else{a=0;for(c=b.length;a<c;++a)b[a]=f[a+32768]}this.g.push(b);this.l+=b.length;if(u)f.set(f.subarray(d,d+32768));else for(a=0;32768>a;++a)f[a]=f[d+a];this.b=32768;return f};
w.prototype.z=function(b){var d,a=this.input.length/this.a+1|0,c,f,e,g=this.input,h=this.c;b&&("number"===typeof b.p&&(a=b.p),"number"===typeof b.u&&(a+=b.u));2>a?(c=(g.length-this.a)/this.o[2],e=258*(c/2)|0,f=e<h.length?h.length+e:h.length<<1):f=h.length*a;u?(d=new Uint8Array(f),d.set(h)):d=h;return this.c=d};
w.prototype.n=function(){var b=0,d=this.c,a=this.g,c,f=new (u?Uint8Array:Array)(this.l+(this.b-32768)),e,g,h,k;if(0===a.length)return u?this.c.subarray(32768,this.b):this.c.slice(32768,this.b);e=0;for(g=a.length;e<g;++e){c=a[e];h=0;for(k=c.length;h<k;++h)f[b++]=c[h]}e=32768;for(g=this.b;e<g;++e)f[b++]=d[e];this.g=[];return this.buffer=f};
w.prototype.v=function(){var b,d=this.b;u?this.r?(b=new Uint8Array(d),b.set(this.c.subarray(0,d))):b=this.c.subarray(0,d):(this.c.length>d&&(this.c.length=d),b=this.c);return this.buffer=b};function U(b,d){var a,c;this.input=b;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.A=d.verify);a=b[this.a++];c=b[this.a++];switch(a&15){case V:this.method=V;break;default:m(Error("unsupported compression method"))}0!==((a<<8)+c)%31&&m(Error("invalid fcheck flag:"+((a<<8)+c)%31));c&32&&m(Error("fdict flag is not supported"));this.q=new w(b,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})}
U.prototype.k=function(){var b=this.input,d,a;d=this.q.k();this.a=this.q.a;if(this.A){a=(b[this.a++]<<24|b[this.a++]<<16|b[this.a++]<<8|b[this.a++])>>>0;var c=d;if("string"===typeof c){var f=c.split(""),e,g;e=0;for(g=f.length;e<g;e++)f[e]=(f[e].charCodeAt(0)&255)>>>0;c=f}for(var h=1,k=0,l=c.length,q,p=0;0<l;){q=1024<l?1024:l;l-=q;do h+=c[p++],k+=h;while(--q);h%=65521;k%=65521}a!==(k<<16|h)>>>0&&m(Error("invalid adler-32 checksum"))}return d};var V=8;s("Zlib.Inflate",U);s("Zlib.Inflate.prototype.decompress",U.prototype.k);var W={ADAPTIVE:z.s,BLOCK:z.t},X,Y,Z,$;if(Object.keys)X=Object.keys(W);else for(Y in X=[],Z=0,W)X[Z++]=Y;Z=0;for($=X.length;Z<$;++Z)Y=X[Z],s("Zlib.Inflate.BufferType."+Y,W[Y]);}).call(this); //@ sourceMappingURL=inflate.min.js.map
/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';var m=this;function q(c,d){var a=c.split("."),b=m;!(a[0]in b)&&b.execScript&&b.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)!a.length&&void 0!==d?b[e]=d:b=b[e]?b[e]:b[e]={}};var s="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function t(c){var d=c.length,a=0,b=Number.POSITIVE_INFINITY,e,f,g,h,k,l,p,n,r,K;for(n=0;n<d;++n)c[n]>a&&(a=c[n]),c[n]<b&&(b=c[n]);e=1<<a;f=new (s?Uint32Array:Array)(e);g=1;h=0;for(k=2;g<=a;){for(n=0;n<d;++n)if(c[n]===g){l=0;p=h;for(r=0;r<g;++r)l=l<<1|p&1,p>>=1;K=g<<16|n;for(r=l;r<e;r+=k)f[r]=K;++h}++g;h<<=1;k<<=1}return[f,a,b]};function u(c,d){this.g=[];this.h=32768;this.d=this.f=this.a=this.l=0;this.input=s?new Uint8Array(c):c;this.m=!1;this.i=v;this.s=!1;if(d||!(d={}))d.index&&(this.a=d.index),d.bufferSize&&(this.h=d.bufferSize),d.bufferType&&(this.i=d.bufferType),d.resize&&(this.s=d.resize);switch(this.i){case w:this.b=32768;this.c=new (s?Uint8Array:Array)(32768+this.h+258);break;case v:this.b=0;this.c=new (s?Uint8Array:Array)(this.h);this.e=this.A;this.n=this.w;this.j=this.z;break;default:throw Error("invalid inflate mode");
}}var w=0,v=1,x={u:w,t:v};
u.prototype.k=function(){for(;!this.m;){var c=y(this,3);c&1&&(this.m=!0);c>>>=1;switch(c){case 0:var d=this.input,a=this.a,b=this.c,e=this.b,f=d.length,g=void 0,h=void 0,k=b.length,l=void 0;this.d=this.f=0;if(a+1>=f)throw Error("invalid uncompressed block header: LEN");g=d[a++]|d[a++]<<8;if(a+1>=f)throw Error("invalid uncompressed block header: NLEN");h=d[a++]|d[a++]<<8;if(g===~h)throw Error("invalid uncompressed block header: length verify");if(a+g>d.length)throw Error("input buffer is broken");switch(this.i){case w:for(;e+
g>b.length;){l=k-e;g-=l;if(s)b.set(d.subarray(a,a+l),e),e+=l,a+=l;else for(;l--;)b[e++]=d[a++];this.b=e;b=this.e();e=this.b}break;case v:for(;e+g>b.length;)b=this.e({p:2});break;default:throw Error("invalid inflate mode");}if(s)b.set(d.subarray(a,a+g),e),e+=g,a+=g;else for(;g--;)b[e++]=d[a++];this.a=a;this.b=e;this.c=b;break;case 1:this.j(z,A);break;case 2:B(this);break;default:throw Error("unknown BTYPE: "+c);}}return this.n()};
var C=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],D=s?new Uint16Array(C):C,E=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],F=s?new Uint16Array(E):E,G=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],H=s?new Uint8Array(G):G,I=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],J=s?new Uint16Array(I):I,L=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,
13],M=s?new Uint8Array(L):L,N=new (s?Uint8Array:Array)(288),O,P;O=0;for(P=N.length;O<P;++O)N[O]=143>=O?8:255>=O?9:279>=O?7:8;var z=t(N),Q=new (s?Uint8Array:Array)(30),R,S;R=0;for(S=Q.length;R<S;++R)Q[R]=5;var A=t(Q);function y(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h;b<d;){if(f>=g)throw Error("input buffer is broken");a|=e[f++]<<b;b+=8}h=a&(1<<d)-1;c.f=a>>>d;c.d=b-d;c.a=f;return h}
function T(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h=d[0],k=d[1],l,p;b<k&&!(f>=g);)a|=e[f++]<<b,b+=8;l=h[a&(1<<k)-1];p=l>>>16;c.f=a>>p;c.d=b-p;c.a=f;return l&65535}
function B(c){function d(a,c,b){var d,e=this.q,f,g;for(g=0;g<a;)switch(d=T(this,c),d){case 16:for(f=3+y(this,2);f--;)b[g++]=e;break;case 17:for(f=3+y(this,3);f--;)b[g++]=0;e=0;break;case 18:for(f=11+y(this,7);f--;)b[g++]=0;e=0;break;default:e=b[g++]=d}this.q=e;return b}var a=y(c,5)+257,b=y(c,5)+1,e=y(c,4)+4,f=new (s?Uint8Array:Array)(D.length),g,h,k,l;for(l=0;l<e;++l)f[D[l]]=y(c,3);if(!s){l=e;for(e=f.length;l<e;++l)f[D[l]]=0}g=t(f);h=new (s?Uint8Array:Array)(a);k=new (s?Uint8Array:Array)(b);c.q=0;
c.j(t(d.call(c,a,g,h)),t(d.call(c,b,g,k)))}u.prototype.j=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length-258,f,g,h,k;256!==(f=T(this,c));)if(256>f)b>=e&&(this.b=b,a=this.e(),b=this.b),a[b++]=f;else{g=f-257;k=F[g];0<H[g]&&(k+=y(this,H[g]));f=T(this,d);h=J[f];0<M[f]&&(h+=y(this,M[f]));b>=e&&(this.b=b,a=this.e(),b=this.b);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b};
u.prototype.z=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length,f,g,h,k;256!==(f=T(this,c));)if(256>f)b>=e&&(a=this.e(),e=a.length),a[b++]=f;else{g=f-257;k=F[g];0<H[g]&&(k+=y(this,H[g]));f=T(this,d);h=J[f];0<M[f]&&(h+=y(this,M[f]));b+k>e&&(a=this.e(),e=a.length);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b};
u.prototype.e=function(){var c=new (s?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,b,e=this.c;if(s)c.set(e.subarray(32768,c.length));else{a=0;for(b=c.length;a<b;++a)c[a]=e[a+32768]}this.g.push(c);this.l+=c.length;if(s)e.set(e.subarray(d,d+32768));else for(a=0;32768>a;++a)e[a]=e[d+a];this.b=32768;return e};
u.prototype.A=function(c){var d,a=this.input.length/this.a+1|0,b,e,f,g=this.input,h=this.c;c&&("number"===typeof c.p&&(a=c.p),"number"===typeof c.v&&(a+=c.v));2>a?(b=(g.length-this.a)/this.o[2],f=258*(b/2)|0,e=f<h.length?h.length+f:h.length<<1):e=h.length*a;s?(d=new Uint8Array(e),d.set(h)):d=h;return this.c=d};
u.prototype.n=function(){var c=0,d=this.c,a=this.g,b,e=new (s?Uint8Array:Array)(this.l+(this.b-32768)),f,g,h,k;if(0===a.length)return s?this.c.subarray(32768,this.b):this.c.slice(32768,this.b);f=0;for(g=a.length;f<g;++f){b=a[f];h=0;for(k=b.length;h<k;++h)e[c++]=b[h]}f=32768;for(g=this.b;f<g;++f)e[c++]=d[f];this.g=[];return this.buffer=e};
u.prototype.w=function(){var c,d=this.b;s?this.s?(c=new Uint8Array(d),c.set(this.c.subarray(0,d))):c=this.c.subarray(0,d):(this.c.length>d&&(this.c.length=d),c=this.c);return this.buffer=c};function U(c,d){var a,b;this.input=c;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.B=d.verify);a=c[this.a++];b=c[this.a++];switch(a&15){case V:this.method=V;break;default:throw Error("unsupported compression method");}if(0!==((a<<8)+b)%31)throw Error("invalid fcheck flag:"+((a<<8)+b)%31);if(b&32)throw Error("fdict flag is not supported");this.r=new u(c,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})}
U.prototype.k=function(){var c=this.input,d,a;d=this.r.k();this.a=this.r.a;if(this.B){a=(c[this.a++]<<24|c[this.a++]<<16|c[this.a++]<<8|c[this.a++])>>>0;var b=d;if("string"===typeof b){var e=b.split(""),f,g;f=0;for(g=e.length;f<g;f++)e[f]=(e[f].charCodeAt(0)&255)>>>0;b=e}for(var h=1,k=0,l=b.length,p,n=0;0<l;){p=1024<l?1024:l;l-=p;do h+=b[n++],k+=h;while(--p);h%=65521;k%=65521}if(a!==(k<<16|h)>>>0)throw Error("invalid adler-32 checksum");}return d};var V=8;q("Zlib.Inflate",U);q("Zlib.Inflate.prototype.decompress",U.prototype.k);var W={ADAPTIVE:x.t,BLOCK:x.u},X,Y,Z,$;if(Object.keys)X=Object.keys(W);else for(Y in X=[],Z=0,W)X[Z++]=Y;Z=0;for($=X.length;Z<$;++Z)Y=X[Z],q("Zlib.Inflate.BufferType."+Y,W[Y]);}).call(this); //@ sourceMappingURL=inflate.min.js.map

File diff suppressed because one or more lines are too long

@ -83,4 +83,4 @@ If you wish to submit code, we try to make the contribution process as simple as
We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes.
If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app.
If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), [twemoji](https://github.com/twitter/twemoji), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app.

@ -8,8 +8,10 @@
"dependencies": {
"angular": "1.3.x",
"angular-route": "1.3.x",
"angular-sanitize": "1.3.x",
"angular-touch": "1.3.x",
"angular-loader": "1.3.x",
"angular-mocks": "~1.3.x",
"angular-mocks": "1.3.x",
"html5-boilerplate": "~4.3.0"
}
}

@ -309,6 +309,10 @@ div.embed img.embed {
max-width: 100%;
}
video.embed {
max-width: 100%;
}
div.colourbox {
display: inline-block;
border-radius: 3px;
@ -522,6 +526,14 @@ li.buffer.indent.private a {
user-select: none;
}
/* Scales emoji to font size */
img.emoji {
height: 1em;
width: 1em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
}
/* */
/* Mobile layout */
/* */

@ -7,23 +7,25 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="theme-color" content="#2779d3">
<title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link rel="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" >
<link href="css/glowingbear.css" rel="stylesheet" media="screen">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-touch.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="//twemoji.maxcdn.com/twemoji.min.js"></script>
<script type="text/javascript" src="3rdparty/inflate.min.js"></script>
<script type="text/javascript" src="min.js"></script>
<script type="text/javascript" src="3rdparty/favico-0.3.5.min.js"></script>
</head>
<body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" lang="en-US">
<link ng-href="css/themes/{{theme}}.css" rel="stylesheet" media="screen" />
<link ng-href="css/themes/{{settings.theme}}.css" rel="stylesheet" media="screen" />
<div ng-hide="connected" class="container">
<h2>
<img alt="logo" src="assets/img/glowing-bear.svg">
@ -56,10 +58,10 @@
<div class="input-group">
<div class="row no-gutter">
<div class="col-sm-9">
<input type="text" class="form-control favorite-font" id="host" ng-model="host" placeholder="Address" autocapitalize="off">
<input type="text" class="form-control favorite-font" id="host" ng-model="settings.host" placeholder="Address" autocapitalize="off">
</div>
<div class="col-sm-3">
<input type="text" class="form-control favorite-font" id="port" ng-model="port" placeholder="Port">
<input type="text" class="form-control favorite-font" id="port" ng-model="settings.port" placeholder="Port">
</div>
</div>
</div>
@ -71,19 +73,19 @@
<div class="checkbox">
<label class="control-label" for="savepassword">
<input type="checkbox" id="savepassword" ng-model="savepassword">
<input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Save password in your browser
</label>
</div>
<div class="checkbox" ng-show="savepassword">
<div class="checkbox" ng-show="settings.savepassword">
<label class="control-label" for="autoconnect">
<input type="checkbox" id="autoconnect" ng-model="autoconnect">
<input type="checkbox" id="autoconnect" ng-model="settings.autoconnect">
Automatically connect
</label>
</div>
<div class="checkbox">
<label class="control-label" for="ssl">
<input type="checkbox" id="ssl" ng-model="ssl">
<input type="checkbox" id="ssl" ng-model="settings.ssl">
Encryption. Read instructions for help
</label>
</div>
@ -107,7 +109,7 @@
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
<pre>
/set relay.network.password yourpassword
/relay add weechat {{ port || 9001 }}
/relay add weechat {{ settings.port || 9001 }}
</pre>
<span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br>
The communication goes directly between your browser and your WeeChat relay in plain text. Check the instructions below for help on setting up encrypted communication.
@ -142,18 +144,18 @@
<div id="collapseThree" class="panel-collapse collapse in">
<div class="panel-body">
<p>If you check the encryption box, the communication between browser and WeeChat will be encrypted with TLS.</p>
<p><strong>Note</strong>: If you are using a self-signed certificate, you have to visit <a href="https://{{ host }}:{{ port }}/">https://{{ host || 'weechathost' }}:{{ port || 'relayport' }}/</a> in your browser first to add a security exception. You can close that tab once you confirmed the certificate, no content will appear. The necessity of this process is a bug in <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">Firefox</a> and other browsers.</p>
<p><strong>Note</strong>: If you are using a self-signed certificate, you have to visit <a href="https://{{ settings.host }}:{{ settings.port }}/">https://{{ settings.host || 'weechathost' }}:{{ settings.port || 'relayport' }}/</a> in your browser first to add a security exception. You can close that tab once you confirmed the certificate, no content will appear. The necessity of this process is a bug in <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">Firefox</a> and other browsers.</p>
<p><strong>Setup</strong>: If you want to use an encrypted session you first have to set up the relay to use TLS. You basically have two options: a self-signed certificate is easier to set up, but requires manual security exceptions. Using a certificate that is trusted by your browser requires more setup, but offers greater convenience later on and does not require security exceptions. You can find a guide to set up WeeChat with a free trusted certificate from StartSSL <a href="https://4z2.de/2014/07/06/weechat-trusted-relay">here</a>. Should you wish to use a self-signed certificate instead, execute the following commands in a shell on the same host and as the user running WeeChat:</p>
<pre>
$ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -subj "/CN={{host || 'your weechat host'}}/"
$ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -subj "/CN={{settings.host || 'your weechat host'}}/"
</pre>
<p>If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ port || 9001 }} with these WeeChat commands:</p>
<p>If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ settings.port || 9001 }} with these WeeChat commands:</p>
<pre>
/set relay.network.password yourpassword
/relay sslcertkey
/relay add ssl.weechat {{ port || 9001 }}
/relay add ssl.weechat {{ settings.port || 9001 }}
</pre>
</div>
</div>
@ -173,7 +175,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<p>If you have a recent version of Firefox you can install glowing bear as an app. Click the button to install.</p>
<button class="btn btn-lg btn-primary" ng-click="install()">Install Firefox app <i class="glyphicon glyphicon-chevron-right"></i></button>
<h3>Chrome</h3>
<p>To install glowing bear as an app in Chrome, select <kbd>Menu - Add to home screen</kbd> (Android) or <kbd>Menu - Tools - Create Application Shortcuts</kbd> (desktop version).</p>
<p>To install glowing bear as an app in Chrome, select <kbd>Menu - Add to home screen</kbd> (Android) or <kbd>Menu - More tools - Create application shortcuts</kbd> (desktop version).</p>
</div>
</div>
</div>
@ -198,11 +200,13 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div id="topbar">
<div class="brand">
<a href="#" ng-click="toggleSidebar()">
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ host }}:{{ port}}">
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}">
</a>
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div>
<div class="title" ng-bind-html="activeBuffer().title | linky:'_blank' | DOMfilter:'irclinky'" title="{{activeBuffer().title}}"></div>
<div class="title" title="{{activeBuffer().rtitle}}">
<span ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span>
</div>
<div class="actions pull-right vertical-line-left">
<div class="pull-left">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu">
@ -222,7 +226,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</form>
</li>
<li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent, 'channel': buffer.type === 'channel', 'channel_hash': buffer.prefix === '#', 'channel_plus': buffer.prefix === '+', 'channel_ampersand': buffer.prefix === '&', 'private': buffer.type === 'private'}" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:{fullName:search} | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))">
<a href="#" ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}">
<a ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}">
<span class="badge pull-right" ng-class="{'danger': buffer.notification}" ng-if="buffer.notification || buffer.unread" ng-bind="buffer.notification || buffer.unread"></span>
<span class="buffer-quick-key">{{ buffer.$quickKey }}</span>
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
@ -238,7 +242,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</li>
</ul>
</div>
<table ng-class="{'notimestamp':!showtimestamp,'notimestampseconds':!showtimestampSeconds}">
<table ng-class="{'notimestamp':!settings.showtimestamp,'notimestampseconds':!settings.showtimestampSeconds}">
<tbody>
<tr class="bufferline">
<td ng-hide="activeBuffer().allLinesFetched" colspan="3">
@ -257,7 +261,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text"></span></a></td><!--
--><td class="message"><!--
--><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!--
--><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'inlinecolour'"></span>
--><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':enableJSEmoji | DOMfilter:'inlinecolour' "></span>
</td>
</tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
@ -289,12 +293,12 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="form-group">
<label for="font" class="col-sm-3 control-label make-thinner">Preferred font</label>
<div class="col-sm-4">
<input type="text" ng-model="fontfamily" class="form-control" id="font">
<input type="text" ng-model="settings.fontfamily" class="form-control" id="font">
</div>
<label for="size" class="col-sm-1 control-label">Size</label>
<div class="col-sm-2">
<input type="text" ng-model="fontsize" class="form-control" id="size">
<input type="text" ng-model="settings.fontsize" class="form-control" id="size">
</div>
</div>
</form>
@ -304,7 +308,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="form-group">
<label for="theme" class="col-sm-3 control-label make-thinner">Theme</label>
<div class="col-sm-7">
<select id="theme" class="form-control" ng-model="theme" ng-options="theme for theme in themes"></select>
<select id="theme" class="form-control" ng-model="settings.theme" ng-options="theme for theme in themes"></select>
</div>
</div>
</form>
@ -314,7 +318,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="onlyUnread">
<input type="checkbox" ng-model="settings.onlyUnread">
Only show buffers with unread messages
</label>
</div>
@ -324,17 +328,17 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="showtimestamp">
<input type="checkbox" ng-model="settings.showtimestamp">
Show timestamps
</label>
</div>
</form>
<ul ng-show="showtimestamp">
<ul ng-show="settings.showtimestamp">
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="showtimestampSeconds">
<input type="checkbox" ng-model="settings.showtimestampSeconds">
Show seconds
</label>
</div>
@ -346,8 +350,8 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="noembed">
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden</span>
<input type="checkbox" ng-model="settings.noembed">
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span>
</label>
</div>
</form>
@ -356,7 +360,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="hotlistsync">
<input type="checkbox" ng-model="settings.hotlistsync">
Mark messages as read in WeeChat
</label>
</div>
@ -366,7 +370,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="nonicklist">
<input type="checkbox" ng-model="settings.nonicklist">
Hide nicklist
</label>
</div>
@ -376,7 +380,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="orderbyserver">
<input type="checkbox" ng-model="settings.orderbyserver">
Hierarchical buffer view (order by server)
</label>
</div>
@ -386,7 +390,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="readlineBindings">
<input type="checkbox" ng-model="settings.readlineBindings">
Enable common readline keybindings in input bar
</label>
</div>
@ -396,7 +400,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="useFavico">
<input type="checkbox" ng-model="settings.useFavico">
Display unread count in favicon
</label>
</div>
@ -406,12 +410,22 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="soundnotification">
<input type="checkbox" ng-model="settings.soundnotification">
Play sound on notification
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="enableJSEmoji">
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images</span>
</label>
</div>
</form>
</li>
</ul>
</div>
<div class="modal-footer">

@ -30,12 +30,19 @@ weechat.filter('irclinky', ['$filter', function($filter) {
return text;
}
// First, escape entities to prevent escaping issues because it's a bad idea
// to parse/modify HTML with regexes, which we do a couple of lines down...
var entities = {"<": "&lt;", ">": "&gt;", '"': '&quot;', "'": '&#39;', "&": "&amp;", "/": '&#x2F;'};
text = text.replace(/[<>"'&\/]/g, function (char) {
return entities[char];
});
// This regex in no way matches all IRC channel names (they could also begin with &, + or an
// exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50).
// However, it matches all *common* IRC channels while trying to minimise false positives.
// "#1" is much more likely to be "number 1" than "IRC channel #1".
// Thus, we only match channels beginning with a # and having at least one letter in them.
var channelRegex = /(^|[\s,.:;?!"'()+@-])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi;
var channelRegex = /(^|[\s,.:;?!"'()+@-\~%])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi;
// This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik.
// Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this.
var substitute = '$1<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'$2\'); $scope.$apply();">$2</a>';
@ -50,10 +57,12 @@ weechat.filter('inlinecolour', function() {
}
// only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc)
var hexColourRegex = /(^|[^&])\#([0-9a-f]{6})($|[^\w'"])/gmi;
var substitute = '$1#$2 <div class="colourbox" style="background-color:#$2"></div> $3';
return text.replace(hexColourRegex, substitute);
var hexColourRegex = /(^|[^&])(\#[0-9a-f]{6};?)(?!\w)/gmi;
var rgbColourRegex = /(.?)(rgba?\((?:\s*\d+\s*,){2}\s*\d+\s*(?:,\s*[\d.]+\s*)?\);?)/gmi;
var substitute = '$1$2 <div class="colourbox" style="background-color:$2"></div>';
text = text.replace(hexColourRegex, substitute);
text = text.replace(rgbColourRegex, substitute);
return text;
};
});
@ -64,6 +73,9 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
return text;
}
// hacky way to pass an extra argument without using .apply, which
// would require assembling an argument array. PERFORMANCE!!!
var extraArgument = (arguments.length > 2) ? arguments[2] : null;
var filterFunction = $filter(filter);
var el = document.createElement('div');
el.innerHTML = text;
@ -71,7 +83,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
// Recursive DOM-walking function applying the filter to the text nodes
var process = function(node) {
if (node.nodeType === 3) { // text node
var value = filterFunction(node.nodeValue);
var value = filterFunction(node.nodeValue, extraArgument);
if (value !== node.nodeValue) {
// we changed something. create a new node to replace the current one
// we could also only add its children but that would probably incur
@ -87,13 +99,15 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
} else {
parent.appendChild(newNode);
}
return newNode;
}
}
// recurse
if (node === undefined || node === null) return;
node = node.firstChild;
while (node) {
process(node);
node = node.nextSibling;
var nextNode = process(node);
node = (nextNode ? nextNode : node).nextSibling;
}
};
@ -125,4 +139,15 @@ weechat.filter('getBufferQuickKeys', function () {
};
});
// Emojifis the string using https://github.com/twitter/twemoji
weechat.filter('emojify', function() {
return function(text, enable_JS_Emoji) {
if (enable_JS_Emoji === true) {
return twemoji.parse(text);
} else {
return(text);
}
};
});
})();

@ -1,16 +1,44 @@
(function() {
'use strict';
var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch']);
var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], ['$compileProvider', function($compileProvider) {
// hacky way to be able to find out if we're in debug mode
weechat.compileProvider = $compileProvider;
}]);
weechat.config(['$compileProvider', function ($compileProvider) {
// hack to determine whether we're executing the tests
if (typeof(it) === "undefined" && typeof(describe) === "undefined") {
$compileProvider.debugInfoEnabled(false);
}
}]);
weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) {
weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings',
function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) {
$scope.command = '';
$scope.themes = ['dark', 'light'];
settings.setDefaults({
'theme': 'dark',
'host': 'localhost',
'port': 9001,
'ssl': (window.location.protocol === "https:"),
'savepassword': false,
'autoconnect': false,
'nonicklist': utils.isMobileUi(),
'noembed': utils.isMobileUi(),
'onlyUnread': false,
'hotlistsync': true,
'orderbyserver': true,
'useFavico': true,
'showtimestamp': true,
'showtimestampSeconds': false,
'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false
});
$scope.settings = settings;
// From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian"
$rootScope.countWatchers = function () {
var q = [$rootScope], watchers = 0, scope;
@ -66,22 +94,16 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Enable debug mode if "?debug=1" or "?debug=true" is set
(function() {
var hasReloaded = false;
window.location.search.substring(1).split('&').forEach(function(f) {
var segs = f.split('=');
if (segs[0] === "debug" && ["true", "1"].indexOf(segs[1]) != -1) {
$rootScope.debugMode = true;
} else if (segs[0] === "debugReload" && segs[1] === "1") {
hasReloaded = true;
}
});
// If we haven't reloaded yet, do an angular reload with debug infos
// store whether this has happened yet in a GET parameter
if ($rootScope.debugMode && !hasReloaded) {
document.location.search += "&debugReload=1";
setTimeout(function() {
if ($rootScope.debugMode && !weechat.compileProvider.debugInfoEnabled()) {
angular.reloadWithDebugInfo();
}, 0);
}
})();
@ -193,7 +215,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// we will send a /buffer bufferName command every time
// the user switches a buffer. This will ensure that notifications
// are cleared in the buffer the user switches to
if ($scope.hotlistsync && ab.fullName) {
if (settings.hotlistsync && ab.fullName) {
connection.sendCoreCommand('/buffer ' + ab.fullName);
}
@ -215,12 +237,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.$on('notificationChanged', function() {
notifications.updateTitle();
if ($scope.useFavico && $rootScope.favico) {
if (settings.useFavico && $rootScope.favico) {
notifications.updateFavico();
}
});
$rootScope.$on('relayDisconnect', function() {
// Reset title
$rootScope.pageTitle = '';
$rootScope.notificationStatus = '';
notifications.cancelAll();
models.reinitialize();
$rootScope.$emit('notificationChanged');
$scope.connectbutton = 'Connect';
@ -241,70 +268,31 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.iterCandidate = null;
$store.bind($scope, "host", "localhost");
$store.bind($scope, "port", "9001");
$store.bind($scope, "proto", "weechat");
$store.bind($scope, "ssl", (window.location.protocol === "https:"));
$store.bind($scope, "savepassword", false);
if ($scope.savepassword) {
$store.bind($scope, "password", "");
if (settings.savepassword) {
$scope.$watch('password', function() {
settings.password = $scope.password;
});
settings.addCallback('password', function(password) {
$scope.password = password;
});
$scope.password = settings.password;
} else {
settings.password = '';
}
$store.bind($scope, "autoconnect", false);
// If we are on mobile change some defaults
// We use 968 px as the cutoff, which should match the value in glowingbear.css
var nonicklist = false;
var noembed = false;
var showtimestamp = true;
$rootScope.wasMobileUi = false;
if (utils.isMobileUi()) {
nonicklist = true;
noembed = true;
$rootScope.wasMobileUi = true;
}
// Save setting for displaying only buffers with unread messages
$store.bind($scope, "onlyUnread", false);
// Save setting for syncing hotlist
$store.bind($scope, "hotlistsync", true);
// Save setting for displaying nicklist
$store.bind($scope, "nonicklist", nonicklist);
// Save setting for displaying embeds
$store.bind($scope, "noembed", noembed);
// Save setting for channel ordering
$store.bind($scope, "orderbyserver", true);
// Save setting for updating favicon
$store.bind($scope, "useFavico", true);
// Save setting for showtimestamp
$store.bind($scope, "showtimestamp", showtimestamp);
// Save setting for showing seconds on timestamps
$store.bind($scope, "showtimestampSeconds", false);
// Save setting for playing sound on notification
$store.bind($scope, "soundnotification", false);
// Save setting for font family
$store.bind($scope, "fontfamily");
// Save setting for theme
$store.bind($scope, "theme", 'dark');
// Save setting for font size
$store.bind($scope, "fontsize", "14px");
// Save setting for readline keybindings
$store.bind($scope, "readlineBindings", false);
if (!$scope.fontfamily) {
if (!settings.fontfamily) {
if (utils.isMobileUi()) {
$scope.fontfamily = 'sans-serif';
settings.fontfamily = 'sans-serif';
} else {
$scope.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace";
settings.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace";
}
}
// Save setting for displaying embeds in rootScope so it can be used from service
$rootScope.auto_display_embedded_content = $scope.noembed === false;
$scope.isSidebarVisible = function() {
return document.getElementById('content').getAttribute('sidebar-state') === 'visible';
};
@ -326,9 +314,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
document.getElementById('content').setAttribute('sidebar-state', 'hidden');
}
};
// This also fires on page load
$scope.$watch('autoconnect', function() {
if ($scope.autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
settings.addCallback('autoconnect', function(autoconnect) {
if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
$scope.connect();
}
});
@ -347,35 +334,31 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Open and close panels while on mobile devices through swiping
$scope.openNick = function() {
if (utils.isMobileUi()) {
if ($scope.nonicklist) {
$scope.nonicklist = false;
if (settings.nonicklist) {
settings.nonicklist = false;
}
}
};
$scope.closeNick = function() {
if (utils.isMobileUi()) {
if (!$scope.nonicklist) {
$scope.nonicklist = true;
if (!settings.nonicklist) {
settings.nonicklist = true;
}
}
};
// Watch model and update show setting when it changes
$scope.$watch('noembed', function() {
$rootScope.auto_display_embedded_content = $scope.noembed === false;
});
// Watch model and update channel sorting when it changes
$scope.$watch('orderbyserver', function() {
$rootScope.predicate = $scope.orderbyserver ? 'serverSortKey' : 'number';
settings.addCallback('orderbyserver', function(orderbyserver) {
$rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number';
});
$scope.$watch('useFavico', function() {
settings.addCallback('useFavico', function(useFavico) {
// this check is necessary as this is called on page load, too
if (!$rootScope.connected) {
return;
}
if ($scope.useFavico) {
if (useFavico) {
notifications.updateFavico();
} else {
$rootScope.favico.reset();
@ -383,17 +366,12 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
});
// Update font family when changed
$scope.$watch('fontfamily', function() {
utils.changeClassStyle('favorite-font', 'fontFamily', $scope.fontfamily);
settings.addCallback('fontfamily', function(fontfamily) {
utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily);
});
// Update font size when changed
$scope.$watch('fontsize', function() {
utils.changeClassStyle('favorite-font', 'fontSize', $scope.fontsize);
});
// Crude scoping hack. The keypress listener does not live in the same scope as
// the checkbox, so we need to transfer this between scopes here.
$scope.$watch('readlineBindings', function() {
$rootScope.readlineBindings = $scope.readlineBindings;
settings.addCallback('fontsize', function(fontsize) {
utils.changeClassStyle('favorite-font', 'fontSize', fontsize);
});
$scope.setActiveBuffer = function(bufferId, key) {
@ -519,7 +497,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.errorMessage = false;
$rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting ...';
connection.connect($scope.host, $scope.port, $scope.password, $scope.ssl);
connection.connect(settings.host, settings.port, $scope.password, settings.ssl);
};
$scope.disconnect = function() {
$scope.connectbutton = 'Connect';
@ -587,7 +565,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
if ($scope.search && $scope.search !== "") {
return true;
}
if ($scope.onlyUnread) {
if (settings.onlyUnread) {
// Always show current buffer in list
if (models.getActiveBuffer() === buffer) {
return true;
@ -602,7 +580,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
};
// Watch model and update show setting when it changes
$scope.$watch('nonicklist', function() {
settings.addCallback('nonicklist', function() {
$scope.showNicklist = $scope.updateShowNicklist();
// restore bottom view
if ($rootScope.connected && $rootScope.bufferBottom) {
@ -621,7 +599,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return false;
}
// Check if option no nicklist is set
if ($scope.nonicklist) {
if (settings.nonicklist) {
return false;
}
// Check if nicklist is empty
@ -655,7 +633,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
};
// Helper function since the keypress handler is in a different scope
$rootScope.toggleNicklist = function() {
$scope.nonicklist = !$scope.nonicklist;
settings.nonicklist = !settings.nonicklist;
};

@ -64,8 +64,13 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.fullName = obj.full_name;
old.title = obj.title;
old.title = models.parseRichText(obj.title);
old.number = obj.number;
old.rtitle = "";
for (var i = 0; i < old.title.length; ++i) {
old.rtitle += old.title[i].text;
}
};
var handleBufferRenamed = function(message) {

@ -14,13 +14,14 @@ weechat.directive('inputBar', function() {
command: '=command'
},
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', function($rootScope,
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', 'settings', function($rootScope,
$scope,
$element, //XXX do we need this? don't seem to be using it
$log,
connection, //XXX we should eliminate this dependency and use signals instead
models,
IrcUtils) {
IrcUtils,
settings) {
/*
* Returns the input element
@ -340,7 +341,7 @@ weechat.directive('inputBar', function() {
}
// Some readline keybindings
if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) {
if (settings.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) {
// get current caret position
caretPos = inputNode.selectionStart;
// Ctrl-a

@ -10,6 +10,10 @@ ls.factory("$store", ["$parse", function($parse){
var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage,
supported = !(typeof storage == 'undefined' || typeof window.JSON == 'undefined');
if (!supported) {
console.log('Warning: localStorage is not supported');
}
var privateMethods = {
/**
* Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object)
@ -29,7 +33,7 @@ ls.factory("$store", ["$parse", function($parse){
if (val === 'false'){
val = false;
}
if (parseFloat(val) === val && !angular.isObject(val)) {
if (parseFloat(val) == val && !angular.isObject(val)) {
val = parseFloat(val);
}
} catch(e){
@ -40,59 +44,55 @@ ls.factory("$store", ["$parse", function($parse){
};
var publicMethods = {
/**
* Set - let's you set a new localStorage key pair set
* Set - lets you set a new localStorage key pair set
* @param key - a string that will be used as the accessor for the pair
* @param value - the value of the localStorage item
* @returns {*} - will return whatever it is you've stored in the local storage
*/
set: function(key,value){
if (!supported){
try {
$.cookie(key, value);
return value;
} catch(e){
console.log('Local Storage not supported, make sure you have the $.cookie supported.');
}
console.log('Local Storage not supported');
}
var saver = JSON.stringify(value);
storage.setItem(key, saver);
return privateMethods.parseValue(saver);
},
/**
* Get - let's you get the value of any pair you've stored
* Get - lets you get the value of any pair you've stored
* @param key - the string that you set as accessor for the pair
* @returns {*} - Object,String,Float,Boolean depending on what you stored
*/
get: function(key){
if (!supported){
try {
return privateMethods.parseValue($.cookie(key));
} catch(e){
return null;
}
}
var item = storage.getItem(key);
return privateMethods.parseValue(item);
},
/**
* Remove - let's you nuke a value from localStorage
* Remove - lets you nuke a value from localStorage
* @param key - the accessor value
* @returns {boolean} - if everything went as planned
*/
remove: function(key) {
if (!supported){
try {
$.cookie(key, null);
return true;
} catch(e){
return false;
}
}
storage.removeItem(key);
return true;
},
/**
* Bind - let's you directly bind a localStorage value to a $scope variable
* Enumerate all keys
*/
enumerateKeys: function() {
var keys = [];
for (var i = 0, len = storage.length; i < len; ++i) {
keys.push(storage.key(i));
}
return keys;
},
/**
* Bind - lets you directly bind a localStorage value to a $scope variable
* @param $scope - the current scope you want the variable available in
* @param key - the name of the variable you are binding
* @param def - the default value (OPTIONAL)

@ -8,6 +8,48 @@
var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) {
var parseRichText = function(text) {
var textElements = weeChat.Protocol.rawText2Rich(text),
typeToClassPrefixFg = {
'option': 'cof-',
'weechat': 'cwf-',
'ext': 'cef-'
},
typeToClassPrefixBg = {
'option': 'cob-',
'weechat': 'cwb-',
'ext': 'ceb-'
};
textElements.forEach(function(textEl) {
textEl.classes = [];
// foreground color
var prefix = typeToClassPrefixFg[textEl.fgColor.type];
textEl.classes.push(prefix + textEl.fgColor.name);
// background color
prefix = typeToClassPrefixBg[textEl.bgColor.type];
textEl.classes.push(prefix + textEl.bgColor.name);
// attributes
if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name);
}
var attr, val;
for (attr in textEl.attrs.override) {
val = textEl.attrs.override[attr];
if (val) {
textEl.classes.push('a-' + attr);
} else {
textEl.classes.push('a-no-' + attr);
}
}
});
return textElements;
};
this.parseRichText = parseRichText;
/*
* Buffer class
*/
@ -21,7 +63,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null);
// get channel identifier
var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : '';
var title = message.title;
var title = parseRichText(message.title);
var number = message.number;
var pointer = message.pointers[0];
var notify = 3; // Default 3 == message
@ -44,6 +86,11 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
notify = message.notify;
}
var rtitle = "";
for (var i = 0; i < title.length; ++i) {
rtitle += title[i].text;
}
/*
* Adds a line to this buffer
*
@ -234,6 +281,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
prefix: prefix,
number: number,
title: title,
rtitle: rtitle,
lines: lines,
clear: clear,
requestedLines: requestedLines,
@ -268,53 +316,11 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var date = message.date;
var shortTime = $filter('date')(date, 'HH:mm');
function addClasses(textElements) {
var typeToClassPrefixFg = {
'option': 'cof-',
'weechat': 'cwf-',
'ext': 'cef-'
};
var typeToClassPrefixBg = {
'option': 'cob-',
'weechat': 'cwb-',
'ext': 'ceb-'
};
textElements.forEach(function(textEl) {
textEl.classes = [];
// foreground color
var prefix = typeToClassPrefixFg[textEl.fgColor.type];
textEl.classes.push(prefix + textEl.fgColor.name);
// background color
prefix = typeToClassPrefixBg[textEl.bgColor.type];
textEl.classes.push(prefix + textEl.bgColor.name);
// attributes
if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name);
}
var val;
for (var attr in textEl.attrs.override) {
val = textEl.attrs.override[attr];
if (val) {
textEl.classes.push('a-' + attr);
} else {
textEl.classes.push('a-no-' + attr);
}
}
});
}
var prefix = weeChat.Protocol.rawText2Rich(message.prefix);
addClasses(prefix);
var prefix = parseRichText(message.prefix);
var tags_array = message.tags_array;
var displayed = message.displayed;
var highlight = message.highlight;
var content = weeChat.Protocol.rawText2Rich(message.message);
addClasses(content);
var content = parseRichText(message.message);
if (highlight) {
prefix.forEach(function(textEl) {

@ -1,7 +1,8 @@
var weechat = angular.module('weechat');
weechat.factory('notifications', ['$rootScope', '$log', 'models', function($rootScope, $log, models) {
weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) {
// Ask for permission to display desktop notifications
var notifications = [];
var requestNotificationPermission = function() {
// Firefox
if (window.Notification) {
@ -52,7 +53,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
var activeBuffer = models.getActiveBuffer();
if (activeBuffer) {
$rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title;
$rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.rtitle;
}
};
@ -109,6 +110,10 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
icon: 'assets/img/favicon.png'
});
// Save notification, so we can close all outstanding ones when disconnecting
notification.id = notifications.length;
notifications.push(notification);
// Cancel notification automatically
var timeout = 15*1000;
notification.onshow = function() {
@ -124,7 +129,12 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
notification.close();
};
if ($rootScope.soundnotification) {
// Remove from list of active notifications
notification.onclose = function() {
delete notifications[this.id];
};
if (settings.soundnotification) {
// TODO fill in a sound file
var audioFile = "assets/audio/sonar";
var soundHTML = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>';
@ -132,10 +142,20 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
}
};
var cancelAll = function() {
while (notifications.length > 0) {
var notification = notifications.pop();
if (notification !== undefined) {
notification.close();
}
}
};
return {
requestNotificationPermission: requestNotificationPermission,
updateTitle: updateTitle,
updateFavico: updateFavico,
createHighlight: createHighlight,
cancelAll: cancelAll,
};
}]);

@ -3,7 +3,7 @@
var weechat = angular.module('weechat');
weechat.directive('plugin', ['$rootScope', function($rootScope) {
weechat.directive('plugin', ['$rootScope', 'settings', function($rootScope, settings) {
/*
* Plugin directive
* Shows additional plugin content
@ -20,7 +20,7 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) {
$scope.displayedContent = "";
// Auto-display embedded content only if it isn't NSFW
$scope.plugin.visible = $rootScope.auto_display_embedded_content && !$scope.plugin.nsfw;
$scope.plugin.visible = !settings.noembed && !$scope.plugin.nsfw;
// user-accessible hash key that is a valid CSS class name
$scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_');
@ -47,8 +47,12 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) {
// TODO store the result between channel switches
if ($scope.plugin.content instanceof Function){
// Don't rerun if the result is already there
if (embed.innerHTML === "") {
if (!embed || embed.innerHTML === "") {
// if we're autoshowing, the element doesn't exist yet, and we need
// to do this async (wrapped in a setTimeout)
setTimeout(function() {
$scope.plugin.content();
});
}
} else {
$scope.displayedContent = $scope.plugin.content;

@ -10,15 +10,42 @@ var plugins = angular.module('plugins', []);
/*
* Definition of a user provided plugin with sensible default values
*
* User plugins are created by providing a contentForMessage function
* that parses a string and return any additional content.
* User plugins are created by providing a name and a contentForMessage
* function that parses a string and returns any additional content.
*/
var Plugin = function(contentForMessage) {
var Plugin = function(name, contentForMessage) {
return {
contentForMessage: contentForMessage,
exclusive: false,
name: "additional content"
name: name
};
};
// Regular expression that detects URLs for UrlPlugin
var urlRegexp = /(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g;
/*
* Definition of a user provided plugin that consumes URLs
*
* URL plugins are created by providing a name and a function that
* that parses a URL and returns any additional content.
*/
var UrlPlugin = function(name, urlCallback) {
return {
contentForMessage: function(message) {
var urls = message.match(urlRegexp);
var content = [];
for (var i = 0; urls && i < urls.length; i++) {
var result = urlCallback(urls[i]);
if (result) {
content.push(result);
}
}
return content;
},
exclusive: false,
name: name
};
};
@ -32,8 +59,6 @@ var Plugin = function(contentForMessage) {
*/
plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) {
var nsfwRegexp = new RegExp('nsfw', 'i');
/*
* Defines the plugin manager object
*/
@ -52,6 +77,8 @@ plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) {
}
};
var nsfwRegexp = new RegExp('nsfw', 'i');
/*
* Iterates through all the registered plugins
* and run their contentForMessage function.
@ -146,23 +173,6 @@ plugins.factory('userPlugins', function() {
document.body.appendChild(script);
};
var urlRegexp = new RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g);
var urlPlugin = function(callback) {
return function(message) {
var urls = message.match(urlRegexp);
var content = [];
for (var i = 0; urls && i < urls.length; i++) {
var result = callback(urls[i]);
if (result) {
content.push(result);
}
}
return content;
};
};
/*
* Spotify Embedded Player
*
@ -170,7 +180,7 @@ plugins.factory('userPlugins', function() {
*
*/
var spotifyPlugin = new Plugin(function(message) {
var spotifyPlugin = new Plugin('Spotify track', function(message) {
var content = [];
var addMatch = function(match) {
for (var i = 0; match && i < match.length; i++) {
@ -182,39 +192,29 @@ plugins.factory('userPlugins', function() {
addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g));
return content;
});
spotifyPlugin.name = 'Spotify track';
/*
* YouTube Embedded Player
*
* See: https://developers.google.com/youtube/player_parameters
*/
var youtubePlugin = new Plugin(function(message) {
var regExp = /(?:https?:\/\/)?(?:www\.)?(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/gm;
var match = regExp.exec(message);
var content = [];
var youtubePlugin = new UrlPlugin('YouTube video', function(url) {
var regex = /(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/i,
match = url.match(regex);
// iterate over all matches
while (match !== null){
var token = match[1];
var embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0";
content.push('<iframe width="560" height="315" src="'+ embedurl + '" frameborder="0" allowfullscreen frameborder="0"></iframe>');
// next match
match = regExp.exec(message);
if (match){
var token = match[1],
embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0";
return '<iframe width="560" height="315" src="'+ embedurl + '" frameborder="0" allowfullscreen frameborder="0"></iframe>';
}
return content;
});
youtubePlugin.name = 'YouTube video';
/*
* Dailymotion Embedded Player
*
* See: http://www.dailymotion.com/doc/api/player.html
*/
var dailymotionPlugin = new Plugin(function(message) {
var dailymotionPlugin = new Plugin('Dailymotion video', function(message) {
var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/;
var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/;
var rShorten = /dai.ly\/([^_?# ]+)/;
@ -228,13 +228,11 @@ plugins.factory('userPlugins', function() {
return null;
});
dailymotionPlugin.name = 'Dailymotion video';
/*
* AlloCine Embedded Player
*/
var allocinePlugin = new Plugin(function(message) {
var allocinePlugin = new Plugin('AlloCine video', function(message) {
var rVideokast = /allocine.fr\/videokast\/video-(\d+)/;
var rCmedia = /allocine.fr\/.*cmedia=(\d+)/;
@ -247,21 +245,12 @@ plugins.factory('userPlugins', function() {
return null;
});
allocinePlugin.name = 'AlloCine video';
/*
* Image Preview
*/
var imagePlugin = new Plugin(
urlPlugin(function(url) {
var embed = false;
// Check the get parameters as well, they might contain an image to load
var segments = url.split(/[?&]/).forEach(function(param) {
if (param.match(/\.(png|gif|jpg|jpeg)$/i)) {
embed = true;
}
});
if (embed) {
var imagePlugin = new UrlPlugin('image', function(url) {
if (url.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?\b/i)) {
/* A fukung.net URL may end by an image extension but is not a direct link. */
if (url.indexOf("^https?://fukung.net/v/") != -1) {
url = url.replace(/.*\//, "http://media.fukung.net/imgs/");
@ -270,20 +259,27 @@ plugins.factory('userPlugins', function() {
url = url.replace(/http:/, "");
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\/[^?]+$/i)) {
// Dropbox requires a get parameter, dl=1
// TODO strip an existing dl=0 parameter
url = url + "?dl=1";
}
return '<a target="_blank" href="'+url+'"><img class="embed" src="' + url + '"></a>';
}
})
);
imagePlugin.name = 'image';
});
/*
* mp4 video Preview
*/
var videoPlugin = new UrlPlugin('video', function(url) {
if (url.match(/\.(mp4|webm|ogv)\b/i)) {
return '<video class="embed" width="560"><source src="'+url+'"></source></video>';
}
});
/*
* Cloud Music Embedded Players
*/
var cloudmusicPlugin = new Plugin(
urlPlugin(function(url) {
var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) {
/* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */
if (url.match(/^https?:\/\/soundcloud.com\//)) {
return '<iframe width="100%" height="120" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=' + url + '&amp;color=ff6600&amp;auto_play=false&amp;show_artwork=true"></iframe>';
@ -293,38 +289,37 @@ plugins.factory('userPlugins', function() {
if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) {
return '<iframe width="480" height="60" src="//www.mixcloud.com/widget/iframe/?feed=' + url + '&mini=1&stylecolor=&hide_artwork=&embed_type=widget_standard&hide_tracklist=1&hide_cover=" frameborder="0"></iframe>';
}
})
);
cloudmusicPlugin.name = 'cloud music';
});
/*
* Google Maps
*/
var googlemapPlugin = new Plugin(
urlPlugin(function(url) {
var googlemapPlugin = new UrlPlugin('Google Map', function(url) {
if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) {
return '<iframe width="450" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="' + url + '&output=embed"></iframe>';
}
})
);
googlemapPlugin.name = 'Google Map';
});
/*
* Asciinema plugin
*/
var asciinemaPlugin = new Plugin(function(message) {
var regexp = /^https?:\/\/(www\.)?asciinema.org\/a\/(\d+)/;
var match = message.match(regexp);
var asciinemaPlugin = new UrlPlugin('ascii cast', function(url) {
var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i,
match = url.match(regexp);
if (match) {
var id = match[3];
return "<script type='text/javascript' src='https://asciinema.org/a/" + id + ".js' id='asciicast-" + id + "' async></script>";
var id = match[1];
return function() {
var element = this.getElement();
var scriptElem = document.createElement('script');
scriptElem.src = 'https://asciinema.org/a/' + id + '.js';
scriptElem.id = 'asciicast-' + id;
scriptElem.async = true;
element.appendChild(scriptElem);
};
}
});
asciinemaPlugin.name = "ascii cast";
var yrPlugin = new Plugin(
urlPlugin(function(url) {
var yrPlugin = new UrlPlugin('meteogram', function(url) {
var regexp = /^https?:\/\/(?:www\.)?yr\.no\/(place|stad|sted|sadji|paikka)\/(([^\s.;,(){}<>\/]+\/){3,})/;
var match = url.match(regexp);
if (match) {
@ -334,13 +329,10 @@ plugins.factory('userPlugins', function() {
url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png";
return "<img src='" + url + "' alt='Meteogram for " + city + "' />";
}
})
);
yrPlugin.name = "meteogram";
});
// Embed GitHub gists
var gistPlugin = new Plugin(
urlPlugin(function(url) {
var gistPlugin = new UrlPlugin('Gist', function(url) {
var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i;
var match = url.match(regexp);
if (match) {
@ -359,12 +351,9 @@ plugins.factory('userPlugins', function() {
});
};
}
})
);
gistPlugin.name = 'Gist';
});
var tweetPlugin = new Plugin(
urlPlugin(function(url) {
var tweetPlugin = new UrlPlugin('Tweet', function(url) {
var regexp = /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/i;
var match = url.match(regexp);
if (match) {
@ -372,7 +361,7 @@ plugins.factory('userPlugins', function() {
return function() {
var element = this.getElement();
jsonp(url, function(data) {
// sepearate the HTML into content and script tag
// separate the HTML into content and script tag
var scriptIndex = data.html.indexOf("<script ");
var content = data.html.substr(0, scriptIndex);
// Set DNT (Do Not Track)
@ -387,12 +376,22 @@ plugins.factory('userPlugins', function() {
});
};
}
})
);
tweetPlugin.name = 'Tweet';
});
/*
* Vine plugin
*/
var vinePlugin = new UrlPlugin('Vine', function (url) {
var regexp = /^https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i,
match = url.match(regexp);
if (match) {
var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1";
return '<iframe class="vine-embed" src="' + embedurl + '" width="600" height="600" frameborder="0"></iframe><script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
}
});
return {
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin]
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin, vinePlugin]
};

@ -0,0 +1,68 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) {
var that = this;
this.callbacks = {};
// Define a property for a setting, retrieving it on read
// and writing it to localStorage on write
var defineProperty = function(key) {
Object.defineProperty(that, key, {
enumerable: true,
key: key,
get: function() {
return $store.get(key);
},
set: function(newVal) {
$store.set(key, newVal);
// Call any callbacks
var callbacks = that.callbacks[key];
for (var i = 0; callbacks !== undefined && i < callbacks.length; i++) {
callbacks[i](newVal);
}
// Update the page (might be needed)
setTimeout(function() {
$rootScope.$apply();
}, 0);
}
});
};
// Define properties for all settings
var keys = $store.enumerateKeys();
for (var keyIdx in keys) {
var key = keys[keyIdx];
defineProperty(key);
}
// Add a callback to be called whenever the value is changed
// It's like a free $watch and used to be called the observer
// pattern, but I guess that's too old-school for JS kids :>
this.addCallback = function(key, callback, callNow) {
if (this.callbacks[key] === undefined) {
this.callbacks[key] = [callback];
} else {
this.callbacks[key].push(callback);
}
// call now to emulate $watch behaviour
setTimeout(function() {
callback($store.get(key));
}, 0);
};
this.setDefaults = function(defaults) {
for (var key in defaults) {
// null means the key isn't set
if ($store.get(key) === null) {
this[key] = defaults[key];
}
}
};
return this;
}]);
})();

@ -1,7 +1,7 @@
{
"name": "Glowing Bear",
"description": "WeeChat Web frontend",
"version": "0.4.4",
"version": "0.4.5",
"manifest_version": 2,
"icons": {
"32": "assets/img/favicon.png",

@ -15,6 +15,15 @@
"name": "The Glowing Bear Authors",
"url": "https://github.com/glowing-bear"
},
"permissions": {
"audio-channel-normal" : {
"description" : "Needed to play this app's audio content on the normal channel"
},
"audio-channel-content" : {
"description" : "Needed to play this app's audio content on the content channel"
},
"desktop-notification":{}
},
"default_locale": "en",
"version": "0.4.4"
"version": "0.4.5"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,7 +1,7 @@
{
"name": "glowing-bear",
"private": true,
"version": "0.4.4",
"version": "0.4.5",
"description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
@ -18,7 +18,7 @@
"scripts": {
"postinstall": "bower install",
"minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map",
"minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map",
"prestart": "npm install",
"start": "http-server -a localhost -p 8000",

@ -7,6 +7,8 @@ module.exports = function(config){
'bower_components/angular/angular.js',
'bower_components/angular-route/angular-route.js',
'bower_components/angular-mocks/angular-mocks.js',
'bower_components/angular-sanitize/angular-sanitize.js',
'bower_components/angular-touch/angular-touch.js',
'js/localstorage.js',
'js/weechat.js',
'js/irc-utils.js',

@ -0,0 +1,65 @@
var weechat = angular.module('weechat');
describe('Filters', function() {
beforeEach(module('weechat'));
/*beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));*/
it('has an irclinky filter', inject(function($filter) {
expect($filter('irclinky')).not.toBeNull();
}));
describe('irclinky', function() {
it('should not mess up text', inject(function(irclinkyFilter) {
expect(irclinkyFilter('foo')).toEqual('foo');
}));
it('should linkify IRC channels', inject(function(irclinkyFilter) {
expect(irclinkyFilter('#foo')).toEqual('<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'#foo\'); $scope.$apply();">#foo</a>');
}));
it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) {
expect(irclinkyFilter('<"#foo">')).toEqual('&lt;&quot;<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'#foo&quot;&gt;\'); $scope.$apply();">#foo&quot;&gt;</a>');
}));
});
describe('inlinecolour', function() {
it('should not mess up normal text', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('foo')).toEqual('foo');
expect(inlinecolourFilter('test #foobar baz')).toEqual('test #foobar baz');
}));
it('should detect inline colours in #rrggbb format', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('#123456')).toEqual('#123456 <div class="colourbox" style="background-color:#123456"></div>');
expect(inlinecolourFilter('#aabbcc')).toEqual('#aabbcc <div class="colourbox" style="background-color:#aabbcc"></div>');
}));
it('should not detect inline colours in #rgb format', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('#123')).toEqual('#123');
expect(inlinecolourFilter('#abc')).toEqual('#abc');
}));
it('should detect inline colours in rgb(12,34,56) and rgba(12,34,56,0.78) format', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('rgb(1,2,3)')).toEqual('rgb(1,2,3) <div class="colourbox" style="background-color:rgb(1,2,3)"></div>');
expect(inlinecolourFilter('rgb(1,2,3);')).toEqual('rgb(1,2,3); <div class="colourbox" style="background-color:rgb(1,2,3);"></div>');
expect(inlinecolourFilter('rgba(1,2,3,0.4)')).toEqual('rgba(1,2,3,0.4) <div class="colourbox" style="background-color:rgba(1,2,3,0.4)"></div>');
expect(inlinecolourFilter('rgba(255,123,0,0.5);')).toEqual('rgba(255,123,0,0.5); <div class="colourbox" style="background-color:rgba(255,123,0,0.5);"></div>');
}));
it('should tolerate whitespace in between numbers in rgb/rgba colours', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('rgb( 1\t, 2 , 3 )')).toEqual('rgb( 1\t, 2 , 3 ) <div class="colourbox" style="background-color:rgb( 1\t, 2 , 3 )"></div>');
}));
it('should handle multiple and mixed occurrences of colour values', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('rgb(1,2,3) #123456')).toEqual('rgb(1,2,3) <div class="colourbox" style="background-color:rgb(1,2,3)"></div> #123456 <div class="colourbox" style="background-color:#123456"></div>');
expect(inlinecolourFilter('#f00baa #123456 #234567')).toEqual('#f00baa <div class="colourbox" style="background-color:#f00baa"></div> #123456 <div class="colourbox" style="background-color:#123456"></div> #234567 <div class="colourbox" style="background-color:#234567"></div>');
expect(inlinecolourFilter('rgba(1,2,3,0.4) foorgb(50,100,150)')).toEqual('rgba(1,2,3,0.4) <div class="colourbox" style="background-color:rgba(1,2,3,0.4)"></div> foorgb(50,100,150) <div class="colourbox" style="background-color:rgb(50,100,150)"></div>');
}));
it('should not replace HTML escaped &#123456;', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('&#123456;')).toEqual('&#123456;');
}));
});
});

@ -47,7 +47,6 @@ describe('filter', function() {
'https://youtu.be/J6vIS8jb6Fs',
'http://www.youtube.com/embed/dQw4w9WgXcQ',
'https://www.youtube.com/embed/dQw4w9WgXcQ',
'youtu.be/dQw4w9WgXcQ'
],
'YouTube video',
plugins);
@ -72,6 +71,16 @@ describe('filter', function() {
plugins);
}));
it('should recognize html5 videos', inject(function(plugins) {
expectTheseMessagesToContain([
'http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4',
'http://www.quirksmode.org/html5/videos/big_buck_bunny.webm',
'http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv',
],
'video',
plugins);
}));
it('should recognize images', inject(function(plugins) {
expectTheseMessagesToContain([
'http://i.imgur.com/BTNIDBR.gif',
@ -80,7 +89,9 @@ describe('filter', function() {
'https://4z2.de/gb-mobile-new.png',
'http://static.weechat.org/images/screenshots/relay/medium/glowing-bear.png',
'http://foo.bar/baz.php?img=trololo.png&dummy=yes',
'https://tro.lo.lo/images/rick.png?size=123x45'
'https://tro.lo.lo/images/rick.png?size=123x45',
'https://pbs.twimg.com/media/B66rbCuIMAAxiFF.jpg:large',
'https://pbs.twimg.com/media/B6OZuCYCEAEV8SA.jpg:medium'
],
'image',
plugins);
@ -136,5 +147,14 @@ describe('filter', function() {
plugins);
}));
it('should recognize vines', inject(function(plugins) {
expectTheseMessagesToContain([
'https://vine.co/v/hWh262H9HM5',
'https://vine.co/v/hWh262H9HM5/embed',
],
'Vine',
plugins);
}));
});
});

Loading…
Cancel
Save