├── COPYING ├── README.md ├── doc ├── SoulseekProtocol – Museek+.html ├── SoulseekProtocol – Museek+_files │ ├── babel.js │ ├── folding.js │ ├── jquery.js │ ├── main.css │ ├── museek_banner_github.png │ ├── search.js │ ├── trac.css │ ├── trac.js │ ├── trac_logo_mini.png │ └── wiki.css └── soulseek_protocol.html ├── index.js ├── lib ├── blacklist.json ├── build-list.js ├── check-port.js ├── client │ ├── handlers.js │ └── index.js ├── distrib-peer │ ├── handlers.js │ └── index.js ├── make-token.js ├── message │ ├── decode-list.js │ ├── decode-room.js │ ├── emitter.js │ ├── encode-list.js │ ├── from-distrib.js │ ├── from-peer.js │ ├── from-server.js │ ├── index.js │ ├── to-distrib.js │ ├── to-peer.js │ └── to-server.js ├── nat-pmp-map.js ├── peer-server.js ├── peer │ ├── handlers.js │ └── index.js ├── search-share-list.js ├── soul-sock.js ├── transfer-peer │ ├── handlers.js │ └── index.js └── upload-speed.js ├── livelook.svg ├── package.json └── test.js /README.md: -------------------------------------------------------------------------------- 1 | # livelook 👀 2 | 3 | 4 | a [soulseek](https://en.wikipedia.org/wiki/Soulseek) client written in 5 | javascript. soulseek allows users from around the world to connect to each other 6 | directly and share music (and other stuff). 7 | 8 | >Soulseek is an ad-free, spyware free, just plain free file sharing network for 9 | >Windows, Mac and Linux. Our rooms, search engine and search correlation system 10 | >make it easy for you to find people with similar interests, and make new 11 | >discoveries! 12 | > 13 | >-- [About Soulseek | Soulseek](https://www.slsknet.org/news/node/680) 14 | 15 | features supported: 16 | * [nat pmp](https://en.wikipedia.org/wiki/NAT_Port_Mapping_Protocol) and 17 | [upnp](https://en.wikipedia.org/wiki/Universal_Plug_and_Play) port-forwarding 18 | * chat rooms and private messages (including direct connections) 19 | * browsing user's share lists and vice versa 20 | * searching the network's files and responding to distributed searches 21 | * downloading and uploading with automatically updating share list 22 | 23 |
24 | i mainly tested this against nicotine-plus, but it works with soulseekqt too. 25 |
26 | 27 | ## example 28 | ```javascript 29 | const LiveLook = require('./'); 30 | const fs = require('fs'); 31 | 32 | let livelook = new LiveLook({ 33 | username: 'toadtripler', 34 | password: 'not my password', 35 | sharedFolder: './mp3s', 36 | autojoin: [ 'nicotine' ] 37 | }); 38 | 39 | livelook.on('error', console.error); 40 | 41 | livelook.login((err, res) => { 42 | if (err || !res.success) { 43 | return console.log('login failed'); 44 | } 45 | 46 | livelook.on('sayChatroom', msg => { 47 | console.log(`[${msg.room}] <${msg.username}> ${msg.message}`); 48 | }); 49 | 50 | livelook.on('messageUser', msg => { 51 | console.log(`<${msg.username}> ${msg.message}`); 52 | }); 53 | 54 | livelook.search('hot dad web love', (err, res) => { 55 | if (err) { 56 | return console.error(err); 57 | } 58 | 59 | res = res.filter(item => item.slotsFree); 60 | 61 | if (!res) { 62 | console.log('no files found :('); 63 | return; 64 | } 65 | 66 | let downloaded = fs.createWriteStream('hot-dad-web-love.mp3'); 67 | res = res.sort((a, b) => a.speed > b.speed ? -1: 0)[0]; 68 | livelook.downloadFile(res.username, res.file).pipe(downloaded); 69 | downloaded.on('end', () => { 70 | livelook.messageUser(res.username, `hey thanks for ${res.file}!`); 71 | }); 72 | }); 73 | }); 74 | ``` 75 | ## install 76 | 77 | $ npm install --save livelook 78 | 79 | ## api 80 | ### livelook = new LiveLook(args) 81 | create a new `livelook` instance. 82 | 83 | ```javascript 84 | // args 85 | { 86 | username: '', 87 | password: '', 88 | server: 'server.slsknet.org', 89 | port: 2242, // port for server above, NOT the port we listen on 90 | waitPort: 2234, // port for peer server. will retry multiple options if fail 91 | sharedFolder: './mp3s', 92 | downloadFolder: './downloads', 93 | description: 'user biography', 94 | autojoin: [ 'chatrooms', 'joined', 'automatically' ], 95 | maxPeers: 100, 96 | uploadSlots: 2, // maximum uploads allowed at one time 97 | uploadThrottle: 56 * 1024, // speed to throttle uploads in bytes 98 | downloadThrottle: 56 * 1024 99 | } 100 | ``` 101 | 102 | ### livelook.init([done]) 103 | initialize our share list and connect to the soulseek server. you don't need to 104 | call this if you just use login below. 105 | 106 | ### livelook.login([username, password, done]) 107 | login to the soulseek server, and initialize our peer server if it isn't 108 | already. username and password are optional if the instance has them. 109 | 110 | ### livelook.refreshShareList([done]) 111 | re-scan `livelook.sharedFolder` and repopulate `livelook.shareList`. this is 112 | what other users see when they browse us, or when we respond to searches. 113 | 114 | ### livelook.sayChatroom(room, message) 115 | send a message to a chatroom. 116 | 117 | ### livelook.leaveChatroom(room) 118 | leave a chatroom and stop receiving messages from it. 119 | 120 | ### livelook.joinRoom(room) 121 | join a chatroom and start accepting messages from it. 122 | 123 | ### livelook.messageUser(username, message) 124 | send a private message to a specific user. 125 | 126 | ### livelook.setStatus(status) 127 | set our online/away status. 128 | 129 | `status` can be a `Number` (1 for away, 2 for online), `'away'` or `'online'`. 130 | 131 | ### livelook.refreshUploadSpeed([done]) 132 | re-calculate our upload speed from [speedtest.net](https://www.speedtest.net/). 133 | 134 | ### livelook.getPeerAddress(username, done) 135 | get a peer's ip address and port based on their username. 136 | 137 | ### livelook.getPeerByUsername(username, done) 138 | get a peer instance based on a username. this will first check our pre-existing 139 | peers, then it tries to make a direct connection to the peer until finally 140 | requesting the server connect the peer to us. 141 | 142 | ### livelook.getShareFileList(username, done) 143 | get all the files a user is sharing. may take a while as some people share 144 | large amounts of files, and the message must be decompressed. 145 | 146 | ### livelook.searchUserShares(username, query, done) 147 | search a user's shares for a query. nicotine users max out at 50 by default. 148 | 149 | ### livelook.getUserInfo(username, done) 150 | get a user's description (biography), picture (avatar) as a buffer, 151 | upload slots, queue size and slots free. 152 | 153 | ### livelook.getFolderContents(username, folder, done) 154 | get a list of all the files in the specified folder. 155 | 156 | ### livelook.downloadFile(username, file, [fileStart = 0]) 157 | download a file from a user. this returns a `ReadableStream`, and will also 158 | emit a `queue` event with its position if we can't download it immediately. pass 159 | in `fileStart` to indicate where to begin downloading the file in bytes (to 160 | resume interrupted downloads). 161 | 162 | ### livelook.searchFiles(query, [args = { timeout, max }, done]) 163 | search other peers for files. this returns a `ReadableStream` in `objectMode`. 164 | on search results it will emit `data` events containing the following: 165 | 166 | ```javascript 167 | { username, file, size, bitrate, vbr, duration, slotsFree, speed, queueSize } 168 | ``` 169 | 170 | if `done` is provided, the terms will be concatted and passed in either after 171 | the timeout finishes or the max results are reached. 172 | 173 | ## how it works 174 | soulseek is largely peer-to-peer, but still relies on a central server for 175 | chat rooms, messaging, piercing firewalls and finding peers. 176 | 177 | ### initializing 178 | first we set up a local server to accept peer connections (and port forward with 179 | nat pmp or upnp if possible). then we connect to slsknet.org (or any other 180 | soulseek server) as a separate client and login. we can now begin to chat and 181 | browse. 182 | 183 | ### finding peers 184 | we can connect to peers by fetching their ip and port based on their username 185 | from the soulseek server, but if they aren't port-forwarded this will fail. the 186 | next step is to send a connection request via the soulseek server to tell them 187 | to connect to us. if this fails, there is no way for them to connect to us. this 188 | is why it's a good idea to enable nat pmp or port forward manually. 189 | 190 | ### searching 191 | after logging in, we tell the server we're orphaned and have no parents. after 192 | an arbitrary amount of time (usually around 30 seconds), the server gives a list 193 | of potential parents. once we connect to one successfully, we can also become a 194 | parent. our parent will contionously send us search requests from other peers. 195 | we can respond to them directly if we have the results, but also send the 196 | request to all of our children so they can do the same. 197 | 198 | ## see also 199 | * [Soulseek.NET](https://github.com/jpdillingham/Soulseek.NET) by @jpdillingham 200 | * [museek-plus](https://github.com/eLvErDe/museek-plus) by @eLvErDe 201 | * [protocol docs](https://htmlpreview.github.io/?http://github.com/misterhat/livelook/blob/master/doc/SoulseekProtocol%20%E2%80%93%20Museek%2B.html) 202 | * [nicotine-plus](https://github.com/Nicotine-Plus/nicotine-plus) 203 | * [slsk-client](https://github.com/f-hj/slsk-client) by @f-hj. 204 | * [soleseek protocol docs](https://htmlpreview.github.io/?https://github.com/misterhat/livelook/blob/master/doc/soulseek_protocol.html) 205 | 206 | ## donate 207 | [donate to keep the central server alive!](https://www.slsknet.org/donate.php) 208 | 209 | ## license 210 | Copyright (C) 2019 Zorian Medwid 211 | 212 | This program is free software: you can redistribute it and/or modify 213 | it under the terms of the GNU Affero General Public License as 214 | published by the Free Software Foundation, either version 3 of the 215 | License, or (at your option) any later version. 216 | 217 | This program is distributed in the hope that it will be useful, 218 | but WITHOUT ANY WARRANTY; without even the implied warranty of 219 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 220 | GNU Affero General Public License for more details. 221 | 222 | You should have received a copy of the GNU Affero General Public License 223 | along with this program. If not, see http://www.gnu.org/licenses/. 224 | 225 | **You may not distribute this program without offering the source code. Hosting 226 | a web service that utilizes livelook is distrubtion.** 227 | -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Babel JavaScript Support 3 | * 4 | * Copyright (C) 2008 Edgewall Software 5 | * All rights reserved. 6 | * 7 | * This software is licensed as described in the file COPYING, which 8 | * you should have received as part of this distribution. The terms 9 | * are also available at http://babel.edgewall.org/wiki/License. 10 | * 11 | * This software consists of voluntary contributions made by many 12 | * individuals. For the exact contribution history, see the revision 13 | * history and logs, available at http://babel.edgewall.org/log/. 14 | */ 15 | 16 | /** 17 | * A simple module that provides a gettext like translation interface. 18 | * The catalog passed to load() must be a object conforming to this 19 | * interface:: 20 | * 21 | * { 22 | * messages: an object of {msgid: translations} items where 23 | * translations is an array of messages or a single 24 | * string if the message is not pluralizable. 25 | * plural_expr: the plural expression for the language. 26 | * locale: the identifier for this locale. 27 | * domain: the name of the domain. 28 | * } 29 | * 30 | * Missing elements in the object are ignored. 31 | * 32 | * Typical usage:: 33 | * 34 | * var translations = babel.Translations.load(...).install(); 35 | */ 36 | var babel = new function() { 37 | 38 | var defaultPluralExpr = function(n) { return n == 1 ? 0 : 1; }; 39 | var formatRegex = /%(?:(?:\(([^\)]+)\))?([disr])|%)/g; 40 | var translations = {}; 41 | var merged; 42 | 43 | /** 44 | * A translations object implementing the gettext interface 45 | */ 46 | var Translations = this.Translations = function(locale, domain) { 47 | this.messages = {}; 48 | this.locale = locale || 'unknown'; 49 | this.domain = domain || 'messages'; 50 | this.pluralexpr = defaultPluralExpr; 51 | }; 52 | 53 | /** 54 | * Create a new translations object from the catalog and return it. 55 | * See the babel-module comment for more details. 56 | */ 57 | Translations.load = function(catalog) { 58 | var rv = new Translations(); 59 | rv.load(catalog); 60 | translations[rv.domain] = rv; 61 | merged.load(catalog); 62 | return rv; 63 | }; 64 | 65 | /** 66 | * Get a Translations instance from the loaded translations. If the 67 | * specified domain doesn't exist, returns a dummy Translations 68 | * instance. 69 | */ 70 | Translations.get = function(domain) { 71 | return translations[domain] || (new Translations({domain: domain})); 72 | }; 73 | 74 | Translations.prototype = { 75 | /** 76 | * translate a single string 77 | * 78 | * If extra parameters are given, use them to fill the format 79 | * specified by the string. 80 | */ 81 | gettext: function(string) { 82 | var translated = this.messages[string]; 83 | if (typeof translated == 'undefined') 84 | translated = string; 85 | else if (typeof translated != 'string') 86 | translated = translated[0]; 87 | if (arguments.length > 1) { 88 | arguments[0] = translated; 89 | return babel.format.apply(this, arguments); 90 | } 91 | return translated; 92 | }, 93 | 94 | /** 95 | * translate a pluralizable string 96 | * 97 | * If extra parameters are given, use them to fill the format 98 | * specified by the string. 99 | */ 100 | ngettext: function(singular, plural, n) { 101 | var translated = this.messages[singular]; 102 | if (typeof translated == 'undefined') 103 | translated = (n == 1) ? singular : plural; 104 | else 105 | translated = translated[this.pluralexpr(n)]; 106 | if (arguments.length > 3) { 107 | var format_args = Array.prototype.slice.call(arguments, 3); 108 | format_args.unshift(translated); 109 | return babel.format.apply(this, format_args) 110 | } 111 | return translated; 112 | }, 113 | 114 | /** 115 | * Install this translation document wide. After this call, there are 116 | * three new methods on the window object: _, gettext and ngettext 117 | */ 118 | install: function() { 119 | window._ = window.gettext = function() { 120 | return merged.gettext.apply(merged, arguments); 121 | }; 122 | window.ngettext = function(singular, plural, n) { 123 | return merged.ngettext.apply(merged, arguments); 124 | }; 125 | return this; 126 | }, 127 | 128 | /** 129 | * Works like Translations.load but updates the instance rather 130 | * then creating a new one. 131 | */ 132 | load: function(catalog) { 133 | if (catalog.messages) 134 | this.update(catalog.messages) 135 | if (catalog.plural_expr) 136 | this.setPluralExpr(catalog.plural_expr); 137 | if (catalog.locale) 138 | this.locale = catalog.locale; 139 | if (catalog.domain) 140 | this.domain = catalog.domain; 141 | return this; 142 | }, 143 | 144 | /** 145 | * Updates the translations with the object of messages. 146 | */ 147 | update: function(mapping) { 148 | for (var key in mapping) 149 | if (mapping.hasOwnProperty(key)) 150 | this.messages[key] = mapping[key]; 151 | return this; 152 | }, 153 | 154 | /** 155 | * Sets the plural expression 156 | */ 157 | setPluralExpr: function(expr) { 158 | this.pluralexpr = new Function('n', 'return +(' + expr + ')'); 159 | return this; 160 | } 161 | }; 162 | 163 | merged = new Translations({}); 164 | 165 | /** 166 | * Translate a single string in the specified domain. 167 | * 168 | * If extra parameters are given, use them to fill the format 169 | * specified by the string. 170 | */ 171 | window.dgettext = this.dgettext = function(domain, string) { 172 | var rv = translations[domain]; 173 | var args = Array.prototype.slice.call(arguments, 1); 174 | if (typeof rv != 'undefined') 175 | return rv.gettext.apply(rv, args); 176 | if (arguments.length > 1) 177 | return babel.format.apply(this, args); 178 | return string; 179 | }; 180 | 181 | /** 182 | * Translate a pluralizable string in the specified domain. 183 | * 184 | * If extra parameters are given, use them to fill the format 185 | * specified by the string. 186 | */ 187 | window.dngettext = this.dngettext = function(domain, singular, plural, n) { 188 | var rv = translations[domain]; 189 | if (typeof rv != 'undefined') { 190 | var args = Array.prototype.slice.call(arguments, 1); 191 | return rv.ngettext.apply(rv, args); 192 | } 193 | if (arguments.length > 4) { 194 | var args = Array.prototype.slice.call(arguments, 4); 195 | args.unshift(singular); 196 | return babel.format.apply(this, format_args) 197 | } 198 | return (n == 1) ? singular : plural; 199 | }; 200 | 201 | /** 202 | * A python inspired string formatting function. Supports named and 203 | * positional placeholders and "s", "d" and "i" as type characters 204 | * without any formatting specifications. 205 | * 206 | * Examples:: 207 | * 208 | * babel.format(_('Hello %s'), name) 209 | * babel.format(_('Progress: %(percent)s%%'), {percent: 100}) 210 | */ 211 | this.format = function() { 212 | var arg, string = arguments[0], idx = 0; 213 | if (arguments.length == 1) 214 | return string; 215 | else if (arguments.length == 2 && typeof arguments[1] == 'object') 216 | arg = arguments[1]; 217 | else { 218 | arg = []; 219 | for (var i = 1, n = arguments.length; i != n; ++i) 220 | arg[i - 1] = arguments[i]; 221 | } 222 | return string.replace(formatRegex, function(all, name, type) { 223 | if (all == '%%') return '%'; 224 | var value = arg[name || idx++]; 225 | return (type == 'i' || type == 'd') ? +value : value; 226 | }); 227 | } 228 | 229 | }; 230 | -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/folding.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | $.fn.enableFolding = function(autofold, snap) { 4 | var fragId = document.location.hash; 5 | if (fragId && /^#no\d+$/.test(fragId)) 6 | fragId = parseInt(fragId.substr(3)); 7 | if (snap == undefined) 8 | snap = false; 9 | 10 | var count = 1; 11 | return this.each(function() { 12 | // Use first child as a trigger, or generate a trigger from the text 13 | var trigger = $(this).children("a").eq(0); 14 | if (trigger.length == 0) { 15 | trigger = $(""); 17 | trigger.html($(this).html()); 18 | $(this).text(""); 19 | $(this).append(trigger); 20 | } 21 | 22 | trigger.click(function() { 23 | var div = $(this.parentNode.parentNode).toggleClass("collapsed"); 24 | return snap && !div.hasClass("collapsed"); 25 | }); 26 | if (autofold && (count != fragId)) 27 | trigger.parents().eq(1).addClass("collapsed"); 28 | count++; 29 | }); 30 | } 31 | 32 | /** Enable columns of a table to be hidden by clicking on the column header. 33 | * 34 | * +------------------+------+---- ... ---+---------------------+ 35 | * |column_headers[0] | ... | | column_headers[k-1] | <- c_h_row 36 | * +==================+======+==== ... ===+=====================+ 37 | * | row_headers[0] | row_headers[1] | row_headers[1*k-1] | <- rows[0] 38 | * | row_headers[k] | row_headers[k+1] | row_headers[2*k-1] | <- rows[1] 39 | * ... 40 | */ 41 | $.fn.enableCollapsibleColumns = function(recovery_area) { 42 | // column headers 43 | var c_h_row = $('thead tr', this); 44 | var column_headers = $('th', c_h_row).not(recovery_area); 45 | var k = column_headers.length; 46 | // row headers 47 | var tbody = $('tbody', this); 48 | var row_headers = $('th', tbody); 49 | var rows = $('tr', tbody); 50 | var n = row_headers.length / k; 51 | 52 | // add a 'hide' callback to each column header 53 | column_headers.each(function(j) { 54 | function hide() { 55 | // remove and save column j 56 | var th = $(this); 57 | th.css('display', 'none'); 58 | for ( var i = 0; i < n; i++ ) 59 | row_headers.eq(i*k+j).css('display', 'none'); 60 | // create a recovery button and its "show" callback 61 | recovery_area.prepend($("").addClass("recover") 62 | .text(_("Show %(title)s", {title: th.text()})) 63 | .click(function() { 64 | $(this).remove(); 65 | th.show(); 66 | for (var i = 0; i < n; i++) 67 | row_headers.eq(i*k+j).css('display', 'table-cell'); 68 | }) 69 | ); 70 | }; 71 | $(this).click(hide) 72 | .css('cursor', 'pointer') 73 | .attr('title', _("%(title)s (click to hide column)", 74 | {title: $(this).attr('title')})); 75 | }); 76 | } 77 | 78 | })(jQuery); 79 | -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/main.css: -------------------------------------------------------------------------------- 1 | /* Link styles */ 2 | a:hover, a:link, a:active, a:visited { 3 | color: #5785ed; 4 | } 5 | /* Link styles */ 6 | 7 | 8 | /* Styles for the roadmap view */ 9 | .milestone .info h2 em { 10 | color: #57c1ed; 11 | } 12 | /* Styles for the roadmap view */ 13 | 14 | 15 | /* Timeline */ 16 | dt em { 17 | color: #5785ed; 18 | } 19 | /* Timeline */ 20 | 21 | 22 | /* Browser */ 23 | h1 :link, h1 :visited { 24 | color: #57c1ed; 25 | } 26 | /* Browser */ 27 | 28 | 29 | /* Forms */ 30 | input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover { 31 | background: #57c1ed; 32 | } 33 | /* Forms */ 34 | 35 | 36 | /* Left menu */ 37 | #menu_left { 38 | width: 135px; 39 | position: absolute; 40 | float: left; 41 | top: 126px; 42 | margin: 0px; 43 | padding: 50px 10px 50px 10px; 44 | color: #57c1ed; 45 | border: black 1px solid; 46 | border-top: none; 47 | background-color: #f7f7f7; 48 | z-index: 900; 49 | } 50 | #menu_left li { 51 | font-weight: bold; 52 | } 53 | #menu_left a:link, a:visited { 54 | color: #5785ed; 55 | } 56 | #menu_left ul li ul li { 57 | font-weight: normal; 58 | } 59 | #menu_left ul { 60 | list-style: none; 61 | margin: 0; 62 | padding: 0; 63 | } 64 | #menu_left ul li { 65 | margin: 0; 66 | padding: 4px; 67 | } 68 | /* Left menu */ 69 | 70 | 71 | /* Banner */ 72 | #header { 73 | padding: 0px; 74 | margin: 10px 0px 0px 10px; 75 | } 76 | /* Banner */ 77 | 78 | 79 | /* Search box */ 80 | #search { 81 | position: absolute; 82 | top: 40px; 83 | right: 15px; 84 | margin: 0px; 85 | padding: 0px; 86 | } 87 | /* Search box */ 88 | 89 | 90 | /* Meta nav */ 91 | #metanav { 92 | position: absolute; 93 | top: 70px; 94 | right: 10px; 95 | margin: 0px; 96 | padding: 0px; 97 | } 98 | /* Meta nav */ 99 | 100 | 101 | /* Main nav */ 102 | #mainnav { 103 | background: #f7f7f7; 104 | position: absolute; 105 | top: 100px; 106 | right: 2%px; 107 | width: 98%; 108 | margin: 0px; 109 | height: 25px; 110 | padding: 0px; 111 | } 112 | #mainnav ul { 113 | display: table; 114 | float: right; 115 | margin: 0px; 116 | padding: 0px; 117 | } 118 | #mainnav li { 119 | display:table-cell; 120 | margin: 0px; 121 | padding: 0px; 122 | } 123 | #mainnav ul li a { 124 | position: relative; 125 | float: right; 126 | height: 21px; 127 | padding: 0px; 128 | margin: 0px; 129 | } 130 | #mainnav .active :link, #mainnav .active :visited { 131 | background: #57c1ed; 132 | } 133 | /* Main nav */ 134 | 135 | 136 | /* Ctx nav */ 137 | #ctxtnav { 138 | margin-top: 7.0em; 139 | } 140 | /* Ctx nav */ 141 | 142 | 143 | /* Main page */ 144 | *>#content { 145 | padding: 0; 146 | margin: 15px 15px 0px 180px; 147 | text-align: left; 148 | min-height: 770px; 149 | } 150 | * html #content { 151 | padding: 0; 152 | margin: 15px 15px 0px 180px; 153 | text-align: left; 154 | height: 770px; 155 | } 156 | /* Main page */ 157 | 158 | 159 | /* Need by Doxygen plugin */ 160 | .tabs{ 161 | margin: 85px 0px 0px 0px; 162 | } 163 | /* Need by Doxygen plugin */ 164 | 165 | 166 | /* NewsFlash Plugin */ 167 | div.newsflash { 168 | color: black; 169 | background: #f7f7f7; 170 | border: solid 2px #43eeff; 171 | } 172 | /* NewsFlash Plugin */ 173 | -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/museek_banner_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterhat/livelook/630241218e6dbd6db19779bbf6e95c1ffe6a8b38/doc/SoulseekProtocol – Museek+_files/museek_banner_github.png -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/search.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | /* Adapted from http://www.kryogenix.org/code/browser/searchhi/ */ 4 | $.fn.highlightText = function(text, className, caseSensitive) { 5 | function highlight(node) { 6 | if (node.nodeType == 3) { // Node.TEXT_NODE 7 | var val = node.nodeValue; 8 | var pos = (caseSensitive ? val : val.toLowerCase()).indexOf(text); 9 | if (pos >= 0 && !$(node.parentNode).hasClass(className)) { 10 | var span = document.createElement("span"); 11 | span.className = className; 12 | var txt = document.createTextNode(val.substr(pos, text.length)); 13 | span.appendChild(txt); 14 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 15 | document.createTextNode(val.substr(pos + text.length)), 16 | node.nextSibling)); 17 | node.nodeValue = val.substr(0, pos); 18 | } 19 | } else if (!$(node).is("button, select, textarea")) { 20 | $.each(node.childNodes, function() { highlight(this) }); 21 | } 22 | } 23 | return this.each(function() { highlight(this) }); 24 | } 25 | 26 | $(document).ready(function() { 27 | $("p.filters").on("click", ":checkbox, :checkbox + label", 28 | function(event) { 29 | if (!event.metaKey && !event.altKey) 30 | return; 31 | var clicked = this.tagName === "LABEL" ? 32 | document.getElementById(this.htmlFor) : this; 33 | var $clicked = $(clicked); 34 | $clicked.prop("checked", true); 35 | $clicked.siblings(":checkbox").prop("checked", false); 36 | if (this.tagName === "LABEL") { 37 | return false; 38 | } 39 | }); 40 | 41 | var elems = $(".searchable"); 42 | if (!elems.length) return; 43 | 44 | function getSearchTerms(url) { 45 | if (url.indexOf("?") == -1) return []; 46 | var params = url.substr(url.indexOf("?") + 1).split("&"); 47 | for (var p in params) { 48 | var param = params[p].split("="); 49 | if (param.length < 2) continue; 50 | if (param[0] == "q" || param[0] == "p") {// q= for Google, p= for Yahoo 51 | var query = decodeURIComponent(param[1].replace(/\+/g, " ")); 52 | if (query[0] == "!") query = query.slice(1); 53 | var terms = []; 54 | $.each(query.split(/(".*?"|'.*?'|\s+)/), function() { 55 | if (terms.length < 10) { 56 | term = this.replace(/^\s+$/, "") 57 | .replace(/^['"]/, "") 58 | .replace(/['"]$/, ""); 59 | if (term.length >= 3) 60 | terms.push(term); 61 | } 62 | }); 63 | return terms; 64 | } 65 | } 66 | return []; 67 | } 68 | 69 | var terms = getSearchTerms(document.URL); 70 | if (!terms.length) terms = getSearchTerms(document.referrer); 71 | if (terms.length) { 72 | $.each(terms, function(idx) { 73 | elems.highlightText(this.toLowerCase(), "searchword" + (idx % 5)); 74 | }); 75 | } else { 76 | function scrollToHashSearchMatch() { 77 | var h = window.location.hash; 78 | var direction = h[1]; 79 | var case_insensitive = h.match(/\/i$/); 80 | if (direction == '/' || direction == '?') { 81 | var hterm = h.substr(2); 82 | if (case_insensitive) 83 | hterm = hterm.substr(0, hterm.length - 2).toLowerCase(); 84 | $('.searchword0').each(function() { 85 | $(this).after($(this).html()).remove(); 86 | }); 87 | elems.highlightText(hterm, "searchword0", !case_insensitive); 88 | var hmatches = $('.searchword0'); 89 | if (direction == '?') 90 | hmatches = hmatches.last(); 91 | hmatches.first().each(function() { 92 | var offset = $(this).offset().top; 93 | window.scrollTo(0, offset); 94 | }); 95 | } 96 | } 97 | window.onhashchange = scrollToHashSearchMatch; 98 | scrollToHashSearchMatch(); 99 | } 100 | }); 101 | 102 | })(jQuery); 103 | -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/trac.css: -------------------------------------------------------------------------------- 1 | /* -*- coding: utf-8 -*- */ 2 | html { overflow-y: scroll } 3 | body { 4 | background: #fff; 5 | color: #000; 6 | margin: 10px; 7 | padding: 0; 8 | } 9 | body, th, tr { 10 | font: normal 13px Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; 11 | } 12 | h1, h2, h3, h4 { 13 | font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; 14 | font-weight: bold; 15 | letter-spacing: -0.018em; 16 | page-break-after: avoid; 17 | } 18 | h1 { font-size: 19px; margin: .15em 1em 0.5em 0 } 19 | h2 { font-size: 16px } 20 | h3 { font-size: 14px } 21 | hr { border: none; border-top: 1px solid #ccb; margin: 2em 0 } 22 | address { font-style: normal } 23 | img { border: none } 24 | 25 | .underline { text-decoration: underline } 26 | ol.loweralpha { list-style-type: lower-alpha } 27 | ol.upperalpha { list-style-type: upper-alpha } 28 | ol.lowerroman { list-style-type: lower-roman } 29 | ol.upperroman { list-style-type: upper-roman } 30 | ol.arabic { list-style-type: decimal } 31 | 32 | /* Link styles */ 33 | :link, :visited { 34 | text-decoration: none; 35 | color: #b00; 36 | border-bottom: 1px dotted #bbb; 37 | } 38 | :link:hover, :visited:hover { background-color: #eee; color: #555 } 39 | h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited, 40 | h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited { 41 | color: inherit; 42 | } 43 | 44 | /* Heading anchors */ 45 | .anchor:link, .anchor:visited { 46 | border: none; 47 | color: #d7d7d7; 48 | font-size: .8em; 49 | vertical-align: text-top; 50 | } 51 | * > .anchor:link, * > .anchor:visited { 52 | visibility: hidden; 53 | } 54 | h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 55 | h4:hover .anchor, h5:hover .anchor, h6:hover .anchor, 56 | span:hover .anchor { 57 | visibility: visible; 58 | } 59 | 60 | h1:target, h2:target, h3:target, h4:target, h5:target, h6:target, 61 | span:target { 62 | background: #ffb; 63 | box-shadow: .1em .1em .4em .1em #DDA; 64 | border-radius: .2em; 65 | } 66 | 67 | @media screen { 68 | a.ext-link .icon { 69 | background: url(../extlink.gif) left center no-repeat; 70 | padding-left: 15px; 71 | } 72 | a.mail-link .icon { 73 | background: url(../envelope.png) left center no-repeat; 74 | padding-left: 16px; 75 | } 76 | a.trac-rawlink, a.trac-ziplink { 77 | background: url('../download.png') right center no-repeat; 78 | padding-right: 16px; 79 | border-bottom: none; 80 | } 81 | } 82 | 83 | /* Forms */ 84 | input, textarea, select, .trac-button { margin: 2px } 85 | /* Avoid respect to system font settings for controls on Firefox, #11607 */ 86 | input, select, button { 87 | font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; 88 | font-size: 100%; 89 | } 90 | /* Avoid to inherit white-space of its parent element for IE 11, #11376 */ 91 | textarea { white-space: pre-wrap } 92 | input, select, .trac-button { vertical-align: middle } 93 | input[type=button], input[type=submit], input[type=reset], button { 94 | *overflow: visible; /* Workaround too much margin on button in IE7 */ 95 | } 96 | input[type=button], input[type=submit], input[type=reset], .trac-button { 97 | background: #eee; 98 | color: #222; 99 | border: 1px outset #eee; 100 | border-radius: .3em; 101 | box-shadow: .1em .1em .4em 0 #888; 102 | padding: .1em .5em .2em; 103 | text-shadow: .04em .04em #ddd; 104 | cursor: pointer; 105 | } 106 | input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover, 107 | .trac-button:hover { 108 | background: #f6f6f6; 109 | box-shadow: .1em .1em .6em 0 #999; 110 | text-shadow: .04em .04em #fcfcfc; 111 | } 112 | input[type=button]:active, input[type=submit]:active, input[type=reset]:active, 113 | .trac-button:active, .inlinebuttons a:active { 114 | position: relative; 115 | top: .1em; 116 | left: .1em; 117 | } 118 | input[type=button][disabled], input[type=submit][disabled], 119 | input[type=reset][disabled], .trac-button[disabled] { 120 | background: #f6f6f6; 121 | border-style: solid; 122 | color: #999; 123 | } 124 | input[type=text], input.textwidget, textarea { 125 | border: 1px solid #d7d7d7; 126 | border-radius: .3em; 127 | } 128 | input[type=text], input.textwidget { padding: .25em .5em } 129 | input[type=text]:focus, input.textwidget:focus, textarea:focus { 130 | border: 1px solid #886; 131 | } 132 | option { border-bottom: 1px dotted #d7d7d7 } 133 | fieldset { 134 | border: 1px solid #d7d7d7; 135 | border-radius: .4em; 136 | margin: 1em 0; 137 | background: #F7F7F0; 138 | box-shadow: .1em .1em 1em 0 #E7E7E7 inset; 139 | padding: 1em; 140 | } 141 | p.hint, span.hint, span.trac-datetimehint { 142 | color: #666; 143 | font-size: 85%; 144 | font-style: italic; 145 | margin: .5em 0; 146 | padding-left: 1em; 147 | } 148 | span.hint { vertical-align: middle; } 149 | p.help { color: #666; font-size: 90%; margin: 1em .5em .5em; } 150 | legend { 151 | color: #999; 152 | margin-left: .6em; 153 | padding: 0 .25em; 154 | font-size: 90%; 155 | font-weight: bold; 156 | } 157 | label.disabled, .disabled span.hint, .disabled p.help, .disabled legend, 158 | .disabled input, .disabled select, .readonly span.hint, .readonly p.help { 159 | color: #d7d7d7 160 | } 161 | .buttons { margin: .5em .5em .5em 0 } 162 | .buttons form, .buttons form div { display: inline } 163 | .buttons input { margin: 1em .5em .1em 0 } 164 | 165 | .inlinebuttons input, 166 | .inlinebuttons .trac-button, 167 | .inlinebuttons a { 168 | border: 1px solid #ddd; 169 | border-radius: 1em; 170 | height: 1.6em; 171 | padding: 0 .4em .1em; 172 | font-size: 70%; 173 | box-shadow: none; 174 | margin: 0 .1em .1em; 175 | background: none; 176 | color: #808080; 177 | cursor: pointer; 178 | } 179 | .uisymbols .inlinebuttons input[type=button], 180 | .uisymbols .inlinebuttons input[type=submit], 181 | .uisymbols .inlinebuttons .trac-button, 182 | .uisymbols .inlinebuttons a { 183 | font-size: 100%; 184 | } 185 | .inlinebuttons input[type=button]:hover, 186 | .inlinebuttons input[type=submit]:hover, 187 | .inlinebuttons .trac-button:hover, 188 | .inlinebuttons a:hover { 189 | background: #f6f6f6; 190 | color: #333; 191 | text-shadow: .04em .04em #fcfcfc; 192 | box-shadow: .1em .1em .6em 0 #999; 193 | } 194 | input[type=button].trac-delete, 195 | input[type=submit].trac-delete, 196 | .trac-button.trac-delete { 197 | color: #d31313; 198 | } 199 | input[type=button].trac-delete:hover, 200 | input[type=submit].trac-delete:hover, 201 | .trac-button.trac-delete:hover { 202 | color: #e31313; 203 | } 204 | textarea.trac-fullwidth, input.trac-fullwidth, button.trac-fullwidth { 205 | -moz-box-sizing: border-box; 206 | box-sizing: border-box; 207 | margin-left: 0; 208 | margin-right: 0; 209 | width: 100%; 210 | } 211 | textarea.trac-fullwidth { padding: 2px; } 212 | /* Resource edit forms (detailed view) */ 213 | form.mod { margin: 1em 0 } 214 | form.mod .field { margin: .5em 0; } 215 | form.mod .field.description { margin-top: 1em; } 216 | form.mod .field label { padding-left: .2em } 217 | form.mod #changeinfo .field { margin: 0.25em 0 0.75em 0 } 218 | form.mod #changeinfo .options { padding: 0 0 0.5em 1em } 219 | 220 | /* Header */ 221 | #header hr { display: none } 222 | #header h1 { margin: 1.5em 0 -1.5em; padding: 0 } 223 | #header img { border: none; margin: 0 0 -3em } 224 | #header :link, #header :visited, #header :link:hover, #header :visited:hover { 225 | background: transparent; 226 | color: #555; 227 | margin-bottom: 2px; 228 | border: none; 229 | padding: 0; 230 | } 231 | #header h1 :link:hover, #header h1 :visited:hover { color: #000 } 232 | 233 | /* Quick search */ 234 | #search { 235 | clear: both; 236 | font-size: 10px; 237 | height: 2.2em; 238 | margin: 0 0 1em; 239 | text-align: right; 240 | } 241 | #search input { font-size: 10px } 242 | #search label { display: none } 243 | 244 | /* Navigation */ 245 | .nav h2, .nav hr { display: none } 246 | .nav ul { 247 | font-size: 10px; 248 | list-style: none; 249 | margin: 0; 250 | padding: 0; 251 | text-align: right; 252 | } 253 | .nav li { 254 | border-right: 1px solid #d7d7d7; 255 | display: inline-block; 256 | padding: 0 .75em; 257 | white-space: nowrap; 258 | /* IE7 hack to make inline-block effective on block element */ 259 | zoom: 1; 260 | *display: inline; 261 | } 262 | .nav li.last { border-right: none } 263 | 264 | /* Meta navigation bar */ 265 | #metanav { 266 | padding-top: .3em; 267 | } 268 | #metanav form.trac-logout { 269 | display: inline; 270 | margin: 0; 271 | padding: 0; 272 | } 273 | #metanav form.trac-logout button { 274 | margin: 0; 275 | padding: 0; 276 | border: 0; 277 | outline: 0; 278 | background: transparent; 279 | font-family: inherit; 280 | font-size: 100%; 281 | color: #b00; 282 | border-bottom: 1px dotted #bbb; 283 | cursor: pointer; 284 | } 285 | #metanav form.trac-logout button::-moz-focus-inner { border: 0; padding: 0 } 286 | #metanav form.trac-logout button:hover { background-color: #eee; color: #555 } 287 | #metanav form.trac-logout button:active { position: static } 288 | 289 | /* Main navigation bar */ 290 | #mainnav { 291 | font: normal 10px verdana,'Bitstream Vera Sans',helvetica,arial,sans-serif; 292 | box-shadow: 0 .5em 1.5em #eee; 293 | border: 1px solid #e4e4e4; 294 | border-radius: .5em; 295 | margin: .66em 0 .33em; 296 | } 297 | #mainnav li { 298 | background: white url(../topbar_gradient.png) 0 0; 299 | border: 1px solid #e4e4e4; 300 | margin: -1px .3em 0 -.4em; 301 | padding: .3em 0; 302 | } 303 | 304 | #mainnav .first, #mainnav .first :link { 305 | border-top-left-radius: .5em; 306 | border-bottom-left-radius: .5em; 307 | } 308 | #mainnav .last, #mainnav .last :link { 309 | border-top-right-radius: .5em; 310 | border-bottom-right-radius: .5em; 311 | margin-right: 0; 312 | } 313 | 314 | #mainnav :link, #mainnav :visited { 315 | border-bottom: none; 316 | box-shadow: 0 .1em .3em 0 #999; 317 | color: #000; 318 | padding: .3em 20px; 319 | } 320 | 321 | #mainnav li:hover { 322 | border: 1px solid #666; 323 | } 324 | #mainnav :link:hover, #mainnav :visited:hover { 325 | background: #000 url(../topbar_gradient2.png) 0 0 repeat-x; 326 | color: #eee; 327 | box-shadow: 0 .1em .6em 0 #666; 328 | } 329 | 330 | #mainnav .active { 331 | border: 1px solid #666; 332 | } 333 | #mainnav .active :link, #mainnav .active :visited { 334 | background: #000 url(../topbar_gradient2.png) 0 0 repeat-x; 335 | color: #eee; 336 | text-shadow: 1px 0 #eee; 337 | } 338 | 339 | /* Context-dependent navigation links */ 340 | #ctxtnav { 341 | min-height: 1em; 342 | padding: .4em 0; 343 | } 344 | #ctxtnav li ul { 345 | background: #f7f7f7; 346 | color: #ccc; 347 | border: 1px solid; 348 | padding: 0; 349 | display: inline; 350 | margin: 0; 351 | } 352 | #ctxtnav li li { padding: 0; } 353 | #ctxtnav li li :link, #ctxtnav li li :visited { padding: 0 1em } 354 | #ctxtnav li li :link:hover, #ctxtnav li li :visited:hover { 355 | background: #bba; 356 | color: #fff; 357 | } 358 | 359 | .trac-nav, .trac-topnav { 360 | float: right; 361 | font-size: 80%; 362 | } 363 | .trac-topnav { 364 | margin-top: 14px; 365 | } 366 | 367 | /* Alternate links */ 368 | #altlinks { 369 | clear: both; 370 | margin-top: .4em; 371 | text-align: center; 372 | } 373 | #altlinks h3 { font-size: 12px; letter-spacing: normal; margin: 0 } 374 | #altlinks ul { list-style: none; margin: 0; padding: 0 } 375 | #altlinks li { 376 | border-right: 1px solid #d7d7d7; 377 | display: inline; 378 | font-size: 11px; 379 | line-height: 1.5; 380 | padding: 0 1em; 381 | white-space: nowrap; 382 | } 383 | #altlinks li.last { border-right: none } 384 | #altlinks li :link, #altlinks li :visited { 385 | background-repeat: no-repeat; 386 | color: #666; 387 | border: none; 388 | padding: 0 0 2px; 389 | } 390 | #altlinks li a.ics { 391 | background: url(../ics.png) left center no-repeat; 392 | padding-left: 22px; 393 | } 394 | #altlinks li a.rss { 395 | background: url(../feed.png) left center no-repeat; 396 | padding-left: 20px; 397 | } 398 | 399 | /* Footer */ 400 | #footer { 401 | clear: both; 402 | color: #bbb; 403 | font-size: 10px; 404 | height: 31px; 405 | padding: .25em 0; 406 | } 407 | #footer :link, #footer :visited { color: #bbb; } 408 | #footer hr { display: none } 409 | #footer #tracpowered { border: 0; float: left } 410 | #footer #tracpowered:hover { background: transparent } 411 | #footer p { margin: 0 } 412 | #footer p.left { 413 | float: left; 414 | margin-left: 1em; 415 | padding: 0 1em; 416 | border-left: 1px solid #d7d7d7; 417 | border-right: 1px solid #d7d7d7; 418 | } 419 | #footer p.right { 420 | float: right; 421 | text-align: right; 422 | } 423 | 424 | /* Container for all the elements in the page, usually single child of the body element */ 425 | #content { 426 | position: relative; /* reference for absolute positioning of children */ 427 | } 428 | 429 | /* Information content */ 430 | div.trac-content { 431 | padding: .5em 1em; 432 | margin: .3em auto; 433 | border: 1px solid #e4e4e4; 434 | border-radius: .5em; 435 | box-shadow: .2em .3em 1.5em #eee; 436 | } 437 | 438 | /* Author info */ 439 | .trac-author-anonymous, .trac-author-none { 440 | font-style: italic; 441 | } 442 | .trac-author-user { 443 | font-weight: bold; 444 | color: #666; 445 | } 446 | 447 | /* Help links */ 448 | .uinohelp #help { display: none } 449 | #help { 450 | clear: both; 451 | color: #999; 452 | font-size: 90%; 453 | margin: 1em; 454 | text-align: right; 455 | } 456 | #help :link, #help :visited { cursor: help } 457 | #help hr { display: none } 458 | 459 | /* Datepicker hints */ 460 | .trac-nodatetimehint .trac-datetimehint { display: none } 461 | 462 | /* Section folding */ 463 | .foldable :link, .foldable :visited { 464 | background: url(../expanded.png) 4px 50% no-repeat; 465 | border: none; 466 | border-radius: .3em; 467 | box-shadow: .1em .1em .3em 0 #bbb; 468 | color: #222; 469 | text-shadow: .04em .04em #fcfcfc; 470 | padding: .3em .5em .3em 20px; 471 | } 472 | .foldable :link:hover, .foldable :visited:hover { background-color: transparent } 473 | .collapsed > .foldable :link, .collapsed > .foldable :visited { 474 | background-image: url(../collapsed.png); 475 | } 476 | .collapsed > div, .collapsed > table, .collapsed > ul, .collapsed > dl { display: none } 477 | fieldset > legend.foldable :link, fieldset > legend.foldable :visited { 478 | color: #666; 479 | font-size: 110%; 480 | text-shadow: .04em .04em #ddd; 481 | } 482 | .collapsed legend.foldable { background: white } 483 | 484 | .expander { 485 | background: url(../expander_normal.png) 0 50% no-repeat; 486 | cursor: pointer; 487 | padding-left: 8px; 488 | margin-left: 4px; 489 | } 490 | .expander:hover { 491 | background: url(../expander_normal_hover.png) 0 50% no-repeat; 492 | } 493 | .expander.expanded { 494 | background: url(../expander_open.png) 0 50% no-repeat; 495 | padding-left: 12px; 496 | margin-left: 0; 497 | } 498 | .expander.expanded:hover { 499 | background: url(../expander_open_hover.png) 0 50% no-repeat; 500 | } 501 | 502 | /* Section with count */ 503 | h2 .trac-count, h3 .trac-count { 504 | margin-left: .2em; 505 | vertical-align: baseline; 506 | color: #999; 507 | font-size: 90%; 508 | font-weight: normal; 509 | } 510 | 511 | /* Page preferences form */ 512 | #prefs { 513 | background: #f7f7f0; 514 | border: 1px outset #eee; 515 | border-radius: 1em; 516 | box-shadow: .2em .2em .7em 0 #777; 517 | float: right; 518 | font-size: 9px; 519 | padding: .8em; 520 | position: relative; 521 | margin: 0 1em 1em; 522 | } 523 | #prefs input, #prefs select { font-size: 9px; vertical-align: middle } 524 | #prefs fieldset { 525 | background: transparent; 526 | border: none; 527 | box-shadow: none; 528 | margin: .5em; 529 | padding: 0; 530 | } 531 | #prefs fieldset legend { 532 | background: transparent; 533 | color: #000; 534 | font-size: 9px; 535 | font-weight: normal; 536 | margin: 0 0 0 -1.5em; 537 | padding: 0; 538 | } 539 | #prefs .buttons { text-align: right } 540 | 541 | /* Version information (browser, wiki, attachments) */ 542 | #info { 543 | margin: 1em 0 0 0; 544 | background: #f7f7f0; 545 | border: 1px solid #d7d7d7; 546 | border-collapse: collapse; 547 | border-spacing: 0; 548 | clear: both; 549 | width: 100%; 550 | } 551 | #info th, #info td { font-size: 85%; padding: 2px .5em; vertical-align: top } 552 | #info th { font-weight: bold; text-align: left } 553 | #info td.message { width: 100% } 554 | #info .message ul { padding: 0; margin: 0 2em } 555 | #info .message p { margin: 0; padding: 0 } 556 | a.trac-diff:after { content: "∆" } 557 | 558 | /* Wiki */ 559 | .wikipage { padding-left: 18px } 560 | .wikipage h1, .wikipage h2, .wikipage h3 { margin-left: -18px } 561 | .wikipage table h1, .wikipage table h2, .wikipage table h3 { margin-left: 0px } 562 | div.compact > p:first-child { margin-top: 0 } 563 | div.compact > p:last-child { margin-bottom: 0 } 564 | 565 | /* Delete */ 566 | #delete-confirm li { list-style-type: square; } 567 | 568 | /* Styles related to RTL support */ 569 | .rtl { direction: rtl; } 570 | .rtl div.wiki-toc { float: left; } 571 | .rtl .wiki-toc ul ul, .wiki-toc ol ol { padding-right: 1.2em } 572 | 573 | a.missing:link, a.missing:visited, a.missing, span.missing, 574 | a.forbidden, span.forbidden { color: #998 } 575 | a.missing:hover { color: #000 } 576 | a.closed:link, a.closed:visited, span.closed { text-decoration: line-through } 577 | 578 | /* User-selectable styles for blocks */ 579 | .important { 580 | background: #fcb; 581 | border: 1px dotted #d00; 582 | color: #500; 583 | padding: 0 .5em 0 .5em; 584 | margin: .5em; 585 | } 586 | 587 | dl.wiki dt { font-weight: bold } 588 | dl.compact dt { float: left; padding-right: .5em } 589 | dl.compact dd { margin: 0; padding: 0 } 590 | 591 | pre.wiki, pre.literal-block { 592 | background: #f7f7f7; 593 | border: 1px solid #d7d7d7; 594 | box-shadow: 0 0 1em #eee; 595 | border-radius: .3em; 596 | margin: 1em 1.75em; 597 | padding: .25em; 598 | overflow: auto; 599 | } 600 | 601 | div.wiki-code { 602 | margin: 1em 1.75em; 603 | } 604 | 605 | code, tt { 606 | color: #600; 607 | border: 1px solid #edc; 608 | border-radius: .25em; 609 | padding: 0 0.3em; 610 | background: #fafafa; 611 | } 612 | 613 | blockquote.citation { 614 | margin: -0.6em 0 0 0; 615 | border-style: solid; 616 | border-width: 0 0 0 2px; 617 | padding-left: .5em; 618 | border-color: #b44; 619 | } 620 | .citation blockquote.citation { border-color: #4b4; } 621 | .citation .citation blockquote.citation { border-color: #44b; } 622 | .citation .citation .citation blockquote.citation { border-color: #c55; } 623 | 624 | table.wiki { 625 | border: 1px solid #ccc; 626 | border-collapse: collapse; 627 | border-spacing: 0; 628 | box-shadow: 0 0 1em #eee; /* from pre.wiki */ 629 | } 630 | table.wiki td { border: 1px solid #ccc; padding: .1em .25em; } 631 | table.wiki th { 632 | border: 1px solid #bbb; 633 | padding: .1em .25em; 634 | background-color: #f7f7f7; 635 | } 636 | table.wiki tbody tr.even { background-color: #fcfcfc } 637 | table.wiki tbody tr.odd { background-color: #f7f7f7 } 638 | 639 | .wikitoolbar { 640 | margin-top: 0.3em; 641 | border: solid #d7d7d7; 642 | border-width: 1px 1px 1px 0; 643 | height: 18px; 644 | width: 234px; 645 | } 646 | .wikitoolbar :link, .wikitoolbar :visited { 647 | background: transparent url(../edit_toolbar.png) no-repeat; 648 | border: 1px solid #fff; 649 | border-left-color: #d7d7d7; 650 | cursor: default; 651 | display: block; 652 | float: left; 653 | width: 24px; 654 | height: 16px; 655 | } 656 | .wikitoolbar :link:hover, .wikitoolbar :visited:hover { 657 | background-color: transparent; 658 | border: 1px solid #fb2; 659 | } 660 | .wikitoolbar a#em { background-position: 0 0 } 661 | .wikitoolbar a#strong { background-position: 0 -16px } 662 | .wikitoolbar a#heading { background-position: 0 -32px } 663 | .wikitoolbar a#link { background-position: 0 -48px } 664 | .wikitoolbar a#code { background-position: 0 -64px } 665 | .wikitoolbar a#hr { background-position: 0 -80px } 666 | .wikitoolbar a#np { background-position: 0 -96px } 667 | .wikitoolbar a#br { background-position: 0 -112px } 668 | .wikitoolbar a#img { background-position: 0 -128px } 669 | 670 | /* Textarea resizer */ 671 | div.trac-resizable { display: table; width: 100% } 672 | div.trac-resizable > div { display: table-cell } 673 | div.trac-resizable textarea { display: block; margin-bottom: 0 } 674 | div.trac-grip { 675 | height: 5px; 676 | overflow: hidden; 677 | background: #eee url(../grip.png) no-repeat center 1px; 678 | border: 1px solid #ddd; 679 | border-top-width: 0; 680 | cursor: s-resize; 681 | } 682 | 683 | /* Styles for the list of attachments. */ 684 | #attachments > div.attachments { 685 | padding: 0 0 1em 1em; 686 | } 687 | #attachments dl.attachments { margin-left: 2em; padding: 0 } 688 | #attachments dt { display: list-item; list-style: square; } 689 | #attachments dd { font-style: italic; margin-left: 0; padding-left: 0; } 690 | #attachments p { 691 | margin-left: 2em; 692 | font-size: 90%; 693 | color: #666; 694 | } 695 | #attachments > div.attachments > p { 696 | float: right; 697 | margin: .4em 0; 698 | } 699 | #attachments span.trac-author { font-style: italic; } 700 | 701 | .attachment #preview { margin-top: 1em } 702 | 703 | /* Styles for tabular listings such as those used for displaying directory 704 | contents and report results. */ 705 | table.listing { 706 | clear: both; 707 | border-bottom: 1px solid #d7d7d7; 708 | border-collapse: collapse; 709 | border-spacing: 0; 710 | box-shadow: .2em .3em 1.5em #eee; /* from .trac-content */ 711 | margin-top: 1em; 712 | width: 100%; 713 | } 714 | table.listing th { text-align: left; padding: 0 1em .1em 0; font-size: 12px } 715 | table.listing th.sel, table.listing td.sel { text-align: center; width: 1% } 716 | table.listing thead tr { background: #f7f7f0 } 717 | table.listing thead th { 718 | border: 1px solid #d7d7d7; 719 | border-bottom-color: #999; 720 | font-size: 11px; 721 | font-weight: bold; 722 | padding: 2px .5em; 723 | vertical-align: bottom; 724 | white-space: nowrap; 725 | } 726 | table.listing thead th :link:hover, table.listing thead th :visited:hover { 727 | background-color: transparent; 728 | } 729 | table.listing thead th a { border: none; padding-right: 12px } 730 | table.listing th.asc a, table.listing th.desc a { 731 | font-weight: bold; 732 | background-position: 100% 50%; 733 | background-repeat: no-repeat; 734 | } 735 | table.listing th.asc a { background-image: url(../asc.png) } 736 | table.listing th.desc a { background-image: url(../desc.png) } 737 | table.listing tbody td, table.listing tbody th { 738 | border: 1px dotted #ddd; 739 | padding: .3em .5em; 740 | vertical-align: top; 741 | } 742 | table.listing tbody td a:hover, table.listing tbody th a:hover { 743 | background-color: transparent; 744 | } 745 | table.listing tbody tr { border-top: 1px solid #ddd } 746 | table.listing tbody tr.even { background-color: #fcfcfc } 747 | table.listing tbody tr.odd { background-color: #f7f7f7 } 748 | table.listing tbody tr:hover td { background: #eed !important } 749 | table.listing tbody tr.focus { background: #ddf !important } 750 | 751 | table.listing pre { white-space: pre-wrap } 752 | 753 | /* Styles for the page history table 754 | (extends the styles for "table.listing") */ 755 | #fieldhist td { padding: 0 .5em } 756 | #fieldhist td.date, #fieldhist td.diff, #fieldhist td.version, 757 | #fieldhist td.author { 758 | white-space: nowrap; 759 | } 760 | #fieldhist td.version { text-align: center } 761 | #fieldhist td.comment { width: 100% } 762 | #fieldhist .inlinebuttons { 763 | display: none; 764 | float: right; 765 | } 766 | .uisymbols #fieldhist .inlinebuttons a { font-size: 85% } 767 | 768 | /* Auto-completion interface */ 769 | .suggestions { background: #fff; border: 1px solid #886; color: #222; } 770 | .suggestions ul { 771 | font-family: sans-serif; 772 | max-height: 20em; 773 | min-height: 3em; 774 | list-style: none; 775 | margin: 0; 776 | overflow: auto; 777 | padding: 0; 778 | width: 440px; 779 | } 780 | * html .suggestions ul { height: 10em; } 781 | .suggestions li { background: #fff; cursor: pointer; padding: 2px 5px } 782 | .suggestions li.selected { background: #b9b9b9 } 783 | 784 | /* Styles for the error page */ 785 | #content.error .message, div.system-message { 786 | background: #fdc; 787 | border: 2px solid #d00; 788 | color: #500; 789 | padding: .5em; 790 | margin: 1em 0; 791 | } 792 | #content.error div.message pre, div.system-message pre { 793 | margin-left: 1em; 794 | overflow: hidden; 795 | white-space: pre-wrap; 796 | } 797 | 798 | div.system-message p { margin: 0 } 799 | div.system-message p.system-message-title { font-weight: bold; } 800 | 801 | /* rst errors are less emphasized */ 802 | span.system-message { 803 | float: left; 804 | background: #fdc; 805 | padding: 0.2em; 806 | margin: 1px; 807 | border-radius: 0.25em; 808 | } 809 | 810 | /* whole-page admonitions in theme.html */ 811 | div.system-message { 812 | border-radius: .5em; 813 | /* taken from #prefs */ 814 | box-shadow: .2em .2em .7em 0 #777; 815 | } 816 | 817 | #warning.system-message, .warning.system-message { 818 | background: #ffb; 819 | border: 1px solid #500; 820 | } 821 | #warning.system-message li { list-style-type: square; } 822 | 823 | #notice.system-message, .notice.system-message { 824 | background: #dfd; 825 | border: 1px solid #500; 826 | } 827 | #notice.system-message li { list-style-type: square; } 828 | 829 | div.system-message .trac-close-msg { 830 | display: none; /* hidden when no Javascript available */ 831 | float: right; 832 | font-size: 80%; 833 | /* taken from .inlinebuttons */ 834 | border: 1px solid #ddd; 835 | border-radius: 1em; 836 | height: 1.6em; 837 | padding: 0 .4em .1em; 838 | box-shadow: none; 839 | margin: 0 .1em .1em 0; 840 | background: none; 841 | color: #999; 842 | /* taken from trac-delete */ 843 | color: #d31313; 844 | /* override */ 845 | height: 1.4em; 846 | } 847 | div.system-message .trac-close-msg:hover { 848 | /* taken from .inlinebuttons */ 849 | background: #f6f6f6; 850 | color: #333; 851 | text-shadow: .04em .04em #fcfcfc; 852 | box-shadow: .1em .1em .6em 0 #999; 853 | } 854 | .uisymbols div.system-message .trac-close-msg > span { 855 | display: none; 856 | } 857 | .uisymbols div.system-message .trac-close-msg:after { 858 | content: "x"; /* or – "–"; */ 859 | } 860 | 861 | /* error.html page */ 862 | #content.error form.newticket { display: inline; } 863 | #content.error form.newticket textarea { display: none; } 864 | 865 | #content.error #traceback { margin-left: 1em; } 866 | #content.error #traceback :link, #content.error #traceback :visited { 867 | border: none; 868 | } 869 | #content.error #tbtoggle { font-size: 80%; } 870 | #content.error #traceback div { margin-left: 1em; } 871 | #content.error #traceback h3 { font-size: 95%; margin: .5em 0 0; } 872 | #content.error #traceback :link var, #content.error #traceback :visited var { 873 | font-family: monospace; 874 | font-style: normal; 875 | font-weight: bold; 876 | } 877 | #content.error #traceback span.file { color: #666; font-size: 85%; } 878 | #content.error #traceback ul { list-style: none; margin: .5em 0; padding: 0; } 879 | #content.error #traceback table.code td { white-space: pre; font-size: 90%; } 880 | #content.error #traceback table.code tr.current td { background: #e6e6e6; } 881 | #content.error #traceback table { margin: .5em 0 1em; } 882 | #content.error #traceback th, #content.error #traceback td { 883 | font-size: 85%; padding: 1px; 884 | } 885 | #content.error #traceback th var { 886 | font-family: monospace; 887 | font-style: normal; 888 | } 889 | #content.error #traceback td code { white-space: pre; } 890 | #content.error #traceback pre { font-size: 95%; } 891 | 892 | #content .paging { margin: 0 0 2em; padding: .5em 0 0; 893 | font-size: 85%; line-height: 2em; text-align: center; 894 | } 895 | #content .paging .current { 896 | padding: .1em .3em; 897 | border: 1px solid #333; 898 | background: #999; color: #fff; 899 | } 900 | 901 | #content .paging :link, #content .paging :visited { 902 | padding: .1em .3em; 903 | border: 1px solid #666; 904 | background: transparent; color: #666; 905 | } 906 | #content .paging :link:hover, #content .paging :visited:hover { 907 | background: #999; color: #fff; border-color: #333; 908 | } 909 | #content .paging .previous a, 910 | #content .paging .next a { 911 | font-size: 150%; font-weight: bold; border: none; 912 | } 913 | #content .paging .previous a:hover, 914 | #content .paging .next a:hover { 915 | background: transparent; color: #666; 916 | } 917 | 918 | #content h2 .numresults { color: #666; font-size: 90%; } 919 | 920 | /* Styles for the About page */ 921 | #content.about a.logo { border: none; float: right; margin-left: 2em; } 922 | #content.about a.logo img { display: block; } 923 | #content.about #python-logo img { width: 140px; height: 56px; } 924 | #content.about p.copyright { color: #999; font-size: 90%; } 925 | 926 | /* Styles for environment info (error and about pages) */ 927 | #environmentinfo h2 { margin-top: 2em; } 928 | #environmentinfo table { margin: 1em; width: auto; } 929 | #environmentinfo table th, #environmentinfo table td { border: 1px solid #ddd; font-size: 90%; } 930 | #environmentinfo table th { background: #f7f7f7; font-weight: bold; white-space: nowrap; } 931 | #environmentinfo table td.file { color: #666; } 932 | #environmentinfo table tr.disabled th, 933 | #environmentinfo table tr.disabled td { color: #999; } 934 | #environmentinfo #config th, #content.about #config td { padding: 3px; } 935 | #environmentinfo #config tr.modified { background: #ffd; } 936 | #environmentinfo #config tr.modified td.value { font-style: italic; } 937 | #environmentinfo #config td.doc { padding: 3px 1em; } 938 | 939 | /* Styles for jquery-ui */ 940 | .trac-placeholder { background: #eed } 941 | 942 | /* Styles for search word highlighting */ 943 | @media screen { 944 | .searchword0 { background: #ff9 } 945 | .searchword1 { background: #cfc } 946 | .searchword2 { background: #cff } 947 | .searchword3 { background: #ccf } 948 | .searchword4 { background: #fcf } 949 | } 950 | 951 | @media print { 952 | #header, #altlinks, #footer, #help, #warning, #notice { display: none } 953 | .nav, form, .buttons form, form .buttons, form .inlinebuttons, 954 | .noprint, .trac-nav, .trac-topnav, 955 | #attachments > div.attachments > p { 956 | display: none; 957 | } 958 | form.printableform { display: block } 959 | div.code pre { white-space: pre-wrap } 960 | :link, :visited { border-bottom: none } 961 | div.trac-content { box-shadow: none } 962 | .foldable :link, .foldable :visited { 963 | text-shadow: none; 964 | box-shadow: none; 965 | } 966 | } 967 | -------------------------------------------------------------------------------- /doc/SoulseekProtocol – Museek+_files/trac.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | if (typeof _ == 'undefined') 4 | babel.Translations.load({}).install(); 5 | 6 | $.fn.addAnchor = function(title) { 7 | title = title || _("Link here"); 8 | return this.filter("*[id]").each(function() { 9 | $(" \u00B6").attr("href", "#" + this.id) 10 | .attr("title", title).appendTo(this); 11 | }); 12 | }; 13 | 14 | $.fn.checked = function(checked) { 15 | if (checked == undefined) { // getter 16 | if (!this.length) return false; 17 | return this.get(0).checked; 18 | } else { // setter 19 | return this.each(function() { 20 | this.checked = checked; 21 | }); 22 | } 23 | }; 24 | 25 | // Add a Select All checkbox to each thead in the table. 26 | $.fn.addSelectAllCheckboxes = function() { 27 | var $table = this; 28 | if ($("tr td.sel", $table).length > 0) { 29 | $("tr th.sel", $table).append( 30 | $('').attr({ 31 | title: _("Toggle group") 32 | }).click(function() { 33 | $("tr td.sel input", 34 | $(this).closest("thead, tbody").next()) 35 | .prop("checked", this.checked).change(); 36 | }) 37 | ); 38 | $("tr td.sel", $table).click(function() { 39 | var $tbody = $(this).closest("tbody"); 40 | var $checkboxes = $("tr td.sel input", $tbody); 41 | var num_selected = $checkboxes.filter(":checked").length; 42 | var none_selected = num_selected === 0; 43 | var all_selected = num_selected === $checkboxes.length; 44 | $("tr th.sel input", $tbody.prev()) 45 | .prop({"checked": all_selected, 46 | "indeterminate": !(none_selected || all_selected)}); 47 | }); 48 | } 49 | }; 50 | 51 | // Conditionally disable the submit button. Returns a jQuery object. 52 | $.fn.disableSubmit = function(determinant) { 53 | determinant = $(determinant); 54 | var subject = $(this); 55 | var isDisabled; 56 | if (determinant.is("input:checkbox")) { 57 | isDisabled = function () { 58 | return determinant.filter(":checked").length === 0; 59 | } 60 | } else if (determinant.is("input:file")) { 61 | isDisabled = function () { 62 | return !determinant.val(); 63 | } 64 | } else { 65 | return subject; 66 | } 67 | function toggleDisabled() { 68 | subject.prop("disabled", isDisabled); 69 | if (subject.prop("disabled")) { 70 | subject.attr("title", _("At least one item must be selected")) 71 | } else { 72 | subject.removeAttr("title"); 73 | } 74 | } 75 | determinant.change(toggleDisabled); 76 | toggleDisabled(); 77 | return subject; 78 | }; 79 | 80 | $.fn.enable = function(enabled) { 81 | if (enabled == undefined) enabled = true; 82 | return this.each(function() { 83 | this.disabled = !enabled; 84 | var label = $(this).parents("label"); 85 | if (!label.length && this.id) { 86 | label = $("label[for='" + this.id + "']"); 87 | } 88 | if (!enabled) { 89 | label.addClass("disabled"); 90 | } else { 91 | label.removeClass("disabled"); 92 | } 93 | }); 94 | }; 95 | 96 | $.fn.getAbsolutePos = function() { 97 | return this.map(function() { 98 | var left = this.offsetLeft; 99 | var top = this.offsetTop; 100 | var parent = this.offsetParent; 101 | while (parent) { 102 | left += parent.offsetLeft; 103 | top += parent.offsetTop; 104 | parent = parent.offsetParent; 105 | } 106 | return {left: left, top: top}; 107 | }); 108 | }; 109 | 110 | $.fn.scrollToTop = function() { 111 | return this.each(function() { 112 | scrollTo(0, $(this).getAbsolutePos()[0].top); 113 | return false; 114 | }); 115 | }; 116 | 117 | // Disable the form's submit action after the submit button is pressed by 118 | // replacing it with a handler that cancels the action. The handler is 119 | // removed when navigating away from the page so that the action will 120 | // be enabled when using the back button to return to the page. 121 | $.fn.disableOnSubmit = function() { 122 | this.click(function() { 123 | var form = $(this).closest("form"); 124 | if (form.hasClass("trac-submit-is-disabled")) { 125 | form.bind("submit.prevent-submit", function() { 126 | return false; 127 | }); 128 | $(window).on("unload", function() { 129 | form.unbind("submit.prevent-submit"); 130 | }); 131 | } else { 132 | form.addClass("trac-submit-is-disabled"); 133 | $(window).on("unload", function() { 134 | form.removeClass("trac-submit-is-disabled"); 135 | }) 136 | } 137 | }); 138 | }; 139 | 140 | $.loadStyleSheet = function(href, type) { 141 | type = type || "text/css"; 142 | $(document).ready(function() { 143 | var link; 144 | $("link[rel=stylesheet]").each(function() { 145 | if (this.getAttribute("href") === href) { 146 | if (this.disabled) 147 | this.disabled = false; 148 | link = this; 149 | return false; 150 | } 151 | }); 152 | if (link !== undefined) 153 | return; 154 | if (document.createStyleSheet) { // MSIE 155 | document.createStyleSheet(href); 156 | } else { 157 | $("") 158 | .appendTo("head"); 159 | } 160 | }); 161 | }; 162 | 163 | // {script.src: [listener1, listener2, ...]} 164 | var readyListeners = {}; 165 | 166 | $.documentReady = function(listener) { 167 | var script = document.currentScript; 168 | if (script === undefined) { 169 | script = $("head script"); 170 | script = script[script.length - 1]; 171 | } 172 | if (script) { 173 | var href = script.getAttribute("src"); 174 | if (!(href in readyListeners)) 175 | readyListeners[href] = []; 176 | var listeners = readyListeners[href]; 177 | listeners.push(listener); 178 | } 179 | $(document).ready(listener); 180 | }; 181 | 182 | $.loadScript = function(href, type, charset) { 183 | var script; 184 | $("head script").each(function() { 185 | if (this.getAttribute("src") === href) { 186 | script = this; 187 | return false; 188 | } 189 | }); 190 | if (script !== undefined) { 191 | // Call registered ready listeners 192 | $.each(readyListeners[href] || [], function(idx, listener) { 193 | listener.call(document, $); 194 | }); 195 | } else { 196 | // Don't use $("