]> git.donarmstrong.com Git - dactyl.git/blob - melodactyl/content/player.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / melodactyl / content / player.js
1 // Copyright (c) 2009 by Prathyush Thota <prathyushthota@gmail.com>
2 // Copyright (c) 2009 by Doug Kearns <dougkearns@gmail.com>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 const Player = Module("player", {
9     init: function init() {
10         this._lastSearchString = "";
11         this._lastSearchIndex = 0;
12         this._lastSearchView = this._currentView; //XXX
13
14         // Get the focus to the visible playlist first
15         //window._SBShowMainLibrary();
16
17         gMM.addListener(this._mediaCoreListener);
18     },
19
20     destroy: function destroy() {
21         gMM.removeListener(this._mediaCoreListener);
22     },
23
24     /**
25      * Moves the track position *interval* milliseconds forwards or backwards.
26      *
27      * @param {number} interval The time interval (ms) to move the track
28      *     position.
29      * @param {boolean} direction The direction in which to move the track
30      *     position, forward if true otherwise backwards.
31      * @private
32      */
33     _seek: function _seek(interval, direction) {
34         let position = gMM.playbackControl ? gMM.playbackControl.position : 0;
35         player.seekTo(position + (direction ? interval : -interval));
36     },
37
38     /**
39      * Listens for media core events and in response dispatches the appropriate
40      * autocommand events.
41      * @private
42      */
43     _mediaCoreListener: {
44         onMediacoreEvent: function (event) {
45             switch (event.type) {
46                 case Ci.sbIMediacoreEvent.BEFORE_TRACK_CHANGE:
47                     dactyl.log(_("player.preTrackChange", event.data));
48                     autocommands.trigger("TrackChangePre", { track: event.data });
49                     break;
50                 case Ci.sbIMediacoreEvent.TRACK_CHANGE:
51                     dactyl.log(_("player.trackChanged", event.data));
52                     autocommands.trigger("TrackChange", { track: event.data });
53                     break;
54                 case Ci.sbIMediacoreEvent.BEFORE_VIEW_CHANGE:
55                     dactyl.log(_("player.preViewChange", event.data));
56                     autocommands.trigger("ViewChangePre", { view: event.data });
57                     break;
58                 case Ci.sbIMediacoreEvent.VIEW_CHANGE:
59                     dactyl.log(_("player.viewChange", event.data));
60                     autocommands.trigger("ViewChange", { view: event.data });
61                     break;
62                 case Ci.sbIMediacoreEvent.STREAM_START:
63                     dactyl.log(_("player.trackStart", gMM.sequencer.currentItem));
64                     autocommands.trigger("StreamStart", { track: gMM.sequencer.currentItem });
65                     break;
66                 case Ci.sbIMediacoreEvent.STREAM_PAUSE:
67                     dactyl.log(_("player.trackPause", gMM.sequencer.currentItem));
68                     autocommands.trigger("StreamPause", { track: gMM.sequencer.currentItem });
69                     break;
70                 case Ci.sbIMediacoreEvent.STREAM_END:
71                     dactyl.log(_("player.trackEnd", gMM.sequencer.currentItem));
72                     autocommands.trigger("StreamEnd", { track: gMM.sequencer.currentItem });
73                     break;
74                 case Ci.sbIMediacoreEvent.STREAM_STOP:
75                     dactyl.log(_("player.trackStop", gMM.sequencer.currentItem));
76                     autocommands.trigger("StreamStop", { track: gMM.sequencer.currentItem });
77                     break;
78             }
79         }
80     },
81
82     /** @property {sbIMediaListView} The current media list view. @private */
83     get _currentView() SBGetBrowser().currentMediaListView,
84
85     /**
86      * @property {number} The player volume in the range 0.0-1.0.
87      */
88     get volume() gMM.volumeControl.volume,
89     set volume(value) {
90         gMM.volumeControl.volume = value;
91     },
92
93     /**
94      * Focuses the specified media item in the current media list view.
95      *
96      * @param {sbIMediaItem} mediaItem The media item to focus.
97      */
98     focusTrack: function focusTrack(mediaItem) {
99         SBGetBrowser().mediaTab.mediaPage.highlightItem(this._currentView.getIndexForItem(mediaItem));
100     },
101
102     /**
103      * Plays the currently selected media item. If no item is selected the
104      * first item in the current media view is played.
105      */
106     play: function play() {
107         // Check if there is any selection in place, else play first item of the visible view.
108         // TODO: this approach, or similar, should be generalised for all commands, PT? --djk
109         if (this._currentView.selection.count != 0)
110             gMM.sequencer.playView(this._currentView,
111                     this._currentView.getIndexForItem(this._currentView.selection.currentMediaItem));
112         else
113             gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0);
114
115         this.focusTrack(gMM.sequencer.currentItem);
116     },
117
118     /**
119      * Stops playback of the currently playing media item.
120      */
121     stop: function stop() {
122         gMM.sequencer.stop();
123     },
124
125     /**
126      * Plays the *count*th next media item in the current media view.
127      *
128      * @param {number} count
129      */
130     next: function next(count) {
131         for (let i = 0; i < count; i++)
132             gSongbirdWindowController.doCommand("cmd_control_next");
133         gSongbirdWindowController.doCommand("cmd_find_current_track");
134     },
135
136     /**
137      * Plays the *count*th previous media item in the current media view.
138      *
139      * @param {number} count
140      */
141     previous: function previous(count) {
142         for (let i = 0; i < count; i++)
143             gSongbirdWindowController.doCommand("cmd_control_previous");
144         gSongbirdWindowController.doCommand("cmd_find_current_track");
145     },
146
147     /**
148      * Toggles the play/pause status of the current media item.
149      */
150     togglePlayPause: function togglePlayPause() {
151         ["cmd_control_playpause", "cmd_find_current_track"].forEach(gSongbirdWindowController.doCommand);
152     },
153
154     /**
155      * Toggles the shuffle status of the sequencer.
156      */
157     toggleShuffle: function toggleShuffle() {
158         if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE)
159             gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE;
160         else
161             gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD;
162     },
163
164     // FIXME: not really toggling (depending on your definition) - good enough for now.
165     /**
166      * Toggles between the sequencer's three repeat modes: Repeat-One,
167      * Repeat-All and Repeat-None.
168      */
169     toggleRepeat: function toggleRepeat() {
170         switch (gMM.sequencer.repeatMode) {
171             case gMM.sequencer.MODE_REPEAT_NONE:
172                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ONE;
173                 break;
174             case gMM.sequencer.MODE_REPEAT_ONE:
175                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL;
176                 break;
177             case gMM.sequencer.MODE_REPEAT_ALL:
178                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
179                 break;
180             default:
181                 gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE;
182                 break;
183         }
184     },
185
186     /**
187      * Seeks forward *interval* milliseconds in the currently playing track.
188      *
189      * @param {number} interval The time interval (ms) to advance the
190      *     current track.
191      */
192     seekForward: function seekForward(interval) {
193         this._seek(interval, true);
194     },
195
196     /**
197      * Seeks backwards *interval* milliseconds in the currently playing track.
198      *
199      * @param {number} interval The time interval (ms) to rewind the
200      *     current track.
201      */
202     seekBackward: function seekBackward(interval) {
203         this._seek(interval, false);
204     },
205
206     /**
207      * Seeks to a specific position in the currently playing track.
208      *
209      * @param {number} The new position (ms) in the track.
210      */
211     seekTo: function seekTo(position) {
212         // FIXME: if not playing
213         if (!gMM.playbackControl)
214             this.play();
215
216         let min = 0;
217         let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable?
218
219         gMM.playbackControl.position = Math.constrain(position, min, max);
220     },
221
222     /**
223      * Increases the volume by 5% of the maximum volume.
224      */
225     increaseVolume: function increaseVolume() {
226         this.volume = Math.constrain(this.volume + 0.05, 0, 1);
227     },
228
229     /**
230      * Decreases the volume by 5% of the maximum volume.
231      */
232     decreaseVolume: function decreaseVolume() {
233         this.volume = Math.constrain(this.volume - 0.05, 0, 1);
234     },
235
236     // TODO: Document what this buys us over and above cmd_find_current_track
237     /**
238      * Focuses the currently playing track.
239      */
240     focusPlayingTrack: function focusPlayingTrack() {
241         this.focusTrack(gMM.sequencer.currentItem);
242     },
243
244     /**
245      * Searches the current media view for *str*
246      *
247      * @param {string} str The search string.
248      */
249     searchView: function searchView(str) {
250         let search = _getSearchString(this._currentView);
251         let searchString = "";
252
253         if (search != "") // XXX
254             searchString = str + " " + search;
255         else
256             searchString = str;
257
258         this._lastSearchString = searchString;
259
260         let searchView = LibraryUtils.createStandardMediaListView(this._currentView.mediaList, searchString);
261
262         if (searchView.length) {
263             this._lastSearchView = searchView;
264             this._lastSearchIndex = 0;
265             this.focusTrack(searchView.getItemByIndex(this._lastSearchIndex));
266         }
267         else
268             dactyl.echoerr(_("finder.notFound", searchString), commandline.FORCE_SINGLELINE);
269     },
270
271     /**
272      * Repeats the previous view search.
273      *
274      * @param {boolean} reverse Search in the reverse direction to the previous
275      *     search.
276      */
277     searchViewAgain: function searchViewAgain(reverse) {
278         function echo(str) {
279             this.timeout(function () {
280                 commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
281             }, 0);
282         }
283
284         if (reverse) {
285             if (this._lastSearchIndex == 0) {
286                 this._lastSearchIndex = this._lastSearchView.length - 1;
287                 echo(_("finder.atTop"));
288             }
289             else
290                 this._lastSearchIndex = this._lastSearchIndex - 1;
291         }
292         else {
293             if (this._lastSearchIndex == (this._lastSearchView.length - 1)) {
294                 this._lastSearchIndex = 0;
295                 echo(_("finder.atBottom"));
296             }
297             else
298                 this._lastSearchIndex = this._lastSearchIndex + 1;
299         }
300
301         // TODO: Implement for "?" --ken
302         commandline.echo("/" + this._lastSearchString, null, commandline.FORCE_SINGLELINE);
303         this.focusTrack(this._lastSearchView.getItemByIndex(this._lastSearchIndex));
304
305     },
306
307     /**
308      * The search dialog keypress callback.
309      *
310      * @param {string} str The contents of the search dialog.
311      */
312     onSearchKeyPress: function onSearchKeyPress(str) {
313         if (options["incsearch"])
314             this.searchView(str);
315     },
316
317     /**
318      * The search dialog submit callback.
319      *
320      * @param {string} str The contents of the search dialog.
321      */
322     onSearchSubmit: function onSearchSubmit(str) {
323         this.searchView(str);
324     },
325
326     /**
327      * The search dialog cancel callback.
328      */
329     onSearchCancel: function onSearchCancel() {
330         // TODO: restore the view state if altered by an 'incsearch' search
331     },
332
333     /**
334      * Returns an array of all available playlists.
335      *
336      * @returns {[sbIMediaList]}
337      */
338     getPlaylists: function getPlaylists() {
339         let mainLibrary = LibraryUtils.mainLibrary;
340         let playlists = [mainLibrary];
341         let listener = {
342             onEnumerationBegin: function () { },
343             onEnumerationEnd: function () { },
344             onEnumeratedItem: function (list, item) {
345                 // FIXME: why are there null items and duplicates?
346                 if (!playlists.some(function (list) list.name == item.name) && item.name != null)
347                     playlists.push(item);
348                 return Ci.sbIMediaListEnumerationListener.CONTINUE;
349             }
350         };
351
352         mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener);
353
354         return playlists;
355     },
356
357     /**
358      * Plays the media item at *index* in *playlist*.
359      *
360      * @param {sbIMediaList} playlist
361      * @param {number} index
362      */
363     playPlaylist: function playPlaylist(playlist, index) {
364         gMM.sequencer.playView(playlist.createView(), index);
365     },
366
367     /**
368      * Returns an array of all available media pages.
369      *
370      * @returns {[sbIMediaPageInfo]}
371      */
372     getMediaPages: function getMediaPages() {
373         let list = SBGetBrowser().currentMediaPage.mediaListView.mediaList;
374         let pages = services.mediaPageManager.getAvailablePages(list);
375         return ArrayConverter.JSArray(pages).map(function (page) page.QueryInterface(Ci.sbIMediaPageInfo));
376     },
377
378     /**
379      * Loads the specified media page into *view* with the given *list* of
380      * media items.
381      *
382      * @param {sbIMediaPage} page
383      * @param {sbIMediaList} list
384      * @param {sbIMediaView} view
385      */
386     loadMediaPage: function loadMediaPage(page, list, view) {
387         services.mediaPageManager.setPage(list, page);
388         SBGetBrowser().loadMediaList(list, null, null, view, null);
389     },
390
391     /**
392      * Applys the specified *rating* to *mediaItem*.
393      *
394      * @param {sbIMediaItem} mediaItem The media item to rate.
395      * @param {number} rating The star rating (1-5).
396      */
397     rateMediaItem: function rateMediaItem(mediaItem, rating) {
398         mediaItem.setProperty(SBProperties.rating, rating);
399     },
400
401     // TODO: add more fields, and generate the list dynamically. PT should the
402     // available fields reflect only the visible view fields or offer others? --djk
403     /**
404      * Sorts the current media view by *field* in the order specified by
405      * *ascending*.
406      *
407      * @param {string} field The sort field.
408      * @param {boolean} ascending If true sort in ascending order, otherwise in
409      *     descending order.
410      */
411     sortBy: function sortBy(field, ascending) {
412         let order = ascending ? "a" : "d";
413         let properties = services.MutablePropertyArray();
414         properties.strict = false;
415
416         switch (field) {
417             case "title":
418                 properties.appendProperty(SBProperties.trackName, order);
419                 break;
420             case "time":
421                 properties.appendProperty(SBProperties.duration, order);
422                 break;
423             case "artist":
424                 properties.appendProperty(SBProperties.artistName, order);
425                 break;
426             case "album":
427                 properties.appendProperty(SBProperties.albumName, order);
428                 break;
429             case "genre":
430                 properties.appendProperty(SBProperties.genre, order);
431                 break;
432             case "rating":
433                 properties.appendProperty(SBProperties.rating, order);
434                 break;
435             default:
436                 properties.appendProperty(SBProperties.trackName, order);
437                 break;
438         }
439
440         this._currentView.setSort(properties);
441     }
442 }, {
443 }, {
444     modes: function initModes(dactyl, modules, window) {
445         modes.addMode("SEARCH_VIEW", {
446             description: "Search View mode",
447             bases: [modes.COMMAND_LINE],
448         });
449         modes.addMode("SEARCH_VIEW_FORWARD", {
450             description: "Forward Search View mode",
451             bases: [modes.SEARCH_VIEW]
452         });
453         modes.addMode("SEARCH_VIEW_BACKWARD", {
454             description: "Backward Search View mode",
455             bases: [modes.SEARCH_VIEW]
456         });
457
458     },
459     commandline: function () {
460         player.CommandMode = Class("CommandSearchViewMode", modules.CommandMode, {
461             init: function init(mode) {
462                 this.mode = mode;
463                 init.supercall(this);
464             },
465
466             historyKey: "search-view",
467
468             get prompt() this.mode === modules.modes.SEARCH_VIEW_BACKWARD ? "?" : "/",
469
470             get onCancel() player.closure.onSearchCancel,
471             get onChange() player.closure.onSearchKeyPress,
472             get onSubmit() player.closure.onSearchSubmit
473         });
474     },
475     commands: function () {
476         commands.add(["f[ilter]"],
477                 "Filter tracks based on keywords {genre/artist/album/track}",
478                 function (args) {
479                     let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg);
480
481                     dactyl.assert(view.length, "No matching tracks");
482
483                     SBGetBrowser().loadMediaList(LibraryUtils.mainLibrary, null, null, view,
484                                                      "chrome://songbird/content/mediapages/filtersPage.xul");
485                     // TODO: make this player.focusTrack work ?
486                     player.focusTrack(view.getItemByIndex(0));
487                 },
488                 {
489                     argCount: "1",
490                     literal: 0
491                     //completer: function (context, args) completion.tracks(context, args);
492                 });
493
494         commands.add(["load"],
495             "Load a playlist",
496             function (args) {
497                 let arg = args.literalArg;
498
499                 if (arg) {
500                     // load the selected playlist/smart playlist
501                     for ([, playlist] in Iterator(player.getPlaylists())) {
502                         if (util.compareIgnoreCase(arg, playlist.name) == 0) {
503                             SBGetBrowser().loadMediaList(playlist);
504                             player.focusTrack(player._currentView.getItemByIndex(0));
505                             return;
506                         }
507                     }
508                     dactyl.echoerr(_("error.invalidArgument", arg));
509                 }
510                 else {
511                     // load main library if there are no args
512                     _SBShowMainLibrary();
513                 }
514             },
515             {
516                 argCount: "?",
517                 completer: function (context, args) completion.playlist(context),
518                 literal: 0
519             });
520
521         // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk
522         commands.add(["playerp[lay]"],
523             "Play track",
524             function () { player.play(); });
525
526         commands.add(["playerpa[use]"],
527             "Pause/unpause track",
528             function () { player.togglePlayPause(); });
529
530         commands.add(["playern[ext]"],
531             "Play next track",
532             function (args) { player.next(Math.max(args.count, 1)); },
533             { count: true });
534
535         commands.add(["playerpr[ev]"],
536             "Play previous track",
537             function (args) { player.previous(Math.max(args.count, 1)); },
538             { count: true });
539
540         commands.add(["players[top]"],
541             "Stop track",
542             function () { player.stop(); });
543
544         commands.add(["see[k]"],
545             "Seek to a track position",
546             function (args) {
547                 let arg = args[0];
548
549                 // intentionally supports 999:99:99
550                 dactyl.assert(/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg),
551                     _("error.invalidArgument", arg));
552
553                 function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m])
554
555                 if (/:/.test(arg)) {
556                     let [seconds, minutes, hours] = arg.split(":").reverse();
557                     hours = hours || 0;
558                     var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h");
559                 }
560                 else {
561                     if (!/[smh]/.test(arg.substr(-1)))
562                         arg += "s"; // default to seconds
563
564                     value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1));
565                 }
566
567                 if (/^[-+]/.test(arg))
568                     arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value);
569                 else
570                     player.seekTo(value);
571
572             },
573             { argCount: "1" });
574
575         commands.add(["mediav[iew]"],
576             "Change the current media view",
577             function (args) {
578                 // FIXME: is this a SB restriction? --djk
579                 dactyl.assert(SBGetBrowser().currentMediaPage,
580                     "Exxx: Can only set the media view from the media tab"); // XXX
581
582                 let arg = args[0];
583
584                 if (arg) {
585                     for ([, page] in Iterator(player.getMediaPages())) {
586                         if (util.compareIgnoreCase(arg, page.contentTitle) == 0) {
587                             player.loadMediaPage(page, SBGetBrowser().currentMediaListView.mediaList,
588                                     SBGetBrowser().currentMediaListView);
589                             return;
590                         }
591                     }
592                     dactyl.echoerr(_("error.invalidArgument", arg));
593                 }
594             },
595             {
596                 argCount: "1",
597                 completer: function (context) completion.mediaView(context),
598                 literal: 0
599             });
600
601         commands.add(["sort[view]"],
602                 "Sort the current media view",
603                 function (args) {
604                     player.sortBy(args[0], args["-order"] == "up");
605                 },
606                 {
607                     argCount: "1",
608                     completer: function (context) completion.mediaListSort(context),
609                     options: [
610                         {
611                             names: ["-order", "-o"], type: CommandOption.STRING,
612                             default: "up",
613                             description: "Specify the sorting order of the given field",
614                             validator: function (arg) /^(up|down)$/.test(arg),
615                             completer: function () [["up", "Sort in ascending order"], ["down", "Sort in descending order"]]
616                         }
617                     ]
618                 });
619
620         // FIXME: use :add -q like cmus? (not very vim-like are it's multi-option commands) --djk
621         commands.add(["qu[eue]"],
622             "Queue tracks by artist/album/track",
623             function (args) {
624                 let properties = services.MutablePropertyArray();
625
626                 // args
627                 switch (args.length) {
628                     case 3:
629                         properties.appendProperty(SBProperties.trackName, args[2]);
630                     case 2:
631                         properties.appendProperty(SBProperties.albumName, args[1]);
632                     case 1:
633                         properties.appendProperty(SBProperties.artistName, args[0]);
634                         break;
635                     default:
636                         break;
637                 }
638
639                 let library = LibraryUtils.mainLibrary;
640                 let mainView = library.createView();
641                 gMM.sequencer.playView(mainView,
642                         mainView.getIndexForItem(library.getItemsByProperties(properties).queryElementAt(0, Ci.sbIMediaItem)));
643                 player.focusPlayingTrack();
644             },
645             {
646                 argCount: "+",
647                 completer: function (context, args) {
648                     if (args.completeArg == 0)
649                         completion.artist(context);
650                     else if (args.completeArg == 1)
651                         completion.album(context, args[0]);
652                     else if (args.completeArg == 2)
653                         completion.song(context, args[0], args[1]);
654                 }
655             });
656
657         // TODO: maybe :vol! could toggle mute on/off? --djk
658         commands.add(["vol[ume]"],
659             "Set the volume",
660             function (args) {
661                 let arg = args[0];
662
663                 dactyl.assert(arg, _("error.argumentRequired"));
664                 dactyl.assert(/^[+-]?\d+$/.test(arg), _("error.trailingCharacters"));
665
666                 let level = parseInt(arg, 10) / 100;
667
668                 if (/^[+-]/.test(arg))
669                     level = player.volume + level;
670
671                 player.volume = Math.constrain(level, 0, 1);
672             },
673             { argCount: "1" });
674     },
675     completion: function () {
676         completion.album = function album(context, artist) {
677             context.title = ["Album"];
678             context.completions = [[v, ""] for ([, v] in Iterator(library.getAlbums(artist)))];
679         };
680
681         completion.artist = function artist(context) {
682             context.title = ["Artist"];
683             context.completions = [[v, ""] for ([, v] in Iterator(library.getArtists()))];
684         };
685
686         completion.playlist = function playlist(context) {
687             context.title = ["Playlist", "Type"];
688             context.keys = { text: "name", description: "type" };
689             context.completions = player.getPlaylists();
690         };
691
692         completion.mediaView = function mediaView(context) {
693             context.title = ["Media View", "URL"];
694             context.anchored = false;
695             context.keys = { text: "contentTitle", description: "contentUrl" };
696             context.completions = player.getMediaPages();
697         };
698
699         completion.mediaListSort = function mediaListSort(context) {
700             context.title = ["Media List Sort Field", "Description"];
701             context.anchored = false;
702             context.completions = [["title", "Track name"], ["time", "Duration"], ["artist", "Artist name"],
703                 ["album", "Album name"], ["genre", "Genre"], ["rating", "Rating"]]; // FIXME: generate this list dynamically - see #sortBy
704         };
705
706         completion.song = function album(context, artist, album) {
707             context.title = ["Song"];
708             context.completions = [[v, ""] for ([, v] in Iterator(library.getTracks(artist, album)))];
709         };
710     },
711     mappings: function () {
712         mappings.add([modes.PLAYER],
713             ["x"], "Play track",
714             function () { ex.playerplay(); });
715
716         mappings.add([modes.PLAYER],
717             ["z"], "Previous track",
718             function (args) { ex.playerprev({ "#": args.count }); },
719             { count: true });
720
721         mappings.add([modes.PLAYER],
722             ["c"], "Pause/unpause track",
723             function () { ex.playerpause(); });
724
725         mappings.add([modes.PLAYER],
726             ["b"], "Next track",
727             function (args) { ex.playernext({ "#": args.count }); },
728             { count: true });
729
730         mappings.add([modes.PLAYER],
731             ["v"], "Stop track",
732             function () { ex.playerstop(); });
733
734         mappings.add([modes.PLAYER],
735             ["Q"], "Queue tracks by artist/album/track",
736             function () { commandline.open(":", "queue ", modes.EX); });
737
738         mappings.add([modes.PLAYER],
739             ["f"], "Loads current view filtered by the keywords",
740             function () { commandline.open(":", "filter ", modes.EX); });
741
742         mappings.add([modes.PLAYER],
743             ["i"], "Select current track",
744             function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); });
745
746         mappings.add([modes.PLAYER],
747             ["s"], "Toggle shuffle",
748             function () { player.toggleShuffle(); });
749
750         mappings.add([modes.PLAYER],
751             ["r"], "Toggle repeat",
752             function () { player.toggleRepeat(); });
753
754         mappings.add([modes.PLAYER],
755             ["h", "<Left>"], "Seek -10s",
756             function (args) { player.seekBackward(Math.max(1, args.count) * 10000); },
757             { count: true });
758
759         mappings.add([modes.PLAYER],
760             ["l", "<Right>"], "Seek +10s",
761             function (args) { player.seekForward(Math.max(1, args.count) * 10000); },
762             { count: true });
763
764         mappings.add([modes.PLAYER],
765             ["H", "<S-Left>"], "Seek -1m",
766             function (args) { player.seekBackward(Math.max(1, args.count) * 60000); },
767             { count: true });
768
769         mappings.add([modes.PLAYER],
770             ["L", "<S-Right>"], "Seek +1m",
771             function (args) { player.seekForward(Math.max(1, args.count) * 60000); },
772             { count: true });
773
774         mappings.add([modes.PLAYER],
775              ["=", "+"], "Increase volume by 5% of the maximum",
776              function () { player.increaseVolume(); });
777
778         mappings.add([modes.PLAYER],
779              ["-"], "Decrease volume by 5% of the maximum",
780              function () { player.decreaseVolume(); });
781
782         mappings.add([modes.PLAYER],
783              ["/"], "Search forward for a track",
784              function () { player.CommandMode(modes.SEARCH_VIEW_FORWARD).open(); });
785
786         mappings.add([modes.PLAYER],
787              ["n"], "Find the next track",
788              function () { player.searchViewAgain(false); });
789
790         mappings.add([modes.PLAYER],
791              ["N"], "Find the previous track",
792              function () { player.searchViewAgain(true); });
793
794         for (let i in util.range(0, 6)) {
795             let (rating = i) {
796                 mappings.add([modes.PLAYER],
797                      ["<C-" + rating + ">"], "Rate the current media item " + rating,
798                      function () {
799                          let item = gMM.sequencer.currentItem || this._currentView.selection.currentMediaItem; // XXX: a bit too magic
800                          if (item)
801                              player.rateMediaItem(item, rating);
802                          else
803                              dactyl.beep();
804                      }
805                 );
806             };
807         }
808     },
809     options: function () {
810         options.add(["repeat"],
811             "Set the playback repeat mode",
812             "number", 0,
813             {
814                 setter: function (value) gMM.sequencer.repeatMode = value,
815                 getter: function () gMM.sequencer.repeatMode,
816                 completer: function (context) [
817                     ["0", "Repeat none"],
818                     ["1", "Repeat one"],
819                     ["2", "Repeat all"]
820                 ]
821             });
822
823         options.add(["shuffle"],
824             "Play tracks in shuffled order",
825             "boolean", false,
826             {
827                 setter: function (value) gMM.sequencer.mode = value ? gMM.sequencer.MODE_SHUFFLE : gMM.sequencer.MODE_FORWARD,
828                 getter: function () gMM.sequencer.mode == gMM.sequencer.MODE_SHUFFLE
829             });
830     }
831 });
832
833 // vim: set fdm=marker sw=4 ts=4 et: