1 /***************************************************************************
2 * Copyright (C) 2008-2014 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include <boost/bind.hpp>
22 #include <boost/filesystem.hpp>
23 #include <boost/locale/conversion.hpp>
33 #include "regex_filter.h"
34 #include "screen_switcher.h"
37 #include "statusbar.h"
38 #include "tag_editor.h"
41 #include "utility/comparators.h"
42 #include "utility/string.h"
43 #include "configuration.h"
45 using Global::MainHeight
;
46 using Global::MainStartY
;
47 using Global::myScreen
;
49 using MPD::itDirectory
;
51 using MPD::itPlaylist
;
53 namespace fs
= boost::filesystem
;
59 std::set
<std::string
> SupportedExtensions
;
60 bool hasSupportedExtension(const std::string
&file
);
62 std::string
ItemToString(const MPD::Item
&item
);
63 bool BrowserEntryMatcher(const boost::regex
&rx
, const MPD::Item
&item
, bool filter
);
67 Browser::Browser() : itsBrowseLocally(0), itsScrollBeginning(0), itsBrowsedDir("/")
69 w
= NC::Menu
<MPD::Item
>(0, MainStartY
, COLS
, MainHeight
, Config
.browser_display_mode
== DisplayMode::Columns
&& Config
.titles_visibility
? Display::Columns(COLS
) : "", Config
.main_color
, NC::Border::None
);
70 w
.setHighlightColor(Config
.main_highlight_color
);
71 w
.cyclicScrolling(Config
.use_cyclic_scrolling
);
72 w
.centeredCursor(Config
.centered_cursor
);
73 w
.setSelectedPrefix(Config
.selected_item_prefix
);
74 w
.setSelectedSuffix(Config
.selected_item_suffix
);
75 w
.setItemDisplayer(boost::bind(Display::Items
, _1
, proxySongList()));
78 void Browser::resize()
80 size_t x_offset
, width
;
81 getWindowResizeParams(x_offset
, width
);
82 w
.resize(width
, MainHeight
);
83 w
.moveTo(x_offset
, MainStartY
);
84 switch (Config
.browser_display_mode
)
86 case DisplayMode::Columns
:
87 if (Config
.titles_visibility
)
89 w
.setTitle(Display::Columns(w
.getWidth()));
92 case DisplayMode::Classic
:
99 void Browser::switchTo()
101 SwitchTo::execute(this);
104 GetDirectory(itsBrowsedDir
);
106 markSongsInPlaylist(proxySongList());
111 std::wstring
Browser::title()
113 std::wstring result
= L
"Browse: ";
114 result
+= Scroller(ToWString(itsBrowsedDir
), itsScrollBeginning
, COLS
-result
.length()-(Config
.design
== Design::Alternative
? 2 : Global::VolumeState
.length()));
118 void Browser::enterPressed()
123 const MPD::Item
&item
= w
.current().value();
128 if (isParentDirectory(item
))
129 GetDirectory(getParentDirectory(itsBrowsedDir
), itsBrowsedDir
);
131 GetDirectory(item
.name
, itsBrowsedDir
);
137 addSongToPlaylist(*item
.song
, true, -1);
143 Mpd
.GetPlaylistContentNoInfo(item
.name
, vectorMoveInserter(list
));
144 bool success
= addSongsToPlaylist(list
.begin(), list
.end(), true, -1);
145 Statusbar::printf("Playlist \"%1%\" loaded%2%",
146 item
.name
, withErrors(success
)
152 void Browser::spacePressed()
157 size_t i
= itsBrowsedDir
!= "/" ? 1 : 0;
158 if (Config
.space_selects
&& w
.choice() >= i
)
161 w
.at(i
).setSelected(!w
.at(i
).isSelected());
162 w
.scroll(NC::Scroll::Down
);
166 const MPD::Item
&item
= w
.current().value();
168 if (isParentDirectory(item
))
181 Statusbar::printf("Scanning directory \"%1%\"...", item
.name
);
182 myBrowser
->GetLocalDirectory(items
, item
.name
, 1);
183 list
.reserve(items
.size());
184 for (MPD::ItemList::const_iterator it
= items
.begin(); it
!= items
.end(); ++it
)
185 list
.push_back(*it
->song
);
186 success
= addSongsToPlaylist(list
.begin(), list
.end(), false, -1);
194 Statusbar::printf("Directory \"%1%\" added%2%",
195 item
.name
, withErrors(success
)
201 addSongToPlaylist(*item
.song
, false);
206 Mpd
.LoadPlaylist(item
.name
);
207 Statusbar::printf("Playlist \"%1%\" loaded", item
.name
);
211 w
.scroll(NC::Scroll::Down
);
214 void Browser::mouseButtonPressed(MEVENT me
)
216 if (w
.empty() || !w
.hasCoords(me
.x
, me
.y
) || size_t(me
.y
) >= w
.size())
218 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
221 switch (w
.current().value().type
)
224 if (me
.bstate
& BUTTON1_PRESSED
)
226 GetDirectory(w
.current().value().name
);
231 size_t pos
= w
.choice();
233 if (pos
< w
.size()-1)
234 w
.scroll(NC::Scroll::Up
);
239 if (me
.bstate
& BUTTON1_PRESSED
)
241 size_t pos
= w
.choice();
243 if (pos
< w
.size()-1)
244 w
.scroll(NC::Scroll::Up
);
252 Screen
<WindowType
>::mouseButtonPressed(me
);
255 /***********************************************************************/
257 bool Browser::allowsFiltering()
262 std::string
Browser::currentFilter()
264 return RegexFilter
<MPD::Item
>::currentFilter(w
);
267 void Browser::applyFilter(const std::string
&filter
)
272 w
.clearFilterResults();
278 auto fun
= boost::bind(BrowserEntryMatcher
, _1
, _2
, true);
279 auto rx
= RegexFilter
<MPD::Item
>(
280 boost::regex(filter
, Config
.regex_type
), fun
);
281 w
.filter(w
.begin(), w
.end(), rx
);
283 catch (boost::bad_expression
&) { }
286 /***********************************************************************/
288 bool Browser::allowsSearching()
293 bool Browser::search(const std::string
&constraint
)
295 if (constraint
.empty())
297 w
.clearSearchResults();
302 auto fun
= boost::bind(BrowserEntryMatcher
, _1
, _2
, false);
303 auto rx
= RegexFilter
<MPD::Item
>(
304 boost::regex(constraint
, Config
.regex_type
), fun
);
305 return w
.search(w
.begin(), w
.end(), rx
);
307 catch (boost::bad_expression
&)
313 void Browser::nextFound(bool wrap
)
318 void Browser::prevFound(bool wrap
)
323 /***********************************************************************/
325 ProxySongList
Browser::proxySongList()
327 return ProxySongList(w
, [](NC::Menu
<MPD::Item
>::Item
&item
) -> MPD::Song
* {
329 if (item
.value().type
== itSong
)
330 ptr
= item
.value().song
.get();
335 bool Browser::allowsSelection()
340 void Browser::reverseSelection()
342 reverseSelectionHelper(w
.begin()+(itsBrowsedDir
== "/" ? 0 : 1), w
.end());
345 MPD::SongList
Browser::getSelectedSongs()
347 MPD::SongList result
;
348 auto item_handler
= [this, &result
](const MPD::Item
&item
) {
349 if (item
.type
== itDirectory
)
355 GetLocalDirectory(list
, item
.name
, true);
356 for (auto it
= list
.begin(); it
!= list
.end(); ++it
)
357 result
.push_back(*it
->song
);
362 Mpd
.GetDirectoryRecursive(item
.name
, vectorMoveInserter(result
));
365 else if (item
.type
== itSong
)
366 result
.push_back(*item
.song
);
367 else if (item
.type
== itPlaylist
)
369 Mpd
.GetPlaylistContent(item
.name
, vectorMoveInserter(result
));
372 for (auto it
= w
.begin(); it
!= w
.end(); ++it
)
373 if (it
->isSelected())
374 item_handler(it
->value());
375 // if no item is selected, add current one
376 if (result
.empty() && !w
.empty())
377 item_handler(w
.current().value());
381 void Browser::fetchSupportedExtensions()
383 SupportedExtensions
.clear();
384 Mpd
.GetSupportedExtensions(SupportedExtensions
);
387 void Browser::LocateSong(const MPD::Song
&s
)
389 if (s
.getDirectory().empty())
392 itsBrowseLocally
= !s
.isFromDatabase();
394 if (myScreen
!= this)
397 if (itsBrowsedDir
!= s
.getDirectory())
398 GetDirectory(s
.getDirectory());
399 for (size_t i
= 0; i
< w
.size(); ++i
)
401 if (w
[i
].value().type
== itSong
&& s
== *w
[i
].value().song
)
410 void Browser::GetDirectory(std::string dir
, std::string subdir
)
415 int highlightme
= -1;
416 itsScrollBeginning
= 0;
417 if (itsBrowsedDir
!= dir
)
427 parent
.type
= itDirectory
;
434 GetLocalDirectory(list
, itsBrowsedDir
, false);
436 Mpd
.GetDirectory(dir
, vectorMoveInserter(list
));
438 list
= Mpd
.GetDirectory(dir
);
440 if (Config
.browser_sort_mode
!= SortMode::NoOp
&& !isLocal()) // local directory is already sorted
441 std::sort(list
.begin(), list
.end(),
442 LocaleBasedItemSorting(std::locale(), Config
.ignore_leading_the
, Config
.browser_sort_mode
)
445 for (MPD::ItemList::iterator it
= list
.begin(); it
!= list
.end(); ++it
)
456 if (it
->name
== subdir
)
457 highlightme
= w
.size();
463 w
.addItem(*it
, myPlaylist
->checkForSong(*it
->song
));
468 if (highlightme
>= 0)
469 w
.highlight(highlightme
);
473 void Browser::GetLocalDirectory(MPD::ItemList
&v
, const std::string
&directory
, bool recursively
) const
475 size_t start_size
= v
.size();
476 fs::path
dir(directory
);
477 std::for_each(fs::directory_iterator(dir
), fs::directory_iterator(), [&](fs::directory_entry
&e
) {
478 if (!Config
.local_browser_show_hidden_files
&& e
.path().filename().native()[0] == '.')
481 if (fs::is_directory(e
))
485 GetLocalDirectory(v
, e
.path().native(), true);
486 start_size
= v
.size();
490 item
.type
= itDirectory
;
491 item
.name
= e
.path().native();
495 else if (hasSupportedExtension(e
.path().native()))
498 mpd_pair file_pair
= { "file", e
.path().native().c_str() };
499 MPD::MutableSong
*s
= new MPD::MutableSong(mpd_song_begin(&file_pair
));
500 item
.song
= std::shared_ptr
<MPD::Song
>(s
);
501 # ifdef HAVE_TAGLIB_H
504 s
->setMTime(fs::last_write_time(e
.path()));
507 # endif // HAVE_TAGLIB_H
512 if (Config
.browser_sort_mode
!= SortMode::NoOp
)
513 std::sort(v
.begin()+start_size
, v
.end(),
514 LocaleBasedItemSorting(std::locale(), Config
.ignore_leading_the
, Config
.browser_sort_mode
)
518 void Browser::ClearDirectory(const std::string
&path
) const
521 std::for_each(fs::directory_iterator(dir
), fs::directory_iterator(), [&](fs::directory_entry
&e
) {
522 if (!fs::is_symlink(e
) && fs::is_directory(e
))
523 ClearDirectory(e
.path().native());
524 const char msg
[] = "Deleting \"%1%\"...";
525 Statusbar::printf(msg
, wideShorten(e
.path().native(), COLS
-const_strlen(msg
)));
526 fs::remove(e
.path());
530 void Browser::ChangeBrowseMode()
532 if (Mpd
.GetHostname()[0] != '/')
534 Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
538 itsBrowseLocally
= !itsBrowseLocally
;
539 Statusbar::printf("Browse mode: %1%",
540 itsBrowseLocally
? "local filesystem" : "MPD database"
542 if (itsBrowseLocally
)
545 expand_home(itsBrowsedDir
);
546 if (*itsBrowsedDir
.rbegin() == '/')
547 itsBrowsedDir
.resize(itsBrowsedDir
.length()-1);
552 GetDirectory(itsBrowsedDir
);
556 bool Browser::deleteItem(const MPD::Item
&item
, std::string
&errmsg
)
558 if (!Config
.allow_for_physical_item_deletion
)
559 FatalError("Browser::deleteItem invoked with allow_for_physical_item_deletion = false");
560 if (isParentDirectory((item
)))
561 FatalError("Parent directory passed to Browser::deleteItem");
563 // playlist created by mpd
564 if (!isLocal() && item
.type
== itPlaylist
&& CurrentDir() == "/")
568 Mpd
.DeletePlaylist(item
.name
);
571 catch (MPD::ServerError
&e
)
573 // if there is no such mpd playlist, we assume it's users's playlist.
574 if (e
.code() != MPD_SERVER_ERROR_NO_EXIST
)
581 path
= Config
.mpd_music_dir
;
582 path
+= item
.type
== itSong
? item
.song
->getURI() : item
.name
;
587 if (item
.type
== itDirectory
)
588 ClearDirectory(path
);
589 if (!boost::filesystem::exists(path
))
591 errmsg
= "No such item: " + path
;
596 boost::filesystem::remove(path
);
600 catch (boost::filesystem::filesystem_error
&err
)
611 bool hasSupportedExtension(const std::string
&file
)
613 size_t last_dot
= file
.rfind(".");
614 if (last_dot
> file
.length())
617 std::string ext
= boost::locale::to_lower(file
.substr(last_dot
+1));
618 return SupportedExtensions
.find(ext
) != SupportedExtensions
.end();
621 std::string
ItemToString(const MPD::Item
&item
)
626 case MPD::itDirectory
:
627 result
= "[" + getBasename(item
.name
) + "]";
630 switch (Config
.browser_display_mode
)
632 case DisplayMode::Classic
:
633 result
= item
.song
->toString(Config
.song_list_format_dollar_free
, Config
.tags_separator
);
635 case DisplayMode::Columns
:
636 result
= item
.song
->toString(Config
.song_in_columns_to_string_format
, Config
.tags_separator
);
640 case MPD::itPlaylist
:
641 result
= Config
.browser_playlist_prefix
.str() + getBasename(item
.name
);
647 bool BrowserEntryMatcher(const boost::regex
&rx
, const MPD::Item
&item
, bool filter
)
649 if (Browser::isParentDirectory(item
))
651 return boost::regex_search(ItemToString(item
), rx
);