From 1d40e16754e8ce60e71fa51be9982aa200a61db3 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Sat, 27 Mar 2010 23:27:58 +0530 Subject: [PATCH 1/3] initialized kettu on gitorious --- README.md | 51 + css/.DS_Store | Bin 0 -> 6148 bytes css/ie.css | 35 + css/images/control_eject.png | Bin 0 -> 603 bytes css/images/control_pause.png | Bin 0 -> 598 bytes css/images/control_play.png | Bin 0 -> 592 bytes css/images/spinner.gif | Bin 0 -> 1849 bytes css/jquery/.DS_Store | Bin 0 -> 6148 bytes css/jquery/images/.DS_Store | Bin 0 -> 6148 bytes .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 180 bytes .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 178 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 120 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 105 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 111 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 110 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 119 bytes .../ui-bg_gloss-wave_55_5c9ccc_500x100.png | Bin 0 -> 3457 bytes .../ui-bg_highlight-hard_75_f5f5b5_1x100.png | Bin 0 -> 116 bytes .../ui-bg_highlight-soft_60_4ca20b_1x100.png | Bin 0 -> 184 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 101 bytes css/jquery/images/ui-icons_222222_256x240.png | Bin 0 -> 4369 bytes css/jquery/images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4369 bytes css/jquery/images/ui-icons_454545_256x240.png | Bin 0 -> 4369 bytes css/jquery/images/ui-icons_888888_256x240.png | Bin 0 -> 4369 bytes css/jquery/images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4369 bytes css/jquery/ui.all.css | 2 + css/jquery/ui.base.css | 2 + css/jquery/ui.core.css | 37 + css/jquery/ui.progressbar.css | 4 + css/jquery/ui.theme.css | 247 + css/screen.css | 258 + css/src/forms.css | 65 + css/src/grid.css | 280 + css/src/grid.png | Bin 0 -> 195 bytes css/src/ie.css | 76 + css/src/print.css | 85 + css/src/reset.css | 45 + css/src/typography.css | 106 + css/transmission.css | 497 ++ features/sort_and_filter_torrents.feature | 78 + .../step_definitions/common_culerity_steps.rb | 169 + features/step_definitions/common_steps.rb | 10 + .../step_definitions/torrent_info_steps.rb | 37 + features/step_definitions/torrent_steps.rb | 67 + features/support/culerity.js | 27 + features/support/env.rb | 32 + features/support/paths.rb | 26 + features/support/testapp.rb | 16 + features/torrent_info.feature | 70 + features/torrents.feature | 26 + images/kettu_bg.png | Bin 0 -> 17771 bytes images/kettu_bg_header.png | Bin 0 -> 29757 bytes images/kettu_bg_info.png | Bin 0 -> 121 bytes index.html | 97 + js/controllers/settings.js | 53 + js/controllers/statistics.js | 16 + js/controllers/torrents.js | 163 + js/helpers/application_helpers.js | 9 + js/helpers/filter_torrents_helpers.js | 18 + js/helpers/info_helpers.js | 79 + js/helpers/link_helpers.js | 93 + js/helpers/math_helpers.js | 143 + js/helpers/setting_helpers.js | 107 + js/helpers/sort_torrents_helpers.js | 58 + js/helpers/statistic_helpers.js | 41 + js/helpers/store_helpers.js | 23 + js/helpers/torrent_helpers.js | 116 + js/helpers/view_helpers.js | 46 + js/models/settings_validator.js | 16 + js/models/torrent.js | 141 + js/models/validator.js | 94 + js/rpc.js | 53 + js/transmission.js | 41 + js/views/statistics.js | 10 + js/views/torrent.js | 87 + js/views/torrents.js | 19 + spec/filter_torrents_helpers_spec.js | 50 + .../pause_and_activate_button.mustache | 4 + spec/fixtures/settings.html | 12 + spec/fixtures/show.mustache | 16 + spec/fixtures/torrents.html | 56 + spec/fixtures/torrents_view_forms.html | 8 + spec/index.html | 42 + spec/jspec/images/bg.png | Bin 0 -> 154 bytes spec/jspec/images/hr.png | Bin 0 -> 321 bytes spec/jspec/images/loading.gif | Bin 0 -> 2608 bytes spec/jspec/images/sprites.bg.png | Bin 0 -> 4876 bytes spec/jspec/images/sprites.png | Bin 0 -> 8600 bytes spec/jspec/images/vr.png | Bin 0 -> 145 bytes spec/jspec/jspec.css | 147 + spec/jspec/jspec.jquery.js | 71 + spec/jspec/jspec.js | 1775 +++++ spec/jspec/jspec.shell.js | 36 + spec/jspec/jspec.timers.js | 90 + spec/jspec/jspec.xhr.js | 183 + spec/setting_helpers_spec.js | 61 + spec/sort_torrents_helpers_spec.js | 103 + spec/sort_torrents_helpers_spec.js~ | 103 + spec/statistic_helpers_spec.js | 17 + spec/store_helpers_spec.js | 36 + spec/torrent_helpers_spec.js | 69 + spec/torrent_spec.js | 134 + spec/torrent_view_spec.js | 122 + spec/torrents_view_spec.js | 33 + spec/validator_spec.js | 96 + templates/settings/index.mustache | 114 + templates/statistics/index.mustache | 35 + templates/torrents/delete_data.mustache | 1 + templates/torrents/new.mustache | 24 + .../pause_and_activate_button.mustache | 4 + templates/torrents/show.mustache | 16 + templates/torrents/show_compact.mustache | 14 + templates/torrents/show_info.mustache | 160 + vendor/.DS_Store | Bin 0 -> 6148 bytes vendor/bluff/bluff-min.js | 1 + vendor/bluff/excanvas.js | 35 + vendor/bluff/js-class.js | 1 + vendor/jquery/jquery.form.js | 660 ++ vendor/jquery/jquery.js | 6078 +++++++++++++++++ vendor/jquery/jquery.ui.js | 1157 ++++ vendor/mustache/mustache.js | 261 + vendor/mustache/sammy.mustache.js | 7 + vendor/sammy/sammy.js | 1381 ++++ vendor/sammy/sammy.json.js | 362 + vendor/sammy/sammy.storage.js | 520 ++ 125 files changed, 17966 insertions(+) create mode 100644 README.md create mode 100644 css/.DS_Store create mode 100644 css/ie.css create mode 100755 css/images/control_eject.png create mode 100755 css/images/control_pause.png create mode 100755 css/images/control_play.png create mode 100644 css/images/spinner.gif create mode 100644 css/jquery/.DS_Store create mode 100644 css/jquery/images/.DS_Store create mode 100755 css/jquery/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100755 css/jquery/images/ui-bg_flat_75_ffffff_40x100.png create mode 100755 css/jquery/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100755 css/jquery/images/ui-bg_glass_65_ffffff_1x400.png create mode 100755 css/jquery/images/ui-bg_glass_75_dadada_1x400.png create mode 100755 css/jquery/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100755 css/jquery/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100755 css/jquery/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png create mode 100755 css/jquery/images/ui-bg_highlight-hard_75_f5f5b5_1x100.png create mode 100755 css/jquery/images/ui-bg_highlight-soft_60_4ca20b_1x100.png create mode 100755 css/jquery/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100755 css/jquery/images/ui-icons_222222_256x240.png create mode 100755 css/jquery/images/ui-icons_2e83ff_256x240.png create mode 100755 css/jquery/images/ui-icons_454545_256x240.png create mode 100755 css/jquery/images/ui-icons_888888_256x240.png create mode 100755 css/jquery/images/ui-icons_cd0a0a_256x240.png create mode 100755 css/jquery/ui.all.css create mode 100755 css/jquery/ui.base.css create mode 100755 css/jquery/ui.core.css create mode 100755 css/jquery/ui.progressbar.css create mode 100755 css/jquery/ui.theme.css create mode 100644 css/screen.css create mode 100644 css/src/forms.css create mode 100755 css/src/grid.css create mode 100644 css/src/grid.png create mode 100644 css/src/ie.css create mode 100755 css/src/print.css create mode 100755 css/src/reset.css create mode 100644 css/src/typography.css create mode 100644 css/transmission.css create mode 100644 features/sort_and_filter_torrents.feature create mode 100644 features/step_definitions/common_culerity_steps.rb create mode 100644 features/step_definitions/common_steps.rb create mode 100644 features/step_definitions/torrent_info_steps.rb create mode 100644 features/step_definitions/torrent_steps.rb create mode 100644 features/support/culerity.js create mode 100644 features/support/env.rb create mode 100644 features/support/paths.rb create mode 100644 features/support/testapp.rb create mode 100644 features/torrent_info.feature create mode 100644 features/torrents.feature create mode 100644 images/kettu_bg.png create mode 100644 images/kettu_bg_header.png create mode 100644 images/kettu_bg_info.png create mode 100644 index.html create mode 100644 js/controllers/settings.js create mode 100644 js/controllers/statistics.js create mode 100644 js/controllers/torrents.js create mode 100644 js/helpers/application_helpers.js create mode 100644 js/helpers/filter_torrents_helpers.js create mode 100644 js/helpers/info_helpers.js create mode 100644 js/helpers/link_helpers.js create mode 100644 js/helpers/math_helpers.js create mode 100644 js/helpers/setting_helpers.js create mode 100644 js/helpers/sort_torrents_helpers.js create mode 100644 js/helpers/statistic_helpers.js create mode 100644 js/helpers/store_helpers.js create mode 100644 js/helpers/torrent_helpers.js create mode 100644 js/helpers/view_helpers.js create mode 100644 js/models/settings_validator.js create mode 100644 js/models/torrent.js create mode 100644 js/models/validator.js create mode 100644 js/rpc.js create mode 100644 js/transmission.js create mode 100644 js/views/statistics.js create mode 100644 js/views/torrent.js create mode 100644 js/views/torrents.js create mode 100644 spec/filter_torrents_helpers_spec.js create mode 100644 spec/fixtures/pause_and_activate_button.mustache create mode 100644 spec/fixtures/settings.html create mode 100644 spec/fixtures/show.mustache create mode 100644 spec/fixtures/torrents.html create mode 100644 spec/fixtures/torrents_view_forms.html create mode 100644 spec/index.html create mode 100644 spec/jspec/images/bg.png create mode 100644 spec/jspec/images/hr.png create mode 100644 spec/jspec/images/loading.gif create mode 100644 spec/jspec/images/sprites.bg.png create mode 100644 spec/jspec/images/sprites.png create mode 100644 spec/jspec/images/vr.png create mode 100644 spec/jspec/jspec.css create mode 100644 spec/jspec/jspec.jquery.js create mode 100644 spec/jspec/jspec.js create mode 100644 spec/jspec/jspec.shell.js create mode 100644 spec/jspec/jspec.timers.js create mode 100644 spec/jspec/jspec.xhr.js create mode 100644 spec/setting_helpers_spec.js create mode 100644 spec/sort_torrents_helpers_spec.js create mode 100644 spec/sort_torrents_helpers_spec.js~ create mode 100644 spec/statistic_helpers_spec.js create mode 100644 spec/store_helpers_spec.js create mode 100644 spec/torrent_helpers_spec.js create mode 100644 spec/torrent_spec.js create mode 100644 spec/torrent_view_spec.js create mode 100644 spec/torrents_view_spec.js create mode 100644 spec/validator_spec.js create mode 100644 templates/settings/index.mustache create mode 100644 templates/statistics/index.mustache create mode 100644 templates/torrents/delete_data.mustache create mode 100644 templates/torrents/new.mustache create mode 100644 templates/torrents/pause_and_activate_button.mustache create mode 100644 templates/torrents/show.mustache create mode 100644 templates/torrents/show_compact.mustache create mode 100644 templates/torrents/show_info.mustache create mode 100644 vendor/.DS_Store create mode 100644 vendor/bluff/bluff-min.js create mode 100644 vendor/bluff/excanvas.js create mode 100644 vendor/bluff/js-class.js create mode 100644 vendor/jquery/jquery.form.js create mode 100644 vendor/jquery/jquery.js create mode 100644 vendor/jquery/jquery.ui.js create mode 100644 vendor/mustache/mustache.js create mode 100644 vendor/mustache/sammy.mustache.js create mode 100644 vendor/sammy/sammy.js create mode 100644 vendor/sammy/sammy.json.js create mode 100644 vendor/sammy/sammy.storage.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..27c0402 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# kettu +Port of http://github.com/kjg/derailleur and the original Transmission Web Client +using [jquery](http://jquery.com), [sammy](http://github.com/quirkey/sammy) and [mustache.js](http://github.com/janl/mustache.js). + +By Frank Prößdorf . + + +## Thanks +* Kriesse for the elegant design. +* kjg for all the work on the transmission web client and help. +* lenalena for introducing proper jspec testing to this project. + + +## Usage +You can use kettu instead of the original web client to remotely administrate your transmission application. + +It is recommended to set the TRANSMISSION_WEB_HOME environment variable to the root path of this web client. Then you just need to open the location to the transmission web server (e.g. localhost:9091) and it will work. + +## Goals +* Usage should be as simple as possible +* Try out new features that older browsers may not support +* Keep the code clean +* Write tests for everything + +## Tests + +### Unit Tests +There are [jspecs](http://github.com/visionmedia/jspec) in `/spec` which you can run by opening the `index.html` file within the spec directory. + +### Acceptance Tests +There are [culerity](http://github.com/langalex/culerity) tests in `features`. You will need culerity, celerity and the most current htmlunit to run them. You will just need to type `cucumber features/`. + + +## Todo +* display errors, also tracker errors in tracker info + torrents list (maybe red) +* when adding new torrent + * select download folder (type in/categories/? => http://trac.transmissionbt.com/ticket/1496) + * select and prioritize files +* specify download folder after download started +* graphing up/download: + * aggregate data +* iphone compability +* debug why browser sometimes seems to fill up memory and become slower (maybe not cleaning up all intervals?) +* add transmission name (plus logo) +* extend stats view +* maybe icons for different file types in file list +* maybe statistics, preferences not as sidebar +* maybe search torrents (maybe by name, tracker) +* maybe show ratio goal in torrent list (if so, it should be changeable in the settings) +* maybe be able to select multiple torrents and pause/activate them +* maybe sort paused torrents (in filter mode) by progress by default \ No newline at end of file diff --git a/css/.DS_Store b/css/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..64f22d01d366fcc11a9467d3e403f1ddbbcfa1b6 GIT binary patch literal 6148 zcmeHKK~BR!3>?!6RqCZjj{60Eu&UY@@ByGj2vk%-)C0HN`fbKu2Nfk;IiRs*Pcmye z${gYtfUQsaTVMrXL3hO2!`S@XePmb3%!n2}p3v>P-Qn?gJdCoZ39r4z3*PX6XI#{? zArD^hjuYt*d=8EMV1|y=ZlI=Hi>IXnSS}xyPqy4p zES}E(CDLKFp`sL!0zVb--J7=Z`M;(Avj2OIObSSWf2x4ZH+P#gUoL0s=rw)T7WzGX qFy=-&LyTf#jAGvSD89MKD}K%Mn%Fmta`I75)Q^DcB9j7tp}-f7y&qx# literal 0 HcmV?d00001 diff --git a/css/ie.css b/css/ie.css new file mode 100644 index 0000000..3dddda9 --- /dev/null +++ b/css/ie.css @@ -0,0 +1,35 @@ +/* ----------------------------------------------------------------------- + + + Blueprint CSS Framework 0.9 + http://blueprintcss.org + + * Copyright (c) 2007-Present. See LICENSE for more info. + * See README for instructions on how to use Blueprint. + * For credits and origins, see AUTHORS. + * This is a compressed file. See the sources in the 'src' directory. + +----------------------------------------------------------------------- */ + +/* ie.css */ +body {text-align:center;} +.container {text-align:left;} +* html .column, * html .span-1, * html .span-2, * html .span-3, * html .span-4, * html .span-5, * html .span-6, * html .span-7, * html .span-8, * html .span-9, * html .span-10, * html .span-11, * html .span-12, * html .span-13, * html .span-14, * html .span-15, * html .span-16, * html .span-17, * html .span-18, * html .span-19, * html .span-20, * html .span-21, * html .span-22, * html .span-23, * html .span-24 {display:inline;overflow-x:hidden;} +* html legend {margin:0px -8px 16px 0;padding:0;} +sup {vertical-align:text-top;} +sub {vertical-align:text-bottom;} +html>body p code {*white-space:normal;} +hr {margin:-8px auto 11px;} +img {-ms-interpolation-mode:bicubic;} +.clearfix, .container {display:inline-block;} +* html .clearfix, * html .container {height:1%;} +fieldset {padding-top:0;} +textarea {overflow:auto;} +input.text, input.title, textarea {background-color:#fff;border:1px solid #bbb;} +input.text:focus, input.title:focus {border-color:#666;} +input.text, input.title, textarea, select {margin:0.5em 0;} +input.checkbox, input.radio {position:relative;top:.25em;} +form.inline div, form.inline p {vertical-align:middle;} +form.inline label {position:relative;top:-0.25em;} +form.inline input.checkbox, form.inline input.radio, form.inline input.button, form.inline button {margin:0.5em 0;} +button, input.button {position:relative;top:0.25em;} \ No newline at end of file diff --git a/css/images/control_eject.png b/css/images/control_eject.png new file mode 100755 index 0000000000000000000000000000000000000000..924d817bb6dba773de390723aa4ad985a9821713 GIT binary patch literal 603 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 EAHN$8j{pDw literal 0 HcmV?d00001 diff --git a/css/jquery/.DS_Store b/css/jquery/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..10f526f8e4dd7b7acd1ba2e1d98b71aa58834ebe GIT binary patch literal 6148 zcmeHKJ8l9o5S<|?S}Dzzl)eHtuvW_nasddSC=!TbMZXp2;%L126hWJGq#$}Djh{W9 zXUnhH*%1-#&by^ZDU%pabM+2j!Q2{DI1*iZOpaKt9AWLlX_VAN=AQhkj&#r)d9}3*CCXRvr>A>JE0C0}58|L0i z0E-2HHE|3?1g1d+2351g(4Zq;GOs3%fk79|=0o#l%??HVcAQ^4U9<*rqykjnUV)cb zwpRaF@L&4>dlFYvfC~I81$5rO_Z^;;wRQ41tF;BbhFi`RZicy2FnBozdO60z%JIUJ bBCpsS`!#V4bUNZr2l8jYbfHm!zgFM|YL^v< literal 0 HcmV?d00001 diff --git a/css/jquery/images/.DS_Store b/css/jquery/images/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0N1x91EQ4=4yQ7#`R^ z$vje}bP0l+XkK DSH>_4 literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_flat_75_ffffff_40x100.png b/css/jquery/images/ui-bg_flat_75_ffffff_40x100.png new file mode 100755 index 0000000000000000000000000000000000000000..ac8b229af950c29356abf64a6c4aa894575445f0 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*FsY*{5$B>N1x91EQ4=4yQYz+E8 zPo9&<{J;c_6SHRil>2s{Zw^OT)6@jj2u|u!(plXsM>LJD`vD!n;OXk;vd$@?2>^GI BH@yG= literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_glass_55_fbf9ee_1x400.png b/css/jquery/images/ui-bg_glass_55_fbf9ee_1x400.png new file mode 100755 index 0000000000000000000000000000000000000000..ad3d6346e00f246102f72f2e026ed0491988b394 GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnour0hLi978O6-<~(*I$*%ybaDOn z{W;e!B}_MSUQoPXhYd^Y6RUoS1yepnPx`2Kz)7OXQG!!=-jY=F+d2OOy?#DnJ32>z UEim$g7SJdLPgg&ebxsLQ09~*s;{X5v literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_glass_65_ffffff_1x400.png b/css/jquery/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100755 index 0000000000000000000000000000000000000000..42ccba269b6e91bef12ad0fa18be651b5ef0ee68 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnouqzpV=978O6-=0?FV^9z|eBtf= z|7WztIJ;WT>{+tN>ySr~=F{k$>;_x^_y?afmf9pRKH0)6?eSP?3s5hEr>mdKI;Vst E0O;M1& literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_glass_75_dadada_1x400.png b/css/jquery/images/ui-bg_glass_75_dadada_1x400.png new file mode 100755 index 0000000000000000000000000000000000000000..5a46b47cb16631068aee9e0bd61269fc4e95e5cd GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnouq|7{B978O6lPf+wIa#m9#>Unb zm^4K~wN3Zq+uP{vDV26o)#~38k_!`W=^oo1w6ixmPC4R1b Tyd6G3lNdZ*{an^LB{Ts5`idse literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png b/css/jquery/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png new file mode 100755 index 0000000000000000000000000000000000000000..81ecc362d50ef5abbc0420aacd5345822f1f6098 GIT binary patch literal 3457 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 QKw}s@UHx3vIVCg!06PyQ`v3p{ literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_highlight-soft_60_4ca20b_1x100.png b/css/jquery/images/ui-bg_highlight-soft_60_4ca20b_1x100.png new file mode 100755 index 0000000000000000000000000000000000000000..8e3c697031ccf869c0a8032359c573dd7aee8d74 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^j6j^i!3Ja)XlH!`a_T)@9780guI=;WI^-bYB3~ME z{_$FyHa!-_jt35He*fj}e(_RvDfzr$f!em+zM==Tk9}~6t#^3-vBT?Zz8J$at|HGD zMsowq-&{=i{`F@6h9?(9s;~UkmU?r|FRE-cSDoa4u9Ykz#SYWbMK-%nd#0wlQ!V-9 k+38*mQ-A%}=dNIKs|Z~<_4V>OK&LQxy85}Sb4q9e054%kvj6}9 literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/css/jquery/images/ui-bg_highlight-soft_75_cccccc_1x100.png new file mode 100755 index 0000000000000000000000000000000000000000..7c9fa6c6edcfcdd3e5b77e6f547b719e6fc66e30 GIT binary patch literal 101 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 literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-icons_2e83ff_256x240.png b/css/jquery/images/ui-icons_2e83ff_256x240.png new file mode 100755 index 0000000000000000000000000000000000000000..45e8928e5284adacea3f9ec07b9b50667d2ac65f GIT binary patch literal 4369 zcmd^?`8O2)_s3^phOrG}UnfiUEn8(9QW1?MNkxXVDEpFin2{xWrLx5kBC;k~GmFhwsn)TR1w<4t)tA3_robX4CdCOHJC|7j+vW z%J-EMX&`87enIluaSc0_SnYUx$GzUc?vrNXt&I`o?~7C3RJ>C-Ajq!3AfU8Dx90^_ zp3}MKjJzYC+`T(&egFXQ#9Ek{*oVAaa!zrZtmlRFnwQPRJXH<%pkK2*eP`pT=lwD7 zifq+4BY_rUTa+U|2#&?i7>PVvD?7R4ZfOLPT{e9G~G!Ls3s8JtQE`jMM9wl2V9&Q+K2DHW0M+uQmEr%nYJ^7cK?uIpU-)=wn71ZZ-=@ar0;3^AY z5+TI{2b(e%t{2PZ^HKF*vu@+Xr&BAc@2BC4 z_vCgww#i=)ea5Vo$glEEVBBg_VPBj!)OO>)f@}#dg6ULOeC>LBHz<;*5Y;YfE0lNx zg{N+4@lO~ozxpF69qV@VOGnc248Iuag4C1T)P^(hWkpP!{h!JekX}m^Q#b2B4f1oT zIjsGz)4}-$rQ*-tSuc%qG>%<4xM#E& zN)7lRK~^2VdiloY4>;#}A!yHOAXEmEi^+eA#05pawGXs>!z)gSoDuI#>bRCq-qjJe zZ)r=A`*EMX6+)~er1kdv1L^)0-PsAEM7JF$O6G8>496$24lkOSR^RTfUuIz%iSfn5b-t!##cs7sQI);gdAvqmn_v|%I9k;fCPl0Z)R1+hNQONJN zH%3jT9sOq*a`LF*MiY=zlSSQZ;{_FL9M07A=In+O!~wR}=bzGEQpk2!Vc0p)qKAH? zOk{(%06W#)DdICQ_S%Q@<0Y+!?9%#$gWJ%)EO->^YZP{<`oB4~9xh zL9-0*c4@B#O2ylYs_g`Ky$zb~v!M`NRaMNFYF*Gsu|7)=JyyMHjFC=HhGUE@{aI|B zJ~ITXU052%7jFb5Ys#fhS_?4kqc7H0EU49B8(Chg0&JzU=Gka#xOz1)H0d4m7ZnRA z=M^tdY|U6T!fmte{W?_r8H~qdq|q{5AMU_2It1I4143n~xL?4&K#BOB48l9_Rdm!(c^C?JU;tF0 zEh@o1y6Qa_>}#AwX{VY+`C^kNkxhgb1P5cB0%xupAXyg9NO=SnXrJUE?rQg{Lcsn+ zAZKctGLfbK_B#^&Nev|0^fB&?DN=ak8|0!np524LD25=s84BP8Vl(3=jflNp{X>e@ z637Ri5xx;&JNl+XYImA|{;XR~P*svYDEWYJ6I5!6uO~2twFC1ZQevB7#3z~(apxn& z^J@>Mc`>PJair{yT`iuan-V+i%|Ho-pA<1?V-k^R2Q<5;Co%XxmL` z018t4T0TTwO^w)Gx{9OSJ^9_|kgwX`7%0Rw!PO~@?xvnfUehvN;2Rc;^l>3kfbtk3 z8{j7p;S&{uTlTe9&HTc38q@%_KQFk<&n{vmrN7y&Cz{etcE->rq!6HL)2F!aa=0%! zM%Bwo!7TQ5t;@a_#Q}sjk{UebWQZ8{cp&HN^$*JfH#8spkhk{R@CVBiPuP@yEhu{} zsQfuhTqV%rioATpEphMfhyRYbVfVW`YwLFXUWm-===J(byMf!5;W^CV1g~2194Xx) zFK|z{pm%n-)-DRe{Qhk(d!QaoI*y%Wn6h7<6A{i*Sob&B^y|Spg!&J$`kN>zwUJ3x zaB$ciu*0FJKg}T ztgnh)ASF8njz5>h6?f#{c=*Yr4W_34$GmVIo8OLWjcZK4a0`+Yv-!*}9 zBwKm;DAsA(nDI-`iH@;`=gP+m{lgFLHK3m$W@?)&dGhDA_Z2xOzI0$p(ZJtH$vCxE zj>+kYNBJzs-TlSx!tSH}%I9fQv)mc!C7X0bKlZv4f&}C3+O-4k7AmVO|KYZ9ydP%(N1^uisV8y;~p`x4qFXD?!_OyN9=w(Od6W; zGrT?G;l2v@Ob5k^8w<9w%Jbjb^|H}PYKo}I~bobd!XrTbzp2Zp~H8lgJ)I3?l&(bDiWf8gE&6b z>)9GB=Iu-6%I((+>=jGP>CzD8c0oWITFZGgM!Q7|JrUYq4#^Y(vuDu-a>OWDa4Y4} z5a_*lW#IL_aVf8L+Ty}c&2VojLEIA-;eQK6Wo?xAuK>i;1VWx3c=!s2;j_*iRHOsb*>6-CgcYP+Ho=L@XLd*j~2ln-;WHg)|cCixksH$K={5rGSD@yB%LI|(NCc8 z1Er8H+QO)~S~K{g?nH|2dB8SKs)BxQ?%G}}o*LV!NG2m*TmR|pWj~g`>)ClJCE#F$ zcj)fBg(dKOKmc$Cy}IRlasngIR>z~kP&WW~9cC951{AKmnZ~ZMsqup6QQf7J0T1;C zK9*Qd5*(HxW=tl|RfjO>nkoW#AU3t>JkuzWxy4-l?xmTv15_r1X@p@dz^{&j&;{Mq z$^0$0q&y?kbdZh)kZ+NfXfqLTG}Q^j>qHlUH4VEK`3y^-z6Y<6O88Hf4v^;}!{t-a zDWg;znYu%6zA1~A5~w?fxO~i8-Ib(^02{c4pXjhDI^2 zXB1LP4dvWuc%PXQ{r!d#6>${rm+M8EJM8yf#!H$Kp8AxwUXm5`7Tu-J$mHeCG>vw|&Ay415}_1w&*9K8+2d3v1N+@a$|820o4u60Tj@u&kI!~q2V9X; z>tMvQDI|O$#m+m2O**ZHq`_{#8)ry6`&5s~2k{O4Du16Fn0P;&_(0!e5%Bel){nU0 zJX~<8U6hoI%yx}qGY_1Tq7YKDJ)ETOCs&W)TiCrK*1%DE*vXdD-7hwE*LUgjeHRM` z&@pkhTi>m#Kc+QIK+2Ybn9-sFVKNHyIgfob4H_77yYh))Rq$7Pw|+aD6&yZ|ki9 z8Zb6s{oBt1G+PgfIcxd}{m@~1nzhe;LH)5;!gS8@ddyabpdBc?7JVl?tS+<#bPSMT z2@0uYdsWN(;Ww)n-PlA-0r+62@bYkEa`k{0s})fJgYZ#5=DmIdEvok7aZJRi{w-|} zkea&6X}ZA3b7&vbDb7)v8CuI(+zzSf3z&P2eOrPNP?D~ zf zn0@)0h;~5F&BG5vOFU!=woW&ZSl~nrs{?1w>nWfW_dnpTd z4qvLDYJ*ft>Sp%M(^_xCZpNBnc66JX}A|ZL9IENM`U>`ph7d<+RQiI}@E8Y)70s zMC*_&))}GlmR}@{v9*nm)29-=rn`Q$rc^4G)GVQHlTr6BpGxtHuU(8AF7Ffh54?5w zj+EYT9>x)PWL-iQ@RNmT?R+|c@=FOmj)5Za6_ z@DkVy4l^L>Z3#SI@s_eVwd3D)<^Ivq8a~J{|4mhOL^<7M4D8){ut;GIqqn`oqCk|x pNh;Wa$C0(mdpqYz&F>xK-uVD=DT5%Jzh8ZT#aXmjr70%*{{S|9XD$E$ literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-icons_454545_256x240.png b/css/jquery/images/ui-icons_454545_256x240.png new file mode 100755 index 0000000000000000000000000000000000000000..7ec70d11bfb2f77374dfd00ef61ba0c3647b5a0c GIT binary patch literal 4369 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 k*Kv;w;*%(i9W@fAqs5i2wiq literal 0 HcmV?d00001 diff --git a/css/jquery/images/ui-icons_888888_256x240.png b/css/jquery/images/ui-icons_888888_256x240.png new file mode 100755 index 0000000000000000000000000000000000000000..5ba708c39172a69e069136bd1309c4322c61f571 GIT binary patch literal 4369 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 literal 0 HcmV?d00001 diff --git a/css/jquery/ui.all.css b/css/jquery/ui.all.css new file mode 100755 index 0000000..543e4c3 --- /dev/null +++ b/css/jquery/ui.all.css @@ -0,0 +1,2 @@ +@import "ui.base.css"; +@import "ui.theme.css"; diff --git a/css/jquery/ui.base.css b/css/jquery/ui.base.css new file mode 100755 index 0000000..3dc53d6 --- /dev/null +++ b/css/jquery/ui.base.css @@ -0,0 +1,2 @@ +@import url("ui.core.css"); +@import url("ui.progressbar.css"); \ No newline at end of file diff --git a/css/jquery/ui.core.css b/css/jquery/ui.core.css new file mode 100755 index 0000000..c2f18f2 --- /dev/null +++ b/css/jquery/ui.core.css @@ -0,0 +1,37 @@ +/* +* jQuery UI CSS Framework +* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) +* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses. +*/ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { display: none; } +.ui-helper-hidden-accessible { position: absolute; left: -99999999px; } +.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } +.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } +.ui-helper-clearfix { display: inline-block; } +/* required comment for clearfix to work in Opera \*/ +* html .ui-helper-clearfix { height:1%; } +.ui-helper-clearfix { display:block; } +/* end clearfix */ +.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { cursor: default !important; } + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } \ No newline at end of file diff --git a/css/jquery/ui.progressbar.css b/css/jquery/ui.progressbar.css new file mode 100755 index 0000000..d5630f8 --- /dev/null +++ b/css/jquery/ui.progressbar.css @@ -0,0 +1,4 @@ +/* Progressbar +----------------------------------*/ +.ui-progressbar { height:0.5em; text-align: left; margin: 4px; width: 95%; float: left; } +.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } \ No newline at end of file diff --git a/css/jquery/ui.theme.css b/css/jquery/ui.theme.css new file mode 100755 index 0000000..01d0e3b --- /dev/null +++ b/css/jquery/ui.theme.css @@ -0,0 +1,247 @@ + + +/* +* jQuery UI CSS Framework +* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) +* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses. +* To view and modify this theme, visit http://jqueryui.com/themeroller/ +*/ + + +/* Component containers +----------------------------------*/ +.ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; } +.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; } +.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(images/ui-bg_flat_75_ffffff_40x100.png)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; } +.ui-widget-content a { color: #222222/*{fcContent}*/; } +.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; + background: #cccccc/*{bgColorHeader}*/ url(images/ui-bg_highlight-soft_75_cccccc_1x100.png)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; + color: #222222/*{fcHeader}*/; font-weight: bold; } +.ui-widget-header a { color: #222222/*{fcHeader}*/; } + +/* Interaction states +----------------------------------*/ +.ui-state-default, .ui-widget-content .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; outline: none; } +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; outline: none; } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { border: 1px solid #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(images/ui-bg_glass_75_dadada_1x400.png)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; outline: none; } +.ui-state-hover a, .ui-state-hover a:hover { color: #212121/*{fcHover}*/; text-decoration: none; outline: none; } +.ui-state-active, .ui-widget-content .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(images/ui-bg_glass_65_ffffff_1x400.png)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; outline: none; } +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; outline: none; text-decoration: none; } + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, .ui-widget-content .ui-state-highlight {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(images/ui-bg_glass_55_fbf9ee_1x400.png)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; } +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: #363636/*{fcHighlight}*/; } +.ui-state-error, .ui-widget-content .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(images/ui-bg_glass_95_fef1ec_1x400.png)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; } +.ui-state-error a, .ui-widget-content .ui-state-error a { color: #cd0a0a/*{fcError}*/; } +.ui-state-error-text, .ui-widget-content .ui-state-error-text { color: #cd0a0a/*{fcError}*/; } +.ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } +.ui-priority-primary, .ui-widget-content .ui-priority-primary { font-weight: bold; } +.ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; } +.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; } +.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsHeader}*/; } +.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png)/*{iconsDefault}*/; } +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsHover}*/; } +.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsActive}*/; } +.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png)/*{iconsHighlight}*/; } +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png)/*{iconsError}*/; } + +/* positioning */ +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-off { background-position: -96px -144px; } +.ui-icon-radio-on { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-tl { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; } +.ui-corner-tr { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; } +.ui-corner-bl { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; } +.ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; } +.ui-corner-top { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; } +.ui-corner-bottom { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; } +.ui-corner-right { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; } +.ui-corner-left { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; } +.ui-corner-all { -moz-border-radius: 4px/*{cornerRadius}*/; -webkit-border-radius: 4px/*{cornerRadius}*/; } + +/* Overlays */ +.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; } +.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRadiusShadow}*/; } \ No newline at end of file diff --git a/css/screen.css b/css/screen.css new file mode 100644 index 0000000..f542e30 --- /dev/null +++ b/css/screen.css @@ -0,0 +1,258 @@ +/* ----------------------------------------------------------------------- + + + Blueprint CSS Framework 0.9 + http://blueprintcss.org + + * Copyright (c) 2007-Present. See LICENSE for more info. + * See README for instructions on how to use Blueprint. + * For credits and origins, see AUTHORS. + * This is a compressed file. See the sources in the 'src' directory. + +----------------------------------------------------------------------- */ + +/* reset.css */ +html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, dialog, figure, footer, header, hgroup, nav, section {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;} +article, aside, dialog, figure, footer, header, hgroup, nav, section {display:block;} +body {line-height:1.5;} +table {border-collapse:separate;border-spacing:0;} +caption, th, td {text-align:left;font-weight:normal;} +table, td, th {vertical-align:middle;} +blockquote:before, blockquote:after, q:before, q:after {content:"";} +blockquote, q {quotes:"" "";} +a img {border:none;} + +/* typography.css */ +html {font-size:100.01%;} +body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;} +h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;} +h1 {font-size:3em;line-height:1;margin-bottom:0.5em;} +h2 {font-size:2em;margin-bottom:0.75em;} +h3 {font-size:1.5em;line-height:1;margin-bottom:1em;} +h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;} +h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;} +h6 {font-size:1em;font-weight:bold;} +h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;} +p {margin:0 0 1.5em;} +p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;} +p img.right {float:right;margin:1.5em 0 1.5em 1.5em;} +a:focus, a:hover {color:#000;} +a {color:#009;text-decoration:underline;} +blockquote {margin:1.5em;color:#666;font-style:italic;} +strong {font-weight:bold;} +em, dfn {font-style:italic;} +dfn {font-weight:bold;} +sup, sub {line-height:0;} +abbr, acronym {border-bottom:1px dotted #666;} +address {margin:0 0 1.5em;font-style:italic;} +del {color:#666;} +pre {margin:1.5em 0;white-space:pre;} +pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;} +li ul, li ol {margin:0;} +ul, ol {margin:0 1.5em 1.5em 0;padding-left:3.333em;} +ul {list-style-type:disc;} +ol {list-style-type:decimal;} +dl {margin:0 0 1.5em 0;} +dl dt {font-weight:bold;} +dd {margin-left:1.5em;} +table {margin-bottom:1.4em;width:100%;} +th {font-weight:bold;} +thead th {background:#c3d9ff;} +th, td, caption {padding:4px 10px 4px 5px;} +tr.even td {background:#e5ecf9;} +tfoot {font-style:italic;} +caption {background:#eee;} +.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;} +.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;} +.hide {display:none;} +.quiet {color:#666;} +.loud {color:#000;} +.highlight {background:#ff0;} +.added {background:#060;color:#fff;} +.removed {background:#900;color:#fff;} +.first {margin-left:0;padding-left:0;} +.last {margin-right:0;padding-right:0;} +.top {margin-top:0;padding-top:0;} +.bottom {margin-bottom:0;padding-bottom:0;} + +/* forms.css */ +label {font-weight:bold;} +fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;} +legend {font-weight:bold;font-size:1.2em;} +input[type=text], input[type=password], input.text, input.title, textarea, select {background-color:#fff;border:1px solid #bbb;} +input[type=text]:focus, input[type=password]:focus, input.text:focus, input.title:focus, textarea:focus, select:focus {border-color:#666;} +input[type=text], input[type=password], input.text, input.title, textarea, select {margin:0.5em 0;} +input.text, input.title {width:300px;padding:5px;} +input.title {font-size:1.5em;} +textarea {width:390px;height:250px;padding:5px;} +input[type=checkbox], input[type=radio], input.checkbox, input.radio {position:relative;top:.25em;} +form.inline {line-height:3;} +form.inline p {margin-bottom:0;} +.error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd;} +.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;} +.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;} +.success {background:#E6EFC2;color:#264409;border-color:#C6D880;} +.error a {color:#8a1f11;} +.notice a {color:#514721;} +.success a {color:#264409;} + +/* grid.css */ +.container {width:950px;margin:0 auto;} +.showgrid {background:url(src/grid.png);} +.column, .span-1, .span-2, .span-3, .span-4, .span-5, .span-6, .span-7, .span-8, .span-9, .span-10, .span-11, .span-12, .span-13, .span-14, .span-15, .span-16, .span-17, .span-18, .span-19, .span-20, .span-21, .span-22, .span-23, .span-24 {float:left;margin-right:10px;} +.last {margin-right:0;} +.span-1 {width:30px;} +.span-2 {width:70px;} +.span-3 {width:110px;} +.span-4 {width:150px;} +.span-5 {width:190px;} +.span-6 {width:230px;} +.span-7 {width:270px;} +.span-8 {width:310px;} +.span-9 {width:350px;} +.span-10 {width:390px;} +.span-11 {width:430px;} +.span-12 {width:470px;} +.span-13 {width:510px;} +.span-14 {width:550px;} +.span-15 {width:590px;} +.span-16 {width:630px;} +.span-17 {width:670px;} +.span-18 {width:710px;} +.span-19 {width:750px;} +.span-20 {width:790px;} +.span-21 {width:830px;} +.span-22 {width:870px;} +.span-23 {width:910px;} +.span-24 {width:950px;margin-right:0;} +input.span-1, textarea.span-1, input.span-2, textarea.span-2, input.span-3, textarea.span-3, input.span-4, textarea.span-4, input.span-5, textarea.span-5, input.span-6, textarea.span-6, input.span-7, textarea.span-7, input.span-8, textarea.span-8, input.span-9, textarea.span-9, input.span-10, textarea.span-10, input.span-11, textarea.span-11, input.span-12, textarea.span-12, input.span-13, textarea.span-13, input.span-14, textarea.span-14, input.span-15, textarea.span-15, input.span-16, textarea.span-16, input.span-17, textarea.span-17, input.span-18, textarea.span-18, input.span-19, textarea.span-19, input.span-20, textarea.span-20, input.span-21, textarea.span-21, input.span-22, textarea.span-22, input.span-23, textarea.span-23, input.span-24, textarea.span-24 {border-left-width:1px!important;border-right-width:1px!important;padding-left:5px!important;padding-right:5px!important;} +input.span-1, textarea.span-1 {width:18px!important;} +input.span-2, textarea.span-2 {width:58px!important;} +input.span-3, textarea.span-3 {width:98px!important;} +input.span-4, textarea.span-4 {width:138px!important;} +input.span-5, textarea.span-5 {width:178px!important;} +input.span-6, textarea.span-6 {width:218px!important;} +input.span-7, textarea.span-7 {width:258px!important;} +input.span-8, textarea.span-8 {width:298px!important;} +input.span-9, textarea.span-9 {width:338px!important;} +input.span-10, textarea.span-10 {width:378px!important;} +input.span-11, textarea.span-11 {width:418px!important;} +input.span-12, textarea.span-12 {width:458px!important;} +input.span-13, textarea.span-13 {width:498px!important;} +input.span-14, textarea.span-14 {width:538px!important;} +input.span-15, textarea.span-15 {width:578px!important;} +input.span-16, textarea.span-16 {width:618px!important;} +input.span-17, textarea.span-17 {width:658px!important;} +input.span-18, textarea.span-18 {width:698px!important;} +input.span-19, textarea.span-19 {width:738px!important;} +input.span-20, textarea.span-20 {width:778px!important;} +input.span-21, textarea.span-21 {width:818px!important;} +input.span-22, textarea.span-22 {width:858px!important;} +input.span-23, textarea.span-23 {width:898px!important;} +input.span-24, textarea.span-24 {width:938px!important;} +.append-1 {padding-right:40px;} +.append-2 {padding-right:80px;} +.append-3 {padding-right:120px;} +.append-4 {padding-right:160px;} +.append-5 {padding-right:200px;} +.append-6 {padding-right:240px;} +.append-7 {padding-right:280px;} +.append-8 {padding-right:320px;} +.append-9 {padding-right:360px;} +.append-10 {padding-right:400px;} +.append-11 {padding-right:440px;} +.append-12 {padding-right:480px;} +.append-13 {padding-right:520px;} +.append-14 {padding-right:560px;} +.append-15 {padding-right:600px;} +.append-16 {padding-right:640px;} +.append-17 {padding-right:680px;} +.append-18 {padding-right:720px;} +.append-19 {padding-right:760px;} +.append-20 {padding-right:800px;} +.append-21 {padding-right:840px;} +.append-22 {padding-right:880px;} +.append-23 {padding-right:920px;} +.prepend-1 {padding-left:40px;} +.prepend-2 {padding-left:80px;} +.prepend-3 {padding-left:120px;} +.prepend-4 {padding-left:160px;} +.prepend-5 {padding-left:200px;} +.prepend-6 {padding-left:240px;} +.prepend-7 {padding-left:280px;} +.prepend-8 {padding-left:320px;} +.prepend-9 {padding-left:360px;} +.prepend-10 {padding-left:400px;} +.prepend-11 {padding-left:440px;} +.prepend-12 {padding-left:480px;} +.prepend-13 {padding-left:520px;} +.prepend-14 {padding-left:560px;} +.prepend-15 {padding-left:600px;} +.prepend-16 {padding-left:640px;} +.prepend-17 {padding-left:680px;} +.prepend-18 {padding-left:720px;} +.prepend-19 {padding-left:760px;} +.prepend-20 {padding-left:800px;} +.prepend-21 {padding-left:840px;} +.prepend-22 {padding-left:880px;} +.prepend-23 {padding-left:920px;} +.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;} +.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;} +.pull-1 {margin-left:-40px;} +.pull-2 {margin-left:-80px;} +.pull-3 {margin-left:-120px;} +.pull-4 {margin-left:-160px;} +.pull-5 {margin-left:-200px;} +.pull-6 {margin-left:-240px;} +.pull-7 {margin-left:-280px;} +.pull-8 {margin-left:-320px;} +.pull-9 {margin-left:-360px;} +.pull-10 {margin-left:-400px;} +.pull-11 {margin-left:-440px;} +.pull-12 {margin-left:-480px;} +.pull-13 {margin-left:-520px;} +.pull-14 {margin-left:-560px;} +.pull-15 {margin-left:-600px;} +.pull-16 {margin-left:-640px;} +.pull-17 {margin-left:-680px;} +.pull-18 {margin-left:-720px;} +.pull-19 {margin-left:-760px;} +.pull-20 {margin-left:-800px;} +.pull-21 {margin-left:-840px;} +.pull-22 {margin-left:-880px;} +.pull-23 {margin-left:-920px;} +.pull-24 {margin-left:-960px;} +.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;} +.push-1 {margin:0 -40px 1.5em 40px;} +.push-2 {margin:0 -80px 1.5em 80px;} +.push-3 {margin:0 -120px 1.5em 120px;} +.push-4 {margin:0 -160px 1.5em 160px;} +.push-5 {margin:0 -200px 1.5em 200px;} +.push-6 {margin:0 -240px 1.5em 240px;} +.push-7 {margin:0 -280px 1.5em 280px;} +.push-8 {margin:0 -320px 1.5em 320px;} +.push-9 {margin:0 -360px 1.5em 360px;} +.push-10 {margin:0 -400px 1.5em 400px;} +.push-11 {margin:0 -440px 1.5em 440px;} +.push-12 {margin:0 -480px 1.5em 480px;} +.push-13 {margin:0 -520px 1.5em 520px;} +.push-14 {margin:0 -560px 1.5em 560px;} +.push-15 {margin:0 -600px 1.5em 600px;} +.push-16 {margin:0 -640px 1.5em 640px;} +.push-17 {margin:0 -680px 1.5em 680px;} +.push-18 {margin:0 -720px 1.5em 720px;} +.push-19 {margin:0 -760px 1.5em 760px;} +.push-20 {margin:0 -800px 1.5em 800px;} +.push-21 {margin:0 -840px 1.5em 840px;} +.push-22 {margin:0 -880px 1.5em 880px;} +.push-23 {margin:0 -920px 1.5em 920px;} +.push-24 {margin:0 -960px 1.5em 960px;} +.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;} +.prepend-top {margin-top:1.5em;} +.append-bottom {margin-bottom:1.5em;} +.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;} +hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;} +hr.space {background:#fff;color:#fff;visibility:hidden;} +.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;} +.clearfix, .container {display:block;} +.clear {clear:both;} \ No newline at end of file diff --git a/css/src/forms.css b/css/src/forms.css new file mode 100644 index 0000000..b491134 --- /dev/null +++ b/css/src/forms.css @@ -0,0 +1,65 @@ +/* -------------------------------------------------------------- + + forms.css + * Sets up some default styling for forms + * Gives you classes to enhance your forms + + Usage: + * For text fields, use class .title or .text + * For inline forms, use .inline (even when using columns) + +-------------------------------------------------------------- */ + +label { font-weight: bold; } +fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; } +legend { font-weight: bold; font-size:1.2em; } + + +/* Form fields +-------------------------------------------------------------- */ + +input[type=text], input[type=password], +input.text, input.title, +textarea, select { + background-color:#fff; + border:1px solid #bbb; +} +input[type=text]:focus, input[type=password]:focus, +input.text:focus, input.title:focus, +textarea:focus, select:focus { + border-color:#666; +} + +input[type=text], input[type=password], +input.text, input.title, +textarea, select { + margin:0.5em 0; +} + +input.text, +input.title { width: 300px; padding:5px; } +input.title { font-size:1.5em; } +textarea { width: 390px; height: 250px; padding:5px; } + +input[type=checkbox], input[type=radio], +input.checkbox, input.radio { + position:relative; top:.25em; +} + +form.inline { line-height:3; } +form.inline p { margin-bottom:0; } + + +/* Success, notice and error boxes +-------------------------------------------------------------- */ + +.error, +.notice, +.success { padding: .8em; margin-bottom: 1em; border: 2px solid #ddd; } + +.error { background: #FBE3E4; color: #8a1f11; border-color: #FBC2C4; } +.notice { background: #FFF6BF; color: #514721; border-color: #FFD324; } +.success { background: #E6EFC2; color: #264409; border-color: #C6D880; } +.error a { color: #8a1f11; } +.notice a { color: #514721; } +.success a { color: #264409; } diff --git a/css/src/grid.css b/css/src/grid.css new file mode 100755 index 0000000..eb07356 --- /dev/null +++ b/css/src/grid.css @@ -0,0 +1,280 @@ +/* -------------------------------------------------------------- + + grid.css + * Sets up an easy-to-use grid of 24 columns. + + By default, the grid is 950px wide, with 24 columns + spanning 30px, and a 10px margin between columns. + + If you need fewer or more columns, namespaces or semantic + element names, use the compressor script (lib/compress.rb) + +-------------------------------------------------------------- */ + +/* A container should group all your columns. */ +.container { + width: 950px; + margin: 0 auto; +} + +/* Use this class on any .span / container to see the grid. */ +.showgrid { + background: url(src/grid.png); +} + + +/* Columns +-------------------------------------------------------------- */ + +/* Sets up basic grid floating and margin. */ +.column, .span-1, .span-2, .span-3, .span-4, .span-5, .span-6, .span-7, .span-8, .span-9, .span-10, .span-11, .span-12, .span-13, .span-14, .span-15, .span-16, .span-17, .span-18, .span-19, .span-20, .span-21, .span-22, .span-23, .span-24 { + float: left; + margin-right: 10px; +} + +/* The last column in a row needs this class. */ +.last { margin-right: 0; } + +/* Use these classes to set the width of a column. */ +.span-1 {width: 30px;} + +.span-2 {width: 70px;} +.span-3 {width: 110px;} +.span-4 {width: 150px;} +.span-5 {width: 190px;} +.span-6 {width: 230px;} +.span-7 {width: 270px;} +.span-8 {width: 310px;} +.span-9 {width: 350px;} +.span-10 {width: 390px;} +.span-11 {width: 430px;} +.span-12 {width: 470px;} +.span-13 {width: 510px;} +.span-14 {width: 550px;} +.span-15 {width: 590px;} +.span-16 {width: 630px;} +.span-17 {width: 670px;} +.span-18 {width: 710px;} +.span-19 {width: 750px;} +.span-20 {width: 790px;} +.span-21 {width: 830px;} +.span-22 {width: 870px;} +.span-23 {width: 910px;} +.span-24 {width:950px; margin-right:0;} + +/* Use these classes to set the width of an input. */ +input.span-1, textarea.span-1, input.span-2, textarea.span-2, input.span-3, textarea.span-3, input.span-4, textarea.span-4, input.span-5, textarea.span-5, input.span-6, textarea.span-6, input.span-7, textarea.span-7, input.span-8, textarea.span-8, input.span-9, textarea.span-9, input.span-10, textarea.span-10, input.span-11, textarea.span-11, input.span-12, textarea.span-12, input.span-13, textarea.span-13, input.span-14, textarea.span-14, input.span-15, textarea.span-15, input.span-16, textarea.span-16, input.span-17, textarea.span-17, input.span-18, textarea.span-18, input.span-19, textarea.span-19, input.span-20, textarea.span-20, input.span-21, textarea.span-21, input.span-22, textarea.span-22, input.span-23, textarea.span-23, input.span-24, textarea.span-24 { + border-left-width: 1px!important; + border-right-width: 1px!important; + padding-left: 5px!important; + padding-right: 5px!important; +} + +input.span-1, textarea.span-1 { width: 18px!important; } +input.span-2, textarea.span-2 { width: 58px!important; } +input.span-3, textarea.span-3 { width: 98px!important; } +input.span-4, textarea.span-4 { width: 138px!important; } +input.span-5, textarea.span-5 { width: 178px!important; } +input.span-6, textarea.span-6 { width: 218px!important; } +input.span-7, textarea.span-7 { width: 258px!important; } +input.span-8, textarea.span-8 { width: 298px!important; } +input.span-9, textarea.span-9 { width: 338px!important; } +input.span-10, textarea.span-10 { width: 378px!important; } +input.span-11, textarea.span-11 { width: 418px!important; } +input.span-12, textarea.span-12 { width: 458px!important; } +input.span-13, textarea.span-13 { width: 498px!important; } +input.span-14, textarea.span-14 { width: 538px!important; } +input.span-15, textarea.span-15 { width: 578px!important; } +input.span-16, textarea.span-16 { width: 618px!important; } +input.span-17, textarea.span-17 { width: 658px!important; } +input.span-18, textarea.span-18 { width: 698px!important; } +input.span-19, textarea.span-19 { width: 738px!important; } +input.span-20, textarea.span-20 { width: 778px!important; } +input.span-21, textarea.span-21 { width: 818px!important; } +input.span-22, textarea.span-22 { width: 858px!important; } +input.span-23, textarea.span-23 { width: 898px!important; } +input.span-24, textarea.span-24 { width: 938px!important; } + +/* Add these to a column to append empty cols. */ + +.append-1 { padding-right: 40px;} +.append-2 { padding-right: 80px;} +.append-3 { padding-right: 120px;} +.append-4 { padding-right: 160px;} +.append-5 { padding-right: 200px;} +.append-6 { padding-right: 240px;} +.append-7 { padding-right: 280px;} +.append-8 { padding-right: 320px;} +.append-9 { padding-right: 360px;} +.append-10 { padding-right: 400px;} +.append-11 { padding-right: 440px;} +.append-12 { padding-right: 480px;} +.append-13 { padding-right: 520px;} +.append-14 { padding-right: 560px;} +.append-15 { padding-right: 600px;} +.append-16 { padding-right: 640px;} +.append-17 { padding-right: 680px;} +.append-18 { padding-right: 720px;} +.append-19 { padding-right: 760px;} +.append-20 { padding-right: 800px;} +.append-21 { padding-right: 840px;} +.append-22 { padding-right: 880px;} +.append-23 { padding-right: 920px;} + +/* Add these to a column to prepend empty cols. */ + +.prepend-1 { padding-left: 40px;} +.prepend-2 { padding-left: 80px;} +.prepend-3 { padding-left: 120px;} +.prepend-4 { padding-left: 160px;} +.prepend-5 { padding-left: 200px;} +.prepend-6 { padding-left: 240px;} +.prepend-7 { padding-left: 280px;} +.prepend-8 { padding-left: 320px;} +.prepend-9 { padding-left: 360px;} +.prepend-10 { padding-left: 400px;} +.prepend-11 { padding-left: 440px;} +.prepend-12 { padding-left: 480px;} +.prepend-13 { padding-left: 520px;} +.prepend-14 { padding-left: 560px;} +.prepend-15 { padding-left: 600px;} +.prepend-16 { padding-left: 640px;} +.prepend-17 { padding-left: 680px;} +.prepend-18 { padding-left: 720px;} +.prepend-19 { padding-left: 760px;} +.prepend-20 { padding-left: 800px;} +.prepend-21 { padding-left: 840px;} +.prepend-22 { padding-left: 880px;} +.prepend-23 { padding-left: 920px;} + + +/* Border on right hand side of a column. */ +.border { + padding-right: 4px; + margin-right: 5px; + border-right: 1px solid #eee; +} + +/* Border with more whitespace, spans one column. */ +.colborder { + padding-right: 24px; + margin-right: 25px; + border-right: 1px solid #eee; +} + + +/* Use these classes on an element to push it into the +next column, or to pull it into the previous column. */ + + +.pull-1 { margin-left: -40px; } +.pull-2 { margin-left: -80px; } +.pull-3 { margin-left: -120px; } +.pull-4 { margin-left: -160px; } +.pull-5 { margin-left: -200px; } +.pull-6 { margin-left: -240px; } +.pull-7 { margin-left: -280px; } +.pull-8 { margin-left: -320px; } +.pull-9 { margin-left: -360px; } +.pull-10 { margin-left: -400px; } +.pull-11 { margin-left: -440px; } +.pull-12 { margin-left: -480px; } +.pull-13 { margin-left: -520px; } +.pull-14 { margin-left: -560px; } +.pull-15 { margin-left: -600px; } +.pull-16 { margin-left: -640px; } +.pull-17 { margin-left: -680px; } +.pull-18 { margin-left: -720px; } +.pull-19 { margin-left: -760px; } +.pull-20 { margin-left: -800px; } +.pull-21 { margin-left: -840px; } +.pull-22 { margin-left: -880px; } +.pull-23 { margin-left: -920px; } +.pull-24 { margin-left: -960px; } + +.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float: left; position:relative;} + + +.push-1 { margin: 0 -40px 1.5em 40px; } +.push-2 { margin: 0 -80px 1.5em 80px; } +.push-3 { margin: 0 -120px 1.5em 120px; } +.push-4 { margin: 0 -160px 1.5em 160px; } +.push-5 { margin: 0 -200px 1.5em 200px; } +.push-6 { margin: 0 -240px 1.5em 240px; } +.push-7 { margin: 0 -280px 1.5em 280px; } +.push-8 { margin: 0 -320px 1.5em 320px; } +.push-9 { margin: 0 -360px 1.5em 360px; } +.push-10 { margin: 0 -400px 1.5em 400px; } +.push-11 { margin: 0 -440px 1.5em 440px; } +.push-12 { margin: 0 -480px 1.5em 480px; } +.push-13 { margin: 0 -520px 1.5em 520px; } +.push-14 { margin: 0 -560px 1.5em 560px; } +.push-15 { margin: 0 -600px 1.5em 600px; } +.push-16 { margin: 0 -640px 1.5em 640px; } +.push-17 { margin: 0 -680px 1.5em 680px; } +.push-18 { margin: 0 -720px 1.5em 720px; } +.push-19 { margin: 0 -760px 1.5em 760px; } +.push-20 { margin: 0 -800px 1.5em 800px; } +.push-21 { margin: 0 -840px 1.5em 840px; } +.push-22 { margin: 0 -880px 1.5em 880px; } +.push-23 { margin: 0 -920px 1.5em 920px; } +.push-24 { margin: 0 -960px 1.5em 960px; } + +.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float: right; position:relative;} + + +/* Misc classes and elements +-------------------------------------------------------------- */ + +/* In case you need to add a gutter above/below an element */ +.prepend-top { + margin-top:1.5em; +} +.append-bottom { + margin-bottom:1.5em; +} + +/* Use a .box to create a padded box inside a column. */ +.box { + padding: 1.5em; + margin-bottom: 1.5em; + background: #E5ECF9; +} + +/* Use this to create a horizontal ruler across a column. */ +hr { + background: #ddd; + color: #ddd; + clear: both; + float: none; + width: 100%; + height: .1em; + margin: 0 0 1.45em; + border: none; +} + +hr.space { + background: #fff; + color: #fff; + visibility: hidden; +} + + +/* Clearing floats without extra markup + Based on How To Clear Floats Without Structural Markup by PiE + [http://www.positioniseverything.net/easyclearing.html] */ + +.clearfix:after, .container:after { + content: "\0020"; + display: block; + height: 0; + clear: both; + visibility: hidden; + overflow:hidden; +} +.clearfix, .container {display: block;} + +/* Regular clearing + apply to column that should drop below previous ones. */ + +.clear { clear:both; } diff --git a/css/src/grid.png b/css/src/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..d42a6c32c173bf067ee9fe1aa062afd915fb366c GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^8bB;0zy>5M`yHMFDYhhUcbETQz!~xV4p4-%z$3C4 zNPB>>+sSM@AS2n+#W5t}@Y@R;c@HQE9K9f5$RPVWzg=TNdlOHhh{)0WhV|C%K~?jM z*S=OS^3yz4TmAiI`@VAx6Brelo!DAbody p code { *white-space: normal; } + +/* IE 6&7 has problems with setting proper
margins. */ +hr { margin:-8px auto 11px; } + +/* Explicitly set interpolation, allowing dynamically resized images to not look horrible */ +img { -ms-interpolation-mode:bicubic; } + +/* Clearing +-------------------------------------------------------------- */ + +/* Makes clearfix actually work in IE */ +.clearfix, .container { display:inline-block; } +* html .clearfix, +* html .container { height:1%; } + + +/* Forms +-------------------------------------------------------------- */ + +/* Fixes padding on fieldset */ +fieldset { padding-top:0; } + +/* Makes classic textareas in IE 6 resemble other browsers */ +textarea { overflow:auto; } + +/* Fixes rule that IE 6 ignores */ +input.text, input.title, textarea { background-color:#fff; border:1px solid #bbb; } +input.text:focus, input.title:focus { border-color:#666; } +input.text, input.title, textarea, select { margin:0.5em 0; } +input.checkbox, input.radio { position:relative; top:.25em; } + +/* Fixes alignment of inline form elements */ +form.inline div, form.inline p { vertical-align:middle; } +form.inline label { position:relative;top:-0.25em; } +form.inline input.checkbox, form.inline input.radio, +form.inline input.button, form.inline button { + margin:0.5em 0; +} +button, input.button { position:relative;top:0.25em; } diff --git a/css/src/print.css b/css/src/print.css new file mode 100755 index 0000000..bbc7948 --- /dev/null +++ b/css/src/print.css @@ -0,0 +1,85 @@ +/* -------------------------------------------------------------- + + print.css + * Gives you some sensible styles for printing pages. + * See Readme file in this directory for further instructions. + + Some additions you'll want to make, customized to your markup: + #header, #footer, #navigation { display:none; } + +-------------------------------------------------------------- */ + +body { + line-height: 1.5; + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + color:#000; + background: none; + font-size: 10pt; +} + + +/* Layout +-------------------------------------------------------------- */ + +.container { + background: none; +} + +hr { + background:#ccc; + color:#ccc; + width:100%; + height:2px; + margin:2em 0; + padding:0; + border:none; +} +hr.space { + background: #fff; + color: #fff; + visibility: hidden; +} + + +/* Text +-------------------------------------------------------------- */ + +h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; } +code { font:.9em "Courier New", Monaco, Courier, monospace; } + +a img { border:none; } +p img.top { margin-top: 0; } + +blockquote { + margin:1.5em; + padding:1em; + font-style:italic; + font-size:.9em; +} + +.small { font-size: .9em; } +.large { font-size: 1.1em; } +.quiet { color: #999; } +.hide { display:none; } + + +/* Links +-------------------------------------------------------------- */ + +a:link, a:visited { + background: transparent; + font-weight:700; + text-decoration: underline; +} + +a:link:after, a:visited:after { + content: " (" attr(href) ")"; + font-size: 90%; +} + +/* If you're having trouble printing relative links, uncomment and customize this: + (note: This is valid CSS3, but it still won't go through the W3C CSS Validator) */ + +/* a[href^="/"]:after { + content: " (http://www.yourdomain.com" attr(href) ") "; +} */ diff --git a/css/src/reset.css b/css/src/reset.css new file mode 100755 index 0000000..09d9131 --- /dev/null +++ b/css/src/reset.css @@ -0,0 +1,45 @@ +/* -------------------------------------------------------------- + + reset.css + * Resets default browser CSS. + +-------------------------------------------------------------- */ + +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, code, +del, dfn, em, img, q, dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, dialog, figure, footer, header, +hgroup, nav, section { + margin: 0; + padding: 0; + border: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; +} + +article, aside, dialog, figure, footer, header, +hgroup, nav, section { + display:block; +} + +body { + line-height: 1.5; +} + +/* Tables still need 'cellspacing="0"' in the markup. */ +table { border-collapse: separate; border-spacing: 0; } +caption, th, td { text-align: left; font-weight: normal; } +table, td, th { vertical-align: middle; } + +/* Remove possible quote marks (") from ,
. */ +blockquote:before, blockquote:after, q:before, q:after { content: ""; } +blockquote, q { quotes: "" ""; } + +/* Remove annoying border on linked images. */ +a img { border: none; } diff --git a/css/src/typography.css b/css/src/typography.css new file mode 100644 index 0000000..a1cfe27 --- /dev/null +++ b/css/src/typography.css @@ -0,0 +1,106 @@ +/* -------------------------------------------------------------- + + typography.css + * Sets up some sensible default typography. + +-------------------------------------------------------------- */ + +/* Default font settings. + The font-size percentage is of 16px. (0.75 * 16px = 12px) */ +html { font-size:100.01%; } +body { + font-size: 75%; + color: #222; + background: #fff; + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; +} + + +/* Headings +-------------------------------------------------------------- */ + +h1,h2,h3,h4,h5,h6 { font-weight: normal; color: #111; } + +h1 { font-size: 3em; line-height: 1; margin-bottom: 0.5em; } +h2 { font-size: 2em; margin-bottom: 0.75em; } +h3 { font-size: 1.5em; line-height: 1; margin-bottom: 1em; } +h4 { font-size: 1.2em; line-height: 1.25; margin-bottom: 1.25em; } +h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.5em; } +h6 { font-size: 1em; font-weight: bold; } + +h1 img, h2 img, h3 img, +h4 img, h5 img, h6 img { + margin: 0; +} + + +/* Text elements +-------------------------------------------------------------- */ + +p { margin: 0 0 1.5em; } +p img.left { float: left; margin: 1.5em 1.5em 1.5em 0; padding: 0; } +p img.right { float: right; margin: 1.5em 0 1.5em 1.5em; } + +a:focus, +a:hover { color: #000; } +a { color: #009; text-decoration: underline; } + +blockquote { margin: 1.5em; color: #666; font-style: italic; } +strong { font-weight: bold; } +em,dfn { font-style: italic; } +dfn { font-weight: bold; } +sup, sub { line-height: 0; } + +abbr, +acronym { border-bottom: 1px dotted #666; } +address { margin: 0 0 1.5em; font-style: italic; } +del { color:#666; } + +pre { margin: 1.5em 0; white-space: pre; } +pre,code,tt { font: 1em 'andale mono', 'lucida console', monospace; line-height: 1.5; } + + +/* Lists +-------------------------------------------------------------- */ + +li ul, +li ol { margin: 0; } +ul, ol { margin: 0 1.5em 1.5em 0; padding-left: 3.333em; } + +ul { list-style-type: disc; } +ol { list-style-type: decimal; } + +dl { margin: 0 0 1.5em 0; } +dl dt { font-weight: bold; } +dd { margin-left: 1.5em;} + + +/* Tables +-------------------------------------------------------------- */ + +table { margin-bottom: 1.4em; width:100%; } +th { font-weight: bold; } +thead th { background: #c3d9ff; } +th,td,caption { padding: 4px 10px 4px 5px; } +tr.even td { background: #e5ecf9; } +tfoot { font-style: italic; } +caption { background: #eee; } + + +/* Misc classes +-------------------------------------------------------------- */ + +.small { font-size: .8em; margin-bottom: 1.875em; line-height: 1.875em; } +.large { font-size: 1.2em; line-height: 2.5em; margin-bottom: 1.25em; } +.hide { display: none; } + +.quiet { color: #666; } +.loud { color: #000; } +.highlight { background:#ff0; } +.added { background:#060; color: #fff; } +.removed { background:#900; color: #fff; } + +.first { margin-left:0; padding-left:0; } +.last { margin-right:0; padding-right:0; } +.top { margin-top:0; padding-top:0; } +.bottom { margin-bottom:0; padding-bottom:0; } diff --git a/css/transmission.css b/css/transmission.css new file mode 100644 index 0000000..586d3fc --- /dev/null +++ b/css/transmission.css @@ -0,0 +1,497 @@ +body { + margin: 0px; + padding: 0px; + font-size: 62.5%; /* 10px */ + color: #444; + background: url("../images/kettu_bg.png") repeat-y 50% 0; + font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; +} + +h1, h2, h3, h4, h5, h6 { + color: #444; +} + +h3 { + font-size: 1.5em; + line-height: 1; + margin-bottom: .5em; +} + + +#info, +#info h1, #info h2, #info h3, #info h4, #info h5, #info h6 { + color: #FFF; +} + +/* structure */ + +header, footer { + position: fixed; + height: 2em; + width: 100%; + line-height: 2; + z-index: 10; +} + +header { + height: 120px; + background: url("../images/kettu_bg_header.png") no-repeat 50% 0; +} + +.container { + padding-top: 120px; + overflow: hidden; +} + +footer { + background-color: #444; + border-top: 1px solid #444; + border-bottom: 1px solid #999; + left: 0; + right: 0; + bottom: 0; + text-align: right; + box-shadow: 0 -10px 10px rgba(0,0,0,.1); + -webkit-box-shadow: 0 -10px 10px rgba(0,0,0,.1); + -moz-box-shadow: 0 -10px 10px rgba(0,0,0,.1); +} + +footer nav { + width: 950px; + margin: 0 auto; + padding: 0; +} + +footer nav ul { + margin: 0; + padding: 0; +} + +/* flash */ +#flash { + display: none; + position: fixed; + top: 1em; + right: 1em; + padding: 1em; + font-weight: bold; + color: #FFF; + background-color: #444; + background-color: rgba(0,0,0,0.7); + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; +} + + +/* bottom navigation */ +nav ul li { + display: inline-block; +} + +nav ul li a { + width: 158px; + height: 2em; + display: block; + color: #DDD; + text-shadow: 1px 1px 1px #000; + text-decoration: none; + text-align: center; + padding: 0; + border-right: 1px solid #555; + border-left: 1px solid #333; + -webkit-transition-property: color, background; + -webkit-transition-duration: 0.4s, 0.1s; + -webkit-transition-timing-function: linear, ease-in; + -moz-transition-property: color, background; + -moz-transition-duration: 0.4s, 0.1s; + -moz-transition-timing-function: linear, ease-in; + -o-transition-property: color, background; + -o-transition-duration: 0.4s, 0.1s; + -o-transition-timing-function: linear, ease-in; +} +nav ul li a.active, +nav ul li a:focus, +nav ul li a:hover { + color: #FFF; + text-shadow: 1px 1px 1px #000; + background-color: #333; +} + + +#globalUpAndDownload { + width: 930px; + height: 20px; + margin: 3.5em auto 0 auto; + padding: 0; + padding-right: 20px; + text-align: right; +} + +.torrent { + background-color: #FFF; + overflow: hidden; +} + +.torrent.even { + background-color: #F8F8F8; +} + +.torrent { + -webkit-transition-property: background; + -webkit-transition-duration: 0.4s, 0.1s; + -webkit-transition-timing-function: linear, ease-in; + -moz-transition-property: background; + -moz-transition-duration: 0.4s, 0.1s; + -moz-transition-timing-function: linear, ease-in; + -o-transition-property: background; + -o-transition-duration: 0.4s, 0.1s; + -o-transition-timing-function: linear, ease-in; +} + +.torrent.active { + background-color: #FFF8DC !important; + color: #000; +} +.torrent.active h3 { + color: #000; + text-shadow: 1px 1px 10px #FFF; +} + +.torrent .ui-widget-header-uploading { + border: 1px solid #AAA; + color: #444; + font-weight: bold; + background: url(jquery/images/ui-bg_highlight-soft_60_4ca20b_1x100.png) repeat-x; +} + +.torrent .ui-widget-header-downloading { + border: 1px solid #AAA; + color: #444; + font-weight: bold; + background: url(jquery/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png) repeat-x; +} + +.torrent .ui-widget-header-meta { + border: 1px solid #AAA; + color: #444; + font-weight: bold; + background: url(jquery/images/ui-bg_highlight-hard_75_f5f5b5_1x100.png) repeat-x; +} + +.torrent .ui-widget-header-paused { + border: 1px solid #AAA; + color: #444; + font-weight: bold; + background: url(jquery/images/ui-bg_highlight-soft_75_cccccc_1x100.png) repeat-x; +} + +.torrent .pauseAndActivateButton { + display: block; + color: transparent; + background-color: transparent; + border: 0px; + float: right; + margin-right: 10px; +} + +.torrent .pauseAndActivateButton.activate { + background-image: url(images/control_play.png); + width: 15px; + height: 15px; +} + +.torrent .pauseAndActivateButton.pause { + background-image: url(images/control_pause.png); + width: 15px; + height: 15px; +} + +.deleteButton { + display: block; + color: transparent; + background-color: transparent; + border: 0px; + float: right; + margin-right: 10px; + background-image: url(images/control_eject.png); + width: 15px; + height: 15px; +} + +.torrent .statusString { + clear: both; + color: #999; +} + +.torrent .name { + +} + +.torrent .progressDetails { + margin: 0px; + padding: 0px; + color: #555; +} + +/* filterbar */ +#filterbar { + width: 920px; + height: 30px; + margin: 34px auto 0 auto; + border-bottom: 1px solid #CCC; + overflow: hidden; + padding-left: 30px; +} + +#filterbar a { + color: #777; + text-decoration: none; + margin-right: 4px; +} + +#filterbar a:hover { + color: #000; +} + +#filterbar a.active { + color: #444; +} + +#filterbar #filter_sort_links { + padding-right: 6px; + width: 400px; +} +#filterbar #filter_sort_links #sorts { + display: none; +} + + +/* torrents list */ +#torrents { + width: 100% !important; + padding: 0px; + margin: 0px; +} + +.main { + position: relative; + overflow: hidden; +} + +.main.info { + background: transparent url(../images/kettu_bg_info.png) repeat-y 100% 0; +} + +.main.info .torrents_container { + display: block; + width: 550px !important; + min-height: 10px; + float: left; +} +/* torrents list container */ + +#torrents li { + display: block; + padding: 6px 10px 6px 30px; + border-bottom: solid 1px #DDD; +} +#torrents li.compact { +padding: 3px 10px 3px 30px; +} + +/* info */ +#info { + width: 379px; + float: left; + padding: 1em 10px 1.5em 10px; + background-color: #444; + display: none; + border-left: 1px solid #DDD; + overflow: auto; +} + +#info table { width: auto;} + +#info .content { + width: 100%; + margin-bottom: 1.5em; + padding-top: .5em; + border-top: 1px solid #555; + border-bottom: 1px solid #555; +} +#info .peers { + margin-bottom: 20px; +} +#info .peers th { + padding: 7px; + margin-right: 10px; + text-align: left; + font-weight: normal; +} + +#info a { + color: #FFF; +} + +#info a:hover, +#info a:active, +#info a:focus { + color: #DDD; +} + +#info .peers th.long { + width: 40%; +} +#info ul { + list-style-type: none; + padding-left: 0; +} +#info li { + padding-bottom: 6px; + padding-left: 0px; + margin-left: 0px; +} + +#info input { + border: solid 2px #dddddd; + padding: 1px; +} + +#info input.short { + width: 35px; +} + +#info input.submit { + background-color: white; + padding: 4px; +} + +#info input.long { + width: 250px; +} + +#info input.file { + border: 0px; +} + +#info td.long { + width: 62px; +} + +#info #port-open { + margin-top: 3px; + width: 15px; + height: 15px; + background-color: red; + float: right; + display: none; +} + +#info #port-open.active { + background-color: green; +} + +#info #port-open.waiting { + background-image: url('images/spinner.gif'); + background-color: transparent !important; +} + +#info #version { + color: #555; +} + +#info .menu { + margin: -1em -10px 1em -10px; + padding: 0; + overflow: hidden; + background-color: #444; + border-top: 1px solid #444; + border-bottom: 1px solid #555; +} +#info .menu li { + float: left; + margin-right: 0; + padding-bottom: 0; +} + +#info .menu li a { + width: 77px; + height: 2em; + line-height: 2em; + display: block; + padding: 0; + border-right: 1px solid #555; + border-left: 1px solid #333; +} + + +#info .percent_done { + font-size: 12px; + color: #666666; +} + +#pie_chart { + margin-left: 30px; + margin-top: 30px; +} + +.menu-item { + text-decoration: none; + color: #DDD; +} +.menu-item.active { + color: #FFF; +} + +.progress { + overflow: hidden; +} + +.progressbar { + width: 80%; +} + +.ui-progressbar { + margin: 4px 4px 4px 0; +} + +/* compact view */ +.compact .progressbar { + width: 60px; + display: inline-block; +} + +.compact .name { + display: inline-block; +} + +.compact .statusString { + display: inline-block; +} + +.compact .buttons { + display: block; + float: right; +} + +.compact .buttons .pauseAndActivateButton { + display: inline-block; +} + +.compact .buttons form { + display: inline-block; +} + +#data { + display: none; +} + +.graphs { + margin-bottom: 50px; +} + +.hint { + color: #888888; + font-size: 80%; +} \ No newline at end of file diff --git a/features/sort_and_filter_torrents.feature b/features/sort_and_filter_torrents.feature new file mode 100644 index 0000000..bc239af --- /dev/null +++ b/features/sort_and_filter_torrents.feature @@ -0,0 +1,78 @@ +Feature: sort and filter torrents + In order to have a better overview + As a user + I want to sort and filter torrents + + Scenario: filter torrents + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the stati "4, 8, 16" + When I go to the start page + And I follow "Filter" + And I follow "Downloading" + Then I should see "Mutant Ninja Turtles" + But I should not see "Donald Duck" + And I should not see "Saber Riders" + When I follow "Seeding" + Then I should see "Donald Duck" + But I should not see "Mutant Ninja Turtles" + And I should not see "Saber Riders" + When I follow "Paused" + Then I should see "Saber Riders" + But I should not see "Mutant Ninja Turtles" + And I should not see "Donald Duck" + When I follow "All" + Then I should see "Mutant Ninja Turtles" + And I should see "Donald Duck" + And I should see "Saber Riders" + When I go to the paused filtered torrents page + Then I should see "Saber Riders" + But I should not see "Mutant Ninja Turtles" + When I follow "Enable Compact View" + Then I should see "Saber Riders" + But I should not see "Mutant Ninja Turtles" + + Scenario: sort torrents by name + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the stati "4, 8, 16" + When I go to the name sorted torrents page + Then I should see "Donald Duck" before "Mutant Ninja Turtles" + And I should see "Mutant Ninja Turtles" before "Saber Riders" + + Scenario: sort torrents by status + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the stati "8, 16, 4" + When I go to the state sorted torrents page + Then I should see "Saber Riders" before "Mutant Ninja Turtles" + And I should see "Mutant Ninja Turtles" before "Donald Duck" + + Scenario: sort torrents by activity + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the download rates "16000, 8000, 4000" + When I go to the activity sorted torrents page + Then I should see "Mutant Ninja Turtles" before "Donald Duck" + And I should see "Donald Duck" before "Saber Riders" + + Scenario: sort torrents by age + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the date added "87742, 84253, 81181" + When I go to the age sorted torrents page + Then I should see "Mutant Ninja Turtles" before "Donald Duck" + And I should see "Donald Duck" before "Saber Riders" + + Scenario: sort torrents by progress + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the left until done "8, 4, 16" + When I go to the progress sorted torrents page + Then I should see "Saber Riders" before "Mutant Ninja Turtles" + And I should see "Mutant Ninja Turtles" before "Donald Duck" + + Scenario: sort torrents by queue + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the ids "1, 2, 3" + When I go to the queue sorted torrents page + Then I should see "Mutant Ninja Turtles" before "Donald Duck" + And I should see "Donald Duck" before "Saber Riders" + + Scenario: filter and sort at the same time + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the stati "4, 4, 16" + When I go to the name sorted torrents page + And I follow "Filter" + And I follow "Downloading" + Then I should see "Donald Duck" before "Mutant Ninja Turtles" + And I should not see "Saber Riders" + + + \ No newline at end of file diff --git a/features/step_definitions/common_culerity_steps.rb b/features/step_definitions/common_culerity_steps.rb new file mode 100644 index 0000000..9182ad3 --- /dev/null +++ b/features/step_definitions/common_culerity_steps.rb @@ -0,0 +1,169 @@ +require 'culerity' + +When /I press "(.*)"/ do |button| + button = [$browser.button(:text, button), $browser.button(:id, button)].find(&:exist?) + button.click + When 'I wait for the AJAX call to finish' +end + +When /I click "(.*)"/ do |link| + When "I follow \"#{link}\"" +end + +When /I follow "(.*)"/ do |link| + _link = [[:text, /^#{Regexp.escape(link)}$/], [:id, link], [:title, link]].map{|args| $browser.link(*args)}.find{|__link| __link.exist?} + raise "link \"#{link}\" not found" unless _link + _link.click + When 'I wait for the AJAX call to finish' +end + +When /I follow \/(.*)\// do |link| + $browser.link(:text, /#{link}/).click + When 'I wait for the AJAX call to finish' +end + +When /I fill in "(.*)" with "(.*)"/ do |field, value| + find_by_label_or_id(:text_field, field).set value +end + +When /I attach "(.*)" to "(.*)"/ do |value, field| + $browser.file_field(:id, find_label(field).for).set(value) +end + +When /I check "(.*)"/ do |field| + find_by_label_or_id(:check_box, field).set true +end + +def find_by_label_or_id(element, field) + begin + $browser.send(element, :id, find_label(/#{field}/).for) + rescue #Celerity::Exception::UnknownObjectException + $browser.send element, :id, field + end +end + +When /^I uncheck "(.*)"$/ do |field| + $browser.check_box(:id, find_label(field).for).set(false) +end + +When /^I select "([^"]+)" from "([^"]+)"$/ do |value, field| + find_by_label_or_id(:select_list, field).select value +end + +When /^I select "([^\"]*)"$/ do |value| + $browser.option(:text => value).select +end + +When /I choose "(.*)"/ do |field| + $browser.radio(:id, find_label(field).for).set(true) +end + +When /I go to the (.+)/ do |path| + $browser.goto host + path_to(path) + When 'I wait for the AJAX call to finish' +end + +When /^I wait for the AJAX call to finish$/ do + # puts 'waiting for the AJAX call ...' + $browser.wait_while do + begin + count = $browser.execute_script("window.running_ajax_calls").to_i + count.to_i > 0 + rescue => e + if e.message.include?('HtmlunitCorejsJavascript::Undefined') + raise "For 'I wait for the AJAX call to finish' to work please include culerity.js after including jQuery. If you don't use jQuery please rewrite culerity.js accordingly." + else + raise(e) + end + end + end + sleep 0.5 +end + +When /^I visit "([^\"]+)"$/ do |url| + $browser.goto host + url +end + +Then /^I should see "([^\"]*)"$/ do |text| + Then "I should see /#{Regexp.escape(text)}/" +end + +Then /^I should see \/(.*)\/$/ do |text| + # if we simply check for the browser.html content we don't find content that has been added dynamically, e.g. after an ajax call + div = $browser.div(:text, /#{text}/) + begin + div.html + rescue + raise("div with '#{text}' not found") + end +end + +Then /I should not see "(.*)"/ do |text| + div_nil = false + if ($browser.div(:text, /#{text}/) rescue nil) + div_nil = true + else + if $browser.div(:text, /#{text}/).style.match(/-9999px/) + div_nil = true + end + end + div_nil.should be_true +end + +Then /I should see the text "(.*)"/ do |text| + $browser.html.should include(text) +end + +Then /I should not see the text "(.*)"/ do |text| + $browser.html.should_not include(text) +end + +Then /I should see no link "([^\"]+)"/ do |text| + $browser.link(:text => text).should_not exist +end + +Then /I should not find the page "([^\"]+)"/ do |url| + no_exception = false + begin + $browser.goto host + url + no_exception = true + rescue => e + e.message.should =~ /404/ + end + no_exception.should be_false +end + +Then /^"([^\"]*)" should be chosen$/ do |field| + find_by_label_or_id(:radio, field).should be_checked +end + +Then /^"([^\"]*)" should be checked$/ do |field| + find_by_label_or_id(:check_box, field).should be_checked +end + +Then /^"([^\"]*)" should not be checked$/ do |field| + find_by_label_or_id(:check_box, field).should_not be_checked +end + +Then /^"([^\"]*)" should be selected$/ do |selection| + $browser.option(:text => selection).should be_selected +end + +When 'show response' do + p $browser.url + open_response_in_browser +end + + +def find_label(text) + $browser.label :text, text +end + +def open_response_in_browser + tmp_file = '/tmp/culerity_results.html' + FileUtils.rm_f tmp_file + File.open(tmp_file, 'w') do |f| + f << $browser.div(:id, 'container').html + end + `open #{tmp_file}` +end \ No newline at end of file diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb new file mode 100644 index 0000000..28c5e2f --- /dev/null +++ b/features/step_definitions/common_steps.rb @@ -0,0 +1,10 @@ +Then /^I should see "([^\"]*)" before "([^\"]*)"$/ do |first, second| + div = $browser.div('container') + unless div.html.match(/#{first}.*#{second}/im) + raise("#{first} can't be found before #{second}") + end +end + +When /I wait for "(\d)"s/ do |seconds| + sleep seconds.to_i +end \ No newline at end of file diff --git a/features/step_definitions/torrent_info_steps.rb b/features/step_definitions/torrent_info_steps.rb new file mode 100644 index 0000000..d31611f --- /dev/null +++ b/features/step_definitions/torrent_info_steps.rb @@ -0,0 +1,37 @@ +Given /a torrent with the tracker "([^\"]+)" a last announce timestamp of "(\d+)" and a next announce in 30 minutes/ do |url, last_announce_timestamp| + Given 'a torrent' + next_announce_timestamp = (Time.now.to_i + 1800).to_s + file_path = File.dirname(__FILE__) + '/../support/singular.json' + File.open(file_path, 'w') {|f| f << '{"arguments": {"torrents": [{"id": 1, "name": "test", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": 0,"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987, "downloadDir": "/downloads", "creator": "chaot", "hashString": "83ui72GYAYDg27ghg22e22e4235215", "comment": "", "isPrivate": true, "downloadedEver": 50, "haveString": "", "errorString": "", "peersGettingFromUs": 0, "peersSendingToUs": 0, "files": [], "pieceCount": 20, "pieceSize": 5, "trackerStats": [{"lastAnnounceTime": "' + last_announce_timestamp + '", "host": "' + url + '", "nextAnnounceTime": "' + next_announce_timestamp + '", "lastScrapeTime": "12345678", "seederCount": 0, "leecherCount": 0, "downloadCount": 1}]}]}}' } +end + +Given /a torrent with the file "([^\"]+)" which has a size of (\d+) bytes and has already downloaded (\d+) bytes/ do |file_name, size, already_downloaded| + Given 'a torrent' + file_path = File.dirname(__FILE__) + '/../support/singular.json' + File.open(file_path, 'w') {|f| f << '{"arguments": {"torrents": [{"id": 1, "name": "test", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": 0,"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987, "downloadDir": "/downloads", "creator": "chaot", "hashString": "83ui72GYAYDg27ghg22e22e4235215", "comment": "", "isPrivate": true, "downloadedEver": 50, "haveString": "", "errorString": "", "peersGettingFromUs": 0, "peersSendingToUs": 0, "files": [], "pieceCount": 20, "pieceSize": 5, "files": [{"key": "1", "bytesCompleted": "' + already_downloaded + '", "length": "' + size + '", "name": "' + file_name + '"}]}]}}' } +end + +Given /a torrent with a peer with IP "([^\"]+)" and client name "([^\"]+)"/ do |ip, client_name| + Given 'a torrent' + file_path = File.dirname(__FILE__) + '/../support/singular.json' + File.open(file_path, 'w') {|f| f << '{"arguments": {"torrents": [{"id": 1, "name": "test", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": 0,"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987, "downloadDir": "/downloads", "creator": "chaot", "hashString": "83ui72GYAYDg27ghg22e22e4235215", "comment": "", "isPrivate": true, "downloadedEver": 50, "haveString": "", "errorString": "", "peersGettingFromUs": 0, "peersSendingToUs": 0, "files": [], "pieceCount": 20, "pieceSize": 5, "peers": [{"address": "' + ip + '", "clientName": "' + client_name + '", "rateToClient": 0, "rateToPeer": 0, "progress": 0}]}]}}' } +end + +Given /^a torrent$/ do + file_path = File.dirname(__FILE__) + '/../support/plural.json' + File.open(file_path, 'w') {|f| f << '{"arguments": {"torrents": [{"id": 1, "name": "test", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": 0,"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987}]}}' } +end + +When /there is new data for the torrent with new IP "([^\"]+)" and new client name "([^\"]+)"/ do |ip, client_name| + file_path = File.dirname(__FILE__) + '/../support/singular.json' + File.open(file_path, 'w') {|f| f << '{"arguments": {"torrents": [{"id": 1, "name": "test", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": 0,"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987, "downloadDir": "/downloads", "creator": "chaot", "hashString": "83ui72GYAYDg27ghg22e22e4235215", "comment": "", "isPrivate": true, "downloadedEver": 50, "haveString": "", "errorString": "", "peersGettingFromUs": 0, "peersSendingToUs": 0, "files": [], "pieceCount": 20, "pieceSize": 5, "peers": [{"address": "' + ip + '", "clientName": "' + client_name + '", "rateToClient": 0, "rateToPeer": 0, "progress": 0}]}]}}' } +end + +Then /I should see a countdown time of about 30 minutes/ do + $browser.div(:text, /(29|30) min, (\d+) sec/).should be_exist +end + +Then /I should see a formatted time for the timestamp/ do + time = Time.at(1266830556).strftime("%m/%e/%Y %k:%M").sub(/^0/, '') + $browser.div(:text, /#{time}/).should be_exist +end diff --git a/features/step_definitions/torrent_steps.rb b/features/step_definitions/torrent_steps.rb new file mode 100644 index 0000000..c93ae21 --- /dev/null +++ b/features/step_definitions/torrent_steps.rb @@ -0,0 +1,67 @@ +Given /a torrent with the name "([^\"]+)"( and a download rate of "([^\"]+)" B\/s)?$/ do |name, has_download_rate, download_rate| + file_path = File.dirname(__FILE__) + '/../support/plural.json' + File.open(file_path, 'w') do |f| + f << '{"arguments": {"torrents": [{"id": 4, "name": "' + name + '", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": ' + (has_download_rate ? download_rate : '0') + ',"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987}]}}' + end +end + +Given /the torrent "([^\"]+)" has more info like the download directory which is "([^\"]+)"/ do |name, download_dir| + file_path = File.dirname(__FILE__) + '/../support/singular.json' + File.open(file_path, 'w') do |f| + f << '{"arguments": {"torrents": [{"id": 4, "name": "' + name + '", "status": 4, "totalSize": 100, "sizeWhenDone": 100,"leftUntilDone": 50, "eta": 0, "uploadedEver": 0, "uploadRatio": 0, "rateDownload": 0,"rateUpload": 0, "metadataPercentComplete": 1, "addedDate": 27762987, "downloadDir": "' + download_dir + '", "creator": "chaot", "hashString": "83ui72GYAYDg27ghg22e22e4235215", "comment": "", "isPrivate": true, "downloadedEver": 50, "haveString": "", "errorString": "", "peersGettingFromUs": 0, "peersSendingToUs": 0, "files": [], "pieceCount": 20, "pieceSize": 5}]}}' + end +end + +Given /three torrents with the names "([^\"]+)" and the (download rates|stati|date added|left until done|ids) "([^\"]+)"/ do |names_string, attribute, attribute_string| + names = names_string.split(',') + attributes = attribute_string.split(',') + torrents = [] + + case(attribute) + when 'stati' + names.each_with_index do |name , i| + torrents.push({"id" => i, "name" => name.strip, "status" => attributes[i].to_i, "totalSize" => 100, "sizeWhenDone" => 100, "leftUntilDone" => 50, "eta" => 0, "uploadedEver" => 0, "uploadRatio" => 0, "rateDownload" => 0,"rateUpload" => 0, "metadataPercentComplete" => 1, "addedDate" => 27762987}) + end + when 'download rates' + names.each_with_index do |name , i| + torrents.push({"id" => i, "name" => name.strip, "status" => 4, "totalSize" => 100, "sizeWhenDone" => 100, "leftUntilDone" => 50, "eta" => 0, "uploadedEver" => 0, "uploadRatio" => 0, "rateDownload" => attributes[i].to_i, "rateUpload" => 0, "metadataPercentComplete" => 1, "addedDate" => 27762987}) + end + when 'date added' + names.each_with_index do |name , i| + torrents.push({"id" => i, "name" => name.strip, "status" => 4, "totalSize" => 100, "sizeWhenDone" => 100, "leftUntilDone" => 50, "eta" => 0, "uploadedEver" => 0, "uploadRatio" => 0, "rateDownload" => 0, "rateUpload" => 0, "metadataPercentComplete" => 1, "addedDate" => attributes[i].to_i}) + end + when 'left until done' + names.each_with_index do |name , i| + torrents.push({"id" => i, "name" => name.strip, "status" => 4, "totalSize" => 100, "sizeWhenDone" => 100, "leftUntilDone" => attributes[i].to_i, "eta" => 0, "uploadedEver" => 0, "uploadRatio" => 0, "rateDownload" => 0, "rateUpload" => 0, "metadataPercentComplete" => 1, "addedDate" => 27762987}) + end + when 'ids' + names.each_with_index do |name , i| + torrents.push({"id" => attributes[i].to_i, "name" => name.strip, "status" => 4, "totalSize" => 100, "sizeWhenDone" => 100, "leftUntilDone" => 50, "eta" => 0, "uploadedEver" => 0, "uploadRatio" => 0, "rateDownload" => 0, "rateUpload" => 0, "metadataPercentComplete" => 1, "addedDate" => 27762987}) + end + end + + file_path = File.dirname(__FILE__) + '/../support/plural.json' + File.open(file_path, 'w') do |f| + f << {"arguments" => {"torrents" => torrents}}.to_json + end +end + +When /^I click on the torrent$/ do + $browser.li(:id, '4').click +end + +When /^I double click on the torrent$/ do + $browser.li(:id, '4').double_click +end + +Then /the torrent should be highlighted/ do + $browser.li(:id, '4').attribute_value(:class).should include('active') +end + +When /I double click on the torrent "([^\"]+)"/ do |id| + $browser.li(:id, id).double_click +end + +When /I click on the torrent "([^\"]+)"/ do |id| + $browser.li(:id, id).click +end \ No newline at end of file diff --git a/features/support/culerity.js b/features/support/culerity.js new file mode 100644 index 0000000..2db12c9 --- /dev/null +++ b/features/support/culerity.js @@ -0,0 +1,27 @@ +// this allows culerity to wait until all ajax requests have finished +jQuery(function($) { + var original_ajax = $.ajax; + var count_down = function(callback) { + return function() { + try { + if(callback) { + callback.apply(this, arguments); + }; + } catch(e) { + window.running_ajax_calls -= 1; + throw(e); + } + window.running_ajax_calls -= 1; + } + }; + window.running_ajax_calls = 0; + + var ajax_with_count = function(options) { + window.running_ajax_calls += 1; + options.success = count_down(options.success); + options.error = count_down(options.error); + original_ajax(options); + }; + + $.ajax = ajax_with_count; +}); \ No newline at end of file diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000..8786fda --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,32 @@ +require 'rubygems' +require 'culerity' +require 'cucumber/formatter/unicode' +require 'json' + +Symbol.class_eval do + def to_proc + Proc.new{|object| object.send(self)} + end +end unless :symbol.respond_to?(:to_proc) + +Before do + $testapp = IO.popen("/usr/bin/env ruby #{File.dirname(__FILE__) + '/testapp.rb'} 2>/dev/null 1>/dev/null", 'r+') + $server ||= Culerity::run_server + $browser = Culerity::RemoteBrowserProxy.new $server, {:browser => :firefox, :javascript_exceptions => true, :resynchronize => false, :status_code_exceptions => true} + $browser.log_level = :warning +end + +def host + 'http://localhost:4567' +end + +def app + 'kettu' +end + +at_exit do + $browser.exit if $browser + $server.close if $server + Process.kill(9, $testapp.pid.to_i) if $testapp # see why ruby process is still running + Dir.glob(File.dirname(__FILE__) + '/*.json').each {|f| File.delete(f)} +end diff --git a/features/support/paths.rb b/features/support/paths.rb new file mode 100644 index 0000000..bd73e81 --- /dev/null +++ b/features/support/paths.rb @@ -0,0 +1,26 @@ +module NavigationHelpers + def path_to(page_name) + case page_name + when /start page/ + "/" + when /paused filtered torrents page/ + "/index.html#/torrents?filter=paused" + when /state sorted torrents page/ + "/index.html#/torrents?sort=state" + when /name sorted torrents page/ + "/index.html#/torrents?sort=name" + when /activity sorted torrents page/ + "/index.html#/torrents?sort=activity" + when /age sorted torrents page/ + "/index.html#/torrents?sort=age" + when /progress sorted torrents page/ + "/index.html#/torrents?sort=progress" + when /queue sorted torrents page/ + "/index.html#/torrents?sort=queue" + else + raise "Can't find mapping from \"#{page_name}\" to a path." + end + end +end + +World(NavigationHelpers) \ No newline at end of file diff --git a/features/support/testapp.rb b/features/support/testapp.rb new file mode 100644 index 0000000..beeb02e --- /dev/null +++ b/features/support/testapp.rb @@ -0,0 +1,16 @@ +require 'rubygems' +require 'sinatra' +require 'json' + +set :static, true +set :public, File.dirname(__FILE__) + '/../../' + +get '/' do + redirect "/index.html" +end + +post '/transmission/rpc' do + content_type :json + file_name = params.keys.first.match(/ids/) ? "singular" : "plural" + File.read(File.dirname(__FILE__) + "/#{file_name}.json") +end \ No newline at end of file diff --git a/features/torrent_info.feature b/features/torrent_info.feature new file mode 100644 index 0000000..7d8aab9 --- /dev/null +++ b/features/torrent_info.feature @@ -0,0 +1,70 @@ +Feature: Torrent info + In order to evaluate the current state of a torrent + As a user + I want to see more information about the torrent + + Scenario: double clicking on a torrent opens info + Given a torrent with the name "Mutant Ninja Turtles" + And the torrent "Mutant Ninja Turtles" has more info like the download directory which is "/downloads" + When I go to the start page + And I wait for the AJAX call to finish + And I double click on the torrent + And I wait for the AJAX call to finish + Then I should see "/downloads" + When I double click on the torrent + Then I should not see "/downloads" + + Scenario: double clicking on a torrent and then single clicking on another one updates info + Given three torrents with the names "Mutant Ninja Turtles, Donald Duck, Saber Riders" and the ids "1, 2, 3" + And the torrent "Mutant Ninja Turtles" has more info like the download directory which is "/downloads" + When I go to the start page + And I double click on the torrent "1" + And the torrent "Donald Duck" has more info like the download directory which is "/my_torrents" + And I click on the torrent "2" + Then I should see "/my_torrents" + + Scenario: info displays tracker information + Given a torrent with the tracker "my.tracker.com:1234" a last announce timestamp of "1266830556" and a next announce in 30 minutes + When I go to the start page + And I wait for the AJAX call to finish + And I double click on the torrent "1" + And I wait for the AJAX call to finish + And I follow "Trackers" + Then I should see "my.tracker.com:1234" + And I should see a formatted time for the timestamp + And I should see a countdown time of about 30 minutes + + Scenario: info displays file information + Given a torrent with the file "README.md" which has a size of 12 bytes and has already downloaded 6 bytes + When I go to the start page + And I wait for the AJAX call to finish + And I double click on the torrent "1" + And I wait for the AJAX call to finish + And I follow "Files" + Then I should see "12 bytes" + And I should see "50%" + + Scenario: info displays peer information + Given a torrent with a peer with IP "1.2.3.4" and client name "Transmission Rocks" + When I go to the start page + And I wait for the AJAX call to finish + And I double click on the torrent "1" + And I wait for the AJAX call to finish + And I follow "Peers" + Then I should see "1.2.3.4" + And I should see "Transmission Rocks" + + Scenario: info updates itself + Given a torrent with a peer with IP "1.2.3.4" and client name "Transmission Rocks" + When I go to the start page + And I wait for the AJAX call to finish + And I double click on the torrent "1" + And I wait for the AJAX call to finish + And I follow "Peers" + Then I should see "1.2.3.4" + When there is new data for the torrent with new IP "6.7.8.9" and new client name "Elephant" + And I wait for "3"s + Then I should see "6.7.8.9" + And I should see "Elephant" + And I should not see "1.2.3.4" + And I should not see "Transmission Rocks" \ No newline at end of file diff --git a/features/torrents.feature b/features/torrents.feature new file mode 100644 index 0000000..6a3ed7f --- /dev/null +++ b/features/torrents.feature @@ -0,0 +1,26 @@ +Feature: view list of torrents + In order to manage my torrents + As a user + I want to see a list of my torrents + + Scenario: see list of torrents + Given a torrent with the name "Mutant Ninja Turtles" + When I go to the start page + Then I should see "Mutant Ninja Turtles" + + Scenario: clicking on a torrent + Given a torrent with the name "Mutant Ninja Turtles" + When I go to the start page + And I click on the torrent + Then the torrent should be highlighted + + Scenario: switch to compact mode + Given a torrent with the name "Mutant Ninja Turtles" + When I go to the start page + Then I should see "remaining" + When I follow "Enable Compact View" + Then I should not see "remaining" + But I should see "Mutant Ninja Turtles" + When I follow "Disable Compact View" + Then I should see "remaining" + \ No newline at end of file diff --git a/images/kettu_bg.png b/images/kettu_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..2bcb0c3e59470f5e8cf9ab2591967cf495a44d17 GIT binary patch literal 17771 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%0A$YI3<;GKxN|z8M8SqHnLbafq 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 + + + + + kettu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+ +
+
+
+
+
+
    +
    +
    +
    +
    + +
    + +
    + + + + \ No newline at end of file diff --git a/js/controllers/settings.js b/js/controllers/settings.js new file mode 100644 index 0000000..d82a32d --- /dev/null +++ b/js/controllers/settings.js @@ -0,0 +1,53 @@ +Settings = function(transmission) { with(transmission) { + get('#/settings', function() { + var context = this; + var request = { 'method': 'session-get', 'arguments': {} }; + + context.remote_query(request, function(view) { + view['reload-interval'] = transmission.reload_interval/1000; + context.partial('./templates/settings/index.mustache', view, function(rendered_view) { + context.openInfo(rendered_view); + trigger('settings-refreshed', view); + }); + }); + }); + + put('#/settings', function() { + var context = this; + var request = { 'method': 'session-set', 'arguments': this.prepare_arguments(context, this.params) }; + + this.manage_handlers(context, this.params); + + if(this.setting_arguments_valid(context, $.extend(request['arguments'], {'reload-interval': this.params['reload-interval']}))) { + delete(request['arguments']['reload-interval']); + context.remote_query(request, function(response) { + trigger('flash', 'Settings updated successfully'); + if(context.params['peer-port']) { updatePeerPortDiv(context); } + if(context.params['reload-interval']) { context.update_reload_interval(context, context.params['reload-interval']); } + }); + } else { + trigger('flash', 'Settings could not be updated.'); + trigger('errors', this.setting_arguments_errors(context)); + } + }); + + updatePeerPortDiv = function(context) { + $('#port-open').addClass('waiting'); + $('#port-open').show(); + var request = { 'method': 'port-test', 'arguments': {} }; + context.remote_query(request, function(response) { + $('#port-open').removeClass('waiting'); + if(response['port-is-open']) { + $('#port-open').addClass('active'); + } else { + $('#port-open').removeClass('active'); + } + }); + }; + + bind('settings-refreshed', function(e, settings){ with(this) { + this.updateSettingsCheckboxes(settings); + this.updateSettingsSelects(settings); + this.menuizeInfo(); + }}); +}}; \ No newline at end of file diff --git a/js/controllers/statistics.js b/js/controllers/statistics.js new file mode 100644 index 0000000..ba1d1f1 --- /dev/null +++ b/js/controllers/statistics.js @@ -0,0 +1,16 @@ +Statistics = function(transmission) { with(transmission) { + get('#/statistics', function() { + var context = this; + var request = { + 'method': 'session-stats', + 'arguments': {'fields': ['current-stats', 'torrentCount']} + } + + context.remote_query(request, function(response) { + context.partial('./templates/statistics/index.mustache', StatisticsView(response), function(rendered_view) { + context.openInfo(rendered_view); + context.drawGraphs(); + }); + }); + }); +}}; \ No newline at end of file diff --git a/js/controllers/torrents.js b/js/controllers/torrents.js new file mode 100644 index 0000000..967780c --- /dev/null +++ b/js/controllers/torrents.js @@ -0,0 +1,163 @@ +Torrents = function(transmission) { with(transmission) { + var context; + + before(function() { + context = this; + }); + + get('#/torrents', function() { + setGlobalModes(this.params); + getAndRenderTorrents(this.params['view'] || this.params['sort'] || this.params['filter']); + if(transmission.interval_id) { clearInterval(transmission.interval_id); } + transmission.reload_interval = transmission.reload_interval || 2000; + transmission.interval_id = setInterval('getAndRenderTorrents()', transmission.reload_interval); + }); + + get('#/torrents/new', function() { + this.partial('./templates/torrents/new.mustache', {}, function(rendered_view) { + context.openInfo(rendered_view); + }); + }); + + // NOTE: this route is not restful, but how else to handle + // registered protocol and content handlers? + get('#/torrents/add', function() { + var request = { + 'method': 'torrent-add', + 'arguments': {'filename': this.params['url'], 'paused': false} + }; + context.remote_query(request, function(response) { + torrentUploaded(response['torrent-added']); + }); + }); + + route('delete', '#/torrents/:id', function() { + var request = { + 'method': 'torrent-remove', + 'arguments': {'ids': parseInt(this.params['id'])} + }; + if(this.params['delete_data']) { + request['arguments']['delete-local-data'] = true; + } + context.remote_query(request, function(response) { + context.trigger('flash', 'Torrent removed successfully.'); + $('#' + context.params['id']).remove(); + }); + }); + + post('#/torrents', function() { + var paused = (this.params['start_when_added'] != "on"); + if(this.params['url'].length > 0) { + var request = { + 'method': 'torrent-add', + 'arguments': {'filename': this.params['url'], 'paused': paused} + }; + context.remote_query(request, function(response) { + torrentUploaded(response['torrent-added']); + }); + } else { + $('#add_torrent_form').ajaxSubmit({ + 'url': '/transmission/upload?paused=' + paused, + 'type': 'POST', + 'data': { 'X-Transmission-Session-Id' : remote_session_id }, + 'dataType': 'xml', + 'iframe': true, + 'success': function(response) { + torrentUploaded($(response).children(':first').text().match(/200/)); + } + }); + } + }); + + get('#/torrents/:id', function() { + var id = parseInt(context.params['id']); + + getAndRenderTorrentInfo(id); + context.clearReloadInterval(); + transmission.info_interval_id = setInterval('getAndRenderTorrentInfo(' + id + ')', transmission.reload_interval); + }); + + put('#/torrents/:id', function() { + var id = parseInt(context.params['id']); + var request = { + 'method': context.params['method'], + 'arguments': {'ids': id} + }; + context.remote_query(request, function(response) { + getTorrent(id, renderTorrent); + }); + }); + + getAndRenderTorrentInfo = function(id) { + if($('.menu-item.active').get(0)) { + context.saveLastMenuItem($('.menu-item.active').attr('id')); + } + getTorrent(id, function(torrent) { + var view = TorrentView(torrent, context, context.params['sort_peers']); + context.partial('./templates/torrents/show_info.mustache', view, function(rendered_view) { + context.openInfo(rendered_view); + context.startCountDownOnNextAnnounce(); + if(context.params['sort_peers']) { + $('#menu-item-peers').click(); + } + }); + }); + }; + + getTorrent = function(id, callback) { + var request = { + 'method': 'torrent-get', + 'arguments': {'ids': id, 'fields': Torrent({})['fields'].concat(Torrent({})['info_fields'])} + }; + context.remote_query(request, function(response) { + if(callback) { + callback(response['torrents'].map( function(row) {return Torrent(row);} )[0]); + } + }); + }; + + renderTorrent = function(torrent) { + context.partial('./templates/torrents/show.mustache', TorrentsView(torrent, context), function(rendered_view) { + $(element_selector).find('#' + torrent.id).replaceWith(rendered_view); + trigger('torrent-refreshed', torrent); + }); + }; + + getAndRenderTorrents = function(need_change) { + var request = { + method: 'torrent-get', + arguments: {fields:Torrent({})['fields']} + }; + context.remote_query(request, function(response) { + var torrents = response['torrents'].map( function(row) {return Torrent(row)} ); + trigger('torrents-refreshed', {"torrents": torrents, "need_change": need_change}); + }); + }; + + setGlobalModes = function(params) { + transmission.reverse_sort = params['reverse'] || false; + transmission.sort_mode = params['sort'] || transmission.sort_mode || 'name'; + transmission.view_mode = params['view'] || transmission.view_mode || 'normal'; + transmission.filter_mode = params['filter'] || transmission.filter_mode || 'all'; + context.highlightLink('#filterbar', '.all'); + $('.torrent').show(); + }; + + torrentUploaded = function(torrent_added) { + var message = (torrent_added) ? 'Torrent added successfully.' : 'Torrent could not be added.'; + context.trigger('flash', message); + context.closeInfo(); + getAndRenderTorrents(); + }; + + bind('torrents-refreshed', function(e, params) { with(this) { + var sorted_torrents = this.sortTorrents(transmission.sort_mode, params['torrents'], transmission.reverse_sort); + var filtered_torrents = this.filterTorrents(transmission.filter_mode, sorted_torrents); + this.updateViewElements(filtered_torrents, params['need_change']); + }}); + + bind('torrent-refreshed', function(e, torrent) { with(this) { + this.updateInfo(torrent); + this.cycleTorrents(); + }}); +}}; \ No newline at end of file diff --git a/js/helpers/application_helpers.js b/js/helpers/application_helpers.js new file mode 100644 index 0000000..4d7675e --- /dev/null +++ b/js/helpers/application_helpers.js @@ -0,0 +1,9 @@ +var ApplicationHelpers = { + cache_partial: function(template, partial, context) { + if(!context.cache(partial)) { + $.ajax({'async': false, 'url': template, 'success': function(response) { + context.cache(partial, response); + }}); + }; + } +} \ No newline at end of file diff --git a/js/helpers/filter_torrents_helpers.js b/js/helpers/filter_torrents_helpers.js new file mode 100644 index 0000000..95f841e --- /dev/null +++ b/js/helpers/filter_torrents_helpers.js @@ -0,0 +1,18 @@ +var FilterTorrentsHelpers = { + filterTorrents: function(filter_mode, torrents) { + var filtered_torrents = []; + var stati = Torrent({}).stati; + + if(filter_mode == 'all') { + filtered_torrents = torrents; + } else { + $.each(torrents, function() { + if(this.status == stati[filter_mode]) { + filtered_torrents.push(this); + } + }); + } + + return filtered_torrents; + } +} \ No newline at end of file diff --git a/js/helpers/info_helpers.js b/js/helpers/info_helpers.js new file mode 100644 index 0000000..45dd31c --- /dev/null +++ b/js/helpers/info_helpers.js @@ -0,0 +1,79 @@ +var InfoHelpers = { + closeInfo: function() { + this.clearReloadInterval(); + $('.main').removeClass('info'); + $('#info').hide(); + var path = transmission.filter_mode ? '#/torrents?filter=' + transmission.filter_mode : '#/torrents'; + this.redirect(path); + return false; + }, + + openInfo: function(view) { + var info = $('#info'); + info.html(view); + info.show(); + $('.main').addClass('info'); + this.menuizeInfo(); + }, + + clearReloadInterval: function() { + if(transmission.info_interval_id) { + clearInterval(transmission.info_interval_id); + } + }, + + infoIsOpen: function() { + return $('.main').hasClass('info'); + }, + + handleDoubleClickOnTorrent: function(torrent) { + var context = this; + $('#' + torrent.id).dblclick(function() { + if(context.infoIsOpen()) { + context.closeInfo(); + } else { + var active_torrent = $('.torrent.active'); + if(active_torrent.get(0)) { + context.redirect('#/torrents/' + active_torrent.attr('id')); + } + } + return false; + }); + }, + + handleClickOnTorrent: function(torrent) { + var context = this; + $('#' + torrent.id).click(function() { + context.highlightLi('#torrents', this); + if(context.infoIsOpen()) { + context.saveLastMenuItem($('.menu-item.active').attr('id')); + window.location = '#/torrents/' + $(this).attr('id'); + // NOTE: a redirect seems to interfere with our double click handling here + } + }); + }, + + updateInfo: function(torrent) { + this.trigger('changed'); + + this.handleClickOnTorrent(torrent); + this.handleDoubleClickOnTorrent(torrent); + }, + + startCountDownOnNextAnnounce: function() { + var context = this; + var timer = setInterval(function() { + var timestamp = $('.countdown').attr('data-timestamp'); + var formatted = context.formatNextAnnounceTime(timestamp); + + if(formatted.match(/59 min/)) { + clearInterval(timer); + context.saveLastMenuItem($('.menu-item.active').attr('id')); + context.closeInfo(); + context.openInfo(); + } else { + $('.countdown').text(formatted); + } + }, 980); + } +}; \ No newline at end of file diff --git a/js/helpers/link_helpers.js b/js/helpers/link_helpers.js new file mode 100644 index 0000000..8a42043 --- /dev/null +++ b/js/helpers/link_helpers.js @@ -0,0 +1,93 @@ +var LinkHelpers = { + activateLinks: function() { + this.activateAddTorrentLink(); + this.activateSettingsLink(); + this.activateStatisticsLink(); + this.activateTurtleModeLink(); + this.activateCompactViewLink(); + this.activateFilterAndSortLink(); + }, + + activateAddTorrentLink: function() { + var context = this; + $('#add_a_torrent').click(function() { + if(context.infoIsOpen()) { + context.closeInfo(); + } else { + window.location.hash = '/torrents/new'; + } + return false; + }); + }, + + activateFilterAndSortLink: function() { + var context = this; + $('#activate_filters').click(function() { + $('#filters').show(); + $('#sorts').hide(); + }); + $('#activate_sorts').click(function() { + $('#filters').hide(); + $('#sorts').show(); + }); + }, + + activateTurtleModeLink: function() { + var context = this; + $('#turtle_mode').click(function() { + var form = $('#turtle_mode_form'); + form.submit(); + if($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).text('Enable Turtle Mode'); + form.find('input:first').attr('value', 'true'); + } else { + $(this).addClass('active'); + $(this).text('Disable Turtle Mode'); + form.find('input:first').attr('value', 'false'); + } + return false; + }); + }, + + activateCompactViewLink: function() { + var context = this, redirect_path = ''; + $('#compact_view').click(function() { + if($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).text('Enable Compact View'); + redirect_path = '#/torrents?view=normal'; + } else { + $(this).addClass('active'); + $(this).text('Disable Compact View'); + redirect_path = '#/torrents?view=compact'; + } + context.redirect(redirect_path); + return false; + }); + }, + + activateSettingsLink: function() { + var context = this; + $('#settings').click(function() { + if(context.infoIsOpen()) { + context.closeInfo(); + } else { + context.redirect('#/settings'); + } + return false; + }); + }, + + activateStatisticsLink: function() { + var context = this; + $('#statistics').click(function() { + if(context.infoIsOpen()) { + context.closeInfo(); + } else { + context.redirect('#/statistics'); + } + return false; + }); + } +} \ No newline at end of file diff --git a/js/helpers/math_helpers.js b/js/helpers/math_helpers.js new file mode 100644 index 0000000..0571fe2 --- /dev/null +++ b/js/helpers/math_helpers.js @@ -0,0 +1,143 @@ +// ========================================================================== +// Project: Derailleur.Torrent +// Copyright: ©2009 Kevin Glowacz +// ========================================================================== + +/* + * Converts file & folder byte size values to more + * readable values (bytes, KB, MB, GB or TB). + * + * @param integer bytes + * @returns string + */ +Math.formatBytes = function(bytes) { + var size, unit; + + // Terabytes (TB). + if ( bytes >= 1099511627776 ) { + size = bytes / 1099511627776; + unit = 'TB'; + } + + // Gigabytes (GB). + else if ( bytes >= 1073741824 ) { + size = bytes / 1073741824; + unit = 'GB'; + } + + // Megabytes (MB). + else if ( bytes >= 1048576 ) { + size = bytes / 1048576; + unit = 'MB'; + } + + // Kilobytes (KB). + else if ( bytes >= 1024 ) { + size = bytes / 1024; + unit = 'KB'; + } + + // The file is less than one KB + else { + size = bytes; + unit = 'bytes'; + } + + // Single-digit numbers have greater precision + var precision = 1; + if (size < 10) { + precision = 2; + } + size = Math.roundWithPrecision(size, precision); + + // Add the decimal if this is an integer + if ((size % 1) == 0 && unit != 'bytes') { + size = size + '.0'; + } + + return size + ' ' + unit; +}; + +/* + * Converts seconds to more readable units (hours, minutes etc). + * + * @param integer seconds + * @returns string + */ +Math.formatSeconds = function(seconds) +{ + var result, days, hours, minutes, seconds; + + days = Math.floor(seconds / 86400); + hours = Math.floor((seconds % 86400) / 3600); + minutes = Math.floor((seconds % 3600) / 60); + seconds = Math.floor((seconds % 3600) % 60); + + if (days > 0 && hours == 0){ + result = days + ' ' + 'days'; + } + else if (days > 0 && hours > 0){ + result = days + ' ' + 'days' + ' ' + hours + ' ' + 'hr'; + } + else if (hours > 0 && minutes == 0){ + result = hours + ' ' + 'hr'; + } + else if (hours > 0 && minutes > 0){ + result = hours + ' ' + 'hr' + ' ' + minutes + ' ' + 'min'; + } + else if (minutes > 0 && seconds == 0){ + result = minutes + ' ' + 'min'; + } + else if (minutes > 0 && seconds > 0){ + result = minutes + ' ' + 'min' + ' ' + seconds + ' ' + 'sec'; + } + else{ + result = seconds + ' ' + 'sec'; + } + + return result; +}; + +/* + * Round a float to a specified number of decimal + * places, stripping trailing zeroes + * + * @param float floatnum + * @param integer precision + * @returns float + */ +Math.roundWithPrecision = function(floatnum, precision) { + return Math.round ( floatnum * Math.pow ( 10, precision ) ) / Math.pow ( 10, precision ); +}; + + +/* + * Given a numerator and denominator + * + * @param float + * @param float + * @returns string + */ +Math.ratio = function(numerator, denominator) { + var result = Math.floor(100 * numerator / denominator) / 100; + + if(isNaN(result)) { result = 0; } + if(result=="Infinity") { result = "∞"; } + if((result % 1) == 0) { result += '.00'; } + + return result; +}; + +/* + * Converts total and left until done to a percenage value + * + * @param float + * @param float + * @returns float + */ +Math.formatPercent = function(total, left_until_done) { + if(!total) { return 0; } + if(!left_until_done && left_until_done != 0) { return 0; } + + return Math.floor( ((total - left_until_done) / total) * 10000 ) / 100; +} \ No newline at end of file diff --git a/js/helpers/setting_helpers.js b/js/helpers/setting_helpers.js new file mode 100644 index 0000000..8b9c99b --- /dev/null +++ b/js/helpers/setting_helpers.js @@ -0,0 +1,107 @@ +var SettingHelpers = { + validator: new SettingsValidator(), + + updateSettingsCheckboxes: function(settings) { + $.each($('#info').find('input[type=checkbox]'), function() { + var checkbox = $(this); + var name = checkbox.attr('name'); + if(settings[name]) { + checkbox.attr('checked', 'checked'); + } + $.each(['protocol-handler-enabled', 'content-handler-enabled'], function() { + if(name == this && transmission.store.exists(this)) { + checkbox.attr('disabled', 'disabled'); + checkbox.attr('checked', 'checked'); + }; + }); + }); + + $('#info input').change(function(event) { + if($(this).attr('name') == 'protocol-handler-enabled' || $(this).attr('name') == 'content-handler-enabled') { + $(this).attr('disabled', 'disabled'); + $(this).attr('checked', 'checked'); + } + $(this).parents('form').trigger('submit'); + return false; + }); + }, + + updateSettingsSelects: function(settings) { + $.each($('#info').find('select'), function() { + var name = $(this).attr('name'); + var value = settings[name]; + $.each($(this).find('option'), function() { + if($(this).val() == value) { + $(this).attr('selected', 'selected'); + } + }); + }); + }, + + setting_arguments_valid: function(context, setting_arguments) { + context.validator.validate(setting_arguments); + return ! context.validator.has_errors(); + }, + + setting_arguments_errors: function(context) { + return context.validator.errors; + }, + + prepare_arguments: function(context, params) { + if(params['alt-speed-enabled']) { + return context.turtle_mode_hash(params['alt-speed-enabled']); + } else { + return context.arguments_hash(params); + } + }, + + turtle_mode_hash: function(turtle_mode) { + return {'alt-speed-enabled': (turtle_mode == "true") ? true : false}; + }, + + arguments_hash: function(params, updatable_settings) { + updatable_settings = updatable_settings || [ + 'dht-enabled', 'pex-enabled', 'speed-limit-up', 'speed-limit-up-enabled', 'speed-limit-down', + 'speed-limit-down-enabled', 'peer-port', 'download-dir', 'alt-speed-down', 'alt-speed-up', + 'encryption' + ]; + var hash = {}; + + $.each(updatable_settings, function() { + var setting = this; + hash[setting] = params[setting] ? true : false; + if(params[setting] && params[setting].match(/^\d+$/)) { + hash[setting] = parseInt(params[setting], 10); + } else if(params[setting] && params[setting] != "on") { + hash[setting] = params[setting]; + } + }); + + return hash; + }, + + update_reload_interval: function(context, new_reload_interval) { + new_reload_interval = parseInt(new_reload_interval, 10); + if(new_reload_interval != (transmission.reload_interval/1000)) { + transmission.reload_interval = new_reload_interval * 1000; + clearInterval(transmission.interval_id); + context.closeInfo(); + } + }, + + manage_handlers: function(context, params) { + if(params['protocol-handler-enabled'] && !transmission.store.exists('protocol-handler-enabled')) { + transmission.store.set('protocol-handler-enabled', true); + window.navigator.registerProtocolHandler('magnet', context.base_url() + '#/torrents/add?url=%s', "Transmission Web"); + } + + if(params['content-handler-enabled'] && !transmission.store.exists('content-handler-enabled')) { + transmission.store.set('content-handler-enabled', true); + window.navigator.registerContentHandler("application/x-bittorrent", context.base_url() + '#/torrents/add?url=%s', "Transmission Web"); + } + }, + + base_url: function() { + return window.location.href.match(/^([^#]+)#.+$/)[1]; + }, +}; \ No newline at end of file diff --git a/js/helpers/sort_torrents_helpers.js b/js/helpers/sort_torrents_helpers.js new file mode 100644 index 0000000..564fd24 --- /dev/null +++ b/js/helpers/sort_torrents_helpers.js @@ -0,0 +1,58 @@ +// +// these sort helpers are based heavily on the previous sort helpers by Dave Perrett and Malcolm Jarvis +// + +var SortTorrentsHelpers = { + sortTorrents: function(sort_mode, torrents, reverse) { + var torrent_sort_function = function() {}; + + switch(sort_mode) { + case 'name': + torrent_sort_function = function(a, b) { + var a_name = a.name.toUpperCase(); + var b_name = b.name.toUpperCase(); + return (a_name < b_name) ? -1 : (a_name > b_name) ? 1 : 0; + }; + break; + case 'activity': + torrent_sort_function = function(a, b) { + return b.activity() - a.activity(); + }; + break; + case 'age': + torrent_sort_function = function(a, b) { + return b.addedDate - a.addedDate; + }; + break; + case 'progress': + torrent_sort_function = function(a, b) { + if(a.percentDone() != b.percentDone()) { + return a.percentDone() - b.percentDone(); + } else { + var a_ratio = Math.ratio(a.uploadedEver, a.downloadedEver); + var b_ratio = Math.ratio(b.uploadedEver, b.downloadedEver); + return a_ratio - b_ratio; + } + }; + break; + case 'queue': + torrent_sort_function = function(a, b) { + return a.id - b.id; + }; + break; + case 'state': + torrent_sort_function = function(a, b) { + return a.status - b.status; + }; + break; + } + + torrents.sort(torrent_sort_function); + + if(reverse) { + torrents.reverse(); + } + + return torrents; + } +} \ No newline at end of file diff --git a/js/helpers/statistic_helpers.js b/js/helpers/statistic_helpers.js new file mode 100644 index 0000000..6f4494e --- /dev/null +++ b/js/helpers/statistic_helpers.js @@ -0,0 +1,41 @@ +var StatisticHelpers = { + drawGraphs: function() { + this.drawPie('torrents_by_status', { + 'Downloading': ($('.downloading').length - 1), + 'Seeding': ($('.seeding').length - 1), + 'Paused': ($('.paused').length - 1) + }); + this.drawLines('up_and_download_stats', { + 'Upload': $.map(transmission.store.get('up_and_download_rate'), function(item) { return (item.up / 1024); }), + 'Download': $.map(transmission.store.get('up_and_download_rate'), function(item) { return (item.down / 1024); }) + }); + }, + + drawPie: function(id, data) { + var bluffGraph = new Bluff.Pie(id, 300); + bluffGraph.set_theme({ + colors: ['#B2DFEE', '#FFEC8B', '#BCEE68'], + marker_color: '#aea9a9', + font_color: '#555555', + background_colors: ['#F8F8F8', '#FFFFFF'] + }); + for(label in data) { + bluffGraph.data(label, data[label]); + } + bluffGraph.draw(); + }, + + drawLines: function(id, data) { + var bluffGraph = new Bluff.Line(id, 300); + bluffGraph.set_theme({ + colors: ['#B2DFEE', '#FFEC8B', '#BCEE68'], + marker_color: '#aea9a9', + font_color: '#555555', + background_colors: ['#F8F8F8', '#FFFFFF'] + }); + for(label in data) { + bluffGraph.data(label, data[label]); + } + bluffGraph.draw(); + } +} \ No newline at end of file diff --git a/js/helpers/store_helpers.js b/js/helpers/store_helpers.js new file mode 100644 index 0000000..f95118d --- /dev/null +++ b/js/helpers/store_helpers.js @@ -0,0 +1,23 @@ +var StoreHelpers = { + initializeStore: function() { + transmission.store = new Sammy.Store({name: 'data', type: 'local'}); + if(!transmission.store.isAvailable()) { + transmission.store = new Sammy.Store({name: 'data', type: 'cookie'}); + } + }, + + addUpAndDownToStore: function(data) { + if(transmission.store.exists('up_and_download_rate')) { + store_data = transmission.store.get('up_and_download_rate'); + if(store_data.length > 29) { + store_data.shift(); + } + store_data.push(data); + data = store_data; + } else { + data = [data]; + } + transmission.store.set('up_and_download_rate', data); + } +}; + diff --git a/js/helpers/torrent_helpers.js b/js/helpers/torrent_helpers.js new file mode 100644 index 0000000..f919e78 --- /dev/null +++ b/js/helpers/torrent_helpers.js @@ -0,0 +1,116 @@ +var TorrentHelpers = { + globalUpAndDownload: function(torrents) { + var uploadRate = 0.0, downloadRate = 0.0; + $.each(torrents, function() { + uploadRate += this.rateUpload; + downloadRate += this.rateDownload; + }); + this.addUpAndDownToStore({"up": uploadRate, "down": downloadRate}); + return Torrent({}).downAndUploadRateString(downloadRate, uploadRate); + }, + + cycleTorrents: function() { + $('.torrent').removeClass('even'); + $('.torrent:even').addClass('even'); + }, + + activateDeleteForm: function(torrent) { + var context = this; + $('#' + torrent.id).find('.torrent_delete_form').submit(function() { + if(confirm('Delete data?')) { + $(this).prepend(context.mustache(context.cache('delete_data'))); + } + return true; + }); + }, + + makeNewTorrent: function(torrent, view) { + var template = (transmission.view_mode == 'compact') ? 'show_compact' : 'show'; + var rendered_view = this.mustache(this.cache(template), TorrentsView(torrent, this)); + $('#torrents').append(rendered_view); + this.updateInfo(torrent); + this.activateDeleteForm(torrent); + }, + + updateStatus: function(old_torrent, torrent) { + old_torrent.removeClass('downloading').removeClass('seeding').removeClass('paused').addClass(torrent.statusWord()); + }, + + updateTorrent: function(torrent) { + var old_torrent = $('#' + torrent.id); + old_torrent.find('.progressDetails').html(torrent.progressDetails()); + old_torrent.find('.progressbar').html(torrent.progressBar()); + old_torrent.find('.statusString').html(torrent.statusString()); + this.updateStatus(old_torrent, torrent); + }, + + addOrUpdateTorrents: function(torrents) { + var context = this; + $.each(torrents, function() { + if(! $('#' + this.id.toString()).get(0)) { + context.makeNewTorrent(this); + } else { + context.updateTorrent(this); + } + }); + }, + + removeOldTorrents: function(torrents) { + var old_ids = $.map($('.torrent'), function(torrent) {return $(torrent).attr('id');}); + var new_ids = $.map(torrents, function(torrent) {return torrent.id}); + $.each(old_ids, function() { + if(new_ids.indexOf(parseInt(this)) < 0) { + $('#' + this).remove(); + } + }); + }, + + updateTorrents: function(torrents, need_change) { + this.cache_partials(); + if(torrents && need_change) { + $('.torrent').remove(); + this.addOrUpdateTorrents(torrents); + } else if(torrents) { + this.removeOldTorrents(torrents); + this.addOrUpdateTorrents(torrents); + } + }, + + updateViewElements: function(torrents, need_change) { + this.makeSortLinkReverse(); + this.updateTorrents(torrents, need_change); + this.cycleTorrents(); + $('#globalUpAndDownload').html(this.globalUpAndDownload(torrents)); + this.highlightLink('#filters', '.' + transmission.filter_mode); + this.highlightLink('#sorts', '.' + transmission.sort_mode); + }, + + cache_partials: function() { + var context = this; + $.each(['delete_data', 'show', 'show_compact'], function() { + context.cache_partial('./templates/torrents/' + this + '.mustache', this, context); + }); + }, + + formatNextAnnounceTime: function(timestamp) { + var now = new Date().getTime(); + var current = new Date(parseInt(timestamp) * 1000 - now); + if(current) { + return current.getMinutes() + ' min, ' + current.getSeconds() + ' sec'; + } else { + return timestamp; + } + }, + + makeSortLinkReverse: function() { + var link = $('#sorts .' + transmission.sort_mode); + var url = link.attr('href'); + + $('#sorts a').each(function() { + $(this).attr('href', $(this).attr('href').match(/([^&]+)(&reverse=true)?/)[1]); + }); + if(!url.match(/reverse/)) { + link.attr('href', url + '&reverse=true'); + } + } +}; \ No newline at end of file diff --git a/js/helpers/view_helpers.js b/js/helpers/view_helpers.js new file mode 100644 index 0000000..1196264 --- /dev/null +++ b/js/helpers/view_helpers.js @@ -0,0 +1,46 @@ +var ViewHelpers = { + highlightLink: function(menu_id, element_class) { + $(menu_id + ' a').removeClass('active'); + $(menu_id + ' ' + element_class).addClass('active'); + }, + + highlightLi: function(menu_id, li) { + $(menu_id + ' li').removeClass('active'); + $(li).addClass('active'); + }, + + showAndHideFlash: function(message) { + $('#flash').html(message); + $('#flash').show().delay(3000).fadeOut('slow'); + }, + + showErrors: function(errors) { + var error_string = '

    '; + $.each(errors, function() { + error_string += this['field'] + ': ' + this['message'] + '
    '; + }); + $('#errors').html(error_string + '.

    '); + }, + + saveLastMenuItem: function(id) { + transmission.last_menu_item = id; + }, + + menuizeInfo: function() { + $('#info .menu-item').click(function() { + $('#info .menu-item').removeClass('active'); + $(this).addClass('active'); + var item = $(this).attr('data-item'); + $('#info .item').hide(); + $('#info .' + item).show(); + }); + $('#info .item').hide(); + if(transmission.last_menu_item) { + $('#' + transmission.last_menu_item).click(); + delete transmission.last_menu_item; + } else { + $('#info .item:first').show(); + $('#info .menu-item:first').addClass('active'); + } + } +}; \ No newline at end of file diff --git a/js/models/settings_validator.js b/js/models/settings_validator.js new file mode 100644 index 0000000..e89523c --- /dev/null +++ b/js/models/settings_validator.js @@ -0,0 +1,16 @@ +var SettingsValidator = function() { + this.schema = { + 'presence_of': [ + 'peer-port', 'alt-speed-down','alt-speed-up', 'dht-enabled', 'download-dir', + 'encryption', 'peer-port', 'pex-enabled', 'speed-limit-down', 'speed-limit-down-enabled', + 'speed-limit-up', 'speed-limit-up-enabled' + ], + 'numericality_of': [ + {'field': 'peer-port', 'max': 65535}, + 'reload-interval', 'speed-limit-down', 'speed-limit-up', 'alt-speed-down', 'alt-speed-up' + ], + 'inclusion_of': {'field': 'encryption', 'in': ['required', 'preferred', 'tolerated']} + } +}; + +SettingsValidator.prototype = Validator.prototype; \ No newline at end of file diff --git a/js/models/torrent.js b/js/models/torrent.js new file mode 100644 index 0000000..8e39f1d --- /dev/null +++ b/js/models/torrent.js @@ -0,0 +1,141 @@ +Torrent = function(attributes) { + var torrent = {}; + + torrent['fields'] = [ + 'id', 'name', 'status', 'totalSize', 'sizeWhenDone', 'haveValid', 'leftUntilDone', + 'eta', 'uploadedEver', 'uploadRatio', 'rateDownload', 'rateUpload', 'metadataPercentComplete', + 'addedDate' + ]; + torrent['info_fields'] = [ + 'downloadDir', 'creator', 'hashString', 'comment', 'isPrivate', 'downloadedEver', + 'haveString', 'errorString', 'peersGettingFromUs', 'peersSendingToUs', 'files', + 'pieceCount', 'pieceSize', 'trackerStats', 'peers' + ]; + $.each(torrent.fields, function() { + torrent[this] = attributes[this]; + }); + $.each(torrent.info_fields, function() { + torrent[this] = attributes[this]; + }); + + $.each(['totalSize', 'downloadedEver', 'uploadedEver', 'pieceSize'], function() { + var attr = this; + torrent[attr + 'String'] = function() { + return Math.formatBytes(torrent[attr]); + } + }); + + torrent.secure = function() { + return (torrent.isPrivate) ? 'Private Torrent' : 'Public Torrent'; + }; + torrent.isActive = function() { + return (torrent.status & (torrent.stati['downloading'] | torrent.stati['seeding'])) > 0; + }; + torrent.isDoneDownloading = function() { + return torrent.status === torrent.stati['seeding'] || torrent.leftUntilDone === 0; + }; + torrent.needsMetaData = function() { + return torrent.metadataPercentComplete < 1 + }; + torrent.percentDone = function() { + return Math.formatPercent(torrent.sizeWhenDone, torrent.leftUntilDone); + }; + torrent.progressDetails = function() { + var progressDetails; + if(torrent.needsMetaData()) { + progressDetails = torrent.metaDataProgress(); + } else if(!torrent.isDoneDownloading()) { + progressDetails = torrent.downloadingProgress(); + if(torrent.isActive()) { + progressDetails += ' - ' + torrent.etaString(); + } + } else { + progressDetails = torrent.uploadingProgress(); + } + + return progressDetails; + }; + torrent.downloadingProgress = function() { + var formattedSizeDownloaded = Math.formatBytes(torrent.sizeWhenDone - torrent.leftUntilDone); + var formattedSizeWhenDone = Math.formatBytes(torrent.sizeWhenDone); + + return (formattedSizeDownloaded + " of " + formattedSizeWhenDone + " (" + torrent.percentDone() + "%)"); + }; + torrent.uploadingProgress = function() { + var formattedSizeWhenDone = Math.formatBytes(torrent.sizeWhenDone); + var formattedUploadedEver = Math.formatBytes(torrent.uploadedEver); + + var uploadingProgress = formattedSizeWhenDone + ", uploaded " + formattedUploadedEver; + return uploadingProgress + " (Ratio: " + torrent.uploadRatio + ")"; + }; + torrent.metaDataProgress = function() { + var percentRetrieved = (Math.floor(torrent.metadataPercentComplete * 10000) / 100).toFixed(1); + return "Magnetized transfer - retrieving metadata (" + percentRetrieved + "%)"; + }; + torrent.progressBar = function() { + var status, progressBar; + + if(torrent.isActive() && torrent.needsMetaData()) { + status = 'meta'; + progressBar = $("
    ").progressbar({value: 100}).html(); + } else if(torrent.isActive() && !torrent.isDoneDownloading()) { + status = 'downloading'; + progressBar = $("
    ").progressbar({value: torrent.percentDone()}).html(); + } else if(torrent.isActive() && torrent.isDoneDownloading()) { + status = 'uploading'; + progressBar = $("
    ").progressbar({value: torrent.percentDone()}).html(); + } else { + status = 'paused'; + progressBar = $("
    ").progressbar({value: torrent.percentDone()}).html(); + } + + return progressBar.replace(/ui-widget-header/, 'ui-widget-header-' + status); + }; + torrent.etaString = function() { + if(torrent.eta < 0) { + return "remaining time unknown"; + } else { + return Math.formatSeconds(torrent.eta) + ' ' + 'remaining'; + } + }; + torrent.statusStringLocalized = function(status) { + var localized_stati = {}; + + localized_stati[torrent.stati['waiting_to_check']] = 'Waiting to verify'; + localized_stati[torrent.stati['checking']] = 'Verifying local data'; + localized_stati[torrent.stati['downloading']] = 'Downloading'; + localized_stati[torrent.stati['seeding']] = 'Seeding'; + localized_stati[torrent.stati['paused']] = 'Paused'; + + return localized_stati[this['status']] ? localized_stati[this['status']] : 'error'; + }; + torrent.statusString = function() { + var currentStatus = torrent.statusStringLocalized(torrent.status); + if(torrent.isActive()) { + currentStatus += ' - ' + torrent.downAndUploadRateString(torrent.rateDownload, torrent.rateUpload); + } + return currentStatus; + }; + torrent.statusWord = function() { + for(var i in torrent.stati) { + if(torrent.stati[i] == torrent.status) { + return i; + } + } + }; + torrent.downAndUploadRateString = function(downloadRate, uploadRate) { + return 'DL: ' + (downloadRate / 1000).toFixed(1) + ' KB/s, UL: ' + (uploadRate / 1000).toFixed(1) + ' KB/s'; + }; + torrent.activity = function() { + return torrent.rateDownload + torrent.rateUpload; + }; + torrent['stati'] = { + 'waiting_to_check': 1, + 'checking': 2, + 'downloading': 4, + 'seeding': 8, + 'paused': 16 + }; + + return torrent; +}; \ No newline at end of file diff --git a/js/models/validator.js b/js/models/validator.js new file mode 100644 index 0000000..627ad5b --- /dev/null +++ b/js/models/validator.js @@ -0,0 +1,94 @@ +var Validator = function() {}; + +Validator.prototype = { + has_errors: function() { + return this.errors.length > 0; + }, + + validate: function(object) { + this.errors = []; + for(validation in this.schema) { + switch(validation) { + case 'presence_of': + this.validate_presence_of(object, this.schema[validation]); + break; + case 'numericality_of': + this.validate_numericality_of(object, this.schema[validation]); + break; + case 'inclusion_of': + this.validate_inclusion_of(object, this.schema[validation]); + break; + } + } + }, + + validate_presence_of: function(object, fields) { + var context = this; + fields = this.arrayfy_fields(fields); + + $.each(fields, function() { + var field = object[this]; + if(typeof(field) == 'undefined') { + context.errors.push({'field': this, 'message': context.error_messages['presence_of']}) + } + }); + }, + + validate_numericality_of: function(object, fields) { + var context = this; + fields = this.arrayfy_fields(fields); + + $.each(fields, function() { + if(this == '[object Object]') { + if(object[this['field']] == undefined) { + context.errors.push({'field': this['field'], 'message': context.error_messages['numericality_of']}); + } else { + var field = object[this['field']].toString(); + if(!field.match(/^\d+$/) || field > this['max']) { + context.errors.push({'field': this['field'], 'message': context.error_messages['numericality_of']}); + } + } + } else { + if(object[this] == undefined) { + context.errors.push({'field': this, 'message': context.error_messages['numericality_of']}); + } else { + var field = object[this].toString(); + if(!field.match(/^\d+$/)) { + context.errors.push({'field': this, 'message': context.error_messages['numericality_of']}); + } + } + } + }); + }, + + validate_inclusion_of: function(object, field) { + var content = object[field['field']]; + var included = false; + + $.each(field['in'], function() { + if(this == content) { + included = true; + } + }); + if(!included) { + this.errors.push({'field': field['field'], 'message': this.error_messages['inclusion_of']}); + } + }, + + arrayfy_fields: function(fields) { + if(!$.isArray(fields)) { + return [fields]; + } else { + return fields; + } + }, + + schema: {}, + errors: [], + + error_messages: { + presence_of: 'should not be empty', + numericality_of: 'is not a valid number', + inclusion_of: 'is not in the list of a valid values' + } +}; \ No newline at end of file diff --git a/js/rpc.js b/js/rpc.js new file mode 100644 index 0000000..3ccabb7 --- /dev/null +++ b/js/rpc.js @@ -0,0 +1,53 @@ +(function($) { + + Sammy = Sammy || {}; + + Sammy.TransmissionRPC = function(app) { + + app.rpc = { + 'url': '/transmission/rpc', + 'session_id': '' + }; + + app.helpers({ + remote_session_id: function() { + return this.app.rpc.session_id; + }, + remote_query: function(params, callback) { + var context = this.app.rpc; + var that = this; + $.ajax({ + type: 'POST', + url: context.url, + dataType: 'json', + data: JSON.stringify(params), + processData: false, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Transmission-Session-Id', context.session_id); + }, + success: function(response) { + if(!response) { + console.log('RPC Connection Failure.'); + console.log('You need to run this web client within the Transmission web server.'); + } + if(callback) { + callback(response['arguments']); + } + }, + error: function(xhr, ajaxOptions, thrownError) { + context.session_id = xhr.getResponseHeader('X-Transmission-Session-Id'); + if(xhr.status === 409 && context.session_id.length > 0) { + that.remote_query(params, callback); + } else { + if(window.console) { + console.log('RPC Connection Failure.'); + console.log(xhr.responseText); + } + } + } + }); + } + }); + }; + +})(jQuery); \ No newline at end of file diff --git a/js/transmission.js b/js/transmission.js new file mode 100644 index 0000000..7d432d2 --- /dev/null +++ b/js/transmission.js @@ -0,0 +1,41 @@ +var transmission = new $.sammy(function() { with(this) { + element_selector = '#container'; + cache_partials = true; + use(Sammy.TransmissionRPC); + use(Sammy.Mustache); + use(Sammy.Cache); + + helpers(ApplicationHelpers); + helpers(LinkHelpers); + helpers(TorrentHelpers); + helpers(SortTorrentsHelpers); + helpers(FilterTorrentsHelpers); + helpers(InfoHelpers); + helpers(ViewHelpers); + helpers(StoreHelpers); + helpers(SettingHelpers); + helpers(StatisticHelpers); + + Torrents(this); + Settings(this); + Statistics(this); + + bind('flash', function(e, message) { with(this) { + this.showAndHideFlash(message); + }}); + + bind('errors', function(e, errors) { with(this) { + this.showErrors(errors); + }}); + + bind('init', function() { with(this) { + this.initializeStore(); + this.activateLinks(); + this.closeInfo(); + }}); +}}); + +$(function() { + transmission.run('#/torrents'); + transmission.trigger('init'); +}); \ No newline at end of file diff --git a/js/views/statistics.js b/js/views/statistics.js new file mode 100644 index 0000000..8a20c7d --- /dev/null +++ b/js/views/statistics.js @@ -0,0 +1,10 @@ +StatisticsView = function(statistics) { + var view = { + 'number_of_torrents': statistics['torrentCount'], + 'uploaded': Math.formatBytes(statistics['current-stats']['uploadedBytes']), + 'downloaded': Math.formatBytes(statistics['current-stats']['downloadedBytes']), + 'time_active': Math.formatSeconds(statistics['current-stats']['secondsActive']) + } + + return view; +} \ No newline at end of file diff --git a/js/views/torrent.js b/js/views/torrent.js new file mode 100644 index 0000000..96c79bf --- /dev/null +++ b/js/views/torrent.js @@ -0,0 +1,87 @@ +TorrentView = function(torrent, context, sort_peers) { + var view = torrent; + view.sort_peers = sort_peers || 'client'; + + view.formatTime = function(timestamp) { + var current = new Date(parseInt(timestamp) * 1000); + if(current) { + var date = (current.getMonth() + 1) + '/' + current.getDate() + '/' + current.getFullYear(); + var time = current.getHours() + ':' + current.getMinutes(); + return date + ' ' + time; + } else { + return timestamp; + } + }; + + view.addFormattedTimes = function() { + if(view.trackerStats !== undefined) { + var i = 0; + $.each(view.trackerStats, function() { + view.trackerStats[i]['lastAnnounceTimeFormatted'] = view.formatTime(this.lastAnnounceTime); + view.trackerStats[i]['nextAnnounceTimeFormatted'] = context.formatNextAnnounceTime(this.nextAnnounceTime); + view.trackerStats[i]['lastScrapeTimeFormatted'] = view.formatTime(this.lastScrapeTime); + i += 1; + }); + } + }; + + view.addFormattedSizes = function() { + var i = 0; + if(view.files !== undefined) { + $.each(view.files, function() { + view.files[i]['lengthFormatted'] = Math.formatBytes(this['length']); + view.files[i]['percentDone'] = Math.formatPercent(this['length'], this['length'] - this.bytesCompleted); + i += 1; + }); + } + if(view.peers !== undefined) { + i = 0; + $.each(view.peers, function() { + view.peers[i]['uploadFormatted'] = this['rateToPeer'] !== 0 ? Math.formatBytes(this['rateToPeer']) : ''; + view.peers[i]['downloadFormatted'] = this['rateToClient'] !== 0? Math.formatBytes(this['rateToClient']) : ''; + view.peers[i]['percentDone'] = Math.formatPercent(100, 100 - (this['progress'] * 100)); + i += 1; + }); + } + }; + + view.sortPeers = function() { + if(view.peers !== undefined) { + var peers = view.peers; + var peer_sort_function = function() {}; + + switch(view.sort_peers) { + case 'client': + peer_sort_function = function(a, b) { + var a_name = a.clientName.toUpperCase(); + var b_name = b.clientName.toUpperCase(); + return (a_name < b_name) ? -1 : (a_name > b_name) ? 1 : 0; + }; + break; + case 'percent': + peer_sort_function = function(a, b) { + return b.percentDone - a.percentDone; + }; + break; + case 'upload': + peer_sort_function = function(a, b) { + return b.rateToPeer - a.rateToPeer; + }; + break; + case 'download': + peer_sort_function = function(a, b) { + return b.rateToClient - a.rateToClient; + }; + break; + } + + view.peers = peers.sort(peer_sort_function); + } + }; + + view.addFormattedTimes(); + view.addFormattedSizes(); + view.sortPeers(); + + return view; +}; \ No newline at end of file diff --git a/js/views/torrents.js b/js/views/torrents.js new file mode 100644 index 0000000..85e8193 --- /dev/null +++ b/js/views/torrents.js @@ -0,0 +1,19 @@ +TorrentsView = function(torrent, context) { + var view = torrent; + + view.pauseAndActivateButton = function() { + var torrent = Torrent(view); + var options = torrent.isActive() ? ['torrent-stop', 'Pause', 'pause'] : ['torrent-start', 'Activate', 'activate']; + this.cache_partial('./templates/torrents/pause_and_activate_button.mustache', 'pause_and_activate_button', context); + return context.mustache(context.cache('pause_and_activate_button'), { + 'id': torrent.id, + 'method': options[0], + 'button': options[1], + 'css_class': options[2] + }); + }; + + view.cache_partial = context.cache_partial; + + return view; +}; \ No newline at end of file diff --git a/spec/filter_torrents_helpers_spec.js b/spec/filter_torrents_helpers_spec.js new file mode 100644 index 0000000..3026b66 --- /dev/null +++ b/spec/filter_torrents_helpers_spec.js @@ -0,0 +1,50 @@ +describe 'FilterTorrentsHelpers' + before_each + filter_helpers = FilterTorrentsHelpers + end + + it 'should not filter if filter is all' + var torrents = [ + Torrent({'id': '1', 'name': 'Zelda'}), + Torrent({'id': '2', 'name': 'Alpha'}), + Torrent({'id': '3', 'name': 'Manfred'}) + ] + var filtered_torrents = filter_helpers.filterTorrents('all', torrents) + filtered_torrents[0].id.should.eql('1') + filtered_torrents[1].id.should.eql('2') + filtered_torrents[2].id.should.eql('3') + end + + it 'should filter by paused' + var torrents = [ + Torrent({'id': '1', 'name': 'Zelda', 'status': 4}), + Torrent({'id': '2', 'name': 'Alpha', 'status': 16}), + Torrent({'id': '3', 'name': 'Manfred', 'status': 8}) + ] + var filtered_torrents = filter_helpers.filterTorrents('paused', torrents) + filtered_torrents[0].id.should.eql('2') + filtered_torrents.length.should.eql(1) + end + + it 'should filter by downloading' + var torrents = [ + Torrent({'id': '1', 'name': 'Zelda', 'status': 4}), + Torrent({'id': '2', 'name': 'Alpha', 'status': 16}), + Torrent({'id': '3', 'name': 'Manfred', 'status': 8}) + ] + var filtered_torrents = filter_helpers.filterTorrents('downloading', torrents) + filtered_torrents[0].id.should.eql('1') + filtered_torrents.length.should.eql(1) + end + + it 'should filter by seeding' + var torrents = [ + Torrent({'id': '1', 'name': 'Zelda', 'status': 4}), + Torrent({'id': '2', 'name': 'Alpha', 'status': 16}), + Torrent({'id': '3', 'name': 'Manfred', 'status': 8}) + ] + var filtered_torrents = filter_helpers.filterTorrents('seeding', torrents) + filtered_torrents[0].id.should.eql('3') + filtered_torrents.length.should.eql(1) + end +end \ No newline at end of file diff --git a/spec/fixtures/pause_and_activate_button.mustache b/spec/fixtures/pause_and_activate_button.mustache new file mode 100644 index 0000000..ae7b094 --- /dev/null +++ b/spec/fixtures/pause_and_activate_button.mustache @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/spec/fixtures/settings.html b/spec/fixtures/settings.html new file mode 100644 index 0000000..e2c80b2 --- /dev/null +++ b/spec/fixtures/settings.html @@ -0,0 +1,12 @@ +
    + + + + + + + + + +
    DHT enabled:
    PEX enabled:
    +
    diff --git a/spec/fixtures/show.mustache b/spec/fixtures/show.mustache new file mode 100644 index 0000000..755cac7 --- /dev/null +++ b/spec/fixtures/show.mustache @@ -0,0 +1,16 @@ +
  • +

    {{name}}

    +

    {{progressDetails}}

    + +
    +
    + {{{progressBar}}} +
    +
    {{{pauseAndActivateButton}}}
    +
    + +
    +
    + +
    {{statusString}}
    +
  • \ No newline at end of file diff --git a/spec/fixtures/torrents.html b/spec/fixtures/torrents.html new file mode 100644 index 0000000..7aa1a94 --- /dev/null +++ b/spec/fixtures/torrents.html @@ -0,0 +1,56 @@ +
      +
    • +
      CouchDB

      +
      22.0 MB, uploaded 13.4 MB (Ratio: 0.5942)

      + +
      +
      +
      +
      +
      +
      + + +
      +
      +
      + +
      Seeding - DL: 0.0 KB/s, UL: 0.0 KB/s
      +
    • +
    • +
      Apache
      +
      82.0 MB, uploaded 13.4 MB (Ratio: 0.1742)
      + +
      +
      +
      +
      +
      +
      + + +
      +
      +
      + +
      Seeding - DL: 0.0 KB/s, UL: 0.0 KB/s
      +
    • +
    • +
      CouchApp
      +
      Magnetized transfer - retrieving metadata (0%)
      + +
      +
      +
      +
      +
      +
      + + +
      +
      +
      + +
      Downloading from 1 of 1 peers - DL: 0 bytes/s, UL: 0 bytes/s
      +
    • +
    diff --git a/spec/fixtures/torrents_view_forms.html b/spec/fixtures/torrents_view_forms.html new file mode 100644 index 0000000..4cb3bb2 --- /dev/null +++ b/spec/fixtures/torrents_view_forms.html @@ -0,0 +1,8 @@ +
    + + +
    +
    + + +
    \ No newline at end of file diff --git a/spec/index.html b/spec/index.html new file mode 100644 index 0000000..9154eda --- /dev/null +++ b/spec/index.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + +
    + + \ No newline at end of file diff --git a/spec/jspec/images/bg.png b/spec/jspec/images/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..947804ff6acaaf93986a0a11d205df3113656816 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^j0_CS3LH#8R&M_M3qVS;#5JNMI6tkVJh3R1!7(L2 zDOJHUH!(dmC^a#qvhZZ84N#Gdr;B4q#jT`Y|Nq+yGczC7kaMcgIDRvs>~})ZZHL{d z3*N|ke!F!0+{6dRCuY6RkTi>G?|T$zbHu=*fssL9)snBgXWIv$ISihzelF{r5}E+; Cx;4N6 literal 0 HcmV?d00001 diff --git a/spec/jspec/images/hr.png b/spec/jspec/images/hr.png new file mode 100644 index 0000000000000000000000000000000000000000..4a94d12fd8405c32b4c20abe777c1a9a7e6a30cb GIT binary patch literal 321 zcmV-H0lxl;P)A5E2-vx}KFiT96$Z17UamL`af0P}@7%Vq>XUIGjN-4D7? ThAmXZ00000NkvXXu0mjfvYCee literal 0 HcmV?d00001 diff --git a/spec/jspec/images/loading.gif b/spec/jspec/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..c69e937232b24ea30f01c68bbd2ebc798dcecfcb GIT binary patch literal 2608 zcmdVcdr(tX9tZGC9yiG~=H_*Q-0%n(kWqP*D#hw{AQu8;1%gl-Hrf&{2?48KX;hHy z3Ze*zEz4t3XdUFyLbNPUYlA`|B}P=N1fqtL1*}S;87#|-W9v<#G;ul(e%d3)N(^9c$d2Dz{7}?ErjNd;{EMKkCsk21~b9Gvg zDo<7L=3Z5HNbVlZUcm1eg#o#CZCJU`3IYHwM->zCd?uYrF3vKFeM}v?f+%s?E>ly|3W25ry9#NNbTx-}0ON58dTrs^ix{_1O0Wh~SVSBlH)Ajn zPn^Gbjz}PCtN@#keR&hK&Dhl-b$kZ8^S)x#dh0{7X=X%CCJk7P1PSO>T&S8I4{#Lg zb5#)o=;!ZP*1nM{cI4@(x7o27*SA()NHmrn67aN@Pmi~(i_SnrjYnwh36aG%!@i0d zqbvfa44f|?OG4ntP|nbjhEl1)Yp6ZN@yjy zy4==QmLy%t;ps3R?~f2KfTTI|2?q8dFd6^z5GF+Xa&Y)sjG)hxit80pPcOP zJ z*LW{SyGHD%hUotV+W%I}fBLAIx!8|7#}$;clKQ+{&FjDqGQ2ZNx(lYM3*%~}ILnao zM`aui55~ZFJlu^!5rdA9Q_7H68H_;##u{x(Yn-vSfIRCb^Nqsg zGRS!Egm>h+o<}LeV4&CLReo9FrDjDvs}8?JwC)#Qs|ie=r?~xUh)&*d`Fx>FG}%X# zNdtDHBKhLPC0wpooFDAQKL%*6T|ULH$=wX!NhcasgD3d;-d$I6yRK3yN+E~C1335_iLOt+*9uvSZ`>*KA}vm}08wRq=>5l|t*Na&jR z-C1&C`nkEk#sB|@yyt-#fXngP04My zm7u$Q%EJbHp`>~`5W&L{W!6`y&}LMS;jfUpgO~7TLVMRZ9IC)IZp0A${`yp0{&wco z#1nx@XMkhqeK%7?RE7JdLr1^nwFfaJ0Q&Lv?WNJ%9}VSJsNY2+UYs2%EU0J~ayFXv zi*?7KCXQHkD)O6!0Q%4N+HTODHxJ{kQSuQX$l-rSwkwh(zMkdfzxyGwl@yHC)C4p< z&n2%8#M?)Q@mgHL1ot8`SFdSEj9ye|jHy+U8#@HoUExG=@AVkRAe_qYm4EpzK6L*& zh`)26?V#f4#_h^P9G^%>h2-H3)$QP zQovu6J9qDvsxqweDdNNa!Lb?L4_UF{tLX_nN7r0U_vF14YKcGR-*Gl} zx3oG)bzf|65dBxD-;2ZCp??K;+TuQ9onnK?==5hzbkb^r_g>z4#D8mcv8(+XdoszA zCx-qhdgxMNMotj}SiL_6V(tLcsK7(M(r(%u<}QrVfOvyK6_;~NOTlPGfX@M7S5YQF z&*$(ylJMHJt^_aQeu{C6NaTE$G3HNN@_SnN8YcaKn%`)F@~L1x+ah7-gEJPpc6w%3 zyX}r+Qk$4RHZzfH){e~F*qJ{d*L8a6n4;U?+{de0-t)mal#TVxe)3F}^UBh+zd T)6_**#cgp_+?JL9(ew3BlNF>u literal 0 HcmV?d00001 diff --git a/spec/jspec/images/sprites.bg.png b/spec/jspec/images/sprites.bg.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8790f338c4fce6611e0c2d2f811882a4136b32 GIT binary patch literal 4876 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 literal 0 HcmV?d00001 diff --git a/spec/jspec/images/sprites.png b/spec/jspec/images/sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..9bbe163ac69349c0155f3dc605a6f0839317c640 GIT binary patch literal 8600 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 literal 0 HcmV?d00001 diff --git a/spec/jspec/jspec.css b/spec/jspec/jspec.css new file mode 100644 index 0000000..4ee31e5 --- /dev/null +++ b/spec/jspec/jspec.css @@ -0,0 +1,147 @@ +body.jspec { + margin: 45px 0; + font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; + background: #efefef url(images/bg.png) top left repeat-x; + text-align: center; +} +#jspec { + margin: 0 auto; + padding-top: 30px; + width: 1008px; + background: url(images/vr.png) top left repeat-y; + text-align: left; +} +#jspec-top { + position: relative; + margin: 0 auto; + width: 1008px; + height: 40px; + background: url(images/sprites.bg.png) top left no-repeat; +} +#jspec-bottom { + margin: 0 auto; + width: 1008px; + height: 15px; + background: url(images/sprites.bg.png) bottom left no-repeat; +} +#jspec .loading { + margin-top: -45px; + width: 1008px; + height: 80px; + background: url(images/loading.gif) 50% 50% no-repeat; +} +#jspec-title { + position: absolute; + top: 15px; + left: 20px; + width: 160px; + font-size: 22px; + font-weight: normal; + background: url(images/sprites.png) 0 -126px no-repeat; + text-align: center; +} +#jspec-title em { + font-size: 10px; + font-style: normal; + color: #BCC8D1; +} +#jspec-report * { + margin: 0; + padding: 0; + background: none; + border: none; +} +#jspec-report { + padding: 15px 40px; + font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; + color: #7B8D9B; +} +#jspec-report.has-failures { + padding-bottom: 30px; +} +#jspec-report .hidden { + display: none; +} +#jspec-report .heading { + margin-bottom: 15px; +} +#jspec-report .heading span { + padding-right: 10px; +} +#jspec-report .heading .passes em { + color: #0ea0eb; +} +#jspec-report .heading .failures em { + color: #FA1616; +} +#jspec-report table { + font-size: 11px; + border-collapse: collapse; +} +#jspec-report td { + padding: 8px; + text-indent: 30px; + color: #7B8D9B; +} +#jspec-report tr.body { + display: none; +} +#jspec-report tr.body pre { + margin: 0; + padding: 0 0 5px 25px; +} +#jspec-report tr.even:hover + tr.body, +#jspec-report tr.odd:hover + tr.body { + display: block; +} +#jspec-report tr td:first-child em { + font-style: normal; + font-weight: normal; + color: #7B8D9B; +} +#jspec-report tr.even:hover, +#jspec-report tr.odd:hover { + text-shadow: 1px 1px 1px #fff; + background: #F2F5F7; +} +#jspec-report td + td { + padding-right: 0; + width: 15px; +} +#jspec-report td.pass { + background: url(images/sprites.png) 3px -7px no-repeat; +} +#jspec-report td.fail { + background: url(images/sprites.png) 3px -47px no-repeat; + font-weight: bold; + color: #FC0D0D; +} +#jspec-report td.requires-implementation { + background: url(images/sprites.png) 3px -87px no-repeat; +} +#jspec-report tr.description td { + margin-top: 25px; + padding-top: 25px; + font-size: 12px; + font-weight: bold; + text-indent: 0; + color: #1a1a1a; +} +#jspec-report tr.description:first-child td { + border-top: none; +} +#jspec-report .assertion { + display: block; + float: left; + margin: 0 0 0 1px; + padding: 0; + width: 1px; + height: 5px; + background: #7B8D9B; +} +#jspec-report .assertion.failed { + background: red; +} +.jspec-sandbox { + display: none; +} \ No newline at end of file diff --git a/spec/jspec/jspec.jquery.js b/spec/jspec/jspec.jquery.js new file mode 100644 index 0000000..c21e5fa --- /dev/null +++ b/spec/jspec/jspec.jquery.js @@ -0,0 +1,71 @@ + +// JSpec - jQuery - Copyright TJ Holowaychuk (MIT Licensed) + +JSpec +.requires('jQuery', 'when using jspec.jquery.js') +.include({ + name: 'jQuery', + + // --- Initialize + + init : function() { + jQuery.ajaxSetup({ async: false }) + }, + + // --- Utilities + + utilities : { + element: jQuery, + elements: jQuery, + sandbox : function() { + return jQuery('
    ') + } + }, + + // --- Matchers + + matchers : { + have_tag : "jQuery(expected, actual).length == 1", + have_one : "alias have_tag", + have_tags : "jQuery(expected, actual).length > 1", + have_many : "alias have_tags", + have_child : "jQuery(actual).children(expected).length == 1", + have_children : "jQuery(actual).children(expected).length > 1", + have_text : "jQuery(actual).text() == expected", + have_value : "jQuery(actual).val() == expected", + be_enabled : "!jQuery(actual).attr('disabled')", + have_class : "jQuery(actual).hasClass(expected)", + + be_visible : function(actual) { + return jQuery(actual).css('display') != 'none' && + jQuery(actual).css('visibility') != 'hidden' && + jQuery(actual).attr('type') != 'hidden' + }, + + be_hidden : function(actual) { + return !JSpec.does(actual, 'be_visible') + }, + + have_classes : function(actual) { + return !JSpec.any(JSpec.argumentsToArray(arguments, 1), function(arg){ + return !JSpec.does(actual, 'have_class', arg) + }) + }, + + have_attr : function(actual, attr, value) { + return value ? jQuery(actual).attr(attr) == value: + jQuery(actual).attr(attr) + }, + + 'be disabled selected checked' : function(attr) { + return 'jQuery(actual).attr("' + attr + '")' + }, + + 'have type id title alt href src sel rev name target' : function(attr) { + return function(actual, value) { + return JSpec.does(actual, 'have_attr', attr, value) + } + } + } +}) + diff --git a/spec/jspec/jspec.js b/spec/jspec/jspec.js new file mode 100644 index 0000000..b72a977 --- /dev/null +++ b/spec/jspec/jspec.js @@ -0,0 +1,1775 @@ + +// JSpec - Core - Copyright TJ Holowaychuk (MIT Licensed) + +(function(){ + + JSpec = { + + version : '2.11.6', + cache : {}, + suites : [], + modules : [], + allSuites : [], + matchers : {}, + stubbed : [], + request : 'XMLHttpRequest' in this ? XMLHttpRequest : null, + stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 }, + options : { profile: false }, + + /** + * Default context in which bodies are evaluated. + * + * Replace context simply by setting JSpec.context + * to your own like below: + * + * JSpec.context = { foo : 'bar' } + * + * Contexts can be changed within any body, this can be useful + * in order to provide specific helper methods to specific suites. + * + * To reset (usually in after hook) simply set to null like below: + * + * JSpec.context = null + * + */ + + defaultContext : { + + /** + * Return an object used for proxy assertions. + * This object is used to indicate that an object + * should be an instance of _object_, not the constructor + * itself. + * + * @param {function} constructor + * @return {hash} + * @api public + */ + + an_instance_of : function(constructor) { + return { an_instance_of : constructor } + }, + + /** + * Load fixture at _path_. This utility function + * supplies the means to resolve, and cache fixture contents + * via the DOM or Rhino. + * + * Fixtures are resolved as: + * + * - + * - fixtures/ + * - fixtures/.html + * + * @param {string} path + * @return {string} + * @api public + */ + + fixture : function(path) { + if (JSpec.cache[path]) return JSpec.cache[path] + return JSpec.cache[path] = + JSpec.tryLoading(path) || + JSpec.tryLoading('fixtures/' + path) || + JSpec.tryLoading('fixtures/' + path + '.html') || + JSpec.tryLoading('spec/' + path) || + JSpec.tryLoading('spec/fixtures/' + path) || + JSpec.tryLoading('spec/fixtures/' + path + '.html') + } + }, + + // --- Objects + + formatters : { + + /** + * Report to server. + * + * Options: + * - uri specific uri to report to. + * - verbose weither or not to output messages + * - failuresOnly output failure messages only + * + * @api public + */ + + Server : function(results, options) { + var uri = options.uri || 'http://' + window.location.host + '/results' + JSpec.post(uri, { + stats: JSpec.stats, + options: options, + results: map(results.allSuites, function(suite) { + if (suite.hasSpecs()) + return { + description: suite.description, + specs: map(suite.specs, function(spec) { + return { + description: spec.description, + message: !spec.passed() ? spec.failure().message : null, + status: spec.requiresImplementation() ? 'pending' : + spec.passed() ? 'pass' : + 'fail', + assertions: map(spec.assertions, function(assertion){ + return { + passed: assertion.passed + } + }) + } + }) + } + }) + }) + if ('close' in main) main.close() + }, + + /** + * Default formatter, outputting to the DOM. + * + * Options: + * - reportToId id of element to output reports to, defaults to 'jspec' + * - failuresOnly displays only suites with failing specs + * + * @api public + */ + + DOM : function(results, options) { + var id = option('reportToId') || 'jspec' + var report = document.getElementById(id) + var failuresOnly = option('failuresOnly') + var classes = results.stats.failures ? 'has-failures' : '' + if (!report) throw 'JSpec requires the element #' + id + ' to output its reports' + + function bodyContents(body) { + return JSpec. + escape(JSpec.contentsOf(body)). + replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }). + replace(/\r\n|\r|\n/gm, '
    ') + } + + report.innerHTML = '
    \ + Passes: ' + results.stats.passes + ' \ + Failures: ' + results.stats.failures + ' \ +
    ' + map(results.allSuites, function(suite) { + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran + if (displaySuite && suite.hasSpecs()) + return '' + + map(suite.specs, function(i, spec) { + return '' + + (spec.requiresImplementation() ? + '' : + (spec.passed() && !failuresOnly) ? + '' : + !spec.passed() ? + '' : + '') + + '' + }).join('') + '' + }).join('') + '
    ' + escape(suite.description) + '
    ' + escape(spec.description) + '' + escape(spec.description)+ '' + spec.assertionsGraph() + '' + escape(spec.description) + ' ' + escape(spec.failure().message) + '' + '' + spec.assertionsGraph() + '
    ' + bodyContents(spec.body) + '
    ' + }, + + /** + * Terminal formatter. + * + * @api public + */ + + Terminal : function(results, options) { + failuresOnly = option('failuresOnly') + print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') + + color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + "\n") + + function indent(string) { + return string.replace(/^(.)/gm, ' $1') + } + + each(results.allSuites, function(suite) { + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran + if (displaySuite && suite.hasSpecs()) { + print(color(' ' + suite.description, 'bold')) + each(suite.specs, function(spec){ + var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){ + return graph + color('.', assertion.passed ? 'green' : 'red') + }) + if (spec.requiresImplementation()) + print(color(' ' + spec.description, 'blue') + assertionsGraph) + else if (spec.passed() && !failuresOnly) + print(color(' ' + spec.description, 'green') + assertionsGraph) + else if (!spec.passed()) + print(color(' ' + spec.description, 'red') + assertionsGraph + + "\n" + indent(spec.failure().message) + "\n") + }) + print("") + } + }) + }, + + /** + * Console formatter. + * + * @api public + */ + + Console : function(results, options) { + console.log('') + console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures) + each(results.allSuites, function(suite) { + if (suite.ran) { + console.group(suite.description) + each(suite.specs, function(spec){ + var assertionCount = spec.assertions.length + ':' + if (spec.requiresImplementation()) + console.warn(spec.description) + else if (spec.passed()) + console.log(assertionCount + ' ' + spec.description) + else + console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message) + }) + console.groupEnd() + } + }) + } + }, + + Assertion : function(matcher, actual, expected, negate) { + extend(this, { + message: '', + passed: false, + actual: actual, + negate: negate, + matcher: matcher, + expected: expected, + + // Report assertion results + + report : function() { + this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++ + return this + }, + + // Run the assertion + + run : function() { + // TODO: remove unshifting + expected.unshift(actual) + this.result = matcher.match.apply(this, expected) + this.passed = negate ? !this.result : this.result + if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name) + return this + } + }) + }, + + ProxyAssertion : function(object, method, times, negate) { + var self = this + var old = object[method] + + // Proxy + + object[method] = function(){ + args = argumentsToArray(arguments) + result = old.apply(object, args) + self.calls.push({ args : args, result : result }) + return result + } + + // Times + + this.times = { + once : 1, + twice : 2 + }[times] || times || 1 + + extend(this, { + calls: [], + message: '', + defer: true, + passed: false, + negate: negate, + object: object, + method: method, + + // Proxy return value + + and_return : function(result) { + this.expectedResult = result + return this + }, + + // Proxy arguments passed + + with_args : function() { + this.expectedArgs = argumentsToArray(arguments) + return this + }, + + // Check if any calls have failing results + + anyResultsFail : function() { + return any(this.calls, function(call){ + return self.expectedResult.an_instance_of ? + call.result.constructor != self.expectedResult.an_instance_of: + hash(self.expectedResult) != hash(call.result) + }) + }, + + // Check if any calls have passing results + + anyResultsPass : function() { + return any(this.calls, function(call){ + return self.expectedResult.an_instance_of ? + call.result.constructor == self.expectedResult.an_instance_of: + hash(self.expectedResult) == hash(call.result) + }) + }, + + // Return the passing result + + passingResult : function() { + return this.anyResultsPass().result + }, + + // Return the failing result + + failingResult : function() { + return this.anyResultsFail().result + }, + + // Check if any arguments fail + + anyArgsFail : function() { + return any(this.calls, function(call){ + return any(self.expectedArgs, function(i, arg){ + if (arg == null) return call.args[i] == null + return arg.an_instance_of ? + call.args[i].constructor != arg.an_instance_of: + hash(arg) != hash(call.args[i]) + + }) + }) + }, + + // Check if any arguments pass + + anyArgsPass : function() { + return any(this.calls, function(call){ + return any(self.expectedArgs, function(i, arg){ + return arg.an_instance_of ? + call.args[i].constructor == arg.an_instance_of: + hash(arg) == hash(call.args[i]) + + }) + }) + }, + + // Return the passing args + + passingArgs : function() { + return this.anyArgsPass().args + }, + + // Return the failing args + + failingArgs : function() { + return this.anyArgsFail().args + }, + + // Report assertion results + + report : function() { + this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures + return this + }, + + // Run the assertion + + run : function() { + var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' ) + + function times(n) { + return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n] + } + + if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail())) + this.message = methodString + ' to return ' + puts(this.expectedResult) + + ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult())) + + if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail())) + this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) + + ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs())) + + if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length == this.times : this.calls.length != this.times) + this.message = methodString + ' to be called ' + times(this.times) + + ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length)) + + if (!this.message.length) + this.passed = true + + return this + } + }) + }, + + /** + * Specification Suite block object. + * + * @param {string} description + * @param {function} body + * @api private + */ + + Suite : function(description, body) { + var self = this + extend(this, { + body: body, + description: description, + suites: [], + specs: [], + ran: false, + hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }, + + // Add a spec to the suite + + addSpec : function(description, body) { + var spec = new JSpec.Spec(description, body) + this.specs.push(spec) + JSpec.stats.specs++ // TODO: abstract + spec.suite = this + }, + + // Add a hook to the suite + + addHook : function(hook, body) { + this.hooks[hook].push(body) + }, + + // Add a nested suite + + addSuite : function(description, body) { + var suite = new JSpec.Suite(description, body) + JSpec.allSuites.push(suite) + suite.name = suite.description + suite.description = this.description + ' ' + suite.description + this.suites.push(suite) + suite.suite = this + }, + + // Invoke a hook in context to this suite + + hook : function(hook) { + if (this.suite) this.suite.hook(hook) + each(this.hooks[hook], function(body) { + JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ") + }) + }, + + // Check if nested suites are present + + hasSuites : function() { + return this.suites.length + }, + + // Check if this suite has specs + + hasSpecs : function() { + return this.specs.length + }, + + // Check if the entire suite passed + + passed : function() { + return !any(this.specs, function(spec){ + return !spec.passed() + }) + } + }) + }, + + /** + * Specification block object. + * + * @param {string} description + * @param {function} body + * @api private + */ + + Spec : function(description, body) { + extend(this, { + body: body, + description: description, + assertions: [], + + // Add passing assertion + + pass : function(message) { + this.assertions.push({ passed: true, message: message }) + ++JSpec.stats.passes + }, + + // Add failing assertion + + fail : function(message) { + this.assertions.push({ passed: false, message: message }) + ++JSpec.stats.failures + }, + + // Run deferred assertions + + runDeferredAssertions : function() { + each(this.assertions, function(assertion){ + if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion) + }) + }, + + // Find first failing assertion + + failure : function() { + return find(this.assertions, function(assertion){ + return !assertion.passed + }) + }, + + // Find all failing assertions + + failures : function() { + return select(this.assertions, function(assertion){ + return !assertion.passed + }) + }, + + // Weither or not the spec passed + + passed : function() { + return !this.failure() + }, + + // Weither or not the spec requires implementation (no assertions) + + requiresImplementation : function() { + return this.assertions.length == 0 + }, + + // Sprite based assertions graph + + assertionsGraph : function() { + return map(this.assertions, function(assertion){ + return '' + }).join('') + } + }) + }, + + Module : function(methods) { + extend(this, methods) + }, + + JSON : { + + /** + * Generic sequences. + */ + + meta : { + '\b' : '\\b', + '\t' : '\\t', + '\n' : '\\n', + '\f' : '\\f', + '\r' : '\\r', + '"' : '\\"', + '\\' : '\\\\' + }, + + /** + * Escapable sequences. + */ + + escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + + /** + * JSON encode _object_. + * + * @param {mixed} object + * @return {string} + * @api private + */ + + encode : function(object) { + var self = this + if (object == undefined || object == null) return 'null' + if (object === true) return 'true' + if (object === false) return 'false' + switch (typeof object) { + case 'number': return object + case 'string': return this.escapable.test(object) ? + '"' + object.replace(this.escapable, function (a) { + return typeof self.meta[a] === 'string' ? self.meta[a] : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4) + }) + '"' : + '"' + object + '"' + case 'object': + if (object.constructor == Array) + return '[' + map(object, function(val){ + return self.encode(val) + }).join(', ') + ']' + else if (object) + return '{' + map(object, function(key, val){ + return self.encode(key) + ':' + self.encode(val) + }).join(', ') + '}' + } + return 'null' + } + }, + + // --- DSLs + + DSLs : { + snake : { + expect : function(actual){ + return JSpec.expect(actual) + }, + + describe : function(description, body) { + return JSpec.currentSuite.addSuite(description, body) + }, + + it : function(description, body) { + return JSpec.currentSuite.addSpec(description, body) + }, + + before : function(body) { + return JSpec.currentSuite.addHook('before', body) + }, + + after : function(body) { + return JSpec.currentSuite.addHook('after', body) + }, + + before_each : function(body) { + return JSpec.currentSuite.addHook('before_each', body) + }, + + after_each : function(body) { + return JSpec.currentSuite.addHook('after_each', body) + }, + + should_behave_like : function(description) { + return JSpec.shareBehaviorsOf(description) + } + } + }, + + // --- Methods + + /** + * Check if _value_ is 'stop'. For use as a + * utility callback function. + * + * @param {mixed} value + * @return {bool} + * @api public + */ + + haveStopped : function(value) { + return value === 'stop' + }, + + /** + * Include _object_ which may be a hash or Module instance. + * + * @param {has, Module} object + * @return {JSpec} + * @api public + */ + + include : function(object) { + var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object) + this.modules.push(module) + if ('init' in module) module.init() + if ('utilities' in module) extend(this.defaultContext, module.utilities) + if ('matchers' in module) this.addMatchers(module.matchers) + if ('formatters' in module) extend(this.formatters, module.formatters) + if ('DSLs' in module) + each(module.DSLs, function(name, methods){ + JSpec.DSLs[name] = JSpec.DSLs[name] || {} + extend(JSpec.DSLs[name], methods) + }) + return this + }, + + /** + * Add a module hook _name_, which is immediately + * called per module with the _args_ given. An array of + * hook return values is returned. + * + * @param {name} string + * @param {...} args + * @return {array} + * @api private + */ + + hook : function(name, args) { + args = argumentsToArray(arguments, 1) + return inject(JSpec.modules, [], function(results, module){ + if (typeof module[name] == 'function') + results.push(JSpec.evalHook(module, name, args)) + }) + }, + + /** + * Eval _module_ hook _name_ with _args_. Evaluates in context + * to the module itself, JSpec, and JSpec.context. + * + * @param {Module} module + * @param {string} name + * @param {array} args + * @return {mixed} + * @api private + */ + + evalHook : function(module, name, args) { + hook('evaluatingHookBody', module, name) + try { return module[name].apply(module, args) } + catch(e) { error('Error in hook ' + module.name + '.' + name + ': ', e) } + }, + + /** + * Same as hook() however accepts only one _arg_ which is + * considered immutable. This function passes the arg + * to the first module, then passes the return value of the last + * module called, to the following module. + * + * @param {string} name + * @param {mixed} arg + * @return {mixed} + * @api private + */ + + hookImmutable : function(name, arg) { + return inject(JSpec.modules, arg, function(result, module){ + if (typeof module[name] == 'function') + return JSpec.evalHook(module, name, [result]) + }) + }, + + /** + * Find a suite by its description or name. + * + * @param {string} description + * @return {Suite} + * @api private + */ + + findSuite : function(description) { + return find(this.allSuites, function(suite){ + return suite.name == description || suite.description == description + }) + }, + + /** + * Share behaviors (specs) of the given suite with + * the current suite. + * + * @param {string} description + * @api public + */ + + shareBehaviorsOf : function(description) { + if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite) + else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name' + }, + + /** + * Copy specs from one suite to another. + * + * @param {Suite} fromSuite + * @param {Suite} toSuite + * @api public + */ + + copySpecs : function(fromSuite, toSuite) { + each(fromSuite.specs, function(spec){ + spec.assertions = [] + toSuite.specs.push(spec) + }) + }, + + /** + * Convert arguments to an array. + * + * @param {object} arguments + * @param {int} offset + * @return {array} + * @api public + */ + + argumentsToArray : function(arguments, offset) { + return Array.prototype.slice.call(arguments, offset || 0) + }, + + /** + * Return ANSI-escaped colored string. + * + * @param {string} string + * @param {string} color + * @return {string} + * @api public + */ + + color : function(string, color) { + return "\u001B[" + { + bold : 1, + black : 30, + red : 31, + green : 32, + yellow : 33, + blue : 34, + magenta : 35, + cyan : 36, + white : 37 + }[color] + 'm' + string + "\u001B[0m" + }, + + /** + * Default matcher message callback. + * + * @api private + */ + + defaultMatcherMessage : function(actual, expected, negate, name) { + return 'expected ' + puts(actual) + ' to ' + + (negate ? 'not ' : '') + + name.replace(/_/g, ' ') + + ' ' + puts.apply(this, expected.slice(1)) + }, + + /** + * Normalize a matcher message. + * + * When no messge callback is present the defaultMatcherMessage + * will be assigned, will suffice for most matchers. + * + * @param {hash} matcher + * @return {hash} + * @api public + */ + + normalizeMatcherMessage : function(matcher) { + if (typeof matcher.message != 'function') + matcher.message = this.defaultMatcherMessage + return matcher + }, + + /** + * Normalize a matcher body + * + * This process allows the following conversions until + * the matcher is in its final normalized hash state. + * + * - '==' becomes 'actual == expected' + * - 'actual == expected' becomes 'return actual == expected' + * - function(actual, expected) { return actual == expected } becomes + * { match : function(actual, expected) { return actual == expected }} + * + * @param {mixed} body + * @return {hash} + * @api public + */ + + normalizeMatcherBody : function(body) { + switch (body.constructor) { + case String: + if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)] + if (body.length < 4) body = 'actual ' + body + ' expected' + return { match: function(actual, expected) { return eval(body) }} + + case Function: + return { match: body } + + default: + return body + } + }, + + /** + * Get option value. This method first checks if + * the option key has been set via the query string, + * otherwise returning the options hash value. + * + * @param {string} key + * @return {mixed} + * @api public + */ + + option : function(key) { + return (value = query(key)) !== null ? value : + JSpec.options[key] || null + }, + + /** + * Generates a hash of the object passed. + * + * @param {object} object + * @return {string} + * @api private + */ + + hash : function(object) { + if (object == null) return 'null' + if (object == undefined) return 'undefined' + function serialize(prefix) { + return inject(object, prefix + ':', function(buffer, key, value){ + return buffer += hash(value) + }) + } + switch (object.constructor) { + case Array : return serialize('a') + case RegExp: return 'r:' + object.toString() + case Number: return 'n:' + object.toString() + case String: return 's:' + object.toString() + case Object: return 'o:' + inject(object, [], function(array, key, value){ + array.push([key, hash(value)]) + }).sort() + default: return object.toString() + } + }, + + /** + * Return last element of an array. + * + * @param {array} array + * @return {object} + * @api public + */ + + last : function(array) { + return array[array.length - 1] + }, + + /** + * Convert object(s) to a print-friend string. + * + * @param {...} object + * @return {string} + * @api public + */ + + puts : function(object) { + if (arguments.length > 1) { + return map(argumentsToArray(arguments), function(arg){ + return puts(arg) + }).join(', ') + } + if (object === undefined) return '' + if (object === null) return 'null' + if (object === true) return 'true' + if (object === false) return 'false' + if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name + if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector) + '' + if (object.jquery) return object.html() + if (object.nodeName) return object.outerHTML + switch (object.constructor) { + case String: return "'" + object + "'" + case Number: return object + case Function: return object.name || object + case Array: + return inject(object, '[', function(b, v){ + return b + ', ' + puts(v) + }).replace('[,', '[') + ' ]' + case Object: + return inject(object, '{', function(b, k, v) { + return b + ', ' + puts(k) + ' : ' + puts(v) + }).replace('{,', '{') + ' }' + default: + return object.toString() + } + }, + + /** + * Escape HTML. + * + * @param {string} html + * @return {string} + * @api public + */ + + escape : function(html) { + return html.toString() + .replace(/&/gmi, '&') + .replace(/"/gmi, '"') + .replace(/>/gmi, '>') + .replace(/ current) while (++current <= end) values.push(current) + else while (--current >= end) values.push(current) + return '[' + values + ']' + }, + + /** + * Report on the results. + * + * @api public + */ + + report : function() { + hook('reporting', JSpec.options) + new (JSpec.options.formatter || JSpec.formatters.DOM)(JSpec, JSpec.options) + }, + + /** + * Run the spec suites. Options are merged + * with JSpec options when present. + * + * @param {hash} options + * @return {JSpec} + * @api public + */ + + run : function(options) { + if (any(hook('running'), haveStopped)) return this + if (options) extend(this.options, options) + if (option('profile')) console.group('Profile') + each(this.suites, function(suite) { JSpec.runSuite(suite) }) + if (option('profile')) console.groupEnd() + return this + }, + + /** + * Run a suite. + * + * @param {Suite} suite + * @api public + */ + + runSuite : function(suite) { + this.currentSuite = suite + this.evalBody(suite.body) + suite.ran = true + hook('beforeSuite', suite), suite.hook('before') + each(suite.specs, function(spec) { + hook('beforeSpec', spec) + suite.hook('before_each') + JSpec.runSpec(spec) + hook('afterSpec', spec) + suite.hook('after_each') + }) + if (suite.hasSuites()) { + each(suite.suites, function(suite) { + JSpec.runSuite(suite) + }) + } + hook('afterSuite', suite), suite.hook('after') + this.stats.suitesFinished++ + }, + + /** + * Report a failure for the current spec. + * + * @param {string} message + * @api public + */ + + fail : function(message) { + JSpec.currentSpec.fail(message) + }, + + /** + * Report a passing assertion for the current spec. + * + * @param {string} message + * @api public + */ + + pass : function(message) { + JSpec.currentSpec.pass(message) + }, + + /** + * Run a spec. + * + * @param {Spec} spec + * @api public + */ + + runSpec : function(spec) { + this.currentSpec = spec + if (option('profile')) console.time(spec.description) + try { this.evalBody(spec.body) } + catch (e) { fail(e) } + if (option('profile')) console.timeEnd(spec.description) + spec.runDeferredAssertions() + destub() + this.stats.specsFinished++ + this.stats.assertions += spec.assertions.length + }, + + /** + * Require a dependency, with optional message. + * + * @param {string} dependency + * @param {string} message (optional) + * @return {JSpec} + * @api public + */ + + requires : function(dependency, message) { + hook('requiring', dependency, message) + try { eval(dependency) } + catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message } + return this + }, + + /** + * Query against the current query strings keys + * or the queryString specified. + * + * @param {string} key + * @param {string} queryString + * @return {string, null} + * @api private + */ + + query : function(key, queryString) { + var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1) + return inject(queryString.split('&'), null, function(value, pair){ + parts = pair.split('=') + return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value + }) + }, + + /** + * Throw a JSpec related error. + * + * @param {string} message + * @param {Exception} e + * @api public + */ + + error : function(message, e) { + throw (message ? message : '') + e.toString() + + (e.line ? ' near line ' + e.line : '') + }, + + /** + * Ad-hoc POST request for JSpec server usage. + * + * @param {string} uri + * @param {string} data + * @api private + */ + + post : function(uri, data) { + if (any(hook('posting', uri, data), haveStopped)) return + var request = this.xhr() + request.open('POST', uri, false) + request.setRequestHeader('Content-Type', 'application/json') + request.send(JSpec.JSON.encode(data)) + }, + + /** + * Instantiate an XMLHttpRequest. + * + * Here we utilize IE's lame ActiveXObjects first which + * allow IE access serve files via the file: protocol, otherwise + * we then default to XMLHttpRequest. + * + * @return {XMLHttpRequest, ActiveXObject} + * @api private + */ + + xhr : function() { + return this.ieXhr() || new JSpec.request + }, + + /** + * Return Microsoft piece of crap ActiveXObject. + * + * @return {ActiveXObject} + * @api public + */ + + ieXhr : function() { + function object(str) { + try { return new ActiveXObject(str) } catch(e) {} + } + return object('Msxml2.XMLHTTP.6.0') || + object('Msxml2.XMLHTTP.3.0') || + object('Msxml2.XMLHTTP') || + object('Microsoft.XMLHTTP') + }, + + /** + * Check for HTTP request support. + * + * @return {bool} + * @api private + */ + + hasXhr : function() { + return JSpec.request || 'ActiveXObject' in main + }, + + /** + * Try loading _file_ returning the contents + * string or null. Chain to locate / read a file. + * + * @param {string} file + * @return {string} + * @api public + */ + + tryLoading : function(file) { + try { return JSpec.load(file) } + catch (e) {console.log(e);} + }, + + /** + * Load a _file_'s contents. + * + * @param {string} file + * @param {function} callback + * @return {string} + * @api public + */ + + load : function(file, callback) { + if (any(hook('loading', file), haveStopped)) return + if ('readFile' in main) + return callback ? readFile(file, callback) : readFile(file) + else if (this.hasXhr()) { + var request = this.xhr() + request.open('GET', file, false) + request.send(null) + if (request.readyState == 4 && + (request.status == 0 || + request.status.toString().charAt(0) == 2)) + return request.responseText + } + else + error("failed to load `" + file + "'") + }, + + /** + * Load, pre-process, and evaluate a file. + * + * @param {string} file + * @param {JSpec} + * @api public + */ + + exec : function(file) { + if (any(hook('executing', file), haveStopped)) return this + if ('node' in main) + this.load(file, function(contents){ + eval('with (JSpec){ ' + JSpec.preprocess(contents) + ' }') + }) + else + eval('with (JSpec){' + this.preprocess(this.load(file)) + '}') + return this + } + } + + // --- Utility functions + + var main = this + var find = JSpec.any + var utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \ + error escape extend puts hash query strip color does addMatchers callIterator argumentsToArray'.split(/\s+/) + while (utils.length) util = utils.shift(), eval('var ' + util + ' = JSpec.' + util) + if (!main.setTimeout) main.setTimeout = function(callback){ callback() } + + // --- Matchers + + addMatchers({ + equal : "===", + be : "alias equal", + be_greater_than : ">", + be_less_than : "<", + be_at_least : ">=", + be_at_most : "<=", + be_a : "actual.constructor == expected", + be_an : "alias be_a", + be_an_instance_of : "actual instanceof expected", + be_null : "actual == null", + be_true : "actual == true", + be_false : "actual == false", + be_undefined : "typeof actual == 'undefined'", + be_type : "typeof actual == expected", + match : "typeof actual == 'string' ? actual.match(expected) : false", + respond_to : "typeof actual[expected] == 'function'", + have_length : "actual.length == expected", + be_within : "actual >= expected[0] && actual <= last(expected)", + have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)", + + eql : function(actual, expected) { + return actual.constructor == Array || + actual instanceof Object ? + hash(actual) == hash(expected): + actual == expected + }, + + receive : { defer : true, match : function(actual, method, times) { + proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate) + JSpec.currentSpec.assertions.push(proxy) + return proxy + }}, + + be_empty : function(actual) { + if (actual.constructor == Object && actual.length == undefined) + for (var key in actual) + return false; + return !actual.length + }, + + include : function(actual) { + for (state = true, i = 1; i < arguments.length; i++) { + arg = arguments[i] + switch (actual.constructor) { + case String: + case Number: + case RegExp: + case Function: + state = actual.toString().match(arg.toString()) + break + + case Object: + state = arg in actual + break + + case Array: + state = any(actual, function(value){ return hash(value) == hash(arg) }) + break + } + if (!state) return false + } + return true + }, + + throw_error : { match : function(actual, expected, message) { + try { actual() } + catch (e) { + this.e = e + var assert = function(arg) { + switch (arg.constructor) { + case RegExp : return arg.test(e) + case String : return arg == (e.message || e.toString()) + case Function : return (e.name || 'Error') == arg.name + } + } + return message ? assert(expected) && assert(message) : + expected ? assert(expected) : + true + } + }, message : function(actual, expected, negate) { + // TODO: refactor when actual is not in expected [0] + var message_for = function(i) { + if (expected[i] == undefined) return 'exception' + switch (expected[i].constructor) { + case RegExp : return 'exception matching ' + puts(expected[i]) + case String : return 'exception of ' + puts(expected[i]) + case Function : return expected[i].name || 'Error' + } + } + exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '') + return 'expected ' + exception + (negate ? ' not ' : '' ) + + ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was') + }}, + + have : function(actual, length, property) { + return actual[property].length == length + }, + + have_at_least : function(actual, length, property) { + return actual[property].length >= length + }, + + have_at_most :function(actual, length, property) { + return actual[property].length <= length + }, + + have_within : function(actual, range, property) { + length = actual[property].length + return length >= range.shift() && length <= range.pop() + }, + + have_prop : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + does(actual[property], 'eql', value) + }, + + have_property : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + value === actual[property] + } + }) + + if ('exports' in main) exports.JSpec = JSpec + +})() \ No newline at end of file diff --git a/spec/jspec/jspec.shell.js b/spec/jspec/jspec.shell.js new file mode 100644 index 0000000..8c6bb5b --- /dev/null +++ b/spec/jspec/jspec.shell.js @@ -0,0 +1,36 @@ + +// JSpec - Shell - Copyright TJ Holowaychuk (MIT Licensed) + +;(function(){ + + var _quit = quit + + Shell = { + + // --- Global + + main: this, + + // --- Commands + + commands: { + quit: ['Terminate the shell', function(){ _quit() }], + exit: ['Terminate the shell', function(){ _quit() }] + }, + + /** + * Start the interactive shell. + * + * @api public + */ + + start : function() { + for (var name in this.commands) + if (this.commands.hasOwnProperty(name)) + this.main.__defineGetter__(name, this.commands[name][1]) + } + } + + Shell.start() + +})() \ No newline at end of file diff --git a/spec/jspec/jspec.timers.js b/spec/jspec/jspec.timers.js new file mode 100644 index 0000000..57d7902 --- /dev/null +++ b/spec/jspec/jspec.timers.js @@ -0,0 +1,90 @@ + +// JSpec - Mock Timers - Copyright TJ Holowaychuk (MIT Licensed) + +;(function(){ + + /** + * Version. + */ + + mockTimersVersion = '1.0.1' + + /** + * Localized timer stack. + */ + + var timers = [] + + /** + * Set mock timeout with _callback_ and timeout of _ms_. + * + * @param {function} callback + * @param {int} ms + * @return {int} + * @api public + */ + + setTimeout = function(callback, ms) { + var id + return id = setInterval(function(){ + callback() + clearInterval(id) + }, ms) + } + + /** + * Set mock interval with _callback_ and interval of _ms_. + * + * @param {function} callback + * @param {int} ms + * @return {int} + * @api public + */ + + setInterval = function(callback, ms) { + callback.step = ms, callback.current = callback.last = 0 + return timers[timers.length] = callback, timers.length + } + + /** + * Destroy timer with _id_. + * + * @param {int} id + * @return {bool} + * @api public + */ + + clearInterval = function(id) { + return delete timers[--id] + } + + /** + * Reset timers. + * + * @return {array} + * @api public + */ + + resetTimers = function() { + return timers = [] + } + + /** + * Increment each timers internal clock by _ms_. + * + * @param {int} ms + * @api public + */ + + tick = function(ms) { + for (var i = 0, len = timers.length; i < len; ++i) + if (timers[i] && (timers[i].current += ms)) + if (timers[i].current - timers[i].last >= timers[i].step) { + var times = Math.floor((timers[i].current - timers[i].last) / timers[i].step) + var remainder = (timers[i].current - timers[i].last) % timers[i].step + timers[i].last = timers[i].current - remainder + while (times-- && timers[i]) timers[i]() + } + } + +})() \ No newline at end of file diff --git a/spec/jspec/jspec.xhr.js b/spec/jspec/jspec.xhr.js new file mode 100644 index 0000000..dfd1418 --- /dev/null +++ b/spec/jspec/jspec.xhr.js @@ -0,0 +1,183 @@ + +// JSpec - XHR - Copyright TJ Holowaychuk (MIT Licensed) + +(function(){ + + // --- Original XMLHttpRequest + + var OriginalXMLHttpRequest = 'XMLHttpRequest' in this ? + XMLHttpRequest : + function(){} + + // --- MockXMLHttpRequest + + var MockXMLHttpRequest = function() { + this.requestHeaders = {} + } + + MockXMLHttpRequest.prototype = { + status: 0, + async: true, + readyState: 0, + responseText: '', + abort: function(){}, + onreadystatechange: function(){}, + + /** + * Return response headers hash. + */ + + getAllResponseHeaders : function(){ + return this.responseHeaders + }, + + /** + * Return case-insensitive value for header _name_. + */ + + getResponseHeader : function(name) { + return this.responseHeaders[name.toLowerCase()] + }, + + /** + * Set case-insensitive _value_ for header _name_. + */ + + setRequestHeader : function(name, value) { + this.requestHeaders[name.toLowerCase()] = value + }, + + /** + * Open mock request. + */ + + open : function(method, url, async, user, password) { + this.user = user + this.password = password + this.url = url + this.readyState = 1 + this.method = method.toUpperCase() + if (async != undefined) this.async = async + if (this.async) this.onreadystatechange() + }, + + /** + * Send request _data_. + */ + + send : function(data) { + this.data = data + this.readyState = 4 + if (this.method == 'HEAD') this.responseText = null + this.responseHeaders['content-length'] = (this.responseText || '').length + if(this.async) this.onreadystatechange() + } + } + + // --- Response status codes + + JSpec.statusCodes = { + 100: 'Continue', + 101: 'Switching Protocols', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 300: 'Multiple Choice', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported' + } + + /** + * Mock XMLHttpRequest requests. + * + * mockRequest().and_return('some data', 'text/plain', 200, { 'X-SomeHeader' : 'somevalue' }) + * + * @return {hash} + * @api public + */ + + function mockRequest() { + return { and_return : function(body, type, status, headers) { + XMLHttpRequest = MockXMLHttpRequest + status = status || 200 + headers = headers || {} + headers['content-type'] = type + JSpec.extend(XMLHttpRequest.prototype, { + responseText: body, + responseHeaders: headers, + status: status, + statusText: JSpec.statusCodes[status] + }) + }} + } + + /** + * Unmock XMLHttpRequest requests. + * + * @api public + */ + + function unmockRequest() { + XMLHttpRequest = OriginalXMLHttpRequest + } + + JSpec.include({ + name: 'Mock XHR', + + // --- Utilities + + utilities : { + mockRequest: mockRequest, + unmockRequest: unmockRequest + }, + + // --- Hooks + + afterSpec : function() { + unmockRequest() + }, + + // --- DSLs + + DSLs : { + snake : { + mock_request: mockRequest, + unmock_request: unmockRequest + } + } + + }) +})() \ No newline at end of file diff --git a/spec/setting_helpers_spec.js b/spec/setting_helpers_spec.js new file mode 100644 index 0000000..44fd7c4 --- /dev/null +++ b/spec/setting_helpers_spec.js @@ -0,0 +1,61 @@ +describe 'SettingHelpers' + before_each + $('body').append(elements(fixture('settings')).get(0)); + setting_helpers = SettingHelpers; + end + + after_each + $('body #info').remove(); + end + + describe 'updateSettingsCheckboxes' + it 'should add a checked="checked" to checkboxes if setting is enabled' + settings = {'pex-enabled': true, 'dht-enabled': true}; + setting_helpers.updateSettingsCheckboxes(settings); + $('input[type=checkbox]:checked').length.should.eql(2); + end + + it 'should not add a checked="checked" to checkboxes if setting is disabled' + settings = {'pex-enabled': false, 'dht-enabled': true}; + setting_helpers.updateSettingsCheckboxes(settings); + $('input[type=checkbox]:checked').length.should.eql(1); + end + end + + describe 'turtle_mode_hash' + it 'should return a hash with alt-speed-enabled set to true if given parameter is string true' + setting_helpers.turtle_mode_hash("true")['alt-speed-enabled'].should.be_true; + end + + it 'should return a hash with alt-speed-enabled set to false if given parameter is string false' + setting_helpers.turtle_mode_hash("false")['alt-speed-enabled'].should.be_false; + end + end + + describe 'arguments_hash' + before_each + updatable_settings = ['dht-enabled', 'pex-enabled', 'download-dir', 'peer-port']; + params = {'dht-enabled': 'on', 'download-dir': '/downloads', 'peer-port': '5327'}; + end + + it 'should set setting to false if it\'s not in the parameters' + hash = setting_helpers.arguments_hash(params, updatable_settings); + hash['pex-enabled'].should.be_false; + end + + it 'should set setting to true if it\'s "on"' + hash = setting_helpers.arguments_hash(params, updatable_settings); + hash['dht-enabled'].should.be_true; + end + + it 'should set setting to a number if parameter is a string with only a number in it' + hash = setting_helpers.arguments_hash(params, updatable_settings); + hash['peer-port'].should.eql(5327); + end + + it 'should set setting to a string if it is a string' + hash = setting_helpers.arguments_hash(params, updatable_settings); + hash['download-dir'].should.eql('/downloads'); + end + end +end \ No newline at end of file diff --git a/spec/sort_torrents_helpers_spec.js b/spec/sort_torrents_helpers_spec.js new file mode 100644 index 0000000..5980dbc --- /dev/null +++ b/spec/sort_torrents_helpers_spec.js @@ -0,0 +1,103 @@ +describe 'SortTorrentsHelpers' + before_each + sort_helpers = SortTorrentsHelpers; + end + + /*it 'should be sortable by name' + var torrents = [ + Torrent({'id': '1', 'name': 'Zelda'}), + Torrent({'id': '2', 'name': 'Alpha'}), + Torrent({'id': '3', 'name': 'Manfred'}) + ] + var sorted_torrents = sort_helpers.sortTorrents('name', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end*/ + + it 'should be sortable by activity' + var torrents = [ + Torrent({'id': '1', 'rateDownload': 0, 'rateUpload': 0}), + Torrent({'id': '2', 'rateDownload': 512, 'rateUpload': 256}), + Torrent({'id': '3', 'rateDownload': 512, 'rateUpload': 5}) + ] + var sorted_torrents = sort_helpers.sortTorrents('activity', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should be sortable by age' + var torrents = [ + Torrent({'id': '1', 'addedDate': 20100102}), + Torrent({'id': '2', 'addedDate': 20100201}), + Torrent({'id': '3', 'addedDate': 20100115}) + ] + var sorted_torrents = sort_helpers.sortTorrents('age', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should be sortable by progress' + var torrents = [ + Torrent({'id': '1', 'sizeWhenDone': 100, 'leftUntilDone': 0, 'uploadedEver': 0, 'downloadedEver': 100}), + Torrent({'id': '2', 'sizeWhenDone': 100, 'leftUntilDone': 50, 'uploadedEver': 30, 'downloadedEver': 50}), + Torrent({'id': '3', 'sizeWhenDone': 100, 'leftUntilDone': 50, 'uploadedEver': 20, 'downloadedEver': 50}), + Torrent({'id': '4', 'sizeWhenDone': 100, 'leftUntilDone': 100, 'uploadedEver': 0, 'downloadedEver': 0}) + ] + var sorted_torrents = sort_helpers.sortTorrents('progress', torrents) + sorted_torrents[0].id.should.eql('4') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('2') + sorted_torrents[3].id.should.eql('1') + end + + it 'should be sortable by queue' + var torrents = [ + Torrent({'id': '2'}), + Torrent({'id': '1'}), + Torrent({'id': '3'}) + ] + var sorted_torrents = sort_helpers.sortTorrents('queue', torrents) + sorted_torrents[0].id.should.eql('1') + sorted_torrents[1].id.should.eql('2') + sorted_torrents[2].id.should.eql('3') + end + + it 'should be sortable by state' + var torrents = [ + Torrent({'id': '1', 'status': 16}), + Torrent({'id': '2', 'status': 4}), + Torrent({'id': '3', 'status': 8}) + ] + var sorted_torrents = sort_helpers.sortTorrents('state', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should sort reverse if reverse is true' + var torrents = [ + Torrent({'id': '1', 'rateDownload': 0, 'rateUpload': 0}), + Torrent({'id': '2', 'rateDownload': 512, 'rateUpload': 256}), + Torrent({'id': '3', 'rateDownload': 512, 'rateUpload': 5}) + ] + var sorted_torrents = sort_helpers.sortTorrents('activity', torrents, true) + sorted_torrents[0].id.should.eql('1') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('2') + end + + it 'should not sort reverse if reverse if false' + var torrents = [ + Torrent({'id': '1', 'rateDownload': 0, 'rateUpload': 0}), + Torrent({'id': '2', 'rateDownload': 512, 'rateUpload': 256}), + Torrent({'id': '3', 'rateDownload': 512, 'rateUpload': 5}) + ] + var sorted_torrents = sort_helpers.sortTorrents('activity', torrents, false) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end +end diff --git a/spec/sort_torrents_helpers_spec.js~ b/spec/sort_torrents_helpers_spec.js~ new file mode 100644 index 0000000..9408ab6 --- /dev/null +++ b/spec/sort_torrents_helpers_spec.js~ @@ -0,0 +1,103 @@ +describe 'SortTorrentsHelpers' + before_each + sort_helpers = SortTorrentsHelpers; + end + + it 'should be sortable by name' + var torrents = [ + Torrent({'id': '1', 'name': 'Zelda'}), + Torrent({'id': '2', 'name': 'Alpha'}), + Torrent({'id': '3', 'name': 'Manfred'}) + ] + var sorted_torrents = sort_helpers.sortTorrents('name', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should be sortable by activity' + var torrents = [ + Torrent({'id': '1', 'rateDownload': 0, 'rateUpload': 0}), + Torrent({'id': '2', 'rateDownload': 512, 'rateUpload': 256}), + Torrent({'id': '3', 'rateDownload': 512, 'rateUpload': 5}) + ] + var sorted_torrents = sort_helpers.sortTorrents('activity', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should be sortable by age' + var torrents = [ + Torrent({'id': '1', 'addedDate': 20100102}), + Torrent({'id': '2', 'addedDate': 20100201}), + Torrent({'id': '3', 'addedDate': 20100115}) + ] + var sorted_torrents = sort_helpers.sortTorrents('age', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should be sortable by progress' + var torrents = [ + Torrent({'id': '1', 'sizeWhenDone': 100, 'leftUntilDone': 0, 'uploadedEver': 0, 'downloadedEver': 100}), + Torrent({'id': '2', 'sizeWhenDone': 100, 'leftUntilDone': 50, 'uploadedEver': 30, 'downloadedEver': 50}), + Torrent({'id': '3', 'sizeWhenDone': 100, 'leftUntilDone': 50, 'uploadedEver': 20, 'downloadedEver': 50}), + Torrent({'id': '4', 'sizeWhenDone': 100, 'leftUntilDone': 100, 'uploadedEver': 0, 'downloadedEver': 0}) + ] + var sorted_torrents = sort_helpers.sortTorrents('progress', torrents) + sorted_torrents[0].id.should.eql('4') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('2') + sorted_torrents[3].id.should.eql('1') + end + + it 'should be sortable by queue' + var torrents = [ + Torrent({'id': '2'}), + Torrent({'id': '1'}), + Torrent({'id': '3'}) + ] + var sorted_torrents = sort_helpers.sortTorrents('queue', torrents) + sorted_torrents[0].id.should.eql('1') + sorted_torrents[1].id.should.eql('2') + sorted_torrents[2].id.should.eql('3') + end + + it 'should be sortable by state' + var torrents = [ + Torrent({'id': '1', 'status': 16}), + Torrent({'id': '2', 'status': 4}), + Torrent({'id': '3', 'status': 8}) + ] + var sorted_torrents = sort_helpers.sortTorrents('state', torrents) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end + + it 'should sort reverse if reverse is true' + var torrents = [ + Torrent({'id': '1', 'rateDownload': 0, 'rateUpload': 0}), + Torrent({'id': '2', 'rateDownload': 512, 'rateUpload': 256}), + Torrent({'id': '3', 'rateDownload': 512, 'rateUpload': 5}) + ] + var sorted_torrents = sort_helpers.sortTorrents('activity', torrents, true) + sorted_torrents[0].id.should.eql('1') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('2') + end + + it 'should not sort reverse if reverse if false' + var torrents = [ + Torrent({'id': '1', 'rateDownload': 0, 'rateUpload': 0}), + Torrent({'id': '2', 'rateDownload': 512, 'rateUpload': 256}), + Torrent({'id': '3', 'rateDownload': 512, 'rateUpload': 5}) + ] + var sorted_torrents = sort_helpers.sortTorrents('activity', torrents, false) + sorted_torrents[0].id.should.eql('2') + sorted_torrents[1].id.should.eql('3') + sorted_torrents[2].id.should.eql('1') + end +end \ No newline at end of file diff --git a/spec/statistic_helpers_spec.js b/spec/statistic_helpers_spec.js new file mode 100644 index 0000000..32ea735 --- /dev/null +++ b/spec/statistic_helpers_spec.js @@ -0,0 +1,17 @@ +describe 'StatisticHelpers' + before_each + statistic_helpers = StatisticHelpers + end + + describe 'drawGraphs' + it 'should draw a graph of the KB up and download rates' + f = function() {} + transmission = {} + graph = {"set_theme": f, "data": f, "draw": f} + Bluff = {"Pie": function() {return graph;}, "Line": function() {return graph;}} + transmission['store'] = {"get": function() {return [{"up": 10240, "down": 20480}, {"up": 20480, "down": 10240}];}} + statistic_helpers.should.receive('drawLines').with_args('up_and_download_stats', {"Upload": [10, 20], "Download": [20, 10]}) + statistic_helpers.drawGraphs() + end + end +end \ No newline at end of file diff --git a/spec/store_helpers_spec.js b/spec/store_helpers_spec.js new file mode 100644 index 0000000..aafca4c --- /dev/null +++ b/spec/store_helpers_spec.js @@ -0,0 +1,36 @@ +describe 'StoreHelpers' + before_each + store_helpers = StoreHelpers + end + + describe 'addUpAndDownToStore' + before_each + transmission = {} + transmission.store = {"set": function() {}, "get": function() {}} + end + + it 'should store the global up and download' + stub(transmission.store, 'exists').and_return(false) + transmission.store.should.receive("set").with_args('up_and_download_rate', [{"up": 20, "down": 10}]) + store_helpers.addUpAndDownToStore({"up": 20, "down": 10}) + end + + it 'should add to the global up and download if it already exists' + stub(transmission.store, 'exists').and_return(true) + transmission.store.get = function() { return [{"up": 20, "down": 10}]; } + transmission.store.should.receive("set").with_args('up_and_download_rate', [{"up": 20, "down": 10}, {"up": 10, "down": 5}]) + store_helpers.addUpAndDownToStore({"up": 10, "down": 5}) + end + + it 'should remove an item if there are more than 30' + stub(transmission.store, 'exists').and_return(true) + items = [] + for(var i = 0; i < 30; i += 1) { + items.push({"up": 10, "down": 20}) + } + transmission.store.get = function() { return items; } + items.should.receive("shift") + store_helpers.addUpAndDownToStore({"up": 10, "down": 5}) + end + end +end \ No newline at end of file diff --git a/spec/torrent_helpers_spec.js b/spec/torrent_helpers_spec.js new file mode 100644 index 0000000..18dfa44 --- /dev/null +++ b/spec/torrent_helpers_spec.js @@ -0,0 +1,69 @@ +describe 'TorrentHelpers' + before_each + $('body').append(elements(fixture('torrents')).get(0)) + old_torrents = $('.torrent') + torrent_helpers = TorrentHelpers + end + + after_each + $('body').find('#torrents').remove() + end + + describe 'updateTorrents' + before_each + torrent_helpers.templates = {} + torrent_helpers.templates['show'] = fixture('show.mustache') + torrent_helpers.templates['pause_and_activate_button'] = fixture('pause_and_activate_button.mustache') + torrent_helpers.cache = function(partial) { return this.templates[partial]; } + torrent_helpers.cache_partial = function() {} + torrent_helpers.updateInfo = function() {} + torrent_helpers.clearCache = function() {} + torrent_helpers.mustache = function(template, view) {return Mustache.to_html(template, view);} + statusWord = function() {return 'seeding';} + transmission = {'view_mode': 'normal'} + end + + it 'should add a new torrent if it came in with the update and is not on the site yet' + updated_torrents = [ + Torrent({'id': 1, 'status': 8}), + Torrent({'id': 2, 'status': 8}), + Torrent({'id': 3, 'status': 8}), + Torrent({'id': 4, 'status': 8}) + ] + torrent_helpers.updateTorrents(updated_torrents) + $('#4').get(0).should_not.be_undefined + end + + it 'should remove an old torrent that did not come in with the update but is still on the site' + updated_torrents = [Torrent({'id': 2, 'status': 8})] + torrent_helpers.updateTorrents(updated_torrents) + $('#1').get(0).should.be_undefined + end + + it 'should update the torrents\' data' + updated_torrents = [ + Torrent({'id': 2, 'status': 8, 'rateUpload': 20000, 'rateDownload': 0}) + ] + torrent_helpers.updateTorrents(updated_torrents) + $('.statusString:first').html().should.match(/20\.0 KB\/s/) + end + + it 'should remove the meta status if downloading started' + updated_torrents = [ + Torrent({'id': 3, 'status': 4, 'metadataPercentComplete': 1}) + ] + torrent_helpers.updateTorrents(updated_torrents) + $('#3').find('.progressDetails').html().should_not.match(/metadata/) + $('#3').find('.progressbar').find('.ui-widget-header-meta').get(0).should.be_undefined + end + end + + describe 'formatNextAnnounceTime' + it 'should return a formatted time for the given nextAnnounceTime' + in_fifteen_minutes = new Date().getTime() + 900000 + timestamp = (new Date(in_fifteen_minutes).getTime()/1000).toFixed(0) + torrent_helpers.formatNextAnnounceTime(timestamp).should.eql("15 min, 0 sec") + end + end + +end \ No newline at end of file diff --git a/spec/torrent_spec.js b/spec/torrent_spec.js new file mode 100644 index 0000000..c58bf06 --- /dev/null +++ b/spec/torrent_spec.js @@ -0,0 +1,134 @@ +describe 'Torrent' + describe 'isActive' + it 'should be true when the torrent is seeding' + var seeding = Torrent({}).stati['seeding']; + Torrent({status: seeding}).isActive().should.be_true; + end + + it 'should be true when the torrent is downloading' + var downloading = Torrent({}).stati['downloading']; + Torrent({status: downloading}).isActive().should.be_true; + end + + it 'should be false when the torrent is waiting for check' + var waiting_for_check = Torrent({}).stati['waiting_to_check']; + Torrent({status: waiting_for_check}).isActive().should.be_false; + end + + it 'should be false when the torrent is checking' + var checking = Torrent({}).stati['checking']; + Torrent({status: checking}).isActive().should.be_false; + end + + it 'should be false when the torrent is paused' + var paused = Torrent({}).stati['paused']; + Torrent({status: paused}).isActive().should.be_false; + end + end + + describe 'isDoneDownloading' + it 'should be true when the status is seeding even if leftUntilDone is great than 0' + var seeding = Torrent({}).stati['seeding']; + Torrent({status: seeding, leftUntilDone: 1000}).isDoneDownloading().should.be_true; + end + + it 'should be true when leftUntilDone is 0' + var paused = Torrent({}).stati['paused']; + Torrent({status: paused, leftUntilDone: 0}).isDoneDownloading().should.be_true; + end + + it 'should be true when leftUntilDone is 0' + var paused = Torrent({}).stati['paused']; + Torrent({status: paused, leftUntilDone: 1000}).isDoneDownloading().should.be_false; + end + end + + describe 'percentDone' + it 'should return 0 when sizeWhenDone is null' + Torrent({sizeWhenDone: null}).percentDone().should.eql('0'); + end + + it 'should return 0 when leftUntilDone is null' + Torrent({leftUntilDone: null}).percentDone().should.eql('0'); + end + + it 'should truncate to 2 decimals' + Torrent({sizeWhenDone: 100000, leftUntilDone: 50666}).percentDone().should.eql('49.33'); + end + + it 'should always round done so that 100% isn\'t premature' + Torrent({sizeWhenDone: 100000, leftUntilDone: 1}).percentDone().should.eql('99.99'); + end + + it 'should return 100 when all is downloaded' + Torrent({sizeWhenDone: 100000, leftUntilDone: 0}).percentDone().should.eql('100.00'); + end + end + + describe 'etaString' + it 'should be unknown when less than 0' + Torrent({eta: -1}).etaString().should.eql('remaining time unknown'); + end + + it 'should format the time correctly' + Torrent({eta: 3660}).etaString().should.eql('1 hr 1 min remaining'); + end + end + + describe 'downloadingProgress' + it 'should create a human readable string from sizeWhenDone and leftUntilDone' + var torrent = Torrent({sizeWhenDone: 100000, leftUntilDone: 50666}); + torrent.downloadingProgress().should.eql('48.2 KB of 97.7 KB (49.33%)'); + end + end + + describe 'uploadingProgress' + it 'should create a human readable string from sizeWhenDone, uploadRatio and uploadedEver' + var torrent = Torrent({sizeWhenDone: 100000, uploadedEver: 50666, uploadRatio: 0.52}); + torrent.uploadingProgress().should.eql('97.7 KB, uploaded 49.5 KB (Ratio: 0.52)'); + end + end + + describe 'statusString' + it 'should contain a human readable status' + Torrent({status: Torrent({}).stati['checking']}).statusString().should.match(/Verifying local data/); + end + + it 'should contain the up and download speed' + var torrent = Torrent({status: Torrent({}).stati['downloading'], rateUpload: 10000, rateDownload: 10000}); + torrent.statusString().should.match(/DL: 10.0 KB\/s, UL: 10.0 KB\/s/); + end + + it 'should not contain up and download speed when torrent is not active' + var torrent = Torrent({status: Torrent({}).stati['paused'], rateUpload: 10000, rateDownload: 10000}); + torrent.statusString().should_not.match(/DL: 10.0 KB\/s, UL: 10.0 KB\/s/); + end + end + + describe 'progressBar' + it 'should add class downloading if it\'s a downloading torrent' + var torrent = Torrent({status: Torrent({}).stati['downloading'], metadataPercentComplete: 1}); + torrent.progressBar().should.match(/downloading/); + end + + it 'should add class uploading if it\'s a seeding torrent' + var torrent = Torrent({status: Torrent({}).stati['seeding'], metadataPercentComplete: 1}); + torrent.progressBar().should.match(/uploading/); + end + + it 'should add class paused if it\'s a paused torrent' + var torrent = Torrent({status: Torrent({}).stati['paused'], metadataPercentComplete: 1}); + torrent.progressBar().should.match(/paused/); + end + + it 'should add class meta if it\'s a torrent retrieving meta data' + var torrent = Torrent({status: Torrent({}).stati['downloading'], metadataPercentComplete: 0}); + torrent.progressBar().should.match(/meta/); + end + + it 'should fill the whole progressbar if it\'s retrieving meta data' + var torrent = Torrent({status: Torrent({}).stati['downloading'], metadataPercentComplete: 0}); + torrent.progressBar().should.match(/100%/); + end + end +end \ No newline at end of file diff --git a/spec/torrent_view_spec.js b/spec/torrent_view_spec.js new file mode 100644 index 0000000..0b5c313 --- /dev/null +++ b/spec/torrent_view_spec.js @@ -0,0 +1,122 @@ +describe 'TorrentView' + before_each + context = {} + context.formatNextAnnounceTime = function() {} + torrent_view = TorrentView({'trackerStats': [], 'files': [], 'peers': []}, context) + timestamp = "1265737984" + hours = 17 - (new Date).getTimezoneOffset()/60 + if(hours > 23) { hours -= 24; } + day = (new Date).getTimezoneOffset()/60 < -6 ? 10 : 9 + end + + describe 'addFormattedTimes' + it 'should add a formatted time for lastAnnounceTime' + torrent_view.trackerStats[0] = {} + torrent_view.trackerStats[0]['lastAnnounceTime'] = timestamp + torrent_view.addFormattedTimes() + torrent_view.trackerStats[0].lastAnnounceTimeFormatted.should.eql("2/" + day + "/2010 " + hours + ":53") + end + + it 'should add a formatted time for lastScrapeTime' + torrent_view.trackerStats[0] = {} + torrent_view.trackerStats[0]['lastScrapeTime'] = timestamp + torrent_view.addFormattedTimes() + torrent_view.trackerStats[0].lastScrapeTimeFormatted.should.eql("2/" + day + "/2010 " + hours + ":53") + end + end + + describe 'addFormattedSizes' + describe 'files' + it 'should add a formatted size for length' + torrent_view.files[0] = {} + torrent_view.files[0]['length'] = 2048 + torrent_view.addFormattedSizes() + torrent_view.files[0].lengthFormatted.should.eql('2.0 KB') + end + + it 'should add a percent done value' + torrent_view.files[0] = {'length': 2048, 'bytesCompleted': 512} + torrent_view.addFormattedSizes() + torrent_view.files[0].percentDone.should.eql('25') + end + end + + describe 'peers' + it 'should add a percent done value' + torrent_view.peers[0] = {'progress': 0.7} + torrent_view.addFormattedSizes() + torrent_view.peers[0].percentDone.should.eql('70') + end + + it 'should add a formatted upload value' + torrent_view.peers[0] = {'rateToPeer': 20} + torrent_view.addFormattedSizes() + torrent_view.peers[0].uploadFormatted.should.eql('20 bytes') + end + + it 'should add an empty string if upload value is 0' + torrent_view.peers[0] = {'rateToPeer': 0} + torrent_view.addFormattedSizes() + torrent_view.peers[0].uploadFormatted.should.eql('') + end + + it 'should add a formatted download value' + torrent_view.peers[0] = {'rateToClient': 20} + torrent_view.addFormattedSizes() + torrent_view.peers[0].downloadFormatted.should.eql('20 bytes') + end + end + end + + describe 'sort peers' + before_each + torrent_view.peers = [ + {'ip': '1.2.3.4', 'clientName': 'Transmission', 'percentDone': 10, 'rateToPeer': 10, 'rateToClient': 50}, + {'ip': '2.2.3.4', 'clientName': 'Beluge', 'percentDone': 30, 'rateToPeer': 50, 'rateToClient': 40}, + {'ip': '4.2.3.4', 'clientName': 'Vuze', 'percentDone': 20, 'rateToPeer': 40, 'rateToClient': 30}, + {'ip': '3.2.3.4', 'clientName': 'rtorrent', 'percentDone': 40, 'rateToPeer': 30, 'rateToClient': 20}, + {'ip': '5.2.3.4', 'clientName': 'BitComet', 'percentDone': 50, 'rateToPeer': 20, 'rateToClient': 10} + ] + end + + it 'should sort by client' + torrent_view.sort_peers = 'client' + torrent_view.sortPeers() + torrent_view.peers[0].clientName.should.eql('Beluge') + torrent_view.peers[1].clientName.should.eql('BitComet') + torrent_view.peers[2].clientName.should.eql('rtorrent') + torrent_view.peers[3].clientName.should.eql('Transmission') + torrent_view.peers[4].clientName.should.eql('Vuze') + end + + it 'should sort by percent' + torrent_view.sort_peers = 'percent' + torrent_view.sortPeers() + torrent_view.peers[0].clientName.should.eql('BitComet') + torrent_view.peers[1].clientName.should.eql('rtorrent') + torrent_view.peers[2].clientName.should.eql('Beluge') + torrent_view.peers[3].clientName.should.eql('Vuze') + torrent_view.peers[4].clientName.should.eql('Transmission') + end + + it 'should sort by upload' + torrent_view.sort_peers = 'upload' + torrent_view.sortPeers() + torrent_view.peers[0].clientName.should.eql('Beluge') + torrent_view.peers[1].clientName.should.eql('Vuze') + torrent_view.peers[2].clientName.should.eql('rtorrent') + torrent_view.peers[3].clientName.should.eql('BitComet') + torrent_view.peers[4].clientName.should.eql('Transmission') + end + + it 'should sort by download' + torrent_view.sort_peers = 'download' + torrent_view.sortPeers() + torrent_view.peers[0].clientName.should.eql('Transmission') + torrent_view.peers[1].clientName.should.eql('Beluge') + torrent_view.peers[2].clientName.should.eql('Vuze') + torrent_view.peers[3].clientName.should.eql('rtorrent') + torrent_view.peers[4].clientName.should.eql('BitComet') + end + end +end \ No newline at end of file diff --git a/spec/torrents_view_spec.js b/spec/torrents_view_spec.js new file mode 100644 index 0000000..ec6852e --- /dev/null +++ b/spec/torrents_view_spec.js @@ -0,0 +1,33 @@ +describe 'TorrentsView' + describe 'pauseAndActivateButton' + before_each + forms = elements(fixture('torrents_view_forms')).find('form') + stop_form = $(forms.get(0)) + start_form = $(forms.get(1)) + context = {} + context.cache_partial = function() {} + context.cache = function() { return fixture('pause_and_activate_button.mustache'); } + context.mustache = function(template, view) {return Mustache.to_html(template, view);} + torrents_view = TorrentsView({}, context) + end + + it 'should return a form to pause the torrent if the torrent is active' + torrents_view.id = 567; + torrents_view.status = Torrent({}).stati['downloading']; + torrents_view.pauseAndActivateButton().should.match(new RegExp(stop_form)); + end + + it 'should return a form to start the torrent if the torrent is paused and not done downloading' + torrents_view.id = 567; + torrents_view.status = Torrent({}).stati['paused']; + torrents_view.pauseAndActivateButton().should.match(new RegExp(start_form)); + end + + it 'should return a form to start the torrent if the torrent is paused and done downloading' + torrents_view.id = 567; + torrents_view.status = Torrent({}).stati['paused']; + torrents_view.leftUntilDone = 0; + torrents_view.pauseAndActivateButton().should.match(new RegExp(start_form)); + end + end +end \ No newline at end of file diff --git a/spec/validator_spec.js b/spec/validator_spec.js new file mode 100644 index 0000000..df4e186 --- /dev/null +++ b/spec/validator_spec.js @@ -0,0 +1,96 @@ +describe 'Validator' + before_each + validator = new Validator() + end + + it 'should have no errors on a valid object' + torrent = {'name': 'coffee', 'totalSize': 160, 'status': 8, 'rateUpload': 20} + validator.schema = { + 'presence_of': ['name', 'status'], + 'numericality_of': ['totalSize', {'field': 'rateUpload', 'max': 20}], + 'inclusion_of': {'field': 'status', 'in': [1, 2, 4, 8, 16]} + } + validator.validate(torrent) + validator.errors.should.be_empty + end + + it 'should validate the presence of a single field' + torrent = {} + validator.schema = { + 'presence_of': 'name' + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('name') + validator.errors[0]['message'].should.eql('should not be empty') + end + + it 'should validate the presence of multiple fields' + torrent = {} + validator.schema = { + 'presence_of': ['name', 'status'] + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('name') + validator.errors[1]['field'].should.eql('status') + end + + it 'should validate the numericality of a field' + torrent = {'totalSize': 'abc'} + validator.schema = { + 'numericality_of': 'totalSize' + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('totalSize') + validator.errors[0]['message'].should.eql('is not a valid number') + end + + it 'should validate the max value of a numeric field' + torrent = {'totalSize': 100} + validator.schema = { + 'numericality_of': {'field': 'totalSize', 'max': 99} + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('totalSize') + validator.errors[0]['message'].should.eql('is not a valid number') + end + + it 'should validate the numericality of multiple fields' + torrent = {'totalSize': 'abc', 'status': 'def'} + validator.schema = { + 'numericality_of': ['totalSize', 'status'] + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('totalSize') + validator.errors[1]['field'].should.eql('status') + end + + it 'should validate the numericality of a missing field and throw an error' + torrent = {} + validator.schema = { + 'numericality_of': 'totalSize' + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('totalSize') + validator.errors[0]['message'].should.eql('is not a valid number') + end + + it 'should validate the numericality of null and throw an error' + torrent = {'totalSize': null} + validator.schema = { + 'numericality_of': 'totalSize' + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('totalSize') + validator.errors[0]['message'].should.eql('is not a valid number') + end + + it 'should validate the inclusion of a field' + torrent = {'encryption': 'strong'} + validator.schema = { + 'inclusion_of': {'field': 'encryption', 'in': ['required', 'preferred', 'tolerated']} + } + validator.validate(torrent) + validator.errors[0]['field'].should.eql('encryption') + validator.errors[0]['message'].should.eql('is not in the list of a valid values') + end +end \ No newline at end of file diff --git a/templates/settings/index.mustache b/templates/settings/index.mustache new file mode 100644 index 0000000..9742b01 --- /dev/null +++ b/templates/settings/index.mustache @@ -0,0 +1,114 @@ + + +

    Preferences

    + +
    + +
    +
    + + + + + + + + + + + + + + + + + +
    Download Directory:
    Encryption: + +
    Enable Protocol Handler (magnet:): +
    + This is a feature not all browsers may support yet. + Once enabled, you can only disable this in your browser settings. +
    Enable Content Handler (.torrent): +
    + This is a feature not all browsers may support yet. + Once enabled, you can only disable this in your browser settings. +
    +
    +
    + + + + + + + + + +
    Incoming TCP Port: + +
    +
    Refresh Web Client every: + seconds +
    +
    +
    +

    Normal mode

    + + + + + + + + + +
    Speed Limit Download: + + KB/s +
    Speed Limit Upload: + + KB/s +
    +

    Turtle mode

    + + + + + + + + + +
    Speed Limit Download: + KB/s +
    Speed Limit Upload: + KB/s +
    +
    +
    + + + + + + + + + +
    DHT enabled:
    PEX enabled:
    +
    +
    + +
    Version: {{version}}
    \ No newline at end of file diff --git a/templates/statistics/index.mustache b/templates/statistics/index.mustache new file mode 100644 index 0000000..61743e6 --- /dev/null +++ b/templates/statistics/index.mustache @@ -0,0 +1,35 @@ + + +

    Current Statistics

    + +
    + + + + + + + + + + + + + + + + + +
    Number of Torrents:{{number_of_torrents}}
    Uploaded:{{uploaded}}
    Downloaded:{{downloaded}}
    Time active:{{time_active}}
    +
    + + +
    + + +
    \ No newline at end of file diff --git a/templates/torrents/delete_data.mustache b/templates/torrents/delete_data.mustache new file mode 100644 index 0000000..88a893e --- /dev/null +++ b/templates/torrents/delete_data.mustache @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/torrents/new.mustache b/templates/torrents/new.mustache new file mode 100644 index 0000000..56fd92c --- /dev/null +++ b/templates/torrents/new.mustache @@ -0,0 +1,24 @@ +

    Add a Torrent

    + +

    Please upload the torrent or provide a URL where the torrent can be found.

    + +
    +

    + + +

    + + or + +

    + + +

    + +

    + + +

    + +

    +
    \ No newline at end of file diff --git a/templates/torrents/pause_and_activate_button.mustache b/templates/torrents/pause_and_activate_button.mustache new file mode 100644 index 0000000..ae7b094 --- /dev/null +++ b/templates/torrents/pause_and_activate_button.mustache @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/templates/torrents/show.mustache b/templates/torrents/show.mustache new file mode 100644 index 0000000..755cac7 --- /dev/null +++ b/templates/torrents/show.mustache @@ -0,0 +1,16 @@ +
  • +

    {{name}}

    +

    {{progressDetails}}

    + +
    +
    + {{{progressBar}}} +
    +
    {{{pauseAndActivateButton}}}
    +
    + +
    +
    + +
    {{statusString}}
    +
  • \ No newline at end of file diff --git a/templates/torrents/show_compact.mustache b/templates/torrents/show_compact.mustache new file mode 100644 index 0000000..9636b0d --- /dev/null +++ b/templates/torrents/show_compact.mustache @@ -0,0 +1,14 @@ +
  • +
    + {{{progressBar}}} +
    +
    {{name}}
    + +
    {{statusString}}
    +
    +
    {{{pauseAndActivateButton}}}
    +
    + +
    +
    +
  • \ No newline at end of file diff --git a/templates/torrents/show_info.mustache b/templates/torrents/show_info.mustache new file mode 100644 index 0000000..14fae08 --- /dev/null +++ b/templates/torrents/show_info.mustache @@ -0,0 +1,160 @@ + +

    {{name}}

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Size:{{totalSizeString}}
    Pieces:{{pieceCount}}, {{pieceSizeString}}
    Hash:{{hashString}}
    Secure:{{secure}}
    Comment:{{comment}}
    Creator:{{creator}}
    Date:{{created_at}}
    Download Directory:{{downloadDir}}
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    State:{{statusWord}}
    Progress:{{percentDone}}%
    Have:{{haveString}}
    Downloaded:{{downloadedEverString}}
    Uploaded:{{uploadedEverString}}
    Ratio:{{uploadRatio}}
    Error:{{errorString}}
    Download speed:{{rateDownload}}
    Upload speed:{{rateUpload}}
    Peers - Upload to:{{peersGettingFromUs}}
    Peers - Download from:{{peersSendingToUs}}
    +
    + +
    + {{#trackerStats}} +

    {{host}}


    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Last Announce:{{lastAnnounceTimeFormatted}}
    Next Announce:{{nextAnnounceTimeFormatted}}
    Last Scrape:{{lastScrapeTimeFormatted}}
    Seeders:{{seederCount}}
    Leechers:{{leecherCount}}
    Downloaded:{{downloadCount}}
    + {{/trackerStats}} +
    + +
    + + + + + + + + + {{#peers}} + + + + + + + + {{/peers}} +
    IP AddressClient%UploadDownload
    {{address}}{{clientName}}{{percentDone}}{{uploadFormatted}}{{downloadFormatted}}
    +
    + +
    +
      + {{#files}} +
    • + {{name}}
      + {{percentDone}}% of {{lengthFormatted}} +
    • + {{/files}} +
    +
    \ No newline at end of file diff --git a/vendor/.DS_Store b/vendor/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e5bb0dea5954103ca468e7fae3c9688949b3b951 GIT binary patch literal 6148 zcmeH~J&pn~427TBSqW(yO3E}GfEz@JJpmV34Tx$a3Phiy@3Z5ES!y&|Jxk7uop^q} zVloC`hsR|PYyhn3uGsl7Gh_UT5fdJ`;(EECr}1{1JdIWE^?=T6Jg?`nLi=gY z3PnH!{uu!qj;G^+FO_HOpV#yJPiB4I=+xNG;pr!UfgiPpCDub!CR8AA!K2 JK?L4P;1`z)6MFyv literal 0 HcmV?d00001 diff --git a/vendor/bluff/bluff-min.js b/vendor/bluff/bluff-min.js new file mode 100644 index 0000000..3a80df0 --- /dev/null +++ b/vendor/bluff/bluff-min.js @@ -0,0 +1 @@ +Bluff={VERSION:'0.3.6',array:function(c){if(c.length===undefined)return[c];var d=[],f=c.length;while(f--)d[f]=c[f];return d},array_new:function(c,d){var f=[];while(c--)f.push(d);return f},each:function(c,d,f){for(var g=0,h=c.length;gthis._5)?g.length:this._5;Bluff.each(g,function(c,d){if(c===undefined)return;if(this.maximum_value===null&&this.minimum_value===null)this.maximum_value=this.minimum_value=c;this.maximum_value=this._1f(c)?c:this.maximum_value;if(this.maximum_value>=0)this._a=true;this.minimum_value=this._1A(c)?c:this.minimum_value;if(this.minimum_value<0)this._a=true},this)},draw:function(){if(this.stacked)this._1B();this._1C();this._u(function(){this._0.rectangle(this.left_margin,this.top_margin,this._d-this.right_margin,this._L-this.bottom_margin);this._0.rectangle(this._1,this._7,this._l,this._g)})},clear:function(){this._X()},_1C:function(){if(!this._a)return this._1D();this._13();this._1E();if(this.sort)this._1F();this._1G();this._M();this._1H();this._1I()},_13:function(g){if(this._9===null||g===true){this._9=[];if(!this._a)return;this._1g();Bluff.each(this._2,function(d){var f=[];Bluff.each(d[this.klass.DATA_VALUES_INDEX],function(c){if(c===null||c===undefined)f.push(null);else f.push((c-this.minimum_value)/this._i)},this);this._9.push([d[this.klass.DATA_LABEL_INDEX],f,d[this.klass.DATA_COLOR_INDEX]])},this)}},_1g:function(){this._i=this.maximum_value-this.minimum_value;this._i=this._i>0?this._i:1;this._1h=100/Math.pow(10,Math.round(Math.LOG10E*Math.log(this._i)))},_1E:function(){this._N=this.hide_line_markers?0:this._D(this.marker_font_size);this._1i=this.hide_title?0:this._D(this.title_font_size);this._1j=this.hide_legend?0:this._D(this.legend_font_size);var c,d,f,g,h,i,j;if(this.hide_line_markers){this._1=this.left_margin;this._14=this.right_margin;this._1k=this.bottom_margin}else{d=0;if(this.has_left_labels){c='';for(j in this.labels){c=c.length>this.labels[j].length?c:this.labels[j]}d=this._O(this.marker_font_size,c)*1.25}else{d=this._O(this.marker_font_size,this._15(this.maximum_value))}f=this.hide_line_numbers&&!this.has_left_labels?0.0:d+this.klass.LABEL_MARGIN*2;this._1=this.left_margin+f+(this.y_axis_label===null?0.0:this._N+this.klass.LABEL_MARGIN*2);g=-Infinity;for(j in this.labels)g=g>Number(j)?g:Number(j);g=Math.round(g);h=(g>=(this._5-1)&&this.center_labels_over_point)?this._O(this.marker_font_size,this.labels[g])/2:0;this._14=this.right_margin+h;this._1k=this.bottom_margin+this._N+this.klass.LABEL_MARGIN}this._l=this._d-this._14;this._6=this._d-this._1-this._14;this._7=this.top_margin+(this.hide_title?this.title_margin:this._1i+this.title_margin)+(this.hide_legend?this.legend_margin:this._1j+this.legend_margin);i=(this.x_axis_label===null)?0.0:this._N+this.klass.LABEL_MARGIN;this._g=this._L-this._1k-i;this._3=this._g-this._7},_1H:function(){if(this.x_axis_label){var c=this._g+this.klass.LABEL_MARGIN*2+this._N;this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='north';this._0.annotate_scaled(this._d,1.0,0.0,c,this.x_axis_label,this._b);this._u(function(){this._0.line(0.0,c,this._d,c)})}},_M:function(){if(this.hide_line_markers)return;if(this.y_axis_increment===null){if(this.marker_count===null){Bluff.each([3,4,5,6,7],function(c){if(!this.marker_count&&this._i%c===0)this.marker_count=c},this);this.marker_count=this.marker_count||4}this._16=(this._i>0)?this._17(this._i/this.marker_count):1}else{this.maximum_value=Math.max(Math.ceil(this.maximum_value),this.y_axis_increment);this.minimum_value=Math.floor(this.minimum_value);this._1g();this._13(true);this.marker_count=Math.round(this._i/this.y_axis_increment);this._16=this.y_axis_increment}this._1J=this._3/(this._i/this._16);var d,f,g,h;for(d=0,f=this.marker_count;d<=f;d++){g=this._7+this._3-d*this._1J;this._0.stroke=this.marker_color;this._0.stroke_width=1;this._0.line(this._1,g,this._l,g);h=d*this._16+this.minimum_value;if(!this.hide_line_numbers){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.font_weight='normal';this._0.stroke='transparent';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='east';this._0.annotate_scaled(this._1-this.klass.LABEL_MARGIN,1.0,0.0,g,this._15(h),this._b)}}},_1l:function(c){return(this._d-c)/2},_1G:function(){if(this.hide_legend)return;this._P=Bluff.map(this._2,function(c){return c[this.klass.DATA_LABEL_INDEX]},this);var i=this.legend_box_size;if(this.font)this._0.font=this.font;this._0.pointsize=this.legend_font_size;var j=[[]];Bluff.each(this._P,function(c){var d=j.length-1;var f=this._0.get_type_metrics(c);var g=f.width+i*2.7;j[d].push(g);if(Bluff.sum(j[d])>(this._d*0.9))j.push([j[d].pop()])},this);var k=this._1l(Bluff.sum(j[0]));var l=this.hide_title?this.top_margin+this.title_margin:this.top_margin+this.title_margin+this._1i;this._u(function(){this._0.stroke_width=1;this._0.line(0,l,this._d,l)});Bluff.each(this._P,function(c,d){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.pointsize=this._e(this.legend_font_size);this._0.stroke='transparent';this._0.font_weight='normal';this._0.gravity='west';this._0.annotate_scaled(this._d,1.0,k+(i*1.7),l,c,this._b);this._0.stroke='transparent';this._0.fill=this._2[d][this.klass.DATA_COLOR_INDEX];this._0.rectangle(k,l-i/2.0,k+i,l+i/2.0);this._0.pointsize=this.legend_font_size;var f=this._0.get_type_metrics(c);var g=f.width+(i*2.7),h;j[0].shift();if(j[0].length==0){this._u(function(){this._0.line(0.0,l,this._d,l)});j.shift();if(j.length>0)k=this._1l(Bluff.sum(j[0]));h=Math.max(this._1j,i)+this.legend_margin;if(j.length>0){l+=h;this._7+=h;this._3=this._g-this._7}}else{k+=g}},this);this._m=0},_1I:function(){if(this.hide_title||!this.title)return;this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.pointsize=this._e(this.title_font_size);this._0.font_weight='bold';this._0.gravity='north';this._0.annotate_scaled(this._d,1.0,0,this.top_margin,this.title,this._b)},_c:function(c,d){if(this.hide_line_markers)return;var f;if(this.labels[d]&&!this._q[d]){f=this._g+this.klass.LABEL_MARGIN;this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.font_weight='normal';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='north';this._0.annotate_scaled(1.0,1.0,c,f,this.labels[d],this._b);this._q[d]=true;this._u(function(){this._0.stroke_width=1;this._0.line(0.0,f,this._d,f)})}},_E:function(c,d,f,g,h,i,j){if(!this.tooltips)return;this._0.tooltip(c,d,f,g,h,i,j)},_1D:function(){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.font_weight='normal';this._0.pointsize=this._e(80);this._0.gravity='center';this._0.annotate_scaled(this._d,this._L/2,0,10,this.no_data_message,this._b)},_X:function(){var c=this._k.background_colors;switch(true){case c instanceof Array:this._1K.apply(this,c);break;case typeof c==='string':this._1L(c);break;default:this._1M(this._k.background_image);break}},_1L:function(c){this._0.render_solid_background(this._j,this._y,c)},_1K:function(c,d){this._0.render_gradiated_background(this._j,this._y,c,d)},_1M:function(c){},_1e:function(){this._m=0;this._q={};this._k={};this._0.scale(this._b,this._b)},_2a:function(c){return this._b*c},_e:function(c){var d=c*this._b;return d},_Q:function(c,d){return(c>d)?d:c},_1f:function(c,d){return c>this.maximum_value},_1A:function(c,d){return c100){c/=10;d*=10}return Math.floor(c)*d},_1F:function(){var f=this._1N,g=this.klass.DATA_VALUES_INDEX;this._9.sort(function(c,d){return f(d[g])-f(c[g])});this._2.sort(function(c,d){return f(d[g])-f(c[g])})},_1N:function(d){var f=0;Bluff.each(d,function(c){f+=(c||0)});return f},_1B:function(){var g=[],h=this._5;while(h--)g[h]=0;Bluff.each(this._2,function(f){Bluff.each(f[this.klass.DATA_VALUES_INDEX],function(c,d){g[d]+=c},this);f[this.klass.DATA_VALUES_INDEX]=Bluff.array(g)},this)},_u:function(c){if(this.klass.DEBUG){this._0.fill='transparent';this._0.stroke='turquoise';c.call(this)}},_1z:function(){if(this._m0&&k>0){i.push(f);i.push(g)}else{i.push(this._1);i.push(this._g-1);i.push(f);i.push(g)}this._c(f,d);j=f;k=g},this);i.push(this._l);i.push(this._g-1);i.push(this._1);i.push(this._g-1);this._0.fill=h[this.klass.DATA_COLOR_INDEX];this._0.polyline(i)},this)}});Bluff.BarConversion=new JS.Class({mode:null,zero:null,graph_top:null,graph_height:null,minimum_value:null,spread:null,getLeftYRightYscaled:function(c,d){var f;switch(this.mode){case 1:d[0]=this.graph_top+this.graph_height*(1-c)+1;d[1]=this.graph_top+this.graph_height-1;break;case 2:d[0]=this.graph_top+1;d[1]=this.graph_top+this.graph_height*(1-c)-1;break;case 3:f=c-this.minimum_value/this.spread;if(c>=this.zero){d[0]=this.graph_top+this.graph_height*(1-(f-this.zero))+1;d[1]=this.graph_top+this.graph_height*(1-this.zero)-1}else{d[0]=this.graph_top+this.graph_height*(1-(f-this.zero))+1;d[1]=this.graph_top+this.graph_height*(1-this.zero)-1}break;default:d[0]=0.0;d[1]=0.0}}});Bluff.Bar=new JS.Class(Bluff.Base,{bar_spacing:0.9,draw:function(){this.center_labels_over_point=(Bluff.keys(this.labels).length>this._5);this.callSuper();if(!this._a)return;this._1O()},_1O:function(){this._8=this._6/(this._5*this._2.length);var n=(this._8*(1-this.bar_spacing))/2;this._0.stroke_opacity=0.0;var m=new Bluff.BarConversion();m.graph_height=this._3;m.graph_top=this._7;if(this.minimum_value>=0){m.mode=1}else{if(this.maximum_value<=0){m.mode=2}else{m.mode=3;m.spread=this._i;m.minimum_value=this.minimum_value;m.zero=-this.minimum_value/this._i}}Bluff.each(this._9,function(j,k){var l=this._2[k][this.klass.DATA_VALUES_INDEX];Bluff.each(j[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(this._8*(k+d+((this._2.length-1)*d)))+n;var g=f+this._8*this.bar_spacing;var h=[];m.getLeftYRightYscaled(c,h);this._0.fill=j[this.klass.DATA_COLOR_INDEX];this._0.rectangle(f,h[0],g,h[1]);this._E(f,h[0],g-f,h[1]-h[0],j[this.klass.DATA_LABEL_INDEX],j[this.klass.DATA_COLOR_INDEX],l[d]);var i=this._1+(this._2.length*this._8*d)+(this._2.length*this._8/2.0);this._c(i-(this.center_labels_over_point?this._8/2.0:0.0),d)},this)},this);if(this.center_labels_over_point)this._c(this._l,this._5)}});Bluff.Line=new JS.Class(Bluff.Base,{baseline_value:null,baseline_color:null,line_width:null,dot_radius:null,hide_dots:null,hide_lines:null,initialize:function(c){if(arguments.length>3)throw'Wrong number of arguments';if(arguments.length===1||(typeof arguments[1]!=='number'&&typeof arguments[1]!=='string'))this.callSuper(c,null);else this.callSuper();this.hide_dots=this.hide_lines=false;this.baseline_color='red';this.baseline_value=null},draw:function(){this.callSuper();if(!this._a)return;this.x_increment=(this._5>1)?(this._6/(this._5-1)):this._6;var m;if(this._S!==undefined){m=this._7+(this._3-this._S*this._3);this._0.push();this._0.stroke=this.baseline_color;this._0.fill_opacity=0.0;this._0.stroke_width=3.0;this._0.line(this._1,m,this._1+this._6,m);this._0.pop()}Bluff.each(this._9,function(i,j){var k=null,l=null;var n=this._2[j][this.klass.DATA_VALUES_INDEX];this._1P=this._1Q(i);Bluff.each(i[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(this.x_increment*d);if(typeof c!=='number')return;this._c(f,d);var g=this._7+(this._3-c*this._3);this._0.stroke=i[this.klass.DATA_COLOR_INDEX];this._0.fill=i[this.klass.DATA_COLOR_INDEX];this._0.stroke_opacity=1.0;this._0.stroke_width=this.line_width||this._Q(this._j/(this._9[0][this.klass.DATA_VALUES_INDEX].length*6),3.0);var h=this.dot_radius||this._Q(this._j/(this._9[0][this.klass.DATA_VALUES_INDEX].length*2),7.0);if(!this.hide_lines&&k!==null&&l!==null){this._0.line(k,l,f,g)}else if(this._1P){this._0.circle(f,g,f-h,g)}if(!this.hide_dots)this._0.circle(f,g,f-h,g);this._E(f-h,g-h,2*h,2*h,i[this.klass.DATA_LABEL_INDEX],i[this.klass.DATA_COLOR_INDEX],n[d]);k=f;l=g},this)},this)},_13:function(){this.maximum_value=Math.max(this.maximum_value,this.baseline_value);this.callSuper();if(this.baseline_value!==null)this._S=this.baseline_value/this.maximum_value},_1Q:function(d){var f=0;Bluff.each(d[this.klass.DATA_VALUES_INDEX],function(c){if(c!==undefined)f+=1});return f===1}});Bluff.Dot=new JS.Class(Bluff.Base,{draw:function(){this.has_left_labels=true;this.callSuper();if(!this._a)return;var k=1.0;this._F=this._3/this._5;this._18=this._F*k/this._9.length;this._0.stroke_opacity=0.0;var l=Bluff.array_new(this._5,0),n=Bluff.array_new(this._5,this._1),m=(this._F*(1-k))/2;Bluff.each(this._9,function(i,j){Bluff.each(i[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(c*this._6)-Math.round(this._18/6.0);var g=this._7+(this._F*d)+m+Math.round(this._18/2.0);if(j===0){this._0.stroke=this.marker_color;this._0.stroke_width=1.0;this._0.opacity=0.1;this._0.line(this._1,g,this._1+this._6,g)}this._0.fill=i[this.klass.DATA_COLOR_INDEX];this._0.stroke='transparent';this._0.circle(f,g,f+Math.round(this._18/3.0),g);var h=this._7+(this._F*d+this._F/2)+m;this._c(h,d)},this)},this)},_M:function(){if(this.hide_line_markers)return;this._0.stroke_antialias=false;this._0.stroke_width=1;var c=5;var d=this._17(this.maximum_value/c);for(var f=0;f<=c;f++){var g=(this._l-this._1)/c,h=this._l-(g*f)-1,i=f-c,j=Math.abs(i)*d;this._0.stroke=this.marker_color;this._0.line(h,this._g,h,this._g+0.5*this.klass.LABEL_MARGIN);if(!this.hide_line_numbers){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='center';this._0.annotate_scaled(0,0,h,this._g+(this.klass.LABEL_MARGIN*2.0),j,this._b)}this._0.stroke_antialias=true}},_c:function(c,d){if(this.labels[d]&&!this._q[d]){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.font_weight='normal';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='east';this._0.annotate_scaled(1,1,this._1-this.klass.LABEL_MARGIN*2.0,c,this.labels[d],this._b);this._q[d]=true}}});Bluff.Net=new JS.Class(Bluff.Base,{hide_dots:null,line_width:null,dot_radius:null,initialize:function(){this.callSuper();this.hide_dots=false;this.hide_line_numbers=true},draw:function(){this.callSuper();if(!this._a)return;this._v=this._3/2.0;this._w=this._1+(this._6/2.0);this._x=this._7+(this._3/2.0)-10;this._R=this._6/(this._5-1);var s=this.dot_radius||this._Q(this._j/(this._9[0][this.klass.DATA_VALUES_INDEX].length*2.5),7.0);this._0.stroke_opacity=1.0;this._0.stroke_width=this.line_width||this._Q(this._j/(this._9[0][this.klass.DATA_VALUES_INDEX].length*4),3.0);var r;if(this._S!==undefined){r=this._7+(this._3-this._S*this._3);this._0.push();this._0.stroke_color=this.baseline_color;this._0.fill_opacity=0.0;this._0.stroke_width=5;this._0.line(this._1,r,this._1+this._6,r);this._0.pop()}Bluff.each(this._9,function(o){var p=null,q=null;Bluff.each(o[this.klass.DATA_VALUES_INDEX],function(c,d){if(c===undefined)return;var f=d*Math.PI*2/this._5,g=c*this._v,h=this._w+Math.sin(f)*g,i=this._x-Math.cos(f)*g,j=(d+10){this._0.fill=c[this.klass.DATA_COLOR_INDEX];var f=(c[this.klass.DATA_VALUES_INDEX][0]/o)*360;this._0.circle(n,m,n+k,m,p,p+f+0.5);var g=p+((p+f)-p)/2,h=Math.round((c[this.klass.DATA_VALUES_INDEX][0]/o)*100.0),i;if(h>=this.hide_labels_less_than){i=this._15(c[this.klass.DATA_VALUES_INDEX][0]);this._c(n,m,g,k+(k*this.klass.TEXT_OFFSET_PERCENTAGE),i)}p+=f}},this)},_c:function(c,d,f,g,h){var i=20.0,j=c,k=d,l=g+i,n=l*0.15,m=j+((l+n)*Math.cos(f*Math.PI/180)),o=k+(l*Math.sin(f*Math.PI/180));this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.pointsize=this._e(this.marker_font_size);this._0.font_weight='bold';this._0.gravity='center';this._0.annotate_scaled(0,0,m,o,h,this._b)},_1R:function(){var d=0;Bluff.each(this._2,function(c){d+=c[this.klass.DATA_VALUES_INDEX][0]},this);return d}});Bluff.SideBar=new JS.Class(Bluff.Base,{bar_spacing:0.9,draw:function(){this.has_left_labels=true;this.callSuper();if(!this._a)return;this._G=this._3/this._5;this._8=this._G*this.bar_spacing/this._9.length;this._0.stroke_opacity=0.0;var q=Bluff.array_new(this._5,0),s=Bluff.array_new(this._5,this._1),r=(this._G*(1-this.bar_spacing))/2;Bluff.each(this._9,function(m,o){var p=this._2[o][this.klass.DATA_VALUES_INDEX];Bluff.each(m[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(this._6-c*this._6-q[d]),g=this._1+this._6-q[d],h=g-f,i=s[d]-1,j=this._7+(this._G*d)+(this._8*o)+r,k=i+h,l=j+this._8;q[d]+=(c*this._6);this._0.stroke='transparent';this._0.fill=m[this.klass.DATA_COLOR_INDEX];this._0.rectangle(i,j,k,l);this._E(i,j,k-i,l-j,m[this.klass.DATA_LABEL_INDEX],m[this.klass.DATA_COLOR_INDEX],p[d]);var n=this._7+(this._G*d+this._G/2);this._c(n,d)},this)},this)},_M:function(){if(this.hide_line_markers)return;this._0.stroke_antialias=false;this._0.stroke_width=1;var c=5;var d=this._17(this.maximum_value/c),f,g,h,i;for(var j=0;j<=c;j++){f=(this._l-this._1)/c;g=this._l-(f*j)-1;h=j-c;i=Math.abs(h)*d;this._0.stroke=this.marker_color;this._0.line(g,this._g,g,this._7);if(!this.hide_line_numbers){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='center';this._0.annotate_scaled(0,0,g,this._g+(this.klass.LABEL_MARGIN*2.0),i,this._b)}}},_c:function(c,d){if(this.labels[d]&&!this._q[d]){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.stroke='transparent';this._0.font_weight='normal';this._0.pointsize=this._e(this.marker_font_size);this._0.gravity='east';this._0.annotate_scaled(1,1,this._1-this.klass.LABEL_MARGIN*2.0,c,this.labels[d],this._b);this._q[d]=true}}});Bluff.Spider=new JS.Class(Bluff.Base,{hide_text:null,hide_axes:null,transparent_background:null,initialize:function(c,d,f){this.callSuper(c,f);this._1S=d;this.hide_legend=true},draw:function(){this.hide_line_markers=true;this.callSuper();if(!this._a)return;var c=this._3,d=this._3/2.0,f=this._1+(this._6-c)/2.0,g=this._1+(this._6/2.0),h=this._7+(this._3/2.0)-25;this._1T=d/this._1S;var i=this._1U(),j=0.0,k=(2*Math.PI)/this._2.length,l=0.0;if(!this.hide_axes)this._1V(g,h,d,k);this._1W(g,h,k)},_1n:function(c){return c*this._1T},_c:function(c,d,f,g,h){var i=50,j=c,k=d+0,l=j+((g+i)*Math.cos(f)),n=k+((g+i)*Math.sin(f));this._0.fill=this.marker_color;if(this.font)this._0.font=this.font;this._0.pointsize=this._e(this.legend_font_size);this._0.stroke='transparent';this._0.font_weight='bold';this._0.gravity='center';this._0.annotate_scaled(0,0,l,n,h,this._b)},_1V:function(g,h,i,j,k){if(this.hide_axes)return;var l=0.0;Bluff.each(this._2,function(c){this._0.stroke=k||c[this.klass.DATA_COLOR_INDEX];this._0.stroke_width=5.0;var d=i*Math.cos(l);var f=i*Math.sin(l);this._0.line(g,h,g+d,h+f);if(!this.hide_text)this._c(g,h,l,i,c[this.klass.DATA_LABEL_INDEX]);l+=j},this)},_1W:function(d,f,g,h){var i=[],j=0.0;Bluff.each(this._2,function(c){i.push(d+this._1n(c[this.klass.DATA_VALUES_INDEX][0])*Math.cos(j));i.push(f+this._1n(c[this.klass.DATA_VALUES_INDEX][0])*Math.sin(j));j+=g},this);this._0.stroke_width=1.0;this._0.stroke=h||this.marker_color;this._0.fill=h||this.marker_color;this._0.fill_opacity=0.4;this._0.polyline(i)},_1U:function(){var d=0.0;Bluff.each(this._2,function(c){d+=c[this.klass.DATA_VALUES_INDEX][0]},this);return d}});Bluff.Base.StackedMixin=new JS.Module({_19:function(){var g={};Bluff.each(this._2,function(f){Bluff.each(f[this.klass.DATA_VALUES_INDEX],function(c,d){if(!g[d])g[d]=0.0;g[d]+=c},this)},this);for(var h in g){if(g[h]>this.maximum_value)this.maximum_value=g[h]}this.minimum_value=0}});Bluff.StackedArea=new JS.Class(Bluff.Base,{include:Bluff.Base.StackedMixin,last_series_goes_on_bottom:null,draw:function(){this._19();this.callSuper();if(!this._a)return;this._R=this._6/(this._5-1);this._0.stroke='transparent';var n=Bluff.array_new(this._5,0);var m=null;var o=this.last_series_goes_on_bottom?'reverse_each':'each';Bluff[o](this._9,function(h){var i=m;m=[];Bluff.each(h[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(this._R*d);var g=this._7+(this._3-c*this._3-n[d]);n[d]+=(c*this._3);m.push(f);m.push(g);this._c(f,d)},this);var j,k,l;if(i){j=Bluff.array(m);for(k=i.length/2-1;k>=0;k--){j.push(i[2*k]);j.push(i[2*k+1])}j.push(m[0]);j.push(m[1])}else{j=Bluff.array(m);j.push(this._l);j.push(this._g-1);j.push(this._1);j.push(this._g-1);j.push(m[0]);j.push(m[1])}this._0.fill=h[this.klass.DATA_COLOR_INDEX];this._0.polyline(j)},this)}});Bluff.StackedBar=new JS.Class(Bluff.Base,{include:Bluff.Base.StackedMixin,bar_spacing:0.9,draw:function(){this._19();this.callSuper();if(!this._a)return;this._8=this._6/this._5;var m=(this._8*(1-this.bar_spacing))/2;this._0.stroke_opacity=0.0;var o=Bluff.array_new(this._5,0);Bluff.each(this._9,function(k,l){var n=this._2[l][this.klass.DATA_VALUES_INDEX];Bluff.each(k[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(this._8*d)+(this._8*this.bar_spacing/2.0);this._c(f,d);if(c==0)return;var g=this._1+(this._8*d)+m;var h=this._7+(this._3-c*this._3-o[d])+1;var i=g+this._8*this.bar_spacing;var j=this._7+this._3-o[d]-1;o[d]+=(c*this._3);this._0.fill=k[this.klass.DATA_COLOR_INDEX];this._0.rectangle(g,h,i,j);this._E(g,h,i-g,j-h,k[this.klass.DATA_LABEL_INDEX],k[this.klass.DATA_COLOR_INDEX],n[d])},this)},this)}});Bluff.AccumulatorBar=new JS.Class(Bluff.StackedBar,{draw:function(){if(this._2.length!==1)throw'Incorrect number of datasets';var g=[],h=0,i=[];Bluff.each(this._2[0][this.klass.DATA_VALUES_INDEX],function(d){var f=-Infinity;Bluff.each(i,function(c){f=Math.max(f,c)});i.push((h>0)?(d+f):d);g.push(i[h]-d);h+=1},this);this.data("Accumulator",g);this.callSuper()}});Bluff.SideStackedBar=new JS.Class(Bluff.SideBar,{include:Bluff.Base.StackedMixin,bar_spacing:0.9,draw:function(){this.has_left_labels=true;this._19();this.callSuper();if(!this._a)return;this._8=this._3/this._5;var q=Bluff.array_new(this._5,0),s=Bluff.array_new(this._5,this._1),r=(this._8*(1-this.bar_spacing))/2;Bluff.each(this._9,function(m,o){this._0.fill=m[this.klass.DATA_COLOR_INDEX];var p=this._2[o][this.klass.DATA_VALUES_INDEX];Bluff.each(m[this.klass.DATA_VALUES_INDEX],function(c,d){var f=this._1+(this._6-c*this._6-q[d])+1;var g=this._1+this._6-q[d]-1;var h=g-f;var i=s[d],j=this._7+(this._8*d)+r,k=i+h,l=j+this._8*this.bar_spacing;s[d]+=h;q[d]+=(c*this._6-2);this._0.rectangle(i,j,k,l);this._E(i,j,k-i,l-j,m[this.klass.DATA_LABEL_INDEX],m[this.klass.DATA_COLOR_INDEX],p[d]);var n=this._7+(this._8*d)+(this._8*this.bar_spacing/2.0);this._c(n,d)},this)},this)},_1f:function(c,d){d=d||0;return this._1m(c,d)>this.maximum_value},_1m:function(d,f){var g=0;Bluff.each(this._2,function(c){g+=c[this.klass.DATA_VALUES_INDEX][f]},this);return g}});Bluff.Mini.Legend=new JS.Module({hide_mini_legend:false,_1a:function(){if(this.hide_mini_legend)return;this._1X=this._L;this._y+=this._2.length*this._D(this._e(this.legend_font_size))*1.7;this._X()},_1b:function(){if(this.hide_mini_legend)return;this._P=Bluff.map(this._2,function(c){return c[this.klass.DATA_LABEL_INDEX]},this);var f=40.0,g=10.0,h=100.0,i=40.0;if(this.font)this._0.font=this.font;this._0.pointsize=this.legend_font_size;var j=h,k=this._1X+i;this._u(function(){this._0.line(0.0,k,this._d,k)});Bluff.each(this._P,function(c,d){this._0.fill=this.font_color;if(this.font)this._0.font=this.font;this._0.pointsize=this._e(this.legend_font_size);this._0.stroke='transparent';this._0.font_weight='normal';this._0.gravity='west';this._0.annotate_scaled(this._d,1.0,j+(f*1.7),k,this._1Y(c),this._b);this._0.stroke='transparent';this._0.fill=this._2[d][this.klass.DATA_COLOR_INDEX];this._0.rectangle(j,k-f/2.0,j+f,k+f/2.0);k+=this._D(this.legend_font_size)*1.7},this);this._m=0},_1Y:function(c){var d=String(c);while(this._O(this._e(this.legend_font_size),d)>(this._j-this.legend_left_margin-this.right_margin)&&(d.length>1))d=d.substr(0,d.length-1);return d+(d.length=1?(c*i):1;var k=(d*i)>=1?(d*i):1;var h=this._T(this.pointsize,h);h.style.color=this.fill;h.style.fontWeight=this.font_weight;h.style.textAlign='center';h.style.left=(this._f*f+this._1Z(h,j))+'px';h.style.top=(this._h*g+this._20(h,k))+'px'},tooltip:function(d,f,g,h,i,j,k){if(g<0)d+=g;if(h<0)f+=h;var l=this._n.parentNode,n=document.createElement('div');n.className=this.klass.TARGET_CLASS;n.style.position='absolute';n.style.left=(this._f*d-3)+'px';n.style.top=(this._h*f-3)+'px';n.style.width=(this._f*Math.abs(g)+5)+'px';n.style.height=(this._h*Math.abs(h)+5)+'px';n.style.fontSize=0;n.style.overflow='hidden';Bluff.Event.observe(n,'mouseover',function(c){Bluff.Tooltip.show(i,j,k)});Bluff.Event.observe(n,'mouseout',function(c){Bluff.Tooltip.hide()});l.appendChild(n)},circle:function(c,d,f,g,h,i){var j=Math.sqrt(Math.pow(f-c,2)+Math.pow(g-d,2));this._4.fillStyle=this.fill;this._4.beginPath();var k=(h||0)*Math.PI/180;var l=(i||360)*Math.PI/180;if(h!==undefined&&i!==undefined){this._4.moveTo(this._f*(c+j*Math.cos(l)),this._h*(d+j*Math.sin(l)));this._4.lineTo(this._f*c,this._h*d);this._4.lineTo(this._f*(c+j*Math.cos(k)),this._h*(d+j*Math.sin(k)))}this._4.arc(this._f*c,this._h*d,this._f*j,k,l,false);this._4.fill()},line:function(c,d,f,g){this._4.strokeStyle=this.stroke;this._4.lineWidth=this.stroke_width;this._4.beginPath();this._4.moveTo(this._f*c,this._h*d);this._4.lineTo(this._f*f,this._h*g);this._4.stroke()},polyline:function(c){this._4.fillStyle=this.fill;this._4.globalAlpha=this.fill_opacity||1;try{this._4.strokeStyle=this.stroke}catch(e){}var d=c.shift(),f=c.shift();this._4.beginPath();this._4.moveTo(this._f*d,this._h*f);while(c.length>0){d=c.shift();f=c.shift();this._4.lineTo(this._f*d,this._h*f)}this._4.fill()},rectangle:function(c,d,f,g){var h;if(c>f){h=c;c=f;f=h}if(d>g){h=d;d=g;g=h}try{this._4.fillStyle=this.fill;this._4.fillRect(this._f*c,this._h*d,this._f*(f-c),this._h*(g-d))}catch(e){}try{this._4.strokeStyle=this.stroke;if(this.stroke!=='transparent')this._4.strokeRect(this._f*c,this._h*d,this._f*(f-c),this._h*(g-d))}catch(e){}},_1Z:function(c,d){var f=this._H(c).width;switch(this.gravity){case'west':return 0;case'east':return d-f;case'north':case'south':case'center':return(d-f)/2}},_20:function(c,d){var f=this._H(c).height;switch(this.gravity){case'north':return 0;case'south':return d-f;case'west':case'east':case'center':return(d-f)/2}},_1o:function(){var c=this._n.parentNode;if(c.className===this.klass.WRAPPER_CLASS)return c;c=document.createElement('div');c.className=this.klass.WRAPPER_CLASS;c.style.position='relative';c.style.border='none';c.style.padding='0 0 0 0';this._n.parentNode.insertBefore(c,this._n);c.appendChild(this._n);return c},_T:function(c,d){var f=this._21(d);f.style.fontFamily=this.font;f.style.fontSize=(typeof c==='number')?c+'px':c;return f},_21:function(c){var d=document.createElement('div');d.className=this.klass.TEXT_CLASS;d.style.position='absolute';d.appendChild(document.createTextNode(c));this._1o().appendChild(d);return d},_U:function(c){c.parentNode.removeChild(c);if(c.className===this.klass.TARGET_CLASS)Bluff.Event.stopObserving(c)},_H:function(c){var d=c.style.display;return(d&&d!=='none')?{width:c.offsetWidth,height:c.offsetHeight}:{width:c.clientWidth,height:c.clientHeight}}});Bluff.Event={_V:[],_1p:(window.attachEvent&&navigator.userAgent.indexOf('Opera')===-1),observe:function(d,f,g,h){var i=Bluff.map(this._1q(d,f),function(c){return c._22});if(Bluff.index(i,g)!==-1)return;var j=function(c){g.call(h||null,d,Bluff.Event._23(c))};this._V.push({_W:d,_1c:f,_22:g,_1r:j});if(d.addEventListener)d.addEventListener(f,j,false);else d.attachEvent('on'+f,j)},stopObserving:function(d){var f=d?this._1q(d):this._V;Bluff.each(f,function(c){if(c._W.removeEventListener)c._W.removeEventListener(c._1c,c._1r,false);else c._W.detachEvent('on'+c._1c,c._1r)})},_1q:function(d,f){var g=[];Bluff.each(this._V,function(c){if(d&&c._W!==d)return;if(f&&c._1c!==f)return;g.push(c)});return g},_23:function(c){if(!this._1p)return c;if(!c)return false;if(c._24)return c;c._24=true;var d=this._25(c);c.target=c.srcElement;c.pageX=d.x;c.pageY=d.y;return c},_25:function(c){var d=document.documentElement,f=document.body||{scrollLeft:0,scrollTop:0};return{x:c.pageX||(c.clientX+(d.scrollLeft||f.scrollLeft)-(d.clientLeft||0)),y:c.pageY||(c.clientY+(d.scrollTop||f.scrollTop)-(d.clientTop||0))}}};if(Bluff.Event._1p)window.attachEvent('onunload',function(){Bluff.Event.stopObserving();Bluff.Event._V=null});if(navigator.userAgent.indexOf('AppleWebKit/')>-1)window.addEventListener('unload',function(){},false);Bluff.Tooltip=new JS.Singleton({LEFT_OFFSET:20,TOP_OFFSET:-6,DATA_LENGTH:8,CLASS_NAME:'bluff-tooltip',setup:function(){this._o=document.createElement('div');this._o.className=this.CLASS_NAME;this._o.style.position='absolute';this.hide();document.body.appendChild(this._o);Bluff.Event.observe(document.body,'mousemove',function(c,d){this._o.style.left=(d.pageX+this.LEFT_OFFSET)+'px';this._o.style.top=(d.pageY+this.TOP_OFFSET)+'px'},this)},show:function(c,d,f){f=Number(String(f).substr(0,this.DATA_LENGTH));this._o.innerHTML='  '+c+' '+f+'';this._o.style.display=''},hide:function(){this._o.style.display='none'}});Bluff.Event.observe(window,'load',Bluff.Tooltip.method('setup'));Bluff.TableReader=new JS.Class({NUMBER_FORMAT:/\-?(0|[1-9]\d*)(\.\d+)?(e[\+\-]?\d+)?/i,initialize:function(c,d){this._26=(typeof c==='string')?document.getElementById(c):c;this._1s=!!d},get_data:function(){if(!this._2)this._1t();return this._2},get_labels:function(){if(!this._1d)this._1t();return this._1d},get_title:function(){return this._27},get_series:function(c){if(this._2[c])return this._2[c];return this._2[c]={points:[]}},_1t:function(){this._I=this._p=0;this._J=this._K=0;this._2=[];this._1d={};this._s=[];this._t=[];this._1u(this._26);if((this._s.length>1&&this._t.length===1)||this._s.length]+>/gi,'')},extend:{Mixin:new JS.Module({data_from_table:function(d,f){var g=new Bluff.TableReader(d,f),h=g.get_data();Bluff.each(h,function(c){this.data(c.name,c.points)},this);this.labels=g.get_labels();this.title=g.get_title()||this.title}})}});Bluff.Base.include(Bluff.TableReader.Mixin); \ No newline at end of file diff --git a/vendor/bluff/excanvas.js b/vendor/bluff/excanvas.js new file mode 100644 index 0000000..a34ca1d --- /dev/null +++ b/vendor/bluff/excanvas.js @@ -0,0 +1,35 @@ +// Copyright 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +document.createElement("canvas").getContext||(function(){var s=Math,j=s.round,F=s.sin,G=s.cos,V=s.abs,W=s.sqrt,k=10,v=k/2;function X(){return this.context_||(this.context_=new H(this))}var L=Array.prototype.slice;function Y(b,a){var c=L.call(arguments,2);return function(){return b.apply(a,c.concat(L.call(arguments)))}}var M={init:function(b){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var a=b||document;a.createElement("canvas");a.attachEvent("onreadystatechange",Y(this.init_,this,a))}},init_:function(b){b.namespaces.g_vml_|| +b.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML");b.namespaces.g_o_||b.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML");if(!b.styleSheets.ex_canvas_){var a=b.createStyleSheet();a.owningElement.id="ex_canvas_";a.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}g_o_\\:*{behavior:url(#default#VML)}"}var c=b.getElementsByTagName("canvas"),d=0;for(;d','","");this.element_.insertAdjacentHTML("BeforeEnd",t.join(""))};i.stroke=function(b){var a=[],c=P(b?this.fillStyle:this.strokeStyle),d=c.color,f=c.alpha*this.globalAlpha;a.push("g.x)g.x=e.x;if(h.y==null||e.yg.y)g.y=e.y}}a.push(' ">');if(b)if(typeof this.fillStyle=="object"){var m=this.fillStyle,r=0,n={x:0,y:0},o=0,q=1;if(m.type_=="gradient"){var t=m.x1_/this.arcScaleX_,E=m.y1_/this.arcScaleY_,p=this.getCoords_(m.x0_/this.arcScaleX_,m.y0_/this.arcScaleY_), +z=this.getCoords_(t,E);r=Math.atan2(z.x-p.x,z.y-p.y)*180/Math.PI;if(r<0)r+=360;if(r<1.0E-6)r=0}else{var p=this.getCoords_(m.x0_,m.y0_),w=g.x-h.x,x=g.y-h.y;n={x:(p.x-h.x)/w,y:(p.y-h.y)/x};w/=this.arcScaleX_*k;x/=this.arcScaleY_*k;var R=s.max(w,x);o=2*m.r0_/R;q=2*m.r1_/R-o}var u=m.colors_;u.sort(function(ba,ca){return ba.offset-ca.offset});var J=u.length,da=u[0].color,ea=u[J-1].color,fa=u[0].alpha*this.globalAlpha,ga=u[J-1].alpha*this.globalAlpha,S=[],l=0;for(;l')}else a.push('');else{var K=this.lineScale_*this.lineWidth;if(K<1)f*=K;a.push("')}a.push("");this.element_.insertAdjacentHTML("beforeEnd",a.join(""))};i.fill=function(){this.stroke(true)};i.closePath=function(){this.currentPath_.push({type:"close"})};i.getCoords_=function(b,a){var c=this.m_;return{x:k*(b*c[0][0]+a*c[1][0]+c[2][0])-v,y:k*(b*c[0][1]+a*c[1][1]+c[2][1])-v}};i.save=function(){var b={};O(this,b);this.aStack_.push(b);this.mStack_.push(this.m_);this.m_=y(I(),this.m_)};i.restore=function(){O(this.aStack_.pop(), +this);this.m_=this.mStack_.pop()};function ha(b){var a=0;for(;a<3;a++){var c=0;for(;c<2;c++)if(!isFinite(b[a][c])||isNaN(b[a][c]))return false}return true}function A(b,a,c){if(!!ha(a)){b.m_=a;if(c)b.lineScale_=W(V(a[0][0]*a[1][1]-a[0][1]*a[1][0]))}}i.translate=function(b,a){A(this,y([[1,0,0],[0,1,0],[b,a,1]],this.m_),false)};i.rotate=function(b){var a=G(b),c=F(b);A(this,y([[a,c,0],[-c,a,0],[0,0,1]],this.m_),false)};i.scale=function(b,a){this.arcScaleX_*=b;this.arcScaleY_*=a;A(this,y([[b,0,0],[0,a, +0],[0,0,1]],this.m_),true)};i.transform=function(b,a,c,d,f,h){A(this,y([[b,a,0],[c,d,0],[f,h,1]],this.m_),true)};i.setTransform=function(b,a,c,d,f,h){A(this,[[b,a,0],[c,d,0],[f,h,1]],true)};i.clip=function(){};i.arcTo=function(){};i.createPattern=function(){return new U};function D(b){this.type_=b;this.r1_=this.y1_=this.x1_=this.r0_=this.y0_=this.x0_=0;this.colors_=[]}D.prototype.addColorStop=function(b,a){a=P(a);this.colors_.push({offset:b,color:a.color,alpha:a.alpha})};function U(){}G_vmlCanvasManager= +M;CanvasRenderingContext2D=H;CanvasGradient=D;CanvasPattern=U})(); diff --git a/vendor/bluff/js-class.js b/vendor/bluff/js-class.js new file mode 100644 index 0000000..923fa96 --- /dev/null +++ b/vendor/bluff/js-class.js @@ -0,0 +1 @@ +JS={extend:function(a,b){b=b||{};for(var c in b){if(a[c]===b[c])continue;a[c]=b[c]}return a},makeFunction:function(){return function(){return this.initialize?(this.initialize.apply(this,arguments)||this):this}},makeBridge:function(a){var b=function(){};b.prototype=a.prototype;return new b},bind:function(){var a=JS.array(arguments),b=a.shift(),c=a.shift()||null;return function(){return b.apply(c,a.concat(JS.array(arguments)))}},callsSuper:function(a){return a.SUPER===undefined?a.SUPER=/\bcallSuper\b/.test(a.toString()):a.SUPER},mask:function(a){var b=a.toString().replace(/callSuper/g,'super');a.toString=function(){return b};return a},array:function(a){if(!a)return[];if(a.toArray)return a.toArray();var b=a.length,c=[];while(b--)c[b]=a[b];return c},indexOf:function(a,b){for(var c=0,d=a.length;c':''),d=this.__meta__=new JS.Module(c?c+'.':'',{},{_1:this});d.include(this.klass.__mod__,false);return d},equals:function(a){return this===a},extend:function(a,b){return this.__eigen__().include(a,b,{_2:this})},hash:function(){return this.__hashcode__=this.__hashcode__||JS.Kernel.getHashCode()},isA:function(a){return this.__eigen__().includes(a)},method:function(a){var b=this,c=b.__mcache__=b.__mcache__||{};if((c[a]||{}).fn===b[a])return c[a].bd;return(c[a]={fn:b[a],bd:JS.bind(b[a],b)}).bd},methods:function(){return this.__eigen__().instanceMethods(true)},tap:function(a,b){a.call(b||null,this);return this}}),{__hashIndex__:0,getHashCode:function(){this.__hashIndex__+=1;return(Math.floor(new Date().getTime()/1000)+this.__hashIndex__).toString(16)}});JS.Module.include(JS.Kernel);JS.extend(JS.Module,JS.Kernel.__fns__);JS.Class.include(JS.Kernel);JS.extend(JS.Class,JS.Kernel.__fns__);JS.Interface=new JS.Class({initialize:function(d){this.test=function(a,b){var c=d.length;while(c--){if(!JS.isFn(a[d[c]]))return b?d[c]:false}return true}},extend:{ensure:function(){var a=JS.array(arguments),b=a.shift(),c,d;while(c=a.shift()){d=c.test(b,true);if(d!==true)throw new Error('object does not implement '+d+'()');}}}});JS.Singleton=new JS.Class({initialize:function(a,b,c){return new(new JS.Class(a,b,c))}}); \ No newline at end of file diff --git a/vendor/jquery/jquery.form.js b/vendor/jquery/jquery.form.js new file mode 100644 index 0000000..7256c35 --- /dev/null +++ b/vendor/jquery/jquery.form.js @@ -0,0 +1,660 @@ +/* + * jQuery Form Plugin + * version: 2.36 (07-NOV-2009) + * @requires jQuery v1.2.6 or later + * + * Examples and documentation at: http://malsup.com/jquery/form/ + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ +;(function($) { + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are intended to be exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').bind('submit', function() { + $(this).ajaxSubmit({ + target: '#output' + }); + return false; // <-- important! + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + if (typeof options == 'function') + options = { success: options }; + + var url = $.trim(this.attr('action')); + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + url = url || window.location.href || ''; + + options = $.extend({ + url: url, + type: this.attr('method') || 'GET', + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options || {}); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var a = this.formToArray(options.semantic); + if (options.data) { + options.extraData = options.data; + for (var n in options.data) { + if(options.data[n] instanceof Array) { + for (var k in options.data[n]) + a.push( { name: n, value: options.data[n][k] } ); + } + else + a.push( { name: n, value: options.data[n] } ); + } + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a); + + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status) { + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i].apply(options, [data, status, $form]); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + var multipart = false; +// var mp = 'multipart/form-data'; +// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if ((files.length && options.iframe !== false) || options.iframe || found || multipart) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) + $.get(options.closeKeepAlive, fileUpload); + else + fileUpload(); + } + else + $.ajax(options); + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + + if ($(':input[name=submit]', form).length) { + alert('Error: Form elements must not be named "submit".'); + return; + } + + var opts = $.extend({}, $.ajaxSettings, options); + var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); + + var id = 'jqFormIO' + (new Date().getTime()); + var $io = $('