[gnome-music] Enable playlists



commit 2609785e5074538d1fb2dab0f0d2cf5e5d791103
Author: Sai Suman Prayaga <suman sai14 gmail com>
Date:   Mon Feb 17 11:38:17 2014 +0100

    Enable playlists

 configure.ac                   |    1 +
 data/PlaylistControls.ui       |   95 ++++++++
 data/PlaylistDialog.ui.in      |   92 ++++++++
 data/SelectionToolbar.ui       |   16 ++
 data/application.css           |   17 ++
 data/gnome-music.gresource.xml |    2 +
 data/headerbar.ui.in           |    2 +
 gnomemusic/Makefile.am         |    3 +-
 gnomemusic/grilo.py            |   30 ++-
 gnomemusic/player.py           |    1 +
 gnomemusic/playlists.py        |  126 +++++++++++
 gnomemusic/query.py            |   21 ++
 gnomemusic/toolbar.py          |    5 +-
 gnomemusic/view.py             |  465 ++++++++++++++++++++++++++++++++++++++--
 gnomemusic/widgets.py          |   96 ++++++++-
 gnomemusic/window.py           |   95 ++++++++-
 po/POTFILES.in                 |    3 +-
 po/POTFILES.skip               |    1 +
 18 files changed, 1030 insertions(+), 41 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index bf07c41..27b806f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -44,6 +44,7 @@ AC_CONFIG_FILES([
   data/Makefile
   data/headerbar.ui
   data/AboutDialog.ui
+  data/PlaylistDialog.ui
   gnomemusic/Makefile
   po/Makefile.in
   libgd/Makefile
diff --git a/data/PlaylistControls.ui b/data/PlaylistControls.ui
new file mode 100644
index 0000000..a1882f4
--- /dev/null
+++ b/data/PlaylistControls.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.10 -->
+  <object class="GtkMenu" id="menu1">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkMenuItem" id="menuitem_play">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">_Play</property>
+        <property name="use_underline">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkMenuItem" id="menuitem_delete">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">_Delete</property>
+        <property name="use_underline">True</property>
+      </object>
+    </child>
+  </object>
+  <object class="GtkGrid" id="grid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_left">18</property>
+    <property name="margin_right">24</property>
+    <property name="margin_top">18</property>
+    <property name="margin_bottom">18</property>
+    <child>
+      <object class="GtkLabel" id="playlist_name">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="label">Playlist Name</property>
+        <property name="ellipsize">middle</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="songs_count">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label">3 Songs</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkMenuButton" id="playlist_menubutton">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="halign">end</property>
+        <property name="valign">center</property>
+        <property name="focus_on_click">False</property>
+        <property name="popup">menu1</property>
+        <style>
+          <class name="circle-button"/>
+        </style>
+        <child>
+          <object class="GtkImage" id="image1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">emblem-system-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/data/PlaylistDialog.ui.in b/data/PlaylistDialog.ui.in
new file mode 100644
index 0000000..d224c6d
--- /dev/null
+++ b/data/PlaylistDialog.ui.in
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.15.2 on Thu Aug 22 16:04:08 2013 -->
+<interface>
+  <!-- interface-requires gtk+ 3.10 -->
+  <object class="GtkDialog" id="dialog1">
+    <property name="width_request">400</property>
+    <property name="height_request">500</property>
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">dialog</property>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox1">
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">2</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack_type">end</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="scrolledwindow1">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkTreeView" id="treeview1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="model">liststore1</property>
+                <property name="headers_visible">False</property>
+                <property name="activate_on_single_click">True</property>
+                <child internal-child="selection">
+                  <object class="GtkTreeSelection" id="treeview-selection1"/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="@GTK_OR_GD HeaderBar" id="headerbar1">
+    <property name="title" translatable="yes">Select Playlist</property>
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkButton" id="cancel-button">
+        <property name="label" translatable="yes">Cancel</property>
+        <property name="visible">True</property>
+        <style>
+          <class name="text-button"/>
+        </style>
+      </object>
+      <packing>
+          <property name="pack-type">start</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="select-button">
+        <property name="label" translatable="yes">Select</property>
+        <property name="visible">True</property>
+        <style>
+          <class name="suggested-action"/>
+          <class name="text-button"/>
+        </style>
+      </object>
+      <packing>
+          <property name="pack-type">end</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkListStore" id="liststore1">
+    <columns>
+      <!-- column-name playlist-name -->
+      <column type="gchararray"/>
+      <!-- column-name editable -->
+      <column type="gboolean"/>
+    </columns>
+  </object>
+</interface>
diff --git a/data/SelectionToolbar.ui b/data/SelectionToolbar.ui
index bb59e08..569bc70 100644
--- a/data/SelectionToolbar.ui
+++ b/data/SelectionToolbar.ui
@@ -35,6 +35,22 @@
             <property name="position">0</property>
           </packing>
         </child>
+        <child>
+          <object class="GtkButton" id="button2">
+            <property name="label" translatable="yes">Remove from Playlist</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="text-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
       </object>
     </child>
   </object>
diff --git a/data/application.css b/data/application.css
index 361b339..48b06f3 100644
--- a/data/application.css
+++ b/data/application.css
@@ -31,8 +31,22 @@
     background-color: #77757A;
 }
 
+.playlist-controls-white{
+    background-color: #d7dad7;
+}
+.playlist-controls-white:selected{
+    background-color: #888A85;
+}
+.playlist-controls-dark{
+    background-color: #282528;
+}
+.playlist-controls-dark:selected{
+    background-color: #77757A;
+}
+
 .songs-list {
     box-shadow: inset 0 -1px shade(@borders, 1.30);
+    box-shadow: inset 0 1px shade(@borders, 1.30);
     background-color: @theme_bg_color;
 }
 
@@ -127,3 +141,6 @@
     color: mix (@theme_fg_color, @theme_bg_color, 0.50);    
 }
 
+.circle-button {
+    border-radius: 50%;
+}
diff --git a/data/gnome-music.gresource.xml b/data/gnome-music.gresource.xml
index 92a4560..b33c23d 100644
--- a/data/gnome-music.gresource.xml
+++ b/data/gnome-music.gresource.xml
@@ -12,5 +12,7 @@
     <file preprocess="xml-stripblanks">headerbar.ui</file>
     <file preprocess="xml-stripblanks">TrackWidget.ui</file>
     <file preprocess="xml-stripblanks">NoMusic.ui</file>
+    <file preprocess="xml-stripblanks">PlaylistControls.ui</file>
+    <file preprocess="xml-stripblanks">PlaylistDialog.ui</file>
   </gresource>
 </gresources>
diff --git a/data/headerbar.ui.in b/data/headerbar.ui.in
index e60fc65..69a8d89 100644
--- a/data/headerbar.ui.in
+++ b/data/headerbar.ui.in
@@ -5,10 +5,12 @@
     <section>
       <item>
         <attribute name="label" translatable="yes">Select All</attribute>
+        <attribute name="action">win.selectAll</attribute>
         <attribute name="accel">&lt;Primary&gt;a</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">Select None</attribute>
+        <attribute name="action">win.selectNone</attribute>
       </item>
     </section>
   </menu>
diff --git a/gnomemusic/Makefile.am b/gnomemusic/Makefile.am
index c1c857a..6658fb7 100644
--- a/gnomemusic/Makefile.am
+++ b/gnomemusic/Makefile.am
@@ -9,7 +9,8 @@ app_PYTHON = \
        notification.py \
        toolbar.py \
        view.py \
-       grilo.py \
+       grilo.py  \
+       playlists.py\
        query.py \
        widgets.py \
        searchbar.py \
diff --git a/gnomemusic/grilo.py b/gnomemusic/grilo.py
index 16e79ce..cf852ac 100644
--- a/gnomemusic/grilo.py
+++ b/gnomemusic/grilo.py
@@ -50,23 +50,26 @@ class Grilo(GObject.GObject):
 
     def __init__(self):
         GObject.GObject.__init__(self)
-
+        self.playlist_path = GLib.build_filenamev([GLib.get_user_data_dir(),
+                                                  "gnome-music", "playlists"])
+        if not (GLib.file_test(self.playlist_path, GLib.FileTest.IS_DIR)):
+            GLib.mkdir_with_parents(self.playlist_path, int("0755", 8))
         self.options = Grl.OperationOptions()
         self.options.set_flags(Grl.ResolutionFlags.FULL |
                                Grl.ResolutionFlags.IDLE_RELAY)
 
-        self.registry = Grl.Registry.get_default()
-        try:
-            self.registry.load_all_plugins()
-        except GLib.GError:
-            print('Failed to load plugins.')
-
         self.sources = {}
         self.tracker = None
 
+        self.registry = Grl.Registry.get_default()
         self.registry.connect('source_added', self._on_source_added)
         self.registry.connect('source_removed', self._on_source_removed)
 
+        try:
+            self.registry.load_all_plugins()
+        except GLib.GError:
+            print('Failed to load plugins.')
+
     def _on_source_added(self, pluginRegistry, mediaSource):
         id = mediaSource.get_id()
         if id == 'grl-tracker-source':
@@ -103,7 +106,7 @@ class Grilo(GObject.GObject):
             options.set_count(count)
 
         def _callback(source, param, item, count, data, offset):
-            callback(source, param, item)
+            callback(source, param, item, count)
         self.tracker.query(query, self.METADATA_KEYS, options, _callback, None)
 
     def _search_callback(self):
@@ -121,6 +124,17 @@ class Grilo(GObject.GObject):
         query = Query.get_album_for_id(album_id)
         self.tracker.query(query, self.METADATA_THUMBNAIL_KEYS, options, _callback, None)
 
+    def get_media_from_uri(self, uri, callback):
+        options = self.options.copy()
+        query = Query.get_song_with_url(uri)
+
+        def _callback(source, param, item, count, data, error):
+            if not error:
+                callback(source, param, item)
+                return
+
+        self.tracker.query(query, self.METADATA_KEYS, options, _callback, None)
+
 Grl.init(None)
 
 grilo = Grilo()
diff --git a/gnomemusic/player.py b/gnomemusic/player.py
index 981ee52..ddb07b9 100644
--- a/gnomemusic/player.py
+++ b/gnomemusic/player.py
@@ -662,4 +662,5 @@ class SelectionToolbar():
         self._ui.add_from_resource('/org/gnome/Music/SelectionToolbar.ui')
         self.eventbox = self._ui.get_object('eventbox1')
         self._add_to_playlist_button = self._ui.get_object('button1')
+        self._remove_from_playlist_button = self._ui.get_object('button2')
         self.eventbox.set_visible(False)
diff --git a/gnomemusic/playlists.py b/gnomemusic/playlists.py
new file mode 100644
index 0000000..38e1277
--- /dev/null
+++ b/gnomemusic/playlists.py
@@ -0,0 +1,126 @@
+from gi.repository import TotemPlParser, Grl, GLib, Gio, GObject
+from gnomemusic.grilo import grilo
+
+import os
+
+
+class Playlists(GObject.GObject):
+    __gsignals__ = {
+        'playlist-created': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
+        'playlist-deleted': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
+        'song-added-to-playlist': (GObject.SIGNAL_RUN_FIRST, None, (str, Grl.Media)),
+        'song-removed-from-playlist': (GObject.SIGNAL_RUN_FIRST, None, (str, str)),
+    }
+    instance = None
+
+    @classmethod
+    def get_default(self):
+        if self.instance:
+            return self.instance
+        else:
+            self.instance = Playlists()
+        return self.instance
+
+    def __init__(self):
+        GObject.GObject.__init__(self)
+        self.playlist_dir = os.path.join(GLib.get_user_data_dir(),
+                                         'gnome-music',
+                                         'playlists')
+
+    def create_playlist(self, name, iterlist=None):
+        parser = TotemPlParser.Parser()
+        playlist = TotemPlParser.Playlist()
+        pl_file = Gio.file_new_for_path(self.get_path_to_playlist(name))
+        if iterlist is not None:
+            for _iter in iterlist:
+                pass
+        else:
+            _iter = playlist.append()
+        parser.save(playlist, pl_file, name, TotemPlParser.ParserType.PLS)
+        self.emit('playlist-created', name)
+        return False
+
+    def get_playlists(self):
+        playlist_files = [pl_file for pl_file in os.listdir(self.playlist_dir)
+                          if os.path.isfile(os.path.join(self.playlist_dir,
+                                                         pl_file))]
+        playlist_names = []
+        for playlist_file in playlist_files:
+            name, ext = os.path.splitext(playlist_file)
+            if ext == '.pls':
+                playlist_names.append(name)
+        return playlist_names
+
+    def add_to_playlist(self, playlist_name, uris):
+        parser = TotemPlParser.Parser()
+        playlist = TotemPlParser.Playlist()
+        pl_file = Gio.file_new_for_path(self.get_path_to_playlist(playlist_name))
+
+        def parse_callback(parser, uri, metadata, data):
+            _iter = playlist.append()
+            playlist.set_value(_iter, TotemPlParser.PARSER_FIELD_URI, uri)
+
+        def end_callback(parser, uri, data):
+            for uri in uris:
+                _iter = playlist.append()
+                playlist.set_value(_iter, TotemPlParser.PARSER_FIELD_URI, uri)
+
+                def get_callback(source, param, item):
+                    self.emit('song-added-to-playlist', playlist_name, item)
+                grilo.get_media_from_uri(uri, get_callback)
+
+            parser.save(playlist, pl_file, playlist_name, TotemPlParser.ParserType.PLS)
+
+        parser.connect('entry-parsed', parse_callback, playlist)
+        parser.connect('playlist-ended', end_callback, playlist)
+        parser.parse_async(
+            GLib.filename_to_uri(self.get_path_to_playlist(playlist_name), None),
+            False, None, None, None
+        )
+
+    def remove_from_playlist(self, playlist_name, uris):
+        parser = TotemPlParser.Parser()
+        playlist = TotemPlParser.Playlist()
+        pl_file = Gio.file_new_for_path(self.get_path_to_playlist(playlist_name))
+
+        def parse_callback(parser, uri, metadata, data):
+            if uri in uris:
+                uris.remove(uri)
+                self.emit('song-removed-from-playlist', playlist_name, uri)
+            else:
+                _iter = playlist.append()
+                playlist.set_value(_iter, TotemPlParser.PARSER_FIELD_URI, uri)
+
+        def end_callback(parser, uri, data):
+            parser.save(playlist, pl_file, playlist_name, TotemPlParser.ParserType.PLS)
+
+        parser.connect('entry-parsed', parse_callback, playlist)
+        parser.connect('playlist-ended', end_callback, playlist)
+        parser.parse_async(
+            GLib.filename_to_uri(self.get_path_to_playlist(playlist_name), None),
+            False, None, None, None
+        )
+
+    def delete_playlist(self, playlist_name):
+        playlist_file = self.get_path_to_playlist(playlist_name)
+        if os.path.isfile(playlist_file):
+            os.remove(playlist_file)
+            self.emit('playlist-deleted', playlist_name)
+
+    def get_path_to_playlist(self, playlist_name):
+        return os.path.join(self.playlist_dir, "%s.pls" % playlist_name)
+
+    def parse_playlist(self, playlist_name, callback):
+        parser = TotemPlParser.Parser()
+        parser.connect('entry-parsed', self._on_entry_parsed, callback)
+        parser.parse_async(
+            GLib.filename_to_uri(self.get_path_to_playlist(playlist_name), None),
+            False, None, None, None
+        )
+
+    def _on_entry_parsed(self, parser, uri, metadata, data=None):
+        filename = GLib.filename_from_uri(uri)[0]
+        if filename and not os.path.isfile(filename):
+            return
+
+        grilo.get_media_from_uri(uri, data)
diff --git a/gnomemusic/query.py b/gnomemusic/query.py
index b350e22..7b7227e 100644
--- a/gnomemusic/query.py
+++ b/gnomemusic/query.py
@@ -275,3 +275,24 @@ class Query():
     }
     """.replace("\n", " ").strip() % {'album_id': album_id}
         return query
+
+    @staticmethod
+    def get_song_with_url(url):
+        query = '''
+    SELECT DISTINCT
+        rdf:type(?song)
+        tracker:id(?song) AS id
+        nie:url(?song) AS url
+        nie:title(?song) AS title
+        nmm:artistName(nmm:performer(?song)) AS artist
+        nie:title(nmm:musicAlbum(?song)) AS album
+        nfo:duration(?song) AS duration
+    WHERE {
+        ?song a nmm:MusicPiece .
+        FILTER (
+            nie:url(?song) = '%(url)s'
+        )
+    }
+    '''.replace('\n', ' ').strip() % {'url': url}
+
+        return query
diff --git a/gnomemusic/toolbar.py b/gnomemusic/toolbar.py
index 6b1d700..2890ae5 100644
--- a/gnomemusic/toolbar.py
+++ b/gnomemusic/toolbar.py
@@ -48,7 +48,8 @@ class ToolbarState:
 class Toolbar(GObject.GObject):
 
     __gsignals__ = {
-        'state-changed': (GObject.SIGNAL_RUN_FIRST, None, ())
+        'state-changed': (GObject.SIGNAL_RUN_FIRST, None, ()),
+        'selection-mode-changed': (GObject.SIGNAL_RUN_FIRST, None, ()),
     }
     _selectionMode = False
     _maximized = False
@@ -69,6 +70,7 @@ class Toolbar(GObject.GObject):
         self._close_button = self._ui.get_object('close-button')
         self._selection_menu = self._ui.get_object('selection-menu')
         self._selection_menu_button = self._ui.get_object('selection-menu-button')
+        self._selection_menu_label = self._ui.get_object('selection-menu-button-label')
         self._selection_menu_button.set_relief(Gtk.ReliefStyle.NONE)
         if Gtk.Widget.get_default_direction() is Gtk.TextDirection.RTL:
             _back_button_image = self._ui.get_object('back-button-image')
@@ -129,6 +131,7 @@ class Toolbar(GObject.GObject):
             self._select_button.set_active(False)
             self._select_button.show()
             self._cancel_button.hide()
+        self.emit('selection-mode-changed')
         self._update()
 
     def on_back_button_clicked(self, widget):
diff --git a/gnomemusic/view.py b/gnomemusic/view.py
index f7a2dab..d1e750f 100644
--- a/gnomemusic/view.py
+++ b/gnomemusic/view.py
@@ -40,12 +40,16 @@ from gi.repository import GLib
 from gi.repository import GdkPixbuf
 from gi.repository import Tracker
 from gi.repository import Gio
-from gettext import gettext as _
+
+from gettext import gettext as _, ngettext
 from gnomemusic.grilo import grilo
+from gnomemusic.toolbar import ToolbarState
 import gnomemusic.widgets as Widgets
+from gnomemusic.playlists import Playlists
 from gnomemusic.query import Query
 from gnomemusic.albumArtCache import AlbumArtCache as albumArtCache
 tracker = Tracker.SparqlConnection.get(None)
+playlists = Playlists.get_default()
 
 if Gtk.get_minor_version() > 8:
     from gi.repository.Gtk import Stack, StackTransitionType
@@ -63,10 +67,10 @@ class ViewContainer(Stack):
     countQuery = None
     filter = None
 
-    def __init__(self, title, header_bar, selection_toolbar, useStack=False):
+    def __init__(self, title, header_bar, selection_toolbar, use_sidebar=False, sidebar=None):
         Stack.__init__(self,
                        transition_type=StackTransitionType.CROSSFADE)
-        self._grid = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL)
+        self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
         self._iconWidth = -1
         self._iconHeight = 128
         self._offset = 0
@@ -96,16 +100,19 @@ class ViewContainer(Stack):
         self.selection_toolbar = selection_toolbar
         box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
         box.pack_start(self.view, True, True, 0)
-        if useStack:
+        if use_sidebar:
             self.stack = Stack(
                 transition_type=StackTransitionType.SLIDE_RIGHT,
             )
             dummy = Gtk.Frame(visible=False)
             self.stack.add_named(dummy, 'dummy')
-            self.stack.add_named(box, 'artists')
+            if sidebar:
+                self.stack.add_named(sidebar, 'sidebar')
+            else:
+                self.stack.add_named(box, 'sidebar')
             self.stack.set_visible_child_name('dummy')
             self._grid.add(self.stack)
-        else:
+        if not use_sidebar or sidebar:
             self._grid.add(box)
 
         self._cached_count = -1
@@ -136,6 +143,8 @@ class ViewContainer(Stack):
         grilo.connect('ready', self._on_grilo_ready)
         self.header_bar.header_bar.connect('state-changed',
                                            self._on_state_changed)
+        self.header_bar.connect('selection-mode-changed',
+                                self._on_selection_mode_changed)
         self.view.connect('view-selection-changed',
                           self._on_view_selection_changed)
 
@@ -151,7 +160,8 @@ class ViewContainer(Stack):
             self.view.set_selection_mode(True)
             self.header_bar.set_selection_mode(True)
             self.selection_toolbar.eventbox.set_visible(True)
-            self.selection_toolbar._add_to_playlist_button.sensitive = False
+            self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
+            self.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
         else:
             self.view.set_selection_mode(False)
             self.header_bar.set_selection_mode(False)
@@ -174,8 +184,15 @@ class ViewContainer(Stack):
 
     def _on_view_selection_changed(self, widget):
         items = self.view.get_selection()
-        self.selection_toolbar\
-            ._add_to_playlist_button.set_sensitive(len(items) > 0)
+        self.selection_toolbar._add_to_playlist_button.\
+            set_sensitive(len(items) > 0)
+        self.selection_toolbar._remove_from_playlist_button.\
+            set_sensitive(len(items) > 0)
+        if len(items) > 0:
+            self.header_bar._selection_menu_label.set_text(
+                ngettext(_("Selected %d item"), _("Selected %d items"), len(items)) % len(items))
+        else:
+            self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))
 
     def _populate(self, data=None):
         self._init = True
@@ -184,6 +201,9 @@ class ViewContainer(Stack):
     def _on_state_changed(self, widget, data=None):
         pass
 
+    def _on_selection_mode_changed(self, widget, data=None):
+        pass
+
     def _connect_view(self):
         self._adjustmentValueId = self.vadjustment.connect(
             'value-changed',
@@ -219,7 +239,7 @@ class ViewContainer(Stack):
         if error:
             self._model.set(_iter, [8, 10], [self.errorIconName, True])
 
-    def _add_item(self, source, param, item):
+    def _add_item(self, source, param, item, remaining):
         if not item:
             return
         self._offset += 1
@@ -280,6 +300,9 @@ class ViewContainer(Stack):
     def _on_selection_mode_request(self, *args):
         self.header_bar._select_button.clicked()
 
+    def get_selected_track_uris(self, callback):
+        callback([])
+
 
 #Class for the Empty View
 class Empty(Stack):
@@ -305,6 +328,9 @@ class Albums(ViewContainer):
         self._albumWidget = Widgets.AlbumWidget(player)
         self.player = player
         self.add(self._albumWidget)
+        self.albums_selected = []
+        self.items_selected = []
+        self.items_selected_callback = None
 
     def _back_button_clicked(self, widget, data=None):
         self.set_visible_child(self._grid)
@@ -327,6 +353,37 @@ class Albums(ViewContainer):
         if grilo.tracker:
             GLib.idle_add(grilo.populate_albums, self._offset, self._add_item)
 
+    def get_selected_track_uris(self, callback):
+        if self.header_bar._state == ToolbarState.SINGLE:
+            uris = []
+            for path in self._albumWidget.view.get_selection():
+                _iter = self._albumWidget.model.get_iter(path)
+                uris.append(self._albumWidget.model.get_value(_iter, 5).get_url())
+            callback(uris)
+        else:
+            self.items_selected = []
+            self.items_selected_callback = callback
+            self.albums_index = 0
+            self.albums_selected = [self.filter.get_value(self.filter.get_iter(path), 5)
+                                    for path in self.view.get_selection()]
+            if len(self.albums_selected):
+                self._get_selected_album_songs()
+
+    def _get_selected_album_songs(self):
+        grilo.populate_album_songs(
+            self.albums_selected[self.albums_index].get_id(),
+            self._add_selected_item)
+        self.albums_index += 1
+
+    def _add_selected_item(self, source, param, item, remaining):
+        if item:
+            self.items_selected.append(item.get_url())
+        if remaining == 0:
+            if self.albums_index < len(self.albums_selected):
+                self._get_selected_album_songs()
+            else:
+                self.items_selected_callback(self.items_selected)
+
 
 class Songs(ViewContainer):
     def __init__(self, header_bar, selection_toolbar, player):
@@ -367,7 +424,7 @@ class Songs(ViewContainer):
             self.iter_to_clean = child_iter.copy()
         return False
 
-    def _add_item(self, source, param, item):
+    def _add_item(self, source, param, item, remaining):
         if not item:
             return
         self._offset += 1
@@ -478,11 +535,9 @@ class Songs(ViewContainer):
         if grilo.tracker:
             GLib.idle_add(grilo.populate_songs, self._offset, self._add_item)
 
-
-class Playlist(ViewContainer):
-    def __init__(self, header_bar, selection_toolbar, player):
-        ViewContainer.__init__(self, _("Playlists"), header_bar,
-                               selection_toolbar)
+    def get_selected_track_uris(self, callback):
+        callback([self.filter.get_value(self.filter.get_iter(path), 5).get_url()
+                  for path in self.view.get_selection()])
 
 
 class Artists (ViewContainer):
@@ -492,6 +547,9 @@ class Artists (ViewContainer):
         self.artists_counter = 0
         self.player = player
         self._artists = {}
+        self.albums_selected = []
+        self.items_selected = []
+        self.items_selected_callback = None
         self.countQuery = Query.ARTISTS_COUNT
         self.artistAlbumsStack = Stack(
             transition_type=StackTransitionType.CROSSFADE,
@@ -500,15 +558,13 @@ class Artists (ViewContainer):
             shadow_type=Gtk.ShadowType.NONE,
             hexpand=True
         )
-        self.artistAlbumsStack.add_named(self._artistAlbumsWidget, "artists")
-        self.artistAlbumsStack.set_visible_child_name("artists")
+        self.artistAlbumsStack.add_named(self._artistAlbumsWidget, "sidebar")
+        self.artistAlbumsStack.set_visible_child_name("sidebar")
         self.view.set_view_type(Gd.MainViewType.LIST)
         self.view.set_hexpand(False)
         self.view.get_style_context().add_class('artist-panel')
         self.view.get_generic_view().get_selection().set_mode(
             Gtk.SelectionMode.SINGLE)
-        self._grid.attach(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL),
-                          1, 0, 1, 1)
         self._grid.attach(self.artistAlbumsStack, 2, 0, 2, 2)
         self._add_list_renderers()
         if (Gtk.Settings.get_default().get_property(
@@ -582,7 +638,7 @@ class Artists (ViewContainer):
         self._artistAlbumsWidget = self.new_artistAlbumsWidget
         GLib.idle_add(self.artistAlbumsStack.set_visible_child_name, child_name)
 
-    def _add_item(self, source, param, item):
+    def _add_item(self, source, param, item, remaining):
         if item is None:
             return
         self._offset += 1
@@ -613,3 +669,370 @@ class Artists (ViewContainer):
             if self._last_selection is not None:
                 self.view.get_generic_view().get_selection().select_iter(
                     self._last_selection)
+
+    def get_selected_track_uris(self, callback):
+        self.items_selected = []
+        self.items_selected_callback = callback
+        self.albums_index = 0
+        self.albums_selected = []
+
+        for path in self.view.get_selection():
+            _iter = self.filter.get_iter(path)
+            artist = self.filter.get_value(_iter, 2)
+            albums = self._artists[artist.lower()]['albums']
+            if (self.filter.get_string_from_iter(_iter) !=
+                    self.filter.get_string_from_iter(self._allIter)):
+                self.albums_selected.extend(albums)
+
+        if len(self.albums_selected):
+            self._get_selected_album_songs()
+
+    def _get_selected_album_songs(self):
+        grilo.populate_album_songs(
+            self.albums_selected[self.albums_index].get_id(),
+            self._add_selected_item)
+        self.albums_index += 1
+
+    def _add_selected_item(self, source, param, item, remaining):
+        if item:
+            self.items_selected.append(item.get_url())
+        if remaining == 0:
+            if self.albums_index < len(self.albums_selected):
+                self._get_selected_album_songs()
+            else:
+                self.items_selected_callback(self.items_selected)
+
+
+class Playlist(ViewContainer):
+    playlists_list = playlists.get_playlists()
+
+    def __init__(self, header_bar, selection_toolbar, player):
+        self.playlists_sidebar = Gd.MainView(
+            shadow_type=Gtk.ShadowType.NONE
+        )
+
+        ViewContainer.__init__(self, _("Playlists"), header_bar,
+                               selection_toolbar, True, self.playlists_sidebar)
+
+        self.view.set_view_type(Gd.MainViewType.LIST)
+        self.view.get_generic_view().get_style_context()\
+            .add_class('songs-list')
+        self._add_list_renderers()
+
+        builder = Gtk.Builder()
+        builder.add_from_resource('/org/gnome/Music/PlaylistControls.ui')
+        self.headerbar = builder.get_object('grid')
+        self.name_label = builder.get_object('playlist_name')
+        self.songs_count_label = builder.get_object('songs_count')
+        self.menubutton = builder.get_object('playlist_menubutton')
+        self.play_menuitem = builder.get_object('menuitem_play')
+        self.play_menuitem.connect('activate', self._on_play_activate)
+        self.delete_menuitem = builder.get_object('menuitem_delete')
+        self.delete_menuitem.connect('activate', self._on_delete_activate)
+        self._grid.insert_row(0)
+        self._grid.attach(self.headerbar, 1, 0, 1, 1)
+
+        self.playlists_model = Gtk.ListStore(
+            GObject.TYPE_STRING,
+            GObject.TYPE_STRING,
+            GObject.TYPE_STRING,
+            GObject.TYPE_STRING,
+            GdkPixbuf.Pixbuf,
+            GObject.TYPE_OBJECT,
+            GObject.TYPE_BOOLEAN,
+            GObject.TYPE_INT,
+            GObject.TYPE_STRING,
+            GObject.TYPE_BOOLEAN,
+            GObject.TYPE_BOOLEAN
+        )
+        self.playlists_sidebar.set_model(self.playlists_model)
+        self.playlists_sidebar.set_view_type(Gd.MainViewType.LIST)
+        self.playlists_sidebar.set_hexpand(False)
+        self.playlists_sidebar.get_style_context().add_class('artist-panel')
+        self.playlists_sidebar.get_generic_view().get_selection().set_mode(
+            Gtk.SelectionMode.SINGLE)
+        self.playlists_sidebar.connect('item-activated', self._on_playlist_activated)
+        self._grid.insert_column(0)
+        self._grid.child_set_property(self.stack, 'top-attach', 0)
+        self._grid.child_set_property(self.stack, 'height', 2)
+        self._add_sidebar_renderers()
+        if (Gtk.Settings.get_default().get_property(
+                'gtk_application_prefer_dark_theme')):
+            self.playlists_sidebar.get_generic_view().get_style_context().\
+                add_class("artist-panel-dark")
+        else:
+            self.playlists_sidebar.get_generic_view().get_style_context().\
+                add_class("artist-panel-white")
+
+        self.monitors = []
+        self.iter_to_clean = None
+        self.iter_to_clean_model = None
+        self.current_playlist = None
+        self.songs_count = 0
+        self._update_songs_count()
+        self.player = player
+        self.player.connect('playlist-item-changed', self.update_model)
+        playlists.connect('playlist-created', self._on_playlist_created)
+        playlists.connect('song-added-to-playlist', self._on_song_added_to_playlist)
+        playlists.connect('song-removed-from-playlist', self._on_song_removed_from_playlist)
+        self.show_all()
+
+    def _add_list_renderers(self):
+        list_widget = self.view.get_generic_view()
+        cols = list_widget.get_columns()
+        cells = cols[0].get_cells()
+        cells[2].set_visible(False)
+        now_playing_symbol_renderer = Gtk.CellRendererPixbuf(xalign=1.0)
+
+        column_now_playing = Gtk.TreeViewColumn()
+        column_now_playing.set_property('fixed_width', 24)
+        column_now_playing.pack_start(now_playing_symbol_renderer, False)
+        column_now_playing.add_attribute(now_playing_symbol_renderer,
+                                         'visible', 10)
+        column_now_playing.add_attribute(now_playing_symbol_renderer,
+                                         'icon_name', 8)
+        list_widget.insert_column(column_now_playing, 0)
+
+        title_renderer = Gtk.CellRendererText(
+            xpad=0,
+            xalign=0.0,
+            yalign=0.5,
+            height=48,
+            ellipsize=Pango.EllipsizeMode.END
+        )
+        list_widget.add_renderer(title_renderer,
+                                 self._on_list_widget_title_render, None)
+        cols[0].add_attribute(title_renderer, 'text', 2)
+
+        star_renderer = Gtk.CellRendererPixbuf(
+            xpad=32,
+            icon_name=self.starIconName
+        )
+        list_widget.add_renderer(star_renderer,
+                                 self._on_list_widget_star_render, None)
+        cols[0].add_attribute(star_renderer, 'visible', 9)
+
+        duration_renderer = Gd.StyledTextRenderer(
+            xpad=32,
+            xalign=1.0
+        )
+        duration_renderer.add_class('dim-label')
+        list_widget.add_renderer(duration_renderer,
+                                 self._on_list_widget_duration_render, None)
+
+        artist_renderer = Gd.StyledTextRenderer(
+            xpad=32,
+            ellipsize=Pango.EllipsizeMode.END
+        )
+        artist_renderer.add_class('dim-label')
+        list_widget.add_renderer(artist_renderer,
+                                 self._on_list_widget_artist_render, None)
+        cols[0].add_attribute(artist_renderer, 'text', 3)
+
+        type_renderer = Gd.StyledTextRenderer(
+            xpad=32,
+            ellipsize=Pango.EllipsizeMode.END
+        )
+        type_renderer.add_class('dim-label')
+        list_widget.add_renderer(type_renderer,
+                                 self._on_list_widget_type_render, None)
+
+    def _add_sidebar_renderers(self):
+        list_widget = self.playlists_sidebar.get_generic_view()
+
+        cols = list_widget.get_columns()
+        cells = cols[0].get_cells()
+        cells[1].set_visible(False)
+        cells[2].set_visible(False)
+        type_renderer = Gd.StyledTextRenderer(
+            xpad=16,
+            ypad=16,
+            ellipsize=Pango.EllipsizeMode.END,
+            xalign=0.0,
+            width=220
+        )
+        list_widget.add_renderer(type_renderer, lambda *args: None, None)
+        cols[0].clear_attributes(type_renderer)
+        cols[0].add_attribute(type_renderer, "text", 2)
+
+    def _on_list_widget_title_render(self, col, cell, model, _iter, data):
+        pass
+
+    def _on_list_widget_star_render(self, col, cell, model, _iter, data):
+        pass
+
+    def _on_list_widget_duration_render(self, col, cell, model, _iter, data):
+        item = model.get_value(_iter, 5)
+        if item:
+            seconds = item.get_duration()
+            minutes = seconds // 60
+            seconds %= 60
+            cell.set_property('text', '%i:%02i' % (minutes, seconds))
+
+    def _on_list_widget_artist_render(self, col, cell, model, _iter, data):
+        pass
+
+    def _on_list_widget_type_render(self, coll, cell, model, _iter, data):
+        item = model.get_value(_iter, 5)
+        if item:
+            cell.set_property('text', item.get_string(Grl.METADATA_KEY_ALBUM))
+
+    def _populate(self):
+        self._init = True
+        self.populate()
+
+    def update_model(self, player, playlist, currentIter):
+        if self.iter_to_clean:
+            self.iter_to_clean_model.set_value(self.iter_to_clean, 10, False)
+        if playlist != self.filter:
+            return False
+
+        child_iter = self.filter.convert_iter_to_child_iter(currentIter)
+        self._model.set_value(child_iter, 10, True)
+        if self._model.get_value(child_iter, 8) != self.errorIconName:
+            self.iter_to_clean = child_iter.copy()
+            self.iter_to_clean_model = self._model
+        return False
+
+    def _add_playlist_item(self, item):
+        _iter = self.playlists_model.append()
+        self.playlists_model.set(_iter, [2], [item])
+
+    def _on_item_activated(self, widget, id, path):
+        _iter = self.filter.get_iter(path)
+        child_iter = self.filter.convert_iter_to_child_iter(_iter)
+        if self._model.get_value(child_iter, 8) != self.errorIconName:
+            self.player.set_playlist('Playlist', self.current_playlist, self.filter, _iter, 5)
+            self.player.set_playing(True)
+
+    def _on_item_changed(self, monitor, file1, file2, event, _iter):
+        if self._model.iter_is_valid(_iter):
+            if event == Gio.FileMonitorEvent.DELETED:
+                self._model.set(_iter, [8, 10], [self.errorIconName, True])
+
+    def _on_playlist_activated(self, widget, item_id, path):
+        _iter = self.playlists_model.get_iter(path)
+        playlist = self.playlists_model.get_value(_iter, 2)
+        self.current_playlist = playlist
+        self.name_label.set_text(playlist)
+
+        # if the active queue has been set by this playlist,
+        # use it as model, otherwise build the liststore
+        cached_playlist = self.player.running_playlist('Playlist', playlist)
+        if cached_playlist:
+            self._model = cached_playlist.get_model()
+            self.filter = cached_playlist
+            currentTrack = self.player.playlist.get_iter(self.player.currentTrack.get_path())
+            self.update_model(self.player, cached_playlist,
+                              currentTrack)
+            self.view.set_model(self.filter)
+            self.songs_count = self._model.iter_n_children(None)
+            self._update_songs_count()
+        else:
+            self._model = Gtk.ListStore(
+                GObject.TYPE_STRING,
+                GObject.TYPE_STRING,
+                GObject.TYPE_STRING,
+                GObject.TYPE_STRING,
+                GdkPixbuf.Pixbuf,
+                GObject.TYPE_OBJECT,
+                GObject.TYPE_BOOLEAN,
+                GObject.TYPE_INT,
+                GObject.TYPE_STRING,
+                GObject.TYPE_BOOLEAN,
+                GObject.TYPE_BOOLEAN
+            )
+            self.filter = self._model.filter_new(None)
+            self.view.set_model(self.filter)
+            playlists.parse_playlist(playlist, self._add_item)
+            self.songs_count = 0
+            self._update_songs_count()
+
+    def _add_item(self, source, param, item):
+        self._add_item_to_model(item, self._model)
+
+    def _add_item_to_model(self, item, model):
+        if not item:
+            return
+        self._offset += 1
+        item.set_title(albumArtCache.get_media_title(item))
+        artist = item.get_string(Grl.METADATA_KEY_ARTIST)\
+            or item.get_author()\
+            or _("Unknown Artist")
+        _iter = model.insert_with_valuesv(
+            -1,
+            [2, 3, 5, 8, 9, 10],
+            [albumArtCache.get_media_title(item),
+             artist, item, self.nowPlayingIconName, False, False])
+        self.player.discover_item(item, self._on_discovered, _iter)
+        g_file = Gio.file_new_for_uri(item.get_url())
+        self.monitors.append(g_file.monitor_file(Gio.FileMonitorFlags.NONE,
+                                                 None))
+        self.monitors[(self._offset - 1)].connect('changed',
+                                                  self._on_item_changed, _iter)
+        self.songs_count += 1
+        self._update_songs_count()
+
+    def _update_songs_count(self):
+        self.songs_count_label.set_text(
+            ngettext(_("%d Song"), _("%d Songs"), self.songs_count)
+            % self.songs_count)
+
+    def _on_selection_mode_changed(self, widget, data=None):
+        self.playlists_sidebar.set_sensitive(not self.header_bar._selectionMode)
+        self.menubutton.set_sensitive(not self.header_bar._selectionMode)
+
+    def _on_play_activate(self, menuitem, data=None):
+        _iter = self._model.get_iter_first()
+        if not _iter:
+            return
+
+        self.view.get_generic_view().get_selection().\
+            select_path(self._model.get_path(_iter))
+        self.view.emit('item-activated', '0',
+                       self._model.get_path(_iter))
+
+    def _on_delete_activate(self, menuitem, data=None):
+        _iter = self.playlists_sidebar.get_generic_view().get_selection().get_selected()[1]
+        if not _iter:
+            return
+
+        playlist = self.playlists_model.get_value(_iter, 2)
+        playlists.delete_playlist(playlist)
+        self.playlists_model.remove(_iter)
+
+    def _on_playlist_created(self, playlists, name):
+        self._add_playlist_item(name)
+
+    def _on_song_added_to_playlist(self, playlists, name, item):
+        if name == self.current_playlist:
+            self._add_item_to_model(item, self._model)
+        else:
+            cached_playlist = self.player.running_playlist('Playlist', name)
+            if cached_playlist and cached_playlist != self._model:
+                self._add_item_to_model(item, cached_playlist)
+
+    def _on_song_removed_from_playlist(self, playlists, name, uri):
+        if name == self.current_playlist:
+            model = self._model
+        else:
+            cached_playlist = self.player.running_playlist('Playlist', name)
+            if cached_playlist and cached_playlist != self._model:
+                model = cached_playlist
+            else:
+                return
+
+        for row in model:
+            if row[5].get_url() == uri:
+                self._model.remove(row.iter)
+                self.songs_count -= 1
+                self._update_songs_count()
+                return
+
+    def populate(self):
+        for item in sorted(self.playlists_list):
+            self._add_playlist_item(item)
+
+    def get_selected_track_uris(self, callback):
+        callback([self.filter.get_value(self.filter.get_iter(path), 5).get_url()
+                  for path in self.view.get_selection()])
diff --git a/gnomemusic/widgets.py b/gnomemusic/widgets.py
index a8ab1d1..c56da6e 100644
--- a/gnomemusic/widgets.py
+++ b/gnomemusic/widgets.py
@@ -35,11 +35,13 @@ from gi.repository import Gtk, Gd, GLib, GObject, Pango
 from gi.repository import GdkPixbuf, Gio
 from gi.repository import Grl
 from gi.repository import Tracker
-from gettext import gettext as _
+from gettext import gettext as _, ngettext
 from gnomemusic.grilo import grilo
 from gnomemusic.query import Query
 from gnomemusic.albumArtCache import AlbumArtCache
+from gnomemusic.playlists import Playlists
 
+playlist = Playlists.get_default()
 tracker = Tracker.SparqlConnection.get(None)
 ALBUM_ART_CACHE = AlbumArtCache.get_default()
 if Gtk.Widget.get_default_direction() is not Gtk.TextDirection.RTL:
@@ -254,6 +256,11 @@ class AlbumWidget(Gtk.EventBox):
         items = self.view.get_selection()
         self.selection_toolbar\
             ._add_to_playlist_button.set_sensitive(len(items) > 0)
+        if len(items) > 0:
+            self.header_bar._selection_menu_label.set_text(
+                ngettext(_("Selected %d item"), _("Selected %d items"), len(items)) % len(items))
+        else:
+            self.header_bar._selection_menu_label.set_text(_("Click on items to select them"))
 
     def _on_header_cancel_button_clicked(self, button):
         self.view.set_selection_mode(False)
@@ -267,6 +274,7 @@ class AlbumWidget(Gtk.EventBox):
             self.player.eventBox.set_visible(False)
             self.selection_toolbar.eventbox.set_visible(True)
             self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
+            self.header_bar.header_bar.set_custom_title(self.header_bar._selection_menu_button)
         else:
             self.view.set_selection_mode(False)
             self.header_bar.set_selection_mode(False)
@@ -279,7 +287,7 @@ class AlbumWidget(Gtk.EventBox):
         if error:
             self.model.set(_iter, [7, 9], [ERROR_ICON_NAME, True])
 
-    def _on_populate_album_songs(self, source, prefs, track):
+    def _on_populate_album_songs(self, source, prefs, track, remaining):
         if track:
             self.tracks.append(track)
             self.duration = self.duration + track.get_duration()
@@ -492,7 +500,7 @@ class AllArtistsAlbums(ArtistAlbums):
             GLib.idle_add(grilo.populate_albums,
                           self._offset, self.add_item, 5)
 
-    def add_item(self, source, param, item):
+    def add_item(self, source, param, item, remaining):
         if item:
             self._offset += 1
             self.add_album(item)
@@ -537,7 +545,7 @@ class ArtistAlbumWidget(Gtk.HBox):
             song_widget.now_playing_sign.show()
             song_widget.can_be_played = False
 
-    def get_songs(self, source, prefs, track):
+    def get_songs(self, source, prefs, track, remaining):
         if track:
             self.tracks.append(track)
         else:
@@ -602,3 +610,83 @@ class ArtistAlbumWidget(Gtk.HBox):
         self.player.set_playlist('Artist', self.album,
                                  widget.model, widget._iter, 5)
         self.player.set_playing(True)
+
+
+class PlaylistDialog():
+    def __init__(self, parent):
+        self.ui = Gtk.Builder()
+        self.ui.add_from_resource('/org/gnome/Music/PlaylistDialog.ui')
+        self.dialog_box = self.ui.get_object('dialog1')
+        self.dialog_box.set_transient_for(parent)
+
+        self.view = self.ui.get_object('treeview1')
+        self.selection = self.ui.get_object('treeview-selection1')
+        self._add_list_renderers()
+        self.view.connect('row-activated', self._on_item_activated)
+
+        self.model = self.ui.get_object('liststore1')
+        playlist_names = playlist.get_playlists()
+        self.populate(playlist_names)
+
+        self.title_bar = self.ui.get_object('headerbar1')
+        if Gtk.get_minor_version() > 8:
+            self.dialog_box.set_titlebar(self.title_bar)
+        else:
+            self.dialog_box.get_content_area().add(self.title_bar)
+            self.dialog_box.get_content_area().reorder_child(self.title_bar, 0)
+
+        self._cancel_button = self.ui.get_object('cancel-button')
+        self._select_button = self.ui.get_object('select-button')
+        self._cancel_button.connect('clicked', self._on_cancel_button_clicked)
+        self._select_button.connect('clicked', self._on_selection)
+
+    def get_selected(self):
+        _iter = self.selection.get_selected()[1]
+
+        if not _iter or self.model[_iter][1]:
+            return None
+
+        return self.model[_iter][0]
+
+    def _add_list_renderers(self):
+        cols = Gtk.TreeViewColumn()
+        type_renderer = Gd.StyledTextRenderer(
+            xpad=16,
+            ypad=16,
+            ellipsize=Pango.EllipsizeMode.END,
+            xalign=0.0,
+            width=220
+        )
+        type_renderer.connect('editing-started', self._on_editing_started, None)
+        cols.pack_start(type_renderer, True)
+        cols.add_attribute(type_renderer, "text", 0)
+        cols.add_attribute(type_renderer, "editable", 1)
+        self.view.append_column(cols)
+
+    def populate(self, items):
+        for playlist_name in sorted(items):
+            self.model.append([playlist_name, False])
+        add_playlist_iter = self.model.append()
+        self.model.set(add_playlist_iter, [0, 1], [_("New Playlist"), True])
+
+    def _on_selection(self, select_button):
+        self.dialog_box.response(Gtk.ResponseType.ACCEPT)
+
+    def _on_cancel_button_clicked(self, cancel_button):
+        self.dialog_box.response(Gtk.ResponseType.REJECT)
+
+    def _on_item_activated(self, view, path, column):
+        _iter = self.model.get_iter(path)
+        if self.model.get_value(_iter, 1):
+            self.view.set_cursor(path, column, True)
+
+    def _on_editing_started(self, renderer, editable, path, data=None):
+        editable.set_text('')
+        editable.connect('editing-done', self._on_editing_done, None)
+
+    def _on_editing_done(self, editable, data=None):
+        _iter = self.selection.get_selected()[1]
+        if editable.get_text() != '':
+            playlist.create_playlist(editable.get_text())
+            new_iter = self.model.insert_before(_iter)
+            self.model.set(new_iter, [0, 1], [editable.get_text(), False])
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index fa4f372..c8d4337 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -31,13 +31,16 @@
 
 
 from gi.repository import Gtk, Gdk, Gio, GLib, Tracker
-from gettext import gettext as _
+from gettext import gettext as _, ngettext
 
 from gnomemusic.toolbar import Toolbar, ToolbarState
 from gnomemusic.player import Player, SelectionToolbar
 from gnomemusic.query import Query
 import gnomemusic.view as Views
+import gnomemusic.widgets as Widgets
+from gnomemusic.playlists import Playlists
 
+playlist = Playlists.get_default()
 tracker = Tracker.SparqlConnection.get(None)
 
 if Gtk.get_minor_version() > 8:
@@ -55,7 +58,12 @@ class Window(Gtk.ApplicationWindow):
         self.connect('focus-in-event', self._windows_focus_cb)
         self.settings = Gio.Settings.new('org.gnome.Music')
         self.add_action(self.settings.create_action('repeat'))
-
+        selectAll = Gio.SimpleAction.new('selectAll', None)
+        selectAll.connect('activate', self._on_select_all)
+        self.add_action(selectAll)
+        selectNone = Gio.SimpleAction.new('selectNone', None)
+        selectNone.connect('activate', self._on_select_none)
+        self.add_action(selectNone)
         self.set_size_request(887, 640)
 
         size_setting = self.settings.get_value('window-size')
@@ -154,7 +162,7 @@ class Window(Gtk.ApplicationWindow):
             self.views.append(Views.Albums(self.toolbar, self.selection_toolbar, self.player))
             self.views.append(Views.Artists(self.toolbar, self.selection_toolbar, self.player))
             self.views.append(Views.Songs(self.toolbar, self.selection_toolbar, self.player))
-            #self.views.append(Views.Playlist(self.toolbar, self.selection_toolbar, self.player))
+            self.views.append(Views.Playlist(self.toolbar, self.selection_toolbar, self.player))
 
             for i in self.views:
                 self._stack.add_titled(i, i.title, i.title)
@@ -175,6 +183,11 @@ class Window(Gtk.ApplicationWindow):
             self.toolbar._select_button.set_sensitive(False)
 
         self.toolbar._search_button.connect('toggled', self._on_search_toggled)
+        self.toolbar.connect('selection-mode-changed', self._on_selection_mode_changed)
+        self.selection_toolbar._add_to_playlist_button.connect(
+            'clicked', self._on_add_to_playlist_button_clicked)
+        self.selection_toolbar._remove_from_playlist_button.connect(
+            'clicked', self._on_remove_from_playlist_button_clicked)
 
         self.toolbar.set_state(ToolbarState.ALBUMS)
         self.toolbar.header_bar.show()
@@ -182,6 +195,38 @@ class Window(Gtk.ApplicationWindow):
         self._box.show()
         self.show()
 
+    def _on_select_all(self, action, param):
+        if self.toolbar._state != ToolbarState.SINGLE:
+            model = self._stack.get_visible_child()._model
+        else:
+            model = self._stack.get_visible_child()._albumWidget.model
+        _iter = model.get_iter_first()
+        count = 0
+        while _iter is not None:
+            model.set(_iter, [6], [True])
+            _iter = model.iter_next(_iter)
+            count = count + 1
+        if count > 0:
+            self.toolbar._selection_menu_label.set_text(
+                ngettext(_("Selected %d item"), _("Selected %d items"), count) % count)
+            self.selection_toolbar._add_to_playlist_button.set_sensitive(True)
+            self.selection_toolbar._remove_from_playlist_button.set_sensitive(True)
+        elif count == 0:
+            self.toolbar._selection_menu_label.set_text(_("Click on items to select them"))
+
+    def _on_select_none(self, action, param):
+        if self.toolbar._state != ToolbarState.SINGLE:
+            model = self._stack.get_visible_child()._model
+        else:
+            model = self._stack.get_visible_child()._albumWidget.model
+        _iter = model.get_iter_first()
+        self.selection_toolbar._add_to_playlist_button.set_sensitive(False)
+        self.selection_toolbar._remove_from_playlist_button.set_sensitive(False)
+        while _iter is not None:
+            model.set(_iter, [6], [False])
+            _iter = model.iter_next(_iter)
+        self.toolbar._selection_menu_label.set_text(_("Click on items to select them"))
+
     def _on_key_press(self, widget, event):
         modifiers = Gtk.accelerator_get_default_mod_mask()
 
@@ -202,9 +247,10 @@ class Window(Gtk.ApplicationWindow):
 
     def _on_notify_mode(self, stack, param):
         #Slide out artist list on switching to artists view
-        if stack.get_visible_child() == self.views[1]:
+        if stack.get_visible_child() == self.views[1] or \
+           stack.get_visible_child() == self.views[3]:
             stack.get_visible_child().stack.set_visible_child_name('dummy')
-            stack.get_visible_child().stack.set_visible_child_name('artists')
+            stack.get_visible_child().stack.set_visible_child_name('sidebar')
         self.toolbar.searchbar.show_bar(False)
 
     def _toggle_view(self, btn, i):
@@ -212,3 +258,42 @@ class Window(Gtk.ApplicationWindow):
 
     def _on_search_toggled(self, button, data=None):
         self.toolbar.searchbar.show_bar(button.get_active())
+
+    def _on_selection_mode_changed(self, widget, data=None):
+        if self.toolbar._selectionMode:
+            in_playlist = self._stack.get_visible_child() == self.views[3]
+            self.selection_toolbar._add_to_playlist_button.set_visible(not in_playlist)
+            self.selection_toolbar._remove_from_playlist_button.set_visible(in_playlist)
+
+    def _on_add_to_playlist_button_clicked(self, widget):
+        if self._stack.get_visible_child() == self.views[3]:
+            return
+
+        def callback(selected_uris):
+            if len(selected_uris) < 1:
+                return
+
+            add_to_playlist = Widgets.PlaylistDialog(self)
+            if add_to_playlist.dialog_box.run() == Gtk.ResponseType.ACCEPT:
+                playlist.add_to_playlist(
+                    add_to_playlist.get_selected(),
+                    selected_uris)
+            self.toolbar.set_selection_mode(False)
+            add_to_playlist.dialog_box.destroy()
+
+        self._stack.get_visible_child().get_selected_track_uris(callback)
+
+    def _on_remove_from_playlist_button_clicked(self, widget):
+        if self._stack.get_visible_child() != self.views[3]:
+            return
+
+        def callback(selected_uris):
+            if len(selected_uris) < 1:
+                return
+
+            playlist.remove_from_playlist(
+                self.views[3].current_playlist,
+                selected_uris)
+            self.toolbar.set_selection_mode(False)
+
+        self._stack.get_visible_child().get_selected_track_uris(callback)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2504b6e..5e7fea1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -25,4 +25,5 @@ gnomemusic/window.py
 [type: gettext/glade]data/NoMusic.ui
 [type: gettext/glade]data/headerbar.ui.in
 [type: gettext/glade]data/SelectionToolbar.ui
-
+[type: gettext/glade]data/PlaylistControls.ui
+[type: gettext/glade]data/PlaylistDialog.ui.in
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 0c10c9d..c4ce26f 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1,3 +1,4 @@
 data/headerbar.ui
 data/AboutDialog.ui
+data/PlaylistDialog.ui
 data/gnome-music.appdata.xml


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]