1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/glowingbear_ynh.git synced 2024-09-03 18:36:20 +02:00

Fixes package_check compability

for versions

package_check: commit 05434c0d161f053a3f8682ec75c363341fa782e7

yunohost: 2.5.3.1
yunohost-admin: 2.5.0
moulinette: 2.5.1
ssowat: 2.6.0

Note: there are still some open todo's that are waiting on ynh fixes
This commit is contained in:
jodeko 2017-02-18 19:30:24 +01:00
parent b799d63e67
commit 83fde11b35
62 changed files with 229 additions and 13053 deletions

40
check_process Normal file
View file

@ -0,0 +1,40 @@
;; Nom du test
auto_remove=1
# Commentaire ignoré
; Manifest
domain="domain.tld" (DOMAIN)
path="/path" (PATH)
admin="john" (USER)
language="fr"
# is_public="Yes" (PUBLIC|public=Yes|private=No)
is_public=1 (PUBLIC|public=1|private=0)
password="pass" (PASSWORD)
port="666" (PORT)
; Checks
pkg_linter=1
setup_sub_dir=1
setup_root=1
setup_nourl=0
setup_private=1
setup_public=1
upgrade=1
backup_restore=1
multi_instance=1
wrong_user=0
wrong_path=1
incorrect_path=1
corrupt_source=0
fail_download_source=0
port_already_use=1 (66)
final_path_already_use=0
;;; Levels
Level 1=auto
Level 2=auto
Level 3=auto
Level 4=0
Level 5=auto
Level 6=auto
Level 7=auto
Level 8=0
Level 9=0
Level 10=0

2
conf/app.src Normal file
View file

@ -0,0 +1,2 @@
SOURCE_URL=https://github.com/glowing-bear/glowing-bear/archive/v0.6.0.tar.gz
SOURCE_SUM=3d536bfa479f2250506ed151945689f60f894a0f3b9e93bab18fcd25401512e7

63
scripts/.hfunctions Normal file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -eu
update_nginx_configuration() {
local app=$1
local nginx_config_template=$2
local domain=$3
local path=$4
local deploy_path=$5
sed --in-place "s@YNH_WWW_PATH@${path}@g" ${nginx_config_template}
sed --in-place "s@YNH_WWW_ALIAS@${deploy_path}/@g" ${nginx_config_template}
sudo cp ${nginx_config_template} /etc/nginx/conf.d/${domain}.d/${app}.conf
sudo service nginx reload
}
download_file() {
local url=$1
local output_document=$2
wget --no-verbose --output-document=${output_document} ${url}
}
check_file_integrity() {
local file=$1
local expected_checksum=$2
echo "${expected_checksum} ${file}" | sha256sum --check --status \
|| ynh_die "Corrupt source!"
}
extract_archive() {
local src_file=$1
local deploy_path=$2
sudo mkdir --parents ${deploy_path}
sudo tar --extract --file=${src_file} --directory=${deploy_path} --overwrite --strip-components 1
sudo chown --recursive root: $deploy_path
}
obtain_and_deploy_source() {
local app_config=$1
local deploy_path=$2
local src_url=$(app_config_get $app_config "SOURCE_URL")
local src_checksum=$(app_config_get $app_config "SOURCE_SUM")
local src_file="/tmp/source.tar.gz"
download_file $src_url $src_file
check_file_integrity $src_file $src_checksum
extract_archive $src_file $deploy_path
}
update_accessibility() {
local app=$1
local is_public=$2
if [[ ${is_public:-0} -eq 1 ]]; then
ynh_app_setting_set $app unprotected_uris "/"
sudo yunohost app ssowatconf
fi
}

View file

@ -1,18 +1,21 @@
#!/bin/bash #!/usr/bin/env bash
# Exit on command errors and treat unset variables as an error
set -eu set -eu
# See comments in install script
app=$YNH_APP_INSTANCE_NAME
# Source YunoHost helpers
source /usr/share/yunohost/helpers source /usr/share/yunohost/helpers
# Backup sources & data main() {
# Note: the last argument is where to save this path, see the restore script. local app=$YNH_APP_INSTANCE_NAME
ynh_backup "/var/www/${app}" "sources" local domain=$(ynh_app_setting_get $app domain)
local path=$(ynh_app_setting_get $app path)
local deploy_path=$(ynh_app_setting_get $app deploy_path)
local backup_dir=$YNH_APP_BACKUP_DIR
local nginx_conf=/etc/nginx/conf.d/$domain.d/$app.conf
# Copy NGINX configuration ynh_backup $deploy_path "sources"
domain=$(ynh_app_setting_get "$app" domain)
ynh_backup "/etc/nginx/conf.d/${domain}.d/${app}.conf" "nginx.conf" sudo mkdir --parent ./conf
ynh_backup $nginx_conf "conf/nginx.conf"
}
main

View file

@ -1,58 +1,62 @@
#!/bin/bash #!/usr/bin/env bash
# Exit on command errors and treat unset variables as an error # Exit on command errors
# set -o errexit
# Treat unset variables as an error
# set -o nounset
set -eu set -eu
# This is a multi-instance app, meaning it can be installed several times independently
# The id of the app as stated in the manifest is available as $YNH_APP_ID
# The instance number is available as $YNH_APP_INSTANCE_NUMBER (equals "1", "2", ...)
# The app instance name is available as $YNH_APP_INSTANCE_NAME
# - the first time the app is installed, YNH_APP_INSTANCE_NAME = ynhexample
# - the second time the app is installed, YNH_APP_INSTANCE_NAME = ynhexample__2
# - ynhexample__{N} for the subsequent installations, with N=3,4, ...
# The app instance name is probably what you are interested the most, since this is
# guaranteed to be unique. This is a good unique identifier to define installation path,
# db names, ...
app=$YNH_APP_INSTANCE_NAME
# Retrieve arguments
domain=$YNH_APP_ARG_DOMAIN
path=$YNH_APP_ARG_PATH
is_public=$YNH_APP_ARG_IS_PUBLIC
# Source YunoHost helpers
source /usr/share/yunohost/helpers source /usr/share/yunohost/helpers
source .hfunctions
# Save app settings app_config_get() {
ynh_app_setting_set "$app" is_public "$is_public" local app_config=$1
local attribute=$2
cat ${app_config} \
| grep ${attribute} \
| cut --delimiter== --fields=2
}
# Check domain/path availability
sudo yunohost app checkurl "${domain}${path}" -a "$app" \
|| ynh_die "Path not available: ${domain}${path}"
# Copy source files #
src_path=/var/www/$app # With reference to https://github.com/YunoHost/yunohost/pull/234/files
sudo mkdir -p $src_path # TODO: remove me ynh_normalize_path is in stable
sudo cp -a ../sources/. $src_path #
ynh_normalize_path() {
local path=$1
# Set permissions to app files if [ "${path:0:1}" != "/" ]; then
# you may need to make some file and/or directory writeable by www-data (nginx user) path="/$path"
sudo chown -R root: $src_path fi
if [ "${path:${#path}-1}" == "/" ] && [ ${#path} -gt 1 ]; then
path="${path:0:${#path}-1}"
fi
echo $path
}
# Modify Nginx configuration file and copy it to Nginx conf directory main() {
nginx_conf=../conf/nginx.conf local app=$YNH_APP_INSTANCE_NAME
sed -i "s@YNH_WWW_PATH@$path@g" $nginx_conf local is_public=$YNH_APP_ARG_IS_PUBLIC
sed -i "s@YNH_WWW_ALIAS@$src_path/@g" $nginx_conf local domain=$YNH_APP_ARG_DOMAIN
# If a dedicated php-fpm process is used: local path=$(ynh_normalize_path $YNH_APP_ARG_PATH)
# Don't forget to modify ../conf/nginx.conf accordingly or your app will not work! local deploy_path=/var/www/$app
# sed -i "s@YNH_WWW_APP@$app@g" $nginx_conf local app_config=../conf/app.src #should we make this a convention?
sudo cp $nginx_conf /etc/nginx/conf.d/$domain.d/$app.conf local nginx_config_template=../conf/nginx.conf
local url=$domain$path
# If app is public, add url to SSOWat conf as skipped_uris ynh_app_setting_set $app is_public $is_public
if [[ $is_public -eq 1 ]]; then ynh_app_setting_set $app domain $domain
# unprotected_uris allows SSO credentials to be passed anyway. ynh_app_setting_set $app path $path
ynh_app_setting_set "$app" unprotected_uris "/" ynh_app_setting_set $app url $url
fi ynh_app_setting_set $app deploy_path $deploy_path
sudo yunohost app checkurl $url -a $app
obtain_and_deploy_source $app_config $deploy_path
update_nginx_configuration $app $nginx_config_template $domain $path $deploy_path
update_accessibility $app $is_public
}
main
# Reload services
sudo service nginx reload

View file

@ -1,34 +1,18 @@
#!/bin/bash #!/usr/bin/env bash
set -u
# See comments in install script
app=$YNH_APP_INSTANCE_NAME
# Source YunoHost helpers
source /usr/share/yunohost/helpers source /usr/share/yunohost/helpers
# Retrieve app settings main() {
domain=$(ynh_app_setting_get "$app" domain) local app=${YNH_APP_INSTANCE_NAME}
local domain=$(ynh_app_setting_get $app domain)
local deploy_path=$(ynh_app_setting_get $app deploy_path)
local nginx_conf="/etc/nginx/conf.d/${domain}.d/${app}.conf"
# Remove sources sudo rm -rf $deploy_path
sudo rm -rf /var/www/$app sudo rm -f $nginx_conf
# Remove nginx configuration file sudo service nginx reload
sudo rm -f /etc/nginx/conf.d/$domain.d/$app.conf }
### PHP (remove if not used) ### main
# If a dedicated php-fpm process is used:
# sudo rm -f /etc/php5/fpm/pool.d/$app.conf
# sudo service php5-fpm reload
### PHP end ###
### MySQL (remove if not used) ###
# If a MySQL database is used:
# # Drop MySQL database and user
# dbname=$app
# dbuser=$app
# ynh_mysql_drop_db "$dbname" || true
# ynh_mysql_drop_user "$dbuser" || true
### MySQL end ###
# Reload nginx service
sudo service nginx reload

View file

@ -1,35 +1,37 @@
#!/bin/bash #!/usr/bin/env bash
# Note: each files and directories you've saved using the ynh_backup helper
# will be located in the current directory, regarding the last argument.
# Exit on command errors and treat unset variables as an error
set -eu set -eu
# See comments in install script
app=$YNH_APP_INSTANCE_NAME
# Source YunoHost helpers
source /usr/share/yunohost/helpers source /usr/share/yunohost/helpers
# Retrieve old app settings # TODO: enable with fix of https://github.com/YunoHost/yunohost/pull/246
domain=$(ynh_app_setting_get "$app" domain) # source .hfunctions
path=$(ynh_app_setting_get "$app" path)
# Check domain/path availability main() {
sudo yunohost app checkurl "${domain}${path}" -a "$app" \ local app=$YNH_APP_INSTANCE_NAME
|| ynh_die "Path not available: ${domain}${path}" local domain=$(ynh_app_setting_get $app domain)
local path=$(ynh_app_setting_get $app path)
local url=$(ynh_app_setting_get $app url)
local deploy_path=$(ynh_app_setting_get $app deploy_path)
local nginx_conf=/etc/nginx/conf.d/$domain.d/$app.conf
# Restore sources & data sudo yunohost app checkurl $url -a $app
src_path="/var/www/${app}"
sudo cp -a ./sources "$src_path"
# Restore permissions to app files sudo cp -a "./sources" $deploy_path
# you may need to make some file and/or directory writeable by www-data (nginx user) sudo chown www-data:www-data $deploy_path -R
sudo chown -R root: "$src_path"
# Restore NGINX configuration sudo cp -a "./conf/nginx.conf" $nginx_conf
sudo cp -a ./nginx.conf "/etc/nginx/conf.d/${domain}.d/${app}.conf"
# Restart webserver sudo service nginx reload
sudo service nginx reload
# TODO: enable with fix of https://github.com/YunoHost/yunohost/pull/246
# update_accessibility $app $is_public
# TODO: remove with fix of https://github.com/YunoHost/yunohost/pull/246
if [[ ${is_public:-0} -eq 1 ]]; then
ynh_app_setting_set $app unprotected_uris "/"
sudo yunohost app ssowatconf
fi
}
main

View file

@ -1,45 +1,26 @@
#!/bin/bash #!/usr/bin/env bash
# Exit on command errors and treat unset variables as an error # Exit on command errors
#set -o errexit
# Treat unset variables as an error
#set -o nounset
set -eu set -eu
# See comments in install script
app=$YNH_APP_INSTANCE_NAME
# Source YunoHost helpers
source /usr/share/yunohost/helpers source /usr/share/yunohost/helpers
source .hfunctions
# Retrieve app settings main() {
domain=$(ynh_app_setting_get "$app" domain) local app=${YNH_APP_INSTANCE_NAME}
path=$(ynh_app_setting_get "$app" path) local domain=$(ynh_app_setting_get ${app} domain)
is_public=$(ynh_app_setting_get "$app" is_public) local path=$(ynh_app_setting_get ${app} path)
local is_public=$(ynh_app_setting_get $app is_public)
local deploy_path=$(ynh_app_setting_get $app deploy_path)
local app_config=../conf/app.src
local nginx_config_template=../conf/nginx.conf
# Remove trailing "/" for next commands path=${path%/}
path=${path%/}
# Copy source files obtain_and_deploy_source $app_config $deploy_path
src_path=/var/www/$app update_nginx_configuration $app $nginx_config_template $domain $path $deploy_path
sudo mkdir -p $src_path update_accessibility $app $is_public
sudo cp -a ../sources/. $src_path }
# Set permissions to app files
# you may need to make some file and/or directory writeable by www-data (nginx user)
sudo chown -R root: $src_path
# Modify Nginx configuration file and copy it to Nginx conf directory
nginx_conf=../conf/nginx.conf
sed -i "s@YNH_WWW_PATH@$path@g" $nginx_conf
sed -i "s@YNH_WWW_ALIAS@$src_path/@g" $nginx_conf
# If a dedicated php-fpm process is used:
#
# sed -i "s@YNH_WWW_APP@$app@g" $nginx_conf
sudo cp $nginx_conf /etc/nginx/conf.d/$domain.d/$app.conf
# If app is public, add url to SSOWat conf as skipped_uris
if [[ $is_public -eq 1 ]]; then
# See install script
ynh_app_setting_set "$app" unprotected_uris "/"
fi
# Reload nginx service
sudo service nginx reload

File diff suppressed because one or more lines are too long

View file

@ -1,15 +0,0 @@
/** @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

View file

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -1,92 +0,0 @@
#A web client for WeeChat [![Build Status](https://api.travis-ci.org/glowing-bear/glowing-bear.png)](https://travis-ci.org/glowing-bear/glowing-bear?branch=master)
Glowing Bear is a web frontend for the [WeeChat](http://weechat.org) IRC client and strives to be a modern interface. It relies on WeeChat to do all the heavy lifting and then provides some nice features on top of that, like embedding images, videos, and other content. The best part, however, is that you can use it from any modern internet device -- whether it's a computer, tablet, or smart phone -- and all your stuff is there, whereever you are. You don't have to deal with the messy technical details, and all you need to have installed is a browser or our app.
##Getting Started
Glowing Bear connects to the WeeChat instance you're already running (version 0.4.2 or later is required), and you need to be able to establish a connection to the WeeChat host from your device. It makes use of the relay plugin, and therefore you need to set up a relay. If you want to get started as quickly as possible, use these commands in WeeChat:
/relay add weechat 9001
/set relay.network.password YOURPASSWORD
Now point your browser to the [Glowing Bear](http://www.glowing-bear.org)! If you're having trouble connecting, check that the host and port of your WeeChat host are entered correctly, and that your server's firewall permits incoming connections on the relay port.
Please note that the above instructions set up an *unencrypted* relay, and all your data will be transmitted in clear. Therefore, we strongly recommend that you set up encryption if you want to keep using Glowing Bear. We've written [a detailed guide on how to set up a trusted secure relay](https://4z2.de/2014/07/06/weechat-trusted-relay) for you.
You can run Glowing Bear in many ways: use it like any other webpage, as an app in Firefox (choose "Install app" on the landing page) or Chrome ("Tools", then "Create application shortcuts"), or a full-screen Chrome app on Android ("Add to homescreen"). We also provide an [Android app](https://play.google.com/store/apps/details?id=com.glowing_bear) that you can install from the Google Play Store, and a [Firefox OS app](https://marketplace.firefox.com/app/glowing-bear/) in the Firefox Marketplace.
<a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="/assets/img/badge_playstore.png" /></a><a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="/assets/img/badge_firefoxos.png" /></a>
##Screenshots
Running as Chrome application in a separate window on Windows and as Android app:
![Glowing bear screenshot](https://4z2.de/glowingbear.png)
Are you good with design? We'd love your help!
![Glowing Bear screenshot with lots of Comic Sans MS](https://4z2.de/glowing-bear3.png)
##How it Works
What follows is a more technical explanation of how Glowing Bear works, and you don't need to understand it to use it.
Glowing Bear uses WeeChat directly as its backend through the relay plugin. This means that we can connect to WeeChat directly from the browser using WebSockets. Therefore, the client does not need a special "backend service", and you don't have to install anything. A connection is made from your browser to your WeeChat, with no services in between. Thus, Glowing Bear is written purely in client-side JavaScript with a bit of HTML and CSS.
##FAQ
- *Can I use Glowing Bear to access a machine or port not exposed to the internet by passing the connection through my server?* No, that's not what Glowing Bear does. You can use a websocket proxy module for your webserver to forward `/weechat` to your WeeChat instance though. Here are some pointers you might find helpful for setting this up with [nginx](http://nginx.com/blog/websocket-nginx/) or [apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html).
- *How does the encryption work?* TLS is used for securing the connection if you enable encryption. This is handled by your browser, and we have no influence on certificate handling, etc. You can find more detailed instructions on how to communicate securely in the "encryption instructions" tab on the [landing page](http://www.glowing-bear.org). A detailed guide on setting up a trusted secure relay is available [here](https://4z2.de/2014/07/06/weechat-trusted-relay).
##Development
###Setup
Getting started with the development of Glowing Bear is really simple, partly because we don't have a build process (pure client-side JS, remember). All you have to do is clone the repository, fire up a webserver to host the files, and start fiddling around. You can try out your changes by reloading the page.
Here's a simple example using the python simple web server:
```bash
git clone https://github.com/glowing-bear/glowing-bear
cd glowing-bear
# python 2.*
python -m SimpleHTTPServer
# or python 3.*
python -m http.server
```
Now you can point your browser to [http://localhost:8000](http://localhost:8000)!
Remember that **you don't need to host Glowing Bear yourself to use it**, you can just use [our hosted version](http://www.glowing-bear.org) powered by GitHub pages, and we'll take care of updates for you. Your browser connects to WeeChat directly, so it does not matter where Glowing Bear is hosted.
If you'd prefer a version hosted with HTTPS, GitHub serves that as well with an undocumented, not officially supported (by GitHub) link. Be careful though, it might break any minute. Anyway, here's the link: [secret GitHub HTTPS link](https://glowing-bear.github.io/glowing-bear/).
You can also use the latest and greatest development version of Glowing Bear at [https://latest.glowing-bear.org/](https://latest.glowing-bear.org/).
###Running the tests
Glowing Bear uses Karma and Jasmine to run its unit tests. To run the tests locally, you will first need to install `npm` on your machine. Check out the wonderful [nvm](https://github.com/creationix/nvm) if you don't know it already, it's highly recommended.
Once this is done, you will need to retrieve the necessary packages for testing Glowing-Bear (first, you might want to use `npm link` on any packages you have already installed globally):
`$ npm install`
Finally, you can run the unit tests:
`$ npm test`
Or the end to end tests:
`$ npm run protractor`
**Note**: the end to end tests assume that a web server is hosting Glowing Bear on `localhost:8000` and that a WeeChat relay is configured on port 9001.
##Contributing
Whether you are interested in contributing or simply want to talk about the project, join us at **#glowing-bear** on **freenode**!
We appreciate all forms of contributions -- whether you're a coder, designer, or user, we are always curious what you have to say. Whether you have suggestions or already implemented a solution, let us know and we'll try to help. We're also very keen to hear which devices and platforms Glowing Bear works on (or doesn't), as we're a small team and don't have access to the resources we would need to test it everywhere.
If you wish to submit code, we try to make the contribution process as simple as possible. Any pull request that is submitted has to go through automatic and manual testing. Please make sure that your changes pass the [Travis](https://travis-ci.org/glowing-bear/glowing-bear) tests before submitting a pull request. Here is how you can run the tests:
`$ ./run_tests.sh`
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/), Emoji provided free by [Emoji One](http://emojione.com/), 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -1,17 +0,0 @@
{
"name": "glowing-bear",
"description": "A webclient for WeeChat",
"version": "0.6.0",
"homepage": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
"private": true,
"dependencies": {
"angular": "1.4.x",
"angular-route": "1.4.x",
"angular-sanitize": "1.4.x",
"angular-touch": "1.4.x",
"angular-loader": "1.4.x",
"angular-mocks": "1.4.x",
"html5-boilerplate": "~4.3.0"
}
}

View file

@ -1,852 +0,0 @@
html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}
.no-overflow {
overflow: hidden;
}
.mobile {
display: none;
}
a {
cursor: pointer;
}
.version {
margin-right: 1em;
}
.hidden-bracket {
position: absolute;
left: -1000px;
overflow: hidden;
}
td.prefix {
text-align: right;
vertical-align: top;
padding: 1px 5px 1px 1px;
white-space: pre;
border-right: 1px solid #444;
}
td.message {
overflow: hidden;
vertical-align: top;
width: 100%;
padding: 1px 1px 1px 5px;
-ms-word-break: break-all;
word-break: break-all;
/* Non standard for webkit */
word-break: break-word;
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
}
#readmarker {
margin-top: 5px;
margin-bottom: 5px;
border-top: 1px solid;
border-bottom: 1px solid;
height: 2px;
}
.text {
white-space: pre-wrap;
}
#sendMessage {
width: 100%;
height: 35px;
resize: none;
}
#sendMessage:focus, #sendMessage:active {
border-bottom: 2px solid #555;
}
.input-group-addon, .input-group-btn {
vertical-align: top;
}
.footer button {
border-radius: 0;
}
.panel input, .panel .input-group {
max-width: 300px;
}
input[type=text], input[type=password], #sendMessage {
border: 0;
border-radius: 0;
margin-bottom: 5px !important;
}
.btn-send-image {
position: relative;
overflow: hidden;
cursor: pointer;
}
.imgur-upload {
position: absolute;
bottom: 0;
right: 0;
cursor: inherit;
font-size: 1000px !important;
height: 300px;
margin: 0;
padding: 0;
opacity: 0;
filter: ~"alpha(opacity=0)";
}
.input-group {
width: 100%;
}
.row {
margin: 0px;
max-width: 300px;
}
.no-gutter [class*="col"] {
padding: 0px
}
.col-sm-9 {
padding-right: 5px !important;
}
.glyphicon {
top: 0; /* Fixes alignment issue in top bar */
}
#topbar {
position: fixed;
width: 100%;
height: 35px;
max-height: 35px;
z-index: 3;
line-height: 35px;
white-space: nowrap;
}
#topbar .brand {
float: left;
height: 35px;
}
#topbar .brand a {
display: inline-block;
padding: 0 10px;
}
#topbar .brand img {
height: 32px;
padding: 2px;
}
#topbar .brand button {
position: absolute;
line-height: 15px;
font-size: 9pt;
margin-left: 10px
}
#topbar .title {
position: fixed;
left: 145px; /* sidebar */
overflow: hidden;
}
#topbar .actions {
margin-left: 5px;
padding-left: 5px;
margin-right: 0;
padding-right: 5px;
height: 35px;
line-height: 35px;
font-size: 22px;
position: fixed;
right: 0;
}
#topbar .actions > * {
padding: 0 5px;
display: inline-block;
}
#topbar .actions .glyphicon {
line-height: 35px;
top: 0;
}
#topbar .dropdown-menu form {
padding-left: 6px;
padding-right: 6px;
}
.upload-error {
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 4;
}
#sidebar {
position: fixed;
width: 140px;
min-height: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding-top: 35px; /* topbar */
padding-bottom: 1px; /* need to force a padding here */
font-size: smaller;
transition:0.2s ease-in-out;
z-index: 2;
}
#sidebar[data-state=visible] {
left: 0px;
}
#sidebar form {
}
#sidebar.ng-hide-add, #sidebar.ng-hide-remove {
/* this needs to be here to make it visible during the animation
since the .ng-hide class is already on the element rendering
it as hidden. */
display:block!important;
}
#sidebar .badge {
border-radius: 0;
margin-right: -10px;
padding: 4px 7px;
}
#sidebar ul.indented li.indent span.buffername {
padding-left: 10px;
}
#sidebar.ng-hide {
width: 0;
}
#nicklist {
position: fixed;
width: 100px;
min-height: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
right: 0;
top: 0;
padding-top: 39px;
padding-left: 5px;
padding-bottom: 35px;
z-index: 2;
}
#nicklist ul {
padding: 0;
margin: 0;
}
#nicklist li,
#nicklist a {
display: block;
}
#nicklist a {
text-decoration: none;
}
#connection-infos {
float: left;
max-width: 10%;
padding-left: 5px;
font-size: 12px;
overflow: hidden;
}
.nav-pills li {
min-height: 20px;
}
.nav-pills li+li {
margin-top: 0;
}
.nav-pills > li > a {
border-radius: 0;
color: #ddd;
padding: 5px 10px;
}
.nav-pills > li > a:hover, .nav-pills > li > a:hover span {
color: #222;
}
/* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span {
text-decoration: none;
background-color: #eee;
color: #222;
}
.nav-pills > li > a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
height: 100%;
min-height: 100%;
}
#bufferlines {
position: relative;
height: 100%;
overflow-y: auto;
width: auto;
bottom: 35px; /* input bar */
padding-top: 42px; /* topbar */
padding-bottom: 7px;
-webkit-transition:0.2 ease-in-out all;
transition:0.2s ease-in-out all;
-webkit-overflow-scrolling: touch; /* Native scroll on ios */
}
#bufferlines > table {
margin-top: 35px;
width: 100%;
}
tr.bufferline {
line-height: 100%;
}
td.time {
padding: 1px 5px 1px 1px;
vertical-align: top;
}
.withnicklist {
margin-right: 100px !important; /* nicklist */
}
.content[sidebar-state=visible] #bufferlines {
margin-left: 145px; /* sidebar */
}
#bufferlines .btn {
font-family: sans-serif;
}
#reconnect {
top: 35px;
position: fixed;
z-index: 9999;
width: 80%;
margin: 0;
padding: 5px;
left: 10%;
}
#reconnect a {
color: white;
}
.footer {
position: fixed;
bottom: 0;
height: 35px;
width: 100%;
-webkit-transition:0.2s ease-in-out all;
transition:0.2s ease-in-out all;
z-index: 1;
}
.content[sidebar-state=visible] .footer {
margin-left: 0;
padding-left: 145px;
}
.footer.withnicklist {
padding-right: 100px;
}
#inputform {
position: relative;
}
#imgur-upload-progress {
width: 100%;
height: auto;
position: absolute;
bottom: 100%;
left: 0;
}
.imgur-progress-bar {
width: 0%;
height: 5px;
margin-top: 1px;
background: #428BCA;
}
/* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span {
text-decoration: none;
}
[ng-click],
[data-ng-click],
[x-ng-click] {
cursor: pointer;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb:vertical {
height: 15px;
}
div.embed * {
max-width: 100%;
}
/* not for all img embeds so as not to affect the yr plugin (302px) */
div.embed img.embed {
max-height: 300px;
max-width: 100%;
}
video.embed {
max-width: 100%;
}
div.colourbox {
display: inline-block;
border-radius: 3px;
border: 1px solid #bbb;
width: 14px;
height: 14px;
margin-bottom: -2px;
}
table.notimestamp td.time {
display: none !important;
}
table.notimestampseconds td.time span.seconds {
display: none !important;
}
#sidebar .showquickkeys .buffer .buffer-quick-key {
transition: all ease-in-out 0.5s;
-webkit-transition: all ease-in-out 0.5s;
transition-delay: 0.2s;
-webkit-transition-delay: 0.2s;
opacity: 0.7;
}
#sidebar .buffer .buffer-quick-key {
margin-left: -0.7em;
margin-right: -0.2em;
font-size: smaller;
transition: all ease-in-out 0.5s;
-webkit-transition: all ease-in-out 0.5s;
opacity: 0;
text-shadow: -1px 0px 4px rgba(255, 255, 255, 0.4),
0px -1px 4px rgba(255, 255, 255, 0.4),
1px 0px 4px rgba(255, 255, 255, 0.4),
0px 1px 4px rgba(255, 255, 255, 0.4);
vertical-align: baseline;
display: inline-block;
width: 1em;
align: right;
}
.gb-modal {
z-index: 1000;
height: 100%;
overflow-y: scroll;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.gb-modal[data-state=hidden] {
transition: .2s ease-in-out;
visibility: hidden;
opacity: 0;
}
.gb-modal[data-state=visible] {
transition: .2s ease-in-out;
visibility: visible;
opacity: 1;
}
.gb-modal[data-state=hidden] .modal-dialog {
transition: top .3s ease-in;
top: -150px;
}
.gb-modal[data-state=visible] .modal-dialog {
transition: top .3s ease-out;
top: 0px;
}
.gb-modal .backdrop {
z-index: 999;
position: fixed;
top: 0;
height: 100%;
width: 100%;
overflow: none;
}
.gb-modal .modal-dialog {
z-index: 1001;
position: absolute;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 35px;
}
.gb-modal[ng-click], .gb-modal div[ng-click] {
cursor: default;
}
.gb-modal ul {
list-style: none;
padding-left: 15px;
}
.gb-modal li {
font-size: larger;
margin-bottom: 10px;
}
.gb-modal li li {
font-size: medium;
}
.modal-header {
padding-top: 23px;
border-bottom: 0;
}
.standard-labels label {
font-weight: normal;
text-align: left;
}
h2 {
padding-bottom: 5px;
height: 72px;
}
h2 img {
padding-right: 5px;
float: left;
height: 72px;
}
h2 span, h2 small {
padding: 5px 0 0 0;
display: block;
}
.panel[data-state=active] .panel-collapse {
transition: max-height 0.5s;
max-height: 60em;
height: auto;
display: block;
}
.panel[data-state=collapsed] .panel-collapse {
transition: max-height 0.5s;
max-height: 0;
}
.panel[data-state=collapsed] {
border: 0px solid transparent;
}
.panel .panel-title:before {
display: inline-block;
font-size: 22px;
line-height: 20px;
margin: -3px 5px -3px 0;
}
.panel[data-state=active] .panel-title:before {
content: "";
}
.panel[data-state=collapsed] .panel-title:before {
content: "+";
}
/* fix for firefox being stupid */
@-moz-document url-prefix() {
.panel[data-state=collapsed] .panel-collapse * {
display: none;
}
}
/* bold hash before channels */
li.buffer.channel a span:last-of-type:before, #topbar .title .channel:before {
color: #888;
font-weight: bold;
}
li.buffer.channel_hash a span:last-of-type:before, #topbar .title .channel_hash:before {
content: '#';
}
li.buffer.channel_plus a span:last-of-type:before, #topbar .title .channel_plus:before {
content: '+';
}
li.buffer.channel_ampersand a span:last-of-type:before, #topbar .title .channel_ampersand:before {
content: '&';
}
li.buffer.channel.active a span:last-of-type:before {
color: #aaa;
}
li.buffer.indent.private a {
padding-left: 17px;
}
.make-thinner {
padding-right: -15px;
}
.settings-help {
display: block;
margin: -5px 0 -3px 19px;
font-size: small;
}
.unselectable {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.emojione {
font-size: inherit;
height: 1em;
width: 1.1em;
min-height: 16px;
min-width: 16px;
display: inline-block;
margin: -.2ex .15em .2ex;
line-height: normal;
vertical-align: middle;
}
img.emojione {
width: auto;
}
.glyphicon-spin {
-webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@media (min-width: 1400px) {
#sidebar[data-state=visible], #sidebar {
width: 200px;
}
.content[sidebar-state="visible"] #bufferlines {
margin-left: 205px;
}
#topbar .title {
left: 205px;
}
.content[sidebar-state=visible] .footer {
padding-left: 200px;
}
.nav-pills {
font-size: 14px;
}
.nav-pills li a {
padding: 10px 15px;
}
#nicklist {
width: 140px;
}
.withnicklist {
margin-right: 140px !important; /* nicklist */
}
.footer.withnicklist {
padding-right: 148px !important;
}
}
/* */
/* Mobile layout */
/* */
@media (max-width: 968px) {
.mobile {
display: inherit;
}
.desktop {
display: none;
}
#bufferlines table {
border-collapse: separate;
border-spacing: 2px 3px;
}
#sidebar {
font-size: normal;
bottom: 0px;
top: 0px;
padding-bottom: 35px;
width: 200px;
}
#sidebar.in, #sidebar.collapsing {
-webkit-box-shadow: 0px 0px 120px #000;
box-shadow: 0px 0px 120px #000;
bottom: 0px;
}
#sidebar[data-state=visible] {
transform: translate(0,0);
-webkit-transform: translate(0,0); /* Safari */
}
#sidebar[data-state=hidden] {
transform: translate(-200px,0);
-webkit-transform: translate(-200px,0);
}
.content[sidebar-state=visible] #bufferlines, .content[sidebar-state=visible] .footer {
margin-left: 0px;
transform: translate(200px,0);
-webkit-transform: translate(200px,0);
}
#topbar .title {
left: 40px;
right: 60px;
text-align: center;
font-size: 18px;
}
#topbar .brand img {
height: 28px;
}
#topbar .badge {
display: none;
}
#bufferlines, #nicklist {
position: relative;
min-height: 0;
margin-left: 0;
max-width: 100%;
border: 0;
}
#nicklist {
height: auto;
padding: 35px 7px 35px 10px;
text-align: center;
position: fixed;
margin-top: 10px;
bottom: 0px;
}
.navbar-fixed-bottom {
margin: 0;
}
.navbar {
min-height: 0%;
}
.nav-pills {
font-size: 14px;
}
.nav-pills li a {
padding: 10px 15px;
}
#bufferlines {
height: 100%;
padding-bottom: 0;
}
#bufferlines tr.bufferline {
display: block;
overflow: hidden;
}
#bufferlines td.time {
display: inline-block;
padding-right: 3px;
font-size: 0.8em;
}
#bufferlines td.time span.date {
display: block;
margin-bottom: -1px;
}
#bufferlines td.prefix {
display: inline;
padding-right: 5px;
border: 0;
font-weight: bold;
font-size: 1.06em;
}
#bufferlines td.message {
display: inline;
padding: 0px !important;
}
.gb-modal .modal-dialog {
margin: 20px 2%;
width: 96%;
}
.footer {
padding-left: 0px !important;
padding-right: 0px !important;
width: 100% !important;
}
.footer.withnicklist {
padding-right: 108px !important;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.col-sm-9 {
padding-right: 0px !important;
}
}

View file

@ -1,22 +0,0 @@
@import "dark.css";
body, .modal-content {
background-color: #000;
}
#topbar, #sidebar, .panel, .dropdown-menu, #topbar .actions {
background: #080808;
}
.nav-pills li:nth-child(2n) {
background: #000;
}
.form-control option, input.form-control, select.form-control {
color: #ccc;
background: #080808;
}
.close, .close:hover, .close:focus {
color: #ddd;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,14 +0,0 @@
<form class="form form-horizontal" id="inputform" ng-submit="sendMessage()" imgur-drop>
<div class="input-group">
<textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" ng-change="inputChanged()" autocomplete="on" ng-model="command" ng-focus="hideSidebar()">
</textarea>
<span class="input-group-btn">
<label class="btn btn-send-image unselectable" for="imgur-upload" title="Send image">
<i class="glyphicon glyphicon-picture"></i>
<input type="file" accept="image/*" multiple title="Send image" id="imgur-upload" class="imgur-upload" file-change="uploadImage($event, files)">
</label>
<button class="btn btn-send unselectable" title="Send"><i class="glyphicon glyphicon-send"></i></button>
</span>
</div>
<div id="imgur-upload-progress"></div>
</form>

View file

@ -1,18 +0,0 @@
<div ng-show="plugin.visible">
<button class="btn btn-default btn-sm pull-right unselectable"
ng-click="hideContent()">
Hide {{ ::plugin.name }}
</button>
<div ng-bind-html="displayedContent" class="embed" ng-class="::plugin.className"></div>
</div>
<div ng-hide="plugin.visible">
<button class="btn btn-default btn-sm pull-right unselectable"
ng-class="::{
'btn-warning': plugin.nsfw,
'btn-primary': !plugin.nsfw}"
ng-click="showContent()">
Show {{ ::plugin.name }}
</button>
</div>

View file

@ -1,513 +0,0 @@
<!DOCTYPE html>
<html ng-app="weechat" ng-cloak>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<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="apple-mobile-web-app-title" content="Glowing Bear">
<meta name="theme-color" content="#181818">
<meta http-equiv="x-dns-prefetch-control" content="off">
<!-- https://w3c.github.io/manifest/ && https://developer.mozilla.org/en-US/docs/Web/Manifest -->
<link rel="manifest" href="webapp.manifest.json">
<title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen" integrity="sha384-7tY7Dc2Q8WQTKGz2Fa0vC4dWQo07N4mJjKvHfIGnxuC4vPqFGFQppd9b3NWpf18/" crossorigin="anonymous">
<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">
<link href="css/themes/dark.css" rel="stylesheet" media="screen" id="themeCSS" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js" integrity="sha384-r1y8TJcloKTvouxnYsi4PJAx+nHNr90ibsEn3zznzDzWBN9X3o3kbHLSgcIPtzAp" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-route.min.js" integrity="sha384-fQQcs0/yvL0uyyzpXoTKfcQl5e9GYh7GKIft35qSjfKXSILYNI6YZOM0Ju94DY+/" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-sanitize.min.js" integrity="sha384-79uolbJAcWnfqb2Oi/w0fEz2NdE5lvY1p+TSew6D3XC7PlZY1OGuvGBiwjZhFvOg" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-touch.min.js" integrity="sha384-bnrVwYH8/uQCvK9n+xYQKdf1xtgSNHBYcy0djCofRUPvAt93iEhBfHlngRP/aXsg" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js" integrity="sha384-nXjwhL1LfWUDVHxQ2R0rHpbr/E6lfCFXR4kfcPHp1eLGH1dH/mZohGINd44EzEya" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojione/2.1.0/lib/js/emojione.min.js" integrity="sha384-pJb7FFLYTcgO7KbgirAXNIHFIKzywqq4LIcWx9cavPapYWdCH5mcYptrkpHHEkH1" crossorigin="anonymous"></script>
<script type="text/javascript" src="3rdparty/inflate.min.js"></script>
<script type="text/javascript" src="js/localstorage.js"></script>
<script type="text/javascript" src="js/weechat.js"></script>
<script type="text/javascript" src="js/irc-utils.js"></script>
<script type="text/javascript" src="js/glowingbear.js"></script>
<script type="text/javascript" src="js/settings.js"></script>
<script type="text/javascript" src="js/utils.js"></script>
<script type="text/javascript" src="js/notifications.js"></script>
<script type="text/javascript" src="js/filters.js"></script>
<script type="text/javascript" src="js/handlers.js"></script>
<script type="text/javascript" src="js/connection.js"></script>
<script type="text/javascript" src="js/file-change.js"></script>
<script type="text/javascript" src="js/imgur-drop-directive.js"></script>
<script type="text/javascript" src="js/whenscrolled-directive.js"></script>
<script type="text/javascript" src="js/inputbar.js"></script>
<script type="text/javascript" src="js/plugin-directive.js"></script>
<script type="text/javascript" src="js/websockets.js"></script>
<script type="text/javascript" src="js/models.js"></script>
<script type="text/javascript" src="js/plugins.js"></script>
<script type="text/javascript" src="js/imgur.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}" ng-init="init()" lang="en-US">
<div class="alert alert-danger upload-error" ng-show="uploadError">
<p><strong>Upload error:</strong> Image upload failed.</p>
</div>
<div ng-hide="connected" class="container">
<h2>
<img alt="logo" src="assets/img/glowing-bear.svg">
<span>Glowing Bear</span>
<small>WeeChat web frontend</small>
</h2>
<div class="alert alert-danger" ng-show="errorMessage">
<strong>Connection error</strong> The client was unable to connect to the WeeChat relay
</div>
<div class="alert alert-danger" ng-show="sslError">
<strong>Secure connection error</strong> A secure connection with the WeeChat relay could not be initiated. This is most likely because your browser does not trust your relay's certificate. Please read the encryption instructions below!
</div>
<div class="alert alert-danger" ng-show="securityError">
<strong>Secure connection error</strong> Unable to connect to unencrypted relay when your are connecting to Glowing Bear over HTTPS. Please use an encrypted relay or load the page without using HTTPS.
</div>
<div class="panel-group accordion">
<div class="panel" data-state="active">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Connection settings
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in">
<div class="panel-body">
<form class="form-signin" role="form">
<div class="form-group">
<label class="control-label" for="host">WeeChat relay hostname and port number</label>
<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="settings.host" placeholder="Address" autocapitalize="off">
</div>
<div class="col-sm-3">
<input type="text" class="form-control favorite-font" id="port" ng-model="settings.port" placeholder="Port">
</div>
</div>
</div>
<label class="control-label" for="password">WeeChat relay password</label>
<input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password">
<div class="alert alert-danger" ng-show="passwordError">
Error: wrong password
</div>
<div class="checkbox">
<label class="control-label" for="savepassword">
<input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Save password in your browser
</label>
</div>
<div class="checkbox" ng-show="settings.savepassword">
<label class="control-label" for="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="settings.ssl">
Encryption. Read instructions for help
</label>
</div>
</div>
<button class="btn btn-lg btn-primary" ng-click="connect()">{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button>
</form>
</div>
</div>
</div>
<div class="panel" data-state="collapsed">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Usage instructions
</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse in">
<div class="panel-body">
<h3>Configuring the relay</h3>
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
<pre>
/set relay.network.password yourpassword
/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.
Connection settings, including your password, are saved locally in your own browser between sessions.
<br>
<h3>Shortcuts</h3>
Glowing Bear has a few shortcuts:
<ul>
<li><kbd>ALT-n</kbd>: Toggle nicklist</li>
<li><kbd>ALT-l</kbd>: Focus on input bar</li>
<li><kbd>ALT-[0-9]</kbd>: Switch to buffer number N</li>
<li><kbd>ALT-a</kbd>: Focus on next buffer with activity</li>
<li><kbd>ALT-&lt;</kbd>: Switch to previous active buffer</li>
<li><kbd>ALT-g</kbd>: Focus on buffer list filter</li>
<li><kbd>Esc-Esc</kbd>: Disconnect (double-tap)</li>
<li>Arrow keys: Navigate history</li>
<li><kbd>Tab</kbd>: Complete nick</li>
<li>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line"><kbd>Ctrl-a</kbd></span>, <span title="Move cursor to te end of the line"><kbd>Ctrl-e</kbd></span>, <span title="Delete from cursor to beginning of the line"><kbd>Ctrl-u</kbd></span>, <span title="Delete from cursor to the end of the line"><kbd>Ctrl-k</kbd></span>, <span title="Delete from cursor to previous space"><kbd>Ctrl-w</kbd></span></li>
</ul>
</div>
</div>
</div>
</div>
<div class="panel" data-state="collapsed">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Encryption instructions
</a>
</h4>
</div>
<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://{{ settings.host }}:{{ settings.port }}/weechat">https://{{ settings.host || 'weechathost' }}:{{ settings.port || 'relayport' }}/weechat</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 -sha256 -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 {{ settings.port || 9001 }} with these WeeChat commands:</p>
<pre>
/set relay.network.password yourpassword
/relay sslcertkey
/relay add ssl.weechat {{ settings.port || 9001 }}
</pre>
</div>
</div>
</div>
<div class="panel" data-state="collapsed" ng-hide="isinstalled">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Install app
</a>
</h4>
</div>
<div id="collapseFour" class="panel-collapse collapse in">
<div class="panel-body">
<p>You don't need to install anything to use this app, it should work with any modern browser. Start using it <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">right now</a>! However, there are a few ways to improve integration with your operating system.</p>
<h3>Mobile Applications</h3>
<p>If you're running Android 4.4 or later, you can install our app from the Google Play Store! We also provide an optimized application for Firefox OS devices. If you're using the Firefox browser, keep on reading below -- the Firefox OS app won't work for you</p>
<p><a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="assets/img/badge_playstore.png" /></a> <a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="assets/img/badge_firefoxos.png" /></a></p>
<h3>Firefox Browser</h3>
<p>If you have a recent version of Firefox you can install Glowing Bear as a Firefox app. Click the button to install.</p>
<p><button class="btn btn-lg btn-primary" ng-click="install()">Install Firefox app <i class="glyphicon glyphicon-chevron-right"></i></button></p>
<p>Note for self-signed certificates: Firefox does not share a certificate storage with Firefox apps, so accepting self-signed certificates is a bit tricky.</p>
<h3>Chrome</h3>
<p>To install Glowing Bear as an app in Chrome for Android, select <kbd>Menu - Add to home screen</kbd>. In the desktop version of Chrome, click <kbd>Menu - More tools - Create application shortcuts</kbd>.</p>
</div>
</div>
</div>
<div class="panel" data-state="collapsed">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Get involved
</a>
</h4>
</div>
<div id="collapseFive" class="panel-collapse collapse in">
<div class="panel-body">
<p>Glowing bear is built by a small group of developers in their free time. As we're always trying to improve it, we would love getting your feedback and help. If that sounds like something you might enjoy, check out our <a href="https://github.com/glowing-bear/glowing-bear">project page</a> on GitHub!</p>
<p>If you're interested in contributing or simply want to say hello, head over to <strong>#glowing-bear</strong> on <strong>freenode!</strong> We won't bite, promise (-ish).</p>
</div>
</div>
</div>
</div>
</div>
<div class="content" id="content" sidebar-state="visible" ng-show="connected">
<div id="topbar">
<div class="brand">
<a href="#" ng-click="toggleSidebar()">
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}">
</a>
<span class="badge" ng-show="unread > 0">{{unread}}</span>
<span class="badge danger" ng-show="notifications > 0">{{notifications}}</span>
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div>
<div class="title" title="{{activeBuffer().rtitle}}">
<span class="desktop" ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span>
<span class="mobile" ng-click="showModal('topicModal')" ng-class="{'active': activeBuffer().active, 'channel': activeBuffer().type === 'channel', 'channel_hash': activeBuffer().prefix === '#', 'channel_plus': activeBuffer().prefix === '+', 'channel_ampersand': activeBuffer().prefix === '&'}">{{ activeBuffer().trimmedName || activeBuffer().fullName }}</span>
</div>
<div class="actions pull-right vertical-line-left">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu" href="#">
<i class="glyphicon glyphicon-cog"></i>
</a>
<a ng-click="disconnect()" title="Disconnect from WeeChat" href="#">
<i class="glyphicon glyphicon-off"></i>
</a>
</div>
</div>
<div id="sidebar" data-state="visible" ng-swipe-left="hideSidebar()" class="vertical-line">
<ul class="nav nav-pills nav-stacked" ng-class="{'indented': (predicate === 'serverSortKey'), 'showquickkeys': showQuickKeys}">
<li class="bufferfilter">
<form role="form">
<input class="form-control favorite-font" type="text" id="bufferFilter" ng-model="search" ng-keydown="handleSearchBoxKey($event)" placeholder="Search" autocomplete="off">
</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 ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}" href="#">
<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>
</a>
</li>
</ul>
</div>
<div id="bufferlines" class="favorite-font" ng-swipe-right="showSidebar()" ng-swipe-left="hideSidebar()" ng-class="{'withnicklist': showNicklist}" when-scrolled="infiniteScroll()" imgur-drop>
<div id="nicklist" ng-if="showNicklist" ng-swipe-right="closeNick()" class="vertical-line-left">
<ul class="nicklistgroup list-unstyled" ng-repeat="group in nicklist">
<li ng-repeat="nick in group.nicks|orderBy:'name'">
<a ng-click="openBuffer(nick.name)"><span ng-class="::nick.prefixClasses" ng-bind="::nick.prefix"></span><span ng-class="::nick.nameClasses" ng-bind="::nick.name"></span></a>
</li>
</ul>
</div>
<table ng-class="{'notimestamp':!settings.showtimestamp,'notimestampseconds':!settings.showtimestampSeconds}">
<tbody>
<tr class="bufferline">
<td ng-hide="activeBuffer().allLinesFetched" colspan="3">
<a class="fetchmorelines btn btn-xs btn-primary" ng-click="fetchMoreLines()" ng-hide="loadingLines" href="#">Fetch more lines</a>
<span ng-show="loadingLines">Fetching more lines <i class="glyphicon glyphicon-refresh glyphicon-spin"></i></span>
</td>
</tr>
</tbody>
<tbody ng-repeat="bufferline in bufferlines">
<tr class="bufferline">
<td class="time">
<span class="date" ng-class="::{'repeated-time': bufferline.shortTime==bufferlines[$index-1].shortTime}">
<span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'HH')"></span><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">:</span><span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'mm')"></span><span class="seconds"><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">:</span><span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'ss')"></span></span>
</span>
</td>
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span class="hidden-bracket">&lt;</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket">&gt;</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.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' | DOMfilter:'mathjax':('.line-' + part.$$hashKey.replace(':','_')):settings.enableMathjax"></span>
</td>
</tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
<td colspan="3">
<hr id="readmarker">
</td>
</tr>
</tbody>
</table><span id="end-of-buffer"></span>
</div>
<div class="footer" ng-class="{'withnicklist': showNicklist}">
<div input-bar input-id="sendMessage" command="command"></div>
</div>
</div>
<div id="soundNotification"></div>
<div id="reconnect" class="alert alert-danger" ng-click="reconnect()" ng-show="reconnecting">
<p><strong>Connection to WeeChat lost</strong></p>
<i class="glyphicon glyphicon-refresh"></i>
Reconnecting... <i class="glyphicon glyphicon-spin glyphicon-refresh"></i> <a class="btn btn-xs" ng-click="reconnect()" href="#">Click to try to reconnect now</a>
</div>
<div id="settingsModal" class="gb-modal" data-state="hidden">
<div class="backdrop" ng-click="closeModal($event)"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<span class="pull-right version">Glowing Bear version 0.6.1-dev</span>
<h4 class="modal-title">Settings</h4>
<p>Settings will be stored in your browser.</p>
</div>
<div class="modal-body">
<ul class="">
<li class="standard-labels">
<form class="form-horizontal" role="form">
<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="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="settings.fontsize" class="form-control" id="size">
</div>
</div>
</form>
</li>
<li class="standard-labels">
<form class="form-horizontal" role="form">
<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="settings.theme" ng-options="theme for theme in themes"></select>
</div>
</div>
</form>
</li>
<li class="standard-labels">
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="custom-css" class="col-sm-3 control-label make-thinner">Custom CSS</label>
<div class="col-sm-7">
<textarea id="custom-css" class="form-control" ng-model="settings.customCSS"></textarea>
</div>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.onlyUnread">
Only show buffers with unread messages
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.showtimestamp">
Show timestamps
</label>
</div>
</form>
<ul ng-show="settings.showtimestamp">
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.showtimestampSeconds">
Show seconds
</label>
</div>
</form>
</li>
</ul>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<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>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.hotlistsync">
Mark messages as read in WeeChat
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.nonicklist">
Hide nicklist
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.orderbyserver">
Hierarchical buffer view (order by server)
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.readlineBindings">
Enable common readline keybindings in input bar
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.useFavico">
Display unread count in favicon
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<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="settings.enableJSEmoji">
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images. Emoji provided free by <a href="http://emojione.com">http://emojione.com</a></span>
</label>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.enableMathjax">
Enable LaTeX math rendering
</label>
</div>
</form>
</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<div id="topicModal" class="gb-modal" data-state="hidden">
<div class="backdrop" ng-click="closeModal($event)"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<h4 class="modal-title">Channel topic</h4>
<p ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</body>
</html>

View file

@ -1,387 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('connection',
['$rootScope', '$log', 'handlers', 'models', 'ngWebsockets', function($rootScope,
$log,
handlers,
models,
ngWebsockets) {
var protocol = new weeChat.Protocol();
var connectionData = [];
var reconnectTimer;
// Takes care of the connection and websocket hooks
var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) {
$rootScope.passwordError = false;
connectionData = [host, port, passwd, ssl, noCompression];
var proto = ssl ? 'wss' : 'ws';
// If host is an IPv6 literal wrap it in brackets
if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") {
host = "[" + host + "]";
}
var url = proto + "://" + host + ":" + port + "/weechat";
$log.debug('Connecting to URL: ', url);
var onopen = function () {
// Helper methods for initialization commands
var _initializeConnection = function(passwd) {
// This is not the proper way to do this.
// WeeChat does not send a confirmation for the init.
// Until it does, We need to "assume" that formatInit
// will be received before formatInfo
ngWebsockets.send(
weeChat.Protocol.formatInit({
password: passwd,
compression: noCompression ? 'off' : 'zlib'
})
);
return ngWebsockets.send(
weeChat.Protocol.formatInfo({
name: 'version'
})
);
};
var _requestHotlist = function() {
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
path: "hotlist:gui_hotlist(*)",
keys: []
})
);
};
var _requestBufferInfos = function() {
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
path: 'buffer:gui_buffers(*)',
keys: ['local_variables,notify,number,full_name,short_name,title,hidden,type']
})
);
};
var _requestSync = function() {
return ngWebsockets.send(
weeChat.Protocol.formatSync({})
);
};
// First command asks for the password and issues
// a version command. If it fails, it means the we
// did not provide the proper password.
_initializeConnection(passwd).then(
function(version) {
handlers.handleVersionInfo(version);
// Connection is successful
// Send all the other commands required for initialization
_requestBufferInfos().then(function(bufinfo) {
handlers.handleBufferInfo(bufinfo);
});
_requestHotlist().then(function(hotlist) {
handlers.handleHotlistInfo(hotlist);
if (successCallback) {
successCallback();
}
});
_requestSync();
$log.info("Connected to relay");
$rootScope.connected = true;
},
function() {
handleWrongPassword();
}
);
};
var onmessage = function() {
// If we recieve a message from WeeChat it means that
// password was OK. Store that result and check for it
// in the failure handler.
$rootScope.waseverconnected = true;
};
var onclose = function (evt) {
/*
* Handles websocket disconnection
*/
$log.info("Disconnected from relay");
$rootScope.$emit('relayDisconnect');
if ($rootScope.userdisconnect || !$rootScope.waseverconnected) {
handleClose(evt);
$rootScope.userdisconnect = false;
} else {
reconnect(evt);
}
handleWrongPassword();
};
var handleClose = function (evt) {
if (ssl && evt && evt.code === 1006) {
// A password error doesn't trigger onerror, but certificate issues do. Check time of last error.
if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) {
// abnormal disconnect by client, most likely ssl error
$rootScope.sslError = true;
$rootScope.$apply();
}
}
};
var handleWrongPassword = function() {
// Connection got closed, lets check if we ever was connected successfully
if (!$rootScope.waseverconnected && !$rootScope.errorMessage) {
$rootScope.passwordError = true;
$rootScope.$apply();
}
};
var onerror = function (evt) {
/*
* Handles cases when connection issues come from
* the relay.
*/
$log.error("Relay error", evt);
$rootScope.lastError = Date.now();
if (evt.type === "error" && this.readyState !== 1) {
ngWebsockets.failCallbacks('error');
$rootScope.errorMessage = true;
}
};
try {
ngWebsockets.connect(url,
protocol,
{
'binaryType': "arraybuffer",
'onopen': onopen,
'onclose': onclose,
'onmessage': onmessage,
'onerror': onerror
});
} catch(e) {
$log.debug("Websocket caught DOMException:", e);
$rootScope.lastError = Date.now();
$rootScope.errorMessage = true;
$rootScope.securityError = true;
$rootScope.$emit('relayDisconnect');
if (failCallback) {
failCallback();
}
}
};
var attemptReconnect = function (bufferId, timeout) {
$log.info('Attempting to reconnect...');
var d = connectionData;
connect(d[0], d[1], d[2], d[3], d[4], function() {
$rootScope.reconnecting = false;
// on success, update active buffer
models.setActiveBuffer(bufferId);
$log.info('Sucessfully reconnected to relay');
}, function() {
// on failure, schedule another attempt
if (timeout >= 600000) {
// If timeout is ten minutes or more, give up
$log.info('Failed to reconnect, giving up');
handleClose();
} else {
$log.info('Failed to reconnect, scheduling next attempt in', timeout/1000, 'seconds');
// Clear previous timer, if exists
if (reconnectTimer !== undefined) {
clearTimeout(reconnectTimer);
}
reconnectTimer = setTimeout(function() {
// exponential timeout increase
attemptReconnect(bufferId, timeout * 1.5);
}, timeout);
}
});
};
var reconnect = function (evt) {
if (connectionData.length < 5) {
// something is wrong
$log.error('Cannot reconnect, connection information is missing');
return;
}
// reinitialise everything, clear all buffers
// TODO: this can be further extended in the future by looking
// at the last line in ever buffer and request more buffers from
// WeeChat based on that
models.reinitialize();
$rootScope.reconnecting = true;
// Have to do this to get the reconnect banner to show
$rootScope.$apply();
var bufferId = models.getActiveBuffer().id,
timeout = 3000; // start with a three-second timeout
reconnectTimer = setTimeout(function() {
attemptReconnect(bufferId, timeout);
}, timeout);
};
var disconnect = function() {
$log.info('Disconnecting from relay');
$rootScope.userdisconnect = true;
ngWebsockets.send(weeChat.Protocol.formatQuit());
// In case the backend doesn't repond we will close from our end
var closeTimer = setTimeout(function() {
ngWebsockets.disconnect();
// We pretend we are not connected anymore
// The connection can time out on its own
ngWebsockets.failCallbacks('disconnection');
$rootScope.connected = false;
$rootScope.$emit('relayDisconnect');
$rootScope.$apply();
});
};
/*
* Format and send a weechat message
*
* @returns the angular promise
*/
var sendMessage = function(message) {
ngWebsockets.send(weeChat.Protocol.formatInput({
buffer: models.getActiveBufferReference(),
data: message
}));
};
var sendCoreCommand = function(command) {
ngWebsockets.send(weeChat.Protocol.formatInput({
buffer: 'core.weechat',
data: command
}));
};
var sendHotlistClear = function() {
if (models.version[0] >= 1) {
// WeeChat >= 1 supports clearing hotlist with this command
sendMessage('/buffer set hotlist -1');
// Also move read marker
sendMessage('/input set_unread_current_buffer');
} else {
// If user wants to sync hotlist with weechat
// 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
sendCoreCommand('/buffer ' + models.getActiveBuffer().fullName);
}
};
var requestNicklist = function(bufferId, callback) {
// Prevent requesting nicklist for all buffers if bufferId is invalid
if (!bufferId) {
return;
}
ngWebsockets.send(
weeChat.Protocol.formatNicklist({
buffer: "0x"+bufferId
})
).then(function(nicklist) {
handlers.handleNicklist(nicklist);
if (callback !== undefined) {
callback();
}
});
};
var fetchConfValue = function(name) {
ngWebsockets.send(
weeChat.Protocol.formatInfolist({
name: "option",
pointer: 0,
args: name
})
).then(function(i) {
handlers.handleConfValue(i);
});
};
var fetchMoreLines = function(numLines) {
$log.debug('Fetching ', numLines, ' lines');
var buffer = models.getActiveBuffer();
if (numLines === undefined) {
// Math.max(undefined, *) = NaN -> need a number here
numLines = 0;
}
// Calculate number of lines to fetch, at least as many as the parameter
numLines = Math.max(numLines, buffer.requestedLines * 2);
// Indicator that we are loading lines, hides "load more lines" link
$rootScope.loadingLines = true;
// Send hdata request to fetch lines for this particular buffer
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
// "0x" is important, otherwise it won't work
path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data",
keys: []
})
).then(function(lineinfo) {
//XXX move to handlers?
// delete old lines and add new ones
var oldLength = buffer.lines.length;
// whether we already had all unread lines
var hadAllUnreadLines = buffer.lastSeen >= 0;
// clear the old lines
buffer.lines.length = 0;
// We need to set the number of requested lines to 0 here, because parsing a line
// increments it. This is needed to also count newly arriving lines while we're
// already connected.
buffer.requestedLines = 0;
// Count number of lines recieved
var linesReceivedCount = lineinfo.objects[0].content.length;
// Parse the lines
handlers.handleLineInfo(lineinfo, true);
// Correct the read marker for the lines that were counted twice
buffer.lastSeen -= oldLength;
// We requested more lines than we got, no more lines.
if (linesReceivedCount < numLines) {
buffer.allLinesFetched = true;
}
$rootScope.loadingLines = false;
// Only scroll to read marker if we didn't have all unread lines previously, but have them now
var scrollToReadmarker = !hadAllUnreadLines && buffer.lastSeen >= 0;
// Scroll to correct position
$rootScope.scrollWithBuffer(scrollToReadmarker, true);
});
};
return {
connect: connect,
disconnect: disconnect,
sendMessage: sendMessage,
sendCoreCommand: sendCoreCommand,
sendHotlistClear: sendHotlistClear,
fetchMoreLines: fetchMoreLines,
requestNicklist: requestNicklist,
attemptReconnect: attemptReconnect
};
}]);
})();

View file

@ -1,23 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('fileChange', ['$parse', function($parse) {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
var attrHandler = $parse(attrs.fileChange);
var handler = function (e) {
$scope.$apply(function () {
attrHandler($scope, { $event: e, files: e.target.files });
});
};
element[0].addEventListener('change', handler, false);
}
};
}]);
})();

View file

@ -1,205 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.filter('toArray', function () {
return function (obj, storeIdx) {
if (!(obj instanceof Object)) {
return obj;
}
if (storeIdx) {
return Object.keys(obj).map(function (key, idx) {
return Object.defineProperties(obj[key], {
'$key' : { value: key },
'$idx' : { value: idx, configurable: true }
});
});
}
return Object.keys(obj).map(function (key) {
return Object.defineProperty(obj[key], '$key', { value: key });
});
};
});
weechat.filter('irclinky', function() {
return function(text) {
if (!text) {
return text;
}
// 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;
// Call the method we bound to window.openBuffer when we instantiated
// the Weechat controller.
var substitute = '$1<a href="#" onclick="openBuffer(\'$2\');">$2</a>';
return text.replace(channelRegex, substitute);
};
});
weechat.filter('inlinecolour', function() {
return function(text) {
if (!text) {
return text;
}
// 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 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;
};
});
// apply a filter to an HTML string's text nodes, and do so with not exceedingly terrible performance
weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
// To prevent nested anchors, we need to know if a filter is going to create them.
// Here's a list of names. See #681 for more information.
var filtersThatCreateAnchors = ['irclinky'];
return function(text, filter) {
if (!text || !filter) {
return text;
}
var createsAnchor = filtersThatCreateAnchors.indexOf(filter) > -1;
var escape_html = function(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;'};
return text.replace(/[<>"'&\/]/g, function (char) {
return entities[char];
});
};
// hacky way to pass extra arguments without using .apply, which
// would require assembling an argument array. PERFORMANCE!!!
var extraArgument = (arguments.length > 2) ? arguments[2] : null;
var thirdArgument = (arguments.length > 3) ? arguments[3] : null;
var filterFunction = $filter(filter);
var el = document.createElement('div');
el.innerHTML = text;
// Recursive DOM-walking function applying the filter to the text nodes
var process = function(node) {
if (node.nodeType === 3) { // text node
// apply the filter to *escaped* HTML, and only commit changes if
// it changed the escaped value. This is because setting the result
// as innerHTML causes it to be unescaped.
var input = escape_html(node.nodeValue);
var value = filterFunction(input, extraArgument, thirdArgument);
if (value !== input) {
// we changed something. create a new node to replace the current one
// we could also only add its children but that would probably incur
// more overhead than it would gain us
var newNode = document.createElement('span');
newNode.innerHTML = value;
var parent = node.parentNode;
var sibling = node.nextSibling;
parent.removeChild(node);
if (sibling) {
parent.insertBefore(newNode, sibling);
} else {
parent.appendChild(newNode);
}
return newNode;
}
}
// recurse
if (node === undefined || node === null) return;
node = node.firstChild;
while (node) {
var nextNode = null;
// do not recurse inside links if the filter would create a nested link
if (!(createsAnchor && node.tagName === 'A')) {
nextNode = process(node);
}
node = (nextNode ? nextNode : node).nextSibling;
}
};
process(el);
return $sce.trustAsHtml(el.innerHTML);
};
}]);
weechat.filter('getBufferQuickKeys', function () {
return function (obj, $scope) {
if (!$scope) { return obj; }
if (($scope.search !== undefined && $scope.search.length) || $scope.onlyUnread) {
obj.forEach(function(buf, idx) {
buf.$quickKey = idx < 10 ? (idx + 1) % 10 : '';
});
} else {
_.map(obj, function(buffer, idx) {
return [buffer.number, buffer.$idx, idx];
}).sort(function(left, right) {
// By default, Array.prototype.sort() sorts alphabetically.
// Pass an ordering function to sort by first element.
return left[0] - right[0] || left[1] - right[1];
}).forEach(function(info, keyIdx) {
obj[ info[2] ].$quickKey = keyIdx < 10 ? (keyIdx + 1) % 10 : '';
});
}
return obj;
};
});
// Emojifis the string using https://github.com/Ranks/emojione
weechat.filter('emojify', function() {
return function(text, enable_JS_Emoji) {
if (enable_JS_Emoji === true && window.emojione !== undefined) {
// Emoji live in the D800-DFFF surrogate plane; only bother passing
// this range to CPU-expensive unicodeToImage();
var emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
if (emojiRegex.test(text)) {
return emojione.unicodeToImage(text);
} else {
return(text);
}
} else {
return(text);
}
};
});
weechat.filter('mathjax', function() {
return function(text, selector, enabled) {
if (!enabled || typeof(MathJax) === "undefined") {
return text;
}
if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) {
// contains math
var math = document.querySelector(selector);
MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]);
}
return text;
};
});
weechat.filter('prefixlimit', function() {
return function(input, chars) {
if (isNaN(chars)) return input;
if (chars <= 0) return '';
if (input && input.length > chars) {
input = input.substring(0, chars);
return input + '+';
}
return input;
};
});
})();

View file

@ -1,848 +0,0 @@
(function() {
'use strict';
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', 'settings',
function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) {
window.openBuffer = function(channel) {
$scope.openBuffer(channel);
$scope.$apply();
};
$scope.command = '';
$scope.themes = ['dark', 'light', 'black'];
// Initialise all our settings, this needs to include all settings
// or else they won't be saved to the localStorage.
settings.setDefaults({
'theme': 'dark',
'host': 'localhost',
'port': 9001,
'ssl': (window.location.protocol === "https:"),
'savepassword': false,
'autoconnect': false,
'nonicklist': utils.isMobileUi(),
'noembed': true,
'onlyUnread': false,
'hotlistsync': true,
'orderbyserver': true,
'useFavico': true,
'showtimestamp': true,
'showtimestampSeconds': false,
'soundnotification': true,
'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false,
'enableJSEmoji': (utils.isMobileUi() ? false : true),
'enableMathjax': false,
'customCSS': '',
});
$scope.settings = settings;
$rootScope.countWatchers = function () {
$log.debug($rootScope.$$watchersCount);
};
$scope.isinstalled = (function() {
// Check for firefox & app installed
if (navigator.mozApps !== undefined) {
navigator.mozApps.getSelf().onsuccess = function _onAppReady(evt) {
var app = evt.target.result;
if (app) {
return true;
} else {
return false;
}
};
} else {
return false;
}
}());
// Detect page visibility attributes
(function() {
// Sadly, the page visibility API still has a lot of vendor prefixes
if (typeof document.hidden !== "undefined") { // Chrome >= 33, Firefox >= 18, Opera >= 12.10, Safari >= 7
$scope.documentHidden = "hidden";
$scope.documentVisibilityChange = "visibilitychange";
} else if (typeof document.webkitHidden !== "undefined") { // 13 <= Chrome < 33
$scope.documentHidden = "webkitHidden";
$scope.documentVisibilityChange = "webkitvisibilitychange";
} else if (typeof document.mozHidden !== "undefined") { // 10 <= Firefox < 18
$scope.documentHidden = "mozHidden";
$scope.documentVisibilityChange = "mozvisibilitychange";
} else if (typeof document.msHidden !== "undefined") { // IE >= 10
$scope.documentHidden = "msHidden";
$scope.documentVisibilityChange = "msvisibilitychange";
}
})();
// Enable debug mode if "?debug=1" or "?debug=true" is set
(function() {
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;
}
});
// 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 && !weechat.compileProvider.debugInfoEnabled()) {
angular.reloadWithDebugInfo();
}
})();
$rootScope.isWindowFocused = function() {
if (typeof $scope.documentHidden === "undefined") {
// Page Visibility API not supported, assume yes
return true;
} else {
var isHidden = document[$scope.documentHidden];
return !isHidden;
}
};
if (typeof $scope.documentVisibilityChange !== "undefined") {
document.addEventListener($scope.documentVisibilityChange, function() {
if (!document[$scope.documentHidden]) {
// We just switched back to the glowing-bear window and unread messages may have
// accumulated in the active buffer while the window was in the background
var buffer = models.getActiveBuffer();
// This can also be triggered before connecting to the relay, check for null (not undefined!)
if (buffer !== null) {
buffer.unread = 0;
buffer.notification = 0;
// Trigger title and favico update
$rootScope.$emit('notificationChanged');
}
// the unread badge in the bufferlist doesn't update if we don't do this
$rootScope.$apply();
}
}, false);
}
$rootScope.$on('activeBufferChanged', function(event, unreadSum) {
var ab = models.getActiveBuffer();
// Discard surplus lines. This is done *before* lines are fetched because that saves us the effort of special handling for the
// case where a buffer is opened for the first time ;)
var minRetainUnread = ab.lines.length - unreadSum + 5; // do not discard unread lines and keep 5 additional lines for context
var surplusLines = ab.lines.length - (2 * $scope.lines_per_screen + 10); // retain up to 2*(screenful + 10) + 10 lines because magic numbers
var linesToRemove = Math.min(minRetainUnread, surplusLines);
if (linesToRemove > 0) {
ab.lines.splice(0, linesToRemove); // remove the lines from the buffer
ab.requestedLines -= linesToRemove; // to ensure that the correct amount of lines is fetched should more be requested
ab.lastSeen -= linesToRemove; // adjust readmarker
ab.allLinesFetched = false; // we just removed lines, so we don't have all of them. re-enable "fetch more lines"
}
$scope.bufferlines = ab.lines;
$scope.nicklist = ab.nicklist;
// Send a request for the nicklist if it hasn't been loaded yet
if (!ab.nicklistRequested()) {
connection.requestNicklist(ab.id, function() {
$scope.showNicklist = $scope.updateShowNicklist();
// Scroll after nicklist has been loaded, as it may break long lines
$rootScope.scrollWithBuffer(true);
});
} else {
// Check if we should show nicklist or not
$scope.showNicklist = $scope.updateShowNicklist();
}
if (ab.requestedLines < $scope.lines_per_screen) {
// buffer has not been loaded, but some lines may already be present if they arrived after we connected
// try to determine how many lines to fetch
var numLines = $scope.lines_per_screen + 10; // that's (a screenful plus 10 lines) plus 10 lines, just to be safe
if (unreadSum > numLines) {
// request up to 4*(screenful + 10 lines)
numLines = Math.min(4*numLines, unreadSum);
}
$scope.fetchMoreLines(numLines).then(
// Update initial scroll position
// Most relevant when first connecting to properly initalise
function() {
$timeout(function() {
var bl = document.getElementById("bufferlines");
var lastScrollHeight = bl.scrollHeight;
var scrollHeightObserver = function() {
if (bl) {
var newScrollHeight = bl.scrollHeight;
if (newScrollHeight !== lastScrollHeight) {
$rootScope.updateBufferBottom($rootScope.bufferBottom);
lastScrollHeight = newScrollHeight;
}
setTimeout(scrollHeightObserver, 500);
}
};
$rootScope.updateBufferBottom(true);
$rootScope.scrollWithBuffer(true);
bl.onscroll = _.debounce(function() {
$rootScope.updateBufferBottom();
}, 80);
setTimeout(scrollHeightObserver, 500);
});
}
);
}
notifications.updateTitle(ab);
setTimeout(function(){
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
});
$timeout(function() {
$rootScope.scrollWithBuffer(true);
});
// Clear search term on buffer change
$scope.search = '';
if (!utils.isMobileUi()) {
// This needs to happen asynchronously to prevent the enter key handler
// of the input bar to be triggered on buffer switch via the search.
// Otherwise its current contents would be sent to the new buffer
setTimeout(function() {
document.getElementById('sendMessage').focus();
}, 0);
}
// Do this part last since it's not important for the UI
if (settings.hotlistsync && ab.fullName) {
connection.sendHotlistClear();
}
});
$rootScope.favico = new Favico({animation: 'none'});
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
$rootScope.$on('notificationChanged', function() {
notifications.updateTitle();
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
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';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
});
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
$scope.getBuffers = models.getBuffers.bind(models);
$scope.bufferlines = {};
$scope.nicklist = {};
$scope.activeBuffer = models.getActiveBuffer;
$rootScope.connected = false;
$rootScope.waseverconnected = false;
$rootScope.userdisconnect = false;
$rootScope.reconnecting = false;
$rootScope.models = models;
$rootScope.iterCandidate = null;
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 = '';
}
// Check if user decides to save password, and copy it over
settings.addCallback('savepassword', function(newvalue) {
if (settings.savepassword) {
// Init value in settings module
settings.setDefaults({'password': $scope.password});
settings.password = $scope.password;
}
});
$rootScope.wasMobileUi = false;
if (utils.isMobileUi()) {
$rootScope.wasMobileUi = true;
}
if (!settings.fontfamily) {
if (utils.isMobileUi()) {
settings.fontfamily = 'sans-serif';
} else {
settings.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace";
}
}
$scope.isSidebarVisible = function() {
return document.getElementById('content').getAttribute('sidebar-state') === 'visible';
};
$scope.showSidebar = function() {
document.getElementById('sidebar').setAttribute('data-state', 'visible');
document.getElementById('content').setAttribute('sidebar-state', 'visible');
if (utils.isMobileUi()) {
// de-focus the input bar when opening the sidebar on mobile, so that the keyboard goes down
_.each(document.getElementsByTagName('textarea'), function(elem) {
$timeout(function(){elem.blur();});
});
}
};
$rootScope.hideSidebar = function() {
if (utils.isMobileUi()) {
document.getElementById('sidebar').setAttribute('data-state', 'hidden');
document.getElementById('content').setAttribute('sidebar-state', 'hidden');
}
};
settings.addCallback('autoconnect', function(autoconnect) {
if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
$scope.connect();
}
});
// toggle sidebar (if on mobile)
$scope.toggleSidebar = function() {
if (utils.isMobileUi()) {
if ($scope.isSidebarVisible()) {
$scope.hideSidebar();
} else {
$scope.showSidebar();
}
}
};
// Open and close panels while on mobile devices through swiping
$scope.openNick = function() {
if (utils.isMobileUi()) {
if (settings.nonicklist) {
settings.nonicklist = false;
}
}
};
$scope.closeNick = function() {
if (utils.isMobileUi()) {
if (!settings.nonicklist) {
settings.nonicklist = true;
}
}
};
// Watch model and update channel sorting when it changes
settings.addCallback('orderbyserver', function(orderbyserver) {
$rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number';
});
settings.addCallback('useFavico', function(useFavico) {
// this check is necessary as this is called on page load, too
if (!$rootScope.connected) {
return;
}
if (useFavico) {
notifications.updateFavico();
} else {
$rootScope.favico.reset();
}
});
// To prevent unnecessary loading times for users who don't
// want MathJax, load it only if the setting is enabled.
// This also fires when the page is loaded if enabled.
settings.addCallback('enableMathjax', function(enabled) {
if (enabled && !$rootScope.mathjax_init) {
// Load MathJax only once
$rootScope.mathjax_init = true;
(function () {
var head = document.getElementsByTagName("head")[0], script;
script = document.createElement("script");
script.type = "text/x-mathjax-config";
script[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({\n" +
" tex2jax: { inlineMath: [['$$','$$'], ['\\\\(','\\\\)']], displayMath: [['\\\\[','\\\\]']] },\n" +
"});";
head.appendChild(script);
script = document.createElement("script");
script.type = "text/javascript";
script.src = "//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML";
head.appendChild(script);
})();
}
});
// Inject theme CSS
settings.addCallback('theme', function(theme) {
// Unload old theme
var oldThemeCSS = document.getElementById("themeCSS");
if (oldThemeCSS) {
oldThemeCSS.parentNode.removeChild(oldThemeCSS);
}
// Load new theme
(function() {
var elem = document.createElement("link");
elem.rel = "stylesheet";
elem.href = "css/themes/" + theme + ".css";
elem.media = "screen";
elem.id = "themeCSS";
document.getElementsByTagName("head")[0].appendChild(elem);
})();
});
settings.addCallback('customCSS', function(css) {
// We need to delete the old tag and add a new one so that the browser
// notices the change. Thus, first remove old custom CSS.
var old_css = document.getElementById('custom-css-tag');
if (old_css) {
old_css.parentNode.removeChild(old_css);
}
// Create new CSS tag
var new_css = document.createElement("style");
new_css.type = "text/css";
new_css.id = "custom-css-tag";
new_css.appendChild(document.createTextNode(css));
// Append it to the <head> tag
var heads = document.getElementsByTagName("head");
heads[0].appendChild(new_css);
});
// Update font family when changed
settings.addCallback('fontfamily', function(fontfamily) {
utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily);
});
// Update font size when changed
settings.addCallback('fontsize', function(fontsize) {
utils.changeClassStyle('favorite-font', 'fontSize', fontsize);
});
$scope.setActiveBuffer = function(bufferId, key) {
// If we are on mobile we need to collapse the menu on sidebar clicks
// We use 968 px as the cutoff, which should match the value in glowingbear.css
if (utils.isMobileUi()) {
$scope.hideSidebar();
}
// Clear the hotlist for this buffer, because presumable you have read
// the messages in this buffer before you switched to the new one
// this is only needed with new type of clearing since in the old
// way WeeChat itself takes care of that part
if (models.version[0] >= 1) {
connection.sendHotlistClear();
}
return models.setActiveBuffer(bufferId, key);
};
$scope.openBuffer = function(bufferName) {
var fullName = models.getActiveBuffer().fullName;
fullName = fullName.substring(0, fullName.lastIndexOf('.') + 1) + bufferName; // substitute the last part
if (!$scope.setActiveBuffer(fullName, 'fullName')) {
// WeeChat 0.4.0+ supports /join -noswitch
// As Glowing Bear requires 0.4.2+, we don't need to check the version
var command = 'join -noswitch';
// Check if it's a query and we need to use /query instead
if (['#', '&', '+', '!'].indexOf(bufferName.charAt(0)) < 0) { // these are the characters a channel name can start with (RFC 2813-2813)
command = 'query';
// WeeChat 1.2+ supports /query -noswitch. See also #577 (different context)
if ((models.version[0] == 1 && models.version[1] >= 2) || models.version[1] > 1) {
command += " -noswitch";
}
}
connection.sendMessage('/' + command + ' ' + bufferName);
}
};
//XXX this does not belong here (or does it?)
// Calculate number of lines to fetch
$scope.calculateNumLines = function() {
var bufferlineElements = document.querySelectorAll(".bufferline");
var lineHeight = 0, idx = 0;
while (lineHeight === 0 && idx < bufferlineElements.length) {
lineHeight = bufferlineElements[idx++].clientHeight;
}
var areaHeight = document.querySelector("#bufferlines").clientHeight;
// Fetch 10 lines more than theoretically needed so that scrolling up will correctly trigger the loading of more lines
// Also, some lines might be hidden, so it's probably better to have a bit of buffer there
var numLines = Math.ceil(areaHeight/lineHeight + 10);
$scope.lines_per_screen = numLines;
};
$scope.calculateNumLines();
// get animationframe method
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
// Recalculate number of lines on resize
window.addEventListener("resize", _.debounce(function() {
// Recalculation fails when not connected
if ($rootScope.connected) {
// Show the sidebar if switching away from mobile view, hide it when switching to mobile
// Wrap in a condition so we save ourselves the $apply if nothing changes (50ms or more)
if ($scope.wasMobileUi && !utils.isMobileUi()) {
$scope.showSidebar();
}
$scope.wasMobileUi = utils.isMobileUi();
$scope.calculateNumLines();
// if we're scrolled to the bottom, scroll down to the same position after the resize
// most common use case: opening the keyboard on a mobile device
if ($rootScope.bufferBottom) {
var rescroll = function(){
$rootScope.updateBufferBottom(true);
};
$timeout(rescroll, 500);
window.requestAnimationFrame(rescroll);
}
}
}, 100));
$rootScope.loadingLines = false;
$scope.fetchMoreLines = function(numLines) {
if (!numLines) {
numLines = $scope.lines_per_screen;
}
return connection.fetchMoreLines(numLines);
};
$scope.infiniteScroll = function() {
// Check if we are already fetching
if ($rootScope.loadingLines) {
return;
}
var buffer = models.getActiveBuffer();
if (!buffer.allLinesFetched) {
$scope.fetchMoreLines();
}
};
$rootScope.updateBufferBottom = function(bottom) {
var eob = document.getElementById("end-of-buffer");
var bl = document.getElementById('bufferlines');
if (bottom) {
eob.scrollIntoView();
}
$rootScope.bufferBottom = eob.offsetTop <= bl.scrollTop + bl.clientHeight;
};
$rootScope.scrollWithBuffer = function(scrollToReadmarker, moreLines) {
// First, get scrolling status *before* modification
// This is required to determine where we were in the buffer pre-change
var bl = document.getElementById('bufferlines');
var sVal = bl.scrollHeight - bl.clientHeight;
var scroll = function() {
var sTop = bl.scrollTop;
// Determine if we want to scroll at all
// Give the check 3 pixels of slack so you don't have to hit
// the exact spot. This fixes a bug in some browsers
if (((scrollToReadmarker || moreLines) && sTop < sVal) || (Math.abs(sTop - sVal) < 3)) {
var readmarker = document.querySelector(".readmarker");
if (scrollToReadmarker && readmarker) {
// Switching channels, scroll to read marker
bl.scrollTop = readmarker.offsetTop - readmarker.parentElement.scrollHeight + readmarker.scrollHeight;
} else if (moreLines) {
// We fetched more lines but the read marker is still out of view
// Keep the scroll position constant
bl.scrollTop = bl.scrollHeight - bl.clientHeight - sVal;
} else {
// New message, scroll with buffer (i.e. to bottom)
var eob = document.getElementById("end-of-buffer");
eob.scrollIntoView();
}
$rootScope.updateBufferBottom();
}
};
// Here be scrolling dragons
$timeout(scroll);
window.requestAnimationFrame(scroll);
};
$scope.connect = function() {
notifications.requestNotificationPermission();
$rootScope.sslError = false;
$rootScope.securityError = false;
$rootScope.errorMessage = false;
$rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting';
$scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin';
connection.connect(settings.host, settings.port, $scope.password, settings.ssl);
};
$scope.disconnect = function() {
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
connection.disconnect();
};
$scope.reconnect = function() {
var bufferId = models.getActiveBuffer().id;
connection.attemptReconnect(bufferId, 3000);
};
//XXX this is a bit out of place here, either move up to the rest of the firefox install code or remove
$scope.install = function() {
if (navigator.mozApps !== undefined) {
// Find absolute url with trailing '/' or '/index.html' removed
var base_url = location.protocol + '//' + location.host +
location.pathname.replace(/\/(index\.html)?$/, '');
var request = navigator.mozApps.install(base_url + '/manifest.webapp');
request.onsuccess = function () {
$scope.isinstalled = true;
// Save the App object that is returned
var appRecord = this.result;
// Start the app.
appRecord.launch();
alert('Installation successful!');
};
request.onerror = function () {
// Display the error information from the DOMError object
alert('Install failed, error: ' + this.error.name);
};
} else {
alert('Sorry. Only supported in Firefox v26+');
}
};
$scope.showModal = function(elementId) {
document.getElementById(elementId).setAttribute('data-state', 'visible');
};
$scope.closeModal = function($event) {
function closest(elem, selector) {
var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
while (elem) {
if (matchesSelector.call(elem, selector)) return elem;
else elem = elem.parentElement;
}
}
closest($event.target, '.gb-modal').setAttribute('data-state', 'hidden');
};
$scope.toggleAccordion = function(event) {
event.stopPropagation();
event.preventDefault();
var target = event.target.parentNode.parentNode.parentNode;
target.setAttribute('data-state', target.getAttribute('data-state') === 'active' ? 'collapsed' : 'active');
// Hide all other siblings
var siblings = target.parentNode.children;
for (var childId in siblings) {
var child = siblings[childId];
if (child.nodeType === 1 && child !== target) {
child.setAttribute('data-state', 'collapsed');
}
}
};
//XXX what do we do with this?
$scope.hasUnread = function(buffer) {
// if search is set, return every buffer
if ($scope.search && $scope.search !== "") {
return true;
}
if (settings.onlyUnread) {
// Always show current buffer in list
if (models.getActiveBuffer() === buffer) {
return true;
}
// Always show core buffer in the list (issue #438)
// Also show server buffers in hierarchical view
if (buffer.fullName === "core.weechat" || (settings.orderbyserver && buffer.type === 'server')) {
return true;
}
return (buffer.unread > 0 || buffer.notification > 0) && !buffer.hidden;
}
return !buffer.hidden;
};
// Watch model and update show setting when it changes
settings.addCallback('nonicklist', function() {
$scope.showNicklist = $scope.updateShowNicklist();
// restore bottom view
if ($rootScope.connected && $rootScope.bufferBottom) {
$timeout(function(){
$rootScope.updateBufferBottom(true);
}, 500);
}
});
$scope.showNicklist = false;
// Utility function that template can use to check if nicklist should
// be displayed for current buffer or not
// is called on buffer switch
$scope.updateShowNicklist = function() {
var ab = models.getActiveBuffer();
if (!ab) {
return false;
}
// Check if option no nicklist is set
if (settings.nonicklist) {
return false;
}
// Check if nicklist is empty
if (ab.isNicklistEmpty()) {
return false;
}
return true;
};
//XXX not sure whether this belongs here
$rootScope.switchToActivityBuffer = function() {
// Find next buffer with activity and switch to it
var sortedBuffers = _.sortBy($scope.getBuffers(), 'number');
var i, buffer;
// Try to find buffer with notification
for (i in sortedBuffers) {
buffer = sortedBuffers[i];
if (buffer.notification > 0) {
$scope.setActiveBuffer(buffer.id);
return; // return instead of break so that the second for loop isn't executed
}
}
// No notifications, find first buffer with unread lines instead
for (i in sortedBuffers) {
buffer = sortedBuffers[i];
if (buffer.unread > 0) {
$scope.setActiveBuffer(buffer.id);
return;
}
}
};
// Helper function since the keypress handler is in a different scope
$rootScope.toggleNicklist = function() {
settings.nonicklist = !settings.nonicklist;
};
$rootScope.switchToAdjacentBuffer = function(direction) {
// direction is +1 for next buffer, -1 for previous buffer
var sortedBuffers = _.sortBy($scope.getBuffers(), $rootScope.predicate);
var activeBuffer = models.getActiveBuffer();
var index = sortedBuffers.indexOf(activeBuffer);
if (index >= 0) {
var newBuffer = sortedBuffers[index + direction];
if (newBuffer) {
$scope.setActiveBuffer(newBuffer.id);
}
}
};
$scope.handleSearchBoxKey = function($event) {
// Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode;
// Handle escape
if (code === 27) {
$event.preventDefault();
$scope.search = '';
} // Handle enter
else if (code === 13) {
$event.preventDefault();
if ($scope.filteredBuffers.length > 0) {
$scope.setActiveBuffer($scope.filteredBuffers[0].id);
}
$scope.search = '';
}
};
$rootScope.supports_formatting_date = (function() {
// function toLocaleDateStringSupportsLocales taken from MDN:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString#Checking_for_support_for_locales_and_options_arguments
try {
new Date().toLocaleDateString('i');
} catch (e) {
if (e.name !== 'RangeError') {
$log.info("Browser does not support toLocaleDateString()," +
" falling back to en-US");
}
return e.name === 'RangeError';
}
$log.info("Browser does not support toLocaleDateString()," +
" falling back to en-US");
return false;
})();
// Prevent user from accidentally leaving the page
window.onbeforeunload = function(event) {
if ($scope.command !== null && $scope.command !== '') {
event.preventDefault();
// Chrome requires this
// Firefox does not show the site provides message
event.returnValue = "Any unsent input will be lost. Are you sure that you want to quit?";
} else {
if ($rootScope.connected) {
$scope.disconnect();
}
$scope.favico.reset();
}
};
$scope.init = function() {
if (window.location.hash) {
var rawStr = atob(window.location.hash.substring(1));
window.location.hash = "";
var spl = rawStr.split(":");
var host = spl[0];
var port = parseInt(spl[1]);
var password = spl[2];
var ssl = spl.length > 3;
notifications.requestNotificationPermission();
$rootScope.sslError = false;
$rootScope.securityError = false;
$rootScope.errorMessage = false;
$rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
connection.connect(host, port, password, ssl);
}
};
}]);
weechat.config(['$routeProvider',
function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'index.html',
controller: 'WeechatCtrl'
});
}
]);
})();

View file

@ -1,451 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) {
var handleVersionInfo = function(message) {
var content = message.objects[0].content;
var version = content.value;
// Store the WeeChat version in models
// this eats things like 1.3-dev -> [1,3]
models.version = version.split(".").map(function(c) { return parseInt(c); });
};
var handleConfValue = function(message) {
var infolist = message.objects[0].content;
for (var i = 0; i < infolist.length ; i++) {
var key, val;
var item = infolist[i];
for (var j = 0; j < item.length ; j++) {
var confitem = item[j];
if (confitem.full_name) {
key = confitem.full_name;
}
if (confitem.value) {
val = confitem.value;
}
}
if (key && val) {
$log.debug('Setting wconfig "' + key + '" to value "' + val + '"');
models.wconfig[key] = val;
}
}
};
var handleBufferClosing = function(message) {
var bufferMessage = message.objects[0].content[0];
var bufferId = bufferMessage.pointers[0];
models.closeBuffer(bufferId);
};
// inject a fake buffer line for date change if needed
var injectDateChangeMessageIfNeeded = function(buffer, manually, old_date, new_date) {
if (buffer.bufferType === 1) {
// Don't add date change messages to free buffers
return;
}
old_date.setHours(0, 0, 0, 0);
new_date.setHours(0, 0, 0, 0);
// Check if the date changed
if (old_date.valueOf() !== new_date.valueOf()) {
if (manually) {
// if the message that caused this date change to be sent
// would increment buffer.lastSeen, we should increment as
// well.
++buffer.lastSeen;
}
var old_date_plus_one = old_date;
old_date_plus_one.setDate(old_date.getDate() + 1);
// it's not always true that a date with time 00:00:00
// plus one day will be time 00:00:00
old_date_plus_one.setHours(0, 0, 0, 0);
var content = "\u001943"; // this colour corresponds to chat_day_change
// Add day of the week
if ($rootScope.supports_formatting_date) {
content += new_date.toLocaleDateString(window.navigator.language,
{weekday: "long"});
} else {
// Gross code that only does English dates ew gross
var dow_to_word = [
"Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday"];
content += dow_to_word[new_date.getDay()];
}
// if you're testing different date formats,
// make sure to test different locales such as "en-US",
// "en-US-u-ca-persian" (which has different weekdays, year 0, and an ERA)
// "ja-JP-u-ca-persian-n-thai" (above, diff numbering, diff text)
var extra_date_format = {
day: "numeric",
month: "long"
};
if (new_date.getYear() !== old_date.getYear()) {
extra_date_format.year = "numeric";
}
content += " (";
if ($rootScope.supports_formatting_date) {
content += new_date.toLocaleDateString(window.navigator.language,
extra_date_format);
} else {
// ew ew not more gross code
var month_to_word = [
"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"];
content += month_to_word[new_date.getMonth()] + " " + new_date.getDate().toString();
if (extra_date_format.year === "numeric") {
content += ", " + new_date.getFullYear().toString();
}
}
// Result should be something like
// Friday (November 27)
// or if the year is different,
// Friday (November 27, 2015)
// Comparing dates in javascript is beyond tedious
if (old_date_plus_one.valueOf() !== new_date.valueOf()) {
var date_diff = Math.round((new_date - old_date)/(24*60*60*1000)) + 1;
if (date_diff < 0) {
date_diff = -1*(date_diff);
if (date_diff === 1) {
content += ", 1 day before";
} else {
content += ", " + date_diff + " days before";
}
} else {
content += ", " + date_diff + " days later";
}
// Result: Friday (November 27, 5 days later)
}
content += ")";
var line = {
buffer: buffer.id,
date: new_date,
prefix: '\u001943\u2500',
tags_array: [],
displayed: true,
highlight: 0,
message: content
};
var new_message = new models.BufferLine(line);
buffer.addLine(new_message);
}
};
var handleLine = function(line, manually) {
var message = new models.BufferLine(line);
var buffer = models.getBuffer(message.buffer);
buffer.requestedLines++;
// Only react to line if its displayed
if (message.displayed) {
// Check for date change
if (buffer.lines.length > 0) {
var old_date = new Date(buffer.lines[buffer.lines.length - 1].date),
new_date = new Date(message.date);
injectDateChangeMessageIfNeeded(buffer, manually, old_date, new_date);
}
message = plugins.PluginManager.contentForMessage(message);
buffer.addLine(message);
if (manually) {
buffer.lastSeen++;
}
if (buffer.active && !manually) {
$rootScope.scrollWithBuffer();
}
if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) {
if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) {
buffer.unread++;
$rootScope.$emit('notificationChanged');
}
if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) {
buffer.notification++;
notifications.createHighlight(buffer, message);
$rootScope.$emit('notificationChanged');
}
}
}
};
var handleBufferInfo = function(message) {
var bufferInfos = message.objects[0].content;
// buffers objects
for (var i = 0; i < bufferInfos.length ; i++) {
var bufferId = bufferInfos[i].pointers[0];
var buffer = models.getBuffer(bufferId);
if (buffer !== undefined) {
// We already know this buffer
handleBufferUpdate(buffer, bufferInfos[i]);
} else {
buffer = new models.Buffer(bufferInfos[i]);
models.addBuffer(buffer);
// Switch to first buffer on startup
if (i === 0) {
models.setActiveBuffer(buffer.id);
}
}
}
};
var handleBufferUpdate = function(buffer, message) {
if (message.pointers[0] !== buffer.id) {
// this is information about some other buffer!
return;
}
// weechat properties -- short name can be changed
buffer.shortName = message.short_name;
buffer.trimmedName = buffer.shortName.replace(/^[#&+]/, '');
buffer.title = message.title;
buffer.number = message.number;
buffer.hidden = message.hidden;
// reset these, hotlist info will arrive shortly
buffer.notification = 0;
buffer.unread = 0;
buffer.lastSeen = -1;
if (message.local_variables.type !== undefined) {
buffer.type = message.local_variables.type;
buffer.indent = (['channel', 'private'].indexOf(buffer.type) >= 0);
}
if (message.notify !== undefined) {
buffer.notify = message.notify;
}
};
var handleBufferLineAdded = function(message) {
message.objects[0].content.forEach(function(l) {
handleLine(l, false);
});
};
var handleBufferOpened = function(message) {
var bufferMessage = message.objects[0].content[0];
var buffer = new models.Buffer(bufferMessage);
models.addBuffer(buffer);
};
var handleBufferTitleChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.fullName = obj.full_name;
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) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.fullName = obj.full_name;
old.shortName = obj.short_name;
// If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer
// has a short name, use a space (because the prefix will be displayed separately, and we don't want
// prefix + fullname, which would happen otherwise). Else, use null so that full_name is used
old.trimmedName = obj.short_name.replace(/^[#&+]/, '') || (obj.short_name ? ' ' : null);
old.prefix = ['#', '&', '+'].indexOf(obj.short_name.charAt(0)) >= 0 ? obj.short_name.charAt(0) : '';
// After a buffer openes we get the name change event from relay protocol
// Here we check our outgoing commands that openes a buffer and switch
// to it if we find the buffer name it the list
var position = models.outgoingQueries.indexOf(old.shortName);
if (position >= 0) {
models.outgoingQueries.splice(position, 1);
models.setActiveBuffer(old.id);
}
};
var handleBufferHidden = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.hidden = true;
};
var handleBufferUnhidden = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.hidden = false;
};
var handleBufferLocalvarChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
var localvars = obj.local_variables;
if (old !== undefined && localvars !== undefined) {
// Update indentation status
old.type = localvars.type;
old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0);
// Update serverSortKey and related variables
old.plugin = localvars.plugin;
old.server = localvars.server;
old.serverSortKey = old.plugin + "." + old.server +
(old.type === "server" ? "" : ("." + old.shortName));
}
};
var handleBufferTypeChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
// 0 = formatted (normal); 1 = free
buffer.bufferType = obj.type;
};
/*
* Handle answers to (lineinfo) messages
*
* (lineinfo) messages are specified by this client. It is request after bufinfo completes
*/
var handleLineInfo = function(message, manually) {
var lines = message.objects[0].content.reverse();
if (manually === undefined) {
manually = true;
}
lines.forEach(function(l) {
handleLine(l, manually);
});
if (message.objects[0].content.length > 0) {
// fiddle out the buffer ID and take the last line's date
var last_line =
message.objects[0].content[message.objects[0].content.length-1];
var buffer = models.getBuffer(last_line.buffer);
if (buffer.lines.length > 0) {
var last_date = new Date(buffer.lines[buffer.lines.length - 1].date);
injectDateChangeMessageIfNeeded(buffer, true, last_date, new Date());
}
}
};
/*
* Handle answers to hotlist request
*/
var handleHotlistInfo = function(message) {
if (message.objects.length === 0) {
return;
}
var hotlist = message.objects[0].content;
hotlist.forEach(function(l) {
var buffer = models.getBuffer(l.buffer);
// 1 is message
buffer.unread += l.count[1];
// 2 is private
buffer.notification += l.count[2];
// 3 is highlight
buffer.notification += l.count[3];
/* Since there is unread messages, we can guess
* what the last read line is and update it accordingly
*/
var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0);
buffer.lastSeen = buffer.lines.length - 1 - unreadSum;
});
};
/*
* Handle nicklist event
*/
var handleNicklist = function(message) {
var nicklist = message.objects[0].content;
var group = 'root';
nicklist.forEach(function(n) {
var buffer = models.getBuffer(n.pointers[0]);
if (n.group === 1) {
var g = new models.NickGroup(n);
group = g.name;
buffer.nicklist[group] = g;
} else {
var nick = new models.Nick(n);
buffer.addNick(group, nick);
}
});
};
/*
* Handle nicklist diff event
*/
var handleNicklistDiff = function(message) {
var nicklist = message.objects[0].content;
var group;
nicklist.forEach(function(n) {
var buffer = models.getBuffer(n.pointers[0]);
var d = n._diff;
if (n.group === 1) {
group = n.name;
if (group === undefined) {
var g = new models.NickGroup(n);
buffer.nicklist[group] = g;
group = g.name;
}
} else {
var nick = new models.Nick(n);
if (d === 43) { // +
buffer.addNick(group, nick);
} else if (d === 45) { // -
buffer.delNick(group, nick);
} else if (d === 42) { // *
buffer.updateNick(group, nick);
}
}
});
};
var eventHandlers = {
_buffer_closing: handleBufferClosing,
_buffer_line_added: handleBufferLineAdded,
_buffer_localvar_added: handleBufferLocalvarChanged,
_buffer_localvar_removed: handleBufferLocalvarChanged,
_buffer_localvar_changed: handleBufferLocalvarChanged,
_buffer_opened: handleBufferOpened,
_buffer_title_changed: handleBufferTitleChanged,
_buffer_type_changed: handleBufferTypeChanged,
_buffer_renamed: handleBufferRenamed,
_buffer_hidden: handleBufferHidden,
_buffer_unhidden: handleBufferUnhidden,
_nicklist: handleNicklist,
_nicklist_diff: handleNicklistDiff
};
$rootScope.$on('onMessage', function(event, message) {
if (_.has(eventHandlers, message.id)) {
eventHandlers[message.id](message);
} else {
$log.debug('Unhandled event received: ' + message.id);
}
});
var handleEvent = function(event) {
if (_.has(eventHandlers, event.id)) {
eventHandlers[event.id](event);
}
};
return {
handleVersionInfo: handleVersionInfo,
handleConfValue: handleConfValue,
handleEvent: handleEvent,
handleLineInfo: handleLineInfo,
handleHotlistInfo: handleHotlistInfo,
handleNicklist: handleNicklist,
handleBufferInfo: handleBufferInfo
};
}]);
})();

View file

@ -1,49 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('imgurDrop', ['connection','imgur','$rootScope', function(connection, imgur, $rootScope) {
return {
restrict: 'A',
link: function($scope, element, attr) {
var elem = element[0];
elem.ondragover = function () { this.classList.add('imgur-drop-hover'); return false; };
elem.ondragend = function () { this.classList.remove('imgur-drop-hover'); return false; };
elem.ondrop = function(e) {
// Remove hover class
this.classList.remove('imgur-drop-hover');
// Get files
var files = e.dataTransfer.files;
// Stop default behaviour
e.stopPropagation();
e.preventDefault();
// Send image url after upload
var sendImageUrl = function(imageUrl) {
// Send image
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
// Check files
if(typeof files !== "undefined" && files.length > 0) {
// Loop through files
for (var i = 0; i < files.length; i++) {
// Upload to imgur
imgur.process(files[i], sendImageUrl);
}
}
};
}
};
}]);
})();

View file

@ -1,128 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('imgur', ['$rootScope', function($rootScope) {
var process = function(image, callback) {
// Is it an image?
if (!image || !image.type.match(/image.*/)) return;
// New file reader
var reader = new FileReader();
// When image is read
reader.onload = function (event) {
var image = event.target.result.split(',')[1];
upload(image, callback);
};
// Read image as data url
reader.readAsDataURL(image);
};
// Upload image to imgur from base64
var upload = function( base64img, callback ) {
// Set client ID (Glowing Bear)
var clientId = "164efef8979cd4b";
// Progress bars container
var progressBars = document.getElementById("imgur-upload-progress"),
currentProgressBar = document.createElement("div");
// Set progress bar attributes
currentProgressBar.className='imgur-progress-bar';
currentProgressBar.style.width = '0';
// Append progress bar
progressBars.appendChild(currentProgressBar);
// Create new form data
var fd = new FormData();
fd.append("image", base64img); // Append the file
fd.append("type", "base64"); // Set image type to base64
// Create new XMLHttpRequest
var xhttp = new XMLHttpRequest();
// Post request to imgur api
xhttp.open("POST", "https://api.imgur.com/3/image", true);
// Set headers
xhttp.setRequestHeader("Authorization", "Client-ID " + clientId);
xhttp.setRequestHeader("Accept", "application/json");
// Handler for response
xhttp.onload = function() {
// Remove progress bar
currentProgressBar.parentNode.removeChild(currentProgressBar);
// Check state and response status
if(xhttp.status === 200) {
// Get response text
var response = JSON.parse(xhttp.responseText);
// Send link as message
if( response.data && response.data.link ) {
if (callback && typeof(callback) === "function") {
callback(response.data.link);
}
} else {
showErrorMsg();
}
} else {
showErrorMsg();
}
};
if( "upload" in xhttp ) {
// Set progress
xhttp.upload.onprogress = function (event) {
// Check if we can compute progress
if (event.lengthComputable) {
// Complete in percent
var complete = (event.loaded / event.total * 100 | 0);
// Set progress bar width
currentProgressBar.style.width = complete + '%';
}
};
}
// Send request with form data
xhttp.send(fd);
};
var showErrorMsg = function() {
// Show error msg
$rootScope.uploadError = true;
$rootScope.$apply();
// Hide after 5 seconds
setTimeout(function(){
// Hide error msg
$rootScope.uploadError = false;
$rootScope.$apply();
}, 5000);
};
return {
process: process
};
}]);
})();

View file

@ -1,489 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('inputBar', function() {
return {
templateUrl: 'directives/input.html',
scope: {
inputId: '@inputId',
command: '=command'
},
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'imgur', '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
imgur,
models,
IrcUtils,
settings) {
// E.g. Turn :smile: into the unicode equivalent
$scope.inputChanged = function() {
$scope.command = emojione.shortnameToUnicode($scope.command);
};
/*
* Returns the input element
*/
$scope.getInputNode = function() {
return document.querySelector('textarea#' + $scope.inputId);
};
$scope.hideSidebar = function() {
$rootScope.hideSidebar();
};
$scope.completeNick = function() {
// input DOM node
var inputNode = $scope.getInputNode();
// get current caret position
var caretPos = inputNode.selectionStart;
// get current active buffer
var activeBuffer = models.getActiveBuffer();
// Empty input makes $scope.command undefined -- use empty string instead
var input = $scope.command || '';
// complete nick
var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate,
activeBuffer.getNicklistByTime(), ':');
// remember iteration candidate
$scope.iterCandidate = nickComp.iterCandidate;
// update current input
$scope.command = nickComp.text;
// update current caret position
setTimeout(function() {
inputNode.focus();
inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos);
}, 0);
};
$rootScope.insertAtCaret = function(toInsert) {
// caret position in the input bar
var inputNode = $scope.getInputNode(),
caretPos = inputNode.selectionStart;
var prefix = $scope.command.substring(0, caretPos),
suffix = $scope.command.substring(caretPos, $scope.command.length);
// Add spaces if missing
if (prefix.length > 0 && prefix[prefix.length - 1] !== ' ') {
prefix += ' ';
}
if (suffix.length > 0 && suffix[0] !== ' ') {
suffix = ' '.concat(suffix);
}
$scope.command = prefix + toInsert + suffix;
setTimeout(function() {
inputNode.focus();
var pos = $scope.command.length - suffix.length;
inputNode.setSelectionRange(pos, pos);
// force refresh?
$scope.$apply();
}, 0);
};
$scope.uploadImage = function($event, files) {
// Send image url after upload
var sendImageUrl = function(imageUrl) {
// Send image
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
if(typeof files !== "undefined" && files.length > 0) {
// Loop through files
for (var i = 0; i < files.length; i++) {
// Process image
imgur.process(files[i], sendImageUrl);
}
}
};
// Send the message to the websocket
$scope.sendMessage = function() {
//XXX Use a signal here
var ab = models.getActiveBuffer();
// It's undefined early in the lifecycle of the program.
// Don't send empty commands
if($scope.command !== undefined && $scope.command !== '') {
// log to buffer history
ab.addToHistory($scope.command);
// Split the command into multiple commands based on line breaks
_.each($scope.command.split(/\r?\n/), function(line) {
// Ask before a /quit
if (line === '/quit' || line.indexOf('/quit ') === 0) {
if (!window.confirm("Are you sure you want to quit WeeChat? This will prevent you from connecting with Glowing Bear until you restart WeeChat on the command line!")) {
// skip this line
return;
}
}
connection.sendMessage(line);
});
// Check for /clear command
if ($scope.command === '/buffer clear' || $scope.command === '/c') {
$log.debug('Clearing lines');
ab.clear();
}
// Check against a list of commands that opens a new
// buffer and save the name of the buffer so we can
// also automatically switch to the new buffer in gb
var opencommands = ['/query', '/join', '/j', '/q'];
var spacepos = $scope.command.indexOf(' ');
var firstword = $scope.command.substr(0, spacepos);
var index = opencommands.indexOf(firstword);
if (index >= 0) {
var queryName = $scope.command.substring(spacepos + 1);
// Cache our queries so when a buffer gets opened we can open in UI
models.outgoingQueries.push(queryName);
}
// Empty the input after it's sent
$scope.command = '';
}
// New style clearing requires this, old does not
if (models.version[0] >= 1) {
connection.sendHotlistClear();
}
$scope.getInputNode().focus();
};
//XXX THIS DOES NOT BELONG HERE!
$rootScope.addMention = function(prefix) {
// Extract nick from bufferline prefix
var nick = prefix[prefix.length - 1].text;
var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string
var addColon = newValue.length === 0;
if (newValue.length > 0) {
// Try to determine if it's a sequence of nicks
var trimmedValue = newValue.trim();
if (trimmedValue.charAt(trimmedValue.length - 1) === ':') {
// get last word
var lastSpace = trimmedValue.lastIndexOf(' ') + 1;
var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1);
var nicklist = models.getActiveBuffer().getNicklistByTime();
// check against nicklist to see if it's a list of highlights
for (var index in nicklist) {
if (nicklist[index].name === lastWord) {
// It's another highlight!
newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' ';
addColon = true;
break;
}
}
}
// Add a space before the nick if there isn't one already
// Last char might have changed above, so re-check
if (newValue.charAt(newValue.length - 1) !== ' ') {
newValue += ' ';
}
}
// Add highlight to nicklist
newValue += nick;
if (addColon) {
newValue += ': ';
}
$scope.command = newValue;
$scope.getInputNode().focus();
};
// Handle key presses in the input bar
$rootScope.handleKeyPress = function($event) {
// don't do anything if not connected
if (!$rootScope.connected) {
return true;
}
var inputNode = $scope.getInputNode();
// Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode;
// Safari doesn't implement DOM 3 input events yet as of 8.0.6
var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false;
// Mac OSX behaves differntly for altgr, so we check for that
if (altg) {
// We don't handle any anything with altgr
return false;
}
// reset quick keys display
$rootScope.showQuickKeys = false;
// any other key than Tab resets nick completion iteration
var tmpIterCandidate = $scope.iterCandidate;
$scope.iterCandidate = null;
// Left Alt+[0-9] -> jump to buffer
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) {
if (code === 48) {
code = 58;
}
var bufferNumber = code - 48 - 1 ;
var activeBufferId;
// quick select filtered entries
if (($scope.$parent.search.length || $scope.$parent.onlyUnread) && $scope.$parent.filteredBuffers.length) {
var filteredBufferNum = $scope.$parent.filteredBuffers[bufferNumber];
if (filteredBufferNum !== undefined) {
activeBufferId = [filteredBufferNum.number, filteredBufferNum.id];
}
} else {
// Map the buffers to only their numbers and IDs so we don't have to
// copy the entire (possibly very large) buffer object, and then sort
// the buffers according to their WeeChat number
var sortedBuffers = _.map(models.getBuffers(), function(buffer) {
return [buffer.number, buffer.id];
}).sort(function(left, right) {
// By default, Array.prototype.sort() sorts alphabetically.
// Pass an ordering function to sort by first element.
return left[0] - right[0];
});
activeBufferId = sortedBuffers[bufferNumber];
}
if (activeBufferId) {
$scope.$parent.setActiveBuffer(activeBufferId[1]);
$event.preventDefault();
}
}
// Tab -> nick completion
if (code === 9 && !$event.altKey && !$event.ctrlKey) {
$event.preventDefault();
$scope.iterCandidate = tmpIterCandidate;
$scope.completeNick();
return true;
}
// Left Alt+n -> toggle nicklist
if ($event.altKey && !$event.ctrlKey && code === 78) {
$event.preventDefault();
$rootScope.toggleNicklist();
return true;
}
// Alt+A -> switch to buffer with activity
if ($event.altKey && (code === 97 || code === 65)) {
$event.preventDefault();
$rootScope.switchToActivityBuffer();
return true;
}
// Alt+Arrow up/down -> switch to prev/next adjacent buffer
if ($event.altKey && !$event.ctrlKey && (code === 38 || code === 40)) {
$event.preventDefault();
var direction = code - 39;
$rootScope.switchToAdjacentBuffer(direction);
return true;
}
// Alt+L -> focus on input bar
if ($event.altKey && (code === 76 || code === 108)) {
$event.preventDefault();
inputNode.focus();
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
return true;
}
// Alt+< -> switch to previous buffer
if ($event.altKey && (code === 60 || code === 226)) {
var previousBuffer = models.getPreviousBuffer();
if (previousBuffer) {
models.setActiveBuffer(previousBuffer.id);
$event.preventDefault();
return true;
}
}
// Double-tap Escape -> disconnect
if (code === 27) {
$event.preventDefault();
// Check if a modal is visible. If so, close it instead of disconnecting
var modals = document.querySelectorAll('.gb-modal');
for (var modalId = 0; modalId < modals.length; modalId++) {
if (modals[modalId].getAttribute('data-state') === 'visible') {
modals[modalId].setAttribute('data-state', 'hidden');
return true;
}
}
if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) {
// Double-tap
connection.disconnect();
}
$scope.lastEscape = Date.now();
return true;
}
// Alt+G -> focus on buffer filter input
if ($event.altKey && (code === 103 || code === 71)) {
$event.preventDefault();
if (!$scope.$parent.isSidebarVisible()) {
$scope.$parent.showSidebar();
}
setTimeout(function() {
document.getElementById('bufferFilter').focus();
});
return true;
}
var caretPos;
// Arrow up -> go up in history
if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) {
caretPos = inputNode.selectionStart;
if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) {
return false;
}
$scope.command = models.getActiveBuffer().getHistoryUp($scope.command);
// Set cursor to last position. Need 0ms timeout because browser sets cursor
// position to the beginning after this key handler returns.
setTimeout(function() {
if ($scope.command) {
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
}
}, 0);
return true;
}
// Arrow down -> go down in history
if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) {
caretPos = inputNode.selectionStart;
if ($scope.command.slice(caretPos).indexOf("\n") !== -1) {
return false;
}
$scope.command = models.getActiveBuffer().getHistoryDown($scope.command);
// We don't need to set the cursor to the rightmost position here, the browser does that for us
return true;
}
// Enter to submit, shift-enter for newline
if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) {
$event.preventDefault();
$scope.sendMessage();
return true;
}
var bufferlines = document.getElementById("bufferlines");
var lines;
var i;
// Page up -> scroll up
if ($event.type === "keydown" && code === 33 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) {
if (bufferlines.scrollTop === 0) {
if (!$rootScope.loadingLines) {
$scope.$parent.fetchMoreLines();
}
return true;
}
lines = bufferlines.querySelectorAll("tr");
for (i = lines.length - 1; i >= 0; i--) {
if ((lines[i].offsetTop-bufferlines.scrollTop)<bufferlines.clientHeight/2) {
lines[i].scrollIntoView(false);
break;
}
}
return true;
}
// Page down -> scroll down
if ($event.type === "keydown" && code === 34 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) {
lines = bufferlines.querySelectorAll("tr");
for (i = 0; i < lines.length; i++) {
if ((lines[i].offsetTop-bufferlines.scrollTop)>bufferlines.clientHeight/2) {
lines[i].scrollIntoView(true);
break;
}
}
return true;
}
// Some readline keybindings
if (settings.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) {
// get current caret position
caretPos = inputNode.selectionStart;
// Ctrl-a
if (code == 65) {
inputNode.setSelectionRange(0, 0);
// Ctrl-e
} else if (code == 69) {
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
// Ctrl-u
} else if (code == 85) {
$scope.command = $scope.command.slice(caretPos);
setTimeout(function() {
inputNode.setSelectionRange(0, 0);
});
// Ctrl-k
} else if (code == 75) {
$scope.command = $scope.command.slice(0, caretPos);
setTimeout(function() {
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
});
// Ctrl-w
} else if (code == 87) {
var trimmedValue = $scope.command.slice(0, caretPos);
var lastSpace = trimmedValue.lastIndexOf(' ') + 1;
$scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length);
setTimeout(function() {
inputNode.setSelectionRange(lastSpace, lastSpace);
});
} else {
return false;
}
$event.preventDefault();
return true;
}
// Alt key down -> display quick key legend
if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey) {
$rootScope.showQuickKeys = true;
}
};
$rootScope.handleKeyRelease = function($event) {
// Alt key up -> remove quick key legend
if ($event.keyCode === 18) {
if ($rootScope.quickKeysTimer !== undefined) {
clearTimeout($rootScope.quickKeysTimer);
}
$rootScope.quickKeysTimer = setTimeout(function() {
if ($rootScope.showQuickKeys) {
$rootScope.showQuickKeys = false;
$rootScope.$apply();
}
delete $rootScope.quickKeysTimer;
}, 1000);
return true;
}
};
}]
};
});
})();

View file

@ -1,228 +0,0 @@
/**
* Portable utilities for IRC.
*/
(function() {
'use strict';
var IrcUtils = angular.module('IrcUtils', []);
IrcUtils.service('IrcUtils', [function() {
/**
* Escape a string for usage in a larger regexp
* @param str String to escape
* @return Escaped string
*/
var escapeRegExp = function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
/**
* Get a new version of a nick list, sorted by last speaker
*
* @param nickList Original nick list
* @return Sorted nick list
*/
var _ciNickList = function(nickList) {
var newList = _(nickList).sortBy(function(nickObj) {
return -nickObj.spokeAt;
});
newList = _(newList).pluck('name');
return newList;
};
/**
* Completes a single nick.
*
* @param candidate What to search for
* @param nickList Array of current nicks sorted for case insensitive searching
* @return Completed nick (null if not found)
*/
var _completeSingleNick = function(candidate, nickList) {
var foundNick = null;
nickList.some(function(nick) {
if (nick.toLowerCase().search(candidate.toLowerCase()) === 0) {
// found!
foundNick = nick;
return true;
}
return false;
});
return foundNick;
};
/**
* Get the next nick when iterating nicks.
*
* @param iterCandidate First characters to look at
* @param currentNick Current selected nick
* @param nickList Array of current nicks sorted for case insensitive searching
* @return Next nick (may be the same)
*/
var _nextNick = function(iterCandidate, currentNick, nickList) {
var matchingNicks = [];
var at = null;
var lcIterCandidate = iterCandidate.toLowerCase();
var lcCurrentNick = currentNick.toLowerCase();
// collect matching nicks
for (var i = 0; i < nickList.length; ++i) {
var lcNick = nickList[i].toLowerCase();
if (lcNick.search(escapeRegExp(lcIterCandidate)) === 0) {
matchingNicks.push(nickList[i]);
if (lcCurrentNick === lcNick) {
at = matchingNicks.length - 1;
}
}
/* Since we aren't sorted any more torhve disabled this:
else if (matchingNicks.length > 0) {
// end of group, no need to check after this
//break;
}
*/
}
if (at === null || matchingNicks.length === 0) {
return currentNick;
} else {
++at;
if (at === matchingNicks.length) {
// cycle
at = 0;
}
return matchingNicks[at];
}
};
/**
* Nicks tab completion.
*
* @param text Plain text (no colors)
* @param caretPos Current caret position (0 means before the first character)
* @param iterCandidate Current iteration candidate (null if not iterating)
* @param nickList Array of current nicks
* @param suf Custom suffix (at least one character, escaped for regex)
* @return Object with following properties:
* text: new complete replacement text
* caretPos: new caret position within new text
* foundNick: completed nick (or null if not possible)
* iterCandidate: current iterating candidate
*/
var completeNick = function(text, caretPos, iterCandidate, nickList, suf) {
var doIterate = (iterCandidate !== null);
if (suf === null) {
suf = ':';
}
// new nick list to search in
var searchNickList = _ciNickList(nickList);
// text before and after caret
var beforeCaret = text.substring(0, caretPos);
var afterCaret = text.substring(caretPos);
// default: don't change anything
var ret = {
text: text,
caretPos: caretPos,
foundNick: null,
iterCandidate: null
};
// iterating nicks at the beginning?
var m = beforeCaret.match(new RegExp('^([a-zA-Z0-9_\\\\\\[\\]{}^`|-]+)' + suf + ' $'));
var newNick = null;
if (m) {
if (doIterate) {
// try iterating
newNick = _nextNick(iterCandidate, m[1], searchNickList);
beforeCaret = newNick + suf + ' ';
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: iterCandidate
};
} else {
// if not iterating, don't do anything
return ret;
}
}
// nick completion in the beginning?
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = _completeSingleNick(escapeRegExp(m[1]), searchNickList);
if (newNick === null) {
// no match
return ret;
}
beforeCaret = newNick + suf + ' ';
if (afterCaret[0] === ' ') {
// swallow first space after caret if any
afterCaret = afterCaret.substring(1);
}
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: m[1]
};
}
// iterating nicks in the middle?
m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+) $/);
if (m) {
if (doIterate) {
// try iterating
newNick = _nextNick(iterCandidate, m[2], searchNickList);
beforeCaret = m[1] + newNick + ' ';
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: iterCandidate
};
} else {
// if not iterating, don't do anything
return ret;
}
}
// nick completion elsewhere in the middle?
m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = _completeSingleNick(m[2], searchNickList);
if (newNick === null) {
// no match
return ret;
}
beforeCaret = m[1] + newNick + ' ';
if (afterCaret[0] === ' ') {
// swallow first space after caret if any
afterCaret = afterCaret.substring(1);
}
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: m[2]
};
}
// completion not possible
return ret;
};
return {
'completeNick': completeNick
};
}]);
})();

View file

@ -1,117 +0,0 @@
(function() {
'use strict';
var ls = angular.module('localStorage',[]);
ls.factory("$store", ["$parse", function($parse){
/**
* Global Vars
*/
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)
* @param res - a string that will be parsed for type
* @returns {*} - whatever the real type of stored value was
*/
parseValue: function(res) {
var val;
try {
val = JSON.parse(res);
if (val === undefined){
val = res;
}
if (val === 'true'){
val = true;
}
if (val === 'false'){
val = false;
}
if (parseFloat(val) == val && !angular.isObject(val)) {
val = parseFloat(val);
}
} catch(e){
val = res;
}
return val;
}
};
var publicMethods = {
/**
* 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){
console.log('Local Storage not supported');
}
var saver = JSON.stringify(value);
storage.setItem(key, saver);
return privateMethods.parseValue(saver);
},
/**
* 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){
return null;
}
var item = storage.getItem(key);
return privateMethods.parseValue(item);
},
/**
* 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){
return false;
}
storage.removeItem(key);
return true;
},
/**
* 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)
* @returns {*} - returns whatever the stored value is
*/
bind: function ($scope, key, def) {
if (def === undefined) {
def = '';
}
if (publicMethods.get(key) === undefined || publicMethods.get(key) === null) {
publicMethods.set(key, def);
}
$parse(key).assign($scope, publicMethods.get(key));
$scope.$watch(key, function (val) {
publicMethods.set(key, val);
}, true);
return publicMethods.get(key);
}
};
return publicMethods;
}]);
})();

View file

@ -1,596 +0,0 @@
/*
* This file contains the weechat models and various
* helper methods to work with them.
*/
(function() {
'use strict';
var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) {
// WeeChat version
this.version = null;
// WeeChat configuration values
this.wconfig = {};
// Save outgoing queries
this.outgoingQueries = [];
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
*/
this.Buffer = function(message) {
// weechat properties
var fullName = message.full_name;
var shortName = message.short_name;
var hidden = message.hidden;
// If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer
// has a short name, use a space (because the prefix will be displayed separately, and we don't want
// prefix + fullname, which would happen otherwise). Else, use null so that full_name is used
var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null);
// get channel identifier
var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : '';
var title = parseRichText(message.title);
var number = message.number;
var pointer = message.pointers[0];
var notify = 3; // Default 3 == message
var lines = [];
var requestedLines = 0;
var allLinesFetched = false;
var nicklist = {};
var history = [];
var historyPos = 0;
var active = false;
var notification = 0;
var unread = 0;
var lastSeen = -1;
// There are two kinds of types: bufferType (free vs formatted) and
// the kind of type that distinguishes queries from channels etc
var bufferType = message.type;
var type = message.local_variables.type;
var indent = (['channel', 'private'].indexOf(type) >= 0);
var plugin = message.local_variables.plugin;
var server = message.local_variables.server;
// Server buffers have this "irc.server.freenode" naming schema, which
// messes the sorting up. We need it to be "irc.freenode" instead.
var serverSortKey = plugin + "." + server +
(type === "server" ? "" : ("." + shortName));
// Lowercase it so alt+up/down traverses buffers in the same order
// angular's sortBy directive puts them in
serverSortKey = serverSortKey.toLowerCase();
// Buffer opened message does not include notify level
if (message.notify !== undefined) {
notify = message.notify;
}
var rtitle = "";
for (var i = 0; i < title.length; ++i) {
rtitle += title[i].text;
}
/*
* Adds a line to this buffer
*
* @param line the BufferLine object
* @return undefined
*/
var addLine = function(line) {
lines.push(line);
updateNickSpeak(line);
};
/*
* Adds a nick to nicklist
*/
var addNick = function(group, nick) {
if (nicklistRequested()) {
nick.spokeAt = Date.now();
nicklist[group].nicks.push(nick);
}
};
/*
* Deletes a nick from nicklist
*/
var delNick = function(group, nick) {
group = nicklist[group];
if (group === undefined) {
return;
}
group.nicks = _.filter(group.nicks, function(n) { return n.name !== nick.name;});
/*
for (i in group.nicks) {
if (group.nicks[i].name == nick.name) {
delete group.nicks[i];
break;
}
}
*/
};
/*
* Updates a nick in nicklist
*/
var updateNick = function(group, nick) {
group = nicklist[group];
if (group === undefined) {
// We are getting nicklist events for a buffer where not yet
// have populated the nicklist, so there will be nothing to
// update. Just ignore the event.
return;
}
for(var i in group.nicks) {
if (group.nicks[i].name === nick.name) {
group.nicks[i] = nick;
break;
}
}
};
/*
* Update a nick with a fresh timestamp so tab completion
* can use time to complete recent speakers
*/
var updateNickSpeak = function(line) {
// Try to find nick from prefix
var prefix = line.prefix;
if (prefix.length === 0) {
// some scripts produce lines without a prefix
return;
}
var nick = prefix[prefix.length - 1].text;
// Action / me, find the nick as the first word of the message
if (nick === " *") {
var match = line.text.match(/^(.+)\s/);
if (match) {
nick = match[1];
}
}
else if (nick === "" || nick === "=!=") {
return;
}
_.each(nicklist, function(nickGroup) {
_.each(nickGroup.nicks, function(nickObj) {
if (nickObj.name === nick) {
// Use the order the line arrive in for simplicity
// instead of using weechat's own timestamp
nickObj.spokeAt = Date.now();
}
});
});
};
/*
* Get a flat nicklist sorted by speaker time. This function is
* called for every tab key press by the user.
*
*/
var getNicklistByTime = function() {
var newlist = [];
_.each(nicklist, function(nickGroup) {
_.each(nickGroup.nicks, function(nickObj) {
newlist.push(nickObj);
});
});
newlist.sort(function(a, b) {
return a.spokeAt < b.spokeAt;
});
return newlist;
};
var addToHistory = function(line) {
var result = "";
if (historyPos !== history.length) {
// Pop cached line from history. Occurs if we submit something from history
result = history.pop();
}
history.push(line);
historyPos = history.length; // Go to end of history
return result;
};
var getHistoryUp = function(currentLine) {
if (historyPos >= history.length) {
// cache current line in history
history.push(currentLine);
}
if (historyPos <= 0 || historyPos >= history.length) {
// Can't go up from first message or from out-of-bounds index
return currentLine;
} else {
// Go up in history
historyPos--;
var line = history[historyPos];
return line;
}
};
var getHistoryDown = function(currentLine) {
if (historyPos === history.length) {
// stash on history like weechat does
if (currentLine !== undefined && currentLine !== '') {
history.push(currentLine);
historyPos++;
}
return '';
} else if (historyPos < 0 || historyPos > history.length) {
// Can't go down from out of bounds or last message
return currentLine;
} else {
historyPos++;
if (history.length > 0 && historyPos == (history.length-1)) {
// return cached line and remove from cache
return history.pop();
} else {
// Go down in history
return history[historyPos];
}
}
};
// Check if the nicklist is empty, i.e., no nicks present
// This checks for the presence of people, not whether a
// request for the nicklist has been made
var isNicklistEmpty = function() {
for (var obj in nicklist) {
if (obj !== 'root') {
return false;
}
}
return true;
};
var nicklistRequested = function() {
// If the nicklist has been requested but is empty, it
// still has a 'root' property. Check for its existence.
return nicklist.hasOwnProperty('root');
};
/* Clear all our buffer lines */
var clear = function() {
while(lines.length > 0) {
lines.pop();
}
requestedLines = 0;
};
return {
id: pointer,
fullName: fullName,
shortName: shortName,
hidden: hidden,
trimmedName: trimmedName,
prefix: prefix,
number: number,
title: title,
rtitle: rtitle,
lines: lines,
clear: clear,
requestedLines: requestedLines,
addLine: addLine,
lastSeen: lastSeen,
unread: unread,
notification: notification,
notify: notify,
nicklist: nicklist,
addNick: addNick,
delNick: delNick,
updateNick: updateNick,
getNicklistByTime: getNicklistByTime,
serverSortKey: serverSortKey,
indent: indent,
bufferType: bufferType,
type: type,
plugin: plugin,
server: server,
history: history,
addToHistory: addToHistory,
getHistoryUp: getHistoryUp,
getHistoryDown: getHistoryDown,
isNicklistEmpty: isNicklistEmpty,
nicklistRequested: nicklistRequested
};
};
/*
* BufferLine class
*/
this.BufferLine = function(message) {
var buffer = message.buffer;
var date = message.date;
var shortTime = $filter('date')(date, 'HH:mm');
var prefix = parseRichText(message.prefix);
var tags_array = message.tags_array;
var displayed = message.displayed;
var highlight = message.highlight;
var content = parseRichText(message.message);
if (highlight) {
prefix.forEach(function(textEl) {
textEl.classes.push('highlight');
});
}
var rtext = "";
for (var i = 0; i < content.length; ++i) {
rtext += content[i].text;
}
return {
prefix: prefix,
content: content,
date: date,
shortTime: shortTime,
buffer: buffer,
tags: tags_array,
highlight: highlight,
displayed: displayed,
text: rtext
};
};
function nickGetColorClasses(nickMsg, propName) {
if (propName in nickMsg && nickMsg[propName] && nickMsg[propName].length > 0) {
var color = nickMsg[propName];
if (color.match(/^weechat/)) {
// color option
var colorName = color.match(/[a-zA-Z0-9_]+$/)[0];
return [
'cof-' + colorName,
'cob-' + colorName,
'coa-' + colorName
];
} else if (color.match(/^[a-zA-Z]+$/)) {
// WeeChat color name
return [
'cwf-' + color
];
} else if (color.match(/^[0-9]+$/)) {
// extended color
return [
'cef-' + color
];
}
}
return [
'cwf-default'
];
}
function nickGetClasses(nickMsg) {
return {
'name': nickGetColorClasses(nickMsg, 'color'),
'prefix': nickGetColorClasses(nickMsg, 'prefix_color')
};
}
/*
* Nick class
*/
this.Nick = function(message) {
var prefix = message.prefix;
var visible = message.visible;
var name = message.name;
var colorClasses = nickGetClasses(message);
return {
prefix: prefix,
visible: visible,
name: name,
prefixClasses: colorClasses.prefix,
nameClasses: colorClasses.name
};
};
/*
* Nicklist Group class
*/
this.NickGroup = function(message) {
var name = message.name;
var visible = message.visible;
var nicks = [];
return {
name: name,
visible: visible,
nicks: nicks
};
};
var activeBuffer = null;
var previousBuffer = null;
this.model = { 'buffers': {} };
/*
* Adds a buffer to the list
*
* @param buffer buffer object
* @return undefined
*/
this.addBuffer = function(buffer) {
this.model.buffers[buffer.id] = buffer;
};
/*
* Returns the current active buffer
*
* @return active buffer object
*/
this.getActiveBuffer = function() {
return activeBuffer;
};
/*
* Returns a reference to the currently active buffer that
* WeeChat understands without crashing, even if it's invalid
*
* @return active buffer pointer (WeeChat 1.0+) or fullname (older versions)
*/
this.getActiveBufferReference = function() {
if (this.version !== null && this.version[0] >= 1) {
// pointers are being validated, they're more reliable than
// fullName (e.g. if fullName contains spaces)
return "0x"+activeBuffer.id;
} else {
return activeBuffer.fullName;
}
};
/*
* Returns the previous current active buffer
*
* @return previous buffer object
*/
this.getPreviousBuffer = function() {
return previousBuffer;
};
/*
* Sets the buffer specifiee by bufferId as active.
* Deactivates the previous current buffer.
*
* @param bufferId id of the new active buffer
* @return true on success, false if buffer was not found
*/
this.setActiveBuffer = function(bufferId, key) {
if (key === undefined) {
key = 'id';
}
previousBuffer = this.getActiveBuffer();
if (key === 'id') {
activeBuffer = this.model.buffers[bufferId];
}
else {
activeBuffer = _.find(this.model.buffers, function(buffer) {
if (buffer[key] === bufferId) {
return buffer;
}
});
}
if (activeBuffer === undefined) {
// Buffer not found, undo assignment
activeBuffer = previousBuffer;
return false;
}
if (previousBuffer) {
// turn off the active status for the previous buffer
previousBuffer.active = false;
// Save the last line we saw
previousBuffer.lastSeen = previousBuffer.lines.length-1;
}
var unreadSum = activeBuffer.unread + activeBuffer.notification;
activeBuffer.active = true;
activeBuffer.unread = 0;
activeBuffer.notification = 0;
$rootScope.$emit('activeBufferChanged', unreadSum);
$rootScope.$emit('notificationChanged');
return true;
};
/*
* Returns the buffer list
*/
this.getBuffers = function() {
return this.model.buffers;
};
/*
* Reinitializes the model
*/
this.reinitialize = function() {
this.model.buffers = {};
};
/*
* Returns a specific buffer object
*
* @param bufferId id of the buffer
* @return the buffer object
*/
this.getBuffer = function(bufferId) {
return this.model.buffers[bufferId];
};
/*
* Closes a weechat buffer. Sets the first buffer
* as active, if the closing buffer was active before
*
* @param bufferId id of the buffer to close
* @return undefined
*/
this.closeBuffer = function(bufferId) {
var buffer = this.getBuffer(bufferId);
// Check if the buffer really exists, just in case
if (buffer === undefined) {
return;
}
if (buffer.active) {
var firstBuffer = _.keys(this.model.buffers)[0];
this.setActiveBuffer(firstBuffer);
}
// Can't use `buffer` here, needs to be deleted from the list
delete(this.model.buffers[bufferId]);
};
}]);
})();

View file

@ -1,210 +0,0 @@
var weechat = angular.module('weechat');
weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) {
var serviceworker = false;
var notifications = [];
// Ask for permission to display desktop notifications
var requestNotificationPermission = function() {
// Firefox
if (window.Notification) {
Notification.requestPermission(function(status) {
$log.info('Notification permission status: ', status);
if (Notification.permission !== status) {
Notification.permission = status;
}
});
}
// Webkit
if (window.webkitNotifications !== undefined) {
var havePermission = window.webkitNotifications.checkPermission();
if (havePermission !== 0) { // 0 is PERMISSION_ALLOWED
$log.info('Notification permission status: ', havePermission === 0);
window.webkitNotifications.requestPermission();
}
}
if ('serviceWorker' in navigator) {
$log.info('Service Worker is supported');
navigator.serviceWorker.register('serviceworker.js').then(function(reg) {
$log.info('Service Worker install:', reg);
serviceworker = true;
}).catch(function(err) {
$log.info('Service Worker err:', err);
});
}
};
var showNotification = function(buffer, title, body) {
if (serviceworker) {
navigator.serviceWorker.ready.then(function(registration) {
registration.showNotification(title, {
body: body,
icon: 'assets/img/glowing_bear_128x128.png',
vibrate: [200, 100],
tag: 'gb-highlight-vib'
});
});
} else if (typeof Windows !== 'undefined' && typeof Windows.UI !== 'undefined' && typeof Windows.UI.Notifications !== 'undefined') {
var winNotifications = Windows.UI.Notifications;
var toastNotifier = winNotifications.ToastNotificationManager.createToastNotifier();
var template = winNotifications.ToastTemplateType.toastText02;
var toastXml = winNotifications.ToastNotificationManager.getTemplateContent(template);
var toastTextElements = toastXml.getElementsByTagName("text");
toastTextElements[0].appendChild(toastXml.createTextNode(title));
toastTextElements[1].appendChild(toastXml.createTextNode(body));
var toast = new winNotifications.ToastNotification(toastXml);
toast.onactivated = function() {
models.setActiveBuffer(buffer.id);
window.focus();
};
toastNotifier.show(toast);
} else {
var notification = new Notification(title, {
body: body,
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() {
setTimeout(function() {
notification.close();
}, timeout);
};
// Click takes the user to the buffer
notification.onclick = function() {
models.setActiveBuffer(buffer.id);
window.focus();
notification.close();
};
// Remove from list of active notifications
notification.onclose = function() {
delete notifications[this.id];
};
}
};
// Reduce buffers with "+" operation over a key. Mostly useful for unread/notification counts.
var unreadCount = function(type) {
if (!type) {
type = "unread";
}
// Do this the old-fashioned way with iterating over the keys, as underscore proved to be error-prone
var keys = Object.keys(models.model.buffers);
var count = 0;
for (var key in keys) {
count += models.model.buffers[keys[key]][type];
}
return count;
};
var updateTitle = function() {
var notifications = unreadCount('notification');
if (notifications > 0) {
// New notifications deserve an exclamation mark
$rootScope.notificationStatus = '(' + notifications + ') ';
} else {
$rootScope.notificationStatus = '';
}
var activeBuffer = models.getActiveBuffer();
if (activeBuffer) {
$rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.rtitle;
}
};
var updateFavico = function() {
var notifications = unreadCount('notification');
if (notifications > 0) {
$rootScope.favico.badge(notifications, {
bgColor: '#d00',
textColor: '#fff'
});
} else {
var unread = unreadCount('unread');
if (unread === 0) {
$rootScope.favico.reset();
} else {
$rootScope.favico.badge(unread, {
bgColor: '#5CB85C',
textColor: '#ff0'
});
}
}
};
/* Function gets called from bufferLineAdded code if user should be notified */
var createHighlight = function(buffer, message) {
var title = '';
var body = '';
var numNotifications = buffer.notification;
if (buffer.type === "private") {
if (numNotifications > 1) {
title = numNotifications.toString() + ' private messages from ';
} else {
title = 'Private message from ';
}
body = message.text;
} else {
if (numNotifications > 1) {
title = numNotifications.toString() + ' highlights in ';
} else {
title = 'Highlight in ';
}
var prefix = '';
for (var i = 0; i < message.prefix.length; i++) {
prefix += message.prefix[i].text;
}
body = '<' + prefix + '> ' + message.text;
}
title += buffer.shortName + " (" + buffer.server + ")";
showNotification(buffer, title, body);
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>';
document.getElementById("soundNotification").innerHTML = soundHTML;
}
};
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,
unreadCount: unreadCount
};
}]);

View file

@ -1,86 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('plugin', ['$rootScope', 'settings', function($rootScope, settings) {
/*
* Plugin directive
* Shows additional plugin content
*/
return {
templateUrl: 'directives/plugin.html',
scope: {
plugin: '=data'
},
controller: ['$scope', function($scope) {
$scope.displayedContent = "";
// Auto-display embedded content only if it isn't 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(':','_');
$scope.plugin.getElement = function() {
return document.querySelector("." + $scope.plugin.className);
};
$scope.hideContent = function() {
$scope.plugin.visible = false;
};
$scope.showContent = function(automated) {
/*
* Shows the plugin content.
* displayedContent is bound to the DOM.
* Actual plugin content is only fetched when
* content is shown.
*/
var embed = $scope.plugin.getElement();
// If the plugin is asynchronous / lazy, execute it now and let it insert itself
// TODO store the result between channel switches
if ($scope.plugin.content instanceof Function){
// Don't rerun if the result is already there
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;
}
$scope.plugin.visible = true;
// Scroll embed content into view
var scroll;
if (automated) {
var wasBottom = $rootScope.bufferBottom;
scroll = function() {
$rootScope.updateBufferBottom(wasBottom);
};
} else {
scroll = function() {
if (embed && embed.scrollIntoViewIfNeeded !== undefined) {
embed.scrollIntoViewIfNeeded();
$rootScope.updateBufferBottom();
}
};
}
setTimeout(scroll, 500);
};
if ($scope.plugin.visible) {
$scope.showContent(true);
}
}]
};
}]);
})();

View file

@ -1,530 +0,0 @@
/*
* This file contains the plugin definitions
*/
(function() {
'use strict';
var plugins = angular.module('plugins', []);
/*
* Definition of a user provided plugin with sensible default values
*
* User plugins are created by providing a name and a contentForMessage
* function that parses a string and returns any additional content.
*/
var Plugin = function(name, contentForMessage) {
return {
contentForMessage: contentForMessage,
exclusive: false,
name: name
};
};
// Regular expression that detects URLs for UrlPlugin
var urlRegexp = /(?:(?:https?|ftp):\/\/|www\.|ftp\.)\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
};
};
/*
* This service provides access to the plugin manager
*
* The plugin manager is where the various user provided plugins
* are registered. It is responsible for finding additional content
* to display when messages are received.
*
*/
plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) {
/*
* Defines the plugin manager object
*/
var PluginManagerObject = function() {
var plugins = [];
/*
* Register the user provides plugins
*
* @param userPlugins user provided plugins
*/
var registerPlugins = function(userPlugins) {
for (var i = 0; i < userPlugins.length; i++) {
plugins.push(userPlugins[i]);
}
};
var nsfwRegexp = new RegExp('nsfw', 'i');
/*
* Iterates through all the registered plugins
* and run their contentForMessage function.
*/
var contentForMessage = function(message) {
message.metadata = [];
var addPluginContent = function(content, pluginName, num) {
if (num) {
pluginName += " " + num;
}
// If content isn't a callback, it's HTML
if (!(content instanceof Function)) {
content = $sce.trustAsHtml(content);
}
message.metadata.push({
'content': content,
'nsfw': nsfw,
'name': pluginName
});
};
for (var i = 0; i < plugins.length; i++) {
var nsfw = false;
if (message.text.match(nsfwRegexp)) {
nsfw = true;
}
var pluginContent = plugins[i].contentForMessage(
message.text
);
if (pluginContent && pluginContent !== []) {
if (pluginContent instanceof Array) {
for (var j = pluginContent.length - 1; j >= 0; j--) {
// only give a number if there are multiple embeds
var num = (pluginContent.length == 1) ? undefined : (j + 1);
addPluginContent(pluginContent[j], plugins[i].name, num);
}
} else {
addPluginContent(pluginContent, plugins[i].name);
}
if (plugins[i].exclusive) {
break;
}
}
}
return message;
};
return {
registerPlugins: registerPlugins,
contentForMessage: contentForMessage
};
};
// Instanciates and registers the plugin manager.
this.PluginManager = new PluginManagerObject();
this.PluginManager.registerPlugins(userPlugins.plugins);
}]);
/*
* This factory exposes the collection of user provided plugins.
*
* To create your own plugin, you need to:
*
* 1. Define its contentForMessage function. The contentForMessage
* function takes a string as a parameter and returns a HTML string.
*
* 2. Instantiate a Plugin object with contentForMessage function as its
* argument.
*
* 3. Add it to the plugins array.
*
*/
plugins.factory('userPlugins', function() {
// standard JSONp origin policy trick
var jsonp = function (url, callback) {
var callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
window[callbackName] = function(data) {
delete window[callbackName];
document.body.removeChild(script);
callback(data);
};
var script = document.createElement('script');
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
document.body.appendChild(script);
};
/*
* Spotify Embedded Player
*
* See: https://developer.spotify.com/technologies/widgets/spotify-play-button/
*
*/
var spotifyPlugin = new Plugin('Spotify track', function(message) {
var content = [];
var addMatch = function(match) {
for (var i = 0; match && i < match.length; i++) {
var id = match[i].substr(match[i].length - 22, match[i].length);
var element = angular.element('<iframe></iframe>')
.attr('src', '//embed.spotify.com/?uri=spotify:track:' + id)
.attr('width', '300')
.attr('height', '80')
.attr('frameborder', '0')
.attr('allowtransparency', 'true');
content.push(element.prop('outerHTML'));
}
};
addMatch(message.match(/spotify:track:([a-zA-Z-0-9]{22})/g));
addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g));
return content;
});
/*
* YouTube Embedded Player
*
* See: https://developers.google.com/youtube/player_parameters
*/
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);
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",
element = angular.element('<iframe></iframe>')
.attr('src', embedurl)
.attr('width', '560')
.attr('height', '315')
.attr('frameborder', '0')
.attr('allowfullscreen', 'true');
return element.prop('outerHTML');
}
});
/*
* Dailymotion Embedded Player
*
* See: http://www.dailymotion.com/doc/api/player.html
*/
var dailymotionPlugin = new Plugin('Dailymotion video', function(message) {
var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/;
var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/;
var rShorten = /dai.ly\/([^_?# ]+)/;
var match = message.match(rPath) || message.match(rAnchor) || message.match(rShorten);
if (match) {
var id = match[1];
var embedurl = 'https://www.dailymotion.com/embed/video/' + id + '?html&controls=html&startscreen=html&info=0&logo=0&related=0';
var element = angular.element('<iframe></iframe>')
.attr('src', embedurl)
.attr('width', '480')
.attr('height', '270')
.attr('frameborder', '0');
return element.prop('outerHTML');
}
return null;
});
/*
* AlloCine Embedded Player
*/
var allocinePlugin = new Plugin('AlloCine video', function(message) {
var rVideokast = /allocine.fr\/videokast\/video-(\d+)/;
var rCmedia = /allocine.fr\/.*cmedia=(\d+)/;
var match = message.match(rVideokast) || message.match(rCmedia);
if (match) {
var id = match[1];
var embedurl = 'http://www.allocine.fr/_video/iblogvision.aspx?cmedia=' + id;
var element = angular.element('<iframe></iframe>')
.attr('src', embedurl)
.attr('width', '480')
.attr('height', '270')
.attr('frameborder', '0');
return element.prop('outerHTML');
}
return null;
});
/*
* Image Preview
*/
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/");
} else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
// remove protocol specification to load over https if used by g-b
url = url.replace(/http:/, "");
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\//i)) {
// Dropbox requires a get parameter, dl=1
var dbox_url = document.createElement("a");
dbox_url.href = url;
var base_url = dbox_url.protocol + '//' + dbox_url.host + dbox_url.pathname + '?';
var dbox_params = dbox_url.search.substring(1).split('&');
var dl_added = false;
for (var i = 0; i < dbox_params.length; i++) {
if (dbox_params[i].split('=')[0] === "dl") {
dbox_params[i] = "dl=1";
dl_added = true;
// we continue looking at the other parameters in case
// it's specified twice or something
}
}
if (!dl_added) {
dbox_params.push("dl=1");
}
url = base_url + dbox_params.join('&');
}
return function() {
var element = this.getElement();
var imgElem = angular.element('<a></a>')
.attr('target', '_blank')
.attr('href', url)
.append(angular.element('<img>')
.addClass('embed')
.attr('src', url));
element.innerHTML = imgElem.prop('outerHTML');
};
}
});
/*
* audio Preview
*/
var audioPlugin = new UrlPlugin('audio', function(url) {
if (url.match(/\.(mp3|ogg|wav)\b/i)) {
return function() {
var element = this.getElement();
var aelement = angular.element('<audio controls></audio>')
.addClass('embed')
.attr('width', '560')
.append(angular.element('<source></source>')
.attr('src', url));
element.innerHTML = aelement.prop('outerHTML');
};
}
});
/*
* mp4 video Preview
*/
var videoPlugin = new UrlPlugin('video', function(url) {
if (url.match(/\.(mp4|webm|ogv|gifv)\b/i)) {
if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
// remove protocol specification to load over https if used by g-b
url = url.replace(/\.(gifv)\b/i, ".webm");
}
return function() {
var element = this.getElement();
var velement = angular.element('<video autoplay loop muted></video>')
.addClass('embed')
.attr('width', '560')
.append(angular.element('<source></source>')
.attr('src', url));
element.innerHTML = velement.prop('outerHTML');
};
}
});
/*
* Cloud Music Embedded Players
*/
var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) {
/* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */
var element;
if (url.match(/^https?:\/\/soundcloud.com\//)) {
element = angular.element('<iframe></iframe>')
.attr('width', '100%')
.attr('height', '120')
.attr('scrolling', 'no')
.attr('frameborder', 'no')
.attr('src', 'https://w.soundcloud.com/player/?url=' + url + '&amp;color=ff6600&amp;auto_play=false&amp;show_artwork=true');
return element.prop('outerHTML');
}
/* MixCloud */
if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) {
element = angular.element('<iframe></iframe>')
.attr('width', '480')
.attr('height', '60')
.attr('frameborder', '0')
.attr('src', '//www.mixcloud.com/widget/iframe/?feed=' + url + '&mini=1&stylecolor=&hide_artwork=&embed_type=widget_standard&hide_tracklist=1&hide_cover=');
return element.prop('outerHTML');
}
});
/*
* Google Maps
*/
var googlemapPlugin = new UrlPlugin('Google Map', function(url) {
if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) {
var element = angular.element('<iframe></iframe>')
.attr('width', '450')
.attr('height', '350')
.attr('frameborder', '0')
.attr('scrolling', 'no')
.attr('marginheight', '0')
.attr('src', url + '&output=embed');
return element.prop('outerHTML');
}
});
/*
* Asciinema plugin
*/
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[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);
};
}
});
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) {
return function() {
var element = this.getElement();
var language = match[1];
var location = match[2];
var city = match[match.length - 1].slice(0, -1);
url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png";
var ielement = angular.element('<img>')
.attr('src', url)
.attr('alt', 'Meteogram for ' + city);
element.innerHTML = ielement.prop('outerHTML');
};
}
});
// Embed GitHub gists
var gistPlugin = new UrlPlugin('Gist', function(url) {
var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i;
var match = url.match(regexp);
if (match) {
// get the URL from the match to trim away pseudo file endings and request parameters
url = match[0] + '.json';
// load gist asynchronously -- return a function here
return function() {
var element = this.getElement();
jsonp(url, function(data) {
// Add the gist stylesheet only once
if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) {
var stylesheet = '<link rel="stylesheet" href="' + data.stylesheet + '"></link>';
document.getElementsByTagName('head')[0].innerHTML += stylesheet;
}
element.innerHTML = '<div style="clear:both">' + data.div + '</div>';
});
};
}
});
/* match giphy links and display the assocaited gif images
* sample input: http://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy
* sample output: https://media.giphy.com/media/feqkVgjJpYtjy/giphy.gif
*/
var giphyPlugin = new UrlPlugin('Giphy', function(url) {
var regex = /^https?:\/\/giphy.com\/gifs\/.*-(.*)\/?/i;
// on match, id will contain the entire url in [0] and the giphy id in [1]
var id = url.match(regex);
if (id) {
var src = "https://media.giphy.com/media/" + id[1] + "/giphy.gif";
return function() {
var element = this.getElement();
var gelement = angular.element('<a></a>')
.attr('target', '_blank')
.attr('href', url)
.append(angular.element('<img>')
.addClass('embed')
.attr('src', src));
element.innerHTML = gelement.prop('outerHTML');
};
}
});
var tweetPlugin = new UrlPlugin('Tweet', function(url) {
var regexp = /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/i;
var match = url.match(regexp);
if (match) {
url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2];
return function() {
var element = this.getElement();
jsonp(url, function(data) {
// 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)
content = content.replace("<blockquote class=\"twitter-tweet\">", "<blockquote class=\"twitter-tweet\" data-dnt=\"true\">");
element.innerHTML = content;
// The script tag needs to be generated manually or the browser won't load it
var scriptElem = document.createElement('script');
// Hardcoding the URL here, I don't suppose it's going to change anytime soon
scriptElem.src = "//platform.twitter.com/widgets.js";
element.appendChild(scriptElem);
});
};
}
});
/*
* 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";
var element = angular.element('<iframe></iframe>')
.addClass('vine-embed')
.attr('src', embedurl)
.attr('width', '600')
.attr('height', '600')
.attr('frameborder', '0');
return element.prop('outerHTML') + '<script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
}
});
return {
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, giphyPlugin, tweetPlugin, vinePlugin]
};
});
})();

View file

@ -1,82 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) {
var that = this;
this.callbacks = {};
// This cache is important for two reasons. One, angular hits it up really often
// (because it needs to check for changes and it's not very clever about it).
// Two, it prevents weird type conversion issues that otherwise arise in
// $store.parseValue (e.g. converting "123." to the number 123 even though it
// actually was the beginning of an IP address that the user was in the
// process of entering)
this.cache = {};
// 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() {
if (!(key in this.cache)) {
this.cache[key] = $store.get(key);
}
return this.cache[key];
},
set: function(newVal) {
this.cache[key] = 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) {
// Define property so it will get saved to store
defineProperty(key);
// Save to settings module AND to store
this[key] = defaults[key];
}
}
};
return this;
}]);
})();

View file

@ -1,29 +0,0 @@
var weechat = angular.module('weechat');
weechat.factory('utils', function() {
// Helper to change style of a class
var changeClassStyle = function(classSelector, attr, value) {
_.each(document.getElementsByClassName(classSelector), function(e) {
e.style[attr] = value;
});
};
// Helper to get style from a class
var getClassStyle = function(classSelector, attr) {
_.each(document.getElementsByClassName(classSelector), function(e) {
return e.style[attr];
});
};
var isMobileUi = function() {
// TODO don't base detection solely on screen width
// You are right. In the meantime I am renaming isMobileDevice to isMobileUi
var mobile_cutoff = 968;
return (document.body.clientWidth < mobile_cutoff);
};
return {
changeClassStyle: changeClassStyle,
getClassStyle: getClassStyle,
isMobileUi: isMobileUi
};
});

View file

@ -1,150 +0,0 @@
(function() {
'use strict';
var websockets = angular.module('ngWebsockets', []);
websockets.factory('ngWebsockets',
['$rootScope','$q',
function($rootScope, $q) {
var protocol = null;
var ws = null;
var callbacks = {};
var currentCallBackId = 0;
/*
* Fails every currently subscribed callback for the
* given reason
*
* @param reason reason for failure
*/
var failCallbacks = function(reason) {
for (var i in callbacks) {
callbacks[i].cb.reject(reason);
}
};
/*
* Returns the current callback id
*/
var getCurrentCallBackId = function() {
currentCallBackId += 1;
if (currentCallBackId > 1000) {
currentCallBackId = 0;
}
return currentCallBackId;
};
/* Send a message to the websocket and returns a promise.
* See: http://docs.angularjs.org/api/ng.$q
*
* @param message message to send
* @returns a promise
*/
var send = function(message) {
var cb = createCallback(message);
message = protocol.setId(cb.id,
message);
ws.send(message);
return cb.promise;
};
/*
* Create a callback, adds it to the callback list
* and return it.
*/
var createCallback = function() {
var defer = $q.defer();
var cbId = getCurrentCallBackId();
callbacks[cbId] = {
time: new Date(),
cb: defer
};
defer.id = cbId;
return defer;
};
/*
* Send all messages to the websocket and returns a promise that is resolved
* when all message are resolved.
*
* @param messages list of messages
* @returns a promise
*/
var sendAll = function(messages) {
var promises = [];
for (var i in messages) {
var promise = send(messages[i]);
promises.push(promise);
}
return $q.all(promises);
};
var onmessage = function (evt) {
/*
* Receives a message on the websocket
*/
var message = protocol.parse(evt.data);
if (_.has(callbacks, message.id)) {
// see if it's bound to one of the callbacks
var promise = callbacks[message.id];
promise.cb.resolve(message);
delete(callbacks[message.id]);
} else {
// otherwise emit it
$rootScope.$emit('onMessage', message);
}
// Make sure all UI is updated with new data
$rootScope.$apply();
};
var connect = function(url,
protocol_,
properties) {
ws = new WebSocket(url);
protocol = protocol_;
for (var property in properties) {
ws[property] = properties[property];
}
if ('onmessage' in properties) {
ws.onmessage = function(event) {
properties.onmessage(event);
onmessage(event);
};
} else {
ws.onmessage = onmessage;
}
};
var disconnect = function() {
ws.close();
};
return {
send: send,
sendAll: sendAll,
connect: connect,
disconnect: disconnect,
failCallbacks: failCallbacks
};
}]);
})();

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('whenScrolled', function() {
return function(scope, elm, attr) {
var raw = elm[0];
var fun = function() {
if (raw.scrollTop === 0) {
scope.$apply(attr.whenScrolled);
}
};
elm.bind('scroll', function() {
_.debounce(fun, 200)();
});
};
});
})();

View file

@ -1,25 +0,0 @@
{
"name": "Glowing Bear",
"description": "WeeChat Web frontend",
"version": "0.6.0",
"manifest_version": 2,
"icons": {
"32": "assets/img/favicon.png",
"128": "assets/img/glowing_bear_128x128.png"
},
"app": {
"urls": [
"http://glowing-bear.github.io/glowing-bear/"
],
"launch": {
"container": "panel",
"web_url": "http://glowing-bear.github.io/glowing-bear/"
}
},
"permissions": [
"notifications"
],
"web_accessible_resources": [
"assets/img/favicon.png"
]
}

View file

@ -1,29 +0,0 @@
{
"name": "Glowing Bear",
"description": "WeeChat Web frontend",
"launch_path": "/glowing-bear/index.html",
"icons": {
"128": "/glowing-bear/assets/img/glowing_bear_128x128.png",
"60": "/glowing-bear/assets/img/glowing_bear_60x60.png",
"90": "/glowing-bear/assets/img/glowing_bear_90x90.png",
"32": "/glowing-bear/assets/img/favicon.png"
},
"installs_allowed_from": [
"*"
],
"developer": {
"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.6.0"
}

View file

@ -1,36 +0,0 @@
{
"name": "glowing-bear",
"private": true,
"version": "0.6.0",
"description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
"devDependencies": {
"bower": "^1.3.1",
"http-server": "^0.6.1",
"jasmine-core": "^2.4.1",
"jshint": "^2.5.2",
"karma": "~0.13",
"karma-jasmine": "^0.3.6",
"karma-junit-reporter": "^0.2.2",
"karma-phantomjs-launcher": "^0.2.1",
"phantomjs": "^1.9.19",
"protractor": "~0.20.1",
"shelljs": "^0.2.6",
"uglify-js": "^2.4"
},
"scripts": {
"postinstall": "bower install",
"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/file-change.js js/imgur-drop-directive.js js/whenscrolled-directive.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js js/imgur.js -c -m --screw-ie8 -o min.js --source-map min.map",
"prestart": "npm install",
"start": "http-server -a localhost -p 8000",
"pretest": "npm install",
"test": "karma start test/karma.conf.js",
"test-single-run": "karma start test/karma.conf.js --single-run",
"preupdate-webdriver": "npm install",
"update-webdriver": "webdriver-manager update",
"preprotractor": "npm run update-webdriver",
"protractor": "protractor test/protractor-conf.js",
"update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('app/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\""
}
}

View file

@ -1,2 +0,0 @@
./node_modules/.bin/jshint js/*.js test/unit/*.js
npm test

View file

@ -1,46 +0,0 @@
// File needs to be stored in the root of the app.
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'assets/img/glowing_bear_128x128.png',
]);
})
);
});
this.addEventListener('push', function(event) {
// TODO, support GCM here
var title = 'Push message';
event.waitUntil(
self.registration.showNotification(title, {
body: 'The Message',
icon: 'assets/img/favicon.png',
tag: 'my-tag'
}));
});
this.onnotificationclick = function(event) {
// Android doesn't close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({
type: "window"
}).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if ('focus' in client) {
return client.focus();
}
}
/*
if (clients.openWindow) {
return clients.openWindow('/glowing-bear/');
}
*/
}));
};

View file

@ -1,26 +0,0 @@
'use strict';
/* https://github.com/angular/protractor/blob/master/docs/getting-started.md */
describe('Auth', function() {
browser.get('index.html');
var ptor = protractor.getInstance();
it('auth should fail when trying to connect to an unused port', function() {
var host = ptor.findElement(protractor.By.model('host'));
var password = ptor.findElement(protractor.By.model('password'));
var port = ptor.findElement(protractor.By.model('port'));
var submit = ptor.findElement(protractor.By.tagName('button'));
// Fill out the form?
host.sendKeys('localhost');
password.sendKeys('password');
port.sendKeys(2462);
submit.click();
var error = ptor.findElement(
protractor.By.css('.alert.alert-danger > strong')
)
expect(error.getText()).toBeDefined();
});
});

View file

@ -1,49 +0,0 @@
module.exports = function(config){
config.set({
basePath : '../',
files : [
'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',
'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',
'test/unit/**/*.js'
],
autoWatch : true,
frameworks: ['jasmine'],
browsers : ['PhantomJS'],
singleRun: true,
plugins : [
'karma-phantomjs-launcher',
'karma-jasmine',
'karma-junit-reporter'
],
junitReporter : {
outputFile: 'test_out/unit.xml',
suite: 'unit'
}
});
};

View file

@ -1,19 +0,0 @@
exports.config = {
allScriptsTimeout: 11000,
specs: [
'e2e/*.js'
],
capabilities: {
'browserName': 'firefox'
},
baseUrl: 'http://localhost:8000/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};

View file

@ -1,95 +0,0 @@
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="openBuffer(\'#foo\');">#foo</a>');
}));
it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) {
expect(irclinkyFilter('<"#foo">')).toEqual('<"<a href="#" onclick="openBuffer(\'#foo">\');">#foo"></a>');
}));
it('should not touch links created by `linky`', inject(function(linkyFilter, DOMfilterFilter) {
var url = 'http://foo.bar/#baz',
link = linkyFilter(url, '_blank'),
result = DOMfilterFilter(link, 'irclinky').$$unwrapTrustedValue();
expect(result).toEqual(link);
}));
});
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;');
}));
});
describe('DOMfilter', function() {
it('should run a filter on all text nodes', inject(function(DOMfilterFilter) {
var dom = 'a<p>b<i>c<b>d</b>e<b>f</b>g</i>h</p>i',
expected = '<span>A</span><p><span>B</span><i><span>C</span><b><span>D</span></b><span>E</span><b><span>F</span></b><span>G</span></i><span>H</span></p><span>I</span>',
result = DOMfilterFilter(dom, 'uppercase').$$unwrapTrustedValue();
expect(result).toEqual(expected);
}));
it('should pass additional arguments to the filter', inject(function(DOMfilterFilter) {
var dom = '1<p>2</p>3.14159265',
expected = '<span>1.00</span><p><span>2.00</span></p><span>3.14</span>',
result = DOMfilterFilter(dom, 'number', 2).$$unwrapTrustedValue();
expect(result).toEqual(expected);
}));
it('should never lock up like in bug #688', inject(function(linkyFilter, DOMfilterFilter) {
var msg = '#crash http://google.com',
linked = linkyFilter(msg),
irclinked = DOMfilterFilter(linked, 'irclinky');
// With the bug, the DOMfilterFilter call ends up in an infinite loop.
// I.e. if we ever got this far, the bug is fixed.
}));
});
});

View file

@ -1,169 +0,0 @@
/* plugins go here */
var msg = function(msg) {
return {'text': msg };
};
var metadata_name = function(message) {
if (message.metadata && message.metadata[0] && message.metadata[0].name) {
return message.metadata[0].name;
}
return null;
};
var expectTheseMessagesToContain = function(urls, pluginType, plugins) {
for (var i = 0; i < urls.length; i++) {
expect(
metadata_name(
plugins.PluginManager.contentForMessage(msg(urls[i]))
)
).toEqual(pluginType);
}
};
describe('filter', function() {
beforeEach(module('plugins'));
describe('Plugins', function() {
beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));
it('should recognize spotify tracks', inject(function(plugins) {
expectTheseMessagesToContain([
'spotify:track:6JEK0CvvjDjjMUBFoXShNZ',
'https://open.spotify.com/track/6JEK0CvvjDjjMUBFoXShNZ'
],
'Spotify track',
plugins);
}));
it('should recognize youtube videos', inject(function(plugins) {
expectTheseMessagesToContain([
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'http://www.youtube.com/watch?v=dQw4w9WgXcQ',
'http://youtu.be/J6vIS8jb6Fs',
'https://youtu.be/J6vIS8jb6Fs',
'http://www.youtube.com/embed/dQw4w9WgXcQ',
'https://www.youtube.com/embed/dQw4w9WgXcQ',
],
'YouTube video',
plugins);
}));
it('should recognize dailymotion videos', inject(function(plugins) {
expectTheseMessagesToContain([
'dailymotion.com/video/test',
'dailymotion.com/video/#video=asdf',
'dai.ly/sfg'
],
'Dailymotion video',
plugins);
}));
it('should recognize allocine videos', inject(function(plugins) {
expectTheseMessagesToContain([
'allocine.fr/videokast/video-12',
'allocine.fr/cmedia=234'
],
'AlloCine video',
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',
'https://i.imgur.com/1LmDmct.jpg',
'http://i.imgur.com/r4FKrnu.jpeg',
'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://pbs.twimg.com/media/B66rbCuIMAAxiFF.jpg:large',
'https://pbs.twimg.com/media/B6OZuCYCEAEV8SA.jpg:medium'
],
'image',
plugins);
}));
it('should recognize cloud music', inject(function(plugins) {
expectTheseMessagesToContain([
'http://soundcloud.com/',
'https://sadf.mixcloud.com/',
],
'cloud music',
plugins);
}));
it('should recognize google map', inject(function(plugins) {
expectTheseMessagesToContain([
'https://www.google.com/maps/@48.0034139,-74.9129088,6z',
],
'Google Map',
plugins);
}));
it('should recognize google map', inject(function(plugins) {
expectTheseMessagesToContain([
'https://asciinema.org/a/10625',
],
'ascii cast',
plugins);
}));
it('should recognize meteograms', inject(function(plugins) {
expectTheseMessagesToContain([
'http://www.yr.no/sted/Canada/Quebec/Montreal/',
],
'meteogram',
plugins);
}));
it('should recognize gists', inject(function(plugins) {
expectTheseMessagesToContain([
'https://gist.github.com/lorenzhs/e8c1a7d56fa170320eb8',
'https://gist.github.com/e8c1a7d56fa170320eb8',
],
'Gist',
plugins);
}));
it('should recognize giphy gifs', inject(function(plugins) {
expectTheseMessagesToContain([
'https://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy/',
'http://giphy.com/gifs/funny-cat-FiGiRei2ICzzG',
],
'Giphy',
plugins);
}));
it('should recognize tweets', inject(function(plugins) {
expectTheseMessagesToContain([
'https://twitter.com/DFB_Team_EN/statuses/488436782959448065',
],
'Tweet',
plugins);
}));
it('should recognize vines', inject(function(plugins) {
expectTheseMessagesToContain([
'https://vine.co/v/hWh262H9HM5',
'https://vine.co/v/hWh262H9HM5/embed',
],
'Vine',
plugins);
}));
});
});

View file

@ -1,33 +0,0 @@
{
"lang": "en-US",
"name": "Glowing Bear",
"short_name": "Glowing Bear",
"icons": [{
"src": "assets/img/glowing_bear_60x60.png",
"sizes": "60x60",
"type": "image/webapp"
}, {
"src": "assets/img/glowing_bear_90x90.png",
"sizes": "90x90"
}, {
"src": "assets/img/glowing_bear_128x128.png",
"sizes": "128x128"
}],
"splash_screens": [{
"src": "assets/img/glowing_bear_128x128.png",
"sizes": "128x128"
}],
"scope": "/glowing-bear/",
"start_url": "/glowing-bear/index.html",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#181818",
"background_color": "#333",
"prefer_related_applications": "false",
"chrome_related_applications": [{
"platform": "web"
}, {
"platform": "android",
"location": "https://play.google.com/store/apps/details?id=com.glowing_bear"
}]
}