Merge branch 'master' into cdrip

This commit is contained in:
asiviero 2014-01-12 14:02:37 -02:00
commit 34c178af65
53 changed files with 694 additions and 121 deletions

View File

@ -158,6 +158,10 @@ include_directories(${Boost_INCLUDE_DIRS})
include_directories(${TAGLIB_INCLUDE_DIRS})
include_directories(${QJSON_INCLUDE_DIRS})
include_directories(${GSTREAMER_INCLUDE_DIRS})
include_directories(${GSTREAMER_APP_INCLUDE_DIRS})
include_directories(${GSTREAMER_BASE_INCLUDE_DIRS})
include_directories(${GSTREAMER_CDDA_INCLUDE_DIRS})
include_directories(${GSTREAMER_TAG_INCLUDE_DIRS})
include_directories(${GLIB_INCLUDE_DIRS})
include_directories(${GLIBCONFIG_INCLUDE_DIRS})
include_directories(${LIBXML_INCLUDE_DIRS})

26
README.md Normal file
View File

@ -0,0 +1,26 @@
Clementine
==========
Clementine is a modern music player and library organizer for Windows, Linux and Mac OS X.
- Website: http://www.clementine-player.org/
- Github: https://github.com/clementine-player/Clementine
- Buildbot: http://buildbot.clementine-player.org/grid
- Latest developer builds: http://builds.clementine-player.org/
Compiling from source
---------------------
Get the code (if you haven't already):
git clone https://github.com/clementine-player/Clementine.git && cd Clementine
Compile and install:
cd bin
cmake ..
make -j8
sudo make install
See the Wiki for more instructions and a list of dependencies:
https://github.com/clementine-player/Clementine/wiki/Compiling-from-Source

View File

@ -257,6 +257,7 @@ message ResponseSongFileChunk {
optional SongMetadata song_metadata = 6; // only sent with first chunk!
optional bytes data = 7;
optional int32 size = 8;
optional bytes file_hash = 9;
}
message ResponseLibraryChunk {
@ -264,6 +265,7 @@ message ResponseLibraryChunk {
optional int32 chunk_count = 2;
optional bytes data = 3;
optional int32 size = 4;
optional bytes file_hash = 5;
}
message ResponseSongOffer {
@ -276,7 +278,7 @@ message RequestRateSong {
// The message itself
message Message {
optional int32 version = 1 [default=13];
optional int32 version = 1 [default=14];
optional MsgType type = 2 [default=UNKNOWN]; // What data is in the message?
optional RequestConnect request_connect = 21;

View File

@ -134,6 +134,7 @@ set(SOURCES
devices/deviceproperties.cpp
devices/devicestatefiltermodel.cpp
devices/deviceview.cpp
devices/deviceviewcontainer.cpp
devices/filesystemdevice.cpp
engines/enginebase.cpp
@ -447,6 +448,7 @@ set(HEADERS
devices/deviceproperties.h
devices/devicestatefiltermodel.h
devices/deviceview.h
devices/deviceviewcontainer.h
devices/filesystemdevice.h
engines/enginebase.h
@ -674,6 +676,7 @@ set(UI
covers/coversearchstatisticsdialog.ui
devices/deviceproperties.ui
devices/deviceviewcontainer.ui
globalsearch/globalsearchsettingspage.ui
globalsearch/globalsearchview.ui

View File

@ -451,4 +451,12 @@ void EnableFullScreen(const QWidget& main_window) {
[window setCollectionBehavior: kFullScreenPrimary];
}
float GetDevicePixelRatio(QWidget* widget) {
NSView* view = reinterpret_cast<NSView*>(widget->winId());
if ([[view window] respondsToSelector: @selector(backingScaleFactor)]) {
return [[view window] backingScaleFactor];
}
return 1.0f;
}
} // namespace mac

View File

@ -29,5 +29,6 @@ namespace mac {
QKeySequence KeySequenceFromNSEvent(NSEvent* event);
void DumpDictionary(CFDictionaryRef dict);
float GetDevicePixelRatio(QWidget* widget);
}

View File

@ -442,6 +442,8 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
if (pb.has_rating()) {
d->rating_ = pb.rating();
}
InitArtManual();
}
void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
@ -545,6 +547,8 @@ void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
d->performer_ = tostr(col + 38);
d->grouping_ = tostr(col + 39);
InitArtManual();
#undef tostr
#undef toint
#undef tolonglong
@ -569,6 +573,17 @@ void Song::InitFromFilePartial(const QString& filename) {
}
}
void Song::InitArtManual() {
// If we don't have an art, check if we have one in the cache
if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty()) {
QString filename(Utilities::Sha1CoverHash(d->artist_, d->album_).toHex() + ".jpg");
QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename);
if (QFile::exists(path)) {
d->art_manual_ = path;
}
}
}
#ifdef HAVE_LIBLASTFM
void Song::InitFromLastFM(const lastfm::Track& track) {
d->valid_ = true;

View File

@ -106,6 +106,7 @@ class Song {
void InitFromProtobuf(const pb::tagreader::SongMetadata& pb);
void InitFromQuery(const SqlRow& query, bool reliable_metadata, int col = 0);
void InitFromFilePartial(const QString& filename); // Just store the filename: incomplete but fast
void InitArtManual(); // Check if there is already a art in the cache and store the filename in art_manual
#ifdef HAVE_LIBLASTFM
void InitFromLastFM(const lastfm::Track& track);
#endif

View File

@ -25,6 +25,7 @@
#include <QDateTime>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QIODevice>
#include <QMetaEnum>
#include <QMouseEvent>
@ -455,6 +456,31 @@ QByteArray Sha256(const QByteArray& data) {
return ret;
}
// File must not be open and will be closed afterwards!
QByteArray Md5File(QFile &file) {
file.open(QIODevice::ReadOnly);
QCryptographicHash hash(QCryptographicHash::Md5);
QByteArray data;
while(!file.atEnd()) {
data = file.read(1000000); // 1 mib
hash.addData(data.data(), data.length());
data.clear();
}
file.close();
return hash.result();
}
QByteArray Sha1CoverHash(const QString& artist, const QString& album) {
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(artist.toLower().toUtf8().constData());
hash.addData(album.toLower().toUtf8().constData());
return hash.result();
}
QString PrettySize(const QSize& size) {
return QString::number(size.width()) + "x" +
QString::number(size.height());

View File

@ -19,6 +19,7 @@
#define UTILITIES_H
#include <QColor>
#include <QFile>
#include <QLocale>
#include <QCryptographicHash>
#include <QSize>
@ -66,6 +67,8 @@ namespace Utilities {
QByteArray HmacSha256(const QByteArray& key, const QByteArray& data);
QByteArray HmacSha1(const QByteArray& key, const QByteArray& data);
QByteArray Sha256(const QByteArray& data);
QByteArray Md5File(QFile& file);
QByteArray Sha1CoverHash(const QString& artist, const QString& album);
// Picks an unused ephemeral port number. Doesn't hold the port open so

View File

@ -0,0 +1,56 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "deviceviewcontainer.h"
#include "ui_deviceviewcontainer.h"
#include "ui/iconloader.h"
DeviceViewContainer::DeviceViewContainer(QWidget *parent)
: QWidget(parent),
ui_(new Ui::DeviceViewContainer),
loaded_icons_(false) {
ui_->setupUi(this);
QPalette palette(ui_->windows_is_broken_frame->palette());
palette.setColor(QPalette::Background, QColor(255, 255, 222));
ui_->windows_is_broken_frame->setPalette(palette);
#ifdef Q_OS_WIN
ui_->windows_is_broken_frame->show();
#else
ui_->windows_is_broken_frame->hide();
#endif
}
DeviceViewContainer::~DeviceViewContainer() {
delete ui_;
}
void DeviceViewContainer::showEvent(QShowEvent* e) {
if (!loaded_icons_) {
loaded_icons_ = true;
ui_->close_frame_button->setIcon(IconLoader::Load("edit-delete"));
ui_->warning_icon->setPixmap(IconLoader::Load("dialog-warning").pixmap(22));
}
QWidget::showEvent(e);
}
DeviceView* DeviceViewContainer::view() const {
return ui_->view;
}

View File

@ -0,0 +1,46 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICEVIEWCONTAINER_H
#define DEVICEVIEWCONTAINER_H
#include <QWidget>
namespace Ui {
class DeviceViewContainer;
}
class DeviceView;
class DeviceViewContainer : public QWidget {
Q_OBJECT
public:
explicit DeviceViewContainer(QWidget* parent = 0);
~DeviceViewContainer();
DeviceView* view() const;
protected:
void showEvent(QShowEvent*);
private:
Ui::DeviceViewContainer* ui_;
bool loaded_icons_;
};
#endif // DEVICEVIEWCONTAINER_H

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DeviceViewContainer</class>
<widget class="QWidget" name="DeviceViewContainer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>391</width>
<height>396</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="windows_is_broken_frame">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="warning_icon"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>iPods and USB devices currently don't work on Windows. Sorry!</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="close_frame_button"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="DeviceView" name="view" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>DeviceView</class>
<extends>QWidget</extends>
<header>devices/deviceview.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>close_frame_button</sender>
<signal>clicked()</signal>
<receiver>windows_is_broken_frame</receiver>
<slot>hide()</slot>
<hints>
<hint type="sourcelabel">
<x>362</x>
<y>31</y>
</hint>
<hint type="destinationlabel">
<x>369</x>
<y>40</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1679,7 +1679,12 @@ Song GroovesharkService::ExtractSong(const QVariantMap& result_song) {
Song song;
if (!result_song.isEmpty()) {
int song_id = result_song["SongID"].toInt();
QString song_name = result_song["SongName"].toString();
QString song_name;
if (result_song.contains("SongName")) {
song_name = result_song["SongName"].toString();
} else {
song_name = result_song["Name"].toString();
}
int artist_id = result_song["ArtistID"].toInt();
QString artist_name = result_song["ArtistName"].toString();
int album_id = result_song["AlbumID"].toInt();

View File

@ -212,6 +212,10 @@ void LibraryFilterWidget::SetQueryMode(QueryOptions::QueryMode query_mode) {
model_->SetFilterQueryMode(query_mode);
}
void LibraryFilterWidget::ShowInLibrary(const QString& search) {
ui_->filter->setText(search);
}
void LibraryFilterWidget::SetAgeFilterEnabled(bool enabled) {
filter_age_menu_->setEnabled(enabled);
}

View File

@ -56,6 +56,7 @@ class LibraryFilterWidget : public QWidget {
void SetDelayBehaviour(DelayBehaviour behaviour) { delay_behaviour_ = behaviour; }
void SetAgeFilterEnabled(bool enabled);
void SetGroupByEnabled(bool enabled);
void ShowInLibrary(const QString& search);
QMenu* menu() const { return library_menu_; }
void AddMenuAction(QAction* action);

View File

@ -641,6 +641,11 @@ void OutgoingDataCreator::SendSingleSong(RemoteClient* client, const Song &song,
// Open the file
QFile file(song.url().toLocalFile());
// Get md5 for file
QByteArray md5 = Utilities::Md5File(file).toHex();
qLog(Debug) << "md5 for file" << song.url().toLocalFile() << "=" << md5;
file.open(QIODevice::ReadOnly);
QByteArray data;
@ -665,6 +670,7 @@ void OutgoingDataCreator::SendSingleSong(RemoteClient* client, const Song &song,
chunk->set_file_number(song_no);
chunk->set_size(file.size());
chunk->set_data(data.data(), data.size());
chunk->set_file_hash(md5.data(), md5.size());
// On the first chunk send the metadata, so the client knows
// what file it receives.
@ -746,6 +752,11 @@ void OutgoingDataCreator::SendLibrary(RemoteClient *client) {
// Open the file
QFile file(temp_file_name);
// Get the md5 hash
QByteArray md5 = Utilities::Md5File(file).toHex();
qLog(Debug) << "Library md5" << md5;
file.open(QIODevice::ReadOnly);
QByteArray data;
@ -766,6 +777,7 @@ void OutgoingDataCreator::SendLibrary(RemoteClient *client) {
chunk->set_chunk_number(chunk_number);
chunk->set_size(file.size());
chunk->set_data(data.data(), data.size());
chunk->set_file_hash(md5.data(), md5.size());
// Send data directly to the client
client->SendData(&msg);

View File

@ -16,13 +16,6 @@
*/
#include "playlistdelegates.h"
#include "queue.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/utilities.h"
#include "library/librarybackend.h"
#include "widgets/trackslider.h"
#include "ui/iconloader.h"
#include <QDateTime>
#include <QDir>
@ -39,6 +32,18 @@
#include <QWhatsThis>
#include <QtConcurrentRun>
#include "queue.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/utilities.h"
#include "library/librarybackend.h"
#include "widgets/trackslider.h"
#include "ui/iconloader.h"
#ifdef Q_OS_DARWIN
#include "core/mac_utilities.h"
#endif // Q_OS_DARWIN
const int QueuedItemDelegate::kQueueBoxBorder = 1;
const int QueuedItemDelegate::kQueueBoxCornerRadius = 3;
const int QueuedItemDelegate::kQueueBoxLength = 30;
@ -492,8 +497,14 @@ void SongSourceDelegate::paint(
const QUrl& url = index.data().toUrl();
QPixmap pixmap = LookupPixmap(url, option_copy.decorationSize);
float device_pixel_ratio = 1.0f;
#ifdef Q_OS_DARWIN
QWidget* parent_widget = reinterpret_cast<QWidget*>(parent());
device_pixel_ratio = mac::GetDevicePixelRatio(parent_widget);
#endif
// Draw the pixmap in the middle of the rectangle
QRect draw_rect(QPoint(0, 0), option_copy.decorationSize);
QRect draw_rect(QPoint(0, 0), option_copy.decorationSize / device_pixel_ratio);
draw_rect.moveCenter(option_copy.rect.center());
painter->drawPixmap(draw_rect, pixmap);

View File

@ -101,6 +101,7 @@ PlaylistListContainer::~PlaylistListContainer() {
void PlaylistListContainer::showEvent(QShowEvent* e) {
// Loading icons is expensive so only do it when the view is first opened
if (loaded_icons_) {
QWidget::showEvent(e);
return;
}
loaded_icons_ = true;

View File

@ -123,6 +123,7 @@ return_song:
void ASXParser::Save(const SongList& songs, QIODevice* device, const QDir&) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeStartDocument();
{
StreamElement asx("asx", &writer);

View File

@ -81,6 +81,8 @@ void WplParser::ParseSeq(const QDir& dir, QXmlStreamReader* reader,
void WplParser::Save(const SongList& songs, QIODevice* device,
const QDir& dir) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeProcessingInstruction("wpl", "version=\"1.0\"");
StreamElement smil("smil", &writer);

View File

@ -104,6 +104,8 @@ return_song:
void XSPFParser::Save(const SongList& songs, QIODevice* device, const QDir&) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeStartDocument();
StreamElement playlist("playlist", &writer);
writer.writeAttribute("version", "1");

View File

@ -156,8 +156,8 @@ void PodcastEpisode::BindToQuery(QSqlQuery* query) const {
Song PodcastEpisode::ToSong(const Podcast& podcast) const {
Song ret;
ret.set_valid(true);
ret.set_title(title());
ret.set_artist(author());
ret.set_title(title().simplified());
ret.set_artist(author().simplified());
ret.set_length_nanosec(kNsecPerSec * duration_secs());
ret.set_year(publication_date().date().year());
ret.set_comment(description());
@ -172,7 +172,7 @@ Song PodcastEpisode::ToSong(const Podcast& podcast) const {
// Use information from the podcast if it's set
if (podcast.is_valid()) {
ret.set_album(podcast.title());
ret.set_album(podcast.title().simplified());
ret.set_art_automatic(podcast.ImageUrlLarge().toString());
}

View File

@ -139,7 +139,7 @@ void PodcastService::PopulatePodcastList(QStandardItem* parent) {
void PodcastService::UpdatePodcastText(QStandardItem* item, int unlistened_count) const {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
QString title = podcast.title();
QString title = podcast.title().simplified();
QFont font;
if (unlistened_count > 0) {
@ -159,7 +159,7 @@ void PodcastService::UpdateEpisodeText(QStandardItem* item,
int percent) {
const PodcastEpisode episode = item->data(Role_Episode).value<PodcastEpisode>();
QString title = episode.title();
QString title = episode.title().simplified();
QString tooltip;
QFont font;
QIcon icon;
@ -237,7 +237,7 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
QStandardItem* PodcastService::CreatePodcastEpisodeItem(const PodcastEpisode& episode) {
QStandardItem* item = new QStandardItem;
item->setText(episode.title());
item->setText(episode.title().simplified());
item->setData(Type_Episode, InternetModel::Role_Type);
item->setData(QVariant::fromValue(episode), Role_Episode);
item->setData(InternetModel::PlayBehaviour_UseSongLoader, InternetModel::Role_PlayBehaviour);

View File

@ -48,13 +48,19 @@ bool ArtistInfoView::NeedsUpdate(const Song& old_metadata, const Song& new_metad
return old_metadata.artist() != new_metadata.artist();
}
void ArtistInfoView::InfoResultReady (int id, const CollapsibleInfoPane::Data& data) {
if (id != current_request_id_)
return;
AddSection (new CollapsibleInfoPane(data, this));
CollapseSections();
}
void ArtistInfoView::ResultReady(int id, const SongInfoFetcher::Result& result) {
if (id != current_request_id_)
return;
Clear();
if (!result.images_.isEmpty()) {
// Image view goes at the top
PrettyImageView* image_view = new PrettyImageView(network_, this);
@ -64,10 +70,6 @@ void ArtistInfoView::ResultReady(int id, const SongInfoFetcher::Result& result)
image_view->AddImage(url);
}
}
foreach (const CollapsibleInfoPane::Data& data, result.info_) {
AddSection(new CollapsibleInfoPane(data, this));
}
CollapseSections();
}

View File

@ -36,6 +36,7 @@ public:
~ArtistInfoView();
protected:
virtual void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
bool NeedsUpdate(const Song& old_metadata, const Song& new_metadata) const;
protected slots:
@ -43,3 +44,4 @@ protected slots:
};
#endif // ARTISTINFOVIEW_H

View File

@ -89,18 +89,24 @@ void EchoNestBiographies::RequestFinished() {
data.icon_ = site_icons_[canonical_site];
SongInfoTextView* editor = new SongInfoTextView;
QString text;
// Add a link to the bio webpage at the top if we have one
if (!bio.url().isEmpty()) {
text += "<p><a href=\"" + bio.url().toEncoded() + "\">" +
tr("Open in your browser") +
"</a></p>";
}
text += bio.text();
if (bio.site() == "last.fm") {
// Echonest lost formatting and it seems there is currently no plans on Echonest side for changing this.
// But with last.fm, we can guess newlines: " " corresponds to a newline
// (this seems to be because on last.fm' website, extra blank is inserted
// before <br /> tag, and this blank is kept).
// This is tricky, but this make the display nicer for last.fm biographies.
QString copy(bio.text());
copy.replace(" ","<p>");
editor->SetHtml(copy);
} else {
editor->SetHtml(bio.text());
text.replace(" ","<p>");
}
editor->SetHtml(text);
data.contents_ = editor;
emit InfoReady(request->id_, data);

View File

@ -64,6 +64,8 @@ SongInfoBase::SongInfoBase(QWidget* parent)
connect(fetcher_, SIGNAL(ResultReady(int,SongInfoFetcher::Result)),
SLOT(ResultReady(int,SongInfoFetcher::Result)));
connect(fetcher_, SIGNAL(InfoResultReady(int,CollapsibleInfoPane::Data)),
SLOT(InfoResultReady(int,CollapsibleInfoPane::Data)));
}
void SongInfoBase::Clear() {
@ -142,9 +144,13 @@ void SongInfoBase::Update(const Song& metadata) {
// Do this after the new pane has been shown otherwise it'll just grab a
// black rectangle.
Clear ();
QTimer::singleShot(0, fader_, SLOT(StartBlur()));
}
void SongInfoBase::InfoResultReady (int id, const CollapsibleInfoPane::Data& data) {
}
void SongInfoBase::ResultReady(int id, const SongInfoFetcher::Result& result) {
foreach (const CollapsibleInfoPane::Data& data, result.info_) {
delete data.contents_;
@ -208,10 +214,6 @@ void SongInfoBase::ReloadSettings() {
QMetaObject::invokeMethod(contents, "ReloadSettings");
}
QSettings s;
s.beginGroup(kSettingsGroup);
fetcher_->set_timeout(s.value("timeout", SongInfoFetcher::kDefaultTimeoutDuration).toInt());
}
void SongInfoBase::ConnectWidget(QWidget* widget) {
@ -225,3 +227,4 @@ void SongInfoBase::ConnectWidget(QWidget* widget) {
connect(widget, SIGNAL(DoGlobalSearch(QString)), SIGNAL(DoGlobalSearch(QString)));
}
}

View File

@ -63,6 +63,7 @@ protected:
void CollapseSections();
protected slots:
virtual void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
virtual void ResultReady(int id, const SongInfoFetcher::Result& result);
protected:
@ -94,3 +95,4 @@ private:
};
#endif // SONGINFOBASE_H

View File

@ -68,6 +68,10 @@ void SongInfoFetcher::InfoReady(int id, const CollapsibleInfoPane::Data& data) {
if (!results_.contains(id))
return;
results_[id].info_ << data;
if (!waiting_for_.contains(id))
return;
emit InfoResultReady (id, data);
}
void SongInfoFetcher::ProviderFinished(int id) {
@ -107,3 +111,4 @@ void SongInfoFetcher::Timeout(int id) {
// Remove the timer
delete timeout_timers_.take(id);
}

View File

@ -40,9 +40,7 @@ public:
QList<CollapsibleInfoPane::Data> info_;
};
static const int kDefaultTimeoutDuration = 2500; // msec
void set_timeout(int msec) { timeout_duration_ = msec; }
static const int kDefaultTimeoutDuration = 25000; // msec
void AddProvider(SongInfoProvider* provider);
int FetchInfo(const Song& metadata);
@ -50,6 +48,7 @@ public:
QList<SongInfoProvider*> providers() const { return providers_; }
signals:
void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
void ResultReady(int id, const SongInfoFetcher::Result& result);
private slots:
@ -72,3 +71,4 @@ private:
};
#endif // SONGINFOFETCHER_H

View File

@ -59,8 +59,6 @@ void SongInfoSettingsPage::Load() {
s.beginGroup(SongInfoTextView::kSettingsGroup);
ui_->song_info_font_size->setValue(
s.value("font_size", SongInfoTextView::kDefaultFontSize).toReal());
ui_->song_info_timeout->setValue(
s.value("timeout", SongInfoFetcher::kDefaultTimeoutDuration).toInt());
s.endGroup();
QList<const UltimateLyricsProvider*> providers = dialog()->song_info_view()->lyric_providers();
@ -80,7 +78,6 @@ void SongInfoSettingsPage::Save() {
s.beginGroup(SongInfoTextView::kSettingsGroup);
s.setValue("font_size", ui_->song_info_font_preview->font().pointSizeF());
s.setValue("timeout", ui_->song_info_timeout->value());
s.endGroup();
s.beginGroup(SongInfoView::kSettingsGroup);

View File

@ -75,38 +75,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Network</string>
</property>
<layout class="QFormLayout" name="formLayout_6">
<item row="0" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Timeout</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="song_info_timeout">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>1000</number>
</property>
<property name="maximum">
<number>60000</number>
</property>
<property name="value">
<number>2500</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_6">
<property name="sizePolicy">

View File

@ -76,19 +76,17 @@ bool SongInfoView::NeedsUpdate(const Song& old_metadata, const Song& new_metadat
old_metadata.artist() != new_metadata.artist();
}
void SongInfoView::ResultReady(int id, const SongInfoFetcher::Result& result) {
void SongInfoView::InfoResultReady (int id, const CollapsibleInfoPane::Data& data) {
if (id != current_request_id_)
return;
Clear();
foreach (const CollapsibleInfoPane::Data& data, result.info_) {
AddSection(new CollapsibleInfoPane(data, this));
}
AddSection (new CollapsibleInfoPane(data, this));
CollapseSections();
}
void SongInfoView::ResultReady(int id, const SongInfoFetcher::Result& result) {
}
void SongInfoView::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
@ -170,3 +168,4 @@ QList<const UltimateLyricsProvider*> SongInfoView::lyric_providers() const {
qSort(ret.begin(), ret.end(), CompareLyricProviders);
return ret;
}

View File

@ -43,7 +43,8 @@ protected:
bool NeedsUpdate(const Song& old_metadata, const Song& new_metadata) const;
protected slots:
void ResultReady(int id, const SongInfoFetcher::Result& result);
virtual void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
virtual void ResultReady(int id, const SongInfoFetcher::Result& result);
private:
SongInfoProvider* ProviderByName(const QString& name) const;
@ -56,3 +57,4 @@ private:
};
#endif // SONGINFOVIEW_H

View File

@ -141,7 +141,7 @@ void UltimateLyricsProvider::LyricsFetched() {
ApplyExcludeRule(rule, &lyrics);
}
if (!content.isEmpty()) {
if (!content.isEmpty() and HTMLHasAlphaNumeric(content)) {
lyrics = content;
break;
}
@ -150,7 +150,7 @@ void UltimateLyricsProvider::LyricsFetched() {
lyrics = original_content;
}
if (!lyrics.isEmpty()) {
if (!lyrics.isEmpty() and HTMLHasAlphaNumeric(lyrics)) {
CollapsibleInfoPane::Data data;
data.id_ = "ultimatelyrics/" + name_;
data.title_ = tr("Lyrics from %1").arg(name_);
@ -317,3 +317,19 @@ QString UltimateLyricsProvider::NoSpace(const QString& text) {
ret.remove(' ');
return ret;
}
// tells whether a html block has alphanumeric characters (skipping tags)
// TODO: handle special characters (e.g. &reg; &aacute;)
bool UltimateLyricsProvider::HTMLHasAlphaNumeric(const QString& html) {
bool in_tag = false;
foreach (const QChar& c, html) {
if (!in_tag and c.isLetterOrNumber())
return true;
else if (c == QChar('<'))
in_tag = true;
else if (c == QChar('>'))
in_tag = false;
}
qLog(Debug) << html;
return false;
}

View File

@ -73,6 +73,7 @@ private:
static QString FirstChar(const QString& text);
static QString TitleCase(const QString& text);
static QString NoSpace(const QString& text);
static bool HTMLHasAlphaNumeric(const QString& html);
void ReplaceField(const QString& tag, const QString& value, QString* text) const;
void ReplaceFields(const Song& metadata, QString* text) const;

View File

@ -20,6 +20,7 @@
#include "transcoderoptionsdialog.h"
#include "ui_transcodedialog.h"
#include "ui_transcodelogdialog.h"
#include "ui/iconloader.h"
#include "ui/mainwindow.h"
#include "widgets/fileview.h"
@ -35,6 +36,7 @@
const char* TranscodeDialog::kSettingsGroup = "Transcoder";
const int TranscodeDialog::kProgressInterval = 500;
const int TranscodeDialog::kMaxDestinationItems = 10;
static bool ComparePresetsByName(const TranscoderPreset& left,
const TranscoderPreset& right) {
@ -103,6 +105,8 @@ TranscodeDialog::TranscodeDialog(QWidget *parent)
connect(close_button_, SIGNAL(clicked()), SLOT(hide()));
connect(ui_->details, SIGNAL(clicked()), log_dialog_, SLOT(show()));
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
connect(transcoder_, SIGNAL(JobComplete(QString,bool)), SLOT(JobComplete(QString,bool)));
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
@ -138,7 +142,8 @@ void TranscodeDialog::Start() {
// Add jobs to the transcoder
for (int i=0 ; i<file_model->rowCount() ; ++i) {
QString filename = file_model->index(i, 0).data(Qt::UserRole).toString();
transcoder_->AddJob(filename, preset);
QString outfilename = GetOutputFileName(filename, preset);
transcoder_->AddJob(filename, preset, outfilename);
}
// Set up the progressbar
@ -265,3 +270,50 @@ void TranscodeDialog::Options() {
dialog.exec();
}
}
// Adds a folder to the destination box.
void TranscodeDialog::AddDestination() {
int index = ui_->destination->currentIndex();
QString initial_dir = (!ui_->destination->itemData(index).isNull() ?
ui_->destination->itemData(index).toString() :
QDir::homePath());
QString dir = QFileDialog::getExistingDirectory(
this, tr("Add folder"), initial_dir);
if (!dir.isEmpty()) {
// Keep only a finite number of items in the box.
while (ui_->destination->count() >= kMaxDestinationItems) {
ui_->destination->removeItem(1); // The oldest folder item.
}
QIcon icon = IconLoader::Load("folder");
QVariant data = QVariant::fromValue(dir);
// Do not insert duplicates.
int duplicate_index = ui_->destination->findData(data);
if (duplicate_index == -1) {
ui_->destination->addItem(icon, dir, data);
ui_->destination->setCurrentIndex(ui_->destination->count() - 1);
} else {
ui_->destination->setCurrentIndex(duplicate_index);
}
}
}
// Returns the rightmost non-empty part of 'path'.
QString TranscodeDialog::TrimPath(const QString& path) const {
return path.section('/', -1, -1, QString::SectionSkipEmpty);
}
QString TranscodeDialog::GetOutputFileName(const QString& input,
const TranscoderPreset &preset) const {
QString path = ui_->destination->itemData(
ui_->destination->currentIndex()).toString();
if (path.isEmpty()) {
// Keep the original path.
return input.section('.', 0, -2) + '.' + preset.extension_;
} else {
QString file_name = TrimPath(input);
file_name = file_name.section('.', 0, -2);
return path + '/' + file_name + '.' + preset.extension_;
}
}

View File

@ -25,6 +25,8 @@ class Transcoder;
class Ui_TranscodeDialog;
class Ui_TranscodeLogDialog;
struct TranscoderPreset;
class TranscodeDialog : public QDialog {
Q_OBJECT
@ -34,6 +36,7 @@ class TranscodeDialog : public QDialog {
static const char* kSettingsGroup;
static const int kProgressInterval;
static const int kMaxDestinationItems;
void SetFilenames(const QStringList& filenames);
@ -49,11 +52,15 @@ class TranscodeDialog : public QDialog {
void LogLine(const QString& message);
void AllJobsComplete();
void Options();
void AddDestination();
private:
void SetWorking(bool working);
void UpdateStatusText();
void UpdateProgress();
QString TrimPath(const QString& path) const;
QString GetOutputFileName(const QString& input,
const TranscoderPreset& preset) const;
private:
Ui_TranscodeDialog* ui_;

View File

@ -98,7 +98,7 @@
<property name="title">
<string>Output options</string>
</property>
<layout class="QFormLayout" name="formLayout">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
@ -107,25 +107,21 @@
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QComboBox" name="format">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="options">
<property name="text">
<string>Options...</string>
</property>
</widget>
</item>
</layout>
<widget class="QComboBox" name="format">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="options">
<property name="text">
<string>Options...</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
@ -136,6 +132,15 @@
</item>
<item row="1" column="1">
<widget class="QComboBox" name="destination">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Alongside the originals</string>
@ -143,6 +148,13 @@
</item>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="select">
<property name="text">
<string>Select...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -196,7 +208,6 @@
<tabstop>add</tabstop>
<tabstop>remove</tabstop>
<tabstop>format</tabstop>
<tabstop>destination</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>

View File

@ -18,6 +18,7 @@
#include "core/application.h"
#include "core/logging.h"
#include "core/utilities.h"
#include "covers/albumcoverfetcher.h"
#include "covers/albumcoverloader.h"
#include "covers/currentartloader.h"
@ -29,7 +30,6 @@
#include "ui/iconloader.h"
#include <QAction>
#include <QCryptographicHash>
#include <QDialog>
#include <QDragEnterEvent>
#include <QFileDialog>
@ -63,6 +63,10 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget* parent)
unset_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Unset cover"), this);
show_cover_ = new QAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this);
search_cover_auto_ = new QAction(IconLoader::Load("find"), tr("Search automatically"), this);
search_cover_auto_->setCheckable(true);
search_cover_auto_->setChecked(false);
separator_ = new QAction(this);
separator_->setSeparator(true);
}
@ -77,6 +81,9 @@ void AlbumCoverChoiceController::SetApplication(Application* app) {
cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
cover_searcher_ = new AlbumCoverSearcher(QIcon(":/nocover.png"), app, this);
cover_searcher_->Init(cover_fetcher_);
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64,QImage,CoverSearchStatistics)),
this, SLOT(AlbumCoverFetched(quint64,QImage,CoverSearchStatistics)));
}
QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
@ -204,6 +211,27 @@ void AlbumCoverChoiceController::ShowCover(const Song& song) {
dialog->show();
}
void AlbumCoverChoiceController::SearchCoverAutomatically(const Song& song) {
qint64 id = cover_fetcher_->FetchAlbumCover(song.artist(), song.album());
cover_fetching_tasks_[id] = song;
}
void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id,
const QImage& image,
const CoverSearchStatistics& statistics) {
Song song;
if (cover_fetching_tasks_.contains(id)) {
song = cover_fetching_tasks_.take(id);
}
if (!image.isNull()) {
QString cover = SaveCoverInCache(song.artist(), song.album(), image);
SaveCover(&song, cover);
}
emit AutomaticCoverSearchDone();
}
void AlbumCoverChoiceController::SaveCover(Song* song, const QString &cover) {
if(song->is_valid() && song->id() != -1) {
song->set_art_manual(cover);
@ -219,11 +247,7 @@ QString AlbumCoverChoiceController::SaveCoverInCache(
const QString& artist, const QString& album, const QImage& image) {
// Hash the artist and album into a filename for the image
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(artist.toLower().toUtf8().constData());
hash.addData(album.toLower().toUtf8().constData());
QString filename(hash.result().toHex() + ".jpg");
QString filename(Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg");
QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename);
// Make sure this directory exists first

View File

@ -30,6 +30,8 @@ class CoverFromURLDialog;
class QFileDialog;
class Song;
struct CoverSearchStatistics;
// Controller for the common album cover related menu options.
class AlbumCoverChoiceController : public QWidget {
Q_OBJECT
@ -52,6 +54,7 @@ class AlbumCoverChoiceController : public QWidget {
QAction* search_for_cover_action() const { return search_for_cover_; }
QAction* unset_cover_action() const { return unset_cover_; }
QAction* show_cover_action() const { return show_cover_; }
QAction* search_cover_auto_action() const { return search_cover_auto_; }
// Returns QAction* for every operation implemented by this controller.
// The list contains QAction* for:
@ -91,6 +94,9 @@ class AlbumCoverChoiceController : public QWidget {
// Shows the cover of given song in it's original size.
void ShowCover(const Song& song);
// Search for covers automatically
void SearchCoverAutomatically(const Song& song);
// Saves the chosen cover as manual cover path of this song in library.
void SaveCover(Song* song, const QString& cover);
@ -103,6 +109,13 @@ class AlbumCoverChoiceController : public QWidget {
static bool CanAcceptDrag(const QDragEnterEvent* e);
signals:
void AutomaticCoverSearchDone();
private slots:
void AlbumCoverFetched(quint64 id, const QImage& image,
const CoverSearchStatistics& statistics);
private:
QString GetInitialPathForFileDialog(const Song& song,
const QString& filename);
@ -124,6 +137,9 @@ private:
QAction* search_for_cover_;
QAction* unset_cover_;
QAction* show_cover_;
QAction* search_cover_auto_;
QMap<quint64, Song> cover_fetching_tasks_;
};
#endif // ALBUMCOVERCHOICECONTROLLER_H

View File

@ -115,6 +115,8 @@ void Equalizer::LoadDefaultPresets() {
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Classical"), Params(0, 0, 0, 0, 0, 0, -40, -40, -40, -50));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Club"), Params(0, 0, 20, 30, 30, 30, 20, 0, 0, 0));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Dance"), Params(50, 35, 10, 0, 0, -30, -40, -40, 0, 0));
// Dubstep equalizer created by Devyn Collier Johnson
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Dubstep"), Params(0, 36, 85, 58, 30, 0, 36, 60, 96, 62, 0));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Full Bass"), Params(70, 70, 70, 40, 20, -45, -50, -55, -55, -55));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Full Treble"), Params(-50, -50, -50, -25, 15, 55, 80, 80, 80, 85));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Full Bass + Treble"), Params(35, 30, 0, -40, -25, 10, 45, 55, 60, 60));
@ -123,6 +125,8 @@ void Equalizer::LoadDefaultPresets() {
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Live"), Params(-25, 0, 20, 25, 30, 30, 20, 15, 15, 10));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Party"), Params(35, 35, 0, 0, 0, 0, 0, 0, 35, 35));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Pop"), Params(-10, 25, 35, 40, 25, -5, -15, -15, -10, -10));
// Psychedelic equalizer created by Devyn Collier Johnson
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Psychedelic"), Params(100, 100, 0, 40, 0, 67, 79, 0, 30, -100, 37));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Reggae"), Params(0, 0, -5, -30, 0, -35, -35, 0, 0, 0));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Rock"), Params(40, 25, -30, -40, -20, 20, 45, 55, 55, 55));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Soft"), Params(25, 10, -5, -15, -5, 20, 45, 50, 55, 60));

View File

@ -41,6 +41,7 @@
#include "devices/devicemanager.h"
#include "devices/devicestatefiltermodel.h"
#include "devices/deviceview.h"
#include "devices/deviceviewcontainer.h"
#include "engines/enginebase.h"
#include "engines/gstengine.h"
#include "globalsearch/globalsearch.h"
@ -173,7 +174,8 @@ MainWindow::MainWindow(Application* app,
file_view_(new FileView(this)),
playlist_list_(new PlaylistListContainer(this)),
internet_view_(new InternetViewContainer(this)),
device_view_(new DeviceView(this)),
device_view_container_(new DeviceViewContainer(this)),
device_view_(device_view_container_->view()),
song_info_view_(new SongInfoView(this)),
artist_info_view_(new ArtistInfoView(this)),
settings_dialog_(NULL),
@ -238,7 +240,7 @@ MainWindow::MainWindow(Application* app,
ui_->tabs->AddTab(file_view_, IconLoader::Load("document-open"), tr("Files"));
ui_->tabs->AddTab(playlist_list_, IconLoader::Load("view-media-playlist"), tr("Playlists"));
ui_->tabs->AddTab(internet_view_, IconLoader::Load("applications-internet"), tr("Internet"));
ui_->tabs->AddTab(device_view_, IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Devices"));
ui_->tabs->AddTab(device_view_container_, IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Devices"));
ui_->tabs->AddSpacer();
ui_->tabs->AddTab(song_info_view_, IconLoader::Load("view-media-lyrics"), tr("Song info"));
ui_->tabs->AddTab(artist_info_view_, IconLoader::Load("x-clementine-artist"), tr("Artist info"));
@ -509,6 +511,7 @@ MainWindow::MainWindow(Application* app,
playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Copy to device..."), this, SLOT(PlaylistCopyToDevice()));
playlist_delete_ = playlist_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(PlaylistDelete()));
playlist_open_in_browser_ = playlist_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(PlaylistOpenInBrowser()));
playlist_show_in_library_ = playlist_menu_->addAction(IconLoader::Load("edit-find"), tr("Show in library..."), this, SLOT(ShowInLibrary()));
playlist_menu_->addSeparator();
playlistitem_actions_separator_ = playlist_menu_->addSeparator();
playlist_menu_->addAction(ui_->action_clear_playlist);
@ -690,6 +693,10 @@ MainWindow::MainWindow(Application* app,
app_->playlist_manager()->Init(app_->library_backend(), app_->playlist_backend(),
ui_->playlist_sequence, ui_->playlist);
// This connection must be done after the playlists have been initialized.
connect(this, SIGNAL(StopAfterToggled(bool)),
osd_, SLOT(StopAfterToggle(bool)));
// We need to connect these global shortcuts here after the playlist have been initialized
connect(global_shortcuts_, SIGNAL(CycleShuffleMode()), app_->playlist_manager()->sequence(), SLOT(CycleShuffleMode()));
connect(global_shortcuts_, SIGNAL(CycleRepeatMode()), app_->playlist_manager()->sequence(), SLOT(CycleRepeatMode()));
@ -1058,7 +1065,8 @@ void MainWindow::ToggleShowHide() {
}
void MainWindow::StopAfterCurrent() {
app_->playlist_manager()->current()->StopAfter(app_->playlist_manager()->current()->current_row());
app_->playlist_manager()->active()->StopAfter(app_->playlist_manager()->active()->current_row());
emit StopAfterToggled(app_->playlist_manager()->active()->stop_after_current());
}
void MainWindow::closeEvent(QCloseEvent* event) {
@ -1355,6 +1363,7 @@ void MainWindow::PlaylistRightClick(const QPoint& global_pos, const QModelIndex&
ui_->action_edit_value->setVisible(editable);
ui_->action_remove_from_playlist->setEnabled(!selection.isEmpty());
playlist_show_in_library_->setVisible(false);
playlist_copy_to_library_->setVisible(false);
playlist_move_to_library_->setVisible(false);
playlist_organise_->setVisible(false);
@ -1403,6 +1412,7 @@ void MainWindow::PlaylistRightClick(const QPoint& global_pos, const QModelIndex&
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (item->IsLocalLibraryItem() && item->Metadata().id() != -1) {
playlist_organise_->setVisible(editable);
playlist_show_in_library_->setVisible(editable);
} else {
playlist_copy_to_library_->setVisible(editable);
playlist_move_to_library_->setVisible(editable);
@ -1678,6 +1688,22 @@ void MainWindow::AddCDTracks() {
AddToPlaylist(data);
}
void MainWindow::ShowInLibrary() {
// Show the first valid selected track artist/album in LibraryView
QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
SongList songs;
foreach (const QModelIndex& proxy_index, proxy_indexes) {
QModelIndex index = app_->playlist_manager()->current()->proxy()->mapToSource(proxy_index);
if (app_->playlist_manager()->current()->item_at(index.row())->IsLocalLibraryItem()) {
songs << app_->playlist_manager()->current()->item_at(index.row())->Metadata();
break;
}
}
QString search = "artist:"+songs[0].artist()+" album:"+songs[0].album();
library_view_->filter()->ShowInLibrary(search);
}
void MainWindow::PlaylistRemoveCurrent() {
ui_->playlist->view()->RemoveSelected();
}

View File

@ -44,6 +44,7 @@ class CoverProviders;
class Database;
class DeviceManager;
class DeviceView;
class DeviceViewContainer;
class EditTagDialog;
class Equalizer;
class ErrorDialog;
@ -135,6 +136,10 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void Activate();
bool LoadUrl(const QString& url);
signals:
// Signals that stop playing after track was toggled.
void StopAfterToggled(bool stop);
private slots:
void FilePathChanged(const QString& path);
@ -168,6 +173,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void PlaylistOrganiseSelected(bool copy);
void PlaylistDelete();
void PlaylistOpenInBrowser();
void ShowInLibrary();
void ChangeLibraryQueryMode(QAction* action);
@ -293,6 +299,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
boost::scoped_ptr<RipCD> rip_cd_;
PlaylistListContainer* playlist_list_;
InternetViewContainer* internet_view_;
DeviceViewContainer* device_view_container_;
DeviceView* device_view_;
SongInfoView* song_info_view_;
ArtistInfoView* artist_info_view_;
@ -327,6 +334,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
QAction* playlist_stop_after_;
QAction* playlist_undoredo_;
QAction* playlist_organise_;
QAction* playlist_show_in_library_;
QAction* playlist_copy_to_library_;
QAction* playlist_move_to_library_;
QAction* playlist_copy_to_device_;

View File

@ -32,7 +32,7 @@
const char* FileView::kFileFilter = "*.mp3 *.ogg *.flac *.mpc *.m4a *.aac *.wma "
"*.mp4 *.spx *.wav *.m3u *.m3u8 *.pls *.xspf "
"*.asx *.asxini *.cue *.ape *.wv *.mka *.opus "
"*.oga *.mka";
"*.oga *.mka *.mp2";
FileView::FileView(QWidget* parent)
: QWidget(parent),

View File

@ -32,7 +32,8 @@ ExtendedEditor::ExtendedEditor(QWidget* widget, int extra_right_padding,
reset_button_(new QToolButton(widget)),
extra_right_padding_(extra_right_padding),
draw_hint_(draw_hint),
font_point_size_(widget->font().pointSizeF() - 1)
font_point_size_(widget->font().pointSizeF() - 1),
is_rtl_(false)
{
clear_button_->setIcon(IconLoader::Load("edit-clear-locationbar-ltr"));
clear_button_->setIconSize(QSize(16, 16));
@ -118,15 +119,22 @@ void ExtendedEditor::Paint(QPaintDevice* device) {
}
} else {
clear_button_->setVisible(has_clear_button_);
Resize();
}
}
void ExtendedEditor::Resize() {
const QSize sz = clear_button_->sizeHint();
const int frame_width = widget_->style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
clear_button_->move(frame_width, (widget_->rect().height() - sz.height()) / 2);
reset_button_->move(widget_->width() - frame_width - sz.width() - extra_right_padding_,
(widget_->rect().height() - sz.height()) / 2);
const int y = (widget_->rect().height() - sz.height()) / 2;
clear_button_->move(frame_width, y);
if (!is_rtl_) {
reset_button_->move(widget_->width() - frame_width - sz.width() - extra_right_padding_, y);
} else {
reset_button_->move((has_clear_button() ? sz.width() + 4 : 0) + frame_width, y);
}
}
@ -137,6 +145,16 @@ LineEdit::LineEdit(QWidget* parent)
connect(reset_button_, SIGNAL(clicked()), SIGNAL(Reset()));
}
void LineEdit::set_text(const QString& text) {
QLineEdit::setText(text);
// For some reason Qt will detect any text with LTR at the end as LTR, so instead
// compare only the first character
if (!text.isEmpty()) {
set_rtl(QString(text.at(0)).isRightToLeft());
}
}
void LineEdit::paintEvent(QPaintEvent* e) {
QLineEdit::paintEvent(e);
Paint(this);

View File

@ -86,6 +86,7 @@ protected:
int extra_right_padding_;
bool draw_hint_;
qreal font_point_size_;
bool is_rtl_;
};
class LineEdit : public QLineEdit,
@ -102,13 +103,17 @@ public:
// ExtendedEditor
void set_focus() { QLineEdit::setFocus(); }
QString text() const { return QLineEdit::text(); }
void set_text(const QString& text) { QLineEdit::setText(text); }
void set_text(const QString& text);
void set_enabled(bool enabled) { QLineEdit::setEnabled(enabled); }
protected:
void paintEvent(QPaintEvent*);
void resizeEvent(QResizeEvent*);
private:
bool is_rtl() const { return is_rtl_; }
void set_rtl(bool rtl) { is_rtl_ = rtl; }
signals:
void Reset();
};

View File

@ -72,6 +72,7 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
details_(new QTextDocument(this)),
previous_track_opacity_(0.0),
bask_in_his_glory_action_(NULL),
downloading_covers_(false),
aww_(false),
kittens_(NULL),
pending_kitten_(0)
@ -80,6 +81,7 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
QSettings s;
s.beginGroup(kSettingsGroup);
mode_ = Mode(s.value("mode", SmallSongDetails).toInt());
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value("search_for_cover_auto", false).toBool());
// Accept drops for setting album art
setAcceptDrops(true);
@ -96,6 +98,9 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
// Here we add the search automatically action, too!
actions.append(album_cover_choice_controller_->search_cover_auto_action());
connect(album_cover_choice_controller_->cover_from_file_action(),
SIGNAL(triggered()), this, SLOT(LoadCoverFromFile()));
connect(album_cover_choice_controller_->cover_to_file_action(),
@ -108,6 +113,8 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
SIGNAL(triggered()), this, SLOT(UnsetCover()));
connect(album_cover_choice_controller_->show_cover_action(),
SIGNAL(triggered()), this, SLOT(ShowCover()));
connect(album_cover_choice_controller_->search_cover_auto_action(),
SIGNAL(triggered()), this, SLOT(SearchCoverAutomatically()));
menu_->addActions(actions);
menu_->addSeparator();
@ -129,6 +136,9 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
fade_animation_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0
UpdateHeight();
connect(album_cover_choice_controller_, SIGNAL(AutomaticCoverSearchDone()),
this, SLOT(AutomaticCoverSearchDone()));
}
NowPlayingWidget::~NowPlayingWidget() {
@ -233,9 +243,10 @@ void NowPlayingWidget::KittenLoaded(quint64 id, const QImage& image) {
}
}
void NowPlayingWidget::AlbumArtLoaded(const Song& metadata, const QString&,
void NowPlayingWidget::AlbumArtLoaded(const Song& metadata, const QString& uri,
const QImage& image) {
metadata_ = metadata;
downloading_covers_ = false;
if (aww_) {
pending_kitten_ = kittens_->LoadKitten(app_->current_art_loader()->options());
@ -243,6 +254,9 @@ void NowPlayingWidget::AlbumArtLoaded(const Song& metadata, const QString&,
}
SetImage(image);
// Search for cover automatically?
GetCoverAutomatically();
}
void NowPlayingWidget::SetImage(const QImage& image) {
@ -301,6 +315,9 @@ void NowPlayingWidget::DrawContents(QPainter *p) {
} else {
// Draw the cover
p->drawPixmap(0, 0, small_ideal_height_, small_ideal_height_, cover_);
if (downloading_covers_) {
p->drawPixmap(small_ideal_height_ - 18, 6, 16, 16, spinner_animation_->currentPixmap());
}
}
// Draw the details
@ -321,6 +338,9 @@ void NowPlayingWidget::DrawContents(QPainter *p) {
p->drawPixmap(x_offset, kTopBorder, total_size, total_size, hypnotoad_->currentPixmap());
} else {
p->drawPixmap(x_offset, kTopBorder, total_size, total_size, cover_);
if (downloading_covers_) {
p->drawPixmap(total_size - 31, 40, 16, 16, spinner_animation_->currentPixmap());
}
}
// Work out how high the text is going to be
@ -454,6 +474,15 @@ void NowPlayingWidget::ShowCover() {
album_cover_choice_controller_->ShowCover(metadata_);
}
void NowPlayingWidget::SearchCoverAutomatically() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("search_for_cover_auto", album_cover_choice_controller_->search_cover_auto_action()->isChecked());
// Search for cover automatically?
GetCoverAutomatically();
}
void NowPlayingWidget::Bask() {
big_hypnotoad_.reset(new FullscreenHypnotoad);
big_hypnotoad_->showFullScreen();
@ -472,3 +501,31 @@ void NowPlayingWidget::dropEvent(QDropEvent* e) {
QWidget::dropEvent(e);
}
bool NowPlayingWidget::GetCoverAutomatically() {
// Search for cover automatically?
bool search = album_cover_choice_controller_->search_cover_auto_action()->isChecked() &&
!metadata_.has_manually_unset_cover() &&
metadata_.art_automatic().isEmpty() &&
metadata_.art_manual().isEmpty();
if (search) {
qLog(Debug) << "GetCoverAutomatically";
downloading_covers_ = true;
album_cover_choice_controller_->SearchCoverAutomatically(metadata_);
// Show a spinner animation
spinner_animation_.reset(new QMovie(":/spinner.gif", QByteArray(), this));
connect(spinner_animation_.get(), SIGNAL(updated(const QRect&)), SLOT(update()));
spinner_animation_->start();
update();
}
return search;
}
void NowPlayingWidget::AutomaticCoverSearchDone() {
downloading_covers_ = false;
spinner_animation_.reset();
update();
}

View File

@ -99,9 +99,12 @@ private slots:
void SearchForCover();
void UnsetCover();
void ShowCover();
void SearchCoverAutomatically();
void Bask();
void AutomaticCoverSearchDone();
private:
void CreateModeAction(Mode mode, const QString& text, QActionGroup* group,
QSignalMapper* mapper);
@ -110,6 +113,7 @@ private:
void DrawContents(QPainter* p);
void SetImage(const QImage& image);
void ScaleCover();
bool GetCoverAutomatically();
private:
Application* app_;
@ -144,6 +148,9 @@ private:
boost::scoped_ptr<QMovie> hypnotoad_;
boost::scoped_ptr<FullscreenHypnotoad> big_hypnotoad_;
boost::scoped_ptr<QMovie> spinner_animation_;
bool downloading_covers_;
bool aww_;
KittenLoader* kittens_;
quint64 pending_kitten_;

View File

@ -166,6 +166,11 @@ void OSD::Stopped() {
ShowMessage(QCoreApplication::applicationName(), tr("Stopped"));
}
void OSD::StopAfterToggle(bool stop) {
ShowMessage(QCoreApplication::applicationName(),
tr("Stop playing after track: %1").arg(stop ? tr("On") : tr("Off")));
}
void OSD::PlaylistFinished() {
// We get a PlaylistFinished followed by a Stopped from the player
ignore_next_stopped_ = true;

View File

@ -71,6 +71,7 @@ class OSD : public QObject {
void Paused();
void Stopped();
void StopAfterToggle(bool stop);
void PlaylistFinished();
void VolumeChanged(int value);
void MagnatuneDownloadFinished(const QStringList& albums);

View File

@ -38,7 +38,7 @@ TrackSlider::TrackSlider(QWidget* parent)
{
ui_->setupUi(this);
QFont font("Courier");
QFont font("Comic Sans MS");
ui_->elapsed->setFont(font);
ui_->remaining->setFont(font);