├── resources └── public │ ├── images │ ├── favicon.ico │ ├── greeting-cloud.png │ ├── index-header.png │ ├── glyphicons-halflings.png │ ├── main-background-noise.png │ ├── form-background-gradient.png │ └── glyphicons-halflings-white.png │ ├── less │ ├── main.less~ │ ├── grid.less │ ├── utilities.less │ ├── component-animations.less │ ├── layouts.less │ ├── responsive-768px-979px.less │ ├── hero-unit.less │ ├── responsive-1200px-min.less │ ├── breadcrumbs.less │ ├── wells.less │ ├── scaffolding.less │ ├── close.less │ ├── pager.less │ ├── accordion.less │ ├── tooltip.less │ ├── tests │ │ ├── css-tests.css │ │ ├── navbar.html │ │ └── forms.html │ ├── thumbnails.less │ ├── responsive.less │ ├── pagination.less │ ├── alerts.less │ ├── popovers.less │ ├── responsive-utilities.less │ ├── code.less │ ├── labels-badges.less │ ├── bootstrap.less │ ├── modals.less │ ├── carousel.less │ ├── main.less │ ├── progress-bars.less │ ├── reset.less │ ├── responsive-767px-max.less │ ├── dropdowns.less │ ├── responsive-navbar.less │ ├── type.less │ ├── tables.less │ ├── buttons.less │ ├── button-groups.less │ ├── variables.less │ ├── navs.less │ └── navbar.less │ ├── the_weeknd.jpg │ ├── Rescue You.mp3.torrent │ ├── css │ ├── fonts │ │ ├── Chunkfive-webfont.eot │ │ ├── Chunkfive-webfont.ttf │ │ ├── Chunkfive-webfont.woff │ │ ├── ._Chunkfive-webfont.eot │ │ ├── ._Chunkfive-webfont.svg │ │ ├── ._Chunkfive-webfont.ttf │ │ ├── ._Chunkfive-webfont.woff │ │ ├── Museo300-Regular-webfont.eot │ │ ├── Museo300-Regular-webfont.ttf │ │ ├── Museo300-Regular-webfont.woff │ │ ├── Museo500-Regular-webfont.eot │ │ ├── Museo500-Regular-webfont.ttf │ │ ├── Museo500-Regular-webfont.woff │ │ ├── Museo700-Regular-webfont.eot │ │ ├── Museo700-Regular-webfont.ttf │ │ └── Museo700-Regular-webfont.woff │ ├── default.css │ └── reset.css │ ├── js │ ├── monkey.js │ └── humane.js │ └── index.html ├── src-cljs ├── torrent_client │ ├── main.cljs │ ├── core │ │ ├── url.cljs │ │ ├── metadata.cljs │ │ ├── incubator.cljs │ │ ├── reader.cljs │ │ ├── string.cljs │ │ ├── byte_array.cljs │ │ ├── queue.cljs │ │ ├── pieces.cljs │ │ ├── crypt.cljs │ │ ├── db.cljs │ │ ├── dispatch.cljs │ │ └── bencode.cljs │ ├── ui │ │ ├── notifications.cljs │ │ ├── online.cljs │ │ ├── jayq.cljs │ │ └── views.cljs │ ├── browser │ │ ├── features.cljs │ │ └── prefix.cljs │ ├── protocol │ │ ├── main.cljs │ │ └── bittorrent.cljs │ ├── peer_id.cljs │ ├── speed.cljs │ ├── torrents.cljs │ ├── storage.cljs │ ├── files.cljs │ ├── metadata.cljs │ ├── bitfield.cljs │ ├── peers.cljs │ └── tracker.cljs ├── task │ ├── macros.clj │ └── main.cljs ├── cljconsole │ ├── macros.clj │ └── main.cljs ├── filesystem │ ├── entry.cljs │ ├── writer.cljs │ ├── prefix.cljs │ └── filesystem.cljs └── async │ ├── macros.clj │ └── helpers.cljs ├── .gitignore ├── src-clj └── torrent_client │ ├── views │ ├── main.clj │ └── common.clj │ └── server.clj ├── project.clj └── README.md /resources/public/images/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/main.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.main) -------------------------------------------------------------------------------- /resources/public/less/main.less~: -------------------------------------------------------------------------------- 1 | table td{ 2 | height: 50px; 3 | line-height: 34px; 4 | } 5 | -------------------------------------------------------------------------------- /resources/public/the_weeknd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/the_weeknd.jpg -------------------------------------------------------------------------------- /resources/public/Rescue You.mp3.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/Rescue You.mp3.torrent -------------------------------------------------------------------------------- /resources/public/images/greeting-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/images/greeting-cloud.png -------------------------------------------------------------------------------- /resources/public/images/index-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/images/index-header.png -------------------------------------------------------------------------------- /resources/public/css/fonts/Chunkfive-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Chunkfive-webfont.eot -------------------------------------------------------------------------------- /resources/public/css/fonts/Chunkfive-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Chunkfive-webfont.ttf -------------------------------------------------------------------------------- /resources/public/css/fonts/Chunkfive-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Chunkfive-webfont.woff -------------------------------------------------------------------------------- /resources/public/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /resources/public/images/main-background-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/images/main-background-noise.png -------------------------------------------------------------------------------- /resources/public/css/fonts/._Chunkfive-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/._Chunkfive-webfont.eot -------------------------------------------------------------------------------- /resources/public/css/fonts/._Chunkfive-webfont.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/._Chunkfive-webfont.svg -------------------------------------------------------------------------------- /resources/public/css/fonts/._Chunkfive-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/._Chunkfive-webfont.ttf -------------------------------------------------------------------------------- /resources/public/css/fonts/._Chunkfive-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/._Chunkfive-webfont.woff -------------------------------------------------------------------------------- /resources/public/images/form-background-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/images/form-background-gradient.png -------------------------------------------------------------------------------- /src-cljs/task/macros.clj: -------------------------------------------------------------------------------- 1 | (ns task.macros) 2 | 3 | (defmacro deftask [label period bindings & body] 4 | `(task/main/task ~period (fn ~bindings ~@body))) -------------------------------------------------------------------------------- /resources/public/images/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/images/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo300-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo300-Regular-webfont.eot -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo300-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo300-Regular-webfont.ttf -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo300-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo300-Regular-webfont.woff -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo500-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo500-Regular-webfont.eot -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo500-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo500-Regular-webfont.ttf -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo500-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo500-Regular-webfont.woff -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo700-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo700-Regular-webfont.eot -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo700-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo700-Regular-webfont.ttf -------------------------------------------------------------------------------- /resources/public/css/fonts/Museo700-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcliff/ampere/HEAD/resources/public/css/fonts/Museo700-Regular-webfont.woff -------------------------------------------------------------------------------- /resources/public/less/grid.less: -------------------------------------------------------------------------------- 1 | // Fixed (940px) 2 | #grid > .core(@gridColumnWidth, @gridGutterWidth); 3 | 4 | // Fluid (940px) 5 | #grid > .fluid(@fluidGridColumnWidth, @fluidGridGutterWidth); -------------------------------------------------------------------------------- /src-cljs/cljconsole/macros.clj: -------------------------------------------------------------------------------- 1 | (ns cljconsole.macros) 2 | 3 | (defmacro defn-method [fname & [jsf]] 4 | (let [jsf (or jsf fname)] 5 | `(def ~(symbol fname) ~(list 'apply-method (str jsf))))) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | .lein-deps-sum 6 | .lein-git-deps 7 | .lein-env 8 | resources/public/cljs/*/ 9 | *log 10 | .repl/ 11 | target 12 | .lein-repl-history 13 | phantom/ 14 | repl/ 15 | out/ -------------------------------------------------------------------------------- /src-clj/torrent_client/views/main.clj: -------------------------------------------------------------------------------- 1 | (ns torrent-client.views.main 2 | (:require [torrent-client.views.common :as common]) 3 | (:use [noir.core :only [defpage]] 4 | [hiccup.core :only [html]])) 5 | 6 | (defpage index "/" [] 7 | (common/layout [:div#content])) -------------------------------------------------------------------------------- /src-cljs/task/main.cljs: -------------------------------------------------------------------------------- 1 | (ns task.main 2 | (:require 3 | [cljconsole.main :as console] 4 | [goog.Timer :as Timer] 5 | [goog.events :as events])) 6 | 7 | (defn task [period f] 8 | (let [timer (goog/Timer. period)] 9 | (.start timer) 10 | (events/listen timer Timer/TICK f))) -------------------------------------------------------------------------------- /resources/public/css/default.css: -------------------------------------------------------------------------------- 1 | body { padding: 75px 80px; background: #454545; color: #b2b2b2; font-family: "Helvetica Neue", "Verdana", sans-serif;} 2 | a {text-decoration:none; color: #b2b2b2; } 3 | 4 | #wrapper { margin:auto; width: 1024px; } 5 | 6 | .alert { border-radius:10px; background: #3f634d; border: 2px solid #3c8455; padding: 15px; } 7 | -------------------------------------------------------------------------------- /src-clj/torrent_client/server.clj: -------------------------------------------------------------------------------- 1 | (ns torrent-client.server 2 | (:require [noir.server :as server])) 3 | 4 | (server/load-views-ns 'views) 5 | 6 | (defn -main [& m] 7 | (let [mode (keyword (or (first m) :dev)) 8 | port (Integer. (get (System/getenv) "PORT" "8091"))] 9 | (server/start port {:mode mode 10 | :ns 'torrent-client}))) -------------------------------------------------------------------------------- /resources/public/less/utilities.less: -------------------------------------------------------------------------------- 1 | // UTILITY CLASSES 2 | // --------------- 3 | 4 | // Quick floats 5 | .pull-right { 6 | float: right; 7 | } 8 | .pull-left { 9 | float: left; 10 | } 11 | 12 | // Toggling content 13 | .hide { 14 | display: none; 15 | } 16 | .show { 17 | display: block; 18 | } 19 | 20 | // Visibility 21 | .invisible { 22 | visibility: hidden; 23 | } 24 | -------------------------------------------------------------------------------- /resources/public/less/component-animations.less: -------------------------------------------------------------------------------- 1 | // COMPONENT ANIMATIONS 2 | // -------------------- 3 | 4 | .fade { 5 | opacity: 0; 6 | .transition(opacity .15s linear); 7 | &.in { 8 | opacity: 1; 9 | } 10 | } 11 | 12 | .collapse { 13 | position: relative; 14 | height: 0; 15 | overflow: hidden; 16 | .transition(height .35s ease); 17 | &.in { 18 | height: auto; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/public/less/layouts.less: -------------------------------------------------------------------------------- 1 | // 2 | // Layouts 3 | // Fixed-width and fluid (with sidebar) layouts 4 | // -------------------------------------------- 5 | 6 | 7 | // Container (centered, fixed-width layouts) 8 | .container { 9 | .container-fixed(); 10 | } 11 | 12 | // Fluid layouts (left aligned, with sidebar, min- & max-width content) 13 | .container-fluid { 14 | padding-right: @gridGutterWidth; 15 | padding-left: @gridGutterWidth; 16 | .clearfix(); 17 | } -------------------------------------------------------------------------------- /resources/public/less/responsive-768px-979px.less: -------------------------------------------------------------------------------- 1 | // PORTRAIT TABLET TO DEFAULT DESKTOP 2 | // ---------------------------------- 3 | 4 | @media (min-width: 768px) and (max-width: 979px) { 5 | 6 | // Fixed grid 7 | #grid > .core(42px, 20px); 8 | 9 | // Fluid grid 10 | #grid > .fluid(5.801104972%, 2.762430939%); 11 | 12 | // Input grid 13 | #grid > .input(42px, 20px); 14 | 15 | // No need to reset .thumbnails here since it's the same @gridGutterWidth 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/core/url.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.core.url 2 | (:require [goog.Uri :as uri])) 3 | 4 | (defn http-scheme? 5 | "Given a url return whether it uses http(s)" 6 | [url] 7 | (let [uri (uri/parse url) 8 | scheme (.getScheme uri)] 9 | (or (= "http" scheme) (= "https" scheme)))) 10 | 11 | (defn ws-scheme? 12 | "Given a url return whether it uses ws(s)" 13 | [url] 14 | (let [uri (uri/parse url) 15 | scheme (.getScheme uri)] 16 | (or (= "ws" scheme) (= "wss" scheme)))) -------------------------------------------------------------------------------- /src-cljs/cljconsole/main.cljs: -------------------------------------------------------------------------------- 1 | (ns cljconsole.main 2 | (:use-macros [cljconsole.macros :only [defn-method]])) 3 | 4 | (defn apply-method [fname] 5 | "Return a function for the method" 6 | (fn [& args] 7 | ; Turn all our arguments into javascript objects and call the method 8 | (.apply (aget js/console fname) js/console (clj->js args)))) 9 | 10 | (defn-method "log") 11 | (defn-method "warn") 12 | (defn-method "error") 13 | (defn-method "info") 14 | (defn-method "time") 15 | (defn-method "time-end" "timeEnd") -------------------------------------------------------------------------------- /resources/public/less/hero-unit.less: -------------------------------------------------------------------------------- 1 | // HERO UNIT 2 | // --------- 3 | 4 | .hero-unit { 5 | padding: 60px; 6 | margin-bottom: 30px; 7 | background-color: @heroUnitBackground; 8 | .border-radius(6px); 9 | h1 { 10 | margin-bottom: 0; 11 | font-size: 60px; 12 | line-height: 1; 13 | color: @heroUnitHeadingColor; 14 | letter-spacing: -1px; 15 | } 16 | p { 17 | font-size: 18px; 18 | font-weight: 200; 19 | line-height: @baseLineHeight * 1.5; 20 | color: @heroUnitLeadColor; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/public/less/responsive-1200px-min.less: -------------------------------------------------------------------------------- 1 | // LARGE DESKTOP & UP 2 | // ------------------ 3 | 4 | @media (min-width: 1200px) { 5 | 6 | // Fixed grid 7 | #grid > .core(70px, 30px); 8 | 9 | // Fluid grid 10 | #grid > .fluid(5.982905983%, 2.564102564%); 11 | 12 | // Input grid 13 | #grid > .input(70px, 30px); 14 | 15 | // Thumbnails 16 | .thumbnails { 17 | margin-left: -30px; 18 | } 19 | .thumbnails > li { 20 | margin-left: 30px; 21 | } 22 | .row-fluid .thumbnails { 23 | margin-left: 0; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /resources/public/less/breadcrumbs.less: -------------------------------------------------------------------------------- 1 | // BREADCRUMBS 2 | // ----------- 3 | 4 | .breadcrumb { 5 | padding: 7px 14px; 6 | margin: 0 0 @baseLineHeight; 7 | list-style: none; 8 | #gradient > .vertical(@white, #f5f5f5); 9 | border: 1px solid #ddd; 10 | .border-radius(3px); 11 | .box-shadow(inset 0 1px 0 @white); 12 | li { 13 | display: inline-block; 14 | .ie7-inline-block(); 15 | text-shadow: 0 1px 0 @white; 16 | } 17 | .divider { 18 | padding: 0 5px; 19 | color: @grayLight; 20 | } 21 | .active a { 22 | color: @grayDark; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/public/less/wells.less: -------------------------------------------------------------------------------- 1 | // WELLS 2 | // ----- 3 | 4 | .well { 5 | min-height: 20px; 6 | padding: 19px; 7 | margin-bottom: 20px; 8 | background-color: #f5f5f5; 9 | border: 1px solid #eee; 10 | border: 1px solid rgba(0,0,0,.05); 11 | .border-radius(4px); 12 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); 13 | blockquote { 14 | border-color: #ddd; 15 | border-color: rgba(0,0,0,.15); 16 | } 17 | } 18 | 19 | // Sizes 20 | .well-large { 21 | padding: 24px; 22 | .border-radius(6px); 23 | } 24 | .well-small { 25 | padding: 9px; 26 | .border-radius(3px); 27 | } 28 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/ui/notifications.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.ui.notifications) 2 | 3 | ; returns 0 when we have persmission 4 | ; (if-not (= 0 (.checkPermission webkit-notifications)) 5 | ; (.requestPermission webkit-notifications)))) 6 | 7 | ; When a torrent completes, try to inform the user 8 | ; (dispatch/react-to #{:completed-torrent} (fn [_ torrent] 9 | ; ; If the user is currently on the site don't show the notification 10 | ; (if-not (.hasFocus js/document) 11 | ; (.createNotification webkit-notifications 12 | ; nil 13 | ; "notification title" 14 | ; "notification content" 15 | ; )))) -------------------------------------------------------------------------------- /src-cljs/torrent_client/core/metadata.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.core.metadata 2 | (:use [torrent-client.core.byte-array :only [uint8-array]])) 3 | 4 | ; The bytes in each metainfo piece 5 | (def piece-length 700) 6 | 7 | (defn pieces->metadata 8 | "Build metadata from its pieces" 9 | [pieces] 10 | (let [info-length (reduce + (map count (vals pieces))) 11 | ; Build a byte array long enough for all the pieces 12 | byte-array (uint8-array info-length)] 13 | ; Then add all the pieces at their correct offset 14 | (doseq [[piece-index piece] pieces 15 | :let [offset (* piece-index piece-length)]] 16 | (.set byte-array piece offset)) 17 | byte-array)) -------------------------------------------------------------------------------- /resources/public/less/scaffolding.less: -------------------------------------------------------------------------------- 1 | // Scaffolding 2 | // Basic and global styles for generating a grid system, structural layout, and page templates 3 | // ------------------------------------------------------------------------------------------- 4 | 5 | 6 | // Body reset 7 | // ---------- 8 | 9 | body { 10 | margin: 0; 11 | font-family: @baseFontFamily; 12 | font-size: @baseFontSize; 13 | line-height: @baseLineHeight; 14 | color: @textColor; 15 | background-color: @bodyBackground; 16 | } 17 | 18 | 19 | // Links 20 | // ----- 21 | 22 | a { 23 | color: @linkColor; 24 | text-decoration: none; 25 | } 26 | a:hover { 27 | color: @linkColorHover; 28 | text-decoration: underline; 29 | } 30 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/core/incubator.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.core.incubator) 2 | 3 | ; https://github.com/clojure/core.incubator/blob/master/src/main/clojure/clojure/core/incubator.clj#L56 4 | (defn dissoc-in 5 | "Dissociates an entry from a nested associative structure returning a new 6 | nested structure. keys is a sequence of keys. Any empty maps that result 7 | will not be present in the new structure." 8 | [m [k & ks :as keys]] 9 | (if ks 10 | (if-let [nextmap (get m k)] 11 | (let [newmap (dissoc-in nextmap ks)] 12 | (if (seq newmap) 13 | (assoc m k newmap) 14 | (dissoc m k))) 15 | m) 16 | (dissoc m k))) 17 | 18 | (defn aget-in [a keys] 19 | (reduce #(aget % %2) keys a)) -------------------------------------------------------------------------------- /resources/public/less/close.less: -------------------------------------------------------------------------------- 1 | // CLOSE ICONS 2 | // ----------- 3 | 4 | .close { 5 | float: right; 6 | font-size: 20px; 7 | font-weight: bold; 8 | line-height: @baseLineHeight; 9 | color: @black; 10 | text-shadow: 0 1px 0 rgba(255,255,255,1); 11 | .opacity(20); 12 | &:hover { 13 | color: @black; 14 | text-decoration: none; 15 | cursor: pointer; 16 | .opacity(40); 17 | } 18 | } 19 | 20 | // Additional properties for button version 21 | // iOS requires the button element instead of an anchor tag. 22 | // If you want the anchor version, it requires `href="#"`. 23 | button.close { 24 | padding: 0; 25 | cursor: pointer; 26 | background: transparent; 27 | border: 0; 28 | -webkit-appearance: none; 29 | } -------------------------------------------------------------------------------- /src-cljs/torrent_client/ui/online.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.ui.online 2 | (:require 3 | [cljconsole.main :as console] 4 | [torrent-client.core.dispatch :as dispatch]) 5 | (:use [jayq.core :only [$ on]])) 6 | 7 | (def $window ($ js/window)) 8 | 9 | (def online (atom nil)) 10 | 11 | (on $window :offline (fn [_] 12 | (reset! online false))) 13 | 14 | (on $window :online (fn [_] 15 | (reset! online true))) 16 | 17 | (add-watch online nil (fn [_ _ old-val new-val] 18 | (if (and (false? old-val) new-val) 19 | (console/info "Client online")) 20 | (if (and old-val (false? new-val)) 21 | (console/info "Client offline")))) 22 | 23 | (dispatch/react-to #{:document-ready} (fn [_] 24 | (reset! online (.-onLine js/navigator)))) -------------------------------------------------------------------------------- /resources/public/less/pager.less: -------------------------------------------------------------------------------- 1 | // PAGER 2 | // ----- 3 | 4 | .pager { 5 | margin-left: 0; 6 | margin-bottom: @baseLineHeight; 7 | list-style: none; 8 | text-align: center; 9 | .clearfix(); 10 | } 11 | .pager li { 12 | display: inline; 13 | } 14 | .pager a { 15 | display: inline-block; 16 | padding: 5px 14px; 17 | background-color: #fff; 18 | border: 1px solid #ddd; 19 | .border-radius(15px); 20 | } 21 | .pager a:hover { 22 | text-decoration: none; 23 | background-color: #f5f5f5; 24 | } 25 | .pager .next a { 26 | float: right; 27 | } 28 | .pager .previous a { 29 | float: left; 30 | } 31 | .pager .disabled a, 32 | .pager .disabled a:hover { 33 | color: @grayLight; 34 | background-color: #fff; 35 | cursor: default; 36 | } -------------------------------------------------------------------------------- /resources/public/less/accordion.less: -------------------------------------------------------------------------------- 1 | // ACCORDION 2 | // --------- 3 | 4 | 5 | // Parent container 6 | .accordion { 7 | margin-bottom: @baseLineHeight; 8 | } 9 | 10 | // Group == heading + body 11 | .accordion-group { 12 | margin-bottom: 2px; 13 | border: 1px solid #e5e5e5; 14 | .border-radius(4px); 15 | } 16 | .accordion-heading { 17 | border-bottom: 0; 18 | } 19 | .accordion-heading .accordion-toggle { 20 | display: block; 21 | padding: 8px 15px; 22 | } 23 | 24 | // General toggle styles 25 | .accordion-toggle { 26 | cursor: pointer; 27 | } 28 | 29 | // Inner needs the styles because you can't animate properly with any styles on the element 30 | .accordion-inner { 31 | padding: 9px 15px; 32 | border-top: 1px solid #e5e5e5; 33 | } 34 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/browser/features.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.browser.features 2 | (:require [torrent-client.browser.prefix :as prefix])) 3 | 4 | ;************************************************ 5 | ; Browser feature detection 6 | ;************************************************ 7 | 8 | (defn create-data-channel? [_] 9 | "Rough check for datachannel support, not 100% accurate" 10 | (let [pc (prefix/RTCPeerConnection. nil)] 11 | (and pc (aget pc "createDataChannel")))) 12 | 13 | (defn persistent-storage? [_] 14 | "Does this browser support permanant locastorage" 15 | (not (and (nil? prefix/PersistentStorage) 16 | (nil? (.-webkitStorageInfo js/window))))) 17 | 18 | (defn supported? [browser] 19 | "Sniff support for required features" 20 | ((every-pred create-data-channel? persistent-storage?) browser)) -------------------------------------------------------------------------------- /src-cljs/torrent_client/protocol/main.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.protocol.main) 2 | 3 | (defprotocol Protocol 4 | (send-data [client data] [client type data] "") 5 | (send-handshake [client] "") 6 | (send-extended [client id message] [client id message data] "") 7 | (send-extended-handshake [client] "") 8 | (send-metadata-request [client piece-index] "") 9 | (send-metadata-piece [client piece-index info-length data] "") 10 | (send-metadata-reject [client piece-index] "") 11 | (send-choke [client] "") 12 | (send-unchoke [client] "") 13 | (send-interested [client] "") 14 | (send-not-interested [client] "") 15 | (send-have [client index] "") 16 | (send-bitfield [client] "") 17 | (send-request [client index begin piece] "") 18 | (send-block [client index begin piece] "") 19 | (send-cancel [client index begin piece] "") 20 | ) -------------------------------------------------------------------------------- /src-cljs/filesystem/entry.cljs: -------------------------------------------------------------------------------- 1 | (ns filesystem.entry 2 | (:use-macros [async.macros :only [async]])) 3 | 4 | (defn create-writer [file-entry] 5 | (async [success-callback error-callback] 6 | (letfn [(error-callback [e] 7 | (js* "debugger;") 8 | (.error js/console e))] 9 | (.createWriter file-entry success-callback error-callback)))) 10 | 11 | (defn get-entry 12 | "Return a fileentry (information on the file)" 13 | ([filesystem path] (get-entry filesystem path {})) 14 | ([filesystem path options] 15 | (async [success-callback error-callback] 16 | (.getFile (.-root filesystem) path (clj->js options) 17 | success-callback error-callback)))) 18 | 19 | (defn file 20 | "Given a FileEntry return the underlying file object" 21 | [entry] 22 | (async [success-callback error-callback] 23 | (.file entry success-callback (fn [e] (js* "debugger;") (.log js/console "failed"))))) -------------------------------------------------------------------------------- /src-cljs/async/macros.clj: -------------------------------------------------------------------------------- 1 | (ns async.macros 2 | "Macros to avoid callback soup, act as defer/await") 3 | 4 | (defmacro let-async [bindings & body] 5 | (cond 6 | ; when there are no more bindings execute the body 7 | (empty? bindings) `(do ~@body) 8 | 9 | ; a regular (not async let) 10 | ; e.g: [:let one "hello world"] 11 | (= (first bindings) :let) 12 | (let [[_ binding-name binding-value & remaining] bindings] 13 | `(let [~binding-name ~binding-value] 14 | (let-async ~remaining ~@body))) 15 | 16 | ; an async let 17 | :else 18 | (let [[binding-name binding-value & remaining] bindings] 19 | `(~binding-value 20 | (fn [binding-name#] 21 | (let [~binding-name binding-name#] 22 | (let-async ~remaining ~@body))) 23 | (fn [error#] 24 | (.error js/console error#)))))) 25 | 26 | (defmacro async [bindings & body] 27 | `(fn ~bindings ~@body)) -------------------------------------------------------------------------------- /resources/public/less/tooltip.less: -------------------------------------------------------------------------------- 1 | // TOOLTIP 2 | // ------= 3 | 4 | .tooltip { 5 | position: absolute; 6 | z-index: @zindexTooltip; 7 | display: block; 8 | visibility: visible; 9 | padding: 5px; 10 | font-size: 11px; 11 | .opacity(0); 12 | &.in { .opacity(80); } 13 | &.top { margin-top: -2px; } 14 | &.right { margin-left: 2px; } 15 | &.bottom { margin-top: 2px; } 16 | &.left { margin-left: -2px; } 17 | &.top .tooltip-arrow { #popoverArrow > .top(); } 18 | &.left .tooltip-arrow { #popoverArrow > .left(); } 19 | &.bottom .tooltip-arrow { #popoverArrow > .bottom(); } 20 | &.right .tooltip-arrow { #popoverArrow > .right(); } 21 | } 22 | .tooltip-inner { 23 | max-width: 200px; 24 | padding: 3px 8px; 25 | color: @white; 26 | text-align: center; 27 | text-decoration: none; 28 | background-color: @black; 29 | .border-radius(4px); 30 | } 31 | .tooltip-arrow { 32 | position: absolute; 33 | width: 0; 34 | height: 0; 35 | } 36 | -------------------------------------------------------------------------------- /resources/public/less/tests/css-tests.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap CSS Tests 3 | */ 4 | 5 | 6 | /* Remove background image */ 7 | body { 8 | background-image: none; 9 | } 10 | 11 | /* Space out subhead */ 12 | .subhead { 13 | margin-bottom: 36px; 14 | } 15 | h4 { 16 | margin-bottom: 5px; 17 | } 18 | 19 | 20 | /* colgroup tests */ 21 | .col1 { 22 | background-color: rgba(255,0,0,.1); 23 | } 24 | .col2 { 25 | background-color: rgba(0,255,0,.1); 26 | } 27 | .col3 { 28 | background-color: rgba(0,0,255,.1); 29 | } 30 | 31 | 32 | /* Fluid row inputs */ 33 | #rowInputs .row > [class*=span], 34 | #fluidRowInputs .row-fluid > [class*=span] { 35 | background-color: rgba(255,0,0,.1); 36 | } 37 | 38 | 39 | /* Fluid grid */ 40 | .fluid-grid { 41 | margin-bottom: 45px; 42 | } 43 | .fluid-grid .row { 44 | height: 40px; 45 | padding-top: 10px; 46 | margin-top: 10px; 47 | color: #ddd; 48 | text-align: center; 49 | } 50 | .fluid-grid .span1 { 51 | background-color: #999; 52 | } 53 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/peer_id.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.peer-id 2 | (:require 3 | [torrent-client.core.dispatch :as dispatch] 4 | [torrent-client.core.crypt :as crypt] 5 | [goog.string :as string] 6 | [goog.crypt :as gcrypt] 7 | [cljconsole.main :as console])) 8 | 9 | ; Create the unique peer-id atom 10 | ; nil is an invalid peer-id and should be switched 11 | ; on domready 12 | (def peer-id (atom nil)) 13 | 14 | (defn generate-peer-id [] 15 | "Generate a unique 20 byte peer-id" 16 | (let [; The client id identifies ampere 17 | client-id "AM1-0-0--" 18 | ; And a random string - note that this is 19 | ; partially based from the current date 20 | random-string (string/getRandomString) 21 | sha (gcrypt/byteArrayToHex (crypt/sha1 random-string))] 22 | (str client-id (subs sha 0 (- 20 (count client-id)))))) 23 | 24 | (dispatch/react-to #{:document-ready} (fn [_] 25 | (reset! peer-id (generate-peer-id)) 26 | (console/info "Setting peer-id as" @peer-id))) -------------------------------------------------------------------------------- /src-cljs/filesystem/writer.cljs: -------------------------------------------------------------------------------- 1 | (ns filesystem.writer 2 | (:use-macros [async.macros :only [let-async async]])) 3 | 4 | (defn- unbind-events! [writer] 5 | (aset writer "onerror" nil) 6 | (aset writer "onwriteend" nil)) 7 | 8 | (defn- bind-events! [writer success-callback error-callback] 9 | "When an event triggers, unbind it to prevent multiple firing" 10 | (aset writer "onwriteend" (juxt #(unbind-events! writer) success-callback)) 11 | (aset writer "onerror" (juxt #(unbind-events! writer) error-callback))) 12 | 13 | (defn truncate [writer length] 14 | (async [success-callback error-callback] 15 | (bind-events! writer success-callback error-callback) 16 | (.truncate writer length))) 17 | 18 | (defn write [writer data] 19 | (async [success-callback error-callback] 20 | (if (nil? data) 21 | (success-callback)) 22 | (when-not (nil? data) 23 | (bind-events! writer success-callback error-callback) 24 | (.write writer data)))) 25 | 26 | (defn seek [writer position] 27 | (.seek writer position)) -------------------------------------------------------------------------------- /src-cljs/filesystem/prefix.cljs: -------------------------------------------------------------------------------- 1 | (ns filesystem.prefix 2 | (:require [clojure.string :as string])) 3 | 4 | (defn prefix 5 | "Given the name of a feature and optional browser overrides 6 | provide a consistant interface for accessing it" 7 | [name & specifics] 8 | (let [; The official version and the vendor prefixes 9 | prefixes ["webkit" "moz" "ms"] 10 | ; Loop through each prefix, building a dictionary of them 11 | prefixes (into {} (map 12 | (juxt identity #(str % (string/capitalize name))) 13 | prefixes)) 14 | ; And add the actual w3spec version 15 | prefixes (assoc prefixes "" name) 16 | ; Override any prefixes given in specifics 17 | prefixes (vals (merge prefixes specifics)) 18 | ; Fetch the prefixes from the window 19 | prefixes (map #(aget js/window %) prefixes)] 20 | ; Return the first value thats not null 21 | (some #(if-not (nil? %) %) prefixes))) 22 | 23 | (def requestFileSystem 24 | (prefix "requestFileSystem")) -------------------------------------------------------------------------------- /src-cljs/torrent_client/core/reader.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.core.reader 2 | (:refer-clojure :exclude [rem]) 3 | (:use [torrent-client.core.byte-array :only [subarray]])) 4 | 5 | (defprotocol PushbackReader 6 | (read [reader] [reader length] "Returns the next char from the Reader, 7 | nil if the end of stream has been reached") 8 | (rem [reader] "Return the unread part of the array")) 9 | 10 | ; Using two atoms is less idomatic, but saves the repeat overhead of map creation 11 | (deftype ArrayPushbackReader [array index-atom] 12 | PushbackReader 13 | (read [reader] (read reader 1)) 14 | (read [_ length] 15 | (let [idx @index-atom 16 | length (or length 1) 17 | buffer-view (subarray array idx (+ idx length))] 18 | (swap! index-atom + length) 19 | ; If reading only 1 character return just the character 20 | ; otherwise return an array of characters 21 | (if (= length 1) 22 | (aget buffer-view 0) 23 | buffer-view))) 24 | (rem [_] (subarray array @index-atom))) 25 | 26 | (defn push-back-reader [array] 27 | "Creates an ArrayPushbackReader from a given array" 28 | (ArrayPushbackReader. array (atom 0))) -------------------------------------------------------------------------------- /resources/public/css/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin:0; 3 | padding:0; 4 | border:0; 5 | } 6 | 7 | body, div, span, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, code, 10 | del, dfn, em, img, q, dl, dt, dd, ol, ul, li, 11 | fieldset, form, label, legend, 12 | table, caption, tbody, tfoot, thead, tr, th, td, 13 | article, aside, dialog, figure, footer, header, 14 | hgroup, nav, section { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | font-weight: inherit; 19 | font-style: inherit; 20 | font-size: 100%; 21 | font-family: inherit; 22 | vertical-align: baseline; 23 | } 24 | 25 | article, aside, dialog, figure, footer, header, 26 | hgroup, nav, section { 27 | display:block; 28 | } 29 | 30 | body { 31 | line-height: 1.5; 32 | background: white; 33 | } 34 | 35 | table { 36 | border-collapse: separate; 37 | border-spacing: 0; 38 | } 39 | 40 | caption, th, td { 41 | text-align: left; 42 | font-weight: normal; 43 | float:none !important; 44 | } 45 | table, th, td { 46 | vertical-align: middle; 47 | } 48 | 49 | blockquote:before, blockquote:after, q:before, q:after { content: ''; } 50 | blockquote, q { quotes: "" ""; } 51 | 52 | a img { border: none; } 53 | 54 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/ui/jayq.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.ui.jayq) 2 | 3 | ; Wrappers around jquery methods 4 | 5 | (defn detach [$elem] 6 | (.detach $elem)) 7 | 8 | (defn append [$elem content] 9 | (if (sequential? content) 10 | ; TODO: Should be able to apply .append? 11 | (doseq [string content] 12 | (append $elem string)) 13 | (.append $elem content))) 14 | 15 | (defn param [obj] 16 | (.param js/jQuery (clj->js obj))) 17 | 18 | ; Bootstrap methods 19 | 20 | (defn modal [$elem params] 21 | (.modal $elem params)) 22 | 23 | (defn tab [$elem params] 24 | (.tab $elem params)) 25 | 26 | ; helpers with file objects 27 | 28 | (defn filelist-files [filelist] 29 | (let [filelist (.-files filelist)] 30 | (vec (for [k (js-keys filelist) 31 | :let [value (aget filelist k)] 32 | :when (identical? (type value) js/File)] 33 | value)))) 34 | 35 | (defn input-files [$elem] 36 | "Returns a vector of files for a given input" 37 | (filelist-files (first $elem))) 38 | 39 | (defn event-files 40 | "Takes a goog event and returns the files 41 | TODO: also get jquery events working" 42 | [event] 43 | (filelist-files (-> event .getBrowserEvent .-dataTransfer))) -------------------------------------------------------------------------------- /resources/public/less/thumbnails.less: -------------------------------------------------------------------------------- 1 | // THUMBNAILS 2 | // ---------- 3 | // Note: `.thumbnails` and `.thumbnails > li` are overriden in responsive files 4 | 5 | // Make wrapper ul behave like the grid 6 | .thumbnails { 7 | margin-left: -@gridGutterWidth; 8 | list-style: none; 9 | .clearfix(); 10 | } 11 | // Fluid rows have no left margin 12 | .row-fluid .thumbnails { 13 | margin-left: 0; 14 | } 15 | 16 | // Float li to make thumbnails appear in a row 17 | .thumbnails > li { 18 | float: left; // Explicity set the float since we don't require .span* classes 19 | margin-bottom: @baseLineHeight; 20 | margin-left: @gridGutterWidth; 21 | } 22 | 23 | // The actual thumbnail (can be `a` or `div`) 24 | .thumbnail { 25 | display: block; 26 | padding: 4px; 27 | line-height: 1; 28 | border: 1px solid #ddd; 29 | .border-radius(4px); 30 | .box-shadow(0 1px 1px rgba(0,0,0,.075)); 31 | } 32 | // Add a hover state for linked versions only 33 | a.thumbnail:hover { 34 | border-color: @linkColor; 35 | .box-shadow(0 1px 4px rgba(0,105,214,.25)); 36 | } 37 | 38 | // Images and captions 39 | .thumbnail > img { 40 | display: block; 41 | max-width: 100%; 42 | margin-left: auto; 43 | margin-right: auto; 44 | } 45 | .thumbnail .caption { 46 | padding: 9px; 47 | } 48 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/speed.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.speed 2 | ; (:use 3 | ; [torrent-client.torrents :only [torrents]] 4 | ; [goog :only [now]]) 5 | ; (:require 6 | ; [torrent-client.core.dispatch :as dispatch] 7 | ; [goog.date :as date]) 8 | ; ; (:use-macros [task.macros :only [deftask]]) 9 | ) 10 | 11 | 12 | ; (def download-pieces (atom {})) 13 | 14 | ; (dispatch/listen #{:receive-piece} (fn [_ [torrent piece]] 15 | ; ; Add this piece to list of latest pieces for this torrent 16 | ; (let [latest (@download-pieces (@torrent :pretty-info-hash))] 17 | ; (swap! @download-pieces (now))))) 18 | 19 | ; (deftask check-speed (* 0.5 1000) [_] 20 | ; (doseq [torrent-pieces download-pieces] 21 | ; (let [torrent (@torrents info-hash) 22 | ; five-seconds (date/Interval. (.-SECONDS date/Interval) 5) 23 | ; ; Only use results from the last five seconds 24 | ; latest (filter #(> (.add % five-seconds) (now)) torrent-pieces) 25 | ; ; Total bytes received over the five seconds averaged 26 | ; speed (/ (* (count latest ) 312740) 5)] 27 | ; ; Remove the older entries 28 | ; (swap! download-pieces info-hash latest) 29 | ; (dispatch/fire :download-speed [info-hash speed])))) 30 | 31 | (.log js/console "EOF") -------------------------------------------------------------------------------- /resources/public/less/responsive.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.4 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | 12 | // Responsive.less 13 | // For phone and tablet devices 14 | // ------------------------------------------------------------- 15 | 16 | 17 | // REPEAT VARIABLES & MIXINS 18 | // ------------------------- 19 | // Required since we compile the responsive stuff separately 20 | 21 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc 22 | @import "mixins.less"; 23 | 24 | 25 | // RESPONSIVE CLASSES 26 | // ------------------ 27 | 28 | @import "responsive-utilities.less"; 29 | 30 | 31 | // MEDIA QUERIES 32 | // ------------------ 33 | 34 | // Phones to portrait tablets and narrow desktops 35 | @import "responsive-767px-max.less"; 36 | 37 | // Tablets to regular desktops 38 | @import "responsive-768px-979px.less"; 39 | 40 | // Large desktops 41 | @import "responsive-1200px-min.less"; 42 | 43 | 44 | // RESPONSIVE NAVBAR 45 | // ------------------ 46 | 47 | // From 979px and below, show a button to toggle navbar contents 48 | @import "responsive-navbar.less"; 49 | -------------------------------------------------------------------------------- /resources/public/less/pagination.less: -------------------------------------------------------------------------------- 1 | // PAGINATION 2 | // ---------- 3 | 4 | .pagination { 5 | height: @baseLineHeight * 2; 6 | margin: @baseLineHeight 0; 7 | } 8 | .pagination ul { 9 | display: inline-block; 10 | .ie7-inline-block(); 11 | margin-left: 0; 12 | margin-bottom: 0; 13 | .border-radius(3px); 14 | .box-shadow(0 1px 2px rgba(0,0,0,.05)); 15 | } 16 | .pagination li { 17 | display: inline; 18 | } 19 | .pagination a { 20 | float: left; 21 | padding: 0 14px; 22 | line-height: (@baseLineHeight * 2) - 2; 23 | text-decoration: none; 24 | border: 1px solid #ddd; 25 | border-left-width: 0; 26 | } 27 | .pagination a:hover, 28 | .pagination .active a { 29 | background-color: #f5f5f5; 30 | } 31 | .pagination .active a { 32 | color: @grayLight; 33 | cursor: default; 34 | } 35 | .pagination .disabled span, 36 | .pagination .disabled a, 37 | .pagination .disabled a:hover { 38 | color: @grayLight; 39 | background-color: transparent; 40 | cursor: default; 41 | } 42 | .pagination li:first-child a { 43 | border-left-width: 1px; 44 | .border-radius(3px 0 0 3px); 45 | } 46 | .pagination li:last-child a { 47 | .border-radius(0 3px 3px 0); 48 | } 49 | 50 | // Centered 51 | .pagination-centered { 52 | text-align: center; 53 | } 54 | .pagination-right { 55 | text-align: right; 56 | } 57 | -------------------------------------------------------------------------------- /resources/public/less/alerts.less: -------------------------------------------------------------------------------- 1 | // ALERT STYLES 2 | // ------------ 3 | 4 | // Base alert styles 5 | .alert { 6 | padding: 8px 35px 8px 14px; 7 | margin-bottom: @baseLineHeight; 8 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 9 | background-color: @warningBackground; 10 | border: 1px solid @warningBorder; 11 | .border-radius(4px); 12 | color: @warningText; 13 | } 14 | .alert-heading { 15 | color: inherit; 16 | } 17 | 18 | // Adjust close link position 19 | .alert .close { 20 | position: relative; 21 | top: -2px; 22 | right: -21px; 23 | line-height: 18px; 24 | } 25 | 26 | // Alternate styles 27 | // ---------------- 28 | 29 | .alert-success { 30 | background-color: @successBackground; 31 | border-color: @successBorder; 32 | color: @successText; 33 | } 34 | .alert-danger, 35 | .alert-error { 36 | background-color: @errorBackground; 37 | border-color: @errorBorder; 38 | color: @errorText; 39 | } 40 | .alert-info { 41 | background-color: @infoBackground; 42 | border-color: @infoBorder; 43 | color: @infoText; 44 | } 45 | 46 | // Block alerts 47 | // ------------------------ 48 | .alert-block { 49 | padding-top: 14px; 50 | padding-bottom: 14px; 51 | } 52 | .alert-block > p, 53 | .alert-block > ul { 54 | margin-bottom: 0; 55 | } 56 | .alert-block p + p { 57 | margin-top: 5px; 58 | } 59 | -------------------------------------------------------------------------------- /resources/public/less/popovers.less: -------------------------------------------------------------------------------- 1 | // POPOVERS 2 | // -------- 3 | 4 | .popover { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | z-index: @zindexPopover; 9 | display: none; 10 | padding: 5px; 11 | &.top { margin-top: -5px; } 12 | &.right { margin-left: 5px; } 13 | &.bottom { margin-top: 5px; } 14 | &.left { margin-left: -5px; } 15 | &.top .arrow { #popoverArrow > .top(); } 16 | &.right .arrow { #popoverArrow > .right(); } 17 | &.bottom .arrow { #popoverArrow > .bottom(); } 18 | &.left .arrow { #popoverArrow > .left(); } 19 | .arrow { 20 | position: absolute; 21 | width: 0; 22 | height: 0; 23 | } 24 | } 25 | .popover-inner { 26 | padding: 3px; 27 | width: 280px; 28 | overflow: hidden; 29 | background: @black; // has to be full background declaration for IE fallback 30 | background: rgba(0,0,0,.8); 31 | .border-radius(6px); 32 | .box-shadow(0 3px 7px rgba(0,0,0,0.3)); 33 | } 34 | .popover-title { 35 | padding: 9px 15px; 36 | line-height: 1; 37 | background-color: #f5f5f5; 38 | border-bottom:1px solid #eee; 39 | .border-radius(3px 3px 0 0); 40 | } 41 | .popover-content { 42 | padding: 14px; 43 | background-color: @white; 44 | .border-radius(0 0 3px 3px); 45 | .background-clip(padding-box); 46 | p, ul, ol { 47 | margin-bottom: 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /resources/public/less/responsive-utilities.less: -------------------------------------------------------------------------------- 1 | // RESPONSIVE CLASSES 2 | // ------------------ 3 | 4 | // Hide from screenreaders and browsers 5 | // Credit: HTML5 Boilerplate 6 | .hidden { 7 | display: none; 8 | visibility: hidden; 9 | } 10 | 11 | // Visibility utilities 12 | 13 | // For desktops 14 | .visible-phone { display: none !important; } 15 | .visible-tablet { display: none !important; } 16 | .visible-desktop { } // Don't set initially 17 | .hidden-phone { } 18 | .hidden-tablet { } 19 | .hidden-desktop { display: none !important; } 20 | 21 | // Phones only 22 | @media (max-width: 767px) { 23 | // Show 24 | .visible-phone { display: inherit !important; } // Use inherit to restore previous behavior 25 | // Hide 26 | .hidden-phone { display: none !important; } 27 | // Hide everything else 28 | .hidden-desktop { display: inherit !important; } 29 | .visible-desktop { display: none !important; } 30 | } 31 | 32 | // Tablets & small desktops only 33 | @media (min-width: 768px) and (max-width: 979px) { 34 | // Show 35 | .visible-tablet { display: inherit !important; } 36 | // Hide 37 | .hidden-tablet { display: none !important; } 38 | // Hide everything else 39 | .hidden-desktop { display: inherit !important; } 40 | .visible-desktop { display: none !important ; } 41 | } 42 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/browser/prefix.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.browser.prefix 2 | (:require [clojure.string :as string])) 3 | 4 | (defn prefix 5 | "Given the name of a feature and optional browser overrides 6 | provide a consistant interface for accessing it" 7 | ([name] (prefix js/window name)) 8 | ([source name & specifics] 9 | (let [; The official version and the vendor prefixes 10 | prefixes ["webkit" "moz" "ms"] 11 | ; A capitilized version 12 | upper-name (str (string/upper-case (subs name 0 1)) 13 | (subs name 1)) 14 | ; Loop through each prefix, building a dictionary of them 15 | prefixes (into {} 16 | (map (juxt identity #(str % upper-name)) prefixes)) 17 | ; And add the actual w3spec version 18 | prefixes (assoc prefixes "" name) 19 | ; Override any prefixes given in specifics 20 | prefixes (vals (merge prefixes specifics)) 21 | ; Fetch the prefixes from the window 22 | prefixes (map #(aget source %) prefixes)] 23 | ; Return the first value thats not null 24 | (some #(if-not (nil? %) %) prefixes)))) 25 | 26 | (def indexedDB 27 | (prefix "indexedDB")) 28 | 29 | (def RTCPeerConnection 30 | (prefix "RTCPeerConnection")) 31 | 32 | (def PersistentStorage 33 | (prefix js/navigator "PersistentStorage")) -------------------------------------------------------------------------------- /resources/public/less/code.less: -------------------------------------------------------------------------------- 1 | // Code.less 2 | // Code typography styles for the and
 elements
 3 | // --------------------------------------------------------
 4 | 
 5 | // Inline and block code styles
 6 | code,
 7 | pre {
 8 |   padding: 0 3px 2px;
 9 |   #font > #family > .monospace;
10 |   font-size: @baseFontSize - 1;
11 |   color: @grayDark;
12 |   .border-radius(3px);
13 | }
14 | 
15 | // Inline code
16 | code {
17 |   padding: 2px 4px;
18 |   color: #d14;
19 |   background-color: #f7f7f9;
20 |   border: 1px solid #e1e1e8;
21 | }
22 | 
23 | // Blocks of code
24 | pre {
25 |   display: block;
26 |   padding: (@baseLineHeight - 1) / 2;
27 |   margin: 0 0 @baseLineHeight / 2;
28 |   font-size: @baseFontSize * .925; // 13px to 12px
29 |   line-height: @baseLineHeight;
30 |   word-break: break-all;
31 |   word-wrap: break-word;
32 |   white-space: pre;
33 |   white-space: pre-wrap;
34 |   background-color: #f5f5f5;
35 |   border: 1px solid #ccc; // fallback for IE7-8
36 |   border: 1px solid rgba(0,0,0,.15);
37 |   .border-radius(4px);
38 | 
39 |   // Make prettyprint styles more spaced out for readability
40 |   &.prettyprint {
41 |     margin-bottom: @baseLineHeight;
42 |   }
43 | 
44 |   // Account for some code outputs that place code tags in pre tags
45 |   code {
46 |     padding: 0;
47 |     color: inherit;
48 |     background-color: transparent;
49 |     border: 0;
50 |   }
51 | }
52 | 
53 | // Enable scrollable blocks of code
54 | .pre-scrollable {
55 |   max-height: 340px;
56 |   overflow-y: scroll;
57 | }


--------------------------------------------------------------------------------
/resources/public/js/monkey.js:
--------------------------------------------------------------------------------
 1 | // http://code.google.com/p/closure-library/source/detail?r=2045
 2 | goog.db.ObjectStore.prototype.openCursor = function(opt_range, opt_direction) {
 3 |   var msg = 'opening cursor ' + this.getName();
 4 |   var cursor = new goog.db.Cursor();
 5 |   var request;
 6 | 
 7 |   try {
 8 |     var range = opt_range ? opt_range.range_ : null;
 9 |     if (opt_direction) {
10 |       request = this.store_.openCursor(range, opt_direction);
11 |     } else {
12 |       request = this.store_.openCursor(range);
13 |     }
14 |   } catch (err) {
15 |     throw new goog.db.Error(err.code, msg);
16 |   }
17 |   request.onsuccess = function(ev) {
18 |     cursor.cursor_ = ev.target.result || null;
19 |     if (cursor.cursor_) {
20 |       cursor.dispatchEvent(goog.db.Cursor.EventType.NEW_DATA);
21 |     } else {
22 |       cursor.dispatchEvent(goog.db.Cursor.EventType.COMPLETE);
23 |     }
24 |   };
25 |   request.onerror = function(ev) {
26 |     cursor.dispatchEvent(goog.db.Cursor.EventType.ERROR);
27 |   };
28 |   return cursor;
29 | };
30 | 
31 | /**
32 |  * Converts a hex string into an integer array.
33 |  * @param {string} hexString Hex string of 16-bit integers (two characters
34 |  *     per integer).
35 |  * @return {!Array.} Array of {0,255} integers for the given string.
36 |  */
37 | goog.crypt.hexToByteArray = function(hexString) {
38 |   goog.asserts.assert(hexString.length % 2 == 0,
39 |                       'Key string length must be multiple of 2');
40 |   var arr = [];
41 |   for (var i = 0; i < hexString.length; i += 2) {
42 |     arr.push(parseInt(hexString.substring(i, i + 2), 16));
43 |   }
44 |   return arr;
45 | };
46 | 


--------------------------------------------------------------------------------
/src-cljs/torrent_client/torrents.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.torrents
 2 |   (:require 
 3 |     [torrent-client.core.dispatch :as dispatch]
 4 |     [cljconsole.main :as console]))
 5 | 
 6 | ; A vector of the torrents currently in use
 7 | ; the key is the pretty-info-hash
 8 | (def torrents (atom {}))
 9 | 
10 | (defn- start-torrent [metadata]
11 |   (let [torrent (atom metadata)]
12 |     (swap! torrents assoc (@torrent :pretty-info-hash) torrent)
13 |     (dispatch/fire :started-torrent torrent)))
14 | 
15 | (defn- update-torrent [torrent metadata]
16 |   (swap! torrent merge metadata)
17 |   (dispatch/fire :updated-torrent torrent))
18 | 
19 | ; When metadata is processed turn it into an atom and track it
20 | (dispatch/react-to #{:processed-metadata} (fn [_ metadata]
21 |   (console/info "Adding torrent to atom:" metadata)
22 |   (if-let [existing (@torrents (metadata :pretty-info-hash))]
23 |     (if (@existing :pieces-hash)
24 |       ; If we have all the metadata why would the user give us more
25 |       (dispatch/fire :duplicate-torrent existing)
26 |       ; If we receieve extra data on the torrent
27 |       (update-torrent existing metadata))
28 |     (start-torrent metadata))))
29 | 
30 | (dispatch/react-to #{:written-piece} (fn [_ [torrent _]]
31 |   (let [pieces-written (inc (or (@torrent :pieces-written) 0))]
32 |     (swap! torrent assoc :pieces-written pieces-written)
33 |     ; If this torrent has all it's pieces mark as such
34 |     (if (= pieces-written (@torrent :pieces-length))
35 |       (dispatch/fire :completed-torrent torrent)))))
36 | 
37 | (dispatch/react-to #{:update-metadata} (fn [_ [torrent metadata]]
38 |   (swap! torrent merge metadata)))


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/string.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.core.string
 2 |   (:require [goog.string :as gstring]))
 3 | 
 4 | (defn partition-string [n string]
 5 |   (let [length (count string)]
 6 |     (map 
 7 |       #(subs string % (min (+ % 20) length))
 8 |       (range 0 length 20))))
 9 | 
10 | (defn- pad-string [string padding length]
11 |   "return the padding needed to bring a string to a length"
12 |   ; Only preceed is padding is needed
13 |   (if (> (count string) length)
14 |     (let [padding-length (- length (count string))
15 |         ; how many times should the padding be repeated
16 |         padding-repeat (/ padding-length (count padding))
17 |         padding (str (take padding-repeat (repeat padding)))]
18 |       (subs padding 0 padding-length))))
19 | 
20 | (defn pad-string-left [string padding length]
21 |   (str string (pad-string string padding length)))
22 | 
23 | (defn pad-string-right [string padding length]
24 |   (str (pad-string string padding length) string))
25 | 
26 | (defn- a2b-part [part]
27 |     (let [part (clojure.string/join part)
28 |           byte (js/parseInt part 16)]
29 |       (if (or (and (>= byte 65) (<= byte 90)) ; A-Z
30 |               (and (>= byte 97) (<= byte 122)) ; a-z
31 |               (and (>= byte 48) (<= byte 57)) ; 0-9
32 |               (= byte 45) (= byte 95) (= byte 46) (= byte 126))
33 |           (.fromCharCode js/String byte)
34 |           (str "%" part))))
35 | 
36 | (defn a2b-hex [string]
37 |   (.log js/console "a2b-hex" string (clojure.string/join (map a2b-part (partition 2 string))))
38 |   (clojure.string/join (map a2b-part (partition 2 string))))
39 | 
40 | (defn camel-case [s] (gstring/toCamelCase s))
41 | (defn selector-case [s] (gstring/toSelectorCase s))


--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
 1 | (defproject torrent-client "0.1.0-SNAPSHOT"
 2 |   :description "A webrtc bittorrent client written in clojurescript"
 3 |   :url "http://github.com/hcliff/ampere"
 4 |   :dependencies [[org.clojure/clojure "1.4.0"]
 5 |                  [noir "1.3.0-beta2"]
 6 |                  [environ "0.3.0"]
 7 |                  [jayq "2.0.0"]
 8 |                  [crate "0.2.4"]]
 9 |   :git-dependencies [["https://github.com/hcliff/waltz.git"]]
10 |   :hooks [environ.leiningen.hooks]
11 |   :plugins [[environ/environ.lein "0.3.0"]
12 |             [lein-exec "0.2.1"]
13 |             [lein-cljsbuild "0.3.0"]
14 |             [lein-git-deps "0.0.1-SNAPSHOT"]]
15 |   :source-paths ["src-clj"]
16 |   :main torrent-client.server
17 |   :cljsbuild {
18 |     :builds {
19 |       :main {
20 |         ; The path to the top-level ClojureScript source directory:
21 |         :source-paths ["src-cljs", 
22 |                        ".lein-git-deps/waltz/src/"
23 |                        ; https://github.com/mmcgrana/clj-stacktrace/issues/19
24 |                        ; ".lein-git-deps/clj-stacktrace/src/"
25 |                        ]
26 |         ; The standard ClojureScript compiler options:
27 |         ; (See the ClojureScript compiler documentation for details.)
28 |         :compiler {
29 |           :output-to "resources/public/cljs/bootstrap.js"
30 |           :optimizations :whitespace
31 |           :pretty-print true}}}
32 |     :repl-listen-port 9000
33 |     :repl-launch-commands{
34 |       "firefox" ["firefox"
35 |                   :stdout ".repl-firefox-out"
36 |                   :stderr ".repl-firefox-err"]
37 |       "chrome"  ["/opt/google/chrome/chrome" "http://localhost:8091/"
38 |                   :stdout ".repl-chrome-out"
39 |                   :stderr ".repl-chrome-err"]}})


--------------------------------------------------------------------------------
/resources/public/less/labels-badges.less:
--------------------------------------------------------------------------------
 1 | // LABELS & BADGES
 2 | // ---------------
 3 | 
 4 | // Base classes
 5 | .label,
 6 | .badge {
 7 |   font-size: @baseFontSize * .846;
 8 |   font-weight: bold;
 9 |   line-height: 14px; // ensure proper line-height if floated
10 |   color: @white;
11 |   vertical-align: baseline;
12 |   white-space: nowrap;
13 |   text-shadow: 0 -1px 0 rgba(0,0,0,.25);
14 |   background-color: @grayLight;
15 | }
16 | // Set unique padding and border-radii
17 | .label {
18 |   padding: 1px 4px 2px;
19 |   .border-radius(3px);
20 | }
21 | .badge {
22 |   padding: 1px 9px 2px;
23 |   .border-radius(9px);
24 | }
25 | 
26 | // Hover state, but only for links
27 | a {
28 |   &.label:hover,
29 |   &.badge:hover {
30 |     color: @white;
31 |     text-decoration: none;
32 |     cursor: pointer;
33 |   }
34 | }
35 | 
36 | // Colors
37 | // Only give background-color difference to links (and to simplify, we don't qualifty with `a` but [href] attribute)
38 | .label,
39 | .badge {
40 |   // Important (red)
41 |   &-important         { background-color: @errorText; }
42 |   &-important[href]   { background-color: darken(@errorText, 10%); }
43 |   // Warnings (orange)
44 |   &-warning           { background-color: @orange; }
45 |   &-warning[href]     { background-color: darken(@orange, 10%); }
46 |   // Success (green)
47 |   &-success           { background-color: @successText; }
48 |   &-success[href]     { background-color: darken(@successText, 10%); }
49 |   // Info (turquoise)
50 |   &-info              { background-color: @infoText; }
51 |   &-info[href]        { background-color: darken(@infoText, 10%); }
52 |   // Inverse (black)
53 |   &-inverse           { background-color: @grayDark; }
54 |   &-inverse[href]     { background-color: darken(@grayDark, 10%); }
55 | }
56 | 


--------------------------------------------------------------------------------
/resources/public/less/bootstrap.less:
--------------------------------------------------------------------------------
 1 | /*!
 2 |  * Bootstrap v2.0.4
 3 |  *
 4 |  * Copyright 2012 Twitter, Inc
 5 |  * Licensed under the Apache License v2.0
 6 |  * http://www.apache.org/licenses/LICENSE-2.0
 7 |  *
 8 |  * Designed and built with all the love in the world @twitter by @mdo and @fat.
 9 |  */
10 | 
11 | // CSS Reset
12 | @import "reset.less";
13 | 
14 | // Core variables and mixins
15 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc
16 | @import "mixins.less";
17 | 
18 | // Grid system and page structure
19 | @import "scaffolding.less";
20 | @import "grid.less";
21 | @import "layouts.less";
22 | 
23 | // Base CSS
24 | @import "type.less";
25 | @import "code.less";
26 | @import "forms.less";
27 | @import "tables.less";
28 | 
29 | // Components: common
30 | @import "sprites.less";
31 | @import "dropdowns.less";
32 | @import "wells.less";
33 | @import "component-animations.less";
34 | @import "close.less";
35 | 
36 | // Components: Buttons & Alerts
37 | @import "buttons.less";
38 | @import "button-groups.less";
39 | @import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less
40 | 
41 | // Components: Nav
42 | @import "navs.less";
43 | @import "navbar.less";
44 | @import "breadcrumbs.less";
45 | @import "pagination.less";
46 | @import "pager.less";
47 | 
48 | // Components: Popovers
49 | @import "modals.less";
50 | @import "tooltip.less";
51 | @import "popovers.less";
52 | 
53 | // Components: Misc
54 | @import "thumbnails.less";
55 | @import "labels-badges.less";
56 | @import "progress-bars.less";
57 | @import "accordion.less";
58 | @import "carousel.less";
59 | @import "hero-unit.less";
60 | 
61 | @import "main.less";// our site specific stylesheets
62 | 
63 | // Utility classes
64 | @import "utilities.less"; // Has to be last to override when necessary
65 | 


--------------------------------------------------------------------------------
/src-cljs/async/helpers.cljs:
--------------------------------------------------------------------------------
 1 | (ns async.helpers
 2 |   (:use-macros [async.macros :only [async let-async]]))
 3 | 
 4 | ; NOTES
 5 | ; use of futures would be nice, this would be ideal
 6 | ; (from get-partial)
 7 | ;
 8 | ; (defn reader [file]
 9 | ; ASYNC WEBWORKER READS FILE
10 | ; somehow keep blocking until the async returns?
11 | ; )
12 | ;
13 | ; (let [readers (pmap (comp future reader) files)]
14 | ;   (doseq [file files
15 |             ; reader readers]
16 | ; )
17 | 
18 | ; take multiple collections 
19 | (defn- map-indexed* [f & collections]
20 |   ; apply the fn with i as the first argument 
21 |   ; and the coll values as subsequent
22 |   (let [f* #(apply f %1 %2)
23 |         ; c1 [:one :two :three] c2 [:uno :dos :tres] =>
24 |         ; c [[:one :uno] [:two :dos] [:three :tres]]
25 |         c (apply (partial map vector) collections)]
26 |     (map-indexed f* c)))
27 | 
28 | (defn map-async 
29 |   "Given an async function and collection(s); apply the
30 |   function to every collection(s) item and when all are done
31 |   return the result"
32 |   [f & collections]
33 |   (async [success-callback]
34 |     (let [results (atom {})]
35 |       (letfn [(success [i data]
36 |                 ; preserve results order by using index
37 |                 (swap! results assoc i data)
38 |                 ; when
39 |                 (if (= (count @results) (count (first collections)))
40 |                   (success-callback (vals @results))))
41 |               (f* [i & arguments]
42 |                 ; run the provided function with the collections as params
43 |                 ; execute success function with index and result
44 |                 ((apply f arguments) (partial success i)))]
45 |         (doall (apply map-indexed* f* collections))))))
46 | 
47 | (defn sleep [period]
48 |   (async [success-callback]
49 |     (.setTimeout js/window success-callback period)))


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/byte_array.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.core.byte-array
 2 |   (:require 
 3 |     [goog.crypt :as crypt]))
 4 | 
 5 | (defprotocol SubArray
 6 |   (subarray [array start] [array start finish] "Return a subarray of the array immediately"))
 7 | 
 8 | (extend-type js/Uint8Array
 9 | 
10 |   ISeqable
11 |   (-seq [array] (array-seq array 0))
12 | 
13 |   ICounted
14 |   (-count [a] (alength a))
15 | 
16 |   IIndexed
17 |   (-nth
18 |     ([array n]
19 |        (if (< n (alength array)) (aget array n)))
20 |     ([array n not-found]
21 |        (if (< n (alength array)) (aget array n)
22 |            not-found)))
23 | 
24 |   ILookup
25 |   (-lookup
26 |     ([array k]
27 |        (aget array k))
28 |     ([array k not-found]
29 |        (-nth array k not-found)))
30 | 
31 |   IReduce
32 |   (-reduce
33 |     ([array f]
34 |        (ci-reduce array f))
35 |     ([array f start]
36 |        (ci-reduce array f start)))
37 | 
38 |   SubArray
39 |   (subarray [array start]
40 |     (.subarray array start))
41 |   (subarray [array start finish]
42 |     (.subarray array start finish))
43 | 
44 |   ICounted
45 |   (-count [array] (.-length array))
46 | 
47 |   )
48 | 
49 | ; H.C Object on type gives error?
50 | (set! js/Uint8Array.prototype.toString (fn []
51 |   (this-as self
52 |     (crypt/byteArrayToString self))))
53 | 
54 | (defn uint8-array
55 |   "Build a new byte array"
56 |   ; x can be length or array buffer
57 |   ([x] (js/Uint8Array. x))
58 |   ([array-buffer offset] (js/Uint8Array. array-buffer offset))
59 |   ([array-buffer offset length] (js/Uint8Array. array-buffer offset length)))
60 | 
61 | (defn subarray 
62 |   ([coll start]
63 |     (.subarray coll start))
64 |   ([coll start finish]
65 |     (.subarray coll start finish)))
66 | 
67 | (defn ^boolean uint8-array? [candidate]
68 |   ; Incorrect; but js/ArrayBufferView doesn't exist
69 |   (instance? js/Uint8Array candidate))


--------------------------------------------------------------------------------
/src-cljs/filesystem/filesystem.cljs:
--------------------------------------------------------------------------------
 1 | (ns filesystem.filesystem
 2 |   (:require [filesystem.prefix :as prefix])
 3 |   (:use-macros [async.macros :only [let-async async]]))
 4 | 
 5 | (defn request-file-system [type size]
 6 |   (async [success-callback error-callback]
 7 |     (.webkitRequestFileSystem js/window 
 8 |                               (aget js/window (name type)) 
 9 |                               size 
10 |                               success-callback 
11 |                               error-callback)))
12 | 
13 | (defn request-quota [type size]
14 |   (async [success-callback error-callback]
15 |     (if-let [storage (.-webkitPersistentStorage js/navigator)]
16 |       ; Chrome 27 and up
17 |       (.requestQuota storage size success-callback error-callback)
18 |       ; Chrome 26
19 |       (.requestQuota (.-webkitStorageInfo js/window)
20 |                      (aget js/window (name type))
21 |                      size
22 |                      success-callback
23 |                      error-callback))))
24 | 
25 | (defn request-quota-then-filesystem [type size]
26 |   "A combination of the above two functions, if request-quota succeeds,
27 |   then call request-file-system"
28 |   (let-async [granted-bytes (request-quota type size)]
29 |     (request-file-system type granted-bytes)))
30 | 
31 | (defn filereader [obj]
32 |   (async [success-callback error-callback]
33 |     (let [reader (js/FileReader.)
34 |           ; onloadend actually triggers a progress event
35 |           ; we want the actual file contents
36 |           success-callback #(success-callback (-> % .-currentTarget .-result))]
37 |       (set! (.-onerror reader) error-callback)
38 |       (set! (.-onloadend reader) success-callback)
39 |       (.readAsArrayBuffer reader obj))))
40 | 
41 | (defn file? [obj] (instance? js/File obj))
42 | 
43 | (defn slice [file offset length]
44 |   {:pre [(file? file)]}
45 |   (.slice file offset (+ offset length)))


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/queue.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.core.queue
 2 |   (:use [torrent-client.core.incubator :only [dissoc-in]])
 3 |   (:refer-clojure :exclude [conj! disj! contains? dissoc!])
 4 |   (:require [cljconsole.main :as console]))
 5 | 
 6 | ; A wrapper around items we want to queue to allow seperate meta handling
 7 | ; this allows for queuing of strings and numbers
 8 | (deftype QueueEntry [obj _meta]
 9 |   Object
10 |   (toString [_] obj)
11 |   IEquiv
12 |   (-equiv [_ other]
13 |     (identical? obj other))
14 | 
15 |   IMeta
16 |   (-meta [_] _meta)
17 |   IWithMeta
18 |   (-with-meta [_ new-meta] (QueueEntry. obj new-meta)))
19 | 
20 | (defn queue-entry [obj]
21 |   "Provide metadata wrapping for objects without it"
22 |   (QueueEntry. obj nil))
23 | 
24 | (defn build-entry [obj]
25 |   "If metadata can't nativly be attached to the object wrap it in a queueobject"
26 |   (if ((some-fn string? number?) obj)
27 |     (queue-entry obj)
28 |     obj))
29 | 
30 | (defn with-data []
31 |   {:added (.getTime (js/Date.))
32 |    :ack false})
33 | 
34 | (defn contains? [queues k1 k2]
35 |   ; Watch the order here, use QueueEntry's IEquiv, not k2s
36 |   (some #(= % k2) (@queues k1)))
37 | 
38 | (defn dissoc! [queues & [keys]]
39 |   (swap! queues dissoc-in keys))
40 | 
41 | (defn- conj-set [c args]
42 |   "Create a set or conjoin to one
43 |   update-in  ... conj would result in a list (which has no disj support)"
44 |   (if c
45 |     (conj c args)
46 |     #{args}))
47 | 
48 | (defn- hash-key [k]
49 |   (if-not ((some-fn string? number?) k)
50 |     (hash k)
51 |     k))
52 | 
53 | (defn conj! [queues k o]
54 |   "Marks that we have started fetching a piece"
55 |   (let [o (build-entry o)
56 |         entry (with-meta o (with-data))]
57 |     (swap! queues update-in [k] conj-set entry)))
58 | 
59 | (defn disj! [queues k1 k2]
60 |   (let [items (filter #(= % k2) (@queues k1))]
61 |     (apply swap! queues update-in [k1] disj items)))
62 | 
63 | (defn expired? [life object]
64 |   "Has an object been around for longer than it is permitted"
65 |   (let [max-life (- (.getTime (js/Date.)) life)]
66 |     (-> object meta :added (partial >= life))))
67 | 
68 | (defn expired [queues life]
69 |   "Given a collection of queues, return the expired pieces"
70 |   (letfn [(f [[k1 c]]
71 |             [k1 (filter (partial expired? life) c)])]
72 |     (map f @queues)))


--------------------------------------------------------------------------------
/resources/public/js/humane.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Javascript Humane Dates
 3 |  * Copyright (c) 2008 Dean Landolt (deanlandolt.com)
 4 |  * Re-write by Zach Leatherman (zachleat.com)
 5 |  * 
 6 |  * Adopted from the John Resig's pretty.js
 7 |  * at http://ejohn.org/blog/javascript-pretty-date
 8 |  * and henrah's proposed modification 
 9 |  * at http://ejohn.org/blog/javascript-pretty-date/#comment-297458
10 |  * 
11 |  * Licensed under the MIT license.
12 |  */
13 | 
14 | function humane_date(date_str){
15 | 	var time_formats = [
16 | 		[60, 'seconds'],
17 | 		[90, '1 minute'], // 60*1.5
18 | 		[3600, 'minutes', 60], // 60*60, 60
19 | 		[5400, '1 hour'], // 60*60*1.5
20 | 		[86400, 'hours', 3600], // 60*60*24, 60*60
21 | 		[129600, '1 day'], // 60*60*24*1.5
22 | 		[604800, 'days', 86400], // 60*60*24*7, 60*60*24
23 | 		[907200, '1 week'], // 60*60*24*7*1.5
24 | 		[2628000, 'weeks', 604800], // 60*60*24*(365/12), 60*60*24*7
25 | 		[3942000, '1 month'], // 60*60*24*(365/12)*1.5
26 | 		[31536000, 'months', 2628000], // 60*60*24*365, 60*60*24*(365/12)
27 | 		[47304000, '1 year'], // 60*60*24*365*1.5
28 | 		[3153600000, 'years', 31536000], // 60*60*24*365*100, 60*60*24*365
29 | 		[4730400000, '1 century'], // 60*60*24*365*100*1.5
30 | 	];
31 | 
32 | 	var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," "),
33 | 		dt = new Date,
34 | 		seconds = ((dt - new Date(time) + (dt.getTimezoneOffset() * 60000)) / 1000),
35 | 		token = ' ago',
36 | 		i = 0,
37 | 		format;
38 | 
39 | 	if (seconds < 0) {
40 | 		seconds = Math.abs(seconds);
41 | 		token = '';
42 | 	}
43 | 
44 | 	while (format = time_formats[i++]) {
45 | 		if (seconds < format[0]) {
46 | 			if (format.length == 2) {
47 | 				return format[1] + (i > 1 ? token : ''); // Conditional so we don't return Just Now Ago
48 | 			} else {
49 | 				return Math.round(seconds / format[2]) + ' ' + format[1] + (i > 1 ? token : '');
50 | 			}
51 | 		}
52 | 	}
53 | 
54 | 	// overflow for centuries
55 | 	if(seconds > 4730400000)
56 | 		return Math.round(seconds / 4730400000) + ' Centuries' + token;
57 | 
58 | 	return date_str;
59 | };
60 | 
61 | if(typeof jQuery != 'undefined') {
62 | 	jQuery.fn.humane_dates = function(){
63 | 		return this.each(function(){
64 | 			var date = humane_date(this.title);
65 | 			if(date && jQuery(this).text() != date) // don't modify the dom if we don't have to
66 | 				jQuery(this).text(date);
67 | 		});
68 | 	};
69 | }


--------------------------------------------------------------------------------
/src-cljs/torrent_client/storage.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.storage
 2 | 	(:require
 3 |     [torrent-client.core.dispatch :as dispatch]
 4 |     [torrent-client.core.db :as db]
 5 |     [cljconsole.main :as console])
 6 |   (:use-macros [async.macros :only [let-async]]))
 7 | 
 8 | ; An atom other db dependant files can use for db connection
 9 | (def connection (atom nil))
10 | 
11 | ; A list of the objectstores available to us
12 | ; this is useful for managing versioning
13 | (def object-stores 
14 |   "All the object stores ampere wll use
15 |    NOTE: key-path cannot have dashes"
16 |   [{:name "metainfo"}])
17 | 
18 | ; NOTE: edge case on magnet links, they will try to add before the db is ready
19 | 
20 | ;;************************************************
21 | ;; DB setup and initial data pull
22 | ;;************************************************
23 | 
24 | (dispatch/react-to #{:document-ready} (fn []
25 |   (let-async [database (db/open-database "torrent-client" 1 object-stores)]
26 |     (console/info "Opened indexeddb")
27 |     ; Swap out the atom with our db connection
28 |     (reset! connection database)
29 |     (let [transaction (db/create-transaction database ["metainfo"] "readonly")
30 |           object-store (.objectStore transaction "metainfo")
31 |           ; And finally fetch all our data from the metainfo objectStore
32 |           objects (.getAll object-store)]
33 |       ; When the entries are loaded from the db add them
34 |       (.addCallback objects (fn [torrents]
35 |         (doseq [torrent (js->clj torrents :keywordize-keys true)]
36 |           (dispatch/fire :add-metainfo-db torrent))))))))
37 | 
38 | ;;************************************************
39 | ;; Maintaining torrent state
40 | ;;************************************************
41 | 
42 | (defn- write-metadata-to-db [metainfo]
43 |   "We can function without indexeddb, so don't crash if there's no connection"
44 |   (if @connection
45 |     (let [transaction (db/create-transaction @connection ["metainfo"] "readwrite")
46 |           object-store (.objectStore transaction "metainfo")]
47 |       (assoc! object-store (metainfo :pretty-info-hash) metainfo))))
48 | 
49 | (dispatch/react-to #{:started-torrent} (fn [_ torrent]
50 |   "When a torrent is started we should set up a handler in the db"
51 |   (add-watch torrent :update-db (fn [_ _ _ new-metainfo]
52 |     (write-metadata-to-db new-metainfo)))
53 |   (swap! torrent identity)))


--------------------------------------------------------------------------------
/resources/public/less/modals.less:
--------------------------------------------------------------------------------
 1 | // MODALS
 2 | // ------
 3 | 
 4 | // Recalculate z-index where appropriate
 5 | .modal-open {
 6 |   .dropdown-menu {  z-index: @zindexDropdown + @zindexModal; }
 7 |   .dropdown.open { *z-index: @zindexDropdown + @zindexModal; }
 8 |   .popover       {  z-index: @zindexPopover  + @zindexModal; }
 9 |   .tooltip       {  z-index: @zindexTooltip  + @zindexModal; }
10 | }
11 | 
12 | // Background
13 | .modal-backdrop {
14 |   position: fixed;
15 |   top: 0;
16 |   right: 0;
17 |   bottom: 0;
18 |   left: 0;
19 |   z-index: @zindexModalBackdrop;
20 |   background-color: @black;
21 |   // Fade for backdrop
22 |   &.fade { opacity: 0; }
23 | }
24 | 
25 | .modal-backdrop,
26 | .modal-backdrop.fade.in {
27 |   .opacity(80);
28 | }
29 | 
30 | // Base modal
31 | .modal {
32 |   position: fixed;
33 |   top: 50%;
34 |   left: 50%;
35 |   z-index: @zindexModal;
36 |   overflow: auto;
37 |   width: 560px;
38 |   margin: -250px 0 0 -280px;
39 |   background-color: @white;
40 |   border: 1px solid #999;
41 |   border: 1px solid rgba(0,0,0,.3);
42 |   *border: 1px solid #999; /* IE6-7 */
43 |   .border-radius(6px);
44 |   .box-shadow(0 3px 7px rgba(0,0,0,0.3));
45 |   .background-clip(padding-box);
46 |   &.fade {
47 |     .transition(e('opacity .3s linear, top .3s ease-out'));
48 |     top: -25%;
49 |   }
50 |   &.fade.in { top: 50%; }
51 | }
52 | .modal-header {
53 |   padding: 9px 15px;
54 |   border-bottom: 1px solid #eee;
55 |   // Close icon
56 |   .close { margin-top: 2px; }
57 | }
58 | 
59 | // Body (where all modal content resides)
60 | .modal-body {
61 |   overflow-y: auto;
62 |   max-height: 400px;
63 |   padding: 15px;
64 | }
65 | // Remove bottom margin if need be
66 | .modal-form {
67 |   margin-bottom: 0;
68 | }
69 | 
70 | // Footer (for actions)
71 | .modal-footer {
72 |   padding: 14px 15px 15px;
73 |   margin-bottom: 0;
74 |   text-align: right; // right align buttons
75 |   background-color: #f5f5f5;
76 |   border-top: 1px solid #ddd;
77 |   .border-radius(0 0 6px 6px);
78 |   .box-shadow(inset 0 1px 0 @white);
79 |   .clearfix(); // clear it in case folks use .pull-* classes on buttons
80 | 
81 |   // Properly space out buttons
82 |   .btn + .btn {
83 |     margin-left: 5px;
84 |     margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
85 |   }
86 |   // but override that for button groups
87 |   .btn-group .btn + .btn {
88 |     margin-left: -1px;
89 |   }
90 | }
91 | 


--------------------------------------------------------------------------------
/resources/public/less/carousel.less:
--------------------------------------------------------------------------------
  1 | // CAROUSEL
  2 | // --------
  3 | 
  4 | .carousel {
  5 |   position: relative;
  6 |   margin-bottom: @baseLineHeight;
  7 |   line-height: 1;
  8 | }
  9 | 
 10 | .carousel-inner {
 11 |   overflow: hidden;
 12 |   width: 100%;
 13 |   position: relative;
 14 | }
 15 | 
 16 | .carousel {
 17 | 
 18 |   .item {
 19 |     display: none;
 20 |     position: relative;
 21 |     .transition(.6s ease-in-out left);
 22 |   }
 23 | 
 24 |   // Account for jankitude on images
 25 |   .item > img {
 26 |     display: block;
 27 |     line-height: 1;
 28 |   }
 29 | 
 30 |   .active,
 31 |   .next,
 32 |   .prev { display: block; }
 33 | 
 34 |   .active {
 35 |     left: 0;
 36 |   }
 37 | 
 38 |   .next,
 39 |   .prev {
 40 |     position: absolute;
 41 |     top: 0;
 42 |     width: 100%;
 43 |   }
 44 | 
 45 |   .next {
 46 |     left: 100%;
 47 |   }
 48 |   .prev {
 49 |     left: -100%;
 50 |   }
 51 |   .next.left,
 52 |   .prev.right {
 53 |     left: 0;
 54 |   }
 55 | 
 56 |   .active.left {
 57 |     left: -100%;
 58 |   }
 59 |   .active.right {
 60 |     left: 100%;
 61 |   }
 62 | 
 63 | }
 64 | 
 65 | // Left/right controls for nav
 66 | // ---------------------------
 67 | 
 68 | .carousel-control {
 69 |   position: absolute;
 70 |   top: 40%;
 71 |   left: 15px;
 72 |   width: 40px;
 73 |   height: 40px;
 74 |   margin-top: -20px;
 75 |   font-size: 60px;
 76 |   font-weight: 100;
 77 |   line-height: 30px;
 78 |   color: @white;
 79 |   text-align: center;
 80 |   background: @grayDarker;
 81 |   border: 3px solid @white;
 82 |   .border-radius(23px);
 83 |   .opacity(50);
 84 | 
 85 |   // we can't have this transition here
 86 |   // because webkit cancels the carousel
 87 |   // animation if you trip this while
 88 |   // in the middle of another animation
 89 |   // ;_;
 90 |   // .transition(opacity .2s linear);
 91 | 
 92 |   // Reposition the right one
 93 |   &.right {
 94 |     left: auto;
 95 |     right: 15px;
 96 |   }
 97 | 
 98 |   // Hover state
 99 |   &:hover {
100 |     color: @white;
101 |     text-decoration: none;
102 |     .opacity(90);
103 |   }
104 | }
105 | 
106 | // Caption for text below images
107 | // -----------------------------
108 | 
109 | .carousel-caption {
110 |   position: absolute;
111 |   left: 0;
112 |   right: 0;
113 |   bottom: 0;
114 |   padding: 10px 15px 5px;
115 |   background: @grayDark;
116 |   background: rgba(0,0,0,.75);
117 | }
118 | .carousel-caption h4,
119 | .carousel-caption p {
120 |   color: @white;
121 | }
122 | 


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/pieces.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.core.pieces
 2 |   (:use 
 3 |     [torrent-client.core.crypt :only [sha1 byte-array->str]]
 4 |     [torrent-client.core.byte-array :only [uint8-array]]))
 5 | 
 6 | (deftype PieceFile [meta file]
 7 | 
 8 |   Object
 9 |   (toString [this]
10 |     (pr-str this))
11 | 
12 |   IWithMeta
13 |   (-with-meta [this meta] (PieceFile. meta file))
14 | 
15 |   IMeta
16 |   (-meta [this] meta)
17 | 
18 |   ILookup
19 |   ; Does this file contain a given block index
20 |   (-lookup [this k]
21 |     (-lookup this k nil))
22 |   ; H.C todo, impliment nth-child and clean this up
23 |   (-lookup [this k not-found]
24 |     ; (js* "debugger;")
25 |     (if (-contains-key? this k)
26 |       true
27 |       not-found))
28 | 
29 |   Fn
30 |   IFn
31 |   (-invoke [this]
32 |     file)
33 |   (-invoke [this k]
34 |     (-lookup this k))
35 |   (-invoke [this k not-found]
36 |     (-lookup this k not-found))
37 | 
38 |   IAssociative
39 |   (-contains-key? [this k]
40 |     "Check if a piece-index is required by this file"
41 |     (and (not (nil? meta))
42 |          (<= (meta :piece-start) k (meta :piece-end))))
43 | 
44 |   IHash
45 |   (-hash [o]
46 |     (goog.getUid o))
47 | 
48 |   )
49 | 
50 | (defn piece-file [file]
51 |   (PieceFile. nil file))
52 | 
53 | (deftype Piece [meta byte-array ^:mutable __hash]
54 | 
55 |   ICounted
56 |   (-count [a] (count byte-array))
57 | 
58 |   IHash
59 |   (-hash [_]
60 |     ; H.C; check caching-hash macro
61 |     (if-not (nil? __hash)
62 |       __hash
63 |       ; sha1 does not return a Uint8Array, it returns a regular array
64 |       (let [hash-str (byte-array->str (sha1 byte-array))]
65 |         ; H.C; this causes a compile fail?
66 |         ; (set! __hash hash-str)
67 | 
68 |         ; If the hash didn't previously exist generate it and
69 |         hash-str)))
70 | 
71 |   IWithMeta
72 |   (-with-meta [_ meta] (Piece. meta byte-array __hash))
73 | 
74 |   IMeta
75 |   (-meta [_] meta)
76 | 
77 |   )
78 | 
79 | (defn piece [byte-array]
80 |   (Piece. nil byte-array nil))
81 | 
82 | (defn blocks->piece [blocks]
83 |   "Build a piece from its component blocks"
84 |   (let [blocks (sort-by :begin blocks)
85 |         piece-size (reduce + (map (comp count :data) blocks))
86 |         ; Build a byte array long enough for all the blocks
87 |         byte-array (uint8-array piece-size)]
88 |     ; Then add all the pieces at their correct offset
89 |     (doseq [block blocks]
90 |       (.set byte-array (block :data) (block :begin)))
91 |     (piece byte-array)))


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Ampere
 2 | 
 3 | A bittorrent client that runs in the browser without plugins. **Currently only works in chrome**
 4 | 
 5 | DEMO: http://bittorrent.io
 6 | 
 7 | ## Bittorrent
 8 | 
 9 | * Built to the official spec 
10 | * Can read .torrent files
11 | * BEP 9 support (magnet links)
12 | * Partial BEP 10 support (extension negotiation)
13 | 
14 | ## Tech
15 | 
16 | Built using clojurescript (that subsequently compiles down to javascript) and utilizing the latest in HTML5 tech
17 | * WebRTC
18 | * HTML5 Filesystem
19 | * Socket.io
20 | * Typed Arrays
21 | * IndexedDb
22 | 
23 | ## Usage
24 | 
25 | ```bash
26 | lein deps
27 | lein git-deps
28 | lein run
29 | ```
30 | 
31 | # Also see
32 | 
33 | * [the actual client](http://bittorrent.io) - View the client in your browser
34 | * [webrtc-tracker](https://github.com/hcliff/webrtc-tracker-nodejs) - A seperate project to build a bittorrent tracker for webrtc based bittorrent clients
35 | 
36 | # Issues
37 | 
38 | * Advanced compile doesn't work; thus simple must be (adds 200k to page weight)
39 | 
40 | ## License
41 | 
42 | Copyright (c) 2013, Henry Clifford
43 | All rights reserved.
44 | 
45 | Redistribution and use in source and binary forms, with or without
46 | modification, are permitted provided that the following conditions are met:
47 | * Redistributions of source code must retain the above copyright
48 |    notice, this list of conditions and the following disclaimer.
49 | * Redistributions in binary form must reproduce the above copyright
50 |    notice, this list of conditions and the following disclaimer in the
51 |    documentation and/or other materials provided with the distribution.
52 | * Neither the name of the organization nor the
53 |    names of its contributors may be used to endorse or promote products
54 |    derived from this software without specific prior written permission.
55 | 
56 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
57 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
58 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
59 | DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
60 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
61 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
62 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
63 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
64 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
65 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/crypt.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.core.crypt
 2 |   (:require
 3 |     [goog.crypt :as crypt]
 4 |     [goog.crypt.Sha1 :as Sha1]
 5 |     [torrent-client.core.reader :as reader])
 6 |   (:use [torrent-client.core.bencode :only [char]]))
 7 | 
 8 | (defn sha1 
 9 |   "A wrapper around the SHA class to get the
10 |   computed value without subsequent calls"
11 |   [obj]
12 |   (let [sha1 (crypt/Sha1.)]
13 |     (.update sha1 obj)
14 |     (.digest sha1)))
15 | 
16 | (def sizes {
17 |   :byte (/ 8 8)
18 |   :char (/ 16 8)
19 |   :short (/ 16 8)
20 |   :int (/ 32 8)
21 |   :long (/ 64 8)
22 |   :float (/ 32 8)
23 |   :double (/ 64 8)
24 |   })
25 | 
26 | (defmulti pack-data (fn [[format data]] format))
27 | 
28 | (defmethod pack-data :int [[_ data]]
29 |   (char [(bit-and 0xff (bit-shift-right data 24))
30 |          (bit-and 0xff (bit-shift-right data 16))
31 |          (bit-and 0xff (bit-shift-right data 8))
32 |          (bit-and 0xff data)]))
33 | 
34 | ; H.C currently packs to string
35 | ; if we change communication to bytearray will need changing
36 | (defn pack [& formatters]
37 |   (let [formatters (partition 2 formatters)]
38 |     (apply str (map pack-data formatters))))
39 | 
40 | (defmulti unpack-data (fn [format data] format))
41 | 
42 | (defmethod unpack-data :int [_ data]
43 |   (+ (bit-shift-left (nth data 0) 24)
44 |      (bit-shift-left (nth data 1) 16)
45 |      (bit-shift-left (nth data 2) 8)
46 |      (nth data 3)))
47 | 
48 | (defn unpack [formatters data]
49 |   (let [reader (reader/push-back-reader data)]
50 |     (loop [formatters formatters
51 |            data []]
52 |       (if-let [formatter (first formatters)]
53 |         (let [bytes (reader/read reader (sizes formatter))]
54 |           (recur (rest formatters)
55 |                  ; Execute this formatter add the result to the data
56 |                  (conj data (unpack-data formatter bytes))))
57 |         ; And return the data when we're done
58 |         data))))
59 | 
60 | (defn b64-encode [string]
61 |   (.btoa js/window string))
62 | 
63 | (defn b64-decode [string]
64 |   (.atob js/window string))
65 | 
66 | (defn str->byte-array
67 |   "The same as googles stringToByteArray
68 |   but works on a typed array instead!"
69 |   [string]
70 |   (let [buffer (js/ArrayBuffer. (count string))
71 |         buffer-view (js/Uint8Array. buffer)]
72 |     (doseq [i (range (count string))
73 |             :let [byte (.charCodeAt string i)]]
74 |       (aset buffer-view i byte))
75 |     buffer-view))
76 | 
77 | (defn byte-array->str
78 |   "Wrapper around the closure library to feed it cljs datatypes"
79 |   [array]
80 |   (crypt/byteArrayToString (clj->js array)))


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/db.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.core.db
 2 |   (:require [goog.db :as db]
 3 |             [goog.db.Cursor :as Cursor]
 4 |             [goog.db.KeyRange :as KeyRange]
 5 |             [goog.db.ObjectStore :as ObjectStore]
 6 |             [goog.db.IndexedDb :as IndexedDb]
 7 |             [torrent-client.browser.prefix :as prefix])
 8 |   (:use 
 9 |     [torrent-client.core.string :only [camel-case selector-case]])
10 |   (:use-macros [async.macros :only [async]]))
11 | 
12 | ;;************************************************
13 | ;; Idiomatic clojure wrapper around the objectstore 
14 | ;;************************************************
15 | 
16 | (extend-type db/ObjectStore
17 |   
18 |   ILookup
19 |   (-lookup
20 |     [os k]
21 |       (js->clj (.get os (clj->js k))))
22 | 
23 |   ; ICounted
24 |   ; (-count [os] (.count (.-store_ os)))
25 | 
26 |   ; ; IFn
27 |   ; ; (-invoke
28 |   ; ;   [os k]
29 |   ; ;     (-lookup os k))
30 | 
31 |   ITransientAssociative
32 |   (-assoc! 
33 |     [os k v]
34 |       (.put os (clj->js v) (clj->js k)))
35 | 
36 |   ; ITransientMap
37 |   ; (-dissoc! [os k]
38 |   ;   (.delete os (clj->js k)))
39 | 
40 |   )
41 | 
42 | ;;************************************************
43 | ;; Wrappers around the closure library
44 | ;;************************************************
45 | 
46 | (defn create-object-store [db name options]
47 |   (.createObjectStore db name (clj->js options)))
48 | 
49 | (defn create-transaction [db object-stores method]
50 |   (.createTransaction db (clj->js object-stores) method))
51 | 
52 | ;;************************************************
53 | ;; Async helpers
54 | ;;************************************************
55 | 
56 | (defn open-database 
57 |   "Manages versioning of object-stores for a database"
58 |   [database version object-stores]
59 |   (async [success-callback error-callback]
60 |     (let [request (.open prefix/indexedDB database version)]
61 |       (letfn [(success [e]
62 |                 (let [db (.-result request)]
63 |                   (success-callback (db/IndexedDb. db))))
64 | 
65 |               (upgrade-needed [e]
66 |                 (let [db (.-result request)]
67 |                   ; Create all the object stores
68 |                   (doseq [store object-stores]
69 |                     (when-not (.contains (.-objectStoreNames db) (:name store))
70 |                       (create-object-store db (:name store) {
71 |                         :keyPath (:key-path store)
72 |                         :autoIncrement (:auto-increment store)
73 |                         })))
74 |                   db))]
75 |         (set! (.-onerror request) error-callback)
76 |         (set! (.-onsuccess request) success)
77 |         (set! (.-onupgradeneeded request) upgrade-needed)))))


--------------------------------------------------------------------------------
/resources/public/less/main.less:
--------------------------------------------------------------------------------
  1 | body{
  2 |   padding-top: 60px;
  3 | }
  4 | 
  5 | .table td, .table th{
  6 |   height: 50px;
  7 |   line-height: 34px;
  8 |   margin-left: 0 !important;
  9 |   box-sizing: border-box;
 10 | }
 11 | table tr:last-child td{
 12 |   border-bottom: 1px solid #DDD;
 13 | }
 14 | .navbar i{
 15 |   margin-right: 5px;
 16 | }
 17 | footer{
 18 |   top: ~"-webkit-calc(100% - 65px)";
 19 |   position:absolute;
 20 | }
 21 | small{
 22 |   font-size:10px;
 23 | }
 24 | .nav{
 25 |   margin-bottom: 0;
 26 | }
 27 | .table .progress{
 28 |   margin-bottom: 0;
 29 |   margin-top: 7px;
 30 |   label{
 31 |       float: right;
 32 |       font-size: 10px;
 33 |       margin-top: -15px;
 34 |       line-height: 10px;
 35 |       margin-right: 3px;
 36 |       background-color: rgba(0,0,0,0.4);
 37 |   }
 38 | }
 39 | 
 40 | th, tr{
 41 |   display: -webkit-box;
 42 |   display: -moz-box;
 43 |   display: box;
 44 | }
 45 | 
 46 | tr{
 47 |   width: 100%;
 48 | }
 49 | 
 50 | .name, .progress-td{
 51 |   min-width: 200px;
 52 | }
 53 | 
 54 | .size, .speed{
 55 |   min-width: 80px;
 56 | }
 57 | 
 58 | .table .actions{
 59 |   width: 160px;
 60 |   display: block;
 61 |   > div{
 62 |     float: right;
 63 |   }
 64 | }
 65 | 
 66 | .flexer(@flex){
 67 |   -webkit-box-flex: @flex;
 68 |   -moz-box-flex: @flex;
 69 |   box-flex: @flex;
 70 | }
 71 | 
 72 | .flexX (@index) when (@index > 0) {
 73 |   (~".flex@{index}") { .flex(@index); }
 74 |   .flexX(@index - 1);
 75 | }
 76 | .flexX (0) {}
 77 | 
 78 | .flex (@columns) {
 79 |   display: block;
 80 |   .flexer(@columns)
 81 | }
 82 | 
 83 | .flexX (@gridColumns);
 84 | 
 85 | .files{
 86 |   width:250px;
 87 |   display:block;
 88 |   float:left;
 89 |   margin-top:7px;
 90 |   font-size: 0;
 91 | 
 92 |   & + .help-inline{
 93 |     display: none;
 94 |   }
 95 | 
 96 |   &:empty{
 97 |     display: none;
 98 |   }
 99 | 
100 |   &:empty + .help-inline{
101 |     display: block;
102 |   }
103 | 
104 |   .label{
105 |     margin: 0 5px 5px 0;
106 |     max-width: 200px;
107 |     display: inline-block;
108 |     text-overflow: ellipsis;
109 |     overflow: hidden;
110 |   }
111 | }
112 | 
113 | #demo-torrent:hover{
114 |   text-decoration: none;
115 | }
116 | 
117 | div.info{
118 |   margin-bottom: @gridGutterWidth;
119 |   color: @infoText;
120 | 
121 | 
122 |   &:hover{
123 |     background-color: @infoBackground;
124 | 
125 |     strong{
126 |       text-decoration: underline;
127 |     }
128 |   }
129 | 
130 |   img{
131 |     width: 60px;
132 |     height: 60px;
133 |     margin-right: @gridGutterWidth;
134 |     float: left;
135 |   }
136 | 
137 |   div.wrapper{
138 |     display: table-cell;
139 |     height: 60px;
140 |     vertical-align: middle;
141 |   }
142 |   strong, small{
143 |     display: block;
144 |   }
145 | }


--------------------------------------------------------------------------------
/src-cljs/torrent_client/core/dispatch.cljs:
--------------------------------------------------------------------------------
 1 | (ns ^{:doc "Event dispatching.
 2 | 
 3 |   Provides a way for code to react to events. Terminology:
 4 | 
 5 |   * event-id: Identifies a class of events. Can be any Clojure value.
 6 | 
 7 |   * event-data: Parameterizes a particular event. Can be any Clojure
 8 |     value.
 9 | 
10 |   * reactor: A function that is invoked in response to an event
11 |     occurring.
12 | 
13 |   * reaction: A relationship between a set of events and a reactor.
14 | 
15 |   * event-pred: A function which takes an event ID and returns true or
16 |     false.
17 | 
18 |   Reactors are associated with events via `react-to`. When events are
19 |   fired with an `event-id` and optional `event-data`, any reactors whose
20 |   `event-pred` returns true for the `event-id` are invoked."}
21 |   torrent-client.core.dispatch)
22 | 
23 | (def ^{:doc "Stores the current reactions."}
24 |   reactions (atom {}))
25 | 
26 | (defn react-to
27 |   "Cause the specified reactor to be invoked whenever an event that
28 |   satisfies `event-pred` is fired. reactor is a function that accepts
29 |   two arguments: `event-id` and `event-data`.
30 | 
31 |   Returns the reaction.
32 | 
33 |   The reactor will continue to be invoked until one of two things
34 |   happens:
35 | 
36 |    1. `delete-reaction` is called on this reaction.
37 | 
38 |    2. The reaction occurs `max-count` times. If `max-count` is not
39 |       specified, the reaction will continue to be invoked until deleted.
40 | 
41 |   If `max-count` is specified, `delete-reaction` will be called
42 |   automatically when the reaction has occurred the specified number of
43 |   times."
44 |   ([event-pred reactor]
45 |      (react-to nil event-pred reactor))
46 |   ([max-count event-pred reactor]
47 |      (let [reaction {:max-count max-count
48 |                      :event-pred event-pred
49 |                      :reactor reactor}]
50 |        (swap! reactions assoc reaction 0)
51 |        reaction)))
52 | 
53 | (defn delete-reaction
54 |   "Delete a reaction. After calling this function, the specified
55 |   reaction will no longer be invoked."
56 |   [reaction]
57 |   (swap! reactions dissoc reaction))
58 | 
59 | (defn fire
60 |   "Raise an event to any reactors whose event-pred returns true for
61 |   `event-id`. The `event-id` and `event-data`, if specified, are passed to
62 |   the reactor."
63 |   ([event-id]
64 |      (fire event-id nil))
65 |   ([event-id event-data]
66 |      (let [matching-reactions (filter (fn [[{event-pred :event-pred} run-count]]
67 |                                         (event-pred event-id))
68 |                                       @reactions)]
69 |        (doseq [[reaction run-count] matching-reactions]
70 |          (let [{:keys [max-count reactor]} reaction
71 |                run-count (inc run-count)]
72 |            (reactor event-id event-data)
73 |            (if (and max-count
74 |                     (<= max-count run-count))
75 |              (delete-reaction reaction)
76 |              (swap! reactions assoc reaction run-count)))))))


--------------------------------------------------------------------------------
/resources/public/less/progress-bars.less:
--------------------------------------------------------------------------------
  1 | // PROGRESS BARS
  2 | // -------------
  3 | 
  4 | 
  5 | // ANIMATIONS
  6 | // ----------
  7 | 
  8 | // Webkit
  9 | @-webkit-keyframes progress-bar-stripes {
 10 |   from  { background-position: 40px 0; }
 11 |   to    { background-position: 0 0; }
 12 | }
 13 | 
 14 | // Firefox
 15 | @-moz-keyframes progress-bar-stripes {
 16 |   from  { background-position: 40px 0; }
 17 |   to    { background-position: 0 0; }
 18 | }
 19 | 
 20 | // IE9
 21 | @-ms-keyframes progress-bar-stripes {
 22 |   from  { background-position: 40px 0; }
 23 |   to    { background-position: 0 0; }
 24 | }
 25 | 
 26 | // Opera
 27 | @-o-keyframes progress-bar-stripes {
 28 |   from  { background-position: 0 0; }
 29 |   to    { background-position: 40px 0; }
 30 | }
 31 | 
 32 | // Spec
 33 | @keyframes progress-bar-stripes {
 34 |   from  { background-position: 40px 0; }
 35 |   to    { background-position: 0 0; }
 36 | }
 37 | 
 38 | 
 39 | 
 40 | // THE BARS
 41 | // --------
 42 | 
 43 | // Outer container
 44 | .progress {
 45 |   overflow: hidden;
 46 |   height: 18px;
 47 |   margin-bottom: 18px;
 48 |   #gradient > .vertical(#f5f5f5, #f9f9f9);
 49 |   .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));
 50 |   .border-radius(4px);
 51 | }
 52 | 
 53 | // Bar of progress
 54 | .progress .bar {
 55 |   width: 0%;
 56 |   height: 18px;
 57 |   color: @white;
 58 |   font-size: 12px;
 59 |   text-align: center;
 60 |   text-shadow: 0 -1px 0 rgba(0,0,0,.25);
 61 |   #gradient > .vertical(#149bdf, #0480be);
 62 |   .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
 63 |   .box-sizing(border-box);
 64 |   .transition(width .6s ease);
 65 | }
 66 | 
 67 | // Striped bars
 68 | .progress-striped .bar {
 69 |   #gradient > .striped(#149bdf);
 70 |   .background-size(40px 40px);
 71 | }
 72 | 
 73 | // Call animation for the active one
 74 | .progress.active .bar {
 75 |   -webkit-animation: progress-bar-stripes 2s linear infinite;
 76 |      -moz-animation: progress-bar-stripes 2s linear infinite;
 77 |       -ms-animation: progress-bar-stripes 2s linear infinite;
 78 |        -o-animation: progress-bar-stripes 2s linear infinite;
 79 |           animation: progress-bar-stripes 2s linear infinite;
 80 | }
 81 | 
 82 | 
 83 | 
 84 | // COLORS
 85 | // ------
 86 | 
 87 | // Danger (red)
 88 | .progress-danger .bar {
 89 |   #gradient > .vertical(#ee5f5b, #c43c35);
 90 | }
 91 | .progress-danger.progress-striped .bar {
 92 |   #gradient > .striped(#ee5f5b);
 93 | }
 94 | 
 95 | // Success (green)
 96 | .progress-success .bar {
 97 |   #gradient > .vertical(#62c462, #57a957);
 98 | }
 99 | .progress-success.progress-striped .bar {
100 |   #gradient > .striped(#62c462);
101 | }
102 | 
103 | // Info (teal)
104 | .progress-info .bar {
105 |   #gradient > .vertical(#5bc0de, #339bb9);
106 | }
107 | .progress-info.progress-striped .bar {
108 |   #gradient > .striped(#5bc0de);
109 | }
110 | 
111 | // Warning (orange)
112 | .progress-warning .bar {
113 |   #gradient > .vertical(lighten(@orange, 15%), @orange);
114 | }
115 | .progress-warning.progress-striped .bar {
116 |   #gradient > .striped(lighten(@orange, 15%));
117 | }
118 | 


--------------------------------------------------------------------------------
/src-cljs/torrent_client/files.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.files
 2 |   (:require
 3 |     [torrent-client.core.dispatch :as dispatch]
 4 |     [torrent-client.core.pieces :as pieces]
 5 |     [filesystem.filesystem :as filesystem]
 6 |     [filesystem.writer :as writer]
 7 |     [filesystem.entry :as entry]
 8 |     [cljconsole.main :as console])
 9 |   (:use-macros 
10 |     [async.macros :only [async let-async]]))
11 | 
12 | (def files (atom {}))
13 | 
14 | ;************************************************
15 | ; Generate data for files
16 | ;************************************************
17 | 
18 | (defn file-boundaries
19 |   "Given files in a set order, calculate start and stop indexes"
20 |   ([files] (file-boundaries files 0 []))
21 |   ([files total boundaries]
22 |     (if (empty? files)
23 |       boundaries
24 |       (let [new-total (+ total ((first files) :length))
25 |             pos {:pos-start total :pos-end new-total}]
26 |         (recur (rest files) new-total (conj boundaries pos))))))
27 | 
28 | (defn piece-boundaries [file piece-length]
29 |   "Given a file and a piece-length calculate the pieces that a file contains, 
30 |   note that two files may require the same piece due to overlap between the 
31 |   head and tail of the files. 
32 |   (e.g piece 3 has the head of file b and tail of a)"
33 |   {:piece-start (Math/floor (/ (file :pos-start) piece-length))
34 |    :piece-end   (Math/floor (/ (file :pos-end)   piece-length))})
35 | 
36 | ;************************************************
37 | ; Manage torrent files
38 | ;************************************************
39 | 
40 | (defn generate-file [file-entry file-data piece-length]
41 |   ; file-data is {:pos-start x :pos-end x}
42 |   ; file-entry is the file on the filesystem
43 |   ; piece length is how we should break apart these files
44 |   (let [boundaries (piece-boundaries file-data piece-length)]
45 |     ; Attach information on the block boundaries to the file
46 |     (with-meta (pieces/piece-file file-entry) (merge file-data boundaries))))
47 | 
48 | (dispatch/react-to #{:add-file} (fn [_ [torrent file-entry file-data]]
49 |   "A file has been added to this torrent"
50 |   (let [file (generate-file file-entry file-data (@torrent :piece-length))]
51 |     ; TODO: use below code
52 |     ; (swap! files update-in [(@torrent :pretty-info-hash)] conj file)
53 |     (swap! files (partial merge-with concat) {(@torrent :pretty-info-hash) [file]})
54 |     ; "touch" the torrent atom to let listeners know related content has chanegd
55 |     (swap! torrent identity))))
56 | 
57 | ;************************************************
58 | ; Filesystem helpers
59 | ;************************************************
60 | 
61 | (defn write-file
62 |   "Create a file on the filesystem, set it to the correct length and then 
63 |    write any data we have for it"
64 |   [fs {:keys [path length]} data]
65 |   (console/info "Write to filesystem. Path: " path " Data: " data)
66 |   (async [success-callback error-callback]
67 |     (let-async [entry (entry/get-entry fs path {:create true})
68 |                 writer (entry/create-writer entry)
69 |                 _ (writer/truncate writer length)
70 |                 _ (writer/write writer data)]
71 |       (success-callback entry))))


--------------------------------------------------------------------------------
/resources/public/less/reset.less:
--------------------------------------------------------------------------------
  1 | // Reset.less
  2 | // Adapted from Normalize.css http://github.com/necolas/normalize.css
  3 | // ------------------------------------------------------------------------
  4 | 
  5 | // Display in IE6-9 and FF3
  6 | // -------------------------
  7 | 
  8 | article,
  9 | aside,
 10 | details,
 11 | figcaption,
 12 | figure,
 13 | footer,
 14 | header,
 15 | hgroup,
 16 | nav,
 17 | section {
 18 |   display: block;
 19 | }
 20 | 
 21 | // Display block in IE6-9 and FF3
 22 | // -------------------------
 23 | 
 24 | audio,
 25 | canvas,
 26 | video {
 27 |   display: inline-block;
 28 |   *display: inline;
 29 |   *zoom: 1;
 30 | }
 31 | 
 32 | // Prevents modern browsers from displaying 'audio' without controls
 33 | // -------------------------
 34 | 
 35 | audio:not([controls]) {
 36 |     display: none;
 37 | }
 38 | 
 39 | // Base settings
 40 | // -------------------------
 41 | 
 42 | html {
 43 |   font-size: 100%;
 44 |   -webkit-text-size-adjust: 100%;
 45 |       -ms-text-size-adjust: 100%;
 46 | }
 47 | // Focus states
 48 | a:focus {
 49 |   .tab-focus();
 50 | }
 51 | // Hover & Active
 52 | a:hover,
 53 | a:active {
 54 |   outline: 0;
 55 | }
 56 | 
 57 | // Prevents sub and sup affecting line-height in all browsers
 58 | // -------------------------
 59 | 
 60 | sub,
 61 | sup {
 62 |   position: relative;
 63 |   font-size: 75%;
 64 |   line-height: 0;
 65 |   vertical-align: baseline;
 66 | }
 67 | sup {
 68 |   top: -0.5em;
 69 | }
 70 | sub {
 71 |   bottom: -0.25em;
 72 | }
 73 | 
 74 | // Img border in a's and image quality
 75 | // -------------------------
 76 | 
 77 | img {
 78 |   max-width: 100%; // Make images inherently responsive
 79 |   vertical-align: middle;
 80 |   border: 0;
 81 |   -ms-interpolation-mode: bicubic;
 82 | }
 83 | 
 84 | // Prevent max-width from affecting Google Maps
 85 | #map_canvas img {
 86 |   max-width: none;
 87 | }
 88 | 
 89 | // Forms
 90 | // -------------------------
 91 | 
 92 | // Font size in all browsers, margin changes, misc consistency
 93 | button,
 94 | input,
 95 | select,
 96 | textarea {
 97 |   margin: 0;
 98 |   font-size: 100%;
 99 |   vertical-align: middle;
100 | }
101 | button,
102 | input {
103 |   *overflow: visible; // Inner spacing ie IE6/7
104 |   line-height: normal; // FF3/4 have !important on line-height in UA stylesheet
105 | }
106 | button::-moz-focus-inner,
107 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4
108 |   padding: 0;
109 |   border: 0;
110 | }
111 | button,
112 | input[type="button"],
113 | input[type="reset"],
114 | input[type="submit"] {
115 |   cursor: pointer; // Cursors on all buttons applied consistently
116 |   -webkit-appearance: button; // Style clickable inputs in iOS
117 | }
118 | input[type="search"] { // Appearance in Safari/Chrome
119 |   -webkit-box-sizing: content-box;
120 |      -moz-box-sizing: content-box;
121 |           box-sizing: content-box;
122 |   -webkit-appearance: textfield;
123 | }
124 | input[type="search"]::-webkit-search-decoration,
125 | input[type="search"]::-webkit-search-cancel-button {
126 |   -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5
127 | }
128 | textarea {
129 |   overflow: auto; // Remove vertical scrollbar in IE6-9
130 |   vertical-align: top; // Readability and alignment cross-browser
131 | }
132 | 


--------------------------------------------------------------------------------
/src-cljs/torrent_client/metadata.cljs:
--------------------------------------------------------------------------------
 1 | (ns torrent-client.metadata
 2 |   (:require 
 3 |     [torrent-client.core.dispatch :as dispatch]
 4 |     [torrent-client.core.queue :as queue]
 5 |     [torrent-client.core.crypt :as crypt]
 6 |     [torrent-client.core.byte-array :as byte-array]
 7 |     [cljconsole.main :as console])
 8 |   (:use
 9 |     [torrent-client.torrent :only [has-full-metadata?]]
10 |     [torrent-client.torrents :only [torrents]]
11 |     [torrent-client.core.metadata :only [piece-length pieces->metadata]])
12 |   (:use-macros 
13 |     [task.macros :only [deftask]]))
14 | 
15 | ;;************************************************
16 | ;; For retrieving metadata per BEP 9
17 | ;;************************************************
18 | 
19 | ; metadata pieces we are currently getting
20 | (def working (atom {}))
21 | 
22 | ; All the metadata pieces we've received
23 | (def received (atom {}))
24 | 
25 | ; How long can a piece be working before we expire it
26 | (def working-life (* 1000 15))
27 | 
28 | (deftask expire-pieces working-life [_]
29 |   (let [expired (queue/expired working working-life)]
30 |     (doseq [[torrent pieces] expired
31 |             piece pieces]
32 |       (console/error "Expiring metadata piece: " piece)
33 |       (queue/disj! working torrent piece)
34 |       (dispatch/fire :expire-piece [torrent piece]))))
35 | 
36 | ; Indexes of all the pieces in a torrent
37 | (defn pieces [torrent]
38 |   (range (Math/ceil (/ (@torrent :info-length) piece-length))))
39 | 
40 | (defn wanted-pieces [torrent]
41 |   "Given a torrent return the pieces missing from its metadata"
42 |   (let [wanted (pieces torrent)]
43 |     (if-not (has-full-metadata? torrent)
44 |       (remove #(queue/contains? working torrent %) wanted))))
45 | 
46 | (dispatch/react-to #{:receive-metadata-reject} (fn [_ [torrent piece-index]]
47 |   "A peer indicates it doesn't have a metadata piece, mark it no longer worked"
48 |   (queue/disj! working torrent piece-index)))
49 | 
50 | (dispatch/react-to #{:receive-metadata-piece} (fn [_ [torrent piece-index data]]
51 |   "When we get a metadata piece add it to the receieved pile"
52 |   (let [info-hash (@torrent :pretty-info-hash)]
53 |     (swap! received assoc-in [info-hash piece-index] data)
54 |     (queue/disj! working torrent piece-index)
55 |     ; And if we have all the metadata pieces 
56 |     (if (= (count (@received info-hash)) (count (pieces torrent)))
57 |       ; String all the pieces into one byte array
58 |       (let [byte-array (pieces->metadata (@received info-hash))]
59 |         (swap! received dissoc info-hash)
60 |         (dispatch/fire :receive-metadata [torrent byte-array]))))))
61 | 
62 | (dispatch/react-to #{:receive-metadata} (fn [_ [torrent byte-array]]
63 |   ; TODO: needless string conversion
64 |   ; Verify that the correct metadata was received
65 |   (if (= (vec (crypt/sha1 byte-array)) (vec (@torrent :info-hash)))
66 |     (dispatch/fire :add-info-byte-array [torrent byte-array])
67 |     (dispatch/fire :corrupt-metadata torrent))))
68 | 
69 | (dispatch/react-to #{:corrupt-metadata} (fn [_ torrent]
70 |   (console/warn "Received incorrect metadata for torrent" 
71 |                 (@torrent :pretty-info-hash))))
72 | 
73 | (defn get-piece [torrent piece-index]
74 |   (byte-array/uint8-array (@torrent :info-byte-array) 
75 |                           (* piece-index piece-length) 
76 |                           piece-length))


--------------------------------------------------------------------------------
/resources/public/less/responsive-767px-max.less:
--------------------------------------------------------------------------------
  1 | // UP TO LANDSCAPE PHONE
  2 | // ---------------------
  3 | 
  4 | @media (max-width: 480px) {
  5 | 
  6 |   // Smooth out the collapsing/expanding nav
  7 |   .nav-collapse {
  8 |     -webkit-transform: translate3d(0, 0, 0); // activate the GPU
  9 |   }
 10 | 
 11 |   // Block level the page header small tag for readability
 12 |   .page-header h1 small {
 13 |     display: block;
 14 |     line-height: @baseLineHeight;
 15 |   }
 16 | 
 17 |   // Update checkboxes for iOS
 18 |   input[type="checkbox"],
 19 |   input[type="radio"] {
 20 |     border: 1px solid #ccc;
 21 |   }
 22 | 
 23 |   // Remove the horizontal form styles
 24 |   .form-horizontal .control-group > label {
 25 |     float: none;
 26 |     width: auto;
 27 |     padding-top: 0;
 28 |     text-align: left;
 29 |   }
 30 |   // Move over all input controls and content
 31 |   .form-horizontal .controls {
 32 |     margin-left: 0;
 33 |   }
 34 |   // Move the options list down to align with labels
 35 |   .form-horizontal .control-list {
 36 |     padding-top: 0; // has to be padding because margin collaspes
 37 |   }
 38 |   // Move over buttons in .form-actions to align with .controls
 39 |   .form-horizontal .form-actions {
 40 |     padding-left: 10px;
 41 |     padding-right: 10px;
 42 |   }
 43 | 
 44 |   // Modals
 45 |   .modal {
 46 |     position: absolute;
 47 |     top:   10px;
 48 |     left:  10px;
 49 |     right: 10px;
 50 |     width: auto;
 51 |     margin: 0;
 52 |     &.fade.in { top: auto; }
 53 |   }
 54 |   .modal-header .close {
 55 |     padding: 10px;
 56 |     margin: -10px;
 57 |   }
 58 | 
 59 |   // Carousel
 60 |   .carousel-caption {
 61 |     position: static;
 62 |   }
 63 | 
 64 | }
 65 | 
 66 | 
 67 | 
 68 | // LANDSCAPE PHONE TO SMALL DESKTOP & PORTRAIT TABLET
 69 | // --------------------------------------------------
 70 | 
 71 | @media (max-width: 767px) {
 72 | 
 73 |   // Padding to set content in a bit
 74 |   body {
 75 |     padding-left: 20px;
 76 |     padding-right: 20px;
 77 |   }
 78 |   // Negative indent the now static "fixed" navbar
 79 |   .navbar-fixed-top,
 80 |   .navbar-fixed-bottom {
 81 |     margin-left: -20px;
 82 |     margin-right: -20px;
 83 |   }
 84 |   // Remove padding on container given explicit padding set on body
 85 |   .container-fluid {
 86 |     padding: 0;
 87 |   }
 88 | 
 89 |   // TYPOGRAPHY
 90 |   // ----------
 91 |   // Reset horizontal dl
 92 |   .dl-horizontal {
 93 |     dt {
 94 |       float: none;
 95 |       clear: none;
 96 |       width: auto;
 97 |       text-align: left;
 98 |     }
 99 |     dd {
100 |       margin-left: 0;
101 |     }
102 |   }
103 | 
104 |   // GRID & CONTAINERS
105 |   // -----------------
106 |   // Remove width from containers
107 |   .container {
108 |     width: auto;
109 |   }
110 |   // Fluid rows
111 |   .row-fluid {
112 |     width: 100%;
113 |   }
114 |   // Undo negative margin on rows and thumbnails
115 |   .row,
116 |   .thumbnails {
117 |     margin-left: 0;
118 |   }
119 |   // Make all grid-sized elements block level again
120 |   [class*="span"],
121 |   .row-fluid [class*="span"] {
122 |     float: none;
123 |     display: block;
124 |     width: auto;
125 |     margin-left: 0;
126 |   }
127 | 
128 |   // FORM FIELDS
129 |   // -----------
130 |   // Make span* classes full width
131 |   .input-large,
132 |   .input-xlarge,
133 |   .input-xxlarge,
134 |   input[class*="span"],
135 |   select[class*="span"],
136 |   textarea[class*="span"],
137 |   .uneditable-input {
138 |     .input-block-level();
139 |   }
140 |   // But don't let it screw up prepend/append inputs
141 |   .input-prepend input,
142 |   .input-append input,
143 |   .input-prepend input[class*="span"],
144 |   .input-append input[class*="span"] {
145 |     display: inline-block; // redeclare so they don't wrap to new lines
146 |     width: auto;
147 |   }
148 | 
149 | }
150 | 


--------------------------------------------------------------------------------
/resources/public/less/dropdowns.less:
--------------------------------------------------------------------------------
  1 | // DROPDOWN MENUS
  2 | // --------------
  3 | 
  4 | // Use the .menu class on any 
  • element within the topbar or ul.tabs and you'll get some superfancy dropdowns 5 | .dropup, 6 | .dropdown { 7 | position: relative; 8 | } 9 | .dropdown-toggle { 10 | // The caret makes the toggle a bit too tall in IE7 11 | *margin-bottom: -3px; 12 | } 13 | .dropdown-toggle:active, 14 | .open .dropdown-toggle { 15 | outline: 0; 16 | } 17 | 18 | // Dropdown arrow/caret 19 | // -------------------- 20 | .caret { 21 | display: inline-block; 22 | width: 0; 23 | height: 0; 24 | vertical-align: top; 25 | border-top: 4px solid @black; 26 | border-right: 4px solid transparent; 27 | border-left: 4px solid transparent; 28 | content: ""; 29 | .opacity(30); 30 | } 31 | 32 | // Place the caret 33 | .dropdown .caret { 34 | margin-top: 8px; 35 | margin-left: 2px; 36 | } 37 | .dropdown:hover .caret, 38 | .open .caret { 39 | .opacity(100); 40 | } 41 | 42 | // The dropdown menu (ul) 43 | // ---------------------- 44 | .dropdown-menu { 45 | position: absolute; 46 | top: 100%; 47 | left: 0; 48 | z-index: @zindexDropdown; 49 | display: none; // none by default, but block on "open" of the menu 50 | float: left; 51 | min-width: 160px; 52 | padding: 4px 0; 53 | margin: 1px 0 0; // override default ul 54 | list-style: none; 55 | background-color: @dropdownBackground; 56 | border: 1px solid #ccc; 57 | border: 1px solid rgba(0,0,0,.2); 58 | *border-right-width: 2px; 59 | *border-bottom-width: 2px; 60 | .border-radius(5px); 61 | .box-shadow(0 5px 10px rgba(0,0,0,.2)); 62 | -webkit-background-clip: padding-box; 63 | -moz-background-clip: padding; 64 | background-clip: padding-box; 65 | 66 | // Aligns the dropdown menu to right 67 | &.pull-right { 68 | right: 0; 69 | left: auto; 70 | } 71 | 72 | // Dividers (basically an hr) within the dropdown 73 | .divider { 74 | .nav-divider(@dropdownDividerTop, @dropdownDividerBottom); 75 | } 76 | 77 | // Links within the dropdown menu 78 | a { 79 | display: block; 80 | padding: 3px 15px; 81 | clear: both; 82 | font-weight: normal; 83 | line-height: @baseLineHeight; 84 | color: @dropdownLinkColor; 85 | white-space: nowrap; 86 | } 87 | } 88 | 89 | // Hover state 90 | // ----------- 91 | .dropdown-menu li > a:hover, 92 | .dropdown-menu .active > a, 93 | .dropdown-menu .active > a:hover { 94 | color: @dropdownLinkColorHover; 95 | text-decoration: none; 96 | background-color: @dropdownLinkBackgroundHover; 97 | } 98 | 99 | // Open state for the dropdown 100 | // --------------------------- 101 | .open { 102 | // IE7's z-index only goes to the nearest positioned ancestor, which would 103 | // make the menu appear below buttons that appeared later on the page 104 | *z-index: @zindexDropdown; 105 | 106 | & > .dropdown-menu { 107 | display: block; 108 | } 109 | } 110 | 111 | // Right aligned dropdowns 112 | // --------------------------- 113 | .pull-right > .dropdown-menu { 114 | right: 0; 115 | left: auto; 116 | } 117 | 118 | // Allow for dropdowns to go bottom up (aka, dropup-menu) 119 | // ------------------------------------------------------ 120 | // Just add .dropup after the standard .dropdown class and you're set, bro. 121 | // TODO: abstract this so that the navbar fixed styles are not placed here? 122 | .dropup, 123 | .navbar-fixed-bottom .dropdown { 124 | // Reverse the caret 125 | .caret { 126 | border-top: 0; 127 | border-bottom: 4px solid @black; 128 | content: "\2191"; 129 | } 130 | // Different positioning for bottom up menu 131 | .dropdown-menu { 132 | top: auto; 133 | bottom: 100%; 134 | margin-bottom: 1px; 135 | } 136 | } 137 | 138 | // Typeahead 139 | // --------- 140 | .typeahead { 141 | margin-top: 2px; // give it some space to breathe 142 | .border-radius(4px); 143 | } 144 | -------------------------------------------------------------------------------- /resources/public/less/responsive-navbar.less: -------------------------------------------------------------------------------- 1 | // TABLETS AND BELOW 2 | // ----------------- 3 | @media (max-width: 979px) { 4 | 5 | // UNFIX THE TOPBAR 6 | // ---------------- 7 | // Remove any padding from the body 8 | body { 9 | padding-top: 0; 10 | } 11 | // Unfix the navbar 12 | .navbar-fixed-top, 13 | .navbar-fixed-bottom { 14 | position: static; 15 | } 16 | .navbar-fixed-top { 17 | margin-bottom: @baseLineHeight; 18 | } 19 | .navbar-fixed-bottom { 20 | margin-top: @baseLineHeight; 21 | } 22 | .navbar-fixed-top .navbar-inner, 23 | .navbar-fixed-bottom .navbar-inner { 24 | padding: 5px; 25 | } 26 | .navbar .container { 27 | width: auto; 28 | padding: 0; 29 | } 30 | // Account for brand name 31 | .navbar .brand { 32 | padding-left: 10px; 33 | padding-right: 10px; 34 | margin: 0 0 0 -5px; 35 | } 36 | 37 | // COLLAPSIBLE NAVBAR 38 | // ------------------ 39 | // Nav collapse clears brand 40 | .nav-collapse { 41 | clear: both; 42 | } 43 | // Block-level the nav 44 | .nav-collapse .nav { 45 | float: none; 46 | margin: 0 0 (@baseLineHeight / 2); 47 | } 48 | .nav-collapse .nav > li { 49 | float: none; 50 | } 51 | .nav-collapse .nav > li > a { 52 | margin-bottom: 2px; 53 | } 54 | .nav-collapse .nav > .divider-vertical { 55 | display: none; 56 | } 57 | .nav-collapse .nav .nav-header { 58 | color: @navbarText; 59 | text-shadow: none; 60 | } 61 | // Nav and dropdown links in navbar 62 | .nav-collapse .nav > li > a, 63 | .nav-collapse .dropdown-menu a { 64 | padding: 6px 15px; 65 | font-weight: bold; 66 | color: @navbarLinkColor; 67 | .border-radius(3px); 68 | } 69 | // Buttons 70 | .nav-collapse .btn { 71 | padding: 4px 10px 4px; 72 | font-weight: normal; 73 | .border-radius(4px); 74 | } 75 | .nav-collapse .dropdown-menu li + li a { 76 | margin-bottom: 2px; 77 | } 78 | .nav-collapse .nav > li > a:hover, 79 | .nav-collapse .dropdown-menu a:hover { 80 | background-color: @navbarBackground; 81 | } 82 | // Buttons in the navbar 83 | .nav-collapse.in .btn-group { 84 | margin-top: 5px; 85 | padding: 0; 86 | } 87 | // Dropdowns in the navbar 88 | .nav-collapse .dropdown-menu { 89 | position: static; 90 | top: auto; 91 | left: auto; 92 | float: none; 93 | display: block; 94 | max-width: none; 95 | margin: 0 15px; 96 | padding: 0; 97 | background-color: transparent; 98 | border: none; 99 | .border-radius(0); 100 | .box-shadow(none); 101 | } 102 | .nav-collapse .dropdown-menu:before, 103 | .nav-collapse .dropdown-menu:after { 104 | display: none; 105 | } 106 | .nav-collapse .dropdown-menu .divider { 107 | display: none; 108 | } 109 | // Forms in navbar 110 | .nav-collapse .navbar-form, 111 | .nav-collapse .navbar-search { 112 | float: none; 113 | padding: (@baseLineHeight / 2) 15px; 114 | margin: (@baseLineHeight / 2) 0; 115 | border-top: 1px solid @navbarBackground; 116 | border-bottom: 1px solid @navbarBackground; 117 | .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1)"); 118 | } 119 | // Pull right (secondary) nav content 120 | .navbar .nav-collapse .nav.pull-right { 121 | float: none; 122 | margin-left: 0; 123 | } 124 | // Hide everything in the navbar save .brand and toggle button */ 125 | .nav-collapse, 126 | .nav-collapse.collapse { 127 | overflow: hidden; 128 | height: 0; 129 | } 130 | // Navbar button 131 | .navbar .btn-navbar { 132 | display: block; 133 | } 134 | 135 | // STATIC NAVBAR 136 | // ------------- 137 | .navbar-static .navbar-inner { 138 | padding-left: 10px; 139 | padding-right: 10px; 140 | } 141 | } 142 | 143 | 144 | // DEFAULT DESKTOP 145 | // --------------- 146 | 147 | // Required to make the collapsing navbar work on regular desktops 148 | @media (min-width: 980px) { 149 | .nav-collapse.collapse { 150 | height: auto !important; 151 | overflow: visible !important; 152 | } 153 | } -------------------------------------------------------------------------------- /src-cljs/torrent_client/core/bencode.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.core.bencode 2 | (:use 3 | [clojure.walk :only [keywordize-keys]] 4 | [torrent-client.core.byte-array :only [uint8-array]]) 5 | (:require 6 | [goog.crypt :as crypt] 7 | [torrent-client.core.reader :as reader])) 8 | 9 | (defn char [characters] 10 | (cond 11 | (goog.isNumber characters) 12 | (.fromCharCode js/String characters) 13 | (vector? characters) 14 | (.apply (.-fromCharCode js/String) nil (clj->js characters)) 15 | :else 16 | (.apply (.-fromCharCode js/String) nil characters))) 17 | 18 | (declare decode-dispatch) 19 | 20 | (defn- decode-number [stream delimeter & ch] 21 | (loop [i (if (nil? ch) (reader/read stream) (first ch)), result ""] 22 | (let [c (char i)] 23 | (if (= c delimeter) 24 | (js/parseInt result) 25 | (recur (reader/read stream) (str result c)))))) 26 | 27 | (defn- decode-string [stream ch] 28 | (let [length (decode-number stream ":" ch)] 29 | (char (reader/read stream length)))) 30 | 31 | (defn- decode-list [stream] 32 | (loop [result []] 33 | (let [c (reader/read stream)] 34 | ; If c is e - the end character then return the result 35 | (if (= c 101) 36 | result 37 | (recur (conj result (decode-dispatch stream c))))))) 38 | 39 | (defn- decode-map [stream] 40 | (let [list (decode-list stream)] 41 | (apply array-map (map #(%1 %2) (cycle [keyword identity]) list)))) 42 | 43 | (defn- decode-dispatch [stream & i] 44 | (let [indicator (if (nil? i) (reader/read stream) (first i))] 45 | (cond 46 | ; indicator is a number (indicates a string) 47 | (and (>= indicator 48) (<= indicator 57)) (decode-string stream indicator) 48 | ; indicator is i 49 | (= indicator 105) (decode-number stream "e") 50 | ; indicator is l 51 | (= indicator 108) (decode-list stream) 52 | ; indicator is d 53 | (= indicator 100) (decode-map stream)))) 54 | 55 | (defn decode [stream & flags] 56 | (let [output (decode-dispatch stream) 57 | data (reader/rem stream)] 58 | ; Check if data at the end of the stream was requested 59 | (if-not (contains? (set flags) :payload) 60 | output 61 | ; Otherwise return the object and the remainder 62 | [output data]))) 63 | 64 | (defprotocol ArrayOutputStream 65 | (write [array bytes] "append the bytes to the array")) 66 | 67 | (deftype ByteArrayOutputStream [array] 68 | ArrayOutputStream 69 | (write [_ bytes] 70 | (if (number? bytes) 71 | (.push array bytes) 72 | (.apply (.-push array) array bytes)) 73 | array) 74 | ) 75 | 76 | (defn byte-array-output-stream [] 77 | "Generate a new ByteArrayOutputStream with a native 78 | javascript array (performance reasons)" 79 | (ByteArrayOutputStream. (js/Array))) 80 | 81 | (declare encode-object) 82 | 83 | (defn- encode-string [obj stream] 84 | (let [bytes (crypt/stringToByteArray obj) 85 | bytes-length (crypt/stringToByteArray (str (count bytes) ":"))] 86 | (write stream bytes-length) 87 | (write stream bytes))) 88 | 89 | (defn- encode-number [number stream] 90 | (let [string (str "i" number "e") 91 | bytes (crypt/stringToByteArray string)] 92 | (write stream bytes))) 93 | 94 | (defn- encode-seq [seq stream] 95 | (write stream 108) 96 | ; (js* "debugger;") 97 | (doseq [item seq] 98 | (encode-object item stream)) 99 | (write stream 101)) 100 | 101 | (defn- encode-dictionary [dictionary stream] 102 | (write stream 100) 103 | ; (js* "debugger;") 104 | (doseq [[key item] (seq dictionary)] 105 | (encode-object key stream) 106 | (encode-object item stream)) 107 | (write stream 101)) 108 | 109 | (defn- encode-object [obj stream] 110 | ; (js* "debugger;") 111 | (cond (keyword? obj) (encode-string (name obj) stream) 112 | (string? obj) (encode-string obj stream) 113 | (number? obj) (encode-number obj stream) 114 | (sequential? obj) (encode-seq obj stream) 115 | (map? obj) (encode-dictionary obj stream))) 116 | 117 | (defn encode [obj] 118 | (let [stream (byte-array-output-stream)] 119 | (encode-object obj stream) 120 | (.-array stream))) -------------------------------------------------------------------------------- /src-cljs/torrent_client/bitfield.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.bitfield 2 | (:require [torrent-client.core.dispatch :as dispatch]) 3 | (:use [torrent-client.core.byte-array :only [uint8-array?]])) 4 | 5 | (deftype Bitfield [byte-array i] 6 | 7 | Object 8 | (toString [this] (pr-str byte-array)) 9 | 10 | IEncodeJS 11 | (-clj->js [_] byte-array) 12 | 13 | IIndexed 14 | (-nth [bitfield n] 15 | (let [n (+ n i) 16 | piece (bit-shift-right n 3) 17 | bit (bit-shift-right 128 (mod n 8))] 18 | (bit-and (aget byte-array piece) bit))) 19 | 20 | IFn 21 | (-invoke [bitfield] byte-array) 22 | 23 | IAssociative 24 | (-assoc [_ k v] 25 | (let [k (+ k i) 26 | byte-index (bit-shift-right k 3) 27 | byte (aget byte-array byte-index) 28 | ; bitorrent uses the high bit as 0 29 | ; but bit-set/clear uses the low bit as 0 30 | piece-bit (- 7 (mod k 8)) 31 | ; How are we modifying the byte? 32 | f (if v bit-set bit-clear)] 33 | (aset byte-array byte-index (f byte piece-bit)))) 34 | 35 | ICollection 36 | (-conj [coll entry] 37 | (if (vector? entry) 38 | (-assoc coll (-nth entry 0) (-nth entry 1)) 39 | (reduce -conj 40 | coll 41 | entry))) 42 | 43 | ISeqable 44 | (-seq [this] this) 45 | 46 | ASeq 47 | ISeq 48 | (-first [bitfield] (nth bitfield 0)) 49 | (-rest [bitfield] 50 | ; If we can create a new bitfield with length >= 1 51 | (if (pos? (dec (count bitfield))) 52 | (Bitfield. byte-array (inc i)))) 53 | 54 | ICounted 55 | ; Return the number of bits in the bitfield 56 | (-count [_] (- (* 8 (count byte-array)) i)) 57 | 58 | ) 59 | 60 | (defn bitfield [bits] 61 | (cond 62 | (integer? bits) 63 | ; Create a new bitfield, given the number 64 | ; of bits in it 65 | (let [length (Math/ceil (/ bits 8)) 66 | byte-array (js/Uint8Array. length)] 67 | (Bitfield. byte-array 0)) 68 | 69 | ; Construct a bitfield from an existing one 70 | ; (received from peer) 71 | (uint8-array? bits) 72 | ; TODO: check this 73 | ; (let [byte-array (js/Uint8Array. bits)] 74 | (Bitfield. bits 0) 75 | 76 | :else 77 | (let [byte-array (js/Uint8Array. (clj->js bits))] 78 | (Bitfield. byte-array 0)))) 79 | 80 | (defn fill-bitfield! 81 | "Given a bitfield mark that we have all the pieces" 82 | [bitfield pieces-length] 83 | (let [full-bytes (quot pieces-length 8) 84 | represented-bits (* full-bytes 8)] 85 | ; The last bitfield byte may be partial 86 | (dotimes [n full-bytes] 87 | (aset (.-byte-array bitfield) n 255)) 88 | (doseq [n (range represented-bits pieces-length)] 89 | (assoc bitfield n true)) 90 | bitfield)) 91 | 92 | ;;************************************************ 93 | ;; Bitfield methods 94 | ;; modeled after clojure.set 95 | ;; TODO: seperate out 96 | ;;************************************************ 97 | 98 | (defn intersection [b1 b2] 99 | (let [byte-array1 (.-byte-array b1) 100 | byte-array2 (.-byte-array b2)] 101 | ; loop over the byte array (each byte) 102 | ; not the bitfield (each bit) 103 | (bitfield (doall (map bit-and byte-array1 byte-array2))))) 104 | 105 | (defn union [b1 b2] 106 | (let [byte-array1 (.-byte-array b1) 107 | byte-array2 (.-byte-array b2)] 108 | (bitfield (doall (map bit-or byte-array1 byte-array2))))) 109 | 110 | ; Modelled after set/difference 111 | (defn difference 112 | ([b1 b2] 113 | (let [byte-array1 (.-byte-array b1) 114 | byte-array2 (.-byte-array b2) 115 | ; get the unique bits (inverse intersection) 116 | unique (map bit-xor byte-array1 byte-array2)] 117 | ; the unique bits in byte-array 1 118 | (intersection b1 (bitfield unique)))) 119 | ([b1 b2 & bitfields] 120 | (reduce difference b1 (conj bitfields b2)))) 121 | 122 | ;;************************************************ 123 | ;; Event handling 124 | ;;************************************************ 125 | 126 | (dispatch/react-to #{:written-piece} (fn [_ [torrent piece-index]] 127 | (let [bitfield (@torrent :bitfield)] 128 | (assoc bitfield piece-index true) 129 | ; (if (filter #(= % 0xff) (.-byte-array bitfield)) 130 | ; (dispatch/fire :completed-torrent torrent)) 131 | ))) -------------------------------------------------------------------------------- /src-cljs/torrent_client/ui/views.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.ui.views 2 | (:require 3 | [clojure.string :as string] 4 | [crate.core :as crate] 5 | [crate.form :as form]) 6 | (:use 7 | [torrent-client.torrent :only [active? paused? completed? downloading? has-full-metadata?]] 8 | [crate.binding :only [bound]] 9 | [goog.format :only [numBytesToString]] 10 | [torrent-client.files :only [files]]) 11 | (:use-macros 12 | [crate.def-macros :only [defpartial]])) 13 | 14 | ;;************************************************ 15 | ;; View methods 16 | ;;************************************************ 17 | 18 | (defn html [& args] 19 | "Help crate deal with logic" 20 | (let [args (remove nil? args)] 21 | (apply crate/html args))) 22 | 23 | (defn download-percent 24 | "Percentage of pieces saved to needed, rough estimate of progress" 25 | [torrent] 26 | (let [percent (/ (torrent :pieces-written) (torrent :pieces-length))] 27 | (str (/ percent 0.01) "%"))) 28 | 29 | (defn total-length-to-string [{:keys [total-length]}] 30 | (string/lower-case (numBytesToString total-length))) 31 | 32 | (defn time-remaining-to-string [torrent] 33 | "-" 34 | ; (if (paused? torrent) 35 | ; "∞" 36 | ; (let [; The number of seconds remaining 37 | ; seconds (/ (torrent :total-length) (* 700 1000))] 38 | ; seconds)) 39 | ) 40 | 41 | (defn torrent-speed-to-string [torrent] 42 | "-") 43 | 44 | (defn file-url [file] 45 | "Given a file return a link to it's localstorage entry" 46 | (.toURL (.-file file))) 47 | 48 | ;;************************************************ 49 | ;; Views 50 | ;;************************************************ 51 | 52 | (defpartial alert [content] 53 | [:div.alert 54 | [:button.close {:type "button" :data-dismiss "alert"} "×"] 55 | content]) 56 | 57 | (defpartial torrent-file-badge [content] 58 | [:span.label (.-name content)]) 59 | 60 | (defpartial share-modal [content] 61 | [:div.modal 62 | [:div.modal-header 63 | [:h3 (str (content :name) " is ready to share!")] 64 | [:button.close {:type "button" :data-dismiss "modal"}]] 65 | [:form#create-form.modal-body.form-horizontal 66 | [:div.control-group 67 | (form/label {:class "control-label"} "link" "download link") 68 | [:div.controls 69 | (form/text-field {:value (content :magnet-url) :class "input-xlarge"} "link")]] 70 | ; [:div.control-group 71 | ; [:div.controls 72 | ; [:a#built-download 73 | ; {:download (get-in content [:torrent-file .-name]) 74 | ; :title (str "download " (get-in content [:torrent-file .-name])) 75 | ; :href (file-url (content :torrent-file))} 76 | ; "or download the .torrent"]]] 77 | ] 78 | [:div.modal-footer 79 | [:a.btn {:data-dismiss "modal"} "close"]]]) 80 | 81 | (defn- torrent-progress [torrent] 82 | (if-not (has-full-metadata? torrent) 83 | [:div.progress.progress-striped.progress-success.active 84 | [:div.bar.bar-success {:style {:width "100%"}}] 85 | [:label.label "starting..."]] 86 | [:div {:class (str "progress progress-striped" 87 | (if (active? torrent) " active"))} 88 | [:div.bar {:style {:width (download-percent torrent)}}] 89 | [:label {:class (str "label" (if-not (active? torrent) " hide"))} 90 | (time-remaining-to-string torrent)]])) 91 | 92 | (defn- action-browse [torrent] 93 | (let [files (@files (torrent :pretty-info-hash))] 94 | (if (and (not-empty files) (completed? torrent)) 95 | [:a.btn {:href (file-url (first files)) :target "_blank"} 96 | [:i.icon-folder-open]]))) 97 | 98 | (defn- torrent-actions [torrent] 99 | [:div.btn-group 100 | (if (has-full-metadata? torrent) 101 | (html 102 | [:button.btn.share 103 | [:i.icon-globe]] 104 | [:button.btn {:disabled true} 105 | [:i {:class (if (active? torrent) "icon-pause" "icon-play")}]] 106 | (action-browse torrent)) 107 | ; Crate cannot take nil thus the if cannot be uncaught 108 | (html)) 109 | [:button.btn {:disabled true} 110 | [:i.icon-trash]]]) 111 | 112 | (defpartial torrent-row [torrent] 113 | [:tr 114 | [:td.flex2.name (@torrent :name)] 115 | [:td.flex1.size (bound torrent total-length-to-string)] 116 | [:td.flex5.progress-td (bound torrent torrent-progress)] 117 | [:td.flex1.speed (bound torrent torrent-speed-to-string)] 118 | [:td.actions (bound torrent torrent-actions)]]) -------------------------------------------------------------------------------- /resources/public/less/type.less: -------------------------------------------------------------------------------- 1 | // Typography.less 2 | // Headings, body text, lists, code, and more for a versatile and durable typography system 3 | // ---------------------------------------------------------------------------------------- 4 | 5 | 6 | // BODY TEXT 7 | // --------- 8 | 9 | p { 10 | margin: 0 0 @baseLineHeight / 2; 11 | small { 12 | font-size: @baseFontSize - 2; 13 | color: @grayLight; 14 | } 15 | } 16 | .lead { 17 | margin-bottom: @baseLineHeight; 18 | font-size: 20px; 19 | font-weight: 200; 20 | line-height: @baseLineHeight * 1.5; 21 | } 22 | 23 | // HEADINGS 24 | // -------- 25 | 26 | h1, h2, h3, h4, h5, h6 { 27 | margin: 0; 28 | font-family: @headingsFontFamily; 29 | font-weight: @headingsFontWeight; 30 | color: @headingsColor; 31 | text-rendering: optimizelegibility; // Fix the character spacing for headings 32 | small { 33 | font-weight: normal; 34 | color: @grayLight; 35 | } 36 | } 37 | h1 { 38 | font-size: 30px; 39 | line-height: @baseLineHeight * 2; 40 | small { 41 | font-size: 18px; 42 | } 43 | } 44 | h2 { 45 | font-size: 24px; 46 | line-height: @baseLineHeight * 2; 47 | small { 48 | font-size: 18px; 49 | } 50 | } 51 | h3 { 52 | font-size: 18px; 53 | line-height: @baseLineHeight * 1.5; 54 | small { 55 | font-size: 14px; 56 | } 57 | } 58 | h4, h5, h6 { 59 | line-height: @baseLineHeight; 60 | } 61 | h4 { 62 | font-size: 14px; 63 | small { 64 | font-size: 12px; 65 | } 66 | } 67 | h5 { 68 | font-size: 12px; 69 | } 70 | h6 { 71 | font-size: 11px; 72 | color: @grayLight; 73 | text-transform: uppercase; 74 | } 75 | 76 | // Page header 77 | .page-header { 78 | padding-bottom: @baseLineHeight - 1; 79 | margin: @baseLineHeight 0; 80 | border-bottom: 1px solid @grayLighter; 81 | } 82 | .page-header h1 { 83 | line-height: 1; 84 | } 85 | 86 | 87 | 88 | // LISTS 89 | // ----- 90 | 91 | // Unordered and Ordered lists 92 | ul, ol { 93 | padding: 0; 94 | margin: 0 0 @baseLineHeight / 2 25px; 95 | } 96 | ul ul, 97 | ul ol, 98 | ol ol, 99 | ol ul { 100 | margin-bottom: 0; 101 | } 102 | ul { 103 | list-style: disc; 104 | } 105 | ol { 106 | list-style: decimal; 107 | } 108 | li { 109 | line-height: @baseLineHeight; 110 | } 111 | ul.unstyled, 112 | ol.unstyled { 113 | margin-left: 0; 114 | list-style: none; 115 | } 116 | 117 | // Description Lists 118 | dl { 119 | margin-bottom: @baseLineHeight; 120 | } 121 | dt, 122 | dd { 123 | line-height: @baseLineHeight; 124 | } 125 | dt { 126 | font-weight: bold; 127 | line-height: @baseLineHeight - 1; // fix jank Helvetica Neue font bug 128 | } 129 | dd { 130 | margin-left: @baseLineHeight / 2; 131 | } 132 | // Horizontal layout (like forms) 133 | .dl-horizontal { 134 | dt { 135 | float: left; 136 | width: 120px; 137 | clear: left; 138 | text-align: right; 139 | .text-overflow(); 140 | } 141 | dd { 142 | margin-left: 130px; 143 | } 144 | } 145 | 146 | // MISC 147 | // ---- 148 | 149 | // Horizontal rules 150 | hr { 151 | margin: @baseLineHeight 0; 152 | border: 0; 153 | border-top: 1px solid @hrBorder; 154 | border-bottom: 1px solid @white; 155 | } 156 | 157 | // Emphasis 158 | strong { 159 | font-weight: bold; 160 | } 161 | em { 162 | font-style: italic; 163 | } 164 | .muted { 165 | color: @grayLight; 166 | } 167 | 168 | // Abbreviations and acronyms 169 | abbr[title] { 170 | cursor: help; 171 | border-bottom: 1px dotted @grayLight; 172 | } 173 | abbr.initialism { 174 | font-size: 90%; 175 | text-transform: uppercase; 176 | } 177 | 178 | // Blockquotes 179 | blockquote { 180 | padding: 0 0 0 15px; 181 | margin: 0 0 @baseLineHeight; 182 | border-left: 5px solid @grayLighter; 183 | p { 184 | margin-bottom: 0; 185 | #font > .shorthand(16px,300,@baseLineHeight * 1.25); 186 | } 187 | small { 188 | display: block; 189 | line-height: @baseLineHeight; 190 | color: @grayLight; 191 | &:before { 192 | content: '\2014 \00A0'; 193 | } 194 | } 195 | 196 | // Float right with text-align: right 197 | &.pull-right { 198 | float: right; 199 | padding-right: 15px; 200 | padding-left: 0; 201 | border-right: 5px solid @grayLighter; 202 | border-left: 0; 203 | p, 204 | small { 205 | text-align: right; 206 | } 207 | } 208 | } 209 | 210 | // Quotes 211 | q:before, 212 | q:after, 213 | blockquote:before, 214 | blockquote:after { 215 | content: ""; 216 | } 217 | 218 | // Addresses 219 | address { 220 | display: block; 221 | margin-bottom: @baseLineHeight; 222 | font-style: normal; 223 | line-height: @baseLineHeight; 224 | } 225 | 226 | // Misc 227 | small { 228 | font-size: 100%; 229 | } 230 | cite { 231 | font-style: normal; 232 | } 233 | -------------------------------------------------------------------------------- /resources/public/less/tests/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap, from Twitter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 55 | 56 |
    57 | 58 | 59 | 78 | 79 | 80 |
    81 |

    Navbar example

    82 |

    This example is a quick exercise to illustrate how the default, static navbar and fixed to top navbar work. It includes the responsive CSS and HTML, so it also adapts to your viewport and device.

    83 |

    84 | View navbar docs » 85 |

    86 |
    87 | 88 |
    89 | 90 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /resources/public/less/tests/forms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap, from Twitter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
    36 | 37 | 40 | 41 |
    42 |
    43 | 44 | 45 | 50 | 51 |
    52 | 53 | 54 | 55 | 56 |
    57 | 58 | 59 | 60 | 61 |
    62 | 63 | 64 | 65 | 66 |
    67 | 68 | 69 | 70 | 71 |
    72 | 73 | 74 | 75 | 76 |
    77 | 78 | 79 | 80 | 81 |
    82 | 83 | 84 | 85 | 86 |
    87 | 88 | 89 | 90 | 91 |
    92 |
    93 | 94 | 95 | 96 | 97 |
    98 | 99 | 100 | 101 | 102 |
    103 | 104 | 105 | 106 | 107 |
    108 | 109 | 110 | 111 | 112 |
    113 | 114 | 115 | 116 | 117 |
    118 | 119 | 120 | 121 | 122 |
    123 | 124 | 125 | 126 | 127 |
    128 | 129 | 130 | 131 | 132 |
    133 | 134 | 135 | 136 | 137 |
    138 |
    139 | 140 | 141 | 142 | 143 |
    144 | 145 | 146 | 147 | 148 |
    149 | 150 | 151 | 152 | 153 |
    154 | 155 | 156 | 157 | 158 |
    159 | 160 | 161 | 162 | 163 |
    164 | 165 | 166 | 167 | 168 |
    169 | 170 | 171 | 172 | 173 |
    174 |
    175 | 176 |
    177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /resources/public/less/tables.less: -------------------------------------------------------------------------------- 1 | // 2 | // Tables.less 3 | // Tables for, you guessed it, tabular data 4 | // ---------------------------------------- 5 | 6 | 7 | // BASE TABLES 8 | // ----------------- 9 | 10 | table { 11 | max-width: 100%; 12 | background-color: @tableBackground; 13 | border-collapse: collapse; 14 | border-spacing: 0; 15 | } 16 | 17 | // BASELINE STYLES 18 | // --------------- 19 | 20 | .table { 21 | width: 100%; 22 | margin-bottom: @baseLineHeight; 23 | // Cells 24 | th, 25 | td { 26 | padding: 8px; 27 | line-height: @baseLineHeight; 28 | text-align: left; 29 | vertical-align: top; 30 | border-top: 1px solid @tableBorder; 31 | } 32 | th { 33 | font-weight: bold; 34 | } 35 | // Bottom align for column headings 36 | thead th { 37 | vertical-align: bottom; 38 | } 39 | // Remove top border from thead by default 40 | caption + thead tr:first-child th, 41 | caption + thead tr:first-child td, 42 | colgroup + thead tr:first-child th, 43 | colgroup + thead tr:first-child td, 44 | thead:first-child tr:first-child th, 45 | thead:first-child tr:first-child td { 46 | border-top: 0; 47 | } 48 | // Account for multiple tbody instances 49 | tbody + tbody { 50 | border-top: 2px solid @tableBorder; 51 | } 52 | } 53 | 54 | 55 | 56 | // CONDENSED TABLE W/ HALF PADDING 57 | // ------------------------------- 58 | 59 | .table-condensed { 60 | th, 61 | td { 62 | padding: 4px 5px; 63 | } 64 | } 65 | 66 | 67 | // BORDERED VERSION 68 | // ---------------- 69 | 70 | .table-bordered { 71 | border: 1px solid @tableBorder; 72 | border-collapse: separate; // Done so we can round those corners! 73 | *border-collapse: collapsed; // IE7 can't round corners anyway 74 | border-left: 0; 75 | .border-radius(4px); 76 | th, 77 | td { 78 | border-left: 1px solid @tableBorder; 79 | } 80 | // Prevent a double border 81 | caption + thead tr:first-child th, 82 | caption + tbody tr:first-child th, 83 | caption + tbody tr:first-child td, 84 | colgroup + thead tr:first-child th, 85 | colgroup + tbody tr:first-child th, 86 | colgroup + tbody tr:first-child td, 87 | thead:first-child tr:first-child th, 88 | tbody:first-child tr:first-child th, 89 | tbody:first-child tr:first-child td { 90 | border-top: 0; 91 | } 92 | // For first th or td in the first row in the first thead or tbody 93 | thead:first-child tr:first-child th:first-child, 94 | tbody:first-child tr:first-child td:first-child { 95 | -webkit-border-top-left-radius: 4px; 96 | border-top-left-radius: 4px; 97 | -moz-border-radius-topleft: 4px; 98 | } 99 | thead:first-child tr:first-child th:last-child, 100 | tbody:first-child tr:first-child td:last-child { 101 | -webkit-border-top-right-radius: 4px; 102 | border-top-right-radius: 4px; 103 | -moz-border-radius-topright: 4px; 104 | } 105 | // For first th or td in the first row in the first thead or tbody 106 | thead:last-child tr:last-child th:first-child, 107 | tbody:last-child tr:last-child td:first-child { 108 | .border-radius(0 0 0 4px); 109 | -webkit-border-bottom-left-radius: 4px; 110 | border-bottom-left-radius: 4px; 111 | -moz-border-radius-bottomleft: 4px; 112 | } 113 | thead:last-child tr:last-child th:last-child, 114 | tbody:last-child tr:last-child td:last-child { 115 | -webkit-border-bottom-right-radius: 4px; 116 | border-bottom-right-radius: 4px; 117 | -moz-border-radius-bottomright: 4px; 118 | } 119 | } 120 | 121 | 122 | // ZEBRA-STRIPING 123 | // -------------- 124 | 125 | // Default zebra-stripe styles (alternating gray and transparent backgrounds) 126 | .table-striped { 127 | tbody { 128 | tr:nth-child(odd) td, 129 | tr:nth-child(odd) th { 130 | background-color: @tableBackgroundAccent; 131 | } 132 | } 133 | } 134 | 135 | 136 | // HOVER EFFECT 137 | // ------------ 138 | // Placed here since it has to come after the potential zebra striping 139 | .table { 140 | tbody tr:hover td, 141 | tbody tr:hover th { 142 | background-color: @tableBackgroundHover; 143 | } 144 | } 145 | 146 | 147 | // TABLE CELL SIZING 148 | // ----------------- 149 | 150 | // Change the columns 151 | table { 152 | .span1 { .tableColumns(1); } 153 | .span2 { .tableColumns(2); } 154 | .span3 { .tableColumns(3); } 155 | .span4 { .tableColumns(4); } 156 | .span5 { .tableColumns(5); } 157 | .span6 { .tableColumns(6); } 158 | .span7 { .tableColumns(7); } 159 | .span8 { .tableColumns(8); } 160 | .span9 { .tableColumns(9); } 161 | .span10 { .tableColumns(10); } 162 | .span11 { .tableColumns(11); } 163 | .span12 { .tableColumns(12); } 164 | .span13 { .tableColumns(13); } 165 | .span14 { .tableColumns(14); } 166 | .span15 { .tableColumns(15); } 167 | .span16 { .tableColumns(16); } 168 | .span17 { .tableColumns(17); } 169 | .span18 { .tableColumns(18); } 170 | .span19 { .tableColumns(19); } 171 | .span20 { .tableColumns(20); } 172 | .span21 { .tableColumns(21); } 173 | .span22 { .tableColumns(22); } 174 | .span23 { .tableColumns(23); } 175 | .span24 { .tableColumns(24); } 176 | } 177 | -------------------------------------------------------------------------------- /resources/public/less/buttons.less: -------------------------------------------------------------------------------- 1 | // BUTTON STYLES 2 | // ------------- 3 | 4 | 5 | // Base styles 6 | // -------------------------------------------------- 7 | 8 | // Core 9 | .btn { 10 | display: inline-block; 11 | .ie7-inline-block(); 12 | padding: 4px 10px 4px; 13 | margin-bottom: 0; // For input.btn 14 | font-size: @baseFontSize; 15 | line-height: @baseLineHeight; 16 | *line-height: 20px; 17 | color: @grayDark; 18 | text-align: center; 19 | text-shadow: 0 1px 1px rgba(255,255,255,.75); 20 | vertical-align: middle; 21 | cursor: pointer; 22 | .buttonBackground(@btnBackground, @btnBackgroundHighlight); 23 | border: 1px solid @btnBorder; 24 | *border: 0; // Remove the border to prevent IE7's black border on input:focus 25 | border-bottom-color: darken(@btnBorder, 10%); 26 | .border-radius(4px); 27 | .ie7-restore-left-whitespace(); // Give IE7 some love 28 | .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)"); 29 | } 30 | 31 | // Hover state 32 | .btn:hover { 33 | color: @grayDark; 34 | text-decoration: none; 35 | background-color: darken(@white, 10%); 36 | *background-color: darken(@white, 15%); /* Buttons in IE7 don't get borders, so darken on hover */ 37 | background-position: 0 -15px; 38 | 39 | // transition is only when going to hover, otherwise the background 40 | // behind the gradient (there for IE<=9 fallback) gets mismatched 41 | .transition(background-position .1s linear); 42 | } 43 | 44 | // Focus state for keyboard and accessibility 45 | .btn:focus { 46 | .tab-focus(); 47 | } 48 | 49 | // Active state 50 | .btn.active, 51 | .btn:active { 52 | background-color: darken(@white, 10%); 53 | background-color: darken(@white, 15%) e("\9"); 54 | background-image: none; 55 | outline: 0; 56 | .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); 57 | } 58 | 59 | // Disabled state 60 | .btn.disabled, 61 | .btn[disabled] { 62 | cursor: default; 63 | background-color: darken(@white, 10%); 64 | background-image: none; 65 | .opacity(65); 66 | .box-shadow(none); 67 | } 68 | 69 | 70 | // Button Sizes 71 | // -------------------------------------------------- 72 | 73 | // Large 74 | .btn-large { 75 | padding: 9px 14px; 76 | font-size: @baseFontSize + 2px; 77 | line-height: normal; 78 | .border-radius(5px); 79 | } 80 | .btn-large [class^="icon-"] { 81 | margin-top: 1px; 82 | } 83 | 84 | // Small 85 | .btn-small { 86 | padding: 5px 9px; 87 | font-size: @baseFontSize - 2px; 88 | line-height: @baseLineHeight - 2px; 89 | } 90 | .btn-small [class^="icon-"] { 91 | margin-top: -1px; 92 | } 93 | 94 | // Mini 95 | .btn-mini { 96 | padding: 2px 6px; 97 | font-size: @baseFontSize - 2px; 98 | line-height: @baseLineHeight - 4px; 99 | } 100 | 101 | 102 | // Alternate buttons 103 | // -------------------------------------------------- 104 | 105 | // Set text color 106 | // ------------------------- 107 | .btn-primary, 108 | .btn-primary:hover, 109 | .btn-warning, 110 | .btn-warning:hover, 111 | .btn-danger, 112 | .btn-danger:hover, 113 | .btn-success, 114 | .btn-success:hover, 115 | .btn-info, 116 | .btn-info:hover, 117 | .btn-inverse, 118 | .btn-inverse:hover { 119 | color: @white; 120 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 121 | } 122 | // Provide *some* extra contrast for those who can get it 123 | .btn-primary.active, 124 | .btn-warning.active, 125 | .btn-danger.active, 126 | .btn-success.active, 127 | .btn-info.active, 128 | .btn-inverse.active { 129 | color: rgba(255,255,255,.75); 130 | } 131 | 132 | // Set the backgrounds 133 | // ------------------------- 134 | .btn { 135 | // reset here as of 2.0.3 due to Recess property order 136 | border-color: #ccc; 137 | border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25); 138 | } 139 | .btn-primary { 140 | .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight); 141 | } 142 | // Warning appears are orange 143 | .btn-warning { 144 | .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight); 145 | } 146 | // Danger and error appear as red 147 | .btn-danger { 148 | .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); 149 | } 150 | // Success appears as green 151 | .btn-success { 152 | .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight); 153 | } 154 | // Info appears as a neutral blue 155 | .btn-info { 156 | .buttonBackground(@btnInfoBackground, @btnInfoBackgroundHighlight); 157 | } 158 | // Inverse appears as dark gray 159 | .btn-inverse { 160 | .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); 161 | } 162 | 163 | 164 | // Cross-browser Jank 165 | // -------------------------------------------------- 166 | 167 | button.btn, 168 | input[type="submit"].btn { 169 | 170 | // Firefox 3.6 only I believe 171 | &::-moz-focus-inner { 172 | padding: 0; 173 | border: 0; 174 | } 175 | 176 | // IE7 has some default padding on button controls 177 | *padding-top: 2px; 178 | *padding-bottom: 2px; 179 | &.btn-large { 180 | *padding-top: 7px; 181 | *padding-bottom: 7px; 182 | } 183 | &.btn-small { 184 | *padding-top: 3px; 185 | *padding-bottom: 3px; 186 | } 187 | &.btn-mini { 188 | *padding-top: 1px; 189 | *padding-bottom: 1px; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | Ampere, abusus non tollit usum -------------------------------------------------------------------------------- /resources/public/less/button-groups.less: -------------------------------------------------------------------------------- 1 | // BUTTON GROUPS 2 | // ------------- 3 | 4 | 5 | // Make the div behave like a button 6 | .btn-group { 7 | position: relative; 8 | .clearfix(); // clears the floated buttons 9 | .ie7-restore-left-whitespace(); 10 | } 11 | 12 | // Space out series of button groups 13 | .btn-group + .btn-group { 14 | margin-left: 5px; 15 | } 16 | 17 | // Optional: Group multiple button groups together for a toolbar 18 | .btn-toolbar { 19 | margin-top: @baseLineHeight / 2; 20 | margin-bottom: @baseLineHeight / 2; 21 | .btn-group { 22 | display: inline-block; 23 | .ie7-inline-block(); 24 | } 25 | } 26 | 27 | // Float them, remove border radius, then re-add to first and last elements 28 | .btn-group > .btn { 29 | position: relative; 30 | float: left; 31 | margin-left: -1px; 32 | .border-radius(0); 33 | } 34 | // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match 35 | .btn-group > .btn:first-child { 36 | margin-left: 0; 37 | -webkit-border-top-left-radius: 4px; 38 | -moz-border-radius-topleft: 4px; 39 | border-top-left-radius: 4px; 40 | -webkit-border-bottom-left-radius: 4px; 41 | -moz-border-radius-bottomleft: 4px; 42 | border-bottom-left-radius: 4px; 43 | } 44 | // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it 45 | .btn-group > .btn:last-child, 46 | .btn-group > .dropdown-toggle { 47 | -webkit-border-top-right-radius: 4px; 48 | -moz-border-radius-topright: 4px; 49 | border-top-right-radius: 4px; 50 | -webkit-border-bottom-right-radius: 4px; 51 | -moz-border-radius-bottomright: 4px; 52 | border-bottom-right-radius: 4px; 53 | } 54 | // Reset corners for large buttons 55 | .btn-group > .btn.large:first-child { 56 | margin-left: 0; 57 | -webkit-border-top-left-radius: 6px; 58 | -moz-border-radius-topleft: 6px; 59 | border-top-left-radius: 6px; 60 | -webkit-border-bottom-left-radius: 6px; 61 | -moz-border-radius-bottomleft: 6px; 62 | border-bottom-left-radius: 6px; 63 | } 64 | .btn-group > .btn.large:last-child, 65 | .btn-group > .large.dropdown-toggle { 66 | -webkit-border-top-right-radius: 6px; 67 | -moz-border-radius-topright: 6px; 68 | border-top-right-radius: 6px; 69 | -webkit-border-bottom-right-radius: 6px; 70 | -moz-border-radius-bottomright: 6px; 71 | border-bottom-right-radius: 6px; 72 | } 73 | 74 | // On hover/focus/active, bring the proper btn to front 75 | .btn-group > .btn:hover, 76 | .btn-group > .btn:focus, 77 | .btn-group > .btn:active, 78 | .btn-group > .btn.active { 79 | z-index: 2; 80 | } 81 | 82 | // On active and open, don't show outline 83 | .btn-group .dropdown-toggle:active, 84 | .btn-group.open .dropdown-toggle { 85 | outline: 0; 86 | } 87 | 88 | 89 | 90 | // Split button dropdowns 91 | // ---------------------- 92 | 93 | // Give the line between buttons some depth 94 | .btn-group > .dropdown-toggle { 95 | padding-left: 8px; 96 | padding-right: 8px; 97 | .box-shadow(~"inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)"); 98 | *padding-top: 4px; 99 | *padding-bottom: 4px; 100 | } 101 | .btn-group > .btn-mini.dropdown-toggle { 102 | padding-left: 5px; 103 | padding-right: 5px; 104 | } 105 | .btn-group > .btn-small.dropdown-toggle { 106 | *padding-top: 4px; 107 | *padding-bottom: 4px; 108 | } 109 | .btn-group > .btn-large.dropdown-toggle { 110 | padding-left: 12px; 111 | padding-right: 12px; 112 | } 113 | 114 | .btn-group.open { 115 | 116 | // The clickable button for toggling the menu 117 | // Remove the gradient and set the same inset shadow as the :active state 118 | .dropdown-toggle { 119 | background-image: none; 120 | .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); 121 | } 122 | 123 | // Keep the hover's background when dropdown is open 124 | .btn.dropdown-toggle { 125 | background-color: @btnBackgroundHighlight; 126 | } 127 | .btn-primary.dropdown-toggle { 128 | background-color: @btnPrimaryBackgroundHighlight; 129 | } 130 | .btn-warning.dropdown-toggle { 131 | background-color: @btnWarningBackgroundHighlight; 132 | } 133 | .btn-danger.dropdown-toggle { 134 | background-color: @btnDangerBackgroundHighlight; 135 | } 136 | .btn-success.dropdown-toggle { 137 | background-color: @btnSuccessBackgroundHighlight; 138 | } 139 | .btn-info.dropdown-toggle { 140 | background-color: @btnInfoBackgroundHighlight; 141 | } 142 | .btn-inverse.dropdown-toggle { 143 | background-color: @btnInverseBackgroundHighlight; 144 | } 145 | } 146 | 147 | 148 | // Reposition the caret 149 | .btn .caret { 150 | margin-top: 7px; 151 | margin-left: 0; 152 | } 153 | .btn:hover .caret, 154 | .open.btn-group .caret { 155 | .opacity(100); 156 | } 157 | // Carets in other button sizes 158 | .btn-mini .caret { 159 | margin-top: 5px; 160 | } 161 | .btn-small .caret { 162 | margin-top: 6px; 163 | } 164 | .btn-large .caret { 165 | margin-top: 6px; 166 | border-left-width: 5px; 167 | border-right-width: 5px; 168 | border-top-width: 5px; 169 | } 170 | // Upside down carets for .dropup 171 | .dropup .btn-large .caret { 172 | border-bottom: 5px solid @black; 173 | border-top: 0; 174 | } 175 | 176 | 177 | 178 | // Account for other colors 179 | .btn-primary, 180 | .btn-warning, 181 | .btn-danger, 182 | .btn-info, 183 | .btn-success, 184 | .btn-inverse { 185 | .caret { 186 | border-top-color: @white; 187 | border-bottom-color: @white; 188 | .opacity(75); 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /resources/public/less/variables.less: -------------------------------------------------------------------------------- 1 | // Variables.less 2 | // Variables to customize the look and feel of Bootstrap 3 | // ----------------------------------------------------- 4 | 5 | 6 | 7 | // GLOBAL VALUES 8 | // -------------------------------------------------- 9 | 10 | 11 | // Grays 12 | // ------------------------- 13 | @black: #000; 14 | @grayDarker: #222; 15 | @grayDark: #333; 16 | @gray: #555; 17 | @grayLight: #999; 18 | @grayLighter: #eee; 19 | @white: #fff; 20 | 21 | 22 | // Accent colors 23 | // ------------------------- 24 | @blue: #049cdb; 25 | @blueDark: #0064cd; 26 | @green: #46a546; 27 | @red: #9d261d; 28 | @yellow: #ffc40d; 29 | @orange: #f89406; 30 | @pink: #c3325f; 31 | @purple: #7a43b6; 32 | 33 | 34 | // Scaffolding 35 | // ------------------------- 36 | @bodyBackground: @white; 37 | @textColor: @grayDark; 38 | 39 | 40 | // Links 41 | // ------------------------- 42 | @linkColor: #08c; 43 | @linkColorHover: darken(@linkColor, 15%); 44 | 45 | 46 | // Typography 47 | // ------------------------- 48 | @sansFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif; 49 | @serifFontFamily: Georgia, "Times New Roman", Times, serif; 50 | @monoFontFamily: Menlo, Monaco, Consolas, "Courier New", monospace; 51 | 52 | @baseFontSize: 13px; 53 | @baseFontFamily: @sansFontFamily; 54 | @baseLineHeight: 18px; 55 | @altFontFamily: @serifFontFamily; 56 | 57 | @headingsFontFamily: inherit; // empty to use BS default, @baseFontFamily 58 | @headingsFontWeight: bold; // instead of browser default, bold 59 | @headingsColor: inherit; // empty to use BS default, @textColor 60 | 61 | 62 | // Tables 63 | // ------------------------- 64 | @tableBackground: transparent; // overall background-color 65 | @tableBackgroundAccent: #f9f9f9; // for striping 66 | @tableBackgroundHover: #f5f5f5; // for hover 67 | @tableBorder: #ddd; // table and cell border 68 | 69 | 70 | // Buttons 71 | // ------------------------- 72 | @btnBackground: @white; 73 | @btnBackgroundHighlight: darken(@white, 10%); 74 | @btnBorder: #ccc; 75 | 76 | @btnPrimaryBackground: @linkColor; 77 | @btnPrimaryBackgroundHighlight: spin(@btnPrimaryBackground, 15%); 78 | 79 | @btnInfoBackground: #5bc0de; 80 | @btnInfoBackgroundHighlight: #2f96b4; 81 | 82 | @btnSuccessBackground: #62c462; 83 | @btnSuccessBackgroundHighlight: #51a351; 84 | 85 | @btnWarningBackground: lighten(@orange, 15%); 86 | @btnWarningBackgroundHighlight: @orange; 87 | 88 | @btnDangerBackground: #ee5f5b; 89 | @btnDangerBackgroundHighlight: #bd362f; 90 | 91 | @btnInverseBackground: @gray; 92 | @btnInverseBackgroundHighlight: @grayDarker; 93 | 94 | 95 | // Forms 96 | // ------------------------- 97 | @inputBackground: @white; 98 | @inputBorder: #ccc; 99 | @inputBorderRadius: 3px; 100 | @inputDisabledBackground: @grayLighter; 101 | @formActionsBackground: #f5f5f5; 102 | 103 | // Dropdowns 104 | // ------------------------- 105 | @dropdownBackground: @white; 106 | @dropdownBorder: rgba(0,0,0,.2); 107 | @dropdownLinkColor: @grayDark; 108 | @dropdownLinkColorHover: @white; 109 | @dropdownLinkBackgroundHover: @linkColor; 110 | @dropdownDividerTop: #e5e5e5; 111 | @dropdownDividerBottom: @white; 112 | 113 | 114 | 115 | // COMPONENT VARIABLES 116 | // -------------------------------------------------- 117 | 118 | // Z-index master list 119 | // ------------------------- 120 | // Used for a bird's eye view of components dependent on the z-axis 121 | // Try to avoid customizing these :) 122 | @zindexDropdown: 1000; 123 | @zindexPopover: 1010; 124 | @zindexTooltip: 1020; 125 | @zindexFixedNavbar: 1030; 126 | @zindexModalBackdrop: 1040; 127 | @zindexModal: 1050; 128 | 129 | 130 | // Sprite icons path 131 | // ------------------------- 132 | @iconSpritePath: "../images/glyphicons-halflings.png"; 133 | @iconWhiteSpritePath: "../images/glyphicons-halflings-white.png"; 134 | 135 | 136 | // Input placeholder text color 137 | // ------------------------- 138 | @placeholderText: @grayLight; 139 | 140 | 141 | // Hr border color 142 | // ------------------------- 143 | @hrBorder: @grayLighter; 144 | 145 | 146 | // Navbar 147 | // ------------------------- 148 | @navbarHeight: 40px; 149 | @navbarBackground: @grayDarker; 150 | @navbarBackgroundHighlight: @grayDark; 151 | 152 | @navbarText: @grayLight; 153 | @navbarLinkColor: @grayLight; 154 | @navbarLinkColorHover: @white; 155 | @navbarLinkColorActive: @navbarLinkColorHover; 156 | @navbarLinkBackgroundHover: transparent; 157 | @navbarLinkBackgroundActive: @navbarBackground; 158 | 159 | @navbarSearchBackground: lighten(@navbarBackground, 25%); 160 | @navbarSearchBackgroundFocus: @white; 161 | @navbarSearchBorder: darken(@navbarSearchBackground, 30%); 162 | @navbarSearchPlaceholderColor: #ccc; 163 | @navbarBrandColor: @navbarLinkColor; 164 | 165 | 166 | // Hero unit 167 | // ------------------------- 168 | @heroUnitBackground: @grayLighter; 169 | @heroUnitHeadingColor: inherit; 170 | @heroUnitLeadColor: inherit; 171 | 172 | 173 | // Form states and alerts 174 | // ------------------------- 175 | @warningText: #c09853; 176 | @warningBackground: #fcf8e3; 177 | @warningBorder: darken(spin(@warningBackground, -10), 3%); 178 | 179 | @errorText: #b94a48; 180 | @errorBackground: #f2dede; 181 | @errorBorder: darken(spin(@errorBackground, -10), 3%); 182 | 183 | @successText: #468847; 184 | @successBackground: #dff0d8; 185 | @successBorder: darken(spin(@successBackground, -10), 5%); 186 | 187 | @infoText: #3a87ad; 188 | @infoBackground: #d9edf7; 189 | @infoBorder: darken(spin(@infoBackground, -10), 7%); 190 | 191 | 192 | 193 | // GRID 194 | // -------------------------------------------------- 195 | 196 | // Default 940px grid 197 | // ------------------------- 198 | @gridColumns: 12; 199 | @gridColumnWidth: 60px; 200 | @gridGutterWidth: 20px; 201 | @gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1)); 202 | 203 | // Fluid grid 204 | // ------------------------- 205 | @fluidGridColumnWidth: 8.333333%; 206 | @fluidGridGutterWidth: 0; 207 | -------------------------------------------------------------------------------- /src-cljs/torrent_client/peers.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.peers 2 | (:require 3 | [torrent-client.core.dispatch :as dispatch] 4 | [waltz.state :as state] 5 | [cljconsole.main :as console] 6 | [goog.Timer :as Timer] 7 | [goog.events :as events]) 8 | (:use 9 | [torrent-client.torrents :only [torrents]] 10 | [torrent-client.torrent :only [has-full-metadata?]] 11 | [torrent-client.peer :only [generate-peer]] 12 | [torrent-client.core.incubator :only [dissoc-in]])) 13 | 14 | ; 30 seconds 15 | (def optimistic-unchoke-period (* 10 1000)) 16 | 17 | ; how many peers should we download from at once 18 | (def download-count 4) 19 | ; how many peers should we get metadat from at once 20 | (def metadata-request-count 2) 21 | 22 | (def peers (atom {})) 23 | 24 | (defn optimistic? [m] (state/in? m :optimistic)) 25 | (def not-optimistic? (complement optimistic?)) 26 | 27 | (defn peer-unchoked? [m] (state/in? m :peer-unchoked)) 28 | (def peer-choked? (complement peer-unchoked?)) 29 | (defn peer-interested? [m] (state/in? m :peer-interested)) 30 | (def peer-uninterested? (complement peer-interested?)) 31 | 32 | ;;************************************************ 33 | ;; Manage events sent from channels 34 | ;;************************************************ 35 | 36 | (dispatch/react-to #{:remove-connection} (fn [peer-id info-hash] 37 | "For now a connection and channel are 1-1 38 | When a connection breaks remove all the peers associated with it" 39 | (swap! peers dissoc-in [info-hash peer-id]))) 40 | 41 | (dispatch/react-to #{:add-channel} (fn [_ [peer-id channel & flags]] 42 | "A new channel has been established to get torrent data" 43 | (let [; If this peer is initiating the handshake 44 | handshake (contains? (set flags) :handshake) 45 | ; Find the torrent this channel is for 46 | info-hash (aget channel "label") 47 | torrent (@torrents info-hash) 48 | peer (generate-peer torrent channel peer-id handshake)] 49 | ; add the peer to the list of peers for this torrent 50 | (console/info "Added peer" peer-id "to torrent" info-hash) 51 | (swap! peers assoc-in [info-hash peer-id] peer)))) 52 | 53 | (dispatch/react-to #{:remove-channel} (fn [_ [peer-id channel]] 54 | (let [info-hash (aget channel "label")] 55 | (swap! peers dissoc-in [info-hash peer-id])))) 56 | 57 | (dispatch/react-to #{:receive-data} (fn [_ [peer-id channel data]] 58 | (let [info-hash (aget channel "label")] 59 | (if-let [peer (get-in @peers [info-hash peer-id])] 60 | (state/trigger peer :receive-data data))))) 61 | 62 | ;;************************************************ 63 | ;; Helper function to determin choked/unchoked peers 64 | ;; Used both when dealing with torrent events and 65 | ;; peer events 66 | ;;************************************************ 67 | 68 | (defn set-unchoked! 69 | "Update the currently unchoked peers, choking & unchoking peers where 70 | appropriate" 71 | [info-hash] 72 | (if-let [peers (vals (@peers info-hash))] 73 | (let [peers (sort-by (juxt optimistic? peer-interested?) peers) 74 | ; is the first peer is optimistically unchoked but not interested 75 | ; TODO: resolve this testing logic 76 | first-peer-unop ((every-pred optimistic? peer-uninterested?) (first peers)) 77 | ; the first n peers are active 78 | active-peers-count (min (count peers) (if first-peer-unop 5 4)) 79 | ; if the optimisticly unchoked peer isn't interested allow 5 active 80 | ; peers otherwise just have the 4 active peers 81 | active (subvec peers 0 active-peers-count) 82 | inactive (if (< active-peers-count (count peers)) 83 | (subvec peers active-peers-count))] 84 | ; Unchoke the peers in the top 4 that are currently choked 85 | ; H.C (comp :choking deref not working...?) 86 | (doseq [peer (filter peer-choked? active)] 87 | (state/trigger peer :unchoke-peer)) 88 | ; choke inactive peers that are unchoked 89 | (doseq [peer (filter peer-unchoked? inactive)] 90 | (state/trigger peer :choke-peer))))) 91 | 92 | ;;************************************************ 93 | ;; Manage events sent from torrent management 94 | ;;************************************************ 95 | 96 | (defn request-metadata! [info-hash] 97 | (if-let [peers (vals (@peers info-hash))] 98 | (let [peers (remove #(state/in? % :rejecting-metadata-requests) peers) 99 | ; has-metadata not being set can also mean it is unknown 100 | ; prefer peers that definately have metadata, but also try unknowns 101 | peers (sort-by #(state/in? % :has-metadata) peers) 102 | peers-count (min metadata-request-count (count peers))] 103 | (doseq [p (subvec peers 0 peers-count)] 104 | (state/trigger p :request-metadata))))) 105 | 106 | (defn unoptimistic 107 | "The optimistic downloader is protected for the first 30 seconds 108 | after that it has to fight for itself" 109 | [info-hash] 110 | (if-let [peers (vals (get @peers info-hash))] 111 | (if-let [peer (first (filter optimistic? peers))] 112 | (state/trigger peer :unoptimistic)))) 113 | 114 | (defn optimistic-unchoke 115 | "Unchoke a peer regardless of its upload speed" 116 | [info-hash] 117 | (if-let [peers (vals (get @peers info-hash))] 118 | ; mark optimistic a random peer that is choked and not allready optimistic 119 | (let [eligible-peers (filter (every-pred not-optimistic? peer-choked?) peers)] 120 | (if (pos? (count eligible-peers)) 121 | (state/trigger (rand-nth eligible-peers) :optimistic))))) 122 | 123 | (defn- manage-peers [torrent] 124 | "Run all the sub functions required to manage all a torrents peers" 125 | (let [info-hash (@torrent :pretty-info-hash)] 126 | (unoptimistic info-hash) 127 | (optimistic-unchoke info-hash) 128 | (if (has-full-metadata? torrent) 129 | (set-unchoked! info-hash) 130 | (request-metadata! info-hash)))) 131 | 132 | ; Periodically update the peers for this torrent 133 | (dispatch/react-to #{:started-torrent} (fn [_ torrent] 134 | (let [timer (goog/Timer. optimistic-unchoke-period)] 135 | (.start timer) 136 | (events/listen timer Timer/TICK #(manage-peers torrent))))) 137 | 138 | (dispatch/react-to #{:updated-torrent} (fn [_ torrent] 139 | "Peers will have been waiting on metadata to continue with the connection 140 | process" 141 | (let [peers (vals (@peers (@torrent :info-hash)))] 142 | (doseq [p peers] 143 | (state/trigger p :received-metadata))))) 144 | 145 | ;;************************************************ 146 | ;; Manage a peer based on what other peers are doing 147 | ;;************************************************ 148 | 149 | (dispatch/react-to #{:written-piece} (fn [_ [torrent piece]] 150 | "When a peer sends us a block we didn't have before notify other peers" 151 | (doseq [peer (@peers (@torrent :info-hash))] 152 | (state/trigger peer :have-piece piece)))) 153 | 154 | (def retry-events #{:invalid-piece :expired-piece}) 155 | (dispatch/react-to retry-events (fn [_ [torrent piece]] 156 | "When a piece needs retrying recheck which peers the client is interested in" 157 | (doseq [peer (@peers (@torrent :info-hash))] 158 | (state/trigger peer :check-downloading)))) 159 | 160 | (def unchoke-events #{:receive-interested :receive-not-interested}) 161 | (dispatch/react-to unchoke-events (fn [_ torrent] 162 | "When a peer becomes interested recalculate choked peers" 163 | (set-unchoked! (@torrent :pretty-info-hash)))) -------------------------------------------------------------------------------- /src-clj/torrent_client/views/common.clj: -------------------------------------------------------------------------------- 1 | (ns torrent-client.views.common 2 | (:use [noir.core :only [defpartial]] 3 | [hiccup.page :only [html5 include-css include-js]] 4 | [hiccup.form :only [label file-upload submit-button text-field]] 5 | [hiccup.element :only [link-to]] 6 | [hiccup.util] 7 | [environ.core])) 8 | 9 | (defn include-less 10 | "Include a list of external stylesheet files." 11 | [& styles] 12 | (for [style styles] 13 | [:link {:type "text/less", :href (to-uri style), :rel "stylesheet"}])) 14 | 15 | (defpartial layout [& content] 16 | (with-base-url (env :base-url) 17 | (html5 18 | [:head 19 | [:title "Ampere, abusus non tollit usum"] 20 | [:link {:href "favicon.ico" :rel "shortcut-icon" :type "image/x-icon"}] 21 | (include-css "css/bootstrap.css") 22 | (include-js 23 | "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" 24 | "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.2.2/bootstrap.min.js" 25 | "http://cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.10/socket.io.min.js" 26 | "/js/humane.js")] 27 | [:body 28 | [:div#unsupported-modal.modal.hide {:data-backdrop "static"} 29 | [:div.modal-header 30 | [:h3 "Browser not supported"]] 31 | [:div.modal-body 32 | [:p "webrtc datachannel and persistent localstorage required"] 33 | (link-to {:class "btn"} "http://www.google.com/chrome" "Download google chrome")]] 34 | [:div#create-modal.modal.hide 35 | [:div.modal-header 36 | [:h3 "Create a torrent"] 37 | [:button.close {:type "button" :data-dismiss "modal"}]] 38 | [:form#create-form.modal-body.form-horizontal 39 | [:div.control-group 40 | (label {:class "control-label"} "name" "name") 41 | [:div.controls 42 | (text-field {:placeholder "My great torrent"} "name")]] 43 | [:div.control-group 44 | (label {:class "control-label"} "tracker" "tracker") 45 | [:div.controls 46 | (text-field {:type "url" :value "http://ec2-184-73-103-94.compute-1.amazonaws.com:80"} "tracker")]] 47 | [:div.control-group 48 | (label {:class "control-label"} "files" "files") 49 | [:div.controls 50 | [:div.files] 51 | [:span.help-inline "Drag files from your computer to add them"]]]] 52 | [:div.modal-footer 53 | [:a.btn {:data-dismiss "modal"} "close"] 54 | (submit-button {:class "btn btn-primary" :form "create-form"} "create")]] 55 | [:div#add-modal.modal.hide 56 | [:div.modal-header 57 | [:h3 "Add a torrent"] 58 | [:button.close {:type "button" :data-dismiss "modal"}]] 59 | [:form#add-form.modal-body.form-horizontal 60 | [:div.control-group 61 | (label {:class "control-label"} "metainfo" ".torrent") 62 | [:div.controls 63 | (file-upload "metainfo") 64 | [:span.help-block "Or drag a file from your computer"] 65 | ]]] 66 | [:div.modal-footer 67 | [:a.btn {:data-dismiss "modal"} "close"] 68 | (submit-button {:class "btn btn-primary" :form "add-form"} "create")]] 69 | 70 | [:div#about-modal.modal.hide 71 | [:div.modal-header 72 | [:h3 "Ampere"] 73 | [:button.close {:type "button" :data-dismiss "modal"}]] 74 | [:div.modal-body 75 | [:pre 76 | "Ampere was built by " 77 | [:a {:href "http://github.com/hcliff" :target "_blank"} "Henry Clifford"]] 78 | [:pre 79 | "Building on the efforts of others" 80 | [:ul 81 | [:li 82 | (link-to {:target "_blank"} "https://github.com/piranna" "Piranna")] 83 | [:li 84 | (link-to {:target "_blank"} "http://nakkaya.com" "Nakkaya")] 85 | [:li 86 | (link-to {:target "_blank"} "https://github.com/Peer5/ShareFest" "ShareFest")] 87 | ]] 88 | [:pre "Ampere is licensed under the MIT license"] 89 | [:pre 90 | "You can contribute or fork ampere " 91 | [:a {:href "http://github.com/hcliff/ampere" :target "_blank"} "on github"]] 92 | ]] 93 | 94 | [:div.navbar.navbar-fixed-top 95 | [:div.navbar-inner 96 | [:div.container-fluid 97 | [:a.brand "Ampere"] 98 | [:div.nav-collapse.pull-right 99 | [:ul.nav 100 | [:li 101 | (link-to {:role "button" :data-toggle "modal"} "#add-modal" 102 | "Add Torrent")] 103 | [:li 104 | (link-to {:role "button" :data-toggle "modal"} "#create-modal" 105 | "Create Torrent")] 106 | [:li 107 | (link-to {:role "button" :data-toggle "modal"} "#about-modal" 108 | "About")]]] 109 | ]]] 110 | [:div.container-fluid 111 | [:div#alerts.row-fluid] 112 | (link-to {:id "demo-torrent"} "http://bittorrent.io?dn=lonely%20star&xt=urn:btih:fc9f1f538d07aee107d3bbfd273e86ffb289f380&tr=http://ec2-184-73-103-94.compute-1.amazonaws.com:80" 113 | [:div.row-fluid 114 | [:div.info 115 | [:img.promo {:src (to-uri "/the_weeknd.jpg")}] 116 | [:div.wrapper 117 | [:strong "Click here to start a demo torrent"] 118 | [:small "A song from The Weeknds debute mixtape, released for free, 4mb"]] 119 | ]]) 120 | [:div.row-fluid 121 | [:ul.nav.nav-tabs 122 | [:li.active 123 | [:a#downloading-tab.active {:data-toggle "tab" :href "#torrents"} 124 | "Downloading " [:span#downloading-count.badge "0"]]] 125 | [:li 126 | [:a#completed-tab {:data-toggle "tab" :href "#torrents"} 127 | "Completed " [:span#completed-count.badge "0"]]] 128 | [:li.hide 129 | [:a#settings-tab {:data-toggle "tab" :href "#settings"} 130 | "Settings"]]] 131 | [:div.tab-content 132 | [:div#torrents.tab-pane.active 133 | [:table.table.table-striped.body 134 | [:thead 135 | [:tr 136 | [:th.flex2.name "Name"] 137 | [:th.flex1.size "Size"] 138 | [:th.flex5.progress-td "Progress"] 139 | [:th.flex1.speed "Speed"] 140 | [:th.actions ""] 141 | ]] 142 | [:tbody] 143 | ]] 144 | [:div#settings.tab-pane 145 | [:form.form-horizontal.offset4 146 | [:div.control-group 147 | (label {:class "control-label"} "settings-notifications" "browser notifications") 148 | [:div.controls 149 | [:a#settings-notifications.btn "Add browser notifications"]]] 150 | [:div.control-group 151 | (label {:class "control-label"} "settings-magnet" "magnet links") 152 | [:div.controls 153 | [:a#settings-magnet.btn "Handle magnet links"]]] 154 | ]] 155 | ]]] 156 | (include-js "/cljs/bootstrap.js" "/js/monkey.js")]))) -------------------------------------------------------------------------------- /src-cljs/torrent_client/tracker.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.tracker 2 | (:require 3 | [torrent-client.core.dispatch :as dispatch] 4 | [cljconsole.main :as console] 5 | [goog.Timer :as Timer] 6 | [goog.events :as events]) 7 | (:use 8 | [torrent-client.torrents :only [torrents]] 9 | [torrent-client.peer-id :only [peer-id]] 10 | [torrent-client.connection :only 11 | [create-connection 12 | create-data-channel 13 | send-offer! 14 | receive-offer! 15 | receive-ice-candidate! 16 | send-answer! 17 | receive-answer! 18 | set-connection-events! 19 | set-channel-events! 20 | connections]]) 21 | (:use-macros [async.macros :only [let-async async]])) 22 | 23 | ; How often should we tell the tracker our download progress 24 | ; NOTE: not currently used 25 | (def announce-period (* 15 60 1000)) 26 | 27 | ;;************************************************ 28 | ;; Use socket.io to communicate peer offers 29 | ;; and answers with a tracker 30 | ;; 1 connection per tracker, support for 31 | ;; multiple torrents from one tracker 32 | ;;************************************************ 33 | 34 | (def trackers (atom {})) 35 | 36 | (defn on [socket event callback] 37 | (.on socket (name event) #(callback (js->clj % :keywordize-keys true)))) 38 | 39 | (defn tracker-socket [tracker] 40 | (async [success-callback error-callback] 41 | (if-let [tracker (@trackers tracker)] 42 | ; If we allready have a socket to this tracker return it 43 | (success-callback tracker) 44 | ; Otherwise build one 45 | (let [socket (.connect js/io tracker)] 46 | (swap! trackers assoc tracker socket) 47 | (aset tracker "onerror" error-callback) 48 | ; Continue once the socket is opened 49 | (on socket :connect #(success-callback socket)) 50 | ; Listen for socket events 51 | (on socket :need_offer #(dispatch/fire :need-offer [socket %])) 52 | (on socket :offer #(dispatch/fire :offer [socket %])) 53 | (on socket :ice-candidate #(dispatch/fire :ice-candidate [socket %])) 54 | (on socket :answer #(dispatch/fire :answer [socket %])))))) 55 | 56 | ;;************************************************ 57 | ;; Reactions to changes in torrent state 58 | ;;************************************************ 59 | 60 | (defn emit [socket event data] 61 | (.emit socket (clj->js event) (clj->js data))) 62 | 63 | ; TODO: logical falicy, find a better way to manage 64 | ; the number of connections, we shouldn't request offers from 65 | ; every tracker 66 | (dispatch/react-to #{:started-torrent :updated-torrent} (fn [_ torrent] 67 | "Update the list of trackers for this torrent 68 | and send out the start event to the trackers" 69 | (doseq [tracker-url (@torrent :announce-list)] 70 | (console/info "Trying tracker" tracker-url) 71 | (let-async [socket (tracker-socket tracker-url)] 72 | (console/info "Torrent" (@torrent :pretty-info-hash) "connected to" tracker-url) 73 | (emit socket :started { 74 | :peer_id @peer-id 75 | :info_hash (@torrent :pretty-info-hash) 76 | :numwant 10 77 | }))))) 78 | 79 | ; (dispatch/react-to #{:completed-torrent} (fn [_ torrent] 80 | ; "When a torrent is finished send the complete event to the trackers" 81 | ; (doseq [socket (@trackers ] 82 | ; (emit socket :completed { 83 | ; :peer_id @peer-id 84 | ; :info_hash (@torrent :pretty-info-hash) 85 | ; })))) 86 | 87 | ; (dispatch/react-to #{:stopped-torrent} (fn [_ torrent] 88 | ; "When a torrent is stopped send the stopped event to the trackers" 89 | ; (doseq [socket @trackers] 90 | ; (emit socket :stopped { 91 | ; :peer_id @peer-id 92 | ; :info_hash (@torrent :pretty-info-hash) 93 | ; })))) 94 | 95 | ;;************************************************ 96 | ;; Reactions to events sent by the tracker 97 | ;;************************************************ 98 | 99 | (dispatch/react-to #{:need-offer} (fn [_ [tracker-socket {:keys [peer_id info_hash]}]] 100 | "The tracker has requested an offer from this client" 101 | (let [connection (create-connection peer_id info_hash) 102 | ; Build a channel on this connection for the torrent] 103 | channel (create-data-channel connection info_hash)] 104 | (set-connection-events! tracker-socket connection peer_id info_hash) 105 | (set-channel-events! channel peer_id) 106 | (let-async [_ (send-offer! connection) 107 | :let sdp (-> connection .-localDescription .-sdp)] 108 | ; Check the peer requested a torrent we have 109 | (if-let [torrent (@torrents info_hash)] 110 | ; TODO: also check numwant 111 | (emit tracker-socket :offer { 112 | :sdp sdp 113 | :info_hash info_hash 114 | :to_peer_id peer_id 115 | :peer_id @peer-id 116 | })))))) 117 | 118 | (dispatch/react-to #{:offer} (fn [_ [tracker-socket {:keys [peer_id info_hash sdp]}]] 119 | "When the tracker sends an offer for a torrent connection 120 | build and return an answer" 121 | (let [connection (create-connection peer_id info_hash)] 122 | (set-connection-events! tracker-socket connection peer_id info_hash) 123 | (receive-offer! connection sdp) 124 | (let-async [_ (send-answer! connection peer_id) 125 | :let sdp (-> connection .-localDescription .-sdp)] 126 | ; Listen for the peer opening a data channel 127 | (if-let [torrent (@torrents info_hash)] 128 | ; The peer that sent the offer will create the datachannel after it 129 | ; gets this clients offer 130 | (emit tracker-socket :answer { 131 | :sdp sdp 132 | :info_hash info_hash 133 | :to_peer_id peer_id 134 | :peer_id @peer-id 135 | })))))) 136 | 137 | (dispatch/react-to #{:answer} (fn [_ [tracker-socket {:keys [peer_id info_hash sdp]}]] 138 | "When the tracker sends an answer for an offer we send" 139 | (if-let [connection (get-in @connections [peer_id info_hash])] 140 | (receive-answer! connection sdp)))) 141 | 142 | (dispatch/react-to #{:ice-candidate} (fn [_ [_ {:keys [peer_id info_hash candidate]}]] 143 | "The peer has provided a new ice candidate" 144 | (if-let [connection (get-in @connections [peer_id info_hash])] 145 | (receive-ice-candidate! connection candidate)))) 146 | 147 | ;;************************************************ 148 | ;; Reactions to events created by the client for the peer 149 | ;;************************************************ 150 | 151 | (dispatch/react-to #{:add-ice-candidate} (fn [_ [tracker-socket to-peer-id info-hash candidate]] 152 | "The peer has provided a new ice candidate" 153 | (emit tracker-socket :ice-candidate { 154 | :candidate (clj->js candidate) 155 | :info_hash info-hash 156 | :to_peer_id to-peer-id 157 | :peer_id @peer-id 158 | }))) 159 | 160 | ;;************************************************ 161 | ;; Statistics 162 | ;;************************************************ 163 | 164 | ; Use a global timer for announce period. this is just for effiency 165 | ; Every n seconds send an announce 166 | ; TODO - use actual interval given by the tracker 167 | ; (let [timer (goog/Timer. announce-period)] 168 | ; (.start timer) 169 | ; (events/listen timer Timer/TICK (fn [_] 170 | ; ; Loop through all the torrents here 171 | ; ; NOTE: at time of writing :when is broken 172 | ; (doseq [torrent (filter #(= :processed (% :status)) @torrents)] 173 | ; ; Update each of the trackers this torrent has 174 | ; (doseq [tracker-socket (@torrent :announce-list)] 175 | ; (.emit tracker-socket :stats { 176 | ; :uploaded 0 177 | ; :downloaded 0 178 | ; :left (@torrent :total-length) 179 | ; :info_hash (@torrent :pretty-info-hash) 180 | ; :peer_id @peer-id 181 | ; })) 182 | ; )))) 183 | 184 | (console/log "EOF") -------------------------------------------------------------------------------- /resources/public/less/navs.less: -------------------------------------------------------------------------------- 1 | // NAVIGATIONS 2 | // ----------- 3 | 4 | 5 | 6 | // BASE CLASS 7 | // ---------- 8 | 9 | .nav { 10 | margin-left: 0; 11 | margin-bottom: @baseLineHeight; 12 | list-style: none; 13 | } 14 | 15 | // Make links block level 16 | .nav > li > a { 17 | display: block; 18 | } 19 | .nav > li > a:hover { 20 | text-decoration: none; 21 | background-color: @grayLighter; 22 | } 23 | 24 | // Redeclare pull classes because of specifity 25 | .nav > .pull-right { 26 | float: right; 27 | } 28 | 29 | // Nav headers (for dropdowns and lists) 30 | .nav .nav-header { 31 | display: block; 32 | padding: 3px 15px; 33 | font-size: 11px; 34 | font-weight: bold; 35 | line-height: @baseLineHeight; 36 | color: @grayLight; 37 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 38 | text-transform: uppercase; 39 | } 40 | // Space them out when they follow another list item (link) 41 | .nav li + .nav-header { 42 | margin-top: 9px; 43 | } 44 | 45 | 46 | // NAV LIST 47 | // -------- 48 | 49 | .nav-list { 50 | padding-left: 15px; 51 | padding-right: 15px; 52 | margin-bottom: 0; 53 | } 54 | .nav-list > li > a, 55 | .nav-list .nav-header { 56 | margin-left: -15px; 57 | margin-right: -15px; 58 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 59 | } 60 | .nav-list > li > a { 61 | padding: 3px 15px; 62 | } 63 | .nav-list > .active > a, 64 | .nav-list > .active > a:hover { 65 | color: @white; 66 | text-shadow: 0 -1px 0 rgba(0,0,0,.2); 67 | background-color: @linkColor; 68 | } 69 | .nav-list [class^="icon-"] { 70 | margin-right: 2px; 71 | } 72 | // Dividers (basically an hr) within the dropdown 73 | .nav-list .divider { 74 | .nav-divider(); 75 | } 76 | 77 | 78 | 79 | // TABS AND PILLS 80 | // ------------- 81 | 82 | // Common styles 83 | .nav-tabs, 84 | .nav-pills { 85 | .clearfix(); 86 | } 87 | .nav-tabs > li, 88 | .nav-pills > li { 89 | float: left; 90 | } 91 | .nav-tabs > li > a, 92 | .nav-pills > li > a { 93 | padding-right: 12px; 94 | padding-left: 12px; 95 | margin-right: 2px; 96 | line-height: 14px; // keeps the overall height an even number 97 | } 98 | 99 | // TABS 100 | // ---- 101 | 102 | // Give the tabs something to sit on 103 | .nav-tabs { 104 | border-bottom: 1px solid #ddd; 105 | } 106 | // Make the list-items overlay the bottom border 107 | .nav-tabs > li { 108 | margin-bottom: -1px; 109 | } 110 | // Actual tabs (as links) 111 | .nav-tabs > li > a { 112 | padding-top: 8px; 113 | padding-bottom: 8px; 114 | line-height: @baseLineHeight; 115 | border: 1px solid transparent; 116 | .border-radius(4px 4px 0 0); 117 | &:hover { 118 | border-color: @grayLighter @grayLighter #ddd; 119 | } 120 | } 121 | // Active state, and it's :hover to override normal :hover 122 | .nav-tabs > .active > a, 123 | .nav-tabs > .active > a:hover { 124 | color: @gray; 125 | background-color: @white; 126 | border: 1px solid #ddd; 127 | border-bottom-color: transparent; 128 | cursor: default; 129 | } 130 | 131 | 132 | // PILLS 133 | // ----- 134 | 135 | // Links rendered as pills 136 | .nav-pills > li > a { 137 | padding-top: 8px; 138 | padding-bottom: 8px; 139 | margin-top: 2px; 140 | margin-bottom: 2px; 141 | .border-radius(5px); 142 | } 143 | 144 | // Active state 145 | .nav-pills > .active > a, 146 | .nav-pills > .active > a:hover { 147 | color: @white; 148 | background-color: @linkColor; 149 | } 150 | 151 | 152 | 153 | // STACKED NAV 154 | // ----------- 155 | 156 | // Stacked tabs and pills 157 | .nav-stacked > li { 158 | float: none; 159 | } 160 | .nav-stacked > li > a { 161 | margin-right: 0; // no need for the gap between nav items 162 | } 163 | 164 | // Tabs 165 | .nav-tabs.nav-stacked { 166 | border-bottom: 0; 167 | } 168 | .nav-tabs.nav-stacked > li > a { 169 | border: 1px solid #ddd; 170 | .border-radius(0); 171 | } 172 | .nav-tabs.nav-stacked > li:first-child > a { 173 | .border-radius(4px 4px 0 0); 174 | } 175 | .nav-tabs.nav-stacked > li:last-child > a { 176 | .border-radius(0 0 4px 4px); 177 | } 178 | .nav-tabs.nav-stacked > li > a:hover { 179 | border-color: #ddd; 180 | z-index: 2; 181 | } 182 | 183 | // Pills 184 | .nav-pills.nav-stacked > li > a { 185 | margin-bottom: 3px; 186 | } 187 | .nav-pills.nav-stacked > li:last-child > a { 188 | margin-bottom: 1px; // decrease margin to match sizing of stacked tabs 189 | } 190 | 191 | 192 | 193 | // DROPDOWNS 194 | // --------- 195 | 196 | .nav-tabs .dropdown-menu { 197 | .border-radius(0 0 5px 5px); // remove the top rounded corners here since there is a hard edge above the menu 198 | } 199 | .nav-pills .dropdown-menu { 200 | .border-radius(4px); // make rounded corners match the pills 201 | } 202 | 203 | // Default dropdown links 204 | // ------------------------- 205 | // Make carets use linkColor to start 206 | .nav-tabs .dropdown-toggle .caret, 207 | .nav-pills .dropdown-toggle .caret { 208 | border-top-color: @linkColor; 209 | border-bottom-color: @linkColor; 210 | margin-top: 6px; 211 | } 212 | .nav-tabs .dropdown-toggle:hover .caret, 213 | .nav-pills .dropdown-toggle:hover .caret { 214 | border-top-color: @linkColorHover; 215 | border-bottom-color: @linkColorHover; 216 | } 217 | 218 | // Active dropdown links 219 | // ------------------------- 220 | .nav-tabs .active .dropdown-toggle .caret, 221 | .nav-pills .active .dropdown-toggle .caret { 222 | border-top-color: @grayDark; 223 | border-bottom-color: @grayDark; 224 | } 225 | 226 | // Active:hover dropdown links 227 | // ------------------------- 228 | .nav > .dropdown.active > a:hover { 229 | color: @black; 230 | cursor: pointer; 231 | } 232 | 233 | // Open dropdowns 234 | // ------------------------- 235 | .nav-tabs .open .dropdown-toggle, 236 | .nav-pills .open .dropdown-toggle, 237 | .nav > li.dropdown.open.active > a:hover { 238 | color: @white; 239 | background-color: @grayLight; 240 | border-color: @grayLight; 241 | } 242 | .nav li.dropdown.open .caret, 243 | .nav li.dropdown.open.active .caret, 244 | .nav li.dropdown.open a:hover .caret { 245 | border-top-color: @white; 246 | border-bottom-color: @white; 247 | .opacity(100); 248 | } 249 | 250 | // Dropdowns in stacked tabs 251 | .tabs-stacked .open > a:hover { 252 | border-color: @grayLight; 253 | } 254 | 255 | 256 | 257 | // TABBABLE 258 | // -------- 259 | 260 | 261 | // COMMON STYLES 262 | // ------------- 263 | 264 | // Clear any floats 265 | .tabbable { 266 | .clearfix(); 267 | } 268 | .tab-content { 269 | overflow: auto; // prevent content from running below tabs 270 | } 271 | 272 | // Remove border on bottom, left, right 273 | .tabs-below > .nav-tabs, 274 | .tabs-right > .nav-tabs, 275 | .tabs-left > .nav-tabs { 276 | border-bottom: 0; 277 | } 278 | 279 | // Show/hide tabbable areas 280 | .tab-content > .tab-pane, 281 | .pill-content > .pill-pane { 282 | display: none; 283 | } 284 | .tab-content > .active, 285 | .pill-content > .active { 286 | display: block; 287 | } 288 | 289 | 290 | // BOTTOM 291 | // ------ 292 | 293 | .tabs-below > .nav-tabs { 294 | border-top: 1px solid #ddd; 295 | } 296 | .tabs-below > .nav-tabs > li { 297 | margin-top: -1px; 298 | margin-bottom: 0; 299 | } 300 | .tabs-below > .nav-tabs > li > a { 301 | .border-radius(0 0 4px 4px); 302 | &:hover { 303 | border-bottom-color: transparent; 304 | border-top-color: #ddd; 305 | } 306 | } 307 | .tabs-below > .nav-tabs > .active > a, 308 | .tabs-below > .nav-tabs > .active > a:hover { 309 | border-color: transparent #ddd #ddd #ddd; 310 | } 311 | 312 | // LEFT & RIGHT 313 | // ------------ 314 | 315 | // Common styles 316 | .tabs-left > .nav-tabs > li, 317 | .tabs-right > .nav-tabs > li { 318 | float: none; 319 | } 320 | .tabs-left > .nav-tabs > li > a, 321 | .tabs-right > .nav-tabs > li > a { 322 | min-width: 74px; 323 | margin-right: 0; 324 | margin-bottom: 3px; 325 | } 326 | 327 | // Tabs on the left 328 | .tabs-left > .nav-tabs { 329 | float: left; 330 | margin-right: 19px; 331 | border-right: 1px solid #ddd; 332 | } 333 | .tabs-left > .nav-tabs > li > a { 334 | margin-right: -1px; 335 | .border-radius(4px 0 0 4px); 336 | } 337 | .tabs-left > .nav-tabs > li > a:hover { 338 | border-color: @grayLighter #ddd @grayLighter @grayLighter; 339 | } 340 | .tabs-left > .nav-tabs .active > a, 341 | .tabs-left > .nav-tabs .active > a:hover { 342 | border-color: #ddd transparent #ddd #ddd; 343 | *border-right-color: @white; 344 | } 345 | 346 | // Tabs on the right 347 | .tabs-right > .nav-tabs { 348 | float: right; 349 | margin-left: 19px; 350 | border-left: 1px solid #ddd; 351 | } 352 | .tabs-right > .nav-tabs > li > a { 353 | margin-left: -1px; 354 | .border-radius(0 4px 4px 0); 355 | } 356 | .tabs-right > .nav-tabs > li > a:hover { 357 | border-color: @grayLighter @grayLighter @grayLighter #ddd; 358 | } 359 | .tabs-right > .nav-tabs .active > a, 360 | .tabs-right > .nav-tabs .active > a:hover { 361 | border-color: #ddd #ddd #ddd transparent; 362 | *border-left-color: @white; 363 | } 364 | -------------------------------------------------------------------------------- /resources/public/less/navbar.less: -------------------------------------------------------------------------------- 1 | // NAVBAR (FIXED AND STATIC) 2 | // ------------------------- 3 | 4 | 5 | // COMMON STYLES 6 | // ------------- 7 | 8 | .navbar { 9 | // Fix for IE7's bad z-indexing so dropdowns don't appear below content that follows the navbar 10 | *position: relative; 11 | *z-index: 2; 12 | 13 | overflow: visible; 14 | margin-bottom: @baseLineHeight; 15 | } 16 | 17 | // Gradient is applied to it's own element because overflow visible is not honored by IE when filter is present 18 | .navbar-inner { 19 | min-height: @navbarHeight; 20 | padding-left: 20px; 21 | padding-right: 20px; 22 | #gradient > .vertical(@navbarBackgroundHighlight, @navbarBackground); 23 | .border-radius(4px); 24 | .box-shadow(~"0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1)"); 25 | } 26 | 27 | // Set width to auto for default container 28 | // We then reset it for fixed navbars in the #gridSystem mixin 29 | .navbar .container { 30 | width: auto; 31 | } 32 | 33 | // Override the default collapsed state 34 | .nav-collapse.collapse { 35 | height: auto; 36 | } 37 | 38 | 39 | // Brand, links, text, and buttons 40 | .navbar { 41 | color: @navbarText; 42 | // Hover and active states 43 | .brand:hover { 44 | text-decoration: none; 45 | } 46 | // Website or project name 47 | .brand { 48 | float: left; 49 | display: block; 50 | // Vertically center the text given @navbarHeight 51 | @elementHeight: 20px; 52 | padding: ((@navbarHeight - @elementHeight) / 2 - 2) 20px ((@navbarHeight - @elementHeight) / 2 + 2); 53 | margin-left: -20px; // negative indent to left-align the text down the page 54 | font-size: 20px; 55 | font-weight: 200; 56 | line-height: 1; 57 | color: @navbarBrandColor; 58 | } 59 | // Plain text in topbar 60 | .navbar-text { 61 | margin-bottom: 0; 62 | line-height: @navbarHeight; 63 | } 64 | // Janky solution for now to account for links outside the .nav 65 | .navbar-link { 66 | color: @navbarLinkColor; 67 | &:hover { 68 | color: @navbarLinkColorHover; 69 | } 70 | } 71 | // Buttons in navbar 72 | .btn, 73 | .btn-group { 74 | .navbarVerticalAlign(30px); // Vertically center in navbar 75 | } 76 | .btn-group .btn { 77 | margin: 0; // then undo the margin here so we don't accidentally double it 78 | } 79 | } 80 | 81 | // Navbar forms 82 | .navbar-form { 83 | margin-bottom: 0; // remove default bottom margin 84 | .clearfix(); 85 | input, 86 | select, 87 | .radio, 88 | .checkbox { 89 | .navbarVerticalAlign(30px); // Vertically center in navbar 90 | } 91 | input, 92 | select { 93 | display: inline-block; 94 | margin-bottom: 0; 95 | } 96 | input[type="image"], 97 | input[type="checkbox"], 98 | input[type="radio"] { 99 | margin-top: 3px; 100 | } 101 | .input-append, 102 | .input-prepend { 103 | margin-top: 6px; 104 | white-space: nowrap; // preven two items from separating within a .navbar-form that has .pull-left 105 | input { 106 | margin-top: 0; // remove the margin on top since it's on the parent 107 | } 108 | } 109 | } 110 | 111 | // Navbar search 112 | .navbar-search { 113 | position: relative; 114 | float: left; 115 | .navbarVerticalAlign(28px); // Vertically center in navbar 116 | margin-bottom: 0; 117 | .search-query { 118 | padding: 4px 9px; 119 | #font > .sans-serif(13px, normal, 1); 120 | color: @white; 121 | background-color: @navbarSearchBackground; 122 | border: 1px solid @navbarSearchBorder; 123 | .box-shadow(~"inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.15)"); 124 | .transition(none); 125 | 126 | .placeholder(@navbarSearchPlaceholderColor); 127 | 128 | // Focus states (we use .focused since IE7-8 and down doesn't support :focus) 129 | &:focus, 130 | &.focused { 131 | padding: 5px 10px; 132 | color: @grayDark; 133 | text-shadow: 0 1px 0 @white; 134 | background-color: @navbarSearchBackgroundFocus; 135 | border: 0; 136 | .box-shadow(0 0 3px rgba(0,0,0,.15)); 137 | outline: 0; 138 | } 139 | } 140 | } 141 | 142 | 143 | 144 | // FIXED NAVBAR 145 | // ------------ 146 | 147 | // Shared (top/bottom) styles 148 | .navbar-fixed-top, 149 | .navbar-fixed-bottom { 150 | position: fixed; 151 | right: 0; 152 | left: 0; 153 | z-index: @zindexFixedNavbar; 154 | margin-bottom: 0; // remove 18px margin for static navbar 155 | } 156 | .navbar-fixed-top .navbar-inner, 157 | .navbar-fixed-bottom .navbar-inner { 158 | padding-left: 0; 159 | padding-right: 0; 160 | .border-radius(0); 161 | } 162 | 163 | .navbar-fixed-top .container, 164 | .navbar-fixed-bottom .container { 165 | #grid > .core > .span(@gridColumns); 166 | } 167 | 168 | // Fixed to top 169 | .navbar-fixed-top { 170 | top: 0; 171 | } 172 | 173 | // Fixed to bottom 174 | .navbar-fixed-bottom { 175 | bottom: 0; 176 | } 177 | 178 | 179 | 180 | // NAVIGATION 181 | // ---------- 182 | 183 | .navbar .nav { 184 | position: relative; 185 | left: 0; 186 | display: block; 187 | float: left; 188 | margin: 0 10px 0 0; 189 | } 190 | .navbar .nav.pull-right { 191 | float: right; // redeclare due to specificity 192 | } 193 | .navbar .nav > li { 194 | display: block; 195 | float: left; 196 | } 197 | 198 | // Links 199 | .navbar .nav > li > a { 200 | float: none; 201 | // Vertically center the text given @navbarHeight 202 | @elementHeight: 20px; 203 | padding: ((@navbarHeight - @elementHeight) / 2 - 1) 10px ((@navbarHeight - @elementHeight) / 2 + 1); 204 | line-height: 19px; 205 | color: @navbarLinkColor; 206 | text-decoration: none; 207 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 208 | } 209 | // Buttons 210 | .navbar .btn { 211 | display: inline-block; 212 | padding: 4px 10px 4px; 213 | // Vertically center the button given @navbarHeight 214 | @elementHeight: 28px; 215 | margin: ((@navbarHeight - @elementHeight) / 2 - 1) 5px ((@navbarHeight - @elementHeight) / 2); 216 | line-height: @baseLineHeight; 217 | } 218 | .navbar .btn-group { 219 | margin: 0; 220 | // Vertically center the button given @navbarHeight 221 | @elementHeight: 28px; 222 | padding: ((@navbarHeight - @elementHeight) / 2 - 1) 5px ((@navbarHeight - @elementHeight) / 2); 223 | } 224 | // Hover 225 | .navbar .nav > li > a:hover { 226 | background-color: @navbarLinkBackgroundHover; // "transparent" is default to differentiate :hover from .active 227 | color: @navbarLinkColorHover; 228 | text-decoration: none; 229 | } 230 | 231 | // Active nav items 232 | .navbar .nav .active > a, 233 | .navbar .nav .active > a:hover { 234 | color: @navbarLinkColorActive; 235 | text-decoration: none; 236 | background-color: @navbarLinkBackgroundActive; 237 | } 238 | 239 | // Dividers (basically a vertical hr) 240 | .navbar .divider-vertical { 241 | height: @navbarHeight; 242 | width: 1px; 243 | margin: 0 9px; 244 | overflow: hidden; 245 | background-color: @navbarBackground; 246 | border-right: 1px solid @navbarBackgroundHighlight; 247 | } 248 | 249 | // Secondary (floated right) nav in topbar 250 | .navbar .nav.pull-right { 251 | margin-left: 10px; 252 | margin-right: 0; 253 | } 254 | 255 | // Navbar button for toggling navbar items in responsive layouts 256 | // These definitions need to come after '.navbar .btn' 257 | .navbar .btn-navbar { 258 | display: none; 259 | float: right; 260 | padding: 7px 10px; 261 | margin-left: 5px; 262 | margin-right: 5px; 263 | .buttonBackground(@navbarBackgroundHighlight, @navbarBackground); 264 | .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075)"); 265 | } 266 | .navbar .btn-navbar .icon-bar { 267 | display: block; 268 | width: 18px; 269 | height: 2px; 270 | background-color: #f5f5f5; 271 | .border-radius(1px); 272 | .box-shadow(0 1px 0 rgba(0,0,0,.25)); 273 | } 274 | .btn-navbar .icon-bar + .icon-bar { 275 | margin-top: 3px; 276 | } 277 | 278 | 279 | // Dropdown menus 280 | // -------------- 281 | 282 | // Menu position and menu carets 283 | .navbar .dropdown-menu { 284 | &:before { 285 | content: ''; 286 | display: inline-block; 287 | border-left: 7px solid transparent; 288 | border-right: 7px solid transparent; 289 | border-bottom: 7px solid #ccc; 290 | border-bottom-color: @dropdownBorder; 291 | position: absolute; 292 | top: -7px; 293 | left: 9px; 294 | } 295 | &:after { 296 | content: ''; 297 | display: inline-block; 298 | border-left: 6px solid transparent; 299 | border-right: 6px solid transparent; 300 | border-bottom: 6px solid @dropdownBackground; 301 | position: absolute; 302 | top: -6px; 303 | left: 10px; 304 | } 305 | } 306 | // Menu position and menu caret support for dropups via extra dropup class 307 | .navbar-fixed-bottom .dropdown-menu { 308 | &:before { 309 | border-top: 7px solid #ccc; 310 | border-top-color: @dropdownBorder; 311 | border-bottom: 0; 312 | bottom: -7px; 313 | top: auto; 314 | } 315 | &:after { 316 | border-top: 6px solid @dropdownBackground; 317 | border-bottom: 0; 318 | bottom: -6px; 319 | top: auto; 320 | } 321 | } 322 | // Dropdown toggle caret 323 | .navbar .nav li.dropdown .dropdown-toggle .caret, 324 | .navbar .nav li.dropdown.open .caret { 325 | border-top-color: @white; 326 | border-bottom-color: @white; 327 | } 328 | .navbar .nav li.dropdown.active .caret { 329 | .opacity(100); 330 | } 331 | 332 | // Remove background color from open dropdown 333 | .navbar .nav li.dropdown.open > .dropdown-toggle, 334 | .navbar .nav li.dropdown.active > .dropdown-toggle, 335 | .navbar .nav li.dropdown.open.active > .dropdown-toggle { 336 | background-color: transparent; 337 | } 338 | 339 | // Dropdown link on hover 340 | .navbar .nav li.dropdown.active > .dropdown-toggle:hover { 341 | color: @white; 342 | } 343 | 344 | // Right aligned menus need alt position 345 | // TODO: rejigger this at some point to simplify the selectors 346 | .navbar .pull-right .dropdown-menu, 347 | .navbar .dropdown-menu.pull-right { 348 | left: auto; 349 | right: 0; 350 | &:before { 351 | left: auto; 352 | right: 12px; 353 | } 354 | &:after { 355 | left: auto; 356 | right: 13px; 357 | } 358 | } -------------------------------------------------------------------------------- /src-cljs/torrent_client/protocol/bittorrent.cljs: -------------------------------------------------------------------------------- 1 | (ns torrent-client.protocol.bittorrent 2 | (:require 3 | [torrent-client.core.crypt :as crypt] 4 | [torrent-client.core.bencode :as bencode] 5 | [torrent-client.core.reader :as reader] 6 | [torrent-client.bitfield :as bitfield] 7 | [torrent-client.protocol.main :as protocol] 8 | [goog.events :as events]) 9 | (:use 10 | [torrent-client.core.byte-array :only [uint8-array subarray]] 11 | [torrent-client.peer-id :only [peer-id]] 12 | [waltz.state :only [trigger]])) 13 | 14 | (defn ^boolean array-buffer-view? [candidate] 15 | ; Incorrect; but js/ArrayBufferView doesn't exist 16 | (instance? js/Uint8Array candidate)) 17 | 18 | (deftype Char [code] 19 | Object 20 | (toString [this] 21 | (.fromCharCode js/String code)) 22 | 23 | IEquiv 24 | (-equiv [_ o] 25 | (cond 26 | ; If a 1 character byte array compare the first bytes 27 | (array-buffer-view? o) (and (= (count o) 1) (= (first o) code)) 28 | ; compare string char codes 29 | (string? o) (= o (str code)) 30 | :else (= o code))) 31 | 32 | IHash 33 | (-hash [this] 34 | (goog.string/hashCode (pr-str this))) 35 | 36 | ICounted 37 | (-count [array] 1) 38 | ) 39 | 40 | (defn char [code] 41 | (Char. code)) 42 | 43 | ; H.C switch over to \x05 44 | (def msg-choke (char 00)) 45 | (def msg-unchoke (char 01)) 46 | (def msg-interested (char 02)) 47 | (def msg-not-interested (char 03)) 48 | (def msg-have (char 04)) 49 | (def msg-bitfield (char 05)) 50 | (def msg-request (char 06)) 51 | (def msg-piece (char 07)) 52 | (def msg-cancel (char 8)) 53 | ; ; The length of the string "BitTorrent protocol" 54 | (def msg-handshake (char 19)) 55 | (def msg-extended (char 20)) 56 | 57 | ; handshake indicated by id of 0 (no extension can have this id) 58 | (def extended-handshake 0) 59 | (def ut-metadata 3) 60 | ; msg_type codes for the ut-metadata extension 61 | (def ut-metadata-request 0) 62 | (def ut-metadata-piece 1) 63 | (def ut-metadata-reject 2) 64 | 65 | ; map extension ids to a keyword 66 | (def extensions 67 | {extended-handshake :extended-handshake 68 | ut-metadata :ut-metadata}) 69 | 70 | ;;************************************************ 71 | ;; Map received extensions based on their 72 | ;; extension and msg_type 73 | ;;************************************************ 74 | 75 | (defmulti receive-extension (fn [peer extension & [message data]] 76 | (if-let [msg-type (message :msg_type)] 77 | [extension (message :msg_type)] 78 | extension))) 79 | 80 | (defmethod receive-extension extended-handshake [p _ message] 81 | (trigger p :receive-extended message)) 82 | 83 | (defmethod receive-extension [ut-metadata ut-metadata-request] [p _ message] 84 | (trigger p :receive-metadata-request (message :piece))) 85 | 86 | (defmethod receive-extension [ut-metadata ut-metadata-piece] [p _ message data] 87 | (trigger p :receive-metadata-piece (message :piece) data)) 88 | 89 | (defmethod receive-extension [ut-metadata ut-metadata-reject] [p _ message] 90 | (trigger p :receive-metadata-reject (message :piece))) 91 | 92 | ;;************************************************ 93 | ;; Map incoming data based on it's first byte 94 | ;; and co-ordinate with the peer with the data 95 | ;;************************************************ 96 | 97 | (defmulti receive-data (fn [peer data] 98 | (char (first data)))) 99 | 100 | (defmethod receive-data msg-extended [p data] 101 | "When given an extension pass it off to a further multimethod" 102 | (let [extension (second data) 103 | reader (reader/push-back-reader (subarray data 2)) 104 | [message data] (bencode/decode reader :payload)] 105 | (receive-extension p extension message data))) 106 | 107 | (defmethod receive-data msg-choke [p _] 108 | (trigger p :receive-choke)) 109 | 110 | (defmethod receive-data msg-unchoke [p _] 111 | (trigger p :receive-unchoke)) 112 | 113 | (defmethod receive-data msg-interested [p _] 114 | (trigger p :receive-interested)) 115 | (defmethod receive-data msg-not-interested [p _] 116 | (trigger p :receive-not-interested)) 117 | 118 | (defmethod receive-data msg-have [p data] 119 | (let [data (crypt/unpack [:int] (rest data))] 120 | (trigger p :receive-have data))) 121 | 122 | (defmethod receive-data msg-bitfield [p data] 123 | (trigger p :receive-bitfield (bitfield/bitfield (subarray data 1)))) 124 | 125 | (defmethod receive-data msg-request [p data] 126 | (let [[index begin length] (crypt/unpack [:int :int :int] (subarray data 1))] 127 | (trigger p :receive-request index begin length))) 128 | 129 | (defmethod receive-data msg-piece [p data] 130 | (let [[index begin] (crypt/unpack [:int :int] (subarray data 1 9)) 131 | piece (subarray data 9)] 132 | (trigger p :receive-block index begin piece))) 133 | 134 | (defmethod receive-data msg-cancel [p data] 135 | (let [[index begin length] (crypt/unpack [:int :int :int] (rest data))] 136 | (trigger p :receive-cancel index begin length))) 137 | 138 | (defmethod receive-data :default [p data] 139 | (let [reserved (bitfield/bitfield (subarray data 20 28)) 140 | info-hash (vec (subarray data 28 48)) 141 | peer-id (crypt/byte-array->str (vec (subarray data 48 68)))] 142 | (trigger p :receive-handshake reserved info-hash peer-id))) 143 | 144 | ;;************************************************ 145 | ;; The bittorrent protocol 146 | ;;************************************************ 147 | 148 | (deftype BittorrentProtocol [torrent channel] 149 | protocol/Protocol 150 | 151 | (send-data [client string] 152 | (.log js/console "sending data" (aget channel "readyState") string) 153 | (.send channel string)) 154 | 155 | (send-data [client type data] 156 | (cond 157 | (nil? data) 158 | (protocol/send-data client (str type)) 159 | 160 | (string? data) 161 | (protocol/send-data client (str type data)) 162 | 163 | :else 164 | (let [; Turn our data into a vector if it isn't 165 | data (if (vector? data) data (vector data)) 166 | ; Concat all our data with the type at the start 167 | string (str type (apply str data))] 168 | (protocol/send-data client string)))) 169 | 170 | ; H.C commented out for the legacy demo 171 | ; (send-data [client & data] 172 | ; (if (string? (second data)) 173 | ; (protocol/send-data client (apply str data)) 174 | ; (let [; Build a buffer big enough to hold our data 175 | ; ; only count byte arrays, add one for the msg-type 176 | ; buffer-size (inc (reduce + (map count (rest data)))) 177 | ; byte-array (uint8-array buffer-size)] 178 | ; (.set byte-array [(first data)]) 179 | ; (loop [data (rest data) 180 | ; offset 1] 181 | ; (if-let [item (first data)] 182 | ; (do 183 | ; ; Add all the data to the buffer at the correct offset 184 | ; (.log js/console item offset) 185 | ; (.set byte-array item offset) 186 | ; (recur (rest data) (+ offset (count item)))) 187 | ; ; Finally send the buffer 188 | ; (protocol/send-data client (.-buffer byte-array))))) 189 | ; )) 190 | 191 | (send-handshake [client] 192 | "Generate a handshake string" 193 | (let [protocol-name "BitTorrent protocol" 194 | reserved (crypt/byte-array->str [00 00 00 00 00 10 00 00]) 195 | info-hash (crypt/byte-array->str (@torrent :info-hash)) 196 | data (str protocol-name reserved info-hash @peer-id)] 197 | (protocol/send-data client msg-handshake data))) 198 | 199 | (send-extended [client id message] 200 | (protocol/send-extended client id message nil)) 201 | 202 | (send-extended [client id message data] 203 | (let [id (if (keyword? id) (get extensions id) id) 204 | body (crypt/byte-array->str (bencode/encode message)) 205 | data (str (char id) body data)] 206 | (protocol/send-data client msg-extended data))) 207 | 208 | (send-extended-handshake [client] 209 | (let [message {:m extensions :metadata_size (@torrent :info-length)}] 210 | (protocol/send-extended client extended-handshake message))) 211 | 212 | (send-metadata-request [client piece-index] 213 | (let [message {:msg_type ut-metadata-request :piece piece-index}] 214 | (protocol/send-extended client ut-metadata message))) 215 | 216 | (send-metadata-piece [client piece-index info-length data] 217 | (let [message {:msg_type ut-metadata-piece 218 | :piece piece-index 219 | :metadata_size info-length}] 220 | (protocol/send-extended client ut-metadata message data))) 221 | 222 | (send-metadata-reject [client piece-index] 223 | (let [message {:msg_type ut-metadata-reject :piece piece-index}] 224 | (protocol/send-extended client ut-metadata message))) 225 | 226 | (send-choke [client] 227 | (protocol/send-data client msg-choke "")) 228 | 229 | (send-unchoke [client] 230 | (protocol/send-data client msg-unchoke "")) 231 | 232 | (send-interested [client] 233 | (protocol/send-data client msg-interested "")) 234 | 235 | (send-not-interested [client] 236 | (protocol/send-data client msg-not-interested "")) 237 | 238 | (send-have [client index] 239 | (let [data (crypt/pack :int index)] 240 | (protocol/send-data client msg-have data))) 241 | 242 | (send-bitfield [client] 243 | (let [byte-array (.-byte-array (@torrent :bitfield))] 244 | (protocol/send-data client msg-bitfield byte-array))) 245 | 246 | (send-request [client piece-index begin length] 247 | ; (js* "debugger;") 248 | (let [data (crypt/pack :int piece-index :int begin :int length)] 249 | (protocol/send-data client msg-request data))) 250 | 251 | ; H.C REVIEW 252 | (send-block [client piece-index begin piece] 253 | (let [data (crypt/pack :int piece-index :int begin)] 254 | ; (js* "debugger;") 255 | (protocol/send-data client msg-piece [data piece]))) 256 | 257 | (send-cancel [client index begin length] 258 | (let [data (crypt/pack :int index :int begin :int length)] 259 | (protocol/send-data client msg-cancel data))) 260 | 261 | ) 262 | 263 | (defn generate-protocol [torrent channel] 264 | "Generate an instance of the protocol" 265 | (BittorrentProtocol. torrent channel)) --------------------------------------------------------------------------------