├── .gitattributes ├── .gitignore ├── .nojekyll ├── LICENSE.md ├── README.md ├── favicon.png ├── index.html ├── src ├── base32.js ├── bencode.js ├── sha1.js ├── sha1.test.js ├── t2m.js └── t2m.loader.js └── style.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutbread/t2m/6a9d26d5c2cf040a6c6ac5c658b4b2e1d8841545/.nojekyll -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Use and/or steal to your heart's desire -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [https://nutbread.github.io/t2m/](https://nutbread.github.io/t2m/) -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutbread/t2m/6a9d26d5c2cf040a6c6ac5c658b4b2e1d8841545/favicon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Torrent to Magnet 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 26 | 29 | 35 | 41 | 42 |
22 |
23 | nutbread/t2m 24 |
25 |
27 |
28 |
30 |
31 |
Torrent to Magnet
32 |
Javascript implementation of magnet URI conversion
33 |
34 |
36 | 37 |
view on
38 |
github
39 |
40 |
43 | 44 |
45 |
46 |
47 | 48 |

Converter

49 |

50 |

51 | Javascript is required to use the inline converter. 52 |
53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 | 62 | 63 |
64 |
65 |
66 |
67 |
Drop .torrent files here
68 |
to convert them to magnet URIs
69 |
Or click to open the file browser
70 | 71 |
72 |
73 | 74 |
75 |
76 |
77 |
78 |

79 | 80 |
81 | 82 |
83 | 84 |

Libraries

85 |

86 | The following Javascript libraries are used on this page: 87 |

88 |

89 |

100 |

101 |

102 | They are primarly designed for web usage, but they can be trivially modified to be used with something such as node.js. 103 |

104 | 105 |

sha1.js

106 |

107 | Computes the SHA-1 hash of the specified data using the basic algorithm described in RFC 3174, with a paradigm similar to python's hashlib. Data can be input in chunks, and the digest can be calculated at any point without modifying the internal state. 108 |

109 | 110 |
Public interface
111 |

112 | The following are public methods on the SHA1 class:
113 |

132 |

133 | 134 |

bencode.js

135 |

136 | Encode/decode an object or string using the Bencode format. 137 |

138 | 139 |
Public interface
140 |

141 | The following are public methods on the Bencode module:
142 |

154 |

155 | 156 |

base32.js

157 |

158 | Encode/decode a string using the RFC 4648 Base32 format. 159 |

160 | 161 |
Public interface
162 |

163 | The following are public methods on the Base32 module:
164 |

174 |

175 | 176 |

Other Javascript files

177 |

178 | These are the other Javascript files included in this repository: 179 |

180 |

181 |

192 |

193 | 194 |
195 | 196 |
197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/base32.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // Module for encoding/decoding base32 5 | var Base32 = (function () { 6 | "use strict"; 7 | 8 | // Vars used 9 | var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", 10 | pad_lengths = [ 0, 1, 3, 4, 6 ], 11 | pad_char = "="; 12 | 13 | 14 | 15 | // Encode/decode functions 16 | return { 17 | /** 18 | Encode a string into base32 19 | 20 | @param str 21 | The string to convert. 22 | This string should be encoded in some way such that each character is in the range [0,255] 23 | @return 24 | A base32 encoded string 25 | */ 26 | encode: function (str) { 27 | var len = str.length, 28 | str_new = "", 29 | i = 0, 30 | c1, c2, c3, c4, c5; 31 | 32 | // Null pad 33 | while ((str.length % 5) !== 0) str += "\x00"; 34 | 35 | // String modify 36 | while (i < len) { 37 | c1 = str.charCodeAt(i++); 38 | c2 = str.charCodeAt(i++); 39 | c3 = str.charCodeAt(i++); 40 | c4 = str.charCodeAt(i++); 41 | c5 = str.charCodeAt(i++); 42 | 43 | str_new += alphabet[(c1 >> 3)]; 44 | str_new += alphabet[((c1 & 0x07) << 2) | (c2 >> 6)]; 45 | str_new += alphabet[((c2 & 0x3F) >> 1)]; 46 | str_new += alphabet[((c2 & 0x01) << 4) | (c3 >> 4)]; 47 | str_new += alphabet[((c3 & 0x0F) << 1) | (c4 >> 7)]; 48 | str_new += alphabet[((c4 & 0x7F) >> 2)]; 49 | str_new += alphabet[((c4 & 0x03) << 3) | (c5 >> 5)]; 50 | str_new += alphabet[(c5 & 0x1F)]; 51 | } 52 | 53 | // Padding 54 | if (i > len) { 55 | i = pad_lengths[i - len]; // (i - len) equals the number of times \x00 was padded 56 | str_new = str_new.substr(0, str_new.length - i); 57 | while ((str_new.length % 8) !== 0) str_new += pad_char; 58 | } 59 | 60 | // Done 61 | return str_new; 62 | }, 63 | /** 64 | Decode a string from base32 65 | 66 | @param str 67 | A valid base32 string 68 | @return 69 | The original string 70 | */ 71 | decode: function (str) { 72 | var len = str.length, 73 | str_new = "", 74 | bits = 0, 75 | char_buffer = 0, 76 | i; 77 | 78 | // Cut off padding 79 | while (len > 0 && str[len - 1] == "=") --len; 80 | 81 | // Iterate 82 | for (i = 0; i < len; ++i) { 83 | // Update with the 32bit value 84 | char_buffer = (char_buffer << 5) | alphabet.indexOf(str[i]); 85 | 86 | // Update bitcount 87 | bits += 5; 88 | if (bits >= 8) { 89 | // Update string 90 | str_new += String.fromCharCode((char_buffer >> (bits - 8)) & 0xFF); 91 | bits -= 8; 92 | } 93 | } 94 | 95 | // Done 96 | return str_new; 97 | }, 98 | }; 99 | 100 | })(); 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/bencode.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // Module for encoding/decoding Bencoded data 5 | var Bencode = (function () { 6 | "use strict"; 7 | 8 | // Encoding functions 9 | var encode = function (value) { 10 | // Type 11 | var t = typeof(value); 12 | 13 | // Number 14 | if (t == "number") return encode_int(Math.floor(value)); 15 | // String 16 | if (t == "string") return encode_string(value); 17 | // Array 18 | if (Array.isArray(value)) return encode_list(value); 19 | // Dict 20 | return encode_dict(value); 21 | }; 22 | 23 | var encode_int = function (value) { 24 | return "i" + value + "e"; 25 | }; 26 | var encode_string = function (value) { 27 | return "" + value.length + ":" + value; 28 | }; 29 | var encode_list = function (value) { 30 | var str = [ "l" ], 31 | i; 32 | 33 | // List values 34 | for (i = 0; i < value.length; ++i) { 35 | str.push(encode(value[i])); 36 | } 37 | 38 | // End 39 | str.push("e"); 40 | return str.join(""); 41 | }; 42 | var encode_dict = function (value) { 43 | var str = [ "d" ], 44 | keys = [], 45 | i; 46 | 47 | // Get and sort keys 48 | for (i in value) keys.push(i); 49 | keys.sort(); 50 | 51 | // Push values 52 | for (i = 0; i < keys.length; ++i) { 53 | str.push(encode_string(keys[i])); 54 | str.push(encode(value[keys[i]])); 55 | } 56 | 57 | // End 58 | str.push("e"); 59 | return str.join(""); 60 | }; 61 | 62 | 63 | 64 | // Decoding class 65 | var Decoder = function () { 66 | this.pos = 0; 67 | }; 68 | 69 | Decoder.prototype = { 70 | constructor: Decoder, 71 | 72 | decode: function (str) { 73 | // Errors 74 | var k = str[this.pos]; 75 | if (!(k in decode_generic)) throw "Invalid format"; 76 | 77 | // Call 78 | return decode_generic[k].call(this, str); 79 | }, 80 | decode_int: function (str) { 81 | // Skip the "i" prefix 82 | ++this.pos; 83 | 84 | var end = str.indexOf("e", this.pos), 85 | value; 86 | 87 | // No end 88 | if (end < 0) throw "Invalid format"; 89 | 90 | // Assume proper number format 91 | value = parseInt(str.substr(this.pos, end - this.pos), 10); 92 | 93 | // Done 94 | this.pos = end + 1; 95 | return value; 96 | }, 97 | decode_string: function (str) { 98 | var delim = str.indexOf(":", this.pos), 99 | length, value; 100 | 101 | // No end 102 | if (delim < 0) throw "Invalid format"; 103 | 104 | // Assume proper number format 105 | length = parseInt(str.substr(this.pos, delim - this.pos), 10); 106 | value = str.substr(delim + 1, length); 107 | 108 | // Done 109 | this.pos = delim + length + 1; 110 | return value; 111 | }, 112 | decode_list: function (str) { 113 | // Skip the "l" prefix 114 | ++this.pos; 115 | 116 | // Read list 117 | var list = [], 118 | value; 119 | 120 | // Loop until end or exception 121 | while (str[this.pos] != "e") { 122 | value = this.decode(str); // this throws errors if str[this.pos] is out of bounds 123 | list.push(value); 124 | } 125 | 126 | // Done; skip "e" suffix 127 | ++this.pos; 128 | return list; 129 | }, 130 | decode_dict: function (str) { 131 | // Skip the "d" prefix 132 | ++this.pos; 133 | 134 | // Read dict 135 | var dict = {}, 136 | key, value; 137 | 138 | // Loop until end or exception 139 | while (str[this.pos] != "e") { 140 | key = this.decode_string(str); 141 | value = this.decode(str); // this throws errors if str[this.pos] is out of bounds 142 | dict[key] = value; 143 | } 144 | 145 | // Done; skip "e" suffix 146 | ++this.pos; 147 | return dict; 148 | }, 149 | }; 150 | 151 | // Generic decode functions 152 | var decode_generic = { 153 | "l": Decoder.prototype.decode_list, 154 | "d": Decoder.prototype.decode_dict, 155 | "i": Decoder.prototype.decode_int, 156 | }, 157 | i; 158 | for (i = 0; i < 10; ++i) decode_generic[i.toString()] = Decoder.prototype.decode_string; 159 | 160 | 161 | 162 | // Encode/decode functions 163 | return { 164 | /** 165 | encode: function (obj) 166 | Encodes an object into a Bencode'd string 167 | 168 | @param obj 169 | The object to encode 170 | This should only be one of the following: 171 | string 172 | number (floats are floor'd to integers) 173 | array (containing things only from this list) 174 | object (containing things only from this list) 175 | Strings should be encoded in some way such that each character is in the range [0,255] 176 | @return 177 | A string representing the object 178 | */ 179 | encode: encode, 180 | /** 181 | decode: function (str) 182 | Decodes a Bencode'd string back into its original type 183 | 184 | @param str 185 | The string to decode 186 | @return 187 | The original object represented by str 188 | @throws 189 | Any one of the following self-explanatory strings 190 | "Invalid format" 191 | "Invalid string" 192 | "Invalid int" 193 | */ 194 | decode: function (str) { 195 | // Create a decoder and call 196 | return (new Decoder()).decode(str); 197 | }, 198 | }; 199 | 200 | })(); 201 | 202 | 203 | -------------------------------------------------------------------------------- /src/sha1.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // Class for SHA1 computation 5 | var SHA1 = (function () { 6 | "use strict"; 7 | 8 | // Private variables 9 | var hash_size = 20, 10 | message_block_length = 64, 11 | message_block_terminator = 0x80, 12 | message_length_bytes = 8, 13 | initial_intermediate_hash = new Uint32Array(5), 14 | K_constants = new Uint32Array(4); 15 | 16 | initial_intermediate_hash[0] = 0x67452301; 17 | initial_intermediate_hash[1] = 0xEFCDAB89; 18 | initial_intermediate_hash[2] = 0x98BADCFE; 19 | initial_intermediate_hash[3] = 0x10325476; 20 | initial_intermediate_hash[4] = 0xC3D2E1F0; 21 | 22 | K_constants[0] = 0x5A827999; 23 | K_constants[1] = 0x6ED9EBA1; 24 | K_constants[2] = 0x8F1BBCDC; 25 | K_constants[3] = 0xCA62C1D6; 26 | 27 | 28 | 29 | /** 30 | SHA1 instance constructor; creates an empty SHA1 object. 31 | 32 | @return 33 | A new SHA1 instance 34 | */ 35 | var SHA1 = function () { 36 | this.length = 0; 37 | this.message_block_index = 0; 38 | this.message_block = new Uint8Array(message_block_length); 39 | this.intermediate_hash = new Uint32Array(initial_intermediate_hash); 40 | }; 41 | 42 | 43 | 44 | // Private methods 45 | var pad = function () { 46 | var maxlen = this.message_block.length - message_length_bytes, 47 | high = Math.floor(this.length / 0x0FFFFFFFF) & 0xFFFFFFFF, 48 | low = this.length & 0xFFFFFFFF, 49 | message_block = this.message_block, 50 | message_block_index = this.message_block_index, 51 | input_intermediate_hash = this.intermediate_hash, 52 | output_intermediate_hash = new Uint32Array(this.intermediate_hash.length); 53 | 54 | // Termination byte 55 | message_block[message_block_index] = message_block_terminator; 56 | 57 | // Process another block if there's no space for the length 58 | if (message_block_index >= maxlen) { 59 | // 0-ify 60 | while (++message_block_index < message_block.length) message_block[message_block_index] = 0; 61 | 62 | // Process block 63 | process.call(this, message_block, input_intermediate_hash, output_intermediate_hash); 64 | 65 | // Create copies that don't interfere with "this" 66 | message_block = new Uint8Array(message_block.length); // no 0-ifying needed 67 | input_intermediate_hash = output_intermediate_hash; 68 | } 69 | else { 70 | // 0-ify 71 | while (++message_block_index < maxlen) message_block[message_block_index] = 0; 72 | } 73 | 74 | // Store length 75 | message_block[maxlen] = (high >>> 24) & 0xFF; 76 | message_block[++maxlen] = (high >>> 16) & 0xFF; 77 | message_block[++maxlen] = (high >>> 8) & 0xFF; 78 | message_block[++maxlen] = (high) & 0xFF; 79 | message_block[++maxlen] = (low >>> 24) & 0xFF; 80 | message_block[++maxlen] = (low >>> 16) & 0xFF; 81 | message_block[++maxlen] = (low >>> 8) & 0xFF; 82 | message_block[++maxlen] = (low) & 0xFF; 83 | 84 | process.call(this, message_block, input_intermediate_hash, output_intermediate_hash); 85 | 86 | // Return hash 87 | return output_intermediate_hash; 88 | }; 89 | var process = function (message_block, intermediate_hash_input, intermediate_hash_output) { 90 | var W = new Uint32Array(80), 91 | i, i4, temp, A, B, C, D, E; 92 | 93 | // Init W 94 | for (i = 0; i < 16; ++i) { 95 | i4 = i * 4; 96 | W[i] = 97 | (message_block[i4] << 24) | 98 | (message_block[i4 + 1] << 16) | 99 | (message_block[i4 + 2] << 8) | 100 | (message_block[i4 + 3]); 101 | } 102 | for (/*i = 16*/; i < 80; ++i) { 103 | W[i] = circular_shift(1, W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]); 104 | } 105 | 106 | A = intermediate_hash_input[0]; 107 | B = intermediate_hash_input[1]; 108 | C = intermediate_hash_input[2]; 109 | D = intermediate_hash_input[3]; 110 | E = intermediate_hash_input[4]; 111 | 112 | for (i = 0; i < 20; ++i) { 113 | temp = circular_shift(5, A) + ((B & C) | ((~B) & D)) + E + W[i] + K_constants[0]; 114 | E = D; 115 | D = C; 116 | C = circular_shift(30, B); 117 | B = A; 118 | A = temp & 0xFFFFFFFF; 119 | } 120 | for (/*i = 20*/; i < 40; ++i) { 121 | temp = circular_shift(5, A) + (B ^ C ^ D) + E + W[i] + K_constants[1]; 122 | E = D; 123 | D = C; 124 | C = circular_shift(30, B); 125 | B = A; 126 | A = temp & 0xFFFFFFFF; 127 | } 128 | for (/*i = 40*/; i < 60; ++i) { 129 | temp = circular_shift(5, A) + ((B & C) | (B & D) | (C & D)) + E + W[i] + K_constants[2]; 130 | E = D; 131 | D = C; 132 | C = circular_shift(30, B); 133 | B = A; 134 | A = temp & 0xFFFFFFFF; 135 | } 136 | for (/*i = 60*/; i < 80; ++i) { 137 | temp = circular_shift(5, A) + (B ^ C ^ D) + E + W[i] + K_constants[3]; 138 | E = D; 139 | D = C; 140 | C = circular_shift(30, B); 141 | B = A; 142 | A = temp & 0xFFFFFFFF; 143 | } 144 | 145 | intermediate_hash_output[0] = intermediate_hash_input[0] + A; 146 | intermediate_hash_output[1] = intermediate_hash_input[1] + B; 147 | intermediate_hash_output[2] = intermediate_hash_input[2] + C; 148 | intermediate_hash_output[3] = intermediate_hash_input[3] + D; 149 | intermediate_hash_output[4] = intermediate_hash_input[4] + E; 150 | }; 151 | var circular_shift = function (bits, word) { 152 | return (word << bits) | (word >>> (32 - bits)); 153 | }; 154 | 155 | 156 | 157 | // Public methods 158 | SHA1.prototype = { 159 | constructor: SHA1, 160 | 161 | /** 162 | Reset the state of the hashing object to its initial state. 163 | */ 164 | reset: function () { 165 | // Reset everything 166 | var i; 167 | 168 | this.length = 0; 169 | this.message_block_index = 0; 170 | 171 | for (i = 0; i < this.intermediate_hash.length; ++i) { 172 | this.intermediate_hash[i] = initial_intermediate_hash[i]; 173 | } 174 | for (i = 0; i < this.message_block.length; ++i) { 175 | this.message_block[i] = 0; 176 | } 177 | }, 178 | 179 | /** 180 | Feed data into the instance to update the hash. 181 | 182 | @param value_array 183 | The values to update with. This can either be: 184 | - A string (encoded so that every character is in the range [0,255]) 185 | - A (typed) array 186 | */ 187 | update: function (value_array) { 188 | var is_string = (typeof(value_array) == "string"), 189 | i; 190 | 191 | for (i = 0; i < value_array.length; ++i) { 192 | // Update block 193 | this.message_block[this.message_block_index] = is_string ? value_array.charCodeAt(i) : value_array[i]; 194 | 195 | // Update length 196 | this.length += 8; 197 | 198 | // Process block 199 | if (++this.message_block_index >= this.message_block.length) { 200 | process.call(this, this.message_block, this.intermediate_hash, this.intermediate_hash); 201 | this.message_block_index = 0; 202 | } 203 | } 204 | }, 205 | 206 | /** 207 | Computes the SHA1 digest of the data input so far. 208 | 209 | @return 210 | A Uint8Array of the computed digest 211 | */ 212 | digest: function () { 213 | // Setup 214 | var digest = new Uint8Array(hash_size), 215 | intermediate_hash_temp = pad.call(this), 216 | i; 217 | 218 | // Hash 219 | for (i = 0; i < digest.length; ++i) { 220 | digest[i] = intermediate_hash_temp[i >> 2] >> (8 * (3 - (i & 0x03))); 221 | } 222 | 223 | // Done 224 | return digest; 225 | }, 226 | }; 227 | 228 | 229 | 230 | // Return the class object 231 | return SHA1; 232 | 233 | })(); 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/sha1.test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // Functions for testing sha1.js 5 | (function () { 6 | "use strict"; 7 | 8 | // Tests 9 | var tests = [ 10 | { 11 | value: "abc", 12 | repeat: 1, 13 | correct: "A9 99 3E 36 47 06 81 6A BA 3E 25 71 78 50 C2 6C 9C D0 D8 9D", 14 | }, 15 | { 16 | value: "abcdbcdecdefdefgefghfghighijhi" + "jkijkljklmklmnlmnomnopnopq", 17 | repeat: 1, 18 | correct: "84 98 3E 44 1C 3B D2 6E BA AE 4A A1 F9 51 29 E5 E5 46 70 F1", 19 | }, 20 | { 21 | value: "a", 22 | repeat: 1000000, 23 | correct: "34 AA 97 3C D4 C4 DA A4 F6 1E EB 2B DB AD 27 31 65 34 01 6F", 24 | }, 25 | { 26 | value: "01234567012345670123456701234567" + "01234567012345670123456701234567", 27 | repeat: 10, 28 | correct: "DE A3 56 A2 CD DD 90 C7 A7 EC ED C5 EB B5 63 93 4F 46 04 52", 29 | }, 30 | { 31 | value: "a", 32 | repeat: 133047678, 33 | correct: "94 A8 41 C9 02 1D 23 57 A8 0A 77 60 01 E2 7D 2F 27 93 91 05", 34 | }, 35 | ]; 36 | 37 | 38 | // Readable hex 39 | var digest_to_spaced_hex = function (digest) { 40 | // String build 41 | var s = [], 42 | c, i; 43 | 44 | for (i = 0; i < digest.length; ++i) { 45 | if (i > 0) s.push(" "); 46 | c = digest[i].toString(16).toUpperCase(); 47 | if (c.length < 2) s.push("0"); 48 | s.push(c); 49 | } 50 | 51 | return s.join(""); 52 | }; 53 | 54 | 55 | // Test function 56 | var execute_tests = function () { 57 | // Vars for testing 58 | var sha = new SHA1(), 59 | failures = 0, 60 | test, digest, i, j; 61 | 62 | // Run tests 63 | for (i = 0; i < tests.length; ++i) { 64 | test = tests[i]; 65 | console.log("Test " + (i + 1) + ": " + JSON.stringify(test.value) + " repeated " + test.repeat + " time" + (test.repeat == 1 ? "" : "s")); 66 | 67 | sha.reset(); 68 | 69 | for (j = 0; j < test.repeat; ++j) { 70 | sha.update(test.value); 71 | } 72 | 73 | digest = digest_to_spaced_hex(sha.digest()); 74 | 75 | if (digest == test.correct) { 76 | console.log("Digest matches: YES"); 77 | console.log(" " + digest); 78 | } 79 | else { 80 | console.log("Digest matches: NO"); 81 | console.log(" " + digest + "(calculated)"); 82 | console.log(" " + test.correct + " (correct)"); 83 | ++failures; 84 | } 85 | 86 | console.log(""); 87 | } 88 | 89 | // Final status 90 | if (failures === 0) { 91 | console.log("All tests okay"); 92 | } 93 | else { 94 | console.log("" + failures + " test" + (failures == 1 ? "" : "s") + " failed"); 95 | } 96 | }; 97 | 98 | execute_tests(); 99 | 100 | })(); 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/t2m.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | var T2M = (function () { 5 | "use strict"; 6 | 7 | // Module for encoding/decoding UTF8 8 | var UTF8 = (function () { 9 | 10 | return { 11 | /** 12 | Encode a string into UTF-8 13 | 14 | @param str 15 | The string to convert. 16 | This string should be encoded in some way such that each character is in the range [0,255] 17 | @return 18 | A UTF-8 encoded string 19 | */ 20 | encode: function (str) { 21 | return unescape(encodeURIComponent(str)); 22 | }, 23 | /** 24 | Decode a string from UTF-8 25 | 26 | @param str 27 | A valid UTF-8 string 28 | @return 29 | The original string 30 | */ 31 | decode: function (str) { 32 | return decodeURIComponent(escape(str)); 33 | }, 34 | }; 35 | 36 | })(); 37 | 38 | 39 | 40 | // Class for reading .torrent files 41 | var Torrent = (function () { 42 | 43 | var Torrent = function () { 44 | this.events = { 45 | "load": [], 46 | "error": [], 47 | "read_error": [], 48 | "read_abort": [], 49 | }; 50 | this.file_name = null; 51 | this.data = null; 52 | }; 53 | 54 | 55 | 56 | var on_reader_load = function (event) { 57 | // Decode 58 | var data_str = event.target.result; 59 | 60 | if (data_str instanceof ArrayBuffer) { 61 | // Convert to string 62 | var data_str2 = "", 63 | i; 64 | 65 | data_str = new Uint8Array(data_str); 66 | for (i = 0; i < data_str.length; ++i) { 67 | data_str2 += String.fromCharCode(data_str[i]); 68 | } 69 | 70 | data_str = data_str2; 71 | } 72 | 73 | try { 74 | this.data = Bencode.decode(data_str); 75 | } 76 | catch (e) { 77 | // Throw an error 78 | this.data = null; 79 | this.file_name = null; 80 | trigger.call(this, "error", { 81 | type: "Bencode error", 82 | exception: e, 83 | }); 84 | return; 85 | } 86 | 87 | // Loaded 88 | trigger.call(this, "load", {}); 89 | }; 90 | var on_reader_error = function () { 91 | trigger.call(this, "read_error", {}); 92 | }; 93 | var on_reader_abort = function () { 94 | trigger.call(this, "read_abort", {}); 95 | }; 96 | var trigger = function (event, data) { 97 | // Trigger an event 98 | var callbacks = this.events[event], 99 | i; 100 | 101 | for (i = 0; i < callbacks.length; ++i) { 102 | callbacks[i].call(this, data, event); 103 | } 104 | }; 105 | var no_change = function (x) { 106 | return x; 107 | }; 108 | 109 | var magnet_component_order_default = [ "xt" , "xl" , "dn" , "tr" ]; 110 | 111 | var format_uri = function (array_values, encode_fcn) { 112 | if (array_values.length <= 1) return encode_fcn(array_values[0]); 113 | 114 | return array_values[0].replace(/\{([0-9]+)\}/, function (match) { 115 | return encode_fcn(array_values[parseInt(match[1], 10) + 1] || ""); 116 | }); 117 | }; 118 | 119 | 120 | 121 | /** 122 | Convert URI components object into a magnet URI. 123 | This is used to format the same object multiple times without rehashing anything. 124 | 125 | @param link_components 126 | An object returned from convert_to_magnet with return_components=true 127 | @param custom_name 128 | Can take one of the following values: 129 | null/undefined: name will remain the same as it originally was 130 | string: the custom name to give the magnet URI 131 | @param tracker_mode 132 | Can take one of the following values: 133 | null/undefined/false/number < 0: single tracker only (primary one) 134 | true: multiple trackers (without numbered suffix) 135 | number >= 0: multiple trackers (with numbered suffix starting at the specified number) 136 | @param uri_encode 137 | Can take one of the following values: 138 | null/undefined/true: encode components using encodeURIComponent 139 | false: no encoding; components are left as-is 140 | function: custom encoding function 141 | @param component_order 142 | A list containing the order URI components should appear in. 143 | Default is [ "xt" , "xl" , "dn" , "tr" ] 144 | null/undefined will use the default 145 | @return 146 | A formatted URI 147 | */ 148 | Torrent.components_to_magnet = function (link_components, custom_name, tracker_mode, uri_encode, component_order) { 149 | // Vars 150 | var link, obj, list1, val, i, j; 151 | 152 | uri_encode = (uri_encode === false) ? no_change : (typeof(uri_encode) == "function" ? uri_encode : encodeURIComponent); 153 | component_order = (component_order === null) ? magnet_component_order_default : component_order; 154 | 155 | // Setup 156 | if (typeof(custom_name) == "string") { 157 | link_components.dn.values = [ custom_name ]; 158 | } 159 | 160 | link_components.tr.suffix = -1; 161 | if (typeof(tracker_mode) == "number") { 162 | tracker_mode = Math.floor(tracker_mode); 163 | if (tracker_mode >= 0) link_components.tr.suffix = tracker_mode; 164 | } 165 | else if (tracker_mode === true) { 166 | link_components.tr.suffix = -2; 167 | } 168 | 169 | // Form into a URL 170 | link = "magnet:"; 171 | val = 0; // number of components added 172 | for (i = 0; i < component_order.length; ++i) { 173 | if (!(component_order[i] in link_components)) continue; // not valid 174 | obj = link_components[component_order[i]]; 175 | list1 = obj.values; 176 | for (j = 0; j < list1.length; ++j) { 177 | // Separator 178 | link += (val === 0 ? "?" : "&"); 179 | ++val; 180 | 181 | // Key 182 | link += component_order[i]; 183 | 184 | // Number 185 | if (obj.suffix >= 0 && list1.length > 1) { 186 | link += "."; 187 | link += obj.suffix; 188 | ++obj.suffix; 189 | } 190 | 191 | // Value 192 | link += "="; 193 | link += format_uri(list1[j], uri_encode); 194 | 195 | // Done 196 | if (obj.suffix == -1) break; 197 | } 198 | } 199 | 200 | // Done 201 | return link; 202 | }; 203 | 204 | 205 | 206 | Torrent.prototype = { 207 | constructor: Torrent, 208 | 209 | read: function (file) { 210 | this.data = null; 211 | this.file_name = file.name; 212 | 213 | var reader = new FileReader(); 214 | 215 | reader.addEventListener("load", on_reader_load.bind(this), false); 216 | reader.addEventListener("error", on_reader_error.bind(this), false); 217 | reader.addEventListener("abort", on_reader_abort.bind(this), false); 218 | 219 | try { 220 | reader.readAsBinaryString(file); 221 | } 222 | catch (e) { 223 | reader.readAsArrayBuffer(file); 224 | } 225 | }, 226 | 227 | on: function (event, callback) { 228 | if (event in this.events) { 229 | this.events[event].push(callback); 230 | return true; 231 | } 232 | return false; 233 | }, 234 | off: function (event, callback) { 235 | if (event in this.events) { 236 | var callbacks = this.events[event], 237 | i; 238 | 239 | for (i = 0; i < callbacks.length; ++i) { 240 | if (callbacks[i] == callback) { 241 | callbacks.splice(i, 1); 242 | return true; 243 | } 244 | } 245 | } 246 | return false; 247 | }, 248 | 249 | /** 250 | Convert the torrent data into a magnet link. 251 | 252 | @param custom_name 253 | Can take one of the following values: 254 | null/undefined: no custom name will be generated, but if the name field is absent, it will be assumed from the original file's name 255 | false: no custom name will be generated OR assumed from the original file name 256 | string: the custom name to give the magnet URI 257 | @param tracker_mode 258 | Can take one of the following values: 259 | null/undefined/false/number < 0: single tracker only (primary one) 260 | true: multiple trackers (without numbered suffix) 261 | number >= 0: multiple trackers (with numbered suffix starting at the specified number) 262 | @param uri_encode 263 | Can take one of the following values: 264 | null/undefined/true: encode components using encodeURIComponent 265 | false: no encoding; components are left as-is 266 | function: custom encoding function 267 | @param component_order 268 | A list containing the order URI components should appear in. 269 | Default is [ "xt" , "xl" , "dn" , "tr" ] 270 | null/undefined will use the default 271 | @param return_components 272 | If true, this returns the link components which can then be used with components_to_magnet 273 | @return 274 | A formatted URI if return_components is falsy, else an object containing the parts of the link 275 | Also can return null if insufficient data is found 276 | */ 277 | convert_to_magnet: function (custom_name, tracker_mode, uri_encode, component_order, return_components) { 278 | // Insufficient data 279 | if (this.data === null || !("info" in this.data)) return null; 280 | 281 | // Bencode info 282 | var info = this.data.info, 283 | info_bencoded = Bencode.encode(info), 284 | info_hasher = new SHA1(), 285 | link_components = {}, 286 | info_hash, link, list1, list2, val, i, j; 287 | 288 | // Hash 289 | info_hasher.update(info_bencoded); 290 | info_hash = info_hasher.digest(); 291 | info_hash = String.fromCharCode.apply(null, info_hash); // convert to binary string 292 | info_hash = Base32.encode(info_hash); // convert to base32 293 | 294 | // Setup link 295 | for (i = 0; i < magnet_component_order_default.length; ++i) { 296 | link_components[magnet_component_order_default[i]] = { 297 | suffix: -1, 298 | values: [], 299 | }; 300 | } 301 | 302 | // Create 303 | link_components.xt.values.push([ "urn:btih:{0}", info_hash ]); 304 | 305 | if ("length" in info) { 306 | link_components.xl.values.push([ info.length ]); 307 | } 308 | 309 | if (typeof(custom_name) == "string") { 310 | link_components.dn.values.push([ custom_name ]); 311 | } 312 | else if ("name" in info) { 313 | link_components.dn.values.push([ UTF8.decode(info.name) ]); 314 | } 315 | else if (custom_name !== false && this.file_name) { 316 | link_components.dn.values.push([ this.file_name ]); 317 | } 318 | 319 | list1 = link_components.tr.values; 320 | if ("announce" in this.data) { 321 | list1.push([ UTF8.decode(this.data.announce) ]); 322 | } 323 | if ("announce-list" in this.data && Array.isArray(list2 = this.data["announce-list"])) { 324 | // Add more trackers 325 | for (i = 0; i < list2.length; ++i) { 326 | if (!Array.isArray(list2[i])) continue; // bad data 327 | for (j = 0; j < list2[i].length; ++j) { 328 | val = UTF8.decode(list2[i][j]); 329 | if (list1.indexOf(val) < 0) list1.push([ val ]); 330 | } 331 | } 332 | } 333 | 334 | // Convert 335 | if (return_components) return link_components; 336 | link = Torrent.components_to_magnet(link_components, null, tracker_mode, uri_encode, component_order); 337 | 338 | // Done 339 | return link; 340 | }, 341 | }; 342 | 343 | 344 | 345 | return Torrent; 346 | 347 | })(); 348 | 349 | 350 | 351 | // Class for enumerating the results in the DOM 352 | var Result = (function () { 353 | 354 | var Result = function () { 355 | this.torrent_magnet_components = null; 356 | 357 | this.container = null; 358 | this.magnet_link = null; 359 | this.magnet_link_text = null; 360 | this.magnet_textbox = null; 361 | this.options_link = null; 362 | this.options_container = null; 363 | this.options = null; 364 | }; 365 | 366 | 367 | 368 | var on_options_link_click = function () { 369 | if (this.options_container.classList.contains("converted_item_options_container_visible")) { 370 | this.options_container.classList.remove("converted_item_options_container_visible"); 371 | this.magnet_textbox.readOnly = true; 372 | } 373 | else { 374 | this.options_container.classList.add("converted_item_options_container_visible"); 375 | this.magnet_textbox.readOnly = false; 376 | } 377 | }; 378 | 379 | var on_textbox_click = function () { 380 | if (this.magnet_textbox.readOnly) { 381 | this.magnet_textbox.select(); 382 | } 383 | }; 384 | var on_textbox_keydown = function () { 385 | if (this.magnet_textbox.readOnly) return; 386 | 387 | setTimeout(on_textbox_update.bind(this), 10); 388 | }; 389 | var on_textbox_change = function () { 390 | on_textbox_update.call(this); 391 | }; 392 | var on_textbox_update = function () { 393 | // Get value 394 | var uri = this.magnet_textbox.value, 395 | protocol = "magnet:"; 396 | 397 | // Must have correct protocol 398 | if (uri.substr(0, protocol.length).toLowerCase() != protocol) { 399 | if (uri.length < protocol.length && uri.toLowerCase() == protocol.substr(0, uri.length)) { 400 | // Almost correct 401 | uri += protocol.substr(uri.length); 402 | } 403 | else { 404 | // Wrong 405 | uri = protocol + uri; 406 | } 407 | } 408 | 409 | // Update 410 | this.magnet_link.setAttribute("href", uri); 411 | this.magnet_link_text.textContent = uri; 412 | }; 413 | 414 | var on_option_change = function () { 415 | update_links.call(this, true); 416 | }; 417 | 418 | var update_links = function (update_displays) { 419 | // Update magnet links 420 | var magnet_uri = "magnet:asdf", 421 | tracker_mode = false, 422 | order = [ "xt" ]; 423 | 424 | if (this.options[0][0][1].checked) { 425 | order.push("dn"); 426 | } 427 | if (this.options[1][0][1].checked) { 428 | order.push("xl"); 429 | } 430 | if (this.options[2][0][1].checked) { 431 | order.push("tr"); 432 | if (this.options[2][1][1].checked) { 433 | tracker_mode = true; 434 | if (this.options[2][2][1].checked) { 435 | tracker_mode = 1; 436 | } 437 | } 438 | } 439 | 440 | magnet_uri = Torrent.components_to_magnet(this.torrent_magnet_components, null, tracker_mode, true, order); 441 | 442 | // Update text/values 443 | this.magnet_link.setAttribute("href", magnet_uri); 444 | this.magnet_link_text.textContent = magnet_uri; 445 | this.magnet_textbox.value = magnet_uri; 446 | 447 | if (!update_displays) return; 448 | 449 | // Update display 450 | var i, j, opt_list; 451 | for (i = 0; i < this.options.length; ++i) { 452 | opt_list = this.options[i]; 453 | 454 | for (j = 0; j < opt_list.length; ++j) { 455 | if (!opt_list[j][1].checked) { 456 | // This is unchecked; modify boxes after 457 | opt_list[j][0].classList.add("converted_item_option_part_visible"); 458 | // Hide 459 | while (++j < opt_list.length) { 460 | opt_list[j][0].classList.remove("converted_item_option_part_visible"); 461 | opt_list[j][1].checked = false; 462 | } 463 | break; 464 | } 465 | } 466 | } 467 | }; 468 | 469 | 470 | 471 | Result.prototype = { 472 | constructor: Result, 473 | 474 | generate: function (torrent_object, parent_node) { 475 | var n1, n2, n3, n4, n5, i, j, ev_bind; 476 | 477 | // Clear 478 | this.options = []; 479 | 480 | 481 | //{ Setup DOM nodes 482 | this.container = document.createElement("div"); 483 | this.container.className = "converted_item"; 484 | 485 | 486 | // Title 487 | n1 = document.createElement("div"); 488 | n1.className = "converted_item_title_container"; 489 | 490 | n2 = document.createElement("div"); 491 | n2.className = "converted_item_title"; 492 | n2.textContent = torrent_object.file_name || (torrent_object.data && torrent_object.data.info ? torrent_object.data.name : null) || ".torrent"; 493 | 494 | n1.appendChild(n2); 495 | this.container.appendChild(n1); 496 | 497 | // Contents 498 | n1 = document.createElement("div"); 499 | n1.className = "converted_item_contents"; 500 | 501 | // Links 502 | n2 = document.createElement("div"); 503 | n2.className = "converted_item_link_container"; 504 | 505 | this.magnet_link = document.createElement("a"); 506 | this.magnet_link.className = "converted_item_link"; 507 | 508 | this.magnet_link_text = document.createElement("span"); 509 | 510 | this.magnet_link.appendChild(this.magnet_link_text); 511 | n2.appendChild(this.magnet_link); 512 | 513 | this.magnet_textbox = document.createElement("input"); 514 | this.magnet_textbox.className = "converted_item_textbox"; 515 | this.magnet_textbox.setAttribute("type", "text"); 516 | this.magnet_textbox.readOnly = true; 517 | 518 | n2.appendChild(this.magnet_textbox); 519 | n1.appendChild(n2); 520 | 521 | 522 | // Options container 523 | this.options_container = document.createElement("div"); 524 | this.options_container.className = "converted_item_options_container"; 525 | 526 | // Header 527 | n2 = document.createElement("div"); 528 | n2.className = "converted_item_header"; 529 | 530 | n3 = document.createElement("span"); 531 | n3.className = "converted_item_header_text"; 532 | n3.textContent = "Options:"; 533 | 534 | n2.appendChild(n3); 535 | 536 | this.options_link = document.createElement("a"); 537 | this.options_link.className = "converted_item_options_toggle"; 538 | 539 | n3 = document.createElement("span"); 540 | this.options_link.appendChild(n3); 541 | 542 | n2.appendChild(this.options_link); 543 | this.options_container.appendChild(n2); 544 | 545 | 546 | // Options 547 | n2 = document.createElement("div"); 548 | n2.className = "converted_item_options"; 549 | 550 | // Name 551 | this.options.push([]); 552 | 553 | n3 = document.createElement("div"); 554 | n3.className = "converted_item_option"; 555 | 556 | n4 = document.createElement("label"); 557 | n4.className = "converted_item_option_part converted_item_option_part_visible"; 558 | 559 | n5 = document.createElement("input"); 560 | n5.className = "converted_item_option_checkbox checkbox"; 561 | n5.setAttribute("type", "checkbox"); 562 | n5.checked = true; 563 | n4.appendChild(n5); 564 | 565 | this.options[this.options.length - 1].push([ n4 , n5 ]); 566 | 567 | n5 = document.createElement("span"); 568 | n5.className = "converted_item_option_text"; 569 | n5.textContent = "Include name"; 570 | n4.appendChild(n5); 571 | 572 | n3.appendChild(n4); 573 | n2.appendChild(n3); 574 | 575 | 576 | // Data length 577 | this.options.push([]); 578 | 579 | n3 = document.createElement("div"); 580 | n3.className = "converted_item_option"; 581 | 582 | n4 = document.createElement("label"); 583 | n4.className = "converted_item_option_part converted_item_option_part_visible"; 584 | 585 | n5 = document.createElement("input"); 586 | n5.className = "converted_item_option_checkbox checkbox"; 587 | n5.setAttribute("type", "checkbox"); 588 | n5.checked = true; 589 | n4.appendChild(n5); 590 | 591 | this.options[this.options.length - 1].push([ n4 , n5 ]); 592 | 593 | n5 = document.createElement("span"); 594 | n5.className = "converted_item_option_text"; 595 | n5.textContent = "Include data length"; 596 | n4.appendChild(n5); 597 | 598 | n3.appendChild(n4); 599 | n2.appendChild(n3); 600 | 601 | 602 | // Tracker 603 | this.options.push([]); 604 | 605 | n3 = document.createElement("div"); 606 | n3.className = "converted_item_option"; 607 | 608 | n4 = document.createElement("label"); 609 | n4.className = "converted_item_option_part converted_item_option_part_visible"; 610 | 611 | n5 = document.createElement("input"); 612 | n5.className = "converted_item_option_checkbox checkbox"; 613 | n5.setAttribute("type", "checkbox"); 614 | n5.checked = true; 615 | n4.appendChild(n5); 616 | 617 | this.options[this.options.length - 1].push([ n4 , n5 ]); 618 | 619 | n5 = document.createElement("span"); 620 | n5.className = "converted_item_option_text"; 621 | n5.textContent = "Include tracker"; 622 | n4.appendChild(n5); 623 | 624 | n3.appendChild(n4); 625 | 626 | 627 | n4 = document.createElement("label"); 628 | n4.className = "converted_item_option_part converted_item_option_part_visible"; 629 | 630 | n5 = document.createElement("input"); 631 | n5.className = "converted_item_option_checkbox checkbox"; 632 | n5.setAttribute("type", "checkbox"); 633 | n5.checked = false; 634 | n4.appendChild(n5); 635 | 636 | this.options[this.options.length - 1].push([ n4 , n5 ]); 637 | 638 | n5 = document.createElement("span"); 639 | n5.className = "converted_item_option_text"; 640 | n5.textContent = "Allow multiple trackers"; 641 | n4.appendChild(n5); 642 | 643 | n3.appendChild(n4); 644 | 645 | 646 | n4 = document.createElement("label"); 647 | n4.className = "converted_item_option_part"; 648 | 649 | n5 = document.createElement("input"); 650 | n5.className = "converted_item_option_checkbox checkbox"; 651 | n5.setAttribute("type", "checkbox"); 652 | n5.checked = false; 653 | n4.appendChild(n5); 654 | 655 | this.options[this.options.length - 1].push([ n4 , n5 ]); 656 | 657 | n5 = document.createElement("span"); 658 | n5.className = "converted_item_option_text"; 659 | n5.textContent = "Numbered keys"; 660 | n4.appendChild(n5); 661 | 662 | n3.appendChild(n4); 663 | n2.appendChild(n3); 664 | 665 | 666 | // Add options 667 | this.options_container.appendChild(n2); 668 | n1.appendChild(this.options_container); 669 | 670 | 671 | // Done 672 | this.container.appendChild(n1); 673 | //} 674 | 675 | 676 | // Data 677 | this.torrent_magnet_components = torrent_object.convert_to_magnet(null, false, true, null, true); 678 | update_links.call(this, false); 679 | 680 | 681 | // Events 682 | this.options_link.addEventListener("click", on_options_link_click.bind(this), false); 683 | this.magnet_textbox.addEventListener("click", on_textbox_click.bind(this), false); 684 | this.magnet_textbox.addEventListener("keydown", on_textbox_keydown.bind(this), false); 685 | this.magnet_textbox.addEventListener("change", on_textbox_change.bind(this), false); 686 | 687 | ev_bind = on_option_change.bind(this); 688 | for (i = 0; i < this.options.length; ++i) { 689 | for (j = 0; j < this.options[i].length; ++j) { 690 | this.options[i][j][1].addEventListener("change", ev_bind, false); 691 | } 692 | } 693 | 694 | 695 | // Rice and add 696 | if (rice_checkboxes) { 697 | rice_checkboxes(this.container.querySelectorAll("input[type=checkbox].checkbox")); 698 | } 699 | if (parent_node) parent_node.appendChild(this.container); 700 | 701 | }, 702 | update: function () { 703 | 704 | }, 705 | }; 706 | 707 | 708 | 709 | return Result; 710 | 711 | })(); 712 | 713 | 714 | 715 | // Other functions 716 | var rice_checkboxes = null; 717 | var on_torrent_load = function () { 718 | var container = document.querySelector(".converted"), 719 | result; 720 | 721 | if (container === null) return; 722 | 723 | container.classList.add("converted_visible"); 724 | 725 | result = new Result(); 726 | result.generate(this, container); 727 | }; 728 | 729 | 730 | 731 | // Exposed functions 732 | var functions = { 733 | setup: function (rice_checkboxes_import) { 734 | rice_checkboxes = rice_checkboxes_import; 735 | }, 736 | queue_torrent_files: function (files) { 737 | // Read files 738 | var i, t; 739 | 740 | for (i = 0; i < files.length; ++i) { 741 | t = new Torrent(); 742 | t.on("load", on_torrent_load); 743 | t.read(files[i]); 744 | } 745 | }, 746 | }; 747 | 748 | return functions; 749 | 750 | })(); 751 | 752 | 753 | -------------------------------------------------------------------------------- /src/t2m.loader.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | (function () { 5 | "use strict"; 6 | 7 | // Function for performing actions as soon as possible 8 | var on_ready = (function () { 9 | 10 | // Vars 11 | var callbacks = [], 12 | check_interval = null, 13 | check_interval_time = 250; 14 | 15 | // Check if ready and run callbacks 16 | var callback_check = function () { 17 | if ( 18 | (document.readyState === "interactive" || document.readyState === "complete") && 19 | callbacks !== null 20 | ) { 21 | // Run callbacks 22 | var cbs = callbacks, 23 | cb_count = cbs.length, 24 | i; 25 | 26 | // Clear 27 | callbacks = null; 28 | 29 | for (i = 0; i < cb_count; ++i) { 30 | cbs[i].call(null); 31 | } 32 | 33 | // Clear events and checking interval 34 | window.removeEventListener("load", callback_check, false); 35 | window.removeEventListener("readystatechange", callback_check, false); 36 | 37 | if (check_interval !== null) { 38 | clearInterval(check_interval); 39 | check_interval = null; 40 | } 41 | 42 | // Okay 43 | return true; 44 | } 45 | 46 | // Not executed 47 | return false; 48 | }; 49 | 50 | // Listen 51 | window.addEventListener("load", callback_check, false); 52 | window.addEventListener("readystatechange", callback_check, false); 53 | 54 | // Callback adding function 55 | return function (cb) { 56 | if (callbacks === null) { 57 | // Ready to execute 58 | cb.call(null); 59 | } 60 | else { 61 | // Delay 62 | callbacks.push(cb); 63 | 64 | // Set a check interval 65 | if (check_interval === null && callback_check() !== true) { 66 | check_interval = setInterval(callback_check, check_interval_time); 67 | } 68 | } 69 | }; 70 | 71 | })(); 72 | 73 | 74 | 75 | // Functions 76 | var script_add = (function () { 77 | 78 | var script_on_load = function (state) { 79 | // Okay 80 | script_remove_event_listeners.call(this, state, true); 81 | }; 82 | var script_on_error = function (state) { 83 | // Error 84 | script_remove_event_listeners.call(this, state, false); 85 | }; 86 | var script_on_readystatechange = function (state) { 87 | if (this.readyState === "loaded" || this.readyState === "complete") { 88 | // Okay 89 | script_remove_event_listeners.call(this, state, true); 90 | } 91 | }; 92 | var script_remove_event_listeners = function (state, okay) { 93 | // Remove event listeners 94 | this.addEventListener("load", state.on_load, false); 95 | this.addEventListener("error", state.on_error, false); 96 | this.addEventListener("readystatechange", state.on_readystatechange, false); 97 | 98 | state.on_load = null; 99 | state.on_error = null; 100 | state.on_readystatechange = null; 101 | 102 | // Trigger 103 | if (state.callback) state.callback.call(null, okay, this); 104 | 105 | // Remove 106 | var par = this.parentNode; 107 | if (par) par.removeChild(this); 108 | }; 109 | 110 | 111 | 112 | return function (url, callback) { 113 | var head = document.head, 114 | script, state; 115 | 116 | if (!head) { 117 | // Callback and done 118 | callback.call(null, false, null); 119 | return false; 120 | } 121 | 122 | // Load state 123 | state = { 124 | on_load: null, 125 | on_error: null, 126 | on_readystatechange: null, 127 | callback: callback, 128 | }; 129 | 130 | // New script tag 131 | script = document.createElement("script"); 132 | script.async = true; 133 | script.setAttribute("src", url); 134 | 135 | // Events 136 | script.addEventListener("load", (state.on_load = script_on_load.bind(script, state)), false); 137 | script.addEventListener("error", (state.on_error = script_on_error.bind(script, state)), false); 138 | script.addEventListener("readystatechange", (state.on_readystatechange = script_on_readystatechange.bind(script, state)), false); 139 | 140 | // Add 141 | head.appendChild(script); 142 | 143 | // Done 144 | return true; 145 | }; 146 | 147 | })(); 148 | 149 | var on_generic_stop_propagation = function (event) { 150 | event.stopPropagation(); 151 | }; 152 | 153 | var on_exclusive_mode_change = function (flag_node) { 154 | exclusive_mode_update.call(this, flag_node, false); 155 | }; 156 | var exclusive_mode_update = (function () { 157 | var previous_fragment = ""; 158 | 159 | return function (flag_node, check_fragment) { 160 | var hash_is_exclusive = (window.location.hash == "#converter.exclusive"); 161 | 162 | if (check_fragment) { 163 | this.checked = hash_is_exclusive; 164 | } 165 | else { 166 | if (this.checked ^ (!hash_is_exclusive)) { 167 | previous_fragment = window.location.hash; 168 | } 169 | window.history.replaceState({}, "", window.location.pathname + (this.checked ? "#converter.exclusive" : previous_fragment)); 170 | } 171 | 172 | if (this.checked) { 173 | flag_node.classList.add("exclusive_enabled"); 174 | } 175 | else { 176 | flag_node.classList.remove("exclusive_enabled"); 177 | } 178 | }; 179 | })(); 180 | 181 | var on_converter_click = function (converter_files_input, event) { 182 | if (event.which != 2 && event.which != 3) { 183 | converter_files_input.click(); 184 | } 185 | }; 186 | var on_converter_files_change = function (converter) { 187 | // Read 188 | on_converter_test_files.call(converter, this.files); 189 | 190 | // Nullify 191 | this.value = null; 192 | }; 193 | 194 | var on_file_dragover = function (converter, event) { 195 | if (Array.prototype.indexOf.call(event.dataTransfer.types, "Files") < 0) return; 196 | 197 | converter.classList.add("converter_files_active"); 198 | if (this === converter) converter.classList.add("converter_files_hover"); 199 | 200 | event.dataTransfer.dropEffect = "copy"; 201 | event.preventDefault(); 202 | event.stopPropagation(); 203 | return false; 204 | }; 205 | var on_file_dragleave = function (converter, event) { 206 | if (Array.prototype.indexOf.call(event.dataTransfer.types, "Files") < 0) return; 207 | 208 | converter.classList.remove("converter_files_hover"); 209 | if (this !== converter) converter.classList.remove("converter_files_active"); 210 | 211 | event.preventDefault(); 212 | event.stopPropagation(); 213 | return false; 214 | }; 215 | var on_file_drop = function (converter, event) { 216 | // Reset style 217 | converter.classList.remove("converter_files_active"); 218 | converter.classList.remove("converter_files_hover"); 219 | event.preventDefault(); 220 | event.stopPropagation(); 221 | 222 | // Not over the converter 223 | if (this !== converter) return false; 224 | 225 | // Read files 226 | on_converter_test_files.call(converter, event.dataTransfer.files); 227 | 228 | // Done 229 | return false; 230 | }; 231 | 232 | var on_converter_test_files = function (files) { 233 | // Read 234 | var re_ext = /(\.[^\.]*|)$/, 235 | read_files = [], 236 | ext, i; 237 | 238 | for (i = 0; i < files.length; ++i) { 239 | ext = re_ext.exec(files[i].name)[1].toLowerCase(); 240 | if (ext == ".torrent") { 241 | read_files.push(files[i]); 242 | } 243 | } 244 | 245 | // Nothing to do 246 | if (read_files.length === 0) return; 247 | 248 | // Load scripts if necessary 249 | load_requirements(function (errors) { 250 | if (errors === 0) { 251 | // Load 252 | var T2M_obj; 253 | try { 254 | T2M_obj = T2M; 255 | } 256 | catch(e) { 257 | return; // not found 258 | } 259 | T2M_obj.queue_torrent_files(read_files); 260 | } 261 | }); 262 | }; 263 | 264 | var load_requirements = (function () { 265 | 266 | // Script requirements 267 | var requirements = [ 268 | "src/sha1.js", 269 | "src/bencode.js", 270 | "src/base32.js", 271 | "src/t2m.js", 272 | ]; 273 | 274 | 275 | var on_all_scripts_loaded = function () { 276 | var T2M_obj; 277 | try { 278 | T2M_obj = T2M; 279 | } 280 | catch(e) { 281 | return; // not found 282 | } 283 | 284 | T2M_obj.setup(rice_checkboxes); 285 | }; 286 | var on_script_load = function (state, callback, okay) { 287 | if (okay) ++state.okay; 288 | 289 | if (++state.count >= state.total) { 290 | // All loaded/errored 291 | if (state.total - state.okay === 0) on_all_scripts_loaded(); 292 | callback.call(null, state.total - state.okay); 293 | } 294 | }; 295 | 296 | // Return the loading function 297 | return function (callback) { 298 | // Already loaded? 299 | if (requirements === null) { 300 | // Yes 301 | callback.call(null, 0); 302 | return; 303 | } 304 | 305 | var head = document.head, 306 | on_load, i; 307 | 308 | if (!head) return false; 309 | 310 | // Load 311 | on_load = on_script_load.bind(null, { okay: 0, count: 0, total: requirements.length, }, callback); 312 | for (i = 0; i < requirements.length; ++i) { 313 | script_add(requirements[i], on_load); 314 | } 315 | 316 | // Done 317 | requirements = null; 318 | return true; 319 | }; 320 | 321 | })(); 322 | 323 | var restyle_noscript = function () { 324 | // Script 325 | var nodes = document.querySelectorAll(".script_disabled"), 326 | i; 327 | 328 | for (i = 0; i < nodes.length; ++i) { 329 | nodes[i].classList.remove("script_visible"); 330 | } 331 | 332 | nodes = document.querySelectorAll(".script_enabled"); 333 | for (i = 0; i < nodes.length; ++i) { 334 | nodes[i].classList.add("script_visible"); 335 | } 336 | }; 337 | 338 | var rice_checkboxes = function (nodes) { 339 | var svgns = "http://www.w3.org/2000/svg", 340 | i, par, sib, node, n1, n2, n3; 341 | 342 | nodes = nodes || document.querySelectorAll("input[type=checkbox].checkbox"); 343 | 344 | for (i = 0; i < nodes.length; ++i) { 345 | node = nodes[i]; 346 | par = node.parentNode; 347 | sib = node.nextSibling; 348 | 349 | // Create new checkbox 350 | n1 = document.createElement("label"); 351 | n1.className = node.className; 352 | 353 | n2 = document.createElementNS(svgns, "svg"); 354 | n2.setAttribute("svgns", svgns); 355 | n2.setAttribute("viewBox", "0 0 16 16"); 356 | 357 | n3 = document.createElementNS(svgns, "polygon"); 358 | n3.setAttribute("points", "13,0 16,2 8,16 5,16 0,11 2,8 6,11.5"); 359 | 360 | // Re-add 361 | n2.appendChild(n3); 362 | n1.appendChild(n2); 363 | par.insertBefore(n1, node); 364 | n1.insertBefore(node, n2); 365 | } 366 | }; 367 | 368 | 369 | 370 | // Execute 371 | on_ready(function () { 372 | // Noscript 373 | var nodes, i; 374 | 375 | // Rice 376 | restyle_noscript(); 377 | rice_checkboxes(); 378 | 379 | // Stop propagation links 380 | nodes = document.querySelectorAll(".link_stop_propagation"); 381 | for (i = 0; i < nodes.length; ++i) { 382 | nodes[i].addEventListener("click", on_generic_stop_propagation, false); 383 | } 384 | 385 | // Setup converter 386 | var converter = document.querySelector(".converter"), 387 | converter_files = document.querySelector(".converter_files_input"), 388 | exclusive_mode = document.querySelector("input.converter_exclusive_mode_check"), 389 | non_exclusive_body = document.querySelector(".non_exclusive"), 390 | body = document.body; 391 | 392 | if (converter !== null) { 393 | // File browser 394 | if (converter_files !== null) { 395 | converter.addEventListener("click", on_converter_click.bind(converter, converter_files), false); 396 | converter_files.addEventListener("change", on_converter_files_change.bind(converter_files, converter), false); 397 | } 398 | 399 | // File drag/drop events 400 | converter.addEventListener("dragover", on_file_dragover.bind(converter, converter), false); 401 | converter.addEventListener("dragleave", on_file_dragleave.bind(converter, converter), false); 402 | converter.addEventListener("drop", on_file_drop.bind(converter, converter), false); 403 | 404 | body.addEventListener("dragover", on_file_dragover.bind(body, converter), false); 405 | body.addEventListener("dragleave", on_file_dragleave.bind(body, converter), false); 406 | body.addEventListener("drop", on_file_drop.bind(body, converter), false); 407 | 408 | // Exclusive 409 | if (exclusive_mode !== null) { 410 | exclusive_mode_update.call(exclusive_mode, non_exclusive_body, true); 411 | exclusive_mode.addEventListener("change", on_exclusive_mode_change.bind(exclusive_mode, non_exclusive_body), false); 412 | } 413 | } 414 | }); 415 | 416 | })(); 417 | 418 | 419 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | border: none; 5 | color: #111111; 6 | background-color: #ffffff; 7 | font-family: Arial; 8 | font-size: 16px; 9 | overflow-x: hidden; 10 | overflow-y: scroll; 11 | text-align: center; 12 | } 13 | 14 | table,tbody,tr,td { 15 | margin: 0; 16 | padding: 0; 17 | border-spacing: 0; 18 | } 19 | 20 | p { 21 | margin: 0; 22 | padding: 0; 23 | line-height: 1.5em; 24 | } 25 | ol,ul { 26 | margin: 0 0 0 1em; 27 | padding: 0; 28 | } 29 | ol>li,ul>li { 30 | line-height: 1.5em; 31 | } 32 | ol>li+li,ul>li+li { 33 | margin-top: 0.5em; 34 | } 35 | 36 | h1,h2,h3,h4,h5,h6 { 37 | margin: 0; 38 | padding: 0; 39 | font-weight: bold; 40 | line-height: 1.5em; 41 | position: relative; 42 | } 43 | h1 { 44 | font-size: 2em; 45 | } 46 | h2 { 47 | font-size: 1.8em; 48 | } 49 | h3 { 50 | font-size: 1.6em; 51 | } 52 | h4 { 53 | font-size: 1.4em; 54 | } 55 | h5 { 56 | font-size: 1.2em; 57 | } 58 | h6 { 59 | font-size: 1em; 60 | } 61 | 62 | input, 63 | textarea { 64 | font-size: inherit; 65 | } 66 | input:focus, 67 | textarea:focus { 68 | outline: none; 69 | } 70 | 71 | .hardlink_text { 72 | vertical-align: middle; 73 | } 74 | .hardlink_text:not(:hover)>a.hardlink { 75 | visibility: hidden; 76 | opacity: 0; 77 | 78 | -webkit-transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0s, visibility 0.25s linear 0s; 79 | -moz-transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0s, visibility 0.25s linear 0s; 80 | -o-transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0s, visibility 0.25s linear 0s; 81 | transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0s, visibility 0.25s linear 0s; 82 | } 83 | a.hardlink { 84 | display: block; 85 | visibility: visible; 86 | position: absolute; 87 | right: 100%; 88 | top: 0; 89 | padding: 0 0.25em 0 0.5em; 90 | color: #c8c8c8; 91 | font-weight: bold; 92 | 93 | -webkit-transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0.5s, visibility 0s linear 0.5s; 94 | -moz-transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0.5s, visibility 0s linear 0.5s; 95 | -o-transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0.5s, visibility 0s linear 0.5s; 96 | transition: color 0.25s ease-in-out 0s, opacity 0.25s ease-in-out 0.5s, visibility 0s linear 0.5s; 97 | } 98 | a.hardlink:hover { 99 | text-decoration: none; 100 | color: #30b194; 101 | } 102 | a.hardlink:after { 103 | content: "#"; 104 | } 105 | 106 | p+p, 107 | h1+p,h2+p,h3+p,h4+p,h5+p,h6+p, 108 | p+h1,p+h2,p+h3,p+h4,p+h5,p+h6, 109 | h4+h6 { 110 | margin-top: 1em; 111 | } 112 | 113 | strong { 114 | font-size: 1em; 115 | } 116 | 117 | code { 118 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 119 | } 120 | code.nowrap { 121 | display: inline-block; 122 | white-space: nowrap; 123 | } 124 | 125 | .codeblock { 126 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 127 | display: block; 128 | background: #f3f3f3; 129 | border: 1px solid #e5e5e5; 130 | overflow-x: auto; 131 | overflow-y: hidden; 132 | padding: 0; 133 | line-height: 1.5em; 134 | } 135 | .codeblock_inner { 136 | display: inline-block; 137 | padding: 0.5em; 138 | } 139 | .codeblock.codeblock_pre>.codeblock_inner { 140 | white-space: pre; 141 | } 142 | 143 | *::selection { 144 | color: #ffffff; 145 | background: #30b194; 146 | text-shadow: none; 147 | } 148 | *::-moz-selection { 149 | color: #ffffff ; 150 | background: #30b194; 151 | text-shadow: none; 152 | } 153 | 154 | .section_id { 155 | } 156 | 157 | .script_disabled { 158 | } 159 | .script_disabled:not(.script_visible) { 160 | display: none; 161 | } 162 | .script_enabled { 163 | } 164 | .script_enabled:not(.script_visible) { 165 | display: none; 166 | } 167 | 168 | input[type=checkbox], 169 | input[type=radio] { 170 | padding: 0; 171 | margin: 0; 172 | vertical-align: middle; 173 | } 174 | 175 | label.checkbox { 176 | display: inline-block; 177 | width: 0.75em; 178 | height: 0.75em; 179 | vertical-align: middle; 180 | border: 0.09375em solid #111111; 181 | border-radius: 0.25em; 182 | padding: 0.125em; 183 | position: relative; 184 | cursor: pointer; 185 | } 186 | label.checkbox:before { 187 | z-index: -1; 188 | position: absolute; 189 | display: block; 190 | left: 0; 191 | right: 0; 192 | bottom: 0; 193 | top: 0; 194 | content: ""; 195 | border: none; 196 | border-radius: 0.125em; 197 | background: #ffffff; 198 | } 199 | label.checkbox:hover:before, 200 | label:not([for]):hover label.checkbox:before { 201 | border: 0.125em solid #30b194; 202 | } 203 | label.checkbox>input[type=checkbox] { 204 | position: absolute; 205 | visibility: hidden; 206 | display: none; 207 | } 208 | label.checkbox>svg { 209 | display: none; 210 | width: 100%; 211 | height: 100%; 212 | position: relative; 213 | } 214 | label.checkbox>svg>polygon { 215 | fill: #111111; 216 | } 217 | label.checkbox.delete_checkbox>svg>polygon:not(:first-child) { 218 | visibility: hidden; 219 | } 220 | label.checkbox.delete_checkbox:hover>svg>polygon:first-child, 221 | label:not([for]):hover label.checkbox.delete_checkbox>svg>polygon:first-child { 222 | visibility: hidden; 223 | } 224 | label.checkbox.delete_checkbox:hover>svg>polygon:not(:first-child), 225 | label:not([for]):hover label.checkbox.delete_checkbox>svg>polygon:not(:first-child) { 226 | visibility: visible; 227 | } 228 | label.checkbox>input[type=checkbox]:checked+svg { 229 | display: block; 230 | } 231 | 232 | 233 | a { 234 | color: #30b194; 235 | cursor: pointer; 236 | text-decoration: none; 237 | } 238 | a:hover { 239 | text-decoration: underline; 240 | } 241 | a.light_underline { 242 | color: #c8c8c8; 243 | } 244 | a.light_underline>span { 245 | color: #30b194; 246 | } 247 | a.light_nohover_color_hover { 248 | color: #c8c8c8; 249 | } 250 | a.light_nohover_color_hover:hover { 251 | color: #30b194; 252 | text-decoration: none; 253 | } 254 | 255 | .main { 256 | text-align: left; 257 | display: inline-block; 258 | width: 60em; 259 | vertical-align: middle; 260 | } 261 | .main.main_no_overflow { 262 | overflow: hidden; 263 | } 264 | .main.body { 265 | margin: 0.5em 0 4em 0; 266 | } 267 | 268 | .header_bar { 269 | position: relative; 270 | 271 | background: #f3f3f3; 272 | background: -webkit-linear-gradient(180deg, #f9f9f9, #f3f3f3); 273 | background: -moz-linear-gradient(180deg, #f9f9f9, #f3f3f3); 274 | background: -o-linear-gradient(180deg, #f9f9f9, #f3f3f3); 275 | background: linear-gradient(180deg, #f9f9f9, #f3f3f3); 276 | 277 | border-bottom: 1px solid #e5e5e5; 278 | } 279 | .header_table { 280 | margin: 0.5em 0; 281 | text-align: left; 282 | } 283 | .header_table td { 284 | vertical-align: middle; 285 | } 286 | .header_table_cell { 287 | width: 0; 288 | } 289 | .header_table_cell.header_table_cell_full { 290 | width: 100%; 291 | } 292 | 293 | .header_table_name { 294 | font-size: 2em; 295 | line-height: 1.2em; 296 | white-space: nowrap; 297 | } 298 | .header_table_name_user { 299 | } 300 | .header_table_name_separator { 301 | display: inline-block; 302 | margin: 0 0.125em; 303 | color: #c8c8c8; 304 | } 305 | .header_table_name_title { 306 | } 307 | .header_table_name_title>span { 308 | font-weight: bold; 309 | } 310 | 311 | .header_table_separator { 312 | vertical-align: middle; 313 | display: inline-block; 314 | font-size: 2em; 315 | height: 1.2em; 316 | margin: 0 0.5em; 317 | border-left: 1px solid #c8c8c8; 318 | } 319 | 320 | .header_table_description { 321 | line-height: 1.1em; 322 | } 323 | .header_table_description_name { 324 | text-transform: lowercase; 325 | font-weight: bold; 326 | color: #404040; 327 | } 328 | .header_table_description_body { 329 | text-transform: lowercase; 330 | color: #606060; 331 | } 332 | 333 | .header_table_view_on_github { 334 | display: block; 335 | white-space: nowrap; 336 | margin-left: 1em; 337 | text-align: right; 338 | } 339 | .header_table_view_on_github_line1 { 340 | font-size: 0.8em; 341 | line-height: 1em; 342 | } 343 | .header_table_view_on_github_line2 { 344 | line-height: 1em; 345 | } 346 | 347 | .light { 348 | color: #808080; 349 | } 350 | .italic { 351 | font-style: italic; 352 | } 353 | 354 | 355 | .converter_exclusive_mode { 356 | white-space: nowrap; 357 | margin-left: 1em; 358 | padding: 0 2em 0 1em; 359 | display: inline-block; 360 | font-size: 0.5em; 361 | font-weight: normal; 362 | line-height: 1em; 363 | cursor: pointer; 364 | } 365 | .converter_exclusive_mode_text { 366 | vertical-align: middle; 367 | opacity: 0; 368 | -webkit-transition: opacity 0.25s ease-in-out 0s; 369 | -moz-transition: opacity 0.25s ease-in-out 0s; 370 | -o-transition: opacity 0.25s ease-in-out 0s; 371 | transition: opacity 0.25s ease-in-out 0s; 372 | } 373 | .converter_exclusive_mode_text:after { 374 | content: "exclusive mode"; 375 | } 376 | .converter_exclusive_mode:hover>.converter_exclusive_mode_text { 377 | opacity: 1; 378 | -webkit-transition: opacity 0.25s ease-in-out 0.5s; 379 | -moz-transition: opacity 0.25s ease-in-out 0.5s; 380 | -o-transition: opacity 0.25s ease-in-out 0.5s; 381 | transition: opacity 0.25s ease-in-out 0.5s; 382 | } 383 | .converter_exclusive_mode_check { 384 | margin-right: 0.25em; 385 | opacity: 0; 386 | -webkit-transition: opacity 0.25s ease-in-out 0s; 387 | -moz-transition: opacity 0.25s ease-in-out 0s; 388 | -o-transition: opacity 0.25s ease-in-out 0s; 389 | transition: opacity 0.25s ease-in-out 0s; 390 | } 391 | .converter_exclusive_mode:hover>.converter_exclusive_mode_check { 392 | opacity: 1; 393 | -webkit-transition: opacity 0.25s ease-in-out 0.5s; 394 | -moz-transition: opacity 0.25s ease-in-out 0.5s; 395 | -o-transition: opacity 0.25s ease-in-out 0.5s; 396 | transition: opacity 0.25s ease-in-out 0.5s; 397 | } 398 | 399 | .converter { 400 | display: block; 401 | padding: 2em; 402 | border: 0.25em dashed #c8c8c8; 403 | cursor: pointer; 404 | text-align: center; 405 | -webkit-transition: border-color 0.25s ease-in-out 0s; 406 | -moz-transition: border-color 0.25s ease-in-out 0s; 407 | -o-transition: border-color 0.25s ease-in-out 0s; 408 | transition: border-color 0.25s ease-in-out 0s; 409 | background: #f3f3f3; 410 | 411 | } 412 | .converter.converter_files_active { 413 | border-color: #30b194; 414 | } 415 | .converter_container { 416 | display: inline-block; 417 | padding-right: 3em; 418 | } 419 | .converter_table { 420 | text-align: left; 421 | display: table; 422 | vertical-align: middle; 423 | } 424 | .converter_cell { 425 | display: table-cell; 426 | vertical-align: middle; 427 | } 428 | .converter_cell_left { 429 | width: 0; 430 | } 431 | .converter_cell_right { 432 | width: 100%; 433 | } 434 | .converter_svg_container { 435 | width: 8.5em; 436 | height: 10em; 437 | } 438 | .converter_svg_graphic { 439 | width: 10em; 440 | height: 10em; 441 | -webkit-transition: transform 0.25s ease-in-out 0s; 442 | -moz-transition: transform 0.25s ease-in-out 0s; 443 | -o-transition: transform 0.25s ease-in-out 0s; 444 | transition: transform 0.25s ease-in-out 0s; 445 | } 446 | .converter_svg_graphic_poly { 447 | fill: #c8c8c8; 448 | stroke: none; 449 | -webkit-transition: fill 0.25s ease-in-out 0s; 450 | -moz-transition: fill 0.25s ease-in-out 0s; 451 | -o-transition: fill 0.25s ease-in-out 0s; 452 | transition: fill 0.25s ease-in-out 0s; 453 | } 454 | .converter_info { 455 | display: inline-block; 456 | text-align: right; 457 | } 458 | .converter_info_line1 { 459 | font-weight: bold; 460 | font-size: 4em; 461 | line-height: 1em; 462 | } 463 | .converter_info_line2 { 464 | font-size: 2em; 465 | line-height: 1em; 466 | } 467 | .converter_info_line3 { 468 | margin-top: 2em; 469 | line-height: 1em; 470 | color: #a0a0a0; 471 | } 472 | .converter_files_input { 473 | display: none; 474 | } 475 | 476 | .converter:hover .converter_svg_graphic, 477 | .converter.converter_files_hover .converter_svg_graphic { 478 | -webkit-transform-origin: 50% 50%; 479 | -moz-transform-origin: 50% 50%; 480 | -ms-transform-origin: 50% 50%; 481 | -o-transform-origin: 50% 50%; 482 | transform-origin: 50% 50%; 483 | -webkit-transform: scale(1.25); 484 | -moz-transform: scale(1.25); 485 | -ms-transform: scale(1.25); 486 | -o-transform: scale(1.25); 487 | transform: scale(1.25); 488 | } 489 | .converter.converter_files_active .converter_svg_graphic_poly { 490 | fill: #30b194; 491 | } 492 | 493 | .converted { 494 | margin-top: 2em; 495 | } 496 | .converted:not(.converted_visible) { 497 | display: none; 498 | } 499 | .converted_item { 500 | } 501 | .converted_item+.converted_item { 502 | margin-top: 1em; 503 | } 504 | .converted_item_title_container { 505 | border-bottom: 0.25em solid #c8c8c8; 506 | margin-bottom: 1em; 507 | } 508 | .converted_item_title { 509 | color: #30b194; 510 | font-size: 2em; 511 | font-weight: bold; 512 | line-height: 1.2em; 513 | max-height: 2.4em; 514 | overflow: hidden; 515 | } 516 | .converted_item_contents { 517 | margin-left: 2em; 518 | } 519 | 520 | 521 | .converted_item_link_container { 522 | width: 100%; 523 | overflow-x: hidden; 524 | margin-bottom: 1em; 525 | } 526 | a.converted_item_link { 527 | font-size: 1.25em; 528 | color: #30b194; 529 | white-space: nowrap; 530 | } 531 | a.converted_item_link:hover { 532 | text-decoration: underline; 533 | } 534 | a.converted_item_link>span { 535 | color: #111111; 536 | } 537 | .converted_item_textbox { 538 | display: block; 539 | width: 100%; 540 | margin: 0.5em 0 0 0; 541 | padding: 0.5em; 542 | border: 1px solid #c8c8c8; 543 | box-sizing: border-box; 544 | -moz-box-sizing: border-box; 545 | line-height: 1.2em; 546 | color: #111111; 547 | } 548 | .converted_item_textbox[readonly] { 549 | color: #808080; 550 | } 551 | 552 | .converted_item_header { 553 | margin: 0; 554 | } 555 | .converted_item_header_text { 556 | display: inline-block; 557 | font-weight: bold; 558 | } 559 | .converted_item_options_container:not(.converted_item_options_container_visible)>.converted_item_header>.converted_item_header_text { 560 | display: none; 561 | } 562 | 563 | a.converted_item_options_toggle { 564 | color: #30b194; 565 | display: inline-block; 566 | } 567 | .converted_item_options_container.converted_item_options_container_visible>.converted_item_header>a.converted_item_options_toggle { 568 | margin-left: 1em; 569 | } 570 | a.converted_item_options_toggle>span { 571 | color: #c8c8c8; 572 | } 573 | a.converted_item_options_toggle:hover>span { 574 | color: #111111; 575 | } 576 | a.converted_item_options_toggle>span:after { 577 | content: "Show options"; 578 | } 579 | .converted_item_options_container.converted_item_options_container_visible>.converted_item_header>a.converted_item_options_toggle>span:after { 580 | content: "Hide"; 581 | } 582 | 583 | .converted_item_options_container { 584 | } 585 | .converted_item_options { 586 | margin-top: 0.5em; 587 | } 588 | .converted_item_options_container:not(.converted_item_options_container_visible)>.converted_item_options { 589 | display: none; 590 | } 591 | .converted_item_option { 592 | } 593 | .converted_item_option+.converted_item_option { 594 | margin-top: 0.5em; 595 | } 596 | .converted_item_option_part { 597 | cursor: pointer; 598 | display: inline-block; 599 | } 600 | .converted_item_option_part+.converted_item_option_part { 601 | margin-left: 1em; 602 | } 603 | .converted_item_option_part:not(.converted_item_option_part_visible) { 604 | display: none; 605 | } 606 | .converted_item_option_checkbox { 607 | vertical-align: middle; 608 | margin-right: 0.25em; 609 | } 610 | .converted_item_option_text { 611 | vertical-align: middle; 612 | } 613 | 614 | .non_exclusive { 615 | margin-top: 2em; 616 | } 617 | .non_exclusive.exclusive_enabled { 618 | display: none; 619 | } 620 | --------------------------------------------------------------------------------