2 Pitchfork Music Player Daemon Client
3 Copyright (C) 2007 Roger Bystrøm
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; version 2 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License along
15 with this program; if not, write to the Free Software Foundation, Inc.,
16 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 /* this file contains anything pertaining to playlist management */
21 var playlist_element = null;
24 function playlist_init() {
25 show_status_bar(LANG.WAIT_LOADING, true);
26 var http = new XMLHttpRequest();
27 http.open("GET", "command.php?ping");
28 remove_children(playlist_element);
30 var c = create_node("tr");
32 var tmp = create_node("td");
33 tmp.style.width = size + "px";
37 for(var i=1; i< pl_entries.length-1; i++) {
38 if(pl_entries[i]=="Track") {
46 c.className = "playlist";
49 http.onreadystatechange = function() {
50 if(http.readyState==4) {
52 if(!http.responseText||!(resp = evaluate_json(http.responseText))) {
53 show_status_bar(LANG.E_INVALID_RESPONSE);
56 else if(http.status==200) {
57 if(resp['connection']&&resp['connection']=="failed") {
58 show_status_bar(LANG.E_CONNECT);
59 throw(LANG.E_CONNECT);
62 /* for pagination to work properly we need to know some values in forehand */
65 if(typeof(s.song)!='undefined')
66 playing.pos = parseInt(s.song);
69 if(pagination_is_following())
70 playlist_scroll_to_playing();
74 setTimeout(need_update,10);
77 show_status_bar(LANG.E_INIT_PL);
85 function create_lazy_pl_row(info, id) {
87 var pos = parseInt(info["cpos"]);
88 tr = pl_entry_clone.cloneNode(true);
89 tr.firstChild.appendChild(create_txt(parseInt(pos)+1));
90 tr.setAttribute("plpos", pos);
95 function get_plid(node) {
96 return node.id.substring(7);
99 function get_plpos(node) {
100 return node.getAttribute("plpos");
103 function set_pl_position(node, new_pos) {
104 node.childNodes[0].childNodes[0].nodeValue = (new_pos+1);
105 node.setAttribute("plpos", new_pos);
108 function moved_plitem(stat) {
110 show_status_bar(LANG.E_PL_MOVE);
111 hide_status_bar(STATUS_DEFAULT_TIMEOUT);
115 function playlist_get_move_txt() {
117 return "Moving selection..";
120 /* a note on this... if to is null then it was put first in the list */
121 /* if move is null then it's a multi-move (which it always will be for now) */
122 function playlist_move(move, to) {
125 var range = get_pl_selection_range(false);
127 send_command("rangemove=" + encodeURIComponent(to) + "&elems=" + encodeURIComponent(range));
132 debug("trying to move to same, you should never see this");
136 var from = get_plid(move);
137 var topos = get_plpos(to);
138 if(from<0 || topos == null || topos == "" ) {
139 debug("Missing id on to or from!");
141 send_command("playlist=move&from=" + from + "&to=" + topos, moved_plitem);
144 /* cut's of elements at the end of playlist */
145 function playlist_resize(size) {
146 if(pagination.max>0&&pagination.pages>1) {
147 if(pagination.page==pagination.pages-1 && size%pagination.max!=0 ) {
148 remove_children_after(playlist_element, size%pagination.max);
152 remove_children_after(playlist_element, size);
156 function select_playing_song() {
157 var new_p = document.getElementById("plitem_" + playing.id);
158 if(playing.show_node) {
159 if(playing.show_node==new_p) // same node
161 apply_border_style_to_children(playing.show_node, "none");
162 playing.show_node.removeAttribute("playing");
163 playing.show_node = null;
166 apply_border_style_to_children(new_p, PLAYLIST_PLAYING_STYLE);
167 new_p.setAttribute("playing", "i");
168 playing.show_node = new_p;
173 /* not really lazy, but it needs a name */
174 function get_lazy_info(need_info) {
176 var start_id=-1, last_id=-1, id;
177 for(id in need_info) {
179 start_id = last_id = id;
181 else if(last_id==id-1) {
185 ids = add_range(ids, start_id, (start_id==last_id?false:last_id));
186 start_id = last_id = id;
189 ids = add_range(ids, start_id, (start_id==last_id?false:last_id));
192 need_info_arr = need_info;
193 /* get missing info and don't get plchanges */
194 send_command("playlist=info", get_lazy_info_cb, LANG.WAIT_UPDATING_PL, true, "ids=" + encodeURIComponent(ids), true);
198 function get_lazy_info_cb(res) {
201 var ple = pl_entries;
203 var need_info = need_info_arr;
204 need_info_arr = null;
205 if(need_info==null) {
206 //debug("get lazy info cb called, but no need_info array available");
207 /* FIXME: we have a potential race condition here
208 (may get several results, but only one of them is the one we need and or can use) */
213 show_status_bar(LANG.E_INVALID_RESULT);
214 hide_status_bar(STATUS_DEFAULT_TIMEOUT);
217 show_status_bar(LANG.WAIT_UPDATING_PL);
219 /* todo: hide when more than 500 changes */
220 var length = res.length;
221 var plength = ple.length;
222 for(var i=0; i<length; i++) {
224 var res_id = info.Id;
226 node = need_info[res_id];
229 for(var j=1; j<plength;j++) {
230 var val = info[ple[j]];
234 val = convert_minsec(val);
237 if(ple[j]=="Title") {
239 val = val.substring(val.lastIndexOf(DIR_SEPARATOR)+1);
246 node.childNodes[j].title = val;
247 val = val.substring(0,27) + "..";
249 else if(node.childNodes[j].title) {
250 node.childNodes[j].removeAttribute("title");
253 if(node.childNodes[j].hasChildNodes())
254 node.childNodes[j].childNodes[0].nodeValue = val;
255 else node.childNodes[j].appendChild(create_txt(val));
261 if(browser_is_konqueror()) {
262 playlist_element.className = "pltemp";
263 setTimeout(cclass_for_konqueror, 1);
268 function cclass_for_konqueror() {
269 playlist_element.className = "";
272 function pl_selection_changed(sel) {
273 if(sel!=last_pl_selected) {
274 last_pl_selected = sel;
275 document.getElementById('crop_items_button').title = sel?LANG.CROP_SELECTION:LANG.CLEAR_PLAYLIST;
279 function plchanges_handler3(list, size) {
280 if(!list||!list.length||list.length<=0)
282 var cont = playlist_element;
283 var df = create_fragment(); // temporary storage until we end
284 var buf = create_fragment(); // for appending
288 var pointer = null; // working point in playlist
289 var cursor = null; // temporary point in playlist to make get from doc more effective
290 var need_info = new Array();
293 /* returns the id of the next item in the list */
296 return "plitem_" + list[i+1]["Id"];
299 /* checks if it is in cache */
300 function _get_from_df(id) {
301 var cf = df.childNodes;
302 for (var j=0; j<cf.length; j++) {
303 if(cf[j].id&&cf[j].id==id) {
304 return df.removeChild(cf[j]);
310 function _get_from_doc(id) {
311 /* document.getElementById seems slow on large lists,
312 and there are a few assumptions we can make.. */
314 cursor = pointer.nextSibling;
315 var start_point = cursor;
318 if(cursor.id == id) {
320 cursor = cursor.nextSibling;
321 return cont.removeChild(ret);
323 cursor = cursor.nextSibling;
324 if(!cursor&&start_point) {
325 if(start_point!=pointer.nextSibling) {
326 stop_at = start_point;
328 cursor = pointer.nextSibling;
331 if(stop_at == cursor)
338 function _create_row(id) {
339 /* wow, what a mess.... */
340 n = create_lazy_pl_row(list[i], "plitem_" + id);
345 function _update_current_row(id) {
346 pointer.id = "plitem_" + id;
347 need_info[id] = pointer;
350 function _get_from_df_or_create(id) {
351 var n = _get_from_df("plitem_" + id);
352 if(!n) n = _create_row(id);
356 var changes_length = list.length;
357 var changes_start = parseInt(list[0]["cpos"]);
358 var changes_end = parseInt(list[list.length-1]['cpos']);
359 var pagination_start = pagination.max * pagination.page;
360 var pagination_end = pagination.max + pagination_start;
363 var pagination_switched_page = pagination.need_update;
365 if(pagination.max==0) {
366 start_pos = changes_start;
368 else if(changes_start<=pagination_start&&changes_end>=pagination_start) {
369 i = pagination_start - changes_start;
371 else if(changes_start<pagination_end) {
373 start_pos = changes_start - pagination_start;
375 else { // outside range
376 return; // let's hope we can just return...
379 if(start_pos<cont.childNodes.length&&start_pos>=0) {
380 pointer = cont.childNodes[start_pos];
382 else if(pointer==null) {
383 pointer = _create_row(list[0]["Id"]);
384 cont.appendChild(pointer);
387 if(start_pos==0) { // make sure there are no-one before us...
388 if(pointer.previousSibling) {
389 // todo obviously (if needed)
390 debug("let me know if you ever see this message (NTS, plchanges_handler)");
395 var length = list.length;
397 var max = pagination.max || Number.MAX_VALUE;
399 var _insert_before = insert_before;
400 var _replace_node = replace_node;
402 for(; i < length && start_pos++ < max; i++) {
405 n = _get_from_df_or_create(id)
409 plid = "plitem_" + id;
411 // if it's a page switch everything will have to be updated
412 if(pagination_switched_page) {
413 _update_current_row(id);
415 else if(pointer.id!=plid) {
417 n = _get_from_df(plid);
418 /* if n is found in df it has been removed, but wants back in */
419 if(n||nid==null||pointer.id==nid) {
421 n = _get_from_doc(plid);
424 _insert_before(n, pointer);
429 n = _get_from_doc(plid);
432 _replace_node(n,pointer);
433 df.appendChild(pointer);
438 if(pointer.nextSibling)
439 pointer = pointer.nextSibling;
444 if(buf.hasChildNodes())
445 cont.appendChild(buf);
446 if(need_info.length>0)
447 get_lazy_info(need_info);
449 playlist_resize(size);
450 update_node_positions2(cont);
451 select_playing_song();
453 // must be called last
454 if(pagination_switched_page)
455 pagination_post_pageswitch();
458 function update_node_positions2(container) {
459 var node = container.firstChild;
460 var found_diff = false;
461 var i = pagination.max * pagination.page;
464 set_pl_position(node, i++);
465 node = node.nextSibling;
468 if(get_plpos(node)==i) {
469 node = node.nextSibling;
480 function pagination_init() {
482 pagination.following = null; // set to true/false in pagination_is_following
484 /* these values shouldn't be hard-coded, but they are */
485 pagination.left = 50;
486 pagination.width = 680;
488 pagination.scroll_id = false;
489 pagination.scroll_left = false;
491 pagination.follow_button = document.getElementById("pagination_follow_current");
492 pagination.scroll_to_pos_after_switch = false;
493 pagination_update_follow();
496 /* should only be called when the size of the list has been changed and pages no longer fit*/
497 function pagination_update_list(size) {
498 if(pagination.max<=0)
500 var l = pagination.list;
501 var npages = Math.ceil(size/pagination.max); // number of pages
502 var cpages = pagination.pages; // current number of pages
503 var cpage = pagination.page;
505 while(npages>cpages) {
506 var n = add_li(l, cpages+1);
507 n.setAttribute("page", cpages++);
509 while(npages<cpages) {
510 remove_node(l.lastChild);
514 pagination.container.style.display = cpages>1?"block":"";
516 pagination.pages = cpages;
517 /* if we have switched page it needs update */
521 pagination_update_current_page(cpage);
524 /* c: new page number */
525 function pagination_update_current_page(c) {
526 if(pagination.max<=0)
528 var p = pagination.list.firstChild;
530 if(pagination.cpage&&pagination.cpage.hasAttribute("cpage"))
531 pagination.cpage.removeAttribute("cpage");
534 if(p.getAttribute("page")==c) {
536 p.setAttribute("cpage", "1");
541 pagination.cpage = p;
545 function pagination_fetch_current_page() {
546 playing.pl_version = -1;
547 pagination.need_update = true;
548 reschedule_update_now();
551 function pagination_change_page(e, page) {
552 if(e!=null&&e.target.hasAttribute("page")) {
553 pagination.page = parseInt(e.target.getAttribute("page"));
555 else if(typeof(page)!='undefined') {
556 pagination.page = page;
562 pagination_update_current_page(pagination.page);
563 pagination_fetch_current_page();
564 unselect_all_nodes(playlist_element);
567 function pagination_scroll_view(e) {
568 var x = e.pageX - pagination.left;
572 if(x>pagination.width-20) {
575 else if(x<20&&pagination.list.scrollLeft>0) {
580 if(!pagination.scroll_id&&!abort) {
581 pagination_scroll_real(left);
583 else if(pagination.scroll_id&&abort) {
584 pagination_scroll_stop();
590 function pagination_scroll_stop(e) {
592 if(e && e.relatedTarget && (e.relatedTarget.id=="pagination_list"||e.relatedTarget.parentNode.id=="pagination_list"||typeof(e.relatedTarget.page)!='undefined'))
595 if(pagination.scroll_id) {
596 clearTimeout(pagination.scroll_id);
597 pagination.scroll_id = false;
601 function pagination_is_following() {
602 if(pagination.following == null) {
603 var f = setting_get("follow_playing");
605 pagination.following = true;
606 else pagination.following = false;
608 return pagination.following;
611 function pagination_set_following(follow) {
612 setting_set("follow_playing", (follow?"1":"0"));
613 pagination.following = follow;
614 pagination_update_follow();
617 function pagination_toggle_following() {
618 pagination_set_following(!pagination_is_following());
621 function pagination_update_follow() {
622 if(!pagination.follow_button)
624 if(pagination_is_following()) {
625 pagination.follow_button.src = IMAGE.PAGINATION_FOLLOW;
626 pagination.follow_button.title = LANG.PAGINATION_FOLLOW;
629 pagination.follow_button.src = IMAGE.PAGINATION_NOFOLLOW;
630 pagination.follow_button.title =LANG.PAGINATION_NOFOLLOW;
635 function pagination_scroll_real(left) {
636 var l = pagination.list;
648 l.scrollLeft += adjust;
649 if(pagination.scroll_id)
650 clearTimeout(pagination.scroll_id);
651 pagination.scroll_id = setTimeout(pagination_scroll_real, 50, left);
652 }catch(e) {debug(e.message);}
655 function pagination_post_pageswitch() {
656 pagination.need_update = false;
657 if(pagination.scroll_to_pos_after_switch!=false) {
658 var n = playlist_scroll_to_pos_real(pagination.scroll_to_pos_after_switch);
659 pagination.scroll_to_pos_after_switch = false;
661 unselect_all_nodes(playlist_element);
665 else if(pagination_is_following()&&playing.show_node) {
666 playlist_scroll_to_pos_real(playing.pos);
671 /* Returns false if have to switch page, true otherwise */
672 function playlist_scroll_to_playing() {
673 return playlist_scroll_to_pos(playing.pos);
676 /* set select to true if all other nodes should be unselected and the right node selected */
677 function playlist_scroll_to_pos(pos, select) {
678 if(pagination.max>0) {
680 var page = Math.floor(pos/pagination.max);
681 if(page!=pagination.page) {
682 pagination_change_page(null, page);
684 pagination.scroll_to_pos_after_switch = pos;
685 return false; // selecting handled in pagination_post_pageswitch
688 var n = playlist_scroll_to_pos_real(pos);
690 unselect_all_nodes(playlist_element);
696 function playlist_scroll_to_pos_real(pos) {
697 if(pagination.max>0) {
698 pos = pos%pagination.max
703 else if(pos<playlist_element.childNodes.length) {
704 var n = playlist_element.childNodes[pos];
705 window.scrollTo(0,n.offsetTop -20);
708 else if(playlist_element.childNodes.length) {//if something in playlist, nag about it
709 debug("scroll to node request outside range");
714 function get_pl_selection_range(invert) {
715 var c = playlist_element.childNodes;
717 var pagination_add = invert&&pagination.max>0;
719 /* if we have pagination and not on page one, we need to add everything up until this page first */
720 if(pagination_add&&pagination.page>0) {
721 sel = add_range(sel, 0, pagination.max * pagination.page -1);
724 var tmp_start = null;
726 var length = c.length;
727 for(var i=0; i<length; i++) {
728 var selected = is_node_selected(c[i]);
730 selected = !selected;
734 else tmp_stop = c[i];
737 tmp_start = get_plpos(tmp_start);
739 tmp_stop = get_plpos(tmp_stop);
740 sel = add_range(sel, tmp_start, tmp_stop);
746 tmp_start = get_plpos(tmp_start);
747 // todo: what if not proper last node
749 tmp_stop = get_plpos(tmp_stop);
750 sel = add_range(sel, tmp_start, tmp_stop);
753 // add after this page
754 if(pagination_add&&pagination.page+1<pagination.pages) {
755 sel = add_range(sel, (pagination.page+1)*pagination.max, playing.pl_size-1);
760 function playlist_dblclick(elem, e) {
761 var id = get_plid(elem);
763 var cmd = "act=play&id=" + get_plid(elem);
768 function playlist_add_button(e) {
769 if(!playlist_add_popup) {
770 playlist_add_popup = playlist_add_create_content(
771 document.getElementById("playlist_add"));
774 playlist_add_popup.show();
777 function playlist_save_button(e) {
778 if(!playlist_save_popup) {
779 playlist_save_popup = playlist_save_create_content(
780 document.getElementById("playlist_save"));
783 playlist_save_popup.show();
786 /* these functions should be merged somehow */
787 function playlist_add_create_content(padd) {
788 var cats = new Array(LANG.BY_URL); //, "From file", "Text");
789 var c = create_fragment();
790 var ul = create_node("ul");
791 ul.className = "playlist_popup";
794 for(var i=0; i < cats.length; i++) {
795 li = add_li(ul, cats[i]);
796 li.className = "playlist_popup";
798 li = add_li(ul, LANG.CLOSE);
799 li.className = "playlist_popup";
800 add_listener(li, "click", playlist_add_close);
801 var d = create_node("div");
804 var tmp = create_node("input", "playlist_add_url");
805 add_listener(tmp, "keydown", playlist_add_by_url);
808 tmp = create_node("span");
810 tmp.className = "playlist_popup";
811 tmp.appendChild(create_txt(" Add"));
812 add_listener(tmp, "click", playlist_add_by_url);
814 var pop = new Popup(padd, c);
815 pop.popup.style.marginLeft = "-140px";
816 /* don't let the click get anywhere else either */
817 add_listener(pop.popup, "click", stop_event);
821 function playlist_save_create_content(psave) {
822 var c = create_fragment();
823 var tmp = create_node("p");
824 tmp.className = "nomargin";
825 tmp.appendChild(create_txt(LANG.PL_SAVE_AS));
826 var close = create_node("span");
827 add_listener(close, "click", playlist_save_close);
828 close.appendChild(create_txt(LANG.CLOSE));
829 close.className = "playlist_popup";
830 tmp.appendChild(close);
831 tmp.appendChild(create_node("br"));
833 tmp = create_node("input", "playlist_save_box");
834 add_listener(tmp, "keydown", playlist_save_listener);
836 tmp = create_node("span");
837 tmp.appendChild(create_txt(" " + LANG.SAVE));
838 tmp.className = "playlist_popup";
839 add_listener(tmp, "click", playlist_save_listener);
842 var pop = new Popup(psave, c);
843 pop.popup.style.marginLeft = "-140px";
844 /* don't let the click get anywhere else either */
845 add_listener(pop.popup, "click", stop_event);
849 function playlist_add_by_url(e) {
851 if((e.type=="keydown"&&e.keyCode==RETURN_KEY_CODE)||e.type=="click") {
853 var p = document.getElementById("playlist_add_url");
857 send_command("playlist_add_url=" + encodeURIComponent(url),
858 playlist_add_by_url_cb, LANG.WAIT_ADDING_PL);
860 playlist_add_close();
865 function playlist_save_listener(e) {
867 if((e.type=="keydown"&&e.keyCode==RETURN_KEY_CODE)||e.type=="click") {
869 var p = document.getElementById("playlist_save_box");
873 send_command("playlist_save=" + encodeURIComponent(name), playlist_save_cb, LANG.PL_SAVING);
875 playlist_save_popup.hide();
880 function playlist_add_by_url_cb(result) {
881 if(result=="failed") {
882 show_status_bar(LANG.E_FAILED_ADD_PL);
883 hide_status_bar(STATUS_DEFAULT_TIMEOUT);
889 function playlist_save_cb(result) {
890 if(result=="failed") {
891 show_status_bar(LANG.E_FAILED_SAVE_PL);
892 hide_status_bar(STATUS_DEFAULT_TIMEOUT);
896 function playlist_add_close(e) {
899 if(playlist_add_popup)
900 playlist_add_popup.hide();
902 function playlist_save_close(e) {
905 if(playlist_save_popup)
906 playlist_save_popup.hide();