Merge remote-tracking branch 'upstream/master' into qt5

This commit is contained in:
Chocobozzz 2018-02-01 09:50:42 +01:00
commit 70f68b1926
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
130 changed files with 12552 additions and 11915 deletions

1
.gitignore vendored
View File

@ -45,3 +45,4 @@ dist/windows/Python27.zip
*.swp
src/translations/translations.pot
*.DS_Store
.vscode

40
.travis.yml Normal file
View File

@ -0,0 +1,40 @@
language: cpp
addons:
apt:
packages:
- cmake
- gettext
- libboost-dev
- libboost-serialization-dev
- libcdio-dev
- libchromaprint-dev
- libcrypto++-dev
- libechonest-dev
- libfftw3-dev
- libglew1.5-dev
- libgpod-dev
- libgstreamer1.0-dev
- libgstreamer-plugins-base1.0-dev
- liblastfm-dev
- libmtp-dev
- libplist-dev
- libprotobuf-dev
- libpulse-dev
- libqca2-dev
- libqca2-plugin-ossl
- libqjson-dev
- libqt4-dev
- libqt4-opengl-dev
- libqtwebkit-dev
- libsparsehash-dev
- libsqlite3-dev
- libtag1-dev
- libusbmuxd-dev
- protobuf-compiler
- qt4-dev-tools
script:
- cd bin
- cmake ..
- make

View File

@ -26,6 +26,8 @@ set(DISABLE_MILKDROP_PRESETS OFF)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++98")
pkg_check_modules(GLEW glew)
if(DISABLE_NATIVE_PRESETS)
ADD_DEFINITIONS(-DDISABLE_NATIVE_PRESETS)
endif(DISABLE_NATIVE_PRESETS)

View File

@ -22,6 +22,6 @@ FIND_PACKAGE(X11)
INCLUDE_DIRECTORIES(${projectM_SOURCE_DIR} ${X11_INCLUDE_DIR})
ADD_LIBRARY(Renderer STATIC ${Renderer_SOURCES})
SET_TARGET_PROPERTIES(Renderer PROPERTIES VERSION 2.00 SOVERSION 2)
TARGET_LINK_LIBRARIES(Renderer ${MATH_LIBRARIES} projectM)
TARGET_LINK_LIBRARIES(Renderer ${MATH_LIBRARIES} ${GLEW_LIBRARIES} projectM)
set_target_properties(Renderer PROPERTIES COMPILE_FLAGS "-Wno-parentheses")

View File

@ -1,7 +1,7 @@
Clementine
==========
Clementine is a modern music player and library organizer for Windows, Linux and Mac OS X.
Clementine is a modern music player and library organizer for Windows, Linux and macOS.
- Website: http://www.clementine-player.org/
- Github: https://github.com/clementine-player/Clementine

View File

@ -471,6 +471,7 @@
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-50.sql</file>
<file>schema/schema-51.sql</file>
<file>schema/schema-6.sql</file>
<file>schema/schema-7.sql</file>
<file>schema/schema-8.sql</file>

View File

@ -71,7 +71,7 @@
</exclude>
<invalidIndicator value="Couldn't find that page."/>
</provider>
<provider name="letras.mus.br" title="" charset="utf-8" url="http://letras.terra.com.br/winamp.php?musica={title}&amp;artista={artist}">
<provider name="letras.mus.br" title="" charset="utf-8" url="https://www.letras.mus.br/winamp.php?musica={title}&amp;artista={artist}">
<urlFormat replace="_@,;&amp;\/&quot;" with="_"/>
<urlFormat replace=" " with="+"/>
<extract>
@ -105,7 +105,7 @@
<urlFormat replace=" _@;\&quot;" with="_"/>
<urlFormat replace="?" with="%3F"/>
<extract>
<item begin="&lt;div class='lyricbox'&gt;" end="&lt;!--"/>
<item begin="&lt;div class='lyricbox'&gt;" end="&lt;div class='lyricsbreak'"/>
</extract>
<exclude>
<item tag="&lt;div class='rtMatcher'&gt;"/>

15
data/schema/schema-51.sql Normal file
View File

@ -0,0 +1,15 @@
DELETE FROM %allsongstables_fts;
DROP TABLE %allsongstables_fts;
CREATE VIRTUAL TABLE %allsongstables_fts USING fts3( ftstitle, ftsalbum, ftsartist, ftsalbumartist,
ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, ftsyear,
tokenize=unicode
);
INSERT INTO %allsongstables_fts ( ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist,
ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, ftsyear)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment, year
FROM %allsongstables;
UPDATE schema_version SET version=51;

View File

@ -2,6 +2,7 @@
Version=1.0
Type=Application
Name=Clementine
Name[hi]=क्लेमेंटैन्
Name[sr]=Клементина
Name[sr@ijekavian]=Клементина
Name[sr@ijekavianlatin]=Klementina
@ -9,6 +10,7 @@ Name[sr@latin]=Klementina
GenericName=Clementine Music Player
GenericName[ca]=Reproductor de música Clementine
GenericName[es]=Reproductor de música Clementine
GenericName[hi]=क्लेमेंटैन् संगीत वादक
GenericName[pl]=Odtwarzacz muzyki Clementine
GenericName[pt]=Reprodutor de músicas Clementine
GenericName[sl]=Predvajalnik glasbe Clementine
@ -56,6 +58,7 @@ Name[fr]=Lecture
Name[ga]=Seinn
Name[gl]=Reproducir
Name[he]=נגינה
Name[hi]=गाना बजाएं
Name[hr]=Pokreni reprodukciju
Name[hu]=Lejátszás
Name[it]=Riproduci
@ -102,6 +105,7 @@ Name[fi]=Keskeytä
Name[ga]=Cuir ar sos
Name[gl]=Pausa
Name[he]=השהייה
Name[hi]=गाना रोकें
Name[hr]=Pauza
Name[hu]=Szünet
Name[it]=Pausa
@ -148,6 +152,7 @@ Name[fi]=Pysäytä
Name[ga]=Stad
Name[gl]=Deter
Name[he]=הפסקה
Name[hi]=गाना बंद करे
Name[hr]=Zaustavi reprodukciju
Name[hu]=Leállít
Name[it]=Ferma
@ -197,6 +202,7 @@ Name[fr]=Arrêter la lecture après cette piste
Name[ga]=Stad i ndiaidh an rian seo
Name[gl]=Deter a reprodución despois da pista actual
Name[he]=הפסקה אחרי רצועה זו
Name[hi]=इस गाने के बाद बंद करे
Name[hr]=Zaustavi reprodukciju nakon ove pjesme
Name[hu]=Leállítás az aktuális szám után
Name[it]=Ferma dopo questa traccia
@ -215,8 +221,6 @@ Name[ru]=Остановить после этого трека
Name[sk]=Zastaviť po tejto skladbe
Name[sl]=Zaustavi po tej skladbi
Name[sr]=Заустави после ове нумере
Name[sr@ijekavian]=
Name[sr@ijekavianlatin]=
Name[sr@latin]=Zaustavi posle ove numere
Name[sv]=Stoppa efter detta spår
Name[tr]=Bu parçadan sonra durdur
@ -245,6 +249,7 @@ Name[fr]=Précédent
Name[ga]=Roimhe
Name[gl]=Anterior
Name[he]=הקודם
Name[hi]=पिछला गाना
Name[hr]=Prijašnje
Name[hu]=Előző
Name[it]=Precedente
@ -292,6 +297,7 @@ Name[fr]=Suivant
Name[ga]=Ar aghaidh
Name[gl]=Próximo
Name[he]=הבא
Name[hi]=अगला गाना
Name[hr]=Sljedeće
Name[hu]=Következő
Name[id]=Berikut

1
dist/macdeploy.py vendored
View File

@ -72,6 +72,7 @@ GSTREAMER_PLUGINS = [
'libgstvorbis.so',
'libgstwavpack.so',
'libgstwavparse.so',
'libgstxingmux.so',
# HTTP src support
'libgstsouphttpsrc.so',

View File

@ -35,6 +35,7 @@ add_library(libclementine-common STATIC
)
target_link_libraries(libclementine-common
${QT_LIBRARIES}
${TAGLIB_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
)

View File

@ -67,7 +67,8 @@ const char* CommandlineOptions::kHelpText =
" --quiet %29\n"
" --verbose %30\n"
" --log-levels <levels> %31\n"
" --version %32\n";
" --version %32\n"
" -x, --delete-current %33\n";
const char* CommandlineOptions::kVersionText = "Clementine %1";
@ -81,6 +82,7 @@ CommandlineOptions::CommandlineOptions(int argc, char** argv)
seek_to_(-1),
seek_by_(0),
play_track_at_(-1),
delete_current_track_(false),
show_osd_(false),
toggle_pretty_osd_(false),
log_levels_(logging::kDefaultLogLevels) {
@ -135,12 +137,13 @@ bool CommandlineOptions::Parse() {
{"verbose", no_argument, 0, Verbose},
{"log-levels", required_argument, 0, LogLevels},
{"version", no_argument, 0, Version},
{"delete-current", no_argument, 0, 'x'},
{0, 0, 0, 0}};
// Parse the arguments
bool ok = false;
forever {
int c = getopt_long(argc_, argv_, "hptusqrfv:c:alk:oyg:", kOptions, nullptr);
int c = getopt_long(argc_, argv_, "xhptusqrfv:c:alk:oyg:", kOptions, nullptr);
// End of the options
if (c == -1) break;
@ -158,8 +161,8 @@ bool CommandlineOptions::Parse() {
.arg(tr("Skip backwards in playlist"),
tr("Skip forwards in playlist"),
tr("Set the volume to <value> percent"),
tr("Increase the volume by 4%"),
tr("Decrease the volume by 4%"),
tr("Increase the volume by 4 percent"),
tr("Decrease the volume by 4 percent"),
tr("Increase the volume by <value> percent"),
tr("Decrease the volume by <value> percent"))
.arg(tr("Seek the currently playing track to an absolute "
@ -179,7 +182,8 @@ bool CommandlineOptions::Parse() {
tr("Equivalent to --log-levels *:1"),
tr("Equivalent to --log-levels *:3"),
tr("Comma separated list of class:level, level is 0-3"))
.arg(tr("Print out version information"));
.arg(tr("Print out version information"),
tr("Delete the currently playing song"));
std::cout << translated_help_text.toLocal8Bit().constData();
return false;
@ -280,6 +284,10 @@ bool CommandlineOptions::Parse() {
if (!ok) play_track_at_ = -1;
break;
case 'x':
delete_current_track_ = true;
break;
case '?':
default:
return false;
@ -303,7 +311,8 @@ bool CommandlineOptions::is_empty() const {
return player_action_ == Player_None && set_volume_ == -1 &&
volume_modifier_ == 0 && seek_to_ == -1 && seek_by_ == 0 &&
play_track_at_ == -1 && show_osd_ == false &&
toggle_pretty_osd_ == false && urls_.isEmpty();
toggle_pretty_osd_ == false && urls_.isEmpty() &&
delete_current_track_ == false;
}
bool CommandlineOptions::contains_play_options() const {
@ -338,7 +347,8 @@ QString CommandlineOptions::tr(const char* source_text) {
QDataStream& operator<<(QDataStream& s, const CommandlineOptions& a) {
s << qint32(a.player_action_) << qint32(a.url_list_action_) << a.set_volume_
<< a.volume_modifier_ << a.seek_to_ << a.seek_by_ << a.play_track_at_
<< a.show_osd_ << a.urls_ << a.log_levels_ << a.toggle_pretty_osd_;
<< a.show_osd_ << a.urls_ << a.log_levels_ << a.toggle_pretty_osd_
<< a.delete_current_track_;
return s;
}
@ -348,7 +358,8 @@ QDataStream& operator>>(QDataStream& s, CommandlineOptions& a) {
quint32 url_list_action = 0;
s >> player_action >> url_list_action >> a.set_volume_ >>
a.volume_modifier_ >> a.seek_to_ >> a.seek_by_ >> a.play_track_at_ >>
a.show_osd_ >> a.urls_ >> a.log_levels_ >> a.toggle_pretty_osd_;
a.show_osd_ >> a.urls_ >> a.log_levels_ >> a.toggle_pretty_osd_ >>
a.delete_current_track_;
a.player_action_ = CommandlineOptions::PlayerAction(player_action);
a.url_list_action_ = CommandlineOptions::UrlListAction(url_list_action);

View File

@ -70,6 +70,7 @@ class CommandlineOptions {
int seek_to() const { return seek_to_; }
int seek_by() const { return seek_by_; }
int play_track_at() const { return play_track_at_; }
bool delete_current_track() const { return delete_current_track_; }
bool show_osd() const { return show_osd_; }
bool toggle_pretty_osd() const { return toggle_pretty_osd_; }
QList<QUrl> urls() const { return urls_; }
@ -113,6 +114,7 @@ class CommandlineOptions {
int seek_to_;
int seek_by_;
int play_track_at_;
bool delete_current_track_;
bool show_osd_;
bool toggle_pretty_osd_;
QString language_;

View File

@ -47,7 +47,7 @@
#include <QVariant>
const char* Database::kDatabaseFilename = "clementine.db";
const int Database::kSchemaVersion = 50;
const int Database::kSchemaVersion = 51;
const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;

View File

@ -57,6 +57,15 @@ void DeleteFiles::Start(const SongList& songs) {
thread_->start();
}
void DeleteFiles::Start(const QUrl& url) {
SongList songs;
Song song;
song.set_url(url);
songs << song;
Start(songs);
}
void DeleteFiles::Start(const QStringList& filenames) {
SongList songs;
for (const QString& filename : filenames) {

View File

@ -40,6 +40,7 @@ class DeleteFiles : public QObject {
void Start(const SongList& songs);
void Start(const QStringList& filenames);
void Start(const QUrl& url);
signals:
void Finished(const SongList& songs_with_errors);

View File

@ -22,6 +22,7 @@
#include "metatypes.h"
#include <QFileInfo>
#include <QMetaType>
#include <QNetworkCookie>
@ -110,6 +111,7 @@ void RegisterMetaTypes() {
qRegisterMetaType<Subdirectory>("Subdirectory");
qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
qRegisterMetaType<QAbstractSocket::SocketState>();
qRegisterMetaType<QFileInfo>("QFileInfo");
#ifdef HAVE_DBUS
qDBusRegisterMetaType<QImage>();

View File

@ -320,7 +320,7 @@ void Mpris2::SetShuffle(bool enable) {
QVariantMap Mpris2::Metadata() const { return last_metadata_; }
QString Mpris2::current_track_id() const {
return QString("/org/mpris/MediaPlayer2/Track/%1")
return QString("/org/clementineplayer/Clementine/Track/%1")
.arg(QString::number(app_->playlist_manager()->active()->current_row()));
}
@ -495,7 +495,7 @@ namespace {
QDBusObjectPath MakePlaylistPath(int id) {
return QDBusObjectPath(
QString("/org/mpris/MediaPlayer2/Playlists/%1").arg(id));
QString("/org/clementineplayer/clementine/PlaylistId/%1").arg(id));
}
}

View File

@ -194,6 +194,13 @@ void Organise::ProcessSomeFiles() {
if (!destination_->CopyToStorage(job)) {
files_with_errors_ << task.song_info_.song_.basefilename();
} else {
if (job.remove_original_) {
// Notify other aspects of system that song has been invalidated
QString root = destination_->LocalPath();
QFileInfo new_file = QFileInfo(
root + "/" + task.song_info_.new_filename_);
emit SongPathChanged(song, new_file);
}
if (job.mark_as_listened_) {
emit FileCopied(job.metadata_.id());
}

View File

@ -24,6 +24,7 @@
#include <memory>
#include <QFileInfo>
#include <QBasicTimer>
#include <QObject>
#include <QTemporaryFile>
@ -60,6 +61,7 @@ class Organise : public QObject {
signals:
void Finished(const QStringList& files_with_errors);
void FileCopied(int database_id);
void SongPathChanged(const Song& song, const QFileInfo& new_file);
protected:
void timerEvent(QTimerEvent* e);

View File

@ -613,7 +613,16 @@ void Player::InvalidSongRequested(const QUrl& url) {
emit SongChangeRequestProcessed(url, false);
// ... and now when our listeners have completed their processing of the
// current item we can change the current item by skipping to the next song
NextItem(Engine::Auto);
QSettings s;
s.beginGroup(kSettingsGroup);
bool stop_playback = s.value("stop_play_if_fail", 0).toBool();
s.endGroup();
if (!stop_playback) {
NextItem(Engine::Auto);
}
}
void Player::RegisterUrlHandler(UrlHandler* handler) {

View File

@ -130,7 +130,8 @@ const QStringList Song::kFtsColumns = QStringList() << "ftstitle"
<< "ftsperformer"
<< "ftsgrouping"
<< "ftsgenre"
<< "ftscomment";
<< "ftscomment"
<< "ftsyear";
const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(", ");
const QString Song::kFtsBindSpec =
@ -407,7 +408,7 @@ QString Song::TextForFiletype(FileType type) {
case Song::Type_Asf:
return QObject::tr("Windows Media audio");
case Song::Type_Flac:
return QObject::tr("Flac");
return QObject::tr("FLAC");
case Song::Type_Mp4:
return QObject::tr("MP4 AAC");
case Song::Type_Mpc:
@ -415,7 +416,7 @@ QString Song::TextForFiletype(FileType type) {
case Song::Type_Mpeg:
return QObject::tr("MP3"); // Not technically correct
case Song::Type_OggFlac:
return QObject::tr("Ogg Flac");
return QObject::tr("Ogg FLAC");
case Song::Type_OggSpeex:
return QObject::tr("Ogg Speex");
case Song::Type_OggVorbis:
@ -996,6 +997,7 @@ void Song::BindToFtsQuery(QSqlQuery* query) const {
query->bindValue(":ftsgrouping", d->grouping_);
query->bindValue(":ftsgenre", d->genre_);
query->bindValue(":ftscomment", d->comment_);
query->bindValue(":ftsyear", d->year_);
}
#ifdef HAVE_LIBLASTFM

View File

@ -113,13 +113,18 @@ QString PrettyTimeNanosec(qint64 nanoseconds) {
}
QString WordyTime(quint64 seconds) {
quint64 days = seconds / (60 * 60 * 24);
quint64 days = seconds / (kSecsPerDay);
quint64 remaining_hours = (seconds - days * kSecsPerDay) / (60 * 60);
// TODO(David Sansome): Make the plural rules translatable
QStringList parts;
if (days) parts << (days == 1 ? tr("1 day") : tr("%1 days").arg(days));
parts << PrettyTime(seconds - days * 60 * 60 * 24);
// Since PrettyTime does not return the hour if it is 0, we need to add it
// explicitly for durations longer than 1 day.
parts << (days && !remaining_hours ? QString("0:") : QString()) +
PrettyTime(seconds - days * kSecsPerDay);
return parts.join(" ");
}

View File

@ -122,7 +122,7 @@ QVariantMap DeviceKitLister::DeviceHardwareInfo(const QString& id) {
if (!device_data_.contains(id)) return ret;
const DeviceData& data = device_data_[id];
ret[QT_TR_NOOP("DBus path")] = data.dbus_path;
ret[QT_TR_NOOP("D-Bus path")] = data.dbus_path;
ret[QT_TR_NOOP("Serial number")] = data.drive_serial;
ret[QT_TR_NOOP("Mount points")] = data.device_mount_paths.join(", ");
ret[QT_TR_NOOP("Device")] = data.device_file;

View File

@ -73,7 +73,7 @@ QVariantMap Udisks2Lister::DeviceHardwareInfo(const QString& id) {
QVariantMap result;
const auto& data = device_data_[id];
result[QT_TR_NOOP("DBus path")] = data.dbus_path;
result[QT_TR_NOOP("D-Bus path")] = data.dbus_path;
result[QT_TR_NOOP("Serial number")] = data.serial;
result[QT_TR_NOOP("Mount points")] = data.mount_paths.join(", ");
result[QT_TR_NOOP("Partition label")] = data.label;

View File

@ -692,6 +692,18 @@ void GstEngine::HandlePipelineError(int pipeline_id, const QString& message,
qLog(Warning) << "Gstreamer error:" << message;
// try to reload the URL in case of a drop of the connection
if (domain == GST_RESOURCE_ERROR && error_code == GST_RESOURCE_ERROR_SEEK) {
if (Load(url_, 0, false, 0, 0)) {
current_pipeline_->SetState(GST_STATE_PLAYING);
return;
}
qLog(Warning) << "Attempt to reload " << url_ << " failed";
}
current_pipeline_.reset();
BufferingFinished();

View File

@ -36,9 +36,11 @@
#include "core/logging.h"
#include "core/mimedata.h"
#include "core/timeconstants.h"
#include "internet/core/internetsongmimedata.h"
#include "library/libraryfilterwidget.h"
#include "library/librarymodel.h"
#include "library/groupbydialog.h"
#include "playlist/songmimedata.h"
using std::placeholders::_1;
using std::placeholders::_2;
@ -446,34 +448,38 @@ bool GlobalSearchView::SearchKeyEvent(QKeyEvent* event) {
}
bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) {
if (!context_menu_) {
context_menu_ = new QMenu(this);
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Append to current playlist"), this,
SLOT(AddSelectedToPlaylist()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Replace current playlist"), this,
SLOT(LoadSelected()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this,
SLOT(OpenSelectedInNewPlaylist()));
context_menu_ = new QMenu(this);
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Replace current playlist"), this, SLOT(LoadSelected()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
context_menu_->addSeparator();
context_actions_ << context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"),
this, SLOT(AddSelectedToPlaylistEnqueue()));
context_menu_->addSeparator();
context_actions_ << context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
SLOT(AddSelectedToPlaylistEnqueue()));
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))
->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure global search..."), this,
SLOT(OpenSettingsDialog()));
context_menu_->addSeparator();
if (ui_->results->selectionModel() &&
ui_->results->selectionModel()->selectedRows().length() == 1) {
context_actions_ << context_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for this"), this, SLOT(SearchForThis()));
}
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))
->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure global search..."), this,
SLOT(OpenSettingsDialog()));
const bool enable_context_actions =
ui_->results->selectionModel() &&
ui_->results->selectionModel()->hasSelection();
@ -515,6 +521,11 @@ void GlobalSearchView::OpenSelectedInNewPlaylist() {
emit AddToPlaylist(data);
}
void GlobalSearchView::SearchForThis() {
StartSearch(
ui_->results->selectionModel()->selectedRows().first().data().toString());
}
void GlobalSearchView::showEvent(QShowEvent* e) {
if (show_suggestions_) {
UpdateSuggestions();
@ -560,9 +571,12 @@ void GlobalSearchView::GroupByClicked(QAction* action) {
}
void GlobalSearchView::SetGroupBy(const LibraryModel::Grouping& g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added
// again, so all the QModelIndex here will become invalid. New requests will be created for those
// songs when they will be displayed again anyway (when GlobalSearchItemDelegate::paint will call
// Clear requests: changing "group by" on the models will cause all the items
// to be removed/added
// again, so all the QModelIndex here will become invalid. New requests will
// be created for those
// songs when they will be displayed again anyway (when
// GlobalSearchItemDelegate::paint will call
// LazyLoadArt)
art_requests_.clear();
// Update the models

View File

@ -21,6 +21,7 @@
#include "searchprovider.h"
#include "library/librarymodel.h"
#include "ui/settingsdialog.h"
#include "playlist/playlistmanager.h"
#include <QWidget>
@ -82,6 +83,8 @@ signals:
void OpenSelectedInNewPlaylist();
void AddSelectedToPlaylistEnqueue();
void SearchForThis();
void GroupByClicked(QAction* action);
void SetGroupBy(const LibraryModel::Grouping& grouping);

View File

@ -32,8 +32,12 @@
#include "lastfmservice.h"
#include <QCryptographicHash>
#include <QDesktopServices>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QUrlQuery>
#ifdef HAVE_LIBLASTFM1
#include <lastfm5/RadioStation.h>
@ -42,14 +46,16 @@
#endif
#include "lastfmcompat.h"
#include "internet/core/internetmodel.h"
#include "internet/core/internetplaylistitem.h"
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/player.h"
#include "core/song.h"
#include "core/taskmanager.h"
#include "internet/core/internetmodel.h"
#include "internet/core/internetplaylistitem.h"
#include "internet/core/localredirectserver.h"
#include "covers/coverproviders.h"
#include "covers/lastfmcoverprovider.h"
#include "ui/iconloader.h"
@ -75,7 +81,8 @@ LastFMService::LastFMService(Application* app, QObject* parent)
already_scrobbled_(false),
scrobbling_enabled_(false),
connection_problems_(false),
app_(app) {
app_(app),
network_(new NetworkAccessManager) {
#ifdef HAVE_LIBLASTFM1
lastfm::ws::setScheme(lastfm::ws::Https);
#endif
@ -123,37 +130,53 @@ bool LastFMService::IsSubscriber() const {
return settings.value("Subscriber", false).toBool();
}
void LastFMService::GetToken() {
QMap<QString, QString> params;
params["method"] = "auth.getToken";
QNetworkReply* reply = lastfm::ws::post(params);
NewClosure(reply, SIGNAL(finished()), this,
SLOT(GetTokenReplyFinished(QNetworkReply*)), reply);
}
void LastFMService::GetTokenReplyFinished(QNetworkReply* reply) {
reply->deleteLater();
// Parse the reply
lastfm::XmlQuery lfm(lastfm::compat::EmptyXmlQuery());
if (lastfm::compat::ParseQuery(reply->readAll(), &lfm)) {
QString token = lfm["token"].text();
emit TokenReceived(true, token);
} else {
emit TokenReceived(false, lfm["error"].text().trimmed());
namespace {
QByteArray SignApiRequest(QList<QPair<QString, QString>> params) {
qSort(params);
QString to_sign;
for (const auto& p : params) {
to_sign += p.first;
to_sign += p.second;
}
to_sign += LastFMService::kSecret;
return QCryptographicHash::hash(to_sign.toUtf8(), QCryptographicHash::Md5).toHex();
}
} // namespace
void LastFMService::Authenticate(const QString& token) {
QMap<QString, QString> params;
params["method"] = "auth.getSession";
params["token"] = token;
void LastFMService::Authenticate() {
QUrl url("https://www.last.fm/api/auth/");
QUrlQuery url_query;
url_query.addQueryItem("api_key", kApiKey);
QNetworkReply* reply = lastfm::ws::post(params);
NewClosure(reply, SIGNAL(finished()), this,
SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
// If we need more detailed error reporting, handle error(NetworkError) signal
LocalRedirectServer* server = new LocalRedirectServer(this);
server->Listen();
url_query.addQueryItem("cb", server->url().toString());
NewClosure(server, SIGNAL(Finished()), [this, server]() {
server->deleteLater();
const QUrl& url = server->request_url();
QString token = QUrlQuery(url).queryItemValue("token");
QUrl session_url("https://ws.audioscrobbler.com/2.0/");
QUrlQuery session_url_query;
session_url_query.addQueryItem("api_key", kApiKey);
session_url_query.addQueryItem("method", "auth.getSession");
session_url_query.addQueryItem("token", token);
session_url_query.addQueryItem("api_sig", SignApiRequest(session_url_query.queryItems()));
session_url.setQuery(session_url_query);
QNetworkReply* reply = network_->get(QNetworkRequest(session_url));
NewClosure(reply, SIGNAL(finished()), this, SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
});
if (!QDesktopServices::openUrl(url)) {
QMessageBox box(QMessageBox::NoIcon, tr("Last.fm Authentication"), tr("Please open this URL in your browser: <a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
box.setTextFormat(Qt::RichText);
qLog(Debug) << "Last.fm authentication URL: " << url.toString();
box.exec();
}
}
void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
@ -174,14 +197,14 @@ void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
settings.setValue("Session", lastfm::ws::SessionKey);
settings.setValue("Subscriber", is_subscriber);
} else {
emit AuthenticationComplete(false, lfm["error"].text().trimmed());
emit AuthenticationComplete(false);
return;
}
// Invalidate the scrobbler - it will get recreated later
scrobbler_.reset(nullptr);
emit AuthenticationComplete(true, QString());
emit AuthenticationComplete(true);
}
void LastFMService::SignOut() {

View File

@ -38,8 +38,8 @@ uint qHash(const lastfm::Track& track);
class Application;
class LastFMUrlHandler;
class NetworkAccessManager;
class QAction;
class QNetworkAccessManager;
class Song;
class LastFMService : public Scrobbler {
@ -69,8 +69,7 @@ class LastFMService : public Scrobbler {
bool PreferAlbumArtist() const { return prefer_albumartist_; }
bool HasConnectionProblems() const { return connection_problems_; }
void GetToken();
void Authenticate(const QString& token);
void Authenticate();
void SignOut();
void UpdateSubscriberStatus();
@ -83,8 +82,7 @@ class LastFMService : public Scrobbler {
void ToggleScrobbling();
signals:
void TokenReceived(bool success, const QString& token);
void AuthenticationComplete(bool success, const QString& error_message);
void AuthenticationComplete(bool success);
void ScrobblingEnabledChanged(bool value);
void ButtonVisibilityChanged(bool value);
void ScrobbleButtonVisibilityChanged(bool value);
@ -97,7 +95,6 @@ signals:
void SavedItemsChanged();
private slots:
void GetTokenReplyFinished(QNetworkReply* reply);
void AuthenticateReplyFinished(QNetworkReply* reply);
void UpdateSubscriberStatusFinished(QNetworkReply* reply);
@ -129,6 +126,7 @@ signals:
bool connection_problems_;
Application* app_;
std::unique_ptr<NetworkAccessManager> network_;
};
#endif // INTERNET_LASTFM_LASTFMSERVICE_H_

View File

@ -43,10 +43,8 @@ LastFMSettingsPage::LastFMSettingsPage(SettingsDialog* dialog)
// Icons
setWindowIcon(IconLoader::Load("lastfm", IconLoader::Provider));
connect(service_, SIGNAL(TokenReceived(bool,QString)),
SLOT(TokenReceived(bool,QString)));
connect(service_, SIGNAL(AuthenticationComplete(bool, QString)),
SLOT(AuthenticationComplete(bool, QString)));
connect(service_, SIGNAL(AuthenticationComplete(bool)),
SLOT(AuthenticationComplete(bool)));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login()));
connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
@ -62,26 +60,10 @@ void LastFMSettingsPage::Login() {
waiting_for_auth_ = true;
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
service_->GetToken();
service_->Authenticate();
}
void LastFMSettingsPage::TokenReceived(bool success, const QString &token) {
if (!success) {
QMessageBox::warning(this, tr("Last.fm authentication failed"), token);
return;
}
QString url = QString(LastFMService::kAuthLoginUrl).arg(LastFMService::kApiKey, token);
QDesktopServices::openUrl(QUrl(url));
QMessageBox::information(this, tr("Last.fm authentication"),
tr("Click Ok once you authenticated Clementine in your last.fm account."));
service_->Authenticate(token);
}
void LastFMSettingsPage::AuthenticationComplete(bool success,
const QString& message) {
void LastFMSettingsPage::AuthenticationComplete(bool success) {
if (!waiting_for_auth_) return; // Wasn't us that was waiting for auth
waiting_for_auth_ = false;
@ -90,10 +72,7 @@ void LastFMSettingsPage::AuthenticationComplete(bool success,
// Save settings
Save();
} else {
QString dialog_text = tr("Your Last.fm credentials were incorrect");
if (!message.isEmpty()) {
dialog_text = message;
}
QString dialog_text = tr("Could not log in to Last.fm. Please try again.");
QMessageBox::warning(this, tr("Authentication failed"), dialog_text);
}

View File

@ -38,8 +38,7 @@ class LastFMSettingsPage : public SettingsPage {
private slots:
void Login();
void TokenReceived(bool success, const QString& token);
void AuthenticationComplete(bool success, const QString& error_message);
void AuthenticationComplete(bool success);
void Logout();
private:

View File

@ -229,9 +229,16 @@ void PodcastParser::ParseItem(QXmlStreamReader* reader, Podcast* ret) const {
}
} else if (name == "enclosure") {
const QString type = reader->attributes().value("type").toString();
const QUrl url = QUrl::fromEncoded(reader->attributes().value("url").toString().toLatin1());
if (type.startsWith("audio/") || type.startsWith("x-audio/")) {
episode.set_url(QUrl::fromEncoded(
reader->attributes().value("url").toString().toLatin1()));
episode.set_url(url);
}
// If the URL doesn't have a type, see if it's one of the obvious types
else if (type.isEmpty() && (
url.path().endsWith(".mp3", Qt::CaseInsensitive) ||
url.path().endsWith(".m4a", Qt::CaseInsensitive) ||
url.path().endsWith(".wav", Qt::CaseInsensitive))) {
episode.set_url(url);
}
Utilities::ConsumeCurrentElement(reader);
} else if (name == "author" && lower_namespace == kItunesNamespace) {

View File

@ -67,7 +67,7 @@ PodcastService::PodcastService(Application* app, InternetModel* parent)
proxy_(new PodcastSortProxyModel(this)),
context_menu_(nullptr),
root_(nullptr),
organise_dialog_(new OrganiseDialog(app_->task_manager(), nullptr)) {
organise_dialog_(new OrganiseDialog(app_->task_manager())) {
icon_loader_->SetModel(model_);
proxy_->setSourceModel(model_);
proxy_->setDynamicSortFilter(true);
@ -128,7 +128,7 @@ bool PodcastSortProxyModel::lessThan(const QModelIndex& left,
}
QStandardItem* PodcastService::CreateRootItem() {
root_ = new QStandardItem(IconLoader::Load("podcast", IconLoader::Provider),
root_ = new QStandardItem(IconLoader::Load("podcast", IconLoader::Provider),
tr("Podcasts"));
root_->setData(true, InternetModel::Role_CanLazyLoad);
return root_;
@ -414,7 +414,7 @@ QStandardItem* PodcastService::CreatePodcastEpisodeItem(
void PodcastService::ShowContextMenu(const QPoint& global_pos) {
if (!context_menu_) {
context_menu_ = new QMenu;
context_menu_->addAction(IconLoader::Load("list-add", IconLoader::Base),
context_menu_->addAction(IconLoader::Load("list-add", IconLoader::Base),
tr("Add podcast..."), this, SLOT(AddPodcast()));
context_menu_->addAction(IconLoader::Load("view-refresh", IconLoader::Base),
tr("Update all podcasts"), app_->podcast_updater(),
@ -425,23 +425,23 @@ void PodcastService::ShowContextMenu(const QPoint& global_pos) {
context_menu_->addSeparator();
update_selected_action_ = context_menu_->addAction(
IconLoader::Load("view-refresh", IconLoader::Base),
IconLoader::Load("view-refresh", IconLoader::Base),
tr("Update this podcast"), this, SLOT(UpdateSelectedPodcast()));
download_selected_action_ =
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
"", this, SLOT(DownloadSelectedEpisode()));
delete_downloaded_action_ = context_menu_->addAction(
IconLoader::Load("edit-delete", IconLoader::Base),
IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete downloaded data"), this, SLOT(DeleteDownloadedData()));
copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base),
tr("Copy to device..."), this, SLOT(CopyToDevice()));
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel",
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel",
IconLoader::Base),
tr("Cancel download"), this,
SLOT(CancelDownload()));
remove_selected_action_ = context_menu_->addAction(
IconLoader::Load("list-remove", IconLoader::Base), tr("Unsubscribe"),
IconLoader::Load("list-remove", IconLoader::Base), tr("Unsubscribe"),
this, SLOT(RemoveSelectedPodcast()));
context_menu_->addSeparator();

View File

@ -42,6 +42,11 @@
#ifdef HAVE_CRYPTOPP
#include <cryptopp/pkcspad.h>
#include <cryptopp/rsa.h>
// Compatibility with cryptocpp >= 6.0.0
namespace CryptoPP {
typedef unsigned char byte;
}
#endif // HAVE_CRYPTOPP
const char* SpotifyBlobDownloader::kSignatureSuffix = ".sha512";
@ -189,7 +194,7 @@ bool SpotifyBlobDownloader::CheckSignature(
try {
CryptoPP::ByteQueue bytes;
bytes.Put(reinterpret_cast<const byte*>(public_key_data.constData()),
bytes.Put(reinterpret_cast<const CryptoPP::byte*>(public_key_data.constData()),
public_key_data.size());
bytes.MessageEnd();
@ -204,9 +209,9 @@ bool SpotifyBlobDownloader::CheckSignature(
actual_filename.remove(kSignatureSuffix);
const bool result = verifier.VerifyMessage(
reinterpret_cast<const byte*>(file_data[actual_filename].constData()),
reinterpret_cast<const CryptoPP::byte*>(file_data[actual_filename].constData()),
file_data[actual_filename].size(),
reinterpret_cast<const byte*>(
reinterpret_cast<const CryptoPP::byte*>(
file_data[signature_filename].constData()),
file_data[signature_filename].size());
qLog(Debug) << "Verifying" << actual_filename << "against"

View File

@ -220,6 +220,11 @@ void SubsonicDynamicPlaylist::GetAlbum(SubsonicService* service,
length *= kNsecPerSec;
song.set_length_nanosec(length);
QUrl url = QUrl(QString("subsonic://%1").arg(id));
QUrl cover_url = service->BuildRequestUrl("getCoverArt");
QUrlQuery cover_url_query(cover_url.query());
cover_url_query.addQueryItem("id", id);
cover_url.setQuery(cover_url_query);
song.set_art_automatic(cover_url.toEncoded());
song.set_url(url);
song.set_filesize(reader.attributes().value("size").toString().toInt());
QFileInfo fi(reader.attributes().value("path").toString());

View File

@ -542,6 +542,11 @@ void SubsonicLibraryScanner::OnGetAlbumFinished(QNetworkReply* reply) {
length *= kNsecPerSec;
song.set_length_nanosec(length);
QUrl url = QUrl(QString("subsonic://%1").arg(id));
QUrl cover_url = service_->BuildRequestUrl("getCoverArt");
QUrlQuery cover_url_query(url.query());
cover_url_query.addQueryItem("id", id);
cover_url.setQuery(cover_url_query);
song.set_art_automatic(cover_url.toEncoded());
song.set_url(url);
song.set_filesize(reader.attributes().value("size").toString().toInt());
// We need to set these to satisfy the database constraints

View File

@ -290,6 +290,16 @@ SongList LibraryBackend::FindSongsInDirectory(int id) {
return ret;
}
void LibraryBackend::SongPathChanged(const Song& song,
const QFileInfo& new_file) {
// Take a song and update its path
Song updated_song = song;
updated_song.InitFromFilePartial(new_file.absoluteFilePath());
SongList updated_songs;
updated_songs << updated_song;
AddOrUpdateSongs(updated_songs);
}
void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());

View File

@ -21,6 +21,7 @@
#include <QObject>
#include <QSet>
#include <QUrl>
#include <QFileInfo>
#include "directory.h"
#include "libraryquery.h"
@ -218,6 +219,8 @@ class LibraryBackend : public LibraryBackendInterface {
void ResetStatistics(int id);
void UpdateSongRating(int id, float rating);
void UpdateSongsRating(const QList<int>& id_list, float rating);
// Tells the library model that a song path has changed
void SongPathChanged(const Song& song, const QFileInfo& new_file);
signals:
void DirectoryDiscovered(const Directory& dir,

View File

@ -109,7 +109,7 @@ LibraryModel::LibraryModel(LibraryBackend* backend, Application* app,
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(
kPrettyCoverSize, kPrettyCoverSize,
kPrettyCoverSize, kPrettyCoverSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
@ -657,6 +657,9 @@ QVariant LibraryModel::data(const LibraryItem* item, int role) const {
case Role_SortText:
return item->SortText();
case Role_DisplayText:
return item->DisplayText();
}
return QVariant();
}

View File

@ -64,6 +64,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
Role_Type = Qt::UserRole + 1,
Role_ContainerType,
Role_SortText,
Role_DisplayText,
Role_Key,
Role_Artist,
Role_IsDivider,

View File

@ -103,7 +103,7 @@ void LibraryItemDelegate::paint(QPainter* painter,
// Draw the line under the item
QColor line_color = opt.palette.color(QPalette::Text);
QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight());
const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width();
const double fade_start_end = (opt.rect.width() / 3.0) / opt.rect.width();
line_color.setAlphaF(0.0);
grad_color.setColorAt(0, line_color);
line_color.setAlphaF(0.5);
@ -175,7 +175,7 @@ LibraryView::LibraryView(QWidget* parent)
total_song_count_(-1),
context_menu_(nullptr),
is_in_keyboard_search_(false) {
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
nomusic_ = nocover.pixmap(nocover.availableSizes().last());
setItemDelegate(new LibraryItemDelegate(this));
setAttribute(Qt::WA_MacShowFocusRect, false);
@ -193,15 +193,17 @@ LibraryView::~LibraryView() {}
void LibraryView::SaveFocus() {
QModelIndex current = currentIndex();
QVariant type = model()->data(current, LibraryModel::Role_Type);
if (!type.isValid() || !(type.toInt() == LibraryItem::Type_Song ||
type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
if (!type.isValid() ||
!(type.toInt() == LibraryItem::Type_Song ||
type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
return;
}
last_selected_path_.clear();
last_selected_song_ = Song();
last_selected_container_ = QString();
last_selected_text_ = QString();
switch (type.toInt()) {
case LibraryItem::Type_Song: {
@ -210,6 +212,7 @@ void LibraryView::SaveFocus() {
SongList songs = app_->library_model()->GetChildSongs(index);
if (!songs.isEmpty()) {
last_selected_song_ = songs.last();
last_selected_text_ = songs.last().title();
}
break;
}
@ -217,8 +220,9 @@ void LibraryView::SaveFocus() {
case LibraryItem::Type_Container:
case LibraryItem::Type_Divider: {
QString text =
model()->data(current, LibraryModel::Role_SortText).toString();
model()->data(current, LibraryModel::Role_Key).toString();
last_selected_container_ = text;
last_selected_text_ = model()->data(current, LibraryModel::Role_DisplayText).toString();
break;
}
@ -232,8 +236,9 @@ void LibraryView::SaveFocus() {
void LibraryView::SaveContainerPath(const QModelIndex& child) {
QModelIndex current = model()->parent(child);
QVariant type = model()->data(current, LibraryModel::Role_Type);
if (!type.isValid() || !(type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
if (!type.isValid() ||
!(type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
return;
}
@ -276,7 +281,7 @@ bool LibraryView::RestoreLevelFocus(const QModelIndex& parent) {
case LibraryItem::Type_Container:
case LibraryItem::Type_Divider: {
QString text =
model()->data(current, LibraryModel::Role_SortText).toString();
model()->data(current, LibraryModel::Role_Key).toString();
if (!last_selected_container_.isEmpty() &&
last_selected_container_ == text) {
emit expand(current);
@ -380,46 +385,48 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Replace current playlist"), this, SLOT(Load()));
open_in_new_playlist_ = context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
context_menu_->addSeparator();
add_to_playlist_enqueue_ =
context_menu_->addAction(IconLoader::Load("go-next", IconLoader::Base),
tr("Queue track"), this,
SLOT(AddToPlaylistEnqueue()));
add_to_playlist_enqueue_ = context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
SLOT(AddToPlaylistEnqueue()));
context_menu_->addSeparator();
search_for_this_ = context_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for this"), this, SLOT(SearchForThis()));
context_menu_->addSeparator();
new_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
IconLoader::Load("document-new", IconLoader::Base),
tr("New smart playlist..."), this, SLOT(NewSmartPlaylist()));
edit_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("edit-rename", IconLoader::Base),
IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit smart playlist..."), this, SLOT(EditSmartPlaylist()));
delete_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("edit-delete", IconLoader::Base),
IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete smart playlist"), this, SLOT(DeleteSmartPlaylist()));
context_menu_->addSeparator();
organise_ = context_menu_->addAction(IconLoader::Load("edit-copy", IconLoader::Base),
tr("Organise files..."), this,
SLOT(Organise()));
organise_ = context_menu_->addAction(
IconLoader::Load("edit-copy", IconLoader::Base),
tr("Organise files..."), this, SLOT(Organise()));
copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base),
tr("Copy to device..."), this, SLOT(CopyToDevice()));
delete_ = context_menu_->addAction(IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete from disk..."), this,
SLOT(Delete()));
delete_ = context_menu_->addAction(
IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete from disk..."), this, SLOT(Delete()));
context_menu_->addSeparator();
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit track information..."),
this, SLOT(EditTracks()));
edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit tracks information..."),
this, SLOT(EditTracks()));
edit_track_ = context_menu_->addAction(
IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit track information..."), this, SLOT(EditTracks()));
edit_tracks_ = context_menu_->addAction(
IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit tracks information..."), this, SLOT(EditTracks()));
show_in_browser_ = context_menu_->addAction(
IconLoader::Load("document-open-folder", IconLoader::Base),
IconLoader::Load("document-open-folder", IconLoader::Base),
tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
context_menu_->addSeparator();
@ -458,6 +465,8 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
int regular_elements = 0;
// number of editable non smart playlists selected
int regular_editable = 0;
// number of container elements selected
int container_elements = 0;
for (const QModelIndex& index : selected_indexes) {
int type =
@ -467,6 +476,10 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
smart_playlists++;
} else if (type == LibraryItem::Type_PlaylistContainer) {
smart_playlists_header++;
} else if (type == LibraryItem::Type_Container) {
container_elements++;
// To preserve expected behavior, since a container is "regular"
regular_elements++;
} else {
regular_elements++;
}
@ -487,6 +500,10 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
songs_selected == smart_playlists + smart_playlists_header;
const bool only_smart_playlist_selected =
smart_playlists == 1 && songs_selected == 1;
const bool one_regular_song_only =
regular_elements_only && container_elements == 0 && regular_elements == 1;
const bool one_container_only =
container_elements == 1 && songs_selected == 1;
// in all modes
load_->setEnabled(songs_selected);
@ -509,6 +526,9 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
show_in_various_->setVisible(regular_elements_only);
no_show_in_various_->setVisible(regular_elements_only);
// only when a single container or one song is selected exclusively
search_for_this_->setVisible(one_container_only || one_regular_song_only);
// only when all selected items are editable
organise_->setEnabled(regular_elements == regular_editable);
copy_to_device_->setEnabled(regular_elements == regular_editable);
@ -617,6 +637,7 @@ void LibraryView::scrollTo(const QModelIndex& index, ScrollHint hint) {
QTreeView::scrollTo(index, hint);
}
// get selected songs
SongList LibraryView::GetSelectedSongs() const {
QModelIndexList selected_indexes =
qobject_cast<QSortFilterProxyModel*>(model())
@ -627,7 +648,8 @@ SongList LibraryView::GetSelectedSongs() const {
void LibraryView::Organise() {
if (!organise_dialog_)
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
organise_dialog_.reset(new OrganiseDialog(app_->task_manager(),
app_->library_backend()));
organise_dialog_->SetDestinationModel(
app_->library_model()->directory_model());
@ -675,6 +697,10 @@ void LibraryView::EditTracks() {
void LibraryView::CopyToDevice() {
if (!organise_dialog_)
// Don't notify song has been replaced if copying to device, so
// don't associate the organise dialog with the library backend.
// Could improve this behavior if the device has a separate set of saved
// playlists that are somehow in sync with the library.
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
organise_dialog_->SetDestinationModel(
@ -709,6 +735,13 @@ void LibraryView::FilterReturnPressed() {
emit doubleClicked(currentIndex());
}
void LibraryView::SearchForThis() {
SaveFocus();
if (!last_selected_text_.isEmpty()) {
filter_->ShowInLibrary(last_selected_text_.simplified());
}
}
void LibraryView::NewSmartPlaylist() {
Wizard* wizard = new Wizard(app_, app_->library_backend(), this);
wizard->setAttribute(Qt::WA_DeleteOnClose);

View File

@ -101,6 +101,8 @@ signals:
void ShowInVarious();
void NoShowInVarious();
void SearchForThis();
void NewSmartPlaylist();
void EditSmartPlaylist();
void DeleteSmartPlaylist();
@ -139,6 +141,8 @@ signals:
QAction* show_in_various_;
QAction* no_show_in_various_;
QAction* search_for_this_;
QAction* new_smart_playlist_;
QAction* edit_smart_playlist_;
QAction* delete_smart_playlist_;
@ -151,6 +155,7 @@ signals:
// Save focus
Song last_selected_song_;
QString last_selected_container_;
QString last_selected_text_;
QSet<QString> last_selected_path_;
};

View File

@ -247,7 +247,7 @@ int main(int argc, char* argv[]) {
if (QSysInfo::MacintoshVersion > QSysInfo::MV_10_8) {
// Work around 10.9 issue.
// https://bugreports.qt-project.org/browse/QTBUG-32789
// https://bugreports.qt.io/browse/QTBUG-32789
QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande");
}
#endif
@ -292,7 +292,10 @@ int main(int argc, char* argv[]) {
qLog(Info)
<< "Clementine is already running - activating existing window";
}
if (a.sendMessage(options.Serialize(), 5000)) {
QByteArray serializedOptions = options.Serialize();
if (a.sendMessage(serializedOptions, 5000)) {
qLog(Info) << "Options found, sent message to running instance";
return 0;
}
// Couldn't send the message so start anyway

View File

@ -19,6 +19,7 @@
#include "playlistmanager.h"
#include "ui_playlistcontainer.h"
#include "core/logging.h"
#include "core/appearance.h"
#include "playlistparsers/playlistparser.h"
#include "ui/iconloader.h"
@ -71,6 +72,11 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
// Remove QFrame border
ui_->toolbar->setStyleSheet("QFrame { border: 0px; }");
QSettings settings;
settings.beginGroup(Appearance::kSettingsGroup);
bool hide_toolbar = settings.value("b_hide_filter_toolbar", false).toBool();
ui_->toolbar->setVisible(!hide_toolbar);
// Make it bold
QFont no_matches_font = no_matches_label_->font();
no_matches_font.setBold(true);
@ -452,3 +458,10 @@ bool PlaylistContainer::eventFilter(QObject* objectWatched, QEvent* event) {
}
return QWidget::eventFilter(objectWatched, event);
}
void PlaylistContainer::ReloadSettings() {
QSettings settings;
settings.beginGroup(Appearance::kSettingsGroup);
bool hide_toolbar = settings.value("b_hide_filter_toolbar", false).toBool();
ui_->toolbar->setVisible(!hide_toolbar);
}

View File

@ -61,6 +61,9 @@ signals:
// QWidget
void resizeEvent(QResizeEvent*);
public slots:
void ReloadSettings();
private slots:
void NewPlaylist();
void LoadPlaylist();

View File

@ -617,9 +617,6 @@ void PlaylistView::keyPressEvent(QKeyEvent* event) {
event->key() == Qt::Key_Space) {
emit PlayPause();
event->accept();
} else if (event->key() == Qt::Key_Up) {
app_->player()->SeekTo(0);
event->accept();
} else if (event->key() == Qt::Key_Left) {
emit SeekBackward();
event->accept();

View File

@ -71,7 +71,7 @@
</size>
</property>
<property name="shortcut">
<string>Ctrl+Up</string>
<string>Ctrl+</string>
</property>
</widget>
</item>
@ -90,7 +90,7 @@
</size>
</property>
<property name="shortcut">
<string>Ctrl+Down</string>
<string>Ctrl+</string>
</property>
</widget>
</item>

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>

View File

@ -230,7 +230,7 @@ QList<TranscoderPreset> Transcoder::GetAllPresets() {
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
switch (type) {
case Song::Type_Flac:
return TranscoderPreset(type, tr("Flac"), "flac", "audio/x-flac");
return TranscoderPreset(type, tr("FLAC"), "flac", "audio/x-flac");
case Song::Type_Mp4:
return TranscoderPreset(type, tr("M4A AAC"), "mp4",
"audio/mpeg, mpegversion=(int)4", "audio/mp4");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More