'));
123 | var channel_description = GroupsController.add_channel(spoilerContent, response.Channels[i], buttonDiv);
124 |
125 | channel_description
126 | .append('Owner: ', $('
')
127 |
128 | $(window).trigger('scroll'); // for lazy load
129 | }
130 | } else {
131 | spoilerContent.html('No results');
132 | }
133 | $('#GroupMembershipTitle')[0].innerText = response.Channels.length + " Group Memberships";
134 | }, channels_url_root);
135 | },
136 |
137 | load_group_invites: function(spoilerContent){
138 | var channels_url_root = 'https://channels.pscp.tv/v1/users/' + loginTwitter.user.id + '/pending-invites';
139 | PeriscopeWrapper.V1_GET_ApiChannels(function (response) {
140 | if (response.ChannelsWithMembership){
141 | for (var i in response.ChannelsWithMembership) {
142 | var inviter_id = response.ChannelsWithMembership[i].Membership.Inviter;
143 | var buttonsDiv = $('
')
144 | .append($('
accept'))
145 | .append($('
reject'));
146 | var channel_description = GroupsController.add_channel(spoilerContent, response.ChannelsWithMembership[i].Channel, buttonsDiv);
147 |
148 | channel_description
149 | .append('Inviter: ',$('
' + inviter_id + '').click(switchSection.bind(null, 'User', inviter_id)), '
')
150 |
151 | $(window).trigger('scroll'); // for lazy load
152 | }
153 | } else {
154 | spoilerContent.html('No results');
155 | }
156 | $('#GroupInvitationsTitle')[0].innerText = (response.ChannelsWithMembership ? response.ChannelsWithMembership.length : '0') + " Group Invitations";
157 | }, channels_url_root, null);
158 | },
159 |
160 | load_group_broadcasts: function(channel_Name, channel_id){
161 | PeriscopeWrapper.V1_GET_ApiChannels(function (channelName) {
162 | return function (chan) {
163 | var ids = [];
164 | for (var i in chan.Broadcasts)
165 | ids.push(chan.Broadcasts[i].BID);
166 | PeriscopeWrapper.V2_POST_Api('getBroadcasts', {
167 | broadcast_ids: ids
168 | }, function(resp){
169 | refreshList($('#GroupBroadcasts'), '
' + emoji_to_img(channelName) + ', ' + chan.NLive + ' lives, ' + chan.NReplay + '
')(resp);
170 | var group_broadcasts_title = $('#GroupBroadcastsTitle');
171 | var numLive = 0;
172 | var numLocked = 0;
173 | resp.forEach(function(element){
174 | element.state === 'RUNNING' ? (numLive += 1) : '';
175 | element.is_locked ? (numLocked += 1) : '';
176 | });
177 |
178 | group_broadcasts_title[0].innerText = numLive + '/' + (resp.length - numLive) + ' (Private '+ numLocked + ')' + " Group Broadcasts";
179 | group_broadcasts_title.click();
180 | group_broadcasts_title[0].scrollIntoView();
181 |
182 | }
183 | );
184 | };
185 | }(channel_Name), 'https://channels.periscope.tv/v1/channels/' + channel_id + '/broadcasts', null);
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/PeriscopeApiWrapper.js:
--------------------------------------------------------------------------------
1 | if (typeof GM_xmlhttpRequest === 'undefined') { // for NW.js
2 | GM_xmlhttpRequest = function(options) {
3 | // re-implementation of GM_xmlhttpRequest for Node.js
4 | // platforms like NW.js
5 | var onload = options.onload;
6 | options.onload = null;
7 | var u = url.parse(options.url);
8 | options.host = u.host;
9 | options.hostname = u.hostname;
10 | options.path = u.path;
11 | options.protocol = u.protocol;
12 | var chunks = '';
13 | var chunks2 = [];// needed for binary decryption key
14 | var req = https.request(options, function (res) {
15 | // res.setEncoding('utf8');
16 | res.on('data', function (chunk) {
17 | chunks += chunk;
18 | chunks2.push(chunk);
19 | });
20 | res.on('end', function() {
21 | onload({
22 | status: res.statusCode,
23 | responseText: chunks,
24 | finalUrl: res.headers['location'],
25 | responseArray: chunks2
26 | });
27 | });
28 | });
29 | req.on('error', function (e) {
30 | console.error(e);
31 | });
32 | if (options.data)
33 | req.write(options.data);
34 | req.end();
35 | return req;
36 | };
37 | }
38 |
39 | function ApiWorker(
40 | http_method, ///< the HTTP method like `POST` `GET`
41 | api_root, ///< The host-name+directory like 'https://api.periscope.tv/api/v2/'
42 | method, ///< The method called. This will get appended to the api_root
43 | headers, ///< HTTP headers
44 | params, ///< form data to be used with HTTP method like `POST`
45 | callback, ///< Call back for success
46 | callback_fail ///< Call back for failed
47 | ) {
48 | if (!params)
49 | params = {};
50 | if (loginTwitter && loginTwitter.cookie)
51 | params.cookie = loginTwitter.cookie;
52 | Progress.start();
53 | var xhrIndex = XHR.length;
54 | var req = GM_xmlhttpRequest({
55 | method: http_method,
56 | url: api_root + method,
57 | headers: headers,
58 | timeout: 10000,
59 | data: JSON.stringify(params),
60 | onload: function (r) {
61 | Progress.stop();
62 | XHR.splice(xhrIndex, 1);
63 | var response, debug = $('#debug').length && $('#debug')[0].checked;
64 | switch (r.status) {
65 | case 200:
66 | try {
67 | response = JSON.parse(r.responseText);
68 | } catch (e) {
69 | if (debug)
70 | console.warn('JSON parse error:', e);
71 | }
72 | if (!!response && callback)
73 | callback(response);
74 | $(window).trigger('scroll'); // for lazy load
75 | break;
76 | case 406:
77 | alert(JSON.parse(r.responseText).errors[0].error);
78 | break;
79 | case 401:
80 | SignOut();
81 | break;
82 | default:
83 | response = 'API error: ' + r.status + ' ' + r.responseText;
84 | if (callback_fail && Object.prototype.toString.call(callback_fail) === '[object Function]')
85 | callback_fail(response);
86 | }
87 | if (debug)
88 | console.log('Method:', method, 'params:', params, 'response:', response);
89 | }
90 | });
91 | XHR.push(req);
92 | }
93 | var authorization_token;
94 | var PeriscopeWrapper = {
95 | default_api_root: 'https://api.periscope.tv/api/v2/',
96 | default_headers: {
97 | 'User-Agent': 'Periscope/2699 (iPhone; iOS 8.1.2; Scale/2.00)'
98 | },
99 | V1_GET_ApiChannels: function(callback, url, langDt) {
100 | return PeriscopeWrapper.V1_ApiChannels(callback, url, langDt, "", "GET");
101 | },
102 | V1_ApiChannels: function(callback, url, langDt, params, http_method) {
103 | if (http_method == null) {
104 | http_method = 'GET'
105 | }
106 | Progress.start();
107 | PeriscopeWrapper.V2_POST_Api('authorizeToken', {
108 | service: 'channels'
109 | }, function (authorizeToken) {
110 | this.authorization_token = authorizeToken.authorization_token;
111 | GM_xmlhttpRequest({
112 | method: http_method,
113 | url: url,
114 | headers: {
115 | Authorization: this.authorization_token,
116 | 'X-Periscope-User-Agent': 'Periscope/2699 (iPhone; iOS 8.1.2; Scale/2.00)',
117 | locale: (langDt ? langDt.find('.lang').val() : "")
118 | },
119 | data: (params? JSON.stringify(params) : null),
120 | onload: function (r) {
121 | Progress.stop();
122 | if (r.status == 200) {
123 | var response = JSON.parse(r.responseText);
124 | if ($('#debug')[0].checked){
125 | console.log('channels ' + http_method + ' ' + url + ' : ', response);
126 | params ? console.log('params', params) : '';
127 | }
128 | callback(response);
129 | }
130 | else
131 | console.log('channels error: ' + http_method + ' ' + url + ' : ' + r.status + ' ' + r.responseText);
132 | }
133 | });
134 | });
135 | },
136 | V2_POST_Api: function(method, params, callback, callback_fail) {
137 | ApiWorker('POST', PeriscopeWrapper.default_api_root, method, PeriscopeWrapper.default_headers, params, callback, callback_fail);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Periscope_Web_Client.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Periscope_Web_Client.meta.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @id My-OpenPeriscope@nothing.com
3 | // @name Periscope Web Client
4 | // @namespace https://greasyfork.org/users/nouser
5 | // @description Periscope client based on API requests. Visit example.net for launch.
6 | // @include https://api.twitter.com/oauth/authorize
7 | // @include http://example.net/*
8 | // @version 0.2.07
9 | // @author Pmmlabs@github modified by gitnew2018@github
10 | // @grant GM_xmlhttpRequest
11 | // @connect periscope.tv
12 | // @connect pscp.tv
13 | // @connect twitter.com
14 | // @connect digits.com
15 | // @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.js
16 | // @require https://github.com/brix/crypto-js/raw/master/crypto-js.js
17 | // @require http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js
18 | // @require http://leaflet.github.io/Leaflet.markercluster/dist/leaflet.markercluster-src.js
19 | // @require https://github.com/iamcal/js-emoji/raw/master/lib/emoji.js
20 | // @require https://github.com/zenorocha/clipboard.js/raw/v2.0.0/dist/clipboard.min.js
21 | // @require https://github.com/le717/jquery-spoiler/raw/master/jquery.spoiler.min.js
22 | // @require https://github.com/gitnew2018/My-OpenPeriscope/raw/master/Groups.js
23 | // @require https://github.com/gitnew2018/My-OpenPeriscope/raw/master/ApiTest.js
24 | // @require https://github.com/gitnew2018/My-OpenPeriscope/raw/master/PeriscopeApiWrapper.js
25 | // @require https://unpkg.com/split.js/dist/split.min.js
26 | // @downloadURL https://github.com/gitnew2018/My-OpenPeriscope/raw/master/Periscope_Web_Client.user.js
27 | // @updateURL https://github.com/gitnew2018/My-OpenPeriscope/raw/master/Periscope_Web_Client.meta.js
28 | // @icon https://github.com/gitnew2018/My-OpenPeriscope/raw/master/images/openperiscope.png
29 | // @noframes
30 | // @grant GM_addStyle
31 | // @grant GM_getResourceText
32 | // @resource CSS https://github.com/gitnew2018/My-OpenPeriscope/raw/master/style.css
33 | // ==/UserScript==
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 | # My-OpenPeriscope
3 | Unofficial in-browser client for Periscope (userscript)
4 |
5 |
6 | ### Using as standalone application
7 |
8 | You can use pre-built executables from [Releases page](https://github.com/gitnew2018/My-OpenPeriscope/releases), or build it by yourself from source [guide link](https://github.com/gitnew2018/My-OpenPeriscope/wiki).
9 |
10 | ### Using as userscript
11 |
12 | 1. Install [userscript manager](https://greasyfork.org/help/installing-user-scripts)
13 | 1. Click to [link](https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/master/Periscope_Web_Client.user.js) and then "Install"
14 | 1. Navigate to http://example.net
15 |
16 | In this case posting to chat will not work.
17 |
18 | In userscript version, "Download" link is absent, so you can use downloaderNode (or other program) to download broadcasts:
19 |
20 | [My standalone periscope nodejs downloader](https://github.com/gitnew2018/nodejs_peri_downloader)
21 |
22 | ### Screenshot
23 |
24 | 
25 |
26 | ### Features added in my version
27 |
28 | * New broadcasts after refresh are highlited
29 | * Now thumbnail previews of replays open in new window even in suerscript
30 | * Download button changes
31 | * You can select users who's braodcasts will be recorded
32 | * New video downloader based on Node.js. It's more reliable imho(downloaderNode.js file)
33 | * Preserve scroll position when switching to other subpages
34 | * Download Manager
35 | * Persistent links between refreshes
36 | * Rename video if one with same name exists
37 | * Copy link with name and cookies to be used in my periscope nodejs downloader,available in userscript only
38 | * Display full size avatars from google profiles
39 | * Dark theme (changes in style.css)
40 | * some other minor tweaks.
41 | * Option to log broadcasts to text file with link to replay
42 | * Profile avatar and link in chat messages + some styling
43 | * Generate proper uuid for chat messages
44 | * Generate partial replay links
45 | * M3U links optional, on/off in settings
46 | * PeriscopeDownloader links optional, on/off in settings
47 | * Generated links stay grayed-out when no response is received
48 | * Changed Following broadcast feed to include deleted broadcasts and now broadcasts appear in new order.
49 | * "Sort by watching" is now toggle
50 | * "Show interesting only" - displays only the ones that you clicked on "get stream link"
51 | * When "Enable automatic downloading of the following items" or "Enable notifications" is on, replay links are saved and displayed on their boradcast card
52 | * In standalone version transitions in css caused heavy cpu usage. Now all are off.
53 | * Added checkbox to activate auto getting partial replay links
54 | * Added filters. Hide replays, producer or by language
55 | * Saved broadcasts now have prefixes:PV_ PR_ R_(private, partial replay, replay)
56 | * Update state of broadcasts, updating thumbnails is optional
57 | * Seach by @username not only by user id
58 | * Screenlist now changed to screenPreviewer
59 | * Added input field to download manager to quickly download from web link
60 | * You can download private broadcasts that were deleted
61 | * Added Download whole broadcast button(combine partial replay with live running broadcast into one)
62 | * Now following section has option to display broadcasts in classic order, as in periscope app
63 | * You can login with session ID, Thanks to kewalsk
64 | * Option to refresh following section on load, Thanks to Max104t
65 | * Option to open multiple preview windows, Thanks to Max104t
66 | * Basic support of groups and group invitations, Thanks to Max104t
67 | * Added file and folder names editor
68 |
69 | ## Known issues
70 | * App crashes when opening live broadcast in new window
71 | * Autodownloading broadcasts of following users also downloads private broadcasts - not easy to fix, use `selected users broadcasts`
72 | * Some broadcasts are marked as deleted but are still running - This happens when periscope removes this broadcast from following feed
73 | * Sign in with twitter and with phone not working - first requires moving to newer version of NWJS and the second just doesn't work ;)
74 |
75 | If you notice any other bug please report.
76 |
77 | ### Donate to original autor
78 | Buy pmmlabs a beer: [paypal.me/pmmlabs](https://paypal.me/pmmlabs)
79 | Bitcoin: [1F1hXcaTjS1UFUqqMzLvVyz4wDSbRJU4Tn](bitcoin:1F1hXcaTjS1UFUqqMzLvVyz4wDSbRJU4Tn)
80 |
81 | More info in original repository https://github.com/Pmmlabs/OpenPeriscope
82 |
--------------------------------------------------------------------------------
/clipboard.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * clipboard.js v2.0.0
3 | * https://zenorocha.github.io/clipboard.js
4 | *
5 | * Licensed MIT © Zeno Rocha
6 | */
7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n
0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])});
--------------------------------------------------------------------------------
/downloaderNode.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const fs = require('fs'),
3 | url = require('url'),
4 | https = require('https'),
5 | keepAliveAgent = new https.Agent({
6 | keepAlive: true
7 | }),
8 | crypto = require('crypto');
9 |
10 | process.stdin.on('data', function (msg) {
11 | if (msg.toString() === 'q') {
12 | process.send('Stopped by User');
13 | process.exit(1);
14 | }
15 | });
16 |
17 | var g_m3u_url = process.argv[process.argv.indexOf('-url') + 1],
18 | g_replay_m3u_url = process.argv[process.argv.indexOf('-rurl') + 1],
19 | g_DOWNLOAD_DIR = process.argv[process.argv.indexOf('-dir') + 1],
20 | g_fileName = process.argv[process.argv.indexOf('-name') + 1],
21 | g_cookies = process.argv[process.argv.indexOf('-cookies') + 1],
22 | g_savedDecryptionKey = process.argv[process.argv.indexOf('-key') + 1],
23 | g_replay_limit = Number(process.argv[process.argv.indexOf('-limit') + 1]),
24 | g_live_stream = null,
25 | g_liveTimeout,
26 | g_timingOut = false,
27 | g_all_chunks = [], //[chunkxxxx0.ts, chunkxxxx1.ts, ...]
28 | g_live_chunks_queue = [],
29 | g_all_replay_chunks = [],
30 | g_batch_done = true,
31 | g_live_End,
32 | g_retries = 70, // number of errors that can happen before downloding stops.
33 | g_beginnig = true, //skip first timeout
34 | g_mainInterval,
35 | g_timeoutInterval,
36 | g_vod_done = false,
37 | g_download_Whole = false,
38 | g_decryptionKey,
39 | g_chunkIvList = {}, // videochunk name + Initialization vector pairs
40 | g_chunksNotAvailable = 0,
41 | g_nokey = 0,
42 | g_encrypted = null;
43 |
44 | process.on('uncaughtException', function (err) {
45 | setTimeout(function() {
46 | process.exit(2);
47 | }, 1000);
48 | });
49 |
50 | g_savedDecryptionKey != 'undefined' ? (g_decryptionKey = Buffer.from(g_savedDecryptionKey, 'base64')) : (g_decryptionKey = null);
51 | if ((g_m3u_url == 'null') || (g_m3u_url == 'undefined')) g_m3u_url = '';
52 | if ((g_replay_m3u_url == 'null') || (g_replay_m3u_url == 'undefined')) g_replay_m3u_url = '';
53 | (g_m3u_url && g_replay_m3u_url) ? (g_download_Whole = true): '';
54 |
55 | if (g_download_Whole) {
56 | g_replay_m3u_url.includes('master_dynamic') ? '' : (g_mainInterval = setInterval(get_playlist, 4000, g_m3u_url));
57 | get_playlist(g_replay_m3u_url);
58 |
59 | } else if (g_replay_m3u_url) {
60 | get_playlist(g_replay_m3u_url);
61 |
62 | } else {
63 | g_vod_done = true;
64 | get_playlist(g_m3u_url);
65 | }
66 |
67 | function request_options(requestUrl, meth) {
68 | var options = {
69 | hostname: url.parse(requestUrl).hostname,
70 | path: url.parse(requestUrl).path,
71 | agent: keepAliveAgent
72 | };
73 | meth ? (options.method = meth) : '';
74 | g_cookies ? options.headers = {
75 | 'cookie': g_cookies
76 | } : '';
77 | return options;
78 | }
79 |
80 | function get_playlist(urlLink) {
81 | var options = request_options(urlLink);
82 | var request = https.get(options, function (res) {
83 | var responseParts = [];
84 | // res.setEncoding('utf8');
85 | res.on('data', function (dataChunk) {
86 | responseParts.push(dataChunk);
87 | });
88 | res.on('end', function () {
89 | var m3u_response = responseParts.join('').trim();
90 | // fs.appendFile(g_DOWNLOAD_DIR + '/' + g_fileName + '.txt', (m3u_response + '\n'), 'utf8', function () {});
91 | var valid_playlist = m3u_response.includes('#EXTM3U');
92 | if (valid_playlist) {
93 | var vod = m3u_response.includes('#EXT-X-PLAYLIST-TYPE:VOD');
94 | g_live_End = ((m3u_response.lastIndexOf('#EXT-X-ENDLIST') !== -1) && !vod);
95 | var m3uLines = m3u_response.split('\n');
96 |
97 | (!g_replay_m3u_url && vod) ? (g_replay_m3u_url = g_m3u_url, g_m3u_url = '') : ''; //if someone manually puts replay url into url field.
98 |
99 | var playlist_video_chunks = [];
100 | for (var i = 0; i < m3uLines.length; i++) {
101 | if (!/(^#.+|^\/.+)/.test(m3uLines[i])) { //finds chunkxxxx.ts lines
102 | if (g_encrypted === null) g_encrypted = m3u_response.includes('#EXT-X-KEY');
103 | if (g_encrypted) { //if encrypted fill g_chunkIvList with, video chunk name and it's initalization vector, objects
104 | g_chunkIvList[m3uLines[i].split('?')[0]] = m3uLines[i - 2].split(',')[2].split('=')[1].slice(2); //{chunkxxxx0.ts : d37d7010a581ce952a7c9fffdb22fd77, ...}
105 | }
106 | playlist_video_chunks.push(m3uLines[i].split('?')[0]);
107 | }
108 | }
109 |
110 | if ((g_decryptionKey === null) && m3u_response.includes('#EXT-X-KEY:')) {
111 | var keyURI = (/(^#EXT-X-KEY:.+)/m.exec(m3u_response))[0].split('"')[1];
112 | getKey(keyURI, 0);
113 | }
114 |
115 | if (g_live_stream) { //live running
116 | g_beginnig = false;
117 | playlist_video_chunks.forEach(function (vid_chunk) {
118 | if (g_all_chunks.lastIndexOf(vid_chunk) === -1) {
119 | g_all_chunks.push(vid_chunk);
120 | g_live_chunks_queue.push(vid_chunk);
121 | }
122 | });
123 |
124 | timeout_check(120, null, false);
125 | if (g_vod_done) {
126 | download_live(g_live_End);
127 | }
128 |
129 | } else {
130 | if (m3u_response.includes('#EXT-X-STREAM-INF')) { // multiple qulities playlist. some producer videos have it.
131 | var availableStreamsURLs = m3uLines.filter(function (line) { //list of available streams.
132 | return /^\/.+/.test(line);
133 | });
134 | var newLink = url.resolve('https://' + url.parse(urlLink).host + '/', availableStreamsURLs[availableStreamsURLs.length - 1]); //pick the best quality one
135 | if (newLink.endsWith('?type=replay')) {
136 | g_replay_m3u_url = newLink;
137 | } else {
138 | g_m3u_url = newLink;
139 | g_mainInterval ? clearInterval(g_mainInterval) : '';
140 | g_mainInterval = setInterval(get_playlist, 4000, newLink);
141 | }
142 | get_playlist(newLink);
143 |
144 | } else if (vod) {
145 | g_download_Whole ? get_playlist(g_m3u_url) : '';
146 | if (playlist_video_chunks.length) {
147 | if (g_replay_limit){
148 | var playlist_duration = Math.round(m3uLines.reduce(function (total, line) {
149 | line.startsWith('#EXTINF:') ? total += Number(/\d+\.\d+/.exec(line)) : '';
150 | return total;
151 | }, 0));
152 |
153 | if(playlist_duration > (g_replay_limit + 5)){
154 | var averageChunkDuration = playlist_duration / playlist_video_chunks.length;
155 | var limitedPlaylistLength = Math.round(g_replay_limit / averageChunkDuration);
156 | playlist_video_chunks.splice(0, playlist_video_chunks.length - limitedPlaylistLength);
157 | }
158 | }
159 | g_all_chunks = playlist_video_chunks;
160 | g_all_replay_chunks = playlist_video_chunks;
161 | download_vod();
162 | }
163 |
164 | } else if (g_live_stream === null) { // live start
165 | if (g_download_Whole) {
166 | g_live_chunks_queue = playlist_video_chunks.filter(function (elem) {
167 | if (!g_all_replay_chunks.includes(elem)) {
168 | return elem;
169 | }
170 | });
171 | g_all_chunks = g_all_replay_chunks.concat(g_live_chunks_queue);
172 | } else {
173 | g_mainInterval ? clearInterval(g_mainInterval) : '';
174 | g_mainInterval = setInterval(get_playlist, 4000, urlLink); //periodically check for updated playlist
175 | }
176 | g_live_stream = true;
177 |
178 | }
179 | }
180 | } else {
181 | // no valid playlist
182 | if (g_live_stream === null && g_m3u_url) { //some broadcasts begin with no valid playlists, treat it as live, with timeout
183 | g_live_stream = true;
184 | g_mainInterval = setInterval(get_playlist, 4000, urlLink); //periodically check for updated playlist
185 | } else if (g_live_chunks_queue.length && g_vod_done) {
186 | download_live(g_live_End);
187 | } else {
188 | timeout_check(30, ('Warning playlist error, status code: ' + res.statusCode), true);
189 | }
190 | }
191 | });
192 | });
193 | request.on('error', function (e) {
194 | process.send(e) //save to log
195 | if (g_m3u_url) {
196 | setTimeout(get_playlist, 4000, urlLink);
197 | }
198 | if(g_live_chunks_queue.length && g_vod_done) {
199 | download_live(g_live_End);
200 | }else if((g_vod_done || !g_all_replay_chunks.length) && g_batch_done){
201 | timeout_check(30, ('Warning error when trying to get m3u file: ' + e));
202 | }
203 | });
204 | }
205 |
206 | function getKey(keyURI, i) {
207 | var options = request_options(keyURI);
208 | var dataParts = [];
209 | https.get(options, function (res) {
210 | if (res.statusCode == 200) {
211 | res.on('data', function (chunk) {
212 | dataParts.push(chunk);
213 | }).on('end', function () {
214 | g_decryptionKey = Buffer.concat(dataParts);
215 | });
216 | } else {
217 | process.send('Warning No access to decryption key, statusCode:' + res.statusCode);
218 | process.exit(3);
219 | }
220 | }).on('error', function (e) {
221 | i += 1;
222 | process.send('Warning download Key error: ' + e);
223 | if (i === 2) {
224 | process.exit(4);
225 | }
226 | getKey(keyURI, i)
227 | });
228 | }
229 |
230 | function decrypt(encryptedBuffer, chunk_name) {
231 | var iv = Buffer.from(g_chunkIvList[chunk_name], "hex");
232 | var decrypt = crypto.createDecipheriv('aes-128-cbc', g_decryptionKey, iv);
233 | return Buffer.concat([decrypt.update(encryptedBuffer), decrypt.final()]);
234 | }
235 |
236 | function timeout_check(time, msg, stopMainInterval) {
237 | if (((!g_live_chunks_queue.length) && !g_timingOut && !g_live_End && g_vod_done) || (g_live_stream === null && !g_beginnig)) {
238 | var counter = 0;
239 | g_timingOut = true;
240 | g_liveTimeout = setTimeout(function () {
241 | endDownloading('Timeout', true);
242 | }, time * 1000);
243 | g_timeoutInterval = setInterval(function () {
244 | counter++;
245 | var defaultMsg = ('No new video chunks in the last ' + counter + 's');
246 | counter > 10 ? process.send(msg ? msg : defaultMsg) : '';
247 | }, 1000);
248 | } else if (((g_live_chunks_queue.length && !stopMainInterval) || (g_live_stream === null)) && g_timingOut) { // cancel timeout
249 | clearTimeout(g_liveTimeout);
250 | clearInterval(g_timeoutInterval)
251 | g_timingOut = false;
252 | process.send(' ');
253 | } else if (stopMainInterval && !g_timingOut){ // no valid playlist, vod might be still downloading.
254 | process.send('playlist unavailable...');
255 | g_timingOut = true;
256 | g_liveTimeout = setTimeout(function () {
257 | clearInterval(g_mainInterval);
258 | if(!g_all_chunks.length && !g_all_replay_chunks.length)
259 | endDownloading();
260 | }, time * 1000);
261 | }
262 | }
263 |
264 | function download_live(end) {
265 | !g_beginnig ? process.send('Uptime: ' + formatTime(Math.floor(process.uptime()))) : '';
266 |
267 | if ((g_encrypted && g_decryptionKey) || (g_encrypted === false)) {
268 | if (g_batch_done && g_live_chunks_queue.length) {
269 | var i = 0;
270 | g_batch_done = false;
271 | var chunks_downloading = g_live_chunks_queue.slice();
272 | chunks_downloading.forEach(function () {
273 | g_live_chunks_queue.shift();
274 | });
275 | download_file_recur(i, chunks_downloading);
276 | }else if(g_batch_done && !g_live_chunks_queue.length && end){ // when no new chunks just broadcast end appears on the playlist
277 | endDownloading('End of broadcast', true);
278 | }
279 | } else {
280 | (g_nokey === 3) ? (process.send('No Key'), process.exit(5)): '';
281 | g_nokey += 1;
282 | setTimeout(download_live, 3000, end); //if key not available try again after some time /async workaround
283 | }
284 |
285 | function download_file_recur(i, chunks_downloading, retryTimes = 3) {
286 | if (i === chunks_downloading.length) {
287 | g_batch_done = true;
288 | if (end) endDownloading('End of broadcast', true);
289 | } else {
290 | var file_url = url.resolve(g_m3u_url, chunks_downloading[i]); //replace /playlist.m3u8 with /chunk_i.ts in url to get chunk url.
291 | var options = request_options(file_url);
292 | var dataParts = [];
293 |
294 | var request = https.get(options, function (res) {
295 | res.on('data', function (data) {
296 | dataParts.push(data);
297 | }).on('end', function () {
298 | var chunkBuffer = Buffer.concat(dataParts);
299 |
300 | if (res.statusCode == 404) {
301 | i += 1;
302 | download_file_recur(i, chunks_downloading);
303 | } else if (res.statusCode == 200) {
304 | if (res.headers['content-length'] === chunkBuffer.length.toString()) {
305 | if (g_encrypted) {
306 | chunkBuffer = decrypt(chunkBuffer, chunks_downloading[i]);
307 | }
308 |
309 | fs.appendFile(g_DOWNLOAD_DIR + g_fileName + '.ts', chunkBuffer, { //concatenate incoming live video chunks
310 | encoding: 'binary'
311 | }, function (err) {
312 | if (err) {
313 | process.send('Error appending live chunk: ' + err.code); // log error and try to continue
314 | if (err.code === 'ENOENT') {
315 | process.send('Error no folder, Exiting.');
316 | throw err;
317 | }
318 | if (g_retries > 0) {
319 | g_retries -= 1;
320 | download_file_recur(i, chunks_downloading);
321 | } else {
322 | process.send('Error appending live chunk, Exiting: ' + err.code);
323 | process.send(err);
324 | throw err;
325 | }
326 | } else {
327 | i += 1;
328 | download_file_recur(i, chunks_downloading);
329 | }
330 | });
331 |
332 | } else {
333 | download_file_recur(i, chunks_downloading);
334 | }
335 | } else {
336 | if (g_retries > 0) {
337 | g_retries -= 1;
338 | setTimeout(download_file_recur, 1000, i, chunks_downloading);
339 | }
340 | }
341 | });
342 | });
343 | request.on('error', function (e) {
344 | process.send('Warning download file error: ' + e);
345 | if (g_retries > 0) {
346 | g_retries -= 1;
347 | setTimeout(download_file_recur, 1000, i, chunks_downloading);
348 | } else {
349 | process.send('Error downloading file, Exiting: ' + e);
350 | process.send(e);
351 | process.exit(10);
352 | }
353 | });
354 | }
355 | }
356 | }
357 |
358 | function download_vod() {
359 | if ((g_encrypted && g_decryptionKey) || (g_encrypted === false)) {
360 | var i = 0;
361 | download_vod_recur(i);
362 | } else {
363 | (g_nokey === 3) ? process.exit(6): '';
364 | g_nokey += 1;
365 | setTimeout(download_vod, 1000); //if key not available try again after some time /async workaround
366 | }
367 |
368 | function download_vod_recur(i) {
369 | if (i === (g_all_replay_chunks.length - g_chunksNotAvailable)) {
370 | if (g_download_Whole) {
371 | endDownloading((g_chunksNotAvailable ? 'Replay missing ' + g_chunksNotAvailable + ' parts' : ('Replay Downloaded, Recording Live')), false);
372 | } else {
373 | endDownloading((g_chunksNotAvailable ? 'Finished, missing ' + g_chunksNotAvailable + ' parts' : ('Replay Downloaded')), false);
374 | }
375 | } else {
376 | var progress = Math.round(((i + 1) / (g_all_replay_chunks.length - g_chunksNotAvailable)) * 100) + '%';
377 | process.send(progress);
378 |
379 | var file_url = url.resolve(g_replay_m3u_url, g_all_replay_chunks[i]); //replace /playlist.m3u8 with /chunk_i.ts in url to get chunk url.
380 | var options = request_options(file_url);
381 | var dataParts = [];
382 |
383 | var request = https.get(options, function (res) {
384 | res.on('data', function (data) {
385 | dataParts.push(data);
386 | }).on('end', function () {
387 | if (res.statusCode == 404) {
388 | process.send('404')
389 | fs.appendFile(g_DOWNLOAD_DIR + '/' + g_fileName + '.txt', (file_url + ' <= was not found, Video is incomplete: ' + res.statusCode + '\n'), function () {});
390 | g_chunksNotAvailable += 1;
391 | i += 1;
392 | download_vod_recur(i);
393 | } else if (res.statusCode == 200) {
394 | var chunkBuffer = Buffer.concat(dataParts);
395 |
396 | if (res.headers['content-length'] === chunkBuffer.length.toString()) {
397 | if (g_encrypted) {
398 | chunkBuffer = decrypt(chunkBuffer, g_all_replay_chunks[i]);
399 | }
400 |
401 | fs.appendFile(g_DOWNLOAD_DIR + g_fileName + '.ts', chunkBuffer, { //concatenate incoming live video chunks
402 | encoding: 'binary'
403 | }, function (err) {
404 | if (err) {
405 | process.send('Error appending vod chunk: ' + err.code); // log error and try to continue
406 | if (err.code === 'ENOENT') {
407 | process.send('Error no folder, Exiting.');
408 | throw err;
409 | }
410 | if (g_retries > 0) {
411 | g_retries -= 1;
412 | download_vod_recur(i);
413 | } else {
414 | process.send('Error appending live chunk, Exiting: ' + err.code);
415 | throw err;
416 | }
417 | } else {
418 | i += 1;
419 | download_vod_recur(i);
420 | }
421 | });
422 | } else {
423 | download_vod_recur(i)
424 | }
425 | } else {
426 | if (g_retries > 0) {
427 | g_retries -= 1;
428 | setTimeout(download_vod_recur, 1000, i);
429 | }
430 | }
431 | });
432 | });
433 | request.on('error', function (e) {
434 | process.send('Warning download file error: ' + e);
435 | if (g_retries > 0) {
436 | g_retries -= 1;
437 | setTimeout(download_vod_recur, 500, i);
438 | } else {
439 | process.send('Error downloading file, Exiting: ' + e);
440 | process.send(e);
441 | throw e;
442 | }
443 | });
444 | }
445 | }
446 | }
447 |
448 | function endDownloading(message, isLive) {
449 | if(g_all_chunks.length || g_all_replay_chunks.length){
450 | if (g_download_Whole) {
451 | if (isLive) {
452 | clearInterval(g_mainInterval);
453 | clearInterval(g_timeoutInterval);
454 | setTimeout(function () {
455 | process.send(message);
456 | process.exit();
457 | }, 1000);
458 | } else {
459 | process.send(message);
460 | g_vod_done = true;
461 | download_live(g_live_End);
462 | }
463 | } else {
464 | setTimeout(function () {
465 | process.send(message);
466 | process.exit();
467 | }, 1000);
468 | }
469 | }else{
470 | process.exit(7);
471 | }
472 | }
473 |
474 | function formatTime(time) {
475 | var hrs = ~~(time / 3600);
476 | var mins = ~~((time % 3600) / 60);
477 | var secs = time % 60;
478 | var ret = '';
479 | if (hrs > 0) {
480 | ret += '' + hrs + ':' + (mins < 10 ? "0" : '');
481 | }
482 | ret += '' + mins + ':' + (secs < 10 ? '0' : '') + secs;
483 | return ret;
484 | }
--------------------------------------------------------------------------------
/fonts/Roboto-cyrillic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/fonts/Roboto-cyrillic.woff2
--------------------------------------------------------------------------------
/fonts/Roboto-latin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/fonts/Roboto-latin.woff2
--------------------------------------------------------------------------------
/if-emoji.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global.ifEmoji = factory());
5 | }(this, function () { 'use strict';
6 |
7 | var _ArrayLikeToString = function _ArrayLikeToString(arg) {
8 | return Array.prototype.toString.call(arg);
9 | };
10 |
11 | var getTextFeature = function getTextFeature(text, color) {
12 | try {
13 | var canvas = document.createElement('canvas');
14 | canvas.width = 1;
15 | canvas.height = 1;
16 |
17 | var ctx = canvas.getContext('2d');
18 | ctx.textBaseline = 'top';
19 | ctx.font = '100px -no-font-family-here-';
20 | ctx.fillStyle = color;
21 | ctx.scale(0.01, 0.01);
22 | ctx.fillText(text, 0, 0);
23 |
24 | return ctx.getImageData(0, 0, 1, 1).data;
25 | } catch (e) {
26 | return false;
27 | }
28 | };
29 |
30 | var compareFeatures = function compareFeatures(feature1, feature2) {
31 | var feature1Str = _ArrayLikeToString(feature1);
32 | var feature2Str = _ArrayLikeToString(feature2);
33 | return feature1Str === feature2Str && feature1Str !== '0,0,0,0';
34 | };
35 |
36 | function index (text) {
37 | var feature1 = getTextFeature(text, '#000');
38 | var feature2 = getTextFeature(text, '#fff');
39 | return feature1 && feature2 && compareFeatures(feature1, feature2);
40 | }
41 |
42 | return index;
43 |
44 | }));
--------------------------------------------------------------------------------
/images/bullets-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/bullets-black.png
--------------------------------------------------------------------------------
/images/calendar-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/calendar-black.png
--------------------------------------------------------------------------------
/images/camera-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/camera-black.png
--------------------------------------------------------------------------------
/images/clock-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/clock-black.png
--------------------------------------------------------------------------------
/images/comment-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/comment-black.png
--------------------------------------------------------------------------------
/images/default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/default_avatar.png
--------------------------------------------------------------------------------
/images/delete-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/delete-black.png
--------------------------------------------------------------------------------
/images/edit-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/edit-black.png
--------------------------------------------------------------------------------
/images/eye-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/eye-black.png
--------------------------------------------------------------------------------
/images/forkme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/forkme.png
--------------------------------------------------------------------------------
/images/heart-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/heart-black.png
--------------------------------------------------------------------------------
/images/lock-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/lock-black.png
--------------------------------------------------------------------------------
/images/lock-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/lock-white.png
--------------------------------------------------------------------------------
/images/openperiscope.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/openperiscope.png
--------------------------------------------------------------------------------
/images/openperiscope3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/openperiscope3.png
--------------------------------------------------------------------------------
/images/user-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/user-black.png
--------------------------------------------------------------------------------
/images/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/vertical.png
--------------------------------------------------------------------------------
/images/video-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitnew2018/My-OpenPeriscope/60868fe32c7fe281a4b097526883ce4c085fbdb9/images/video-black.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/inject.js:
--------------------------------------------------------------------------------
1 | if (location.href == 'https://api.twitter.com/oauth/authorize') {
2 | var meta = $('meta[http-equiv="refresh"]');
3 | meta.attr('content', meta.attr('content').replace('twittersdk', 'chrome-extension'));
4 | } else if (location.host == "www.periscope.tv") {
5 | if (!Array.prototype.findIndex) {
6 | Array.prototype.findIndex = function (predicate) {
7 | if (this == null) {
8 | throw new TypeError('Array.prototype.findIndex called on null or undefined');
9 | }
10 | if (typeof predicate !== 'function') {
11 | throw new TypeError('predicate must be a function');
12 | }
13 | var list = Object(this);
14 | var length = list.length >>> 0;
15 | var thisArg = arguments[1];
16 | var value;
17 |
18 | for (var i = 0; i < length; i++) {
19 | value = list[i];
20 | if (predicate.call(thisArg, value, i, list)) {
21 | return i;
22 | }
23 | }
24 | return -1;
25 | };
26 | }
27 | }
--------------------------------------------------------------------------------
/jquery.spoiler.min.js:
--------------------------------------------------------------------------------
1 | /*! jquery-spoiler v1.3.0 | 2014 Triangle717 (http://le717.github.io) and Jarred Ballard (http://jarred.io/) | MIT License */
2 | !function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){"use strict";a.fn.spoiler=function(b){var c=a.extend({contentClass:"spoiler-content",paddingValue:6,triggerEvents:!1,includePadding:!0,buttonActiveClass:"spoiler-active",spoilerVisibleClass:"spoiler-content-visible"},b),d="."+c.contentClass,e={};return a(d).each(function(){var b=a(this);b.css("overflow","hidden");var d=b.prop("scrollHeight");d=c.includePadding?d+parseInt(c.paddingValue,10):d;var f=b.attr("data-spoiler-link");e[f]=d+"px",b.css("height","0")}),a(this).on("click",function(){var b=a(this),f=b.attr("data-spoiler-link"),g=a(d+"[data-spoiler-link="+f+"]"),h={height:e[f]},i={height:"0"},j=g.hasClass(c.spoilerVisibleClass);g.css(j?i:h),c.triggerEvents&&b.trigger(j?"jq-spoiler-hidden":"jq-spoiler-visible"),g.toggleClass(c.spoilerVisibleClass),b.toggleClass(c.buttonActiveClass)}),this}});
--------------------------------------------------------------------------------
/leaflet.markercluster-src.js:
--------------------------------------------------------------------------------
1 | /*
2 | Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
3 | https://github.com/Leaflet/Leaflet.markercluster
4 | (c) 2012-2013, Dave Leaver, smartrak
5 | */
6 | (function (window, document, undefined) {/*
7 | * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
8 | */
9 |
10 | L.MarkerClusterGroup = L.FeatureGroup.extend({
11 |
12 | options: {
13 | maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
14 | iconCreateFunction: null,
15 |
16 | spiderfyOnMaxZoom: true,
17 | showCoverageOnHover: true,
18 | zoomToBoundsOnClick: true,
19 | singleMarkerMode: false,
20 |
21 | disableClusteringAtZoom: null,
22 |
23 | // Setting this to false prevents the removal of any clusters outside of the viewpoint, which
24 | // is the default behaviour for performance reasons.
25 | removeOutsideVisibleBounds: true,
26 |
27 | //Whether to animate adding markers after adding the MarkerClusterGroup to the map
28 | // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
29 | animateAddingMarkers: false,
30 |
31 | //Increase to increase the distance away that spiderfied markers appear from the center
32 | spiderfyDistanceMultiplier: 1,
33 |
34 | // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts
35 | chunkedLoading: false,
36 | chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
37 | chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser
38 | chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
39 |
40 | //Options to pass to the L.Polygon constructor
41 | polygonOptions: {}
42 | },
43 |
44 | initialize: function (options) {
45 | L.Util.setOptions(this, options);
46 | if (!this.options.iconCreateFunction) {
47 | this.options.iconCreateFunction = this._defaultIconCreateFunction;
48 | }
49 |
50 | this._featureGroup = L.featureGroup();
51 | this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
52 |
53 | this._nonPointGroup = L.featureGroup();
54 | this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
55 |
56 | this._inZoomAnimation = 0;
57 | this._needsClustering = [];
58 | this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of
59 | //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
60 | this._currentShownBounds = null;
61 |
62 | this._queue = [];
63 | },
64 |
65 | addLayer: function (layer) {
66 |
67 | if (layer instanceof L.LayerGroup) {
68 | var array = [];
69 | for (var i in layer._layers) {
70 | array.push(layer._layers[i]);
71 | }
72 | return this.addLayers(array);
73 | }
74 |
75 | //Don't cluster non point data
76 | if (!layer.getLatLng) {
77 | this._nonPointGroup.addLayer(layer);
78 | return this;
79 | }
80 |
81 | if (!this._map) {
82 | this._needsClustering.push(layer);
83 | return this;
84 | }
85 |
86 | if (this.hasLayer(layer)) {
87 | return this;
88 | }
89 |
90 |
91 | //If we have already clustered we'll need to add this one to a cluster
92 |
93 | if (this._unspiderfy) {
94 | this._unspiderfy();
95 | }
96 |
97 | this._addLayer(layer, this._maxZoom);
98 |
99 | //Work out what is visible
100 | var visibleLayer = layer,
101 | currentZoom = this._map.getZoom();
102 | if (layer.__parent) {
103 | while (visibleLayer.__parent._zoom >= currentZoom) {
104 | visibleLayer = visibleLayer.__parent;
105 | }
106 | }
107 |
108 | if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
109 | if (this.options.animateAddingMarkers) {
110 | this._animationAddLayer(layer, visibleLayer);
111 | } else {
112 | this._animationAddLayerNonAnimated(layer, visibleLayer);
113 | }
114 | }
115 | return this;
116 | },
117 |
118 | removeLayer: function (layer) {
119 |
120 | if (layer instanceof L.LayerGroup)
121 | {
122 | var array = [];
123 | for (var i in layer._layers) {
124 | array.push(layer._layers[i]);
125 | }
126 | return this.removeLayers(array);
127 | }
128 |
129 | //Non point layers
130 | if (!layer.getLatLng) {
131 | this._nonPointGroup.removeLayer(layer);
132 | return this;
133 | }
134 |
135 | if (!this._map) {
136 | if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {
137 | this._needsRemoving.push(layer);
138 | }
139 | return this;
140 | }
141 |
142 | if (!layer.__parent) {
143 | return this;
144 | }
145 |
146 | if (this._unspiderfy) {
147 | this._unspiderfy();
148 | this._unspiderfyLayer(layer);
149 | }
150 |
151 | //Remove the marker from clusters
152 | this._removeLayer(layer, true);
153 |
154 | if (this._featureGroup.hasLayer(layer)) {
155 | this._featureGroup.removeLayer(layer);
156 | if (layer.setOpacity) {
157 | layer.setOpacity(1);
158 | }
159 | }
160 |
161 | return this;
162 | },
163 |
164 | //Takes an array of markers and adds them in bulk
165 | addLayers: function (layersArray) {
166 | var fg = this._featureGroup,
167 | npg = this._nonPointGroup,
168 | chunked = this.options.chunkedLoading,
169 | chunkInterval = this.options.chunkInterval,
170 | chunkProgress = this.options.chunkProgress,
171 | newMarkers, i, l, m;
172 |
173 | if (this._map) {
174 | var offset = 0,
175 | started = (new Date()).getTime();
176 | var process = L.bind(function () {
177 | var start = (new Date()).getTime();
178 | for (; offset < layersArray.length; offset++) {
179 | if (chunked && offset % 200 === 0) {
180 | // every couple hundred markers, instrument the time elapsed since processing started:
181 | var elapsed = (new Date()).getTime() - start;
182 | if (elapsed > chunkInterval) {
183 | break; // been working too hard, time to take a break :-)
184 | }
185 | }
186 |
187 | m = layersArray[offset];
188 |
189 | //Not point data, can't be clustered
190 | if (!m.getLatLng) {
191 | npg.addLayer(m);
192 | continue;
193 | }
194 |
195 | if (this.hasLayer(m)) {
196 | continue;
197 | }
198 |
199 | this._addLayer(m, this._maxZoom);
200 |
201 | //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will
202 | if (m.__parent) {
203 | if (m.__parent.getChildCount() === 2) {
204 | var markers = m.__parent.getAllChildMarkers(),
205 | otherMarker = markers[0] === m ? markers[1] : markers[0];
206 | fg.removeLayer(otherMarker);
207 | }
208 | }
209 | }
210 |
211 | if (chunkProgress) {
212 | // report progress and time elapsed:
213 | chunkProgress(offset, layersArray.length, (new Date()).getTime() - started);
214 | }
215 |
216 | if (offset === layersArray.length) {
217 | //Update the icons of all those visible clusters that were affected
218 | this._featureGroup.eachLayer(function (c) {
219 | if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) {
220 | c._updateIcon();
221 | }
222 | });
223 |
224 | this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
225 | } else {
226 | setTimeout(process, this.options.chunkDelay);
227 | }
228 | }, this);
229 |
230 | process();
231 | } else {
232 | newMarkers = [];
233 | for (i = 0, l = layersArray.length; i < l; i++) {
234 | m = layersArray[i];
235 |
236 | //Not point data, can't be clustered
237 | if (!m.getLatLng) {
238 | npg.addLayer(m);
239 | continue;
240 | }
241 |
242 | if (this.hasLayer(m)) {
243 | continue;
244 | }
245 |
246 | newMarkers.push(m);
247 | }
248 | this._needsClustering = this._needsClustering.concat(newMarkers);
249 | }
250 | return this;
251 | },
252 |
253 | //Takes an array of markers and removes them in bulk
254 | removeLayers: function (layersArray) {
255 | var i, l, m,
256 | fg = this._featureGroup,
257 | npg = this._nonPointGroup;
258 |
259 | if (!this._map) {
260 | for (i = 0, l = layersArray.length; i < l; i++) {
261 | m = layersArray[i];
262 | this._arraySplice(this._needsClustering, m);
263 | npg.removeLayer(m);
264 | }
265 | return this;
266 | }
267 |
268 | for (i = 0, l = layersArray.length; i < l; i++) {
269 | m = layersArray[i];
270 |
271 | if (!m.__parent) {
272 | npg.removeLayer(m);
273 | continue;
274 | }
275 |
276 | this._removeLayer(m, true, true);
277 |
278 | if (fg.hasLayer(m)) {
279 | fg.removeLayer(m);
280 | if (m.setOpacity) {
281 | m.setOpacity(1);
282 | }
283 | }
284 | }
285 |
286 | //Fix up the clusters and markers on the map
287 | this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
288 |
289 | fg.eachLayer(function (c) {
290 | if (c instanceof L.MarkerCluster) {
291 | c._updateIcon();
292 | }
293 | });
294 |
295 | return this;
296 | },
297 |
298 | //Removes all layers from the MarkerClusterGroup
299 | clearLayers: function () {
300 | //Need our own special implementation as the LayerGroup one doesn't work for us
301 |
302 | //If we aren't on the map (yet), blow away the markers we know of
303 | if (!this._map) {
304 | this._needsClustering = [];
305 | delete this._gridClusters;
306 | delete this._gridUnclustered;
307 | }
308 |
309 | if (this._noanimationUnspiderfy) {
310 | this._noanimationUnspiderfy();
311 | }
312 |
313 | //Remove all the visible layers
314 | this._featureGroup.clearLayers();
315 | this._nonPointGroup.clearLayers();
316 |
317 | this.eachLayer(function (marker) {
318 | delete marker.__parent;
319 | });
320 |
321 | if (this._map) {
322 | //Reset _topClusterLevel and the DistanceGrids
323 | this._generateInitialClusters();
324 | }
325 |
326 | return this;
327 | },
328 |
329 | //Override FeatureGroup.getBounds as it doesn't work
330 | getBounds: function () {
331 | var bounds = new L.LatLngBounds();
332 |
333 | if (this._topClusterLevel) {
334 | bounds.extend(this._topClusterLevel._bounds);
335 | }
336 |
337 | for (var i = this._needsClustering.length - 1; i >= 0; i--) {
338 | bounds.extend(this._needsClustering[i].getLatLng());
339 | }
340 |
341 | bounds.extend(this._nonPointGroup.getBounds());
342 |
343 | return bounds;
344 | },
345 |
346 | //Overrides LayerGroup.eachLayer
347 | eachLayer: function (method, context) {
348 | var markers = this._needsClustering.slice(),
349 | i;
350 |
351 | if (this._topClusterLevel) {
352 | this._topClusterLevel.getAllChildMarkers(markers);
353 | }
354 |
355 | for (i = markers.length - 1; i >= 0; i--) {
356 | method.call(context, markers[i]);
357 | }
358 |
359 | this._nonPointGroup.eachLayer(method, context);
360 | },
361 |
362 | //Overrides LayerGroup.getLayers
363 | getLayers: function () {
364 | var layers = [];
365 | this.eachLayer(function (l) {
366 | layers.push(l);
367 | });
368 | return layers;
369 | },
370 |
371 | //Overrides LayerGroup.getLayer, WARNING: Really bad performance
372 | getLayer: function (id) {
373 | var result = null;
374 |
375 | this.eachLayer(function (l) {
376 | if (L.stamp(l) === id) {
377 | result = l;
378 | }
379 | });
380 |
381 | return result;
382 | },
383 |
384 | //Returns true if the given layer is in this MarkerClusterGroup
385 | hasLayer: function (layer) {
386 | if (!layer) {
387 | return false;
388 | }
389 |
390 | var i, anArray = this._needsClustering;
391 |
392 | for (i = anArray.length - 1; i >= 0; i--) {
393 | if (anArray[i] === layer) {
394 | return true;
395 | }
396 | }
397 |
398 | anArray = this._needsRemoving;
399 | for (i = anArray.length - 1; i >= 0; i--) {
400 | if (anArray[i] === layer) {
401 | return false;
402 | }
403 | }
404 |
405 | return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);
406 | },
407 |
408 | //Zoom down to show the given layer (spiderfying if necessary) then calls the callback
409 | zoomToShowLayer: function (layer, callback) {
410 |
411 | var showMarker = function () {
412 | if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) {
413 | this._map.off('moveend', showMarker, this);
414 | this.off('animationend', showMarker, this);
415 |
416 | if (layer._icon) {
417 | callback();
418 | } else if (layer.__parent._icon) {
419 | var afterSpiderfy = function () {
420 | this.off('spiderfied', afterSpiderfy, this);
421 | callback();
422 | };
423 |
424 | this.on('spiderfied', afterSpiderfy, this);
425 | layer.__parent.spiderfy();
426 | }
427 | }
428 | };
429 |
430 | if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) {
431 | //Layer is visible ond on screen, immediate return
432 | callback();
433 | } else if (layer.__parent._zoom < this._map.getZoom()) {
434 | //Layer should be visible at this zoom level. It must not be on screen so just pan over to it
435 | this._map.on('moveend', showMarker, this);
436 | this._map.panTo(layer.getLatLng());
437 | } else {
438 | var moveStart = function () {
439 | this._map.off('movestart', moveStart, this);
440 | moveStart = null;
441 | };
442 |
443 | this._map.on('movestart', moveStart, this);
444 | this._map.on('moveend', showMarker, this);
445 | this.on('animationend', showMarker, this);
446 | layer.__parent.zoomToBounds();
447 |
448 | if (moveStart) {
449 | //Never started moving, must already be there, probably need clustering however
450 | showMarker.call(this);
451 | }
452 | }
453 | },
454 |
455 | //Overrides FeatureGroup.onAdd
456 | onAdd: function (map) {
457 | this._map = map;
458 | var i, l, layer;
459 |
460 | if (!isFinite(this._map.getMaxZoom())) {
461 | throw "Map has no maxZoom specified";
462 | }
463 |
464 | this._featureGroup.onAdd(map);
465 | this._nonPointGroup.onAdd(map);
466 |
467 | if (!this._gridClusters) {
468 | this._generateInitialClusters();
469 | }
470 |
471 | for (i = 0, l = this._needsRemoving.length; i < l; i++) {
472 | layer = this._needsRemoving[i];
473 | this._removeLayer(layer, true);
474 | }
475 | this._needsRemoving = [];
476 |
477 | //Remember the current zoom level and bounds
478 | this._zoom = this._map.getZoom();
479 | this._currentShownBounds = this._getExpandedVisibleBounds();
480 |
481 | this._map.on('zoomend', this._zoomEnd, this);
482 | this._map.on('moveend', this._moveEnd, this);
483 |
484 | if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
485 | this._spiderfierOnAdd();
486 | }
487 |
488 | this._bindEvents();
489 |
490 | //Actually add our markers to the map:
491 | l = this._needsClustering;
492 | this._needsClustering = [];
493 | this.addLayers(l);
494 | },
495 |
496 | //Overrides FeatureGroup.onRemove
497 | onRemove: function (map) {
498 | map.off('zoomend', this._zoomEnd, this);
499 | map.off('moveend', this._moveEnd, this);
500 |
501 | this._unbindEvents();
502 |
503 | //In case we are in a cluster animation
504 | this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
505 |
506 | if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
507 | this._spiderfierOnRemove();
508 | }
509 |
510 |
511 |
512 | //Clean up all the layers we added to the map
513 | this._hideCoverage();
514 | this._featureGroup.onRemove(map);
515 | this._nonPointGroup.onRemove(map);
516 |
517 | this._featureGroup.clearLayers();
518 |
519 | this._map = null;
520 | },
521 |
522 | getVisibleParent: function (marker) {
523 | var vMarker = marker;
524 | while (vMarker && !vMarker._icon) {
525 | vMarker = vMarker.__parent;
526 | }
527 | return vMarker || null;
528 | },
529 |
530 | //Remove the given object from the given array
531 | _arraySplice: function (anArray, obj) {
532 | for (var i = anArray.length - 1; i >= 0; i--) {
533 | if (anArray[i] === obj) {
534 | anArray.splice(i, 1);
535 | return true;
536 | }
537 | }
538 | },
539 |
540 | //Internal function for removing a marker from everything.
541 | //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
542 | _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {
543 | var gridClusters = this._gridClusters,
544 | gridUnclustered = this._gridUnclustered,
545 | fg = this._featureGroup,
546 | map = this._map;
547 |
548 | //Remove the marker from distance clusters it might be in
549 | if (removeFromDistanceGrid) {
550 | for (var z = this._maxZoom; z >= 0; z--) {
551 | if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {
552 | break;
553 | }
554 | }
555 | }
556 |
557 | //Work our way up the clusters removing them as we go if required
558 | var cluster = marker.__parent,
559 | markers = cluster._markers,
560 | otherMarker;
561 |
562 | //Remove the marker from the immediate parents marker list
563 | this._arraySplice(markers, marker);
564 |
565 | while (cluster) {
566 | cluster._childCount--;
567 |
568 | if (cluster._zoom < 0) {
569 | //Top level, do nothing
570 | break;
571 | } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required
572 | //We need to push the other marker up to the parent
573 | otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];
574 |
575 | //Update distance grid
576 | gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));
577 | gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));
578 |
579 | //Move otherMarker up to parent
580 | this._arraySplice(cluster.__parent._childClusters, cluster);
581 | cluster.__parent._markers.push(otherMarker);
582 | otherMarker.__parent = cluster.__parent;
583 |
584 | if (cluster._icon) {
585 | //Cluster is currently on the map, need to put the marker on the map instead
586 | fg.removeLayer(cluster);
587 | if (!dontUpdateMap) {
588 | fg.addLayer(otherMarker);
589 | }
590 | }
591 | } else {
592 | cluster._recalculateBounds();
593 | if (!dontUpdateMap || !cluster._icon) {
594 | cluster._updateIcon();
595 | }
596 | }
597 |
598 | cluster = cluster.__parent;
599 | }
600 |
601 | delete marker.__parent;
602 | },
603 |
604 | _isOrIsParent: function (el, oel) {
605 | while (oel) {
606 | if (el === oel) {
607 | return true;
608 | }
609 | oel = oel.parentNode;
610 | }
611 | return false;
612 | },
613 |
614 | _propagateEvent: function (e) {
615 | if (e.layer instanceof L.MarkerCluster) {
616 | //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
617 | if (e.originalEvent && this._isOrIsParent(e.layer._icon, e.originalEvent.relatedTarget)) {
618 | return;
619 | }
620 | e.type = 'cluster' + e.type;
621 | }
622 |
623 | this.fire(e.type, e);
624 | },
625 |
626 | //Default functionality
627 | _defaultIconCreateFunction: function (cluster) {
628 | var childCount = cluster.getChildCount();
629 |
630 | var c = ' marker-cluster-';
631 | if (childCount < 10) {
632 | c += 'small';
633 | } else if (childCount < 100) {
634 | c += 'medium';
635 | } else {
636 | c += 'large';
637 | }
638 |
639 | return new L.DivIcon({ html: '' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
640 | },
641 |
642 | _bindEvents: function () {
643 | var map = this._map,
644 | spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
645 | showCoverageOnHover = this.options.showCoverageOnHover,
646 | zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
647 |
648 | //Zoom on cluster click or spiderfy if we are at the lowest level
649 | if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
650 | this.on('clusterclick', this._zoomOrSpiderfy, this);
651 | }
652 |
653 | //Show convex hull (boundary) polygon on mouse over
654 | if (showCoverageOnHover) {
655 | this.on('clustermouseover', this._showCoverage, this);
656 | this.on('clustermouseout', this._hideCoverage, this);
657 | map.on('zoomend', this._hideCoverage, this);
658 | }
659 | },
660 |
661 | _zoomOrSpiderfy: function (e) {
662 | var map = this._map;
663 | if (map.getMaxZoom() === map.getZoom()) {
664 | if (this.options.spiderfyOnMaxZoom) {
665 | e.layer.spiderfy();
666 | }
667 | } else if (this.options.zoomToBoundsOnClick) {
668 | e.layer.zoomToBounds();
669 | }
670 |
671 | // Focus the map again for keyboard users.
672 | if (e.originalEvent && e.originalEvent.keyCode === 13) {
673 | map._container.focus();
674 | }
675 | },
676 |
677 | _showCoverage: function (e) {
678 | var map = this._map;
679 | if (this._inZoomAnimation) {
680 | return;
681 | }
682 | if (this._shownPolygon) {
683 | map.removeLayer(this._shownPolygon);
684 | }
685 | if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) {
686 | this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions);
687 | map.addLayer(this._shownPolygon);
688 | }
689 | },
690 |
691 | _hideCoverage: function () {
692 | if (this._shownPolygon) {
693 | this._map.removeLayer(this._shownPolygon);
694 | this._shownPolygon = null;
695 | }
696 | },
697 |
698 | _unbindEvents: function () {
699 | var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
700 | showCoverageOnHover = this.options.showCoverageOnHover,
701 | zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
702 | map = this._map;
703 |
704 | if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
705 | this.off('clusterclick', this._zoomOrSpiderfy, this);
706 | }
707 | if (showCoverageOnHover) {
708 | this.off('clustermouseover', this._showCoverage, this);
709 | this.off('clustermouseout', this._hideCoverage, this);
710 | map.off('zoomend', this._hideCoverage, this);
711 | }
712 | },
713 |
714 | _zoomEnd: function () {
715 | if (!this._map) { //May have been removed from the map by a zoomEnd handler
716 | return;
717 | }
718 | this._mergeSplitClusters();
719 |
720 | this._zoom = this._map._zoom;
721 | this._currentShownBounds = this._getExpandedVisibleBounds();
722 | },
723 |
724 | _moveEnd: function () {
725 | if (this._inZoomAnimation) {
726 | return;
727 | }
728 |
729 | var newBounds = this._getExpandedVisibleBounds();
730 |
731 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);
732 | this._topClusterLevel._recursivelyAddChildrenToMap(null, this._map._zoom, newBounds);
733 |
734 | this._currentShownBounds = newBounds;
735 | return;
736 | },
737 |
738 | _generateInitialClusters: function () {
739 | var maxZoom = this._map.getMaxZoom(),
740 | radius = this.options.maxClusterRadius,
741 | radiusFn = radius;
742 |
743 | //If we just set maxClusterRadius to a single number, we need to create
744 | //a simple function to return that number. Otherwise, we just have to
745 | //use the function we've passed in.
746 | if (typeof radius !== "function") {
747 | radiusFn = function () { return radius; };
748 | }
749 |
750 | if (this.options.disableClusteringAtZoom) {
751 | maxZoom = this.options.disableClusteringAtZoom - 1;
752 | }
753 | this._maxZoom = maxZoom;
754 | this._gridClusters = {};
755 | this._gridUnclustered = {};
756 |
757 | //Set up DistanceGrids for each zoom
758 | for (var zoom = maxZoom; zoom >= 0; zoom--) {
759 | this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom));
760 | this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom));
761 | }
762 |
763 | this._topClusterLevel = new L.MarkerCluster(this, -1);
764 | },
765 |
766 | //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
767 | _addLayer: function (layer, zoom) {
768 | var gridClusters = this._gridClusters,
769 | gridUnclustered = this._gridUnclustered,
770 | markerPoint, z;
771 |
772 | if (this.options.singleMarkerMode) {
773 | layer.options.icon = this.options.iconCreateFunction({
774 | getChildCount: function () {
775 | return 1;
776 | },
777 | getAllChildMarkers: function () {
778 | return [layer];
779 | }
780 | });
781 | }
782 |
783 | //Find the lowest zoom level to slot this one in
784 | for (; zoom >= 0; zoom--) {
785 | markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
786 |
787 | //Try find a cluster close by
788 | var closest = gridClusters[zoom].getNearObject(markerPoint);
789 | if (closest) {
790 | closest._addChild(layer);
791 | layer.__parent = closest;
792 | return;
793 | }
794 |
795 | //Try find a marker close by to form a new cluster with
796 | closest = gridUnclustered[zoom].getNearObject(markerPoint);
797 | if (closest) {
798 | var parent = closest.__parent;
799 | if (parent) {
800 | this._removeLayer(closest, false);
801 | }
802 |
803 | //Create new cluster with these 2 in it
804 |
805 | var newCluster = new L.MarkerCluster(this, zoom, closest, layer);
806 | gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));
807 | closest.__parent = newCluster;
808 | layer.__parent = newCluster;
809 |
810 | //First create any new intermediate parent clusters that don't exist
811 | var lastParent = newCluster;
812 | for (z = zoom - 1; z > parent._zoom; z--) {
813 | lastParent = new L.MarkerCluster(this, z, lastParent);
814 | gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));
815 | }
816 | parent._addChild(lastParent);
817 |
818 | //Remove closest from this zoom level and any above that it is in, replace with newCluster
819 | for (z = zoom; z >= 0; z--) {
820 | if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) {
821 | break;
822 | }
823 | }
824 |
825 | return;
826 | }
827 |
828 | //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
829 | gridUnclustered[zoom].addObject(layer, markerPoint);
830 | }
831 |
832 | //Didn't get in anything, add us to the top
833 | this._topClusterLevel._addChild(layer);
834 | layer.__parent = this._topClusterLevel;
835 | return;
836 | },
837 |
838 | //Enqueue code to fire after the marker expand/contract has happened
839 | _enqueue: function (fn) {
840 | this._queue.push(fn);
841 | if (!this._queueTimeout) {
842 | this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300);
843 | }
844 | },
845 | _processQueue: function () {
846 | for (var i = 0; i < this._queue.length; i++) {
847 | this._queue[i].call(this);
848 | }
849 | this._queue.length = 0;
850 | clearTimeout(this._queueTimeout);
851 | this._queueTimeout = null;
852 | },
853 |
854 | //Merge and split any existing clusters that are too big or small
855 | _mergeSplitClusters: function () {
856 |
857 | //Incase we are starting to split before the animation finished
858 | this._processQueue();
859 |
860 | if (this._zoom < this._map._zoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split
861 | this._animationStart();
862 | //Remove clusters now off screen
863 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds());
864 |
865 | this._animationZoomIn(this._zoom, this._map._zoom);
866 |
867 | } else if (this._zoom > this._map._zoom) { //Zoom out, merge
868 | this._animationStart();
869 |
870 | this._animationZoomOut(this._zoom, this._map._zoom);
871 | } else {
872 | this._moveEnd();
873 | }
874 | },
875 |
876 | //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan)
877 | _getExpandedVisibleBounds: function () {
878 | if (!this.options.removeOutsideVisibleBounds) {
879 | return this.getBounds();
880 | }
881 |
882 | var map = this._map,
883 | bounds = map.getBounds(),
884 | sw = bounds._southWest,
885 | ne = bounds._northEast,
886 | latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat),
887 | lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng);
888 |
889 | return new L.LatLngBounds(
890 | new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true),
891 | new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true));
892 | },
893 |
894 | //Shared animation code
895 | _animationAddLayerNonAnimated: function (layer, newCluster) {
896 | if (newCluster === layer) {
897 | this._featureGroup.addLayer(layer);
898 | } else if (newCluster._childCount === 2) {
899 | newCluster._addToMap();
900 |
901 | var markers = newCluster.getAllChildMarkers();
902 | this._featureGroup.removeLayer(markers[0]);
903 | this._featureGroup.removeLayer(markers[1]);
904 | } else {
905 | newCluster._updateIcon();
906 | }
907 | }
908 | });
909 |
910 | L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
911 |
912 | //Non Animated versions of everything
913 | _animationStart: function () {
914 | //Do nothing...
915 | },
916 | _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
917 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
918 | this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
919 |
920 | //We didn't actually animate, but we use this event to mean "clustering animations have finished"
921 | this.fire('animationend');
922 | },
923 | _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
924 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
925 | this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
926 |
927 | //We didn't actually animate, but we use this event to mean "clustering animations have finished"
928 | this.fire('animationend');
929 | },
930 | _animationAddLayer: function (layer, newCluster) {
931 | this._animationAddLayerNonAnimated(layer, newCluster);
932 | }
933 | } : {
934 |
935 | //Animated versions here
936 | _animationStart: function () {
937 | this._map._mapPane.className += ' leaflet-cluster-anim';
938 | this._inZoomAnimation++;
939 | },
940 | _animationEnd: function () {
941 | if (this._map) {
942 | this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
943 | }
944 | this._inZoomAnimation--;
945 | this.fire('animationend');
946 | },
947 | _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
948 | var bounds = this._getExpandedVisibleBounds(),
949 | fg = this._featureGroup,
950 | i;
951 |
952 | //Add all children of current clusters to map and remove those clusters from map
953 | this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
954 | var startPos = c._latlng,
955 | markers = c._markers,
956 | m;
957 |
958 | if (!bounds.contains(startPos)) {
959 | startPos = null;
960 | }
961 |
962 | if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
963 | fg.removeLayer(c);
964 | c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
965 | } else {
966 | //Fade out old cluster
967 | c.setOpacity(0);
968 | c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
969 | }
970 |
971 | //Remove all markers that aren't visible any more
972 | //TODO: Do we actually need to do this on the higher levels too?
973 | for (i = markers.length - 1; i >= 0; i--) {
974 | m = markers[i];
975 | if (!bounds.contains(m._latlng)) {
976 | fg.removeLayer(m);
977 | }
978 | }
979 |
980 | });
981 |
982 | this._forceLayout();
983 |
984 | //Update opacities
985 | this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
986 | //TODO Maybe? Update markers in _recursivelyBecomeVisible
987 | fg.eachLayer(function (n) {
988 | if (!(n instanceof L.MarkerCluster) && n._icon) {
989 | n.setOpacity(1);
990 | }
991 | });
992 |
993 | //update the positions of the just added clusters/markers
994 | this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
995 | c._recursivelyRestoreChildPositions(newZoomLevel);
996 | });
997 |
998 | //Remove the old clusters and close the zoom animation
999 | this._enqueue(function () {
1000 | //update the positions of the just added clusters/markers
1001 | this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
1002 | fg.removeLayer(c);
1003 | c.setOpacity(1);
1004 | });
1005 |
1006 | this._animationEnd();
1007 | });
1008 | },
1009 |
1010 | _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
1011 | this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
1012 |
1013 | //Need to add markers for those that weren't on the map before but are now
1014 | this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1015 | //Remove markers that were on the map before but won't be now
1016 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds());
1017 | },
1018 | _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {
1019 | var bounds = this._getExpandedVisibleBounds();
1020 |
1021 | //Animate all of the markers in the clusters to move to their cluster center point
1022 | cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel);
1023 |
1024 | var me = this;
1025 |
1026 | //Update the opacity (If we immediately set it they won't animate)
1027 | this._forceLayout();
1028 | cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
1029 |
1030 | //TODO: Maybe use the transition timing stuff to make this more reliable
1031 | //When the animations are done, tidy up
1032 | this._enqueue(function () {
1033 |
1034 | //This cluster stopped being a cluster before the timeout fired
1035 | if (cluster._childCount === 1) {
1036 | var m = cluster._markers[0];
1037 | //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it
1038 | m.setLatLng(m.getLatLng());
1039 | if (m.setOpacity) {
1040 | m.setOpacity(1);
1041 | }
1042 | } else {
1043 | cluster._recursively(bounds, newZoomLevel, 0, function (c) {
1044 | c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1);
1045 | });
1046 | }
1047 | me._animationEnd();
1048 | });
1049 | },
1050 | _animationAddLayer: function (layer, newCluster) {
1051 | var me = this,
1052 | fg = this._featureGroup;
1053 |
1054 | fg.addLayer(layer);
1055 | if (newCluster !== layer) {
1056 | if (newCluster._childCount > 2) { //Was already a cluster
1057 |
1058 | newCluster._updateIcon();
1059 | this._forceLayout();
1060 | this._animationStart();
1061 |
1062 | layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
1063 | layer.setOpacity(0);
1064 |
1065 | this._enqueue(function () {
1066 | fg.removeLayer(layer);
1067 | layer.setOpacity(1);
1068 |
1069 | me._animationEnd();
1070 | });
1071 |
1072 | } else { //Just became a cluster
1073 | this._forceLayout();
1074 |
1075 | me._animationStart();
1076 | me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom());
1077 | }
1078 | }
1079 | },
1080 |
1081 | //Force a browser layout of stuff in the map
1082 | // Should apply the current opacity and location to all elements so we can update them again for an animation
1083 | _forceLayout: function () {
1084 | //In my testing this works, infact offsetWidth of any element seems to work.
1085 | //Could loop all this._layers and do this for each _icon if it stops working
1086 |
1087 | L.Util.falseFn(document.body.offsetWidth);
1088 | }
1089 | });
1090 |
1091 | L.markerClusterGroup = function (options) {
1092 | return new L.MarkerClusterGroup(options);
1093 | };
1094 |
1095 |
1096 | L.MarkerCluster = L.Marker.extend({
1097 | initialize: function (group, zoom, a, b) {
1098 |
1099 | L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this });
1100 |
1101 |
1102 | this._group = group;
1103 | this._zoom = zoom;
1104 |
1105 | this._markers = [];
1106 | this._childClusters = [];
1107 | this._childCount = 0;
1108 | this._iconNeedsUpdate = true;
1109 |
1110 | this._bounds = new L.LatLngBounds();
1111 |
1112 | if (a) {
1113 | this._addChild(a);
1114 | }
1115 | if (b) {
1116 | this._addChild(b);
1117 | }
1118 | },
1119 |
1120 | //Recursively retrieve all child markers of this cluster
1121 | getAllChildMarkers: function (storageArray) {
1122 | storageArray = storageArray || [];
1123 |
1124 | for (var i = this._childClusters.length - 1; i >= 0; i--) {
1125 | this._childClusters[i].getAllChildMarkers(storageArray);
1126 | }
1127 |
1128 | for (var j = this._markers.length - 1; j >= 0; j--) {
1129 | storageArray.push(this._markers[j]);
1130 | }
1131 |
1132 | return storageArray;
1133 | },
1134 |
1135 | //Returns the count of how many child markers we have
1136 | getChildCount: function () {
1137 | return this._childCount;
1138 | },
1139 |
1140 | //Zoom to the minimum of showing all of the child markers, or the extents of this cluster
1141 | zoomToBounds: function () {
1142 | var childClusters = this._childClusters.slice(),
1143 | map = this._group._map,
1144 | boundsZoom = map.getBoundsZoom(this._bounds),
1145 | zoom = this._zoom + 1,
1146 | mapZoom = map.getZoom(),
1147 | i;
1148 |
1149 | //calculate how far we need to zoom down to see all of the markers
1150 | while (childClusters.length > 0 && boundsZoom > zoom) {
1151 | zoom++;
1152 | var newClusters = [];
1153 | for (i = 0; i < childClusters.length; i++) {
1154 | newClusters = newClusters.concat(childClusters[i]._childClusters);
1155 | }
1156 | childClusters = newClusters;
1157 | }
1158 |
1159 | if (boundsZoom > zoom) {
1160 | this._group._map.setView(this._latlng, zoom);
1161 | } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead
1162 | this._group._map.setView(this._latlng, mapZoom + 1);
1163 | } else {
1164 | this._group._map.fitBounds(this._bounds);
1165 | }
1166 | },
1167 |
1168 | getBounds: function () {
1169 | var bounds = new L.LatLngBounds();
1170 | bounds.extend(this._bounds);
1171 | return bounds;
1172 | },
1173 |
1174 | _updateIcon: function () {
1175 | this._iconNeedsUpdate = true;
1176 | if (this._icon) {
1177 | this.setIcon(this);
1178 | }
1179 | },
1180 |
1181 | //Cludge for Icon, we pretend to be an icon for performance
1182 | createIcon: function () {
1183 | if (this._iconNeedsUpdate) {
1184 | this._iconObj = this._group.options.iconCreateFunction(this);
1185 | this._iconNeedsUpdate = false;
1186 | }
1187 | return this._iconObj.createIcon();
1188 | },
1189 | createShadow: function () {
1190 | return this._iconObj.createShadow();
1191 | },
1192 |
1193 |
1194 | _addChild: function (new1, isNotificationFromChild) {
1195 |
1196 | this._iconNeedsUpdate = true;
1197 | this._expandBounds(new1);
1198 |
1199 | if (new1 instanceof L.MarkerCluster) {
1200 | if (!isNotificationFromChild) {
1201 | this._childClusters.push(new1);
1202 | new1.__parent = this;
1203 | }
1204 | this._childCount += new1._childCount;
1205 | } else {
1206 | if (!isNotificationFromChild) {
1207 | this._markers.push(new1);
1208 | }
1209 | this._childCount++;
1210 | }
1211 |
1212 | if (this.__parent) {
1213 | this.__parent._addChild(new1, true);
1214 | }
1215 | },
1216 |
1217 | //Expand our bounds and tell our parent to
1218 | _expandBounds: function (marker) {
1219 | var addedCount,
1220 | addedLatLng = marker._wLatLng || marker._latlng;
1221 |
1222 | if (marker instanceof L.MarkerCluster) {
1223 | this._bounds.extend(marker._bounds);
1224 | addedCount = marker._childCount;
1225 | } else {
1226 | this._bounds.extend(addedLatLng);
1227 | addedCount = 1;
1228 | }
1229 |
1230 | if (!this._cLatLng) {
1231 | // when clustering, take position of the first point as the cluster center
1232 | this._cLatLng = marker._cLatLng || addedLatLng;
1233 | }
1234 |
1235 | // when showing clusters, take weighted average of all points as cluster center
1236 | var totalCount = this._childCount + addedCount;
1237 |
1238 | //Calculate weighted latlng for display
1239 | if (!this._wLatLng) {
1240 | this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng);
1241 | } else {
1242 | this._wLatLng.lat = (addedLatLng.lat * addedCount + this._wLatLng.lat * this._childCount) / totalCount;
1243 | this._wLatLng.lng = (addedLatLng.lng * addedCount + this._wLatLng.lng * this._childCount) / totalCount;
1244 | }
1245 | },
1246 |
1247 | //Set our markers position as given and add it to the map
1248 | _addToMap: function (startPos) {
1249 | if (startPos) {
1250 | this._backupLatlng = this._latlng;
1251 | this.setLatLng(startPos);
1252 | }
1253 | this._group._featureGroup.addLayer(this);
1254 | },
1255 |
1256 | _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
1257 | this._recursively(bounds, 0, maxZoom - 1,
1258 | function (c) {
1259 | var markers = c._markers,
1260 | i, m;
1261 | for (i = markers.length - 1; i >= 0; i--) {
1262 | m = markers[i];
1263 |
1264 | //Only do it if the icon is still on the map
1265 | if (m._icon) {
1266 | m._setPos(center);
1267 | m.setOpacity(0);
1268 | }
1269 | }
1270 | },
1271 | function (c) {
1272 | var childClusters = c._childClusters,
1273 | j, cm;
1274 | for (j = childClusters.length - 1; j >= 0; j--) {
1275 | cm = childClusters[j];
1276 | if (cm._icon) {
1277 | cm._setPos(center);
1278 | cm.setOpacity(0);
1279 | }
1280 | }
1281 | }
1282 | );
1283 | },
1284 |
1285 | _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) {
1286 | this._recursively(bounds, newZoomLevel, 0,
1287 | function (c) {
1288 | c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);
1289 |
1290 | //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
1291 | //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
1292 | if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {
1293 | c.setOpacity(1);
1294 | c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1295 | } else {
1296 | c.setOpacity(0);
1297 | }
1298 |
1299 | c._addToMap();
1300 | }
1301 | );
1302 | },
1303 |
1304 | _recursivelyBecomeVisible: function (bounds, zoomLevel) {
1305 | this._recursively(bounds, 0, zoomLevel, null, function (c) {
1306 | c.setOpacity(1);
1307 | });
1308 | },
1309 |
1310 | _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
1311 | this._recursively(bounds, -1, zoomLevel,
1312 | function (c) {
1313 | if (zoomLevel === c._zoom) {
1314 | return;
1315 | }
1316 |
1317 | //Add our child markers at startPos (so they can be animated out)
1318 | for (var i = c._markers.length - 1; i >= 0; i--) {
1319 | var nm = c._markers[i];
1320 |
1321 | if (!bounds.contains(nm._latlng)) {
1322 | continue;
1323 | }
1324 |
1325 | if (startPos) {
1326 | nm._backupLatlng = nm.getLatLng();
1327 |
1328 | nm.setLatLng(startPos);
1329 | if (nm.setOpacity) {
1330 | nm.setOpacity(0);
1331 | }
1332 | }
1333 |
1334 | c._group._featureGroup.addLayer(nm);
1335 | }
1336 | },
1337 | function (c) {
1338 | c._addToMap(startPos);
1339 | }
1340 | );
1341 | },
1342 |
1343 | _recursivelyRestoreChildPositions: function (zoomLevel) {
1344 | //Fix positions of child markers
1345 | for (var i = this._markers.length - 1; i >= 0; i--) {
1346 | var nm = this._markers[i];
1347 | if (nm._backupLatlng) {
1348 | nm.setLatLng(nm._backupLatlng);
1349 | delete nm._backupLatlng;
1350 | }
1351 | }
1352 |
1353 | if (zoomLevel - 1 === this._zoom) {
1354 | //Reposition child clusters
1355 | for (var j = this._childClusters.length - 1; j >= 0; j--) {
1356 | this._childClusters[j]._restorePosition();
1357 | }
1358 | } else {
1359 | for (var k = this._childClusters.length - 1; k >= 0; k--) {
1360 | this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
1361 | }
1362 | }
1363 | },
1364 |
1365 | _restorePosition: function () {
1366 | if (this._backupLatlng) {
1367 | this.setLatLng(this._backupLatlng);
1368 | delete this._backupLatlng;
1369 | }
1370 | },
1371 |
1372 | //exceptBounds: If set, don't remove any markers/clusters in it
1373 | _recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) {
1374 | var m, i;
1375 | this._recursively(previousBounds, -1, zoomLevel - 1,
1376 | function (c) {
1377 | //Remove markers at every level
1378 | for (i = c._markers.length - 1; i >= 0; i--) {
1379 | m = c._markers[i];
1380 | if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1381 | c._group._featureGroup.removeLayer(m);
1382 | if (m.setOpacity) {
1383 | m.setOpacity(1);
1384 | }
1385 | }
1386 | }
1387 | },
1388 | function (c) {
1389 | //Remove child clusters at just the bottom level
1390 | for (i = c._childClusters.length - 1; i >= 0; i--) {
1391 | m = c._childClusters[i];
1392 | if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1393 | c._group._featureGroup.removeLayer(m);
1394 | if (m.setOpacity) {
1395 | m.setOpacity(1);
1396 | }
1397 | }
1398 | }
1399 | }
1400 | );
1401 | },
1402 |
1403 | //Run the given functions recursively to this and child clusters
1404 | // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
1405 | // zoomLevelToStart: zoom level to start running functions (inclusive)
1406 | // zoomLevelToStop: zoom level to stop running functions (inclusive)
1407 | // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
1408 | // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
1409 | _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {
1410 | var childClusters = this._childClusters,
1411 | zoom = this._zoom,
1412 | i, c;
1413 |
1414 | if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters
1415 | for (i = childClusters.length - 1; i >= 0; i--) {
1416 | c = childClusters[i];
1417 | if (boundsToApplyTo.intersects(c._bounds)) {
1418 | c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
1419 | }
1420 | }
1421 | } else { //In required depth
1422 |
1423 | if (runAtEveryLevel) {
1424 | runAtEveryLevel(this);
1425 | }
1426 | if (runAtBottomLevel && this._zoom === zoomLevelToStop) {
1427 | runAtBottomLevel(this);
1428 | }
1429 |
1430 | //TODO: This loop is almost the same as above
1431 | if (zoomLevelToStop > zoom) {
1432 | for (i = childClusters.length - 1; i >= 0; i--) {
1433 | c = childClusters[i];
1434 | if (boundsToApplyTo.intersects(c._bounds)) {
1435 | c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
1436 | }
1437 | }
1438 | }
1439 | }
1440 | },
1441 |
1442 | _recalculateBounds: function () {
1443 | var markers = this._markers,
1444 | childClusters = this._childClusters,
1445 | i;
1446 |
1447 | this._bounds = new L.LatLngBounds();
1448 | delete this._wLatLng;
1449 |
1450 | for (i = markers.length - 1; i >= 0; i--) {
1451 | this._expandBounds(markers[i]);
1452 | }
1453 | for (i = childClusters.length - 1; i >= 0; i--) {
1454 | this._expandBounds(childClusters[i]);
1455 | }
1456 | },
1457 |
1458 |
1459 | //Returns true if we are the parent of only one cluster and that cluster is the same as us
1460 | _isSingleParent: function () {
1461 | //Don't need to check this._markers as the rest won't work if there are any
1462 | return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;
1463 | }
1464 | });
1465 |
1466 |
1467 |
1468 | L.DistanceGrid = function (cellSize) {
1469 | this._cellSize = cellSize;
1470 | this._sqCellSize = cellSize * cellSize;
1471 | this._grid = {};
1472 | this._objectPoint = { };
1473 | };
1474 |
1475 | L.DistanceGrid.prototype = {
1476 |
1477 | addObject: function (obj, point) {
1478 | var x = this._getCoord(point.x),
1479 | y = this._getCoord(point.y),
1480 | grid = this._grid,
1481 | row = grid[y] = grid[y] || {},
1482 | cell = row[x] = row[x] || [],
1483 | stamp = L.Util.stamp(obj);
1484 |
1485 | this._objectPoint[stamp] = point;
1486 |
1487 | cell.push(obj);
1488 | },
1489 |
1490 | updateObject: function (obj, point) {
1491 | this.removeObject(obj);
1492 | this.addObject(obj, point);
1493 | },
1494 |
1495 | //Returns true if the object was found
1496 | removeObject: function (obj, point) {
1497 | var x = this._getCoord(point.x),
1498 | y = this._getCoord(point.y),
1499 | grid = this._grid,
1500 | row = grid[y] = grid[y] || {},
1501 | cell = row[x] = row[x] || [],
1502 | i, len;
1503 |
1504 | delete this._objectPoint[L.Util.stamp(obj)];
1505 |
1506 | for (i = 0, len = cell.length; i < len; i++) {
1507 | if (cell[i] === obj) {
1508 |
1509 | cell.splice(i, 1);
1510 |
1511 | if (len === 1) {
1512 | delete row[x];
1513 | }
1514 |
1515 | return true;
1516 | }
1517 | }
1518 |
1519 | },
1520 |
1521 | eachObject: function (fn, context) {
1522 | var i, j, k, len, row, cell, removed,
1523 | grid = this._grid;
1524 |
1525 | for (i in grid) {
1526 | row = grid[i];
1527 |
1528 | for (j in row) {
1529 | cell = row[j];
1530 |
1531 | for (k = 0, len = cell.length; k < len; k++) {
1532 | removed = fn.call(context, cell[k]);
1533 | if (removed) {
1534 | k--;
1535 | len--;
1536 | }
1537 | }
1538 | }
1539 | }
1540 | },
1541 |
1542 | getNearObject: function (point) {
1543 | var x = this._getCoord(point.x),
1544 | y = this._getCoord(point.y),
1545 | i, j, k, row, cell, len, obj, dist,
1546 | objectPoint = this._objectPoint,
1547 | closestDistSq = this._sqCellSize,
1548 | closest = null;
1549 |
1550 | for (i = y - 1; i <= y + 1; i++) {
1551 | row = this._grid[i];
1552 | if (row) {
1553 |
1554 | for (j = x - 1; j <= x + 1; j++) {
1555 | cell = row[j];
1556 | if (cell) {
1557 |
1558 | for (k = 0, len = cell.length; k < len; k++) {
1559 | obj = cell[k];
1560 | dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);
1561 | if (dist < closestDistSq) {
1562 | closestDistSq = dist;
1563 | closest = obj;
1564 | }
1565 | }
1566 | }
1567 | }
1568 | }
1569 | }
1570 | return closest;
1571 | },
1572 |
1573 | _getCoord: function (x) {
1574 | return Math.floor(x / this._cellSize);
1575 | },
1576 |
1577 | _sqDist: function (p, p2) {
1578 | var dx = p2.x - p.x,
1579 | dy = p2.y - p.y;
1580 | return dx * dx + dy * dy;
1581 | }
1582 | };
1583 |
1584 |
1585 | /* Copyright (c) 2012 the authors listed at the following URL, and/or
1586 | the authors of referenced articles or incorporated external code:
1587 | http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
1588 |
1589 | Permission is hereby granted, free of charge, to any person obtaining
1590 | a copy of this software and associated documentation files (the
1591 | "Software"), to deal in the Software without restriction, including
1592 | without limitation the rights to use, copy, modify, merge, publish,
1593 | distribute, sublicense, and/or sell copies of the Software, and to
1594 | permit persons to whom the Software is furnished to do so, subject to
1595 | the following conditions:
1596 |
1597 | The above copyright notice and this permission notice shall be
1598 | included in all copies or substantial portions of the Software.
1599 |
1600 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1601 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1602 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1603 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1604 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1605 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1606 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1607 |
1608 | Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1609 | */
1610 |
1611 | (function () {
1612 | L.QuickHull = {
1613 |
1614 | /*
1615 | * @param {Object} cpt a point to be measured from the baseline
1616 | * @param {Array} bl the baseline, as represented by a two-element
1617 | * array of latlng objects.
1618 | * @returns {Number} an approximate distance measure
1619 | */
1620 | getDistant: function (cpt, bl) {
1621 | var vY = bl[1].lat - bl[0].lat,
1622 | vX = bl[0].lng - bl[1].lng;
1623 | return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng));
1624 | },
1625 |
1626 | /*
1627 | * @param {Array} baseLine a two-element array of latlng objects
1628 | * representing the baseline to project from
1629 | * @param {Array} latLngs an array of latlng objects
1630 | * @returns {Object} the maximum point and all new points to stay
1631 | * in consideration for the hull.
1632 | */
1633 | findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
1634 | var maxD = 0,
1635 | maxPt = null,
1636 | newPoints = [],
1637 | i, pt, d;
1638 |
1639 | for (i = latLngs.length - 1; i >= 0; i--) {
1640 | pt = latLngs[i];
1641 | d = this.getDistant(pt, baseLine);
1642 |
1643 | if (d > 0) {
1644 | newPoints.push(pt);
1645 | } else {
1646 | continue;
1647 | }
1648 |
1649 | if (d > maxD) {
1650 | maxD = d;
1651 | maxPt = pt;
1652 | }
1653 | }
1654 |
1655 | return { maxPoint: maxPt, newPoints: newPoints };
1656 | },
1657 |
1658 |
1659 | /*
1660 | * Given a baseline, compute the convex hull of latLngs as an array
1661 | * of latLngs.
1662 | *
1663 | * @param {Array} latLngs
1664 | * @returns {Array}
1665 | */
1666 | buildConvexHull: function (baseLine, latLngs) {
1667 | var convexHullBaseLines = [],
1668 | t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
1669 |
1670 | if (t.maxPoint) { // if there is still a point "outside" the base line
1671 | convexHullBaseLines =
1672 | convexHullBaseLines.concat(
1673 | this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints)
1674 | );
1675 | convexHullBaseLines =
1676 | convexHullBaseLines.concat(
1677 | this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)
1678 | );
1679 | return convexHullBaseLines;
1680 | } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull
1681 | return [baseLine[0]];
1682 | }
1683 | },
1684 |
1685 | /*
1686 | * Given an array of latlngs, compute a convex hull as an array
1687 | * of latlngs
1688 | *
1689 | * @param {Array} latLngs
1690 | * @returns {Array}
1691 | */
1692 | getConvexHull: function (latLngs) {
1693 | // find first baseline
1694 | var maxLat = false, minLat = false,
1695 | maxPt = null, minPt = null,
1696 | i;
1697 |
1698 | for (i = latLngs.length - 1; i >= 0; i--) {
1699 | var pt = latLngs[i];
1700 | if (maxLat === false || pt.lat > maxLat) {
1701 | maxPt = pt;
1702 | maxLat = pt.lat;
1703 | }
1704 | if (minLat === false || pt.lat < minLat) {
1705 | minPt = pt;
1706 | minLat = pt.lat;
1707 | }
1708 | }
1709 | var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),
1710 | this.buildConvexHull([maxPt, minPt], latLngs));
1711 | return ch;
1712 | }
1713 | };
1714 | }());
1715 |
1716 | L.MarkerCluster.include({
1717 | getConvexHull: function () {
1718 | var childMarkers = this.getAllChildMarkers(),
1719 | points = [],
1720 | p, i;
1721 |
1722 | for (i = childMarkers.length - 1; i >= 0; i--) {
1723 | p = childMarkers[i].getLatLng();
1724 | points.push(p);
1725 | }
1726 |
1727 | return L.QuickHull.getConvexHull(points);
1728 | }
1729 | });
1730 |
1731 |
1732 | //This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
1733 | //Huge thanks to jawj for implementing it first to make my job easy :-)
1734 |
1735 | L.MarkerCluster.include({
1736 |
1737 | _2PI: Math.PI * 2,
1738 | _circleFootSeparation: 25, //related to circumference of circle
1739 | _circleStartAngle: Math.PI / 6,
1740 |
1741 | _spiralFootSeparation: 28, //related to size of spiral (experiment!)
1742 | _spiralLengthStart: 11,
1743 | _spiralLengthFactor: 5,
1744 |
1745 | _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.
1746 | // 0 -> always spiral; Infinity -> always circle
1747 |
1748 | spiderfy: function () {
1749 | if (this._group._spiderfied === this || this._group._inZoomAnimation) {
1750 | return;
1751 | }
1752 |
1753 | var childMarkers = this.getAllChildMarkers(),
1754 | group = this._group,
1755 | map = group._map,
1756 | center = map.latLngToLayerPoint(this._latlng),
1757 | positions;
1758 |
1759 | this._group._unspiderfy();
1760 | this._group._spiderfied = this;
1761 |
1762 | //TODO Maybe: childMarkers order by distance to center
1763 |
1764 | if (childMarkers.length >= this._circleSpiralSwitchover) {
1765 | positions = this._generatePointsSpiral(childMarkers.length, center);
1766 | } else {
1767 | center.y += 10; //Otherwise circles look wrong
1768 | positions = this._generatePointsCircle(childMarkers.length, center);
1769 | }
1770 |
1771 | this._animationSpiderfy(childMarkers, positions);
1772 | },
1773 |
1774 | unspiderfy: function (zoomDetails) {
1775 | /// Argument from zoomanim if being called in a zoom animation or null otherwise
1776 | if (this._group._inZoomAnimation) {
1777 | return;
1778 | }
1779 | this._animationUnspiderfy(zoomDetails);
1780 |
1781 | this._group._spiderfied = null;
1782 | },
1783 |
1784 | _generatePointsCircle: function (count, centerPt) {
1785 | var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
1786 | legLength = circumference / this._2PI, //radius from circumference
1787 | angleStep = this._2PI / count,
1788 | res = [],
1789 | i, angle;
1790 |
1791 | res.length = count;
1792 |
1793 | for (i = count - 1; i >= 0; i--) {
1794 | angle = this._circleStartAngle + i * angleStep;
1795 | res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
1796 | }
1797 |
1798 | return res;
1799 | },
1800 |
1801 | _generatePointsSpiral: function (count, centerPt) {
1802 | var legLength = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthStart,
1803 | separation = this._group.options.spiderfyDistanceMultiplier * this._spiralFootSeparation,
1804 | lengthFactor = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthFactor,
1805 | angle = 0,
1806 | res = [],
1807 | i;
1808 |
1809 | res.length = count;
1810 |
1811 | for (i = count - 1; i >= 0; i--) {
1812 | angle += separation / legLength + i * 0.0005;
1813 | res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
1814 | legLength += this._2PI * lengthFactor / angle;
1815 | }
1816 | return res;
1817 | },
1818 |
1819 | _noanimationUnspiderfy: function () {
1820 | var group = this._group,
1821 | map = group._map,
1822 | fg = group._featureGroup,
1823 | childMarkers = this.getAllChildMarkers(),
1824 | m, i;
1825 |
1826 | this.setOpacity(1);
1827 | for (i = childMarkers.length - 1; i >= 0; i--) {
1828 | m = childMarkers[i];
1829 |
1830 | fg.removeLayer(m);
1831 |
1832 | if (m._preSpiderfyLatlng) {
1833 | m.setLatLng(m._preSpiderfyLatlng);
1834 | delete m._preSpiderfyLatlng;
1835 | }
1836 | if (m.setZIndexOffset) {
1837 | m.setZIndexOffset(0);
1838 | }
1839 |
1840 | if (m._spiderLeg) {
1841 | map.removeLayer(m._spiderLeg);
1842 | delete m._spiderLeg;
1843 | }
1844 | }
1845 |
1846 | group._spiderfied = null;
1847 | }
1848 | });
1849 |
1850 | L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
1851 | //Non Animated versions of everything
1852 | _animationSpiderfy: function (childMarkers, positions) {
1853 | var group = this._group,
1854 | map = group._map,
1855 | fg = group._featureGroup,
1856 | i, m, leg, newPos;
1857 |
1858 | for (i = childMarkers.length - 1; i >= 0; i--) {
1859 | newPos = map.layerPointToLatLng(positions[i]);
1860 | m = childMarkers[i];
1861 |
1862 | m._preSpiderfyLatlng = m._latlng;
1863 | m.setLatLng(newPos);
1864 | if (m.setZIndexOffset) {
1865 | m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1866 | }
1867 |
1868 | fg.addLayer(m);
1869 |
1870 |
1871 | leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' });
1872 | map.addLayer(leg);
1873 | m._spiderLeg = leg;
1874 | }
1875 | this.setOpacity(0.3);
1876 | group.fire('spiderfied');
1877 | },
1878 |
1879 | _animationUnspiderfy: function () {
1880 | this._noanimationUnspiderfy();
1881 | }
1882 | } : {
1883 | //Animated versions here
1884 | SVG_ANIMATION: (function () {
1885 | return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1;
1886 | }()),
1887 |
1888 | _animationSpiderfy: function (childMarkers, positions) {
1889 | var me = this,
1890 | group = this._group,
1891 | map = group._map,
1892 | fg = group._featureGroup,
1893 | thisLayerPos = map.latLngToLayerPoint(this._latlng),
1894 | i, m, leg, newPos;
1895 |
1896 | //Add markers to map hidden at our center point
1897 | for (i = childMarkers.length - 1; i >= 0; i--) {
1898 | m = childMarkers[i];
1899 |
1900 | //If it is a marker, add it now and we'll animate it out
1901 | if (m.setOpacity) {
1902 | m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1903 | m.setOpacity(0);
1904 |
1905 | fg.addLayer(m);
1906 |
1907 | m._setPos(thisLayerPos);
1908 | } else {
1909 | //Vectors just get immediately added
1910 | fg.addLayer(m);
1911 | }
1912 | }
1913 |
1914 | group._forceLayout();
1915 | group._animationStart();
1916 |
1917 | var initialLegOpacity = L.Path.SVG ? 0 : 0.3,
1918 | xmlns = L.Path.SVG_NS;
1919 |
1920 |
1921 | for (i = childMarkers.length - 1; i >= 0; i--) {
1922 | newPos = map.layerPointToLatLng(positions[i]);
1923 | m = childMarkers[i];
1924 |
1925 | //Move marker to new position
1926 | m._preSpiderfyLatlng = m._latlng;
1927 | m.setLatLng(newPos);
1928 |
1929 | if (m.setOpacity) {
1930 | m.setOpacity(1);
1931 | }
1932 |
1933 |
1934 | //Add Legs.
1935 | leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: initialLegOpacity });
1936 | map.addLayer(leg);
1937 | m._spiderLeg = leg;
1938 |
1939 | //Following animations don't work for canvas
1940 | if (!L.Path.SVG || !this.SVG_ANIMATION) {
1941 | continue;
1942 | }
1943 |
1944 | //How this works:
1945 | //http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios
1946 | //http://dev.opera.com/articles/view/advanced-svg-animation-techniques/
1947 |
1948 | //Animate length
1949 | var length = leg._path.getTotalLength();
1950 | leg._path.setAttribute("stroke-dasharray", length + "," + length);
1951 |
1952 | var anim = document.createElementNS(xmlns, "animate");
1953 | anim.setAttribute("attributeName", "stroke-dashoffset");
1954 | anim.setAttribute("begin", "indefinite");
1955 | anim.setAttribute("from", length);
1956 | anim.setAttribute("to", 0);
1957 | anim.setAttribute("dur", 0.25);
1958 | leg._path.appendChild(anim);
1959 | anim.beginElement();
1960 |
1961 | //Animate opacity
1962 | anim = document.createElementNS(xmlns, "animate");
1963 | anim.setAttribute("attributeName", "stroke-opacity");
1964 | anim.setAttribute("attributeName", "stroke-opacity");
1965 | anim.setAttribute("begin", "indefinite");
1966 | anim.setAttribute("from", 0);
1967 | anim.setAttribute("to", 0.5);
1968 | anim.setAttribute("dur", 0.25);
1969 | leg._path.appendChild(anim);
1970 | anim.beginElement();
1971 | }
1972 | me.setOpacity(0.3);
1973 |
1974 | //Set the opacity of the spiderLegs back to their correct value
1975 | // The animations above override this until they complete.
1976 | // If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts.
1977 | if (L.Path.SVG) {
1978 | this._group._forceLayout();
1979 |
1980 | for (i = childMarkers.length - 1; i >= 0; i--) {
1981 | m = childMarkers[i]._spiderLeg;
1982 |
1983 | m.options.opacity = 0.5;
1984 | m._path.setAttribute('stroke-opacity', 0.5);
1985 | }
1986 | }
1987 |
1988 | setTimeout(function () {
1989 | group._animationEnd();
1990 | group.fire('spiderfied');
1991 | }, 200);
1992 | },
1993 |
1994 | _animationUnspiderfy: function (zoomDetails) {
1995 | var group = this._group,
1996 | map = group._map,
1997 | fg = group._featureGroup,
1998 | thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),
1999 | childMarkers = this.getAllChildMarkers(),
2000 | svg = L.Path.SVG && this.SVG_ANIMATION,
2001 | m, i, a;
2002 |
2003 | group._animationStart();
2004 |
2005 | //Make us visible and bring the child markers back in
2006 | this.setOpacity(1);
2007 | for (i = childMarkers.length - 1; i >= 0; i--) {
2008 | m = childMarkers[i];
2009 |
2010 | //Marker was added to us after we were spidified
2011 | if (!m._preSpiderfyLatlng) {
2012 | continue;
2013 | }
2014 |
2015 | //Fix up the location to the real one
2016 | m.setLatLng(m._preSpiderfyLatlng);
2017 | delete m._preSpiderfyLatlng;
2018 | //Hack override the location to be our center
2019 | if (m.setOpacity) {
2020 | m._setPos(thisLayerPos);
2021 | m.setOpacity(0);
2022 | } else {
2023 | fg.removeLayer(m);
2024 | }
2025 |
2026 | //Animate the spider legs back in
2027 | if (svg) {
2028 | a = m._spiderLeg._path.childNodes[0];
2029 | a.setAttribute('to', a.getAttribute('from'));
2030 | a.setAttribute('from', 0);
2031 | a.beginElement();
2032 |
2033 | a = m._spiderLeg._path.childNodes[1];
2034 | a.setAttribute('from', 0.5);
2035 | a.setAttribute('to', 0);
2036 | a.setAttribute('stroke-opacity', 0);
2037 | a.beginElement();
2038 |
2039 | m._spiderLeg._path.setAttribute('stroke-opacity', 0);
2040 | }
2041 | }
2042 |
2043 | setTimeout(function () {
2044 | //If we have only <= one child left then that marker will be shown on the map so don't remove it!
2045 | var stillThereChildCount = 0;
2046 | for (i = childMarkers.length - 1; i >= 0; i--) {
2047 | m = childMarkers[i];
2048 | if (m._spiderLeg) {
2049 | stillThereChildCount++;
2050 | }
2051 | }
2052 |
2053 |
2054 | for (i = childMarkers.length - 1; i >= 0; i--) {
2055 | m = childMarkers[i];
2056 |
2057 | if (!m._spiderLeg) { //Has already been unspiderfied
2058 | continue;
2059 | }
2060 |
2061 |
2062 | if (m.setOpacity) {
2063 | m.setOpacity(1);
2064 | m.setZIndexOffset(0);
2065 | }
2066 |
2067 | if (stillThereChildCount > 1) {
2068 | fg.removeLayer(m);
2069 | }
2070 |
2071 | map.removeLayer(m._spiderLeg);
2072 | delete m._spiderLeg;
2073 | }
2074 | group._animationEnd();
2075 | }, 200);
2076 | }
2077 | });
2078 |
2079 |
2080 | L.MarkerClusterGroup.include({
2081 | //The MarkerCluster currently spiderfied (if any)
2082 | _spiderfied: null,
2083 |
2084 | _spiderfierOnAdd: function () {
2085 | this._map.on('click', this._unspiderfyWrapper, this);
2086 |
2087 | if (this._map.options.zoomAnimation) {
2088 | this._map.on('zoomstart', this._unspiderfyZoomStart, this);
2089 | }
2090 | //Browsers without zoomAnimation or a big zoom don't fire zoomstart
2091 | this._map.on('zoomend', this._noanimationUnspiderfy, this);
2092 |
2093 | if (L.Path.SVG && !L.Browser.touch) {
2094 | this._map._initPathRoot();
2095 | //Needs to happen in the pageload, not after, or animations don't work in webkit
2096 | // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
2097 | //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
2098 | }
2099 | },
2100 |
2101 | _spiderfierOnRemove: function () {
2102 | this._map.off('click', this._unspiderfyWrapper, this);
2103 | this._map.off('zoomstart', this._unspiderfyZoomStart, this);
2104 | this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2105 |
2106 | this._unspiderfy(); //Ensure that markers are back where they should be
2107 | },
2108 |
2109 |
2110 | //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
2111 | //This means we can define the animation they do rather than Markers doing an animation to their actual location
2112 | _unspiderfyZoomStart: function () {
2113 | if (!this._map) { //May have been removed from the map by a zoomEnd handler
2114 | return;
2115 | }
2116 |
2117 | this._map.on('zoomanim', this._unspiderfyZoomAnim, this);
2118 | },
2119 | _unspiderfyZoomAnim: function (zoomDetails) {
2120 | //Wait until the first zoomanim after the user has finished touch-zooming before running the animation
2121 | if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {
2122 | return;
2123 | }
2124 |
2125 | this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2126 | this._unspiderfy(zoomDetails);
2127 | },
2128 |
2129 |
2130 | _unspiderfyWrapper: function () {
2131 | /// _unspiderfy but passes no arguments
2132 | this._unspiderfy();
2133 | },
2134 |
2135 | _unspiderfy: function (zoomDetails) {
2136 | if (this._spiderfied) {
2137 | this._spiderfied.unspiderfy(zoomDetails);
2138 | }
2139 | },
2140 |
2141 | _noanimationUnspiderfy: function () {
2142 | if (this._spiderfied) {
2143 | this._spiderfied._noanimationUnspiderfy();
2144 | }
2145 | },
2146 |
2147 | //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
2148 | _unspiderfyLayer: function (layer) {
2149 | if (layer._spiderLeg) {
2150 | this._featureGroup.removeLayer(layer);
2151 |
2152 | layer.setOpacity(1);
2153 | //Position will be fixed up immediately in _animationUnspiderfy
2154 | layer.setZIndexOffset(0);
2155 |
2156 | this._map.removeLayer(layer._spiderLeg);
2157 | delete layer._spiderLeg;
2158 | }
2159 | }
2160 | });
2161 |
2162 |
2163 | }(window, document));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-openperiscope",
3 | "version": "0.2.07",
4 | "description": "Unofficial Periscope client, based on Pmmlabs/OpenPeriscope",
5 | "license": "GPL-2.0",
6 | "repository": "gitnew2018/My-OpenPeriscope",
7 | "dependencies": {
8 | "ws": "*"
9 | },
10 | "inject_js_end": "inject.js",
11 | "main": "index.html",
12 | "webkit": {
13 | "plugin": true
14 | },
15 | "single-instance": false,
16 | "chromium-args": "--user-data-dir=userdata",
17 | "window": {
18 | "icon": "images/openperiscope.png",
19 | "toolbar": true,
20 | "frame": true,
21 | "position": "center",
22 | "width": 800,
23 | "height": 500,
24 | "min_width": 550,
25 | "min_height": 200
26 | },
27 | "domain": "openperiscope",
28 | "web_accessible_resources": [
29 | "index.html"
30 | ]
31 | }
--------------------------------------------------------------------------------
/split.min.js:
--------------------------------------------------------------------------------
1 | "use strict";(function(){var e=this,t="addEventListener",i="removeEventListener",s="getBoundingClientRect",r=e.attachEvent&&!e[t],n=e.document,a=function(){var e,t=["","-webkit-","-moz-","-o-"];for(var i=0;i=this.size-this.bMin-u.snapOffset){t=this.size-this.bMin}x.call(this,t);if(u.onDrag){u.onDrag()}},M=function(){var t=e.getComputedStyle(this.parent),i=this.parent[g]-parseFloat(t[d])-parseFloat(t[S]);this.size=this.a[s]()[c]+this.b[s]()[c]+this.aGutterSize+this.bGutterSize;this.percentage=Math.min(this.size/i*100,100);this.start=this.a[s]()[h]},x=function(e){this.a.style[c]=a+"("+e/this.size*this.percentage+"% - "+this.aGutterSize+"px)";this.b.style[c]=a+"("+(this.percentage-e/this.size*this.percentage)+"% - "+this.bGutterSize+"px)"},G=function(){var e=this,t=e.a,i=e.b;if(t[s]()[c]=0;t--){M.call(e[t]);w.call(e[t])}},U=function(){return false},D=o(l[0]).parentNode;if(!u.sizes){var k=100/l.length;u.sizes=[];for(f=0;f0){N={a:o(l[f-1]),b:O,aMin:u.minSize[f-1],bMin:u.minSize[f],dragging:false,parent:D,isFirst:B,isLast:F,direction:u.direction};N.aGutterSize=u.gutterSize;N.bGutterSize=u.gutterSize;if(B){N.aGutterSize=u.gutterSize/2}if(F){N.bGutterSize=u.gutterSize/2}}if(!r){if(f>0){var R=n.createElement("div");R.className=z;R.style[c]=u.gutterSize+"px";R[t]("mousedown",v.bind(N));R[t]("touchstart",v.bind(N));D.insertBefore(R,O);N.gutter=R}if(f===0||f==l.length-1){C=u.gutterSize/2}if(typeof u.sizes[f]==="string"||u.sizes[f]instanceof String){A=u.sizes[f]}else{A=a+"("+u.sizes[f]+"% - "+C+"px)"}}else{if(typeof u.sizes[f]==="string"||u.sizes[f]instanceof String){A=u.sizes[f]}else{A=u.sizes[f]+"%"}}O.style[c]=A;if(f>0){y.push(N)}}E(y)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=l}exports.Split=l}else{e.Split=l}}).call(window);
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | @media (max-width: 640px) {
2 | div#left {
3 | width: 0;
4 | }
5 |
6 | div#right {
7 | margin-left: 20px;
8 | }
9 |
10 | div#left:hover {
11 | width: 200px;
12 | }
13 |
14 | div#left:hover + div {
15 | margin-left: 220px;
16 | }
17 | }
18 |
19 | @media (max-width: 800px) {
20 | div#userlist {
21 | width: 0;
22 | }
23 |
24 | div#userlist:hover {
25 | width: 200px;
26 | }
27 | }
28 |
29 | @font-face {
30 | font-family: "Roboto";
31 | font-style: normal;
32 | font-weight: 400;
33 | src: local("Roboto"), local("Roboto-Regular"), url("/fonts/Roboto-latin.woff2") format("woff2");
34 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
35 | }
36 |
37 | @font-face {
38 | font-family: "Roboto";
39 | font-style: normal;
40 | font-weight: 400;
41 | src: local("Roboto"), local("Roboto-Regular"), url("/fonts/Roboto-cyrillic.woff2") format("woff2");
42 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
43 | }
44 |
45 | ::-webkit-scrollbar {
46 | background: #2A2A2A;
47 | border: solid 6px #202020;
48 | }
49 |
50 | ::-webkit-scrollbar-thumb {
51 | background: rgba(127, 154, 234, 0.2);
52 | border-radius: 2px;
53 | }
54 |
55 | ::-webkit-scrollbar-thumb:hover {
56 | background: rgba(127, 154, 234, 0.6);
57 | }
58 |
59 | ::-webkit-scrollbar-button:start:decrement {
60 | background: #2A2A2A;
61 | border-bottom-right-radius: 100%;
62 | border-bottom-left-radius: 100%;
63 | }
64 |
65 | ::-webkit-scrollbar-button:end:increment {
66 | background: #2A2A2A;
67 | border-top-left-radius: 100%;
68 | border-top-right-radius: 100%;
69 | }
70 |
71 | html, body, #left, #Map, #Chat, #map {
72 | height: 100%;
73 | }
74 |
75 | body {
76 | margin: 0;
77 | font-family: "Roboto", sans-serif;
78 | background: #1A1A1A;
79 | color: #B8B8B8;
80 | }
81 |
82 | body > div {
83 | padding: 10px;
84 | }
85 |
86 | #secret, body > a {
87 | margin: 10px;
88 | }
89 |
90 | body > input[type="text"] {
91 | margin-left: 10px;
92 | }
93 |
94 | a, .edit {
95 | color: #039be5;
96 | text-decoration: none;
97 | cursor: pointer;
98 | }
99 |
100 | input[type="text"], textarea {
101 | border: none;
102 | border-bottom: 1px solid rgba(127, 153, 234, 0.5);
103 | color: #ffffff;
104 | border-radius: 0;
105 | outline: none;
106 | height: 2rem;
107 | margin: 0 10px 9px 0;
108 | transition: box-shadow .3s;
109 | background: transparent;
110 | }
111 |
112 | input[type="text"] {
113 | font-size: 1rem;
114 | color: #ffffff;
115 | }
116 |
117 | textarea {
118 | width: 500px;
119 | height: 100px;
120 | border-left: 1px solid transparent;
121 | border-top: 1px solid transparent;
122 | border-right: 1px solid transparent;
123 | }
124 |
125 | textarea:focus {
126 | border-color: #7F99EA;
127 | }
128 |
129 | input[type="text"]:focus, textarea:focus {
130 | border-bottom: 1px solid #7F99EA;
131 | box-shadow: 0 1px 0 0 #26a69a;
132 | }
133 |
134 | #secret {
135 | font-size: 1.5em;
136 | display: block;
137 | }
138 |
139 | .button {
140 | border-radius: 5px;
141 | line-height: 36px;
142 | outline: 0 none;
143 | padding: 0 1.4rem;
144 | text-transform: uppercase;
145 | color: #FFF;
146 | border: solid 2px #7F99EA;
147 | letter-spacing: 0.5px;
148 | cursor: pointer;
149 | display: inline-block;
150 | vertical-align: middle;
151 | will-change: opacity, transform; /* blurry buttons */
152 | transition: all 0.3s ease-out 0s;
153 | margin-right: 10px;
154 | height: 36px;
155 | overflow: hidden;
156 | }
157 |
158 | .button:hover {
159 | background-color: rgba(127, 153, 234, 0.6);
160 | }
161 |
162 | .button2{
163 | border-radius: 5px;
164 | line-height: 20px;
165 | padding: 0 0.5rem;
166 | cursor: pointer;
167 | display: inline-block;
168 | vertical-align: middle;
169 | transition: all 0.3s ease-out 0s;
170 | overflow: hidden;
171 | border: solid 1px
172 | }
173 |
174 | .menu {
175 | cursor: pointer;
176 | transition: all 0.25s ease 0s;
177 | color: #7f99ea;
178 | line-height: 1.5rem;
179 | height: 1.5rem;
180 | padding: 10px 20px;
181 | margin: 0;
182 | border-bottom: 1px solid rgba(127, 153, 234, 0.5);
183 | }
184 |
185 | .menu.active {
186 | background-color: #7F99EA;
187 | color: #EAFAF9;
188 | }
189 |
190 | .menu:hover:not(.active), .contextmenu div:hover {
191 | background-color: rgba(127, 153, 234, 0.3);
192 | }
193 |
194 | #progress {
195 | position: fixed;
196 | top: 0;
197 | left: 0;
198 | width: 0;
199 | z-index: 1;
200 | padding: 0;
201 | height: 2px;
202 | background: #77b7ff;
203 | box-shadow: 0 0 10px rgba(119, 183, 255, 0.7);
204 | -webkit-transition: width 10s ease;
205 | transition: width 10s ease;
206 | }
207 |
208 | #left > img {
209 | border-radius: 100%;
210 | display: block;
211 | margin: 0 auto 5px auto;
212 | min-height: 128px;
213 | }
214 |
215 | #left > a.button {
216 | border: solid 2px #D32D2D;
217 | margin: 0 0 5px 30px;
218 | }
219 |
220 | #left > a.button:hover {
221 | background-color: rgba(211, 45, 45, 0.6);
222 | }
223 |
224 | #left > label {
225 | display: block;
226 | color: #616161;
227 | direction: rtl;
228 | margin: 10px 20px 25px 20px;
229 | float: left;
230 | }
231 |
232 | input#debug {
233 | direction: ltr;
234 | }
235 |
236 | #left {
237 | position: fixed;
238 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
239 | overflow: auto;
240 | width: 200px;
241 | transition: width 0.2s ease-out;
242 | }
243 |
244 | #right {
245 | width: auto;
246 | height: 95%;
247 | margin-left: 220px;
248 | transition: margin-left 0.2s ease-out;
249 | }
250 |
251 | .username, .leaflet-container a.username {
252 | color: grey;
253 | font-weight: bold;
254 | overflow: hidden;
255 | text-overflow: ellipsis;
256 | cursor: pointer;
257 | }
258 |
259 | #Map, #Chat, #map {
260 | width: 100%;
261 | }
262 |
263 | .live-cluster-small div {
264 | background-color: rgba(222, 0, 0, 0.6);
265 | }
266 |
267 | .live-cluster-medium div {
268 | background-color: rgba(180, 0, 0, 0.7);
269 | }
270 |
271 | .live-cluster-large div {
272 | background-color: rgba(150, 0, 0, 0.9);
273 | }
274 |
275 | .replay-cluster-small div {
276 | background-color: rgba(59, 51, 227, 0.6);
277 | }
278 |
279 | .replay-cluster-medium div {
280 | background-color: rgba(43, 38, 174, 0.7);
281 | }
282 |
283 | .replay-cluster-large div {
284 | background-color: rgba(33, 29, 128, 0.9);
285 | }
286 |
287 | .marker-cluster {
288 | background-clip: padding-box;
289 | border-radius: 20px;
290 | background-color: white;
291 | }
292 |
293 | .marker-cluster div {
294 | width: 36px;
295 | height: 36px;
296 | margin-left: 2px;
297 | margin-top: 2px;
298 | text-align: center;
299 | border-radius: 18px;
300 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
301 | }
302 |
303 | .marker-cluster span {
304 | line-height: 36px;
305 | color: white;
306 | font-weight: bold;
307 | }
308 |
309 | .leaflet-popup-content .description {
310 | min-height: 128px;
311 | }
312 |
313 | .leaflet-popup-content {
314 | width: 350px !important;
315 | }
316 |
317 | .description a {
318 | font-weight: bold;
319 | }
320 |
321 | .description img {
322 | float: left;
323 | margin-right: 10px;
324 | }
325 |
326 | .icon {
327 | padding-left: 25px;
328 | background-repeat: no-repeat;
329 | background-position: 5px center;
330 | -webkit-filter: invert(1) opacity(70%);
331 | filter: invert(1) opacity(70%);
332 | color: #FC641D;
333 | }
334 |
335 | .icon:hover {
336 | -webkit-filter: invert() opacity();
337 | filter: invert(1) opacity();
338 | }
339 |
340 | .chatlink {
341 | background-image: url("/images/comment-black.png");
342 | }
343 |
344 | .watching {
345 | background-image: url("/images/user-black.png");
346 | }
347 |
348 | .hearts {
349 | background-image: url("/images/heart-black.png");
350 | }
351 |
352 | .delete {
353 | background-image: url("/images/delete-black.png");
354 | height: 14px;
355 | }
356 |
357 | .screenlist {
358 | background-image: url("/images/camera-black.png");
359 | height: 14px;
360 | }
361 |
362 | .date {
363 | background-image: url("/images/calendar-black.png");
364 | }
365 |
366 | .time {
367 | background-image: url("/images/clock-black.png");
368 | }
369 |
370 | .friend_chat {
371 | background-image: url("/images/eye-black.png");
372 | }
373 |
374 | .is_locked {
375 | background-image: url("/images/lock-black.png");
376 | }
377 |
378 | .edit {
379 | background-image: url("/images/edit-black.png");
380 | background-position: center;
381 | padding: 7px 20px 7px 20px;
382 | border-radius: 5px;
383 | transition: all 0.3s ease-out 0s;
384 | }
385 |
386 | .edit:hover {
387 | background-color: rgba(252, 100, 29, 0.3);
388 | }
389 |
390 | .lives {
391 | background-image: url("/images/video-black.png");
392 | }
393 |
394 | dt {
395 | min-width: 150px;
396 | float: left;
397 | padding-top: 0.5rem;
398 | }
399 |
400 | #ApiTest input {
401 | width: 500px;
402 | }
403 |
404 | pre {
405 | background-color: #2A2A2A;
406 | padding: 7px;
407 | white-space: pre-wrap;
408 | word-wrap: break-word;
409 | }
410 |
411 | .card {
412 | font: 13px/1.3 "Helvetica Neue", Arial, Helvetica, sans-serif;
413 | min-height: 128px;
414 | margin: 0.2rem 0 0.2rem 0;
415 | background-color: #222222;
416 | border-radius: 2px;
417 | min-width: 350px;
418 | border-left: 5px solid #222222;
419 | padding-left: 5px;
420 | }
421 |
422 | .card .description {
423 | padding-top: 10px;
424 | padding-right: 10px;
425 | }
426 |
427 | .card.RUNNING {
428 | border-color: #ED4D4D;
429 | }
430 |
431 | .card.RUNNING a.broadcastTitle {
432 | color: #ED7272;
433 | }
434 | .card.ENDED {
435 | border-color: #4350E9;
436 | }
437 |
438 | .card.TIMED_OUT {
439 | border-color: #039be5;;
440 | }
441 |
442 | .card img {
443 | height: 128px;
444 | margin-top: -10px;
445 | min-width: 72px;
446 | }
447 |
448 | .links {
449 | min-height: 55px;
450 | line-height: 25px;
451 | display: table;
452 | }
453 | .oldLinks a{
454 | color: gray;
455 | }
456 | /* CHAT */
457 |
458 | #userlist {
459 | float: right;
460 | width: 250px;
461 | transition: width 0.2s ease-out;
462 | }
463 |
464 | #chat {
465 | word-break: break-all;
466 | }
467 |
468 | #chat, #userlist {
469 | border: 1px solid #bcbcbc;
470 | height: 84%;
471 | padding: 5px;
472 | overflow-y: auto;
473 | }
474 |
475 | #resultConsole {
476 | overflow-y: auto;
477 | height: 300px;
478 | }
479 |
480 | .user {
481 | white-space: nowrap;
482 | }
483 |
484 | #chat .user {
485 | color: #7F99EA;
486 | cursor: pointer;
487 | font-size: 0.8em;
488 | vertical-align: top;
489 | }
490 |
491 | #userlist .user {
492 | cursor: default;
493 | }
494 |
495 | .user div {
496 | display: inline;
497 | }
498 |
499 | #title {
500 | font-size: 16px;
501 | }
502 |
503 | #presence {
504 | text-align: right;
505 | }
506 |
507 | #sendLike {
508 | -webkit-touch-callout: none;
509 | -webkit-user-select: none;
510 | -khtml-user-select: none;
511 | -moz-user-select: none;
512 | -ms-user-select: none;
513 | user-select: none;
514 | }
515 |
516 | #underchat {
517 | padding-top: 5px;
518 | }
519 |
520 | #underchat label {
521 | margin-top: 0.5em;
522 | }
523 |
524 | #underchat div {
525 | margin-right: 310px;
526 | }
527 |
528 | #message {
529 | width: 100%;
530 | }
531 |
532 | .service {
533 | color: green;
534 | }
535 |
536 | .error {
537 | color: red;
538 | }
539 |
540 | .messageBox{
541 | display: flex;
542 | margin: 0.2rem 0 0.2rem 0;
543 | background-color: #222222;
544 | }
545 |
546 | .chatUserImg{
547 | height: 45px;
548 | background-color: gray;
549 | min-width: 45px;
550 | display: -webkit-inline-box;
551 | background-size: cover;
552 | opacity: 0.8;
553 | border-top-left-radius: 10px;
554 | }
555 | .chatUserImg:hover{
556 | opacity: 1;
557 | }
558 | .messageTime{
559 | opacity: 0.2;
560 | font-size: 0.8em;
561 | vertical-align: top;
562 | }
563 |
564 | .messageBox:hover .messageTime{
565 | opacity: 1;
566 | }
567 |
568 | .chatMessage {
569 | padding: 5px;
570 | }
571 |
572 | .bigThumbnail{
573 | height: 100px;
574 | min-width: 100px;
575 | }
576 |
577 | .displayName{
578 | font-size: 0.8em;
579 | cursor: pointer;
580 | vertical-align: top;
581 | }
582 |
583 | .hidename{
584 | display: none;
585 | }
586 |
587 | /* EMOJI */
588 | span.emoji {
589 | display: inline-block;
590 | width: 1.5em;
591 | height: 1.5em;
592 | background-size: contain;
593 | }
594 |
595 | span.emoji-sizer {
596 | margin: -2px 0;
597 | }
598 |
599 | span.emoji-outer {
600 | display: inline-block;
601 | height: 1.5em;
602 | width: 1.5em;
603 | }
604 |
605 | span.emoji-inner {
606 | display: inline-block;
607 | width: 100%;
608 | height: 100%;
609 | vertical-align: baseline;
610 | }
611 |
612 | img.emoji {
613 | width: 1.5em;
614 | height: 1.5em;
615 | }
616 |
617 | /* USER */
618 | img.avatar {
619 | border: none;
620 | background: url("/images/default_avatar.png");
621 | border-radius: 100%;
622 | }
623 |
624 | #People .username {
625 | font-size: 17px;
626 | }
627 |
628 | #followers {
629 | width: 50%;
630 | float: right;
631 | }
632 |
633 | #following {
634 | width: 48%;
635 | float: left;
636 | }
637 |
638 | .twitterlink:hover g, .periscopelink:hover .tofill {
639 | fill: #59adeb;
640 | }
641 |
642 | .featured {
643 | padding: 3px;
644 | margin-left: 10px;
645 | border-radius: 3px;
646 | color: white;
647 | }
648 |
649 | .card .userdescription {
650 | overflow: hidden;
651 | max-height: 2.8em;
652 | text-overflow: ellipsis;
653 | }
654 |
655 | .right {
656 | float: right;
657 | }
658 |
659 | .contextmenu {
660 | position: absolute;
661 | padding: 0;
662 | background: #202020;
663 | box-shadow: 0px 2px 20px -1px rgba(0,0,0,0.75);
664 | }
665 |
666 | .contextmenu div {
667 | padding: 5px;
668 | cursor: pointer;
669 | }
670 |
671 | .spoiler-content-visible {
672 | padding: 5px;
673 | background: #1A1A1A;
674 | height: auto !important;
675 | }
676 |
677 | /* Split.js */
678 | .gutter {
679 | background-color: #eee;
680 | background-repeat: no-repeat;
681 | background-position: 50%;
682 | }
683 |
684 | .gutter.gutter-horizontal {
685 | background-image: url("/images/vertical.png");
686 | cursor: ew-resize;
687 | }
688 |
689 | .split, .gutter.gutter-horizontal {
690 | height: 100%;
691 | float: left;
692 | }
693 |
694 | .split {
695 | -webkit-box-sizing: border-box;
696 | -moz-box-sizing: border-box;
697 | box-sizing: border-box;
698 | overflow-y: auto;
699 | overflow-x: hidden;
700 | }
701 |
702 | img.lock {
703 | height: 14px;
704 | width: 14px;
705 | margin-left: -30px;
706 | min-width: 14px;
707 | margin-top: 100px;
708 | border: none;
709 | -webkit-filter: drop-shadow(1px 1px 1px rgba(0,0,0,1));
710 | filter: drop-shadow(0px 1px 1px rgba(0,0,0,1));
711 | }
712 |
713 | .bullets {
714 | background-image: url("/images/bullets-black.png");
715 | width: 14px;
716 | height: 14px;
717 | display: block;
718 | margin: 11px;
719 | }
720 |
721 | .searcher {
722 | background: white;
723 | padding: 0 10px;
724 | }
725 |
726 | .searcher input, .searcher input:focus {
727 | border: none;
728 | box-shadow: none;
729 | margin: 0;
730 | }
731 |
732 | input[type="number"] {
733 | background-color: #2A2A2A;
734 | color: #B8B8B8;
735 | }
736 |
737 | input[type="number"]:focus {
738 | outline: #7F99EA auto 5px;
739 | }
740 |
741 | select {
742 | background-color: #2A2A2A;
743 | color: #B8B8B8;
744 | }
745 |
746 | select:focus {
747 | outline: #7F99EA auto 5px;
748 | }
749 |
750 | .button.activated {
751 | background-color: #7F99EA;
752 | }
753 |
754 | .button.activated:hover {
755 | background-color: rgba(127, 153, 234, 0.6);
756 | }
757 |
758 | span.sProducer {
759 | left: -6px;
760 | top: -8px;
761 | margin-left: -65px;
762 | position: relative;
763 | color: rgba(255, 255, 255, 0.6);
764 | background-color: rgba(0, 0, 0, 0.4);
765 | padding: 2px;
766 | }
767 |
768 | .newHighlight {
769 | background-color: #2F2F2F;
770 | box-shadow: 1.5px 0 0px 2px #838383;
771 | }
772 |
773 | .card.downloading {
774 | border-right: 10px solid #ED4D4D;
775 | min-height: 50px;
776 | }
777 |
778 | .card.finished {
779 | border-right: 10px solid #4350E9;
780 | min-height: 50px;
781 | }
782 |
783 | .linkLive {
784 | color:#ED4D4D;
785 | }
786 |
787 | .linkReplay {
788 | color: white;
789 | border-color: #4350E9
790 | }
791 |
792 | .linkPartialReplay {
793 | color:#4350E9;
794 | }
795 |
796 | .card.deletedBroadcast {
797 | background-color: rgb(20, 20, 20);
798 | -webkit-filter: grayscale(0.3);
799 | filter: grayscale(0.3);
800 | }
801 |
802 | #followingFilters label {
803 | background-color: #222222;
804 | margin: 4px;
805 | padding: 2px 10px 2px 2px;
806 | display: inline-block;
807 | }
808 |
809 | #followingFilters{
810 | background-color: #1A1A1A;
811 | Width:100%;
812 | border: 1px solid #343434
813 | }
814 |
815 | fieldset.languageFilter {
816 | border: 1px solid #343434;
817 | }
818 |
819 | #downloadFrom{
820 | display: flex;
821 | }
822 |
823 | #broadcastLink{
824 | width: 100%;
825 | }
826 |
827 | #optionsContainer{
828 | display: inline-table;
829 | }
830 |
831 | .spoiler:before {
832 | content: ' + \00a0\00a0';
833 | }
834 |
835 | .spoiler.spoiler-active:before {
836 | font-weight: bold;
837 | content: " -- \00a0\00a0";
838 | }
839 |
840 | .groupCard {
841 | font: 13px/1.3 "Helvetica Neue", Arial, Helvetica, sans-serif;
842 | min-height: 70px;
843 | margin: 0.2rem 0 0.2rem 0;
844 | background-color: #222222;
845 | border-radius: 2px;
846 | padding: 5px;
847 | }
848 |
849 | .groupNameTitle {
850 | opacity: 0.3;
851 | }
852 |
853 | .groupName {
854 | font-size: 1.2em;
855 | }
856 |
857 | span.lang.right > span {
858 | width: 2em;
859 | height: 2em;
860 | }
861 |
862 | .card.downloadCard {
863 | border-left: 0px;
864 | padding-left: 0px;
865 | }
866 |
867 | .focusedDownloadCard {
868 | background-color: #2F2F2F;
869 | }
870 |
871 | a.downloadGet, .downloadWhole{
872 | color: #039be5;
873 | font-weight: 700;
874 | }
875 |
876 | /* .card:not(.cardProfileImg) {
877 | height: 190px;
878 | } */
--------------------------------------------------------------------------------