diff --git a/boombox/CMakeLists.txt b/boombox/CMakeLists.txt new file mode 100644 index 00000000..d4ec4b2a --- /dev/null +++ b/boombox/CMakeLists.txt @@ -0,0 +1,20 @@ +project(boombox) + +find_package(KDE4 REQUIRED) +include (KDE4Defaults) + +find_package(Sqlite REQUIRED) +find_package(Taglib REQUIRED) + +include_directories(${SQLITE_INCLUDE_DIR} ${KDE4_INCLUDES} ${QT_INCLUDES} ${TAGLIB_INCLUDES} ${CMAKE_CURRENT_BINARY_DIR}) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + message(WARNING "enabling debug output!") + add_definitions(-DDEBUG) +else() + add_definitions(-DQT_NO_DEBUG_OUTPUT) +endif() + + +add_subdirectory( src ) +add_subdirectory( icons ) diff --git a/boombox/icons/CMakeLists.txt b/boombox/icons/CMakeLists.txt new file mode 100644 index 00000000..7affd27e --- /dev/null +++ b/boombox/icons/CMakeLists.txt @@ -0,0 +1 @@ +kde4_install_icons( ${ICON_INSTALL_DIR} ) diff --git a/boombox/icons/hi16-app-boombox.png b/boombox/icons/hi16-app-boombox.png new file mode 100644 index 00000000..0d3aaaef Binary files /dev/null and b/boombox/icons/hi16-app-boombox.png differ diff --git a/boombox/icons/hi32-app-boombox.png b/boombox/icons/hi32-app-boombox.png new file mode 100644 index 00000000..7141edc5 Binary files /dev/null and b/boombox/icons/hi32-app-boombox.png differ diff --git a/boombox/icons/hi48-app-boombox.png b/boombox/icons/hi48-app-boombox.png new file mode 100644 index 00000000..1f3130cb Binary files /dev/null and b/boombox/icons/hi48-app-boombox.png differ diff --git a/boombox/icons/hi64-app-boombox.png b/boombox/icons/hi64-app-boombox.png new file mode 100644 index 00000000..68a66ec7 Binary files /dev/null and b/boombox/icons/hi64-app-boombox.png differ diff --git a/boombox/icons/hisc-app-boombox.svgz b/boombox/icons/hisc-app-boombox.svgz new file mode 100644 index 00000000..a5c828e6 Binary files /dev/null and b/boombox/icons/hisc-app-boombox.svgz differ diff --git a/boombox/src/CMakeLists.txt b/boombox/src/CMakeLists.txt new file mode 100644 index 00000000..ebf8c563 --- /dev/null +++ b/boombox/src/CMakeLists.txt @@ -0,0 +1,47 @@ +set(boombox_SRCS + bbfilechangejob.cpp + bbresultview.cpp + bbfilterdock.cpp + bbsetmodel.cpp + bbmetadata.cpp + bbdatabase.cpp + threadjob.cpp + dbupdatejob.cpp + dbqueryjob.cpp + bbmetainfodialog.cpp + bbmainwindow.cpp + bbplaylistsystem.cpp + bbcollectiontab.cpp + bbfilesystemtab.cpp + bbstreamstab.cpp + bbstreameditform.cpp + mpris2player.cpp + main.cpp + directoriespage.ui + bbmetainfodialog.ui + bbstreameditform.ui +) + +qt4_add_dbus_adaptor(boombox_SRCS dbus/org.mpris.MediaPlayer2.xml + mpris2player.h Mpris2Player) +qt4_add_dbus_adaptor(boombox_SRCS dbus/org.mpris.MediaPlayer2.Player.xml + mpris2player.h Mpris2Player) +qt4_add_dbus_interface(boombox_SRCS dbus/org.mpris.MediaPlayer2.Player.xml + mpris2playerclient) + +kde4_add_kcfg_files(boombox_SRCS bbsettings.kcfgc) + +add_executable(boombox ${boombox_SRCS}) + +target_link_libraries(boombox ${KDE4_KDEUI_LIBS} ${KDE4_PHONON_LIBS} + ${KDE4_THREADWEAVER_LIBS} ${KDE4_KFILE_LIBS} + ${SQLITE_LIBRARIES} ${TAGLIB_LIBRARIES}) + +add_definitions(${TAGLIB_CFLAGS} ${SQLITE_DEFINITIONS}) + + +########### install files ############### + +install(TARGETS boombox ${INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES boombox.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) +install(FILES boombox.kcfg DESTINATION ${KCFG_INSTALL_DIR}) diff --git a/boombox/src/bbcollectiontab.cpp b/boombox/src/bbcollectiontab.cpp new file mode 100644 index 00000000..c227b7a8 --- /dev/null +++ b/boombox/src/bbcollectiontab.cpp @@ -0,0 +1,584 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbcollectiontab.h" +#include "bbmainwindow.h" +#include "bbsettings.h" +#include "dbupdatejob.h" +#include "bbsetmodel.h" +#include "dbqueryjob.h" +#include "bbfilterdock.h" +#include "bbresultview.h" +#include "bbsetmodel.h" +#include "bbfilechangejob.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace ThreadWeaver; + +BBCollectionTab::BBCollectionTab(int pTabNumber) + : BBPlaylistSystem(pTabNumber) +{ + setWindowTitle(i18n("Music Collection")); + + mActionCollection = new KActionCollection(this); + setupActions(); + setupToolBars(); + + setDockNestingEnabled(true); + + BBFilterDock *lFilterDock = new BBFilterDock("artist", i18n("Artists"), this); + mActionCollection->addAction("show_artist", lFilterDock->toggleViewAction()); + addDockWidget(Qt::LeftDockWidgetArea, lFilterDock); + mDocks.append(lFilterDock); + + BBFilterDock *lFilterDock2 = new BBFilterDock("album", i18n("Albums"), this); + mActionCollection->addAction("show_album", lFilterDock2->toggleViewAction()); + splitDockWidget(lFilterDock, lFilterDock2, Qt::Horizontal); + mDocks.append(lFilterDock2); + + lFilterDock = new BBFilterDock("genre", i18n("Genres"), this); + mActionCollection->addAction("show_genre", lFilterDock->toggleViewAction()); + addDockWidget(Qt::RightDockWidgetArea, lFilterDock); + mDocks.append(lFilterDock); + + lFilterDock = new BBFilterDock("year", i18n("Years"), this); + mActionCollection->addAction("show_year", lFilterDock->toggleViewAction()); + addDockWidget(Qt::RightDockWidgetArea, lFilterDock); + lFilterDock->hide(); + mDocks.append(lFilterDock); + + mModel = new BBAlbumSongModel(this, this); + + mResultView = new BBResultView(this, mModel); + setCentralWidget(mResultView); + + qRegisterMetaType >("QSet"); + qRegisterMetaType >("QSet"); + qRegisterMetaType >("QSet"); + + setAutoSaveSettings("CollectionTab"); + mCurrentSong = 0; +} + +void BBCollectionTab::setupActions() +{ + KStandardAction::openNew(this, SLOT(createNewPlaylist()), mActionCollection); + KStandardAction::save(this, SLOT(savePlaylist()), mActionCollection); + + KAction *lAction = new KAction(KIcon("view-refresh"), i18n("Update database"), this); + lAction->setShortcut(KShortcut(Qt::CTRL + Qt::Key_U)); + mActionCollection->addAction("update_database", lAction); + connect(lAction, SIGNAL(triggered()), SLOT(updateDatabase())); + + lAction = new KAction(KIcon("edit-delete"), i18n("Delete"), this); + mActionCollection->addAction("delete_playlist", lAction); + connect(lAction, SIGNAL(triggered()), SLOT(deletePlaylist())); +} + +void BBCollectionTab::setupToolBars() +{ + mPlaylistToolBar = toolBar("playlist_toolbar"); + mPlaylistToolBar->setWindowTitle(i18n("Playlist Toolbar")); + mPlaylistToolBar->addAction(mActionCollection->action(KStandardAction::name(KStandardAction::New))); + mPlaylistComboBox = new QComboBox(this); + mPlaylistComboBox->setInsertPolicy(QComboBox::InsertAlphabetically); + mPlaylistComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + mPlaylistComboBox->setMinimumWidth(mPlaylistComboBox->sizeHint().width()*2); + mPlaylistToolBar->addWidget(mPlaylistComboBox); + mPlaylistToolBar->addAction(mActionCollection->action(KStandardAction::name(KStandardAction::Save))); + + mControlToolBar = toolBar("control_toolbar"); + mControlToolBar->setWindowTitle(i18n("Playback Control Toolbar")); + mControlToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); + mControlsContainer = new QWidget(this); + mControlsLayout = new QHBoxLayout(mControlsContainer); + mControlToolBar->addWidget(mControlsContainer); +} + +void BBCollectionTab::embedControls(QWidget *pControls) +{ + pControls->setParent(mControlsContainer); + mControlsLayout->addWidget(pControls); + pControls->show(); +} + +BBTabMenuBar *BBCollectionTab::getTabMenuBar() +{ + BBTabMenuBar *lDataMenuBar = new BBTabMenuBar(); + KMenu *lDataMenu = new KMenu(i18n("Playlist")); + lDataMenu->addAction(mActionCollection->action(KStandardAction::name(KStandardAction::New))); + lDataMenu->addAction(mActionCollection->action(KStandardAction::name(KStandardAction::Save))); + lDataMenu->addAction(mActionCollection->action("delete_playlist")); + lDataMenu->addSeparator(); + lDataMenu->addAction(mActionCollection->action("update_database")); + lDataMenuBar->addMenu(lDataMenu); + + KMenu *lViewMenu = new KMenu(i18n("View")); + foreach(BBFilterDock *i, mDocks) + { + lViewMenu->addAction(mActionCollection->action(QString("show_%1").arg(i->category()))); + } + lDataMenuBar->addMenu(lViewMenu); + return lDataMenuBar; +} + +void BBCollectionTab::createNewPlaylist() +{ + QString lNewName = KInputDialog::getText(i18nc("dialog caption", "New Playlist"), + i18n("Please provide a name for the new playlist:"), + i18nc("default playlist name", "New Playlist")); + if(lNewName.isNull()) + return; + if(mPlaylistComboBox->findText(lNewName) != -1) + { + KMessageBox::sorry(this, i18n("The playlist \"%1\" already exists, please choose another name.").arg(lNewName)); + return; + } + savePlaylist(lNewName); + mPlaylistComboBox->disconnect(SIGNAL(currentIndexChanged(QString))); + mPlaylistComboBox->addItem(lNewName); + mPlaylistComboBox->setCurrentIndex(mPlaylistComboBox->findText(lNewName)); + connect(mPlaylistComboBox, SIGNAL(currentIndexChanged(QString)), SLOT(loadPlaylist(QString))); + + KConfigGroup lListGroup(mConfig, "PlaylistSystem"); + QStringList lListNames; + for(int i = 0; i < mPlaylistComboBox->count(); ++i) + lListNames.append(mPlaylistComboBox->itemText(i)); + + lListGroup.writeEntry("ListNames", lListNames); + mConfig->sync(); +} + +void BBCollectionTab::savePlaylist() +{ + savePlaylist(mPlaylistComboBox->currentText()); +} + +void BBCollectionTab::deletePlaylist() +{ + if(mPlaylistComboBox->count() == 1) + return; + + KConfigGroup lGroup(mConfig, QString("__playlist__") + mPlaylistComboBox->currentText()); + lGroup.deleteGroup(); + + mPlaylistComboBox->removeItem(mPlaylistComboBox->currentIndex()); + + KConfigGroup lListGroup(mConfig, "PlaylistSystem"); + QStringList lListNames; + for(int i = 0; i < mPlaylistComboBox->count(); ++i) + lListNames.append(mPlaylistComboBox->itemText(i)); + lListGroup.writeEntry("ListNames", lListNames); + + mConfig->sync(); +} + +void BBCollectionTab::savePlaylist(const QString &pName) +{ + KConfigGroup lGroup(mConfig, QString("__playlist__") + pName); + foreach(BBFilterDock *i, mDocks) + { + lGroup.writeEntry(i->category(), i->currentSelection()); + lGroup.writeEntry(i->category() + "_filter", i->filterText()); + } + mConfig->sync(); +} + +void BBCollectionTab::updateDatabase() +{ + DBUpdateJob *lJob = new DBUpdateJob(); + mJobTracker->registerJob(lJob); + connect(lJob, SIGNAL(result(KJob*)), SLOT(databaseUpdated(KJob*))); + lJob->start(); +} + +void BBCollectionTab::databaseUpdated(KJob *pJob) +{ + if(!pJob->error()) + { + BBSettings::setDatabaseUpdateTime(QDateTime::currentDateTime()); + BBSettings::self()->writeConfig(); + } +} + +void BBCollectionTab::queryNextSong(BBSongQueryJob &pJob) +{ + QModelIndex lCurrentSongIndex = mModel->indexForFileID(mCurrentSong); + if(lCurrentSongIndex.isValid()) + { + QPersistentModelIndex lIndex; + if(pJob.mShuffle) + lIndex = mModel->nextShuffled(lCurrentSongIndex); + else + lIndex = mModel->nextLinear(lCurrentSongIndex); + + if(lIndex.isValid()) + { + pJob.mSong = lIndex.data(BBFileIDRole); + emit songQueryReady(pJob); + return; + } + } + if(pJob.mShuffle) + pJob.mSong = mModel->firstShuffled().data(BBFileIDRole); + else + pJob.mSong = mModel->firstLinear().data(BBFileIDRole); + emit songQueryReady(pJob); +} + +void BBCollectionTab::queryPreviousSong(BBSongQueryJob &pJob) +{ + QModelIndex lCurrentSongIndex = mModel->indexForFileID(mCurrentSong); + if(lCurrentSongIndex.isValid()) + { + QPersistentModelIndex lIndex; + if(pJob.mShuffle) + lIndex = mModel->prevShuffled(lCurrentSongIndex); + else + lIndex = mModel->prevLinear(lCurrentSongIndex); + + if(lIndex.isValid()) + { + pJob.mSong = lIndex.data(BBFileIDRole); + emit songQueryReady(pJob); + return; + } + } + if(pJob.mShuffle) + pJob.mSong = mModel->lastShuffled().data(BBFileIDRole); + else + pJob.mSong = mModel->lastLinear().data(BBFileIDRole); + emit songQueryReady(pJob); +} + +void BBCollectionTab::setCurrentSong(const QVariant &pSong) +{ + int lNewSong = pSong.toInt(); + QModelIndex lNewSongIndex= mModel->indexForFileID(lNewSong); + QModelIndex lCurrentSongIndex= mModel->indexForFileID(mCurrentSong); + int lNewAlbum; + if(lNewSongIndex.isValid()) + lNewAlbum = lNewSongIndex.data(BBAlbumIDRole).toInt(); + else + { + BBResultTable lResult; + if(mDatabase.getTable(QString::fromAscii("SELECT album_ID FROM songs WHERE file_ID = %1").arg(lNewSong), + lResult) && lResult.mNumRows == 1) + { + lNewAlbum = QString(lResult.at(1, 0)).toInt(); + mDatabase.freeTable(lResult); + } + else + lNewAlbum = -1; + } + mModel->setCurrentSong(lNewSong, lNewAlbum, lCurrentSongIndex); + mResultView->scrollToNewSong(lNewSongIndex, lCurrentSongIndex); + mCurrentSong = lNewSong; +} + +QVariant BBCollectionTab::currentSong() +{ + return QVariant::fromValue(mCurrentSong); +} + +QString BBCollectionTab::displayString(const QVariant &pSong) +{ + int lSong = pSong.toInt(); + QModelIndex lSongIndex = mModel->indexForFileID(lSong); + if(lSongIndex.isValid()) + return QString("%1 - %2").arg(lSongIndex.data(BBArtistRole).toString(), + lSongIndex.data(BBTitleRole).toString()); + else + { + BBResultTable lResult; + if(mDatabase.getTable(QString::fromAscii("SELECT artist, title FROM songs WHERE file_ID = %1").arg(lSong), + lResult) && lResult.mNumRows == 1) + { + QString lDisplay = QString("%1 - %2").arg(lResult.at(1, 0), lResult.at(1, 1)); + mDatabase.freeTable(lResult); + return lDisplay; + } + else + return QString(); + } +} + +KUrl BBCollectionTab::songUrl(const QVariant &pSong) +{ + int lSong = pSong.toInt(); + QModelIndex lSongIndex = mModel->indexForFileID(lSong); + if(lSongIndex.isValid()) + return KUrl(QString("%1/%2").arg(lSongIndex.data(BBPathRole).toString(), + lSongIndex.data(BBFileNameRole).toString())); + else + { + BBResultTable lResult; + if(mDatabase.getTable(QString::fromAscii("SELECT path, file_name FROM songs JOIN albums USING (album_ID) " + "WHERE file_ID = %1").arg(lSong), lResult) && lResult.mNumRows == 1) + { + QString lUrl = QString("%1/%2").arg(lResult.at(1, 0), lResult.at(1, 1)); + mDatabase.freeTable(lResult); + return KUrl(lUrl); + } + else + return KUrl(); + } +} + +JobCollection *BBCollectionTab::getRefreshJob(BBFilterDock *pNotThisOne) +{ + JobCollection *lJobCollection = new JobCollection(); + connect(lJobCollection, SIGNAL(done(ThreadWeaver::Job *)), SLOT(deleteJob(ThreadWeaver::Job *))); + + Job *lJob = new SongQueryJob(getSongsCondition()); + lJob->assignQueuePolicy(mDBPool); + lJobCollection->addJob(lJob); + connect(lJob, SIGNAL(resultReady(QSet, QSet)), + mModel, SLOT(setNewSongs(QSet,QSet))); + connect(lJob, SIGNAL(done(ThreadWeaver::Job *)), SLOT(deleteJob(ThreadWeaver::Job *))); + + foreach(BBFilterDock *lCurrent, mDocks) + { + if(lCurrent != pNotThisOne) + { + Job *lJob = new FilterQueryJob(lCurrent->category(), getQueryFor(lCurrent)); + lJob->assignQueuePolicy(mDBPool); + lJobCollection->addJob(lJob); + connect(lJob, SIGNAL(resultReady(QSet)), + lCurrent, SLOT(processResult(QSet))); + connect(lJob, SIGNAL(done(ThreadWeaver::Job *)), SLOT(deleteJob(ThreadWeaver::Job *))); + } + } + return lJobCollection; +} + +void BBCollectionTab::refreshAllExcept(BBFilterDock *pNotThisOne) +{ + if(mRefreshInhibited) + return; + + Weaver::instance()->requestAbort(); + Weaver::instance()->dequeue(); + Weaver::instance()->enqueue(getRefreshJob(pNotThisOne)); +} + +QString BBCollectionTab::getQueryFor(BBFilterDock *pSubject) +{ + QStringList lTerms; + + BBFilterDock *lCurrent; + foreach(lCurrent, mDocks) + { + lCurrent->addCondition(lCurrent != pSubject, lTerms); + } + + return lTerms.join(" AND "); +} + +QString BBCollectionTab::getSongsCondition() +{ + QString lQuery1, lQuery = getQueryFor(NULL); + QString lFilter = BBDatabase::prepareString(mResultView->filterText()); + + if(!lFilter.isEmpty()) + lQuery1 = QString("(artist LIKE '%%1%' OR album LIKE '%%1%' OR title LIKE '%%1%')").arg(lFilter); + + if(!lQuery1.isEmpty()) + { + if(lQuery.isEmpty()) + { + lQuery.append(lQuery1); + } + else + { + lQuery.append(" AND "); + lQuery.append(lQuery1); + } + } + return lQuery; +} + +void BBCollectionTab::deleteJob(ThreadWeaver::Job *pJob) +{ + pJob->deleteLater(); +} + +void BBCollectionTab::changeFiles(const QString &pField, const QString &pOldValue) +{ + QSet lFileSet = mResultView->filesToUpdate(); + QString lCaption = i18ncp("dialog title", "Change Tag", "Change Tags", lFileSet.count()); + QString lLabel = i18np("Change the %2 field of one file to:", "Change the %2 field of %1 files to:", + lFileSet.count(), i18nc("file tag type", pField.toLocal8Bit())); + QString lNewValue = KInputDialog::getText(lCaption, lLabel, pOldValue, 0, this); + if(lNewValue.isNull()) + return; + + //set songs to be removed and added again in the refresh triggered later. + mModel->updateSongs(lFileSet); + + QList lFileIDList; + QSetIterator lIterator(lFileSet); + while(lIterator.hasNext()) + lFileIDList.append(lIterator.next().mData.mFileID); + BBFileChangeJob *lJob = new BBFileChangeJob(lFileIDList, pField, lNewValue); + mJobTracker->registerJob(lJob); + connect(lJob, SIGNAL(result(KJob*)), SLOT(fileChangeJobDone(KJob*))); + lJob->start(); +} + +void BBCollectionTab::fileChangeJobDone(KJob *pJob) +{ + BBFileChangeJob *lJob = qobject_cast(pJob); + + mRefreshInhibited = true; + foreach(BBFilterDock *i, mDocks) + { + if(i->category() == lJob->mField) + { + i->clearSelection(); + i->clearFilterText(); + QStringList lList(lJob->mNewValue); + i->forceSelection(lList); + break; + } + } + mRefreshInhibited = false; + JobCollection *lRefreshJob = getRefreshJob(NULL); + connect(lRefreshJob, SIGNAL(done(ThreadWeaver::Job*)), SLOT(finishLoadingPlaylist())); + Weaver::instance()->enqueue(lRefreshJob); +} + +void BBCollectionTab::resetAllFilters() +{ + mRefreshInhibited = true; + foreach(BBFilterDock *i, mDocks) + { + i->clearFilterText(); + i->clearSelection(); + } + mRefreshInhibited = false; + refreshAllExcept(NULL); +} + +void BBCollectionTab::loadPlaylist(const QString &pName) +{ + if(mRefreshInhibited) + return; + mPlaylistName = pName; + KSharedConfigPtr lConfig = KGlobal::config(); + KConfigGroup lGroup(lConfig, QString("__playlist__") + mPlaylistName); + + mRefreshInhibited = true; + foreach(BBFilterDock *i, mDocks) + { + i->clearFilterText(); + i->clearSelection(); + i->forceSelection(lGroup.readEntry(i->category(), QStringList())); + i->setFilterText(lGroup.readEntry(i->category() + "_filter", QString())); + } + mRefreshInhibited = false; + + JobCollection *lRefreshJob = getRefreshJob(NULL); + connect(lRefreshJob, SIGNAL(done(ThreadWeaver::Job*)), SLOT(finishLoadingPlaylist())); + Weaver::instance()->enqueue(lRefreshJob); +} + +void BBCollectionTab::finishLoadingPlaylist() +{ + mRefreshInhibited = true; + foreach(BBFilterDock *i, mDocks) + { + i->syncSelection(); + } + mRefreshInhibited = false; + mModel->updateSongs(QSet()); +} + +void BBCollectionTab::saveSession(KConfigGroup &pConfigGroup) +{ + pConfigGroup.writeEntry("chosen_playlist", mPlaylistComboBox->currentIndex()); + foreach(BBFilterDock *i, mDocks) + { + pConfigGroup.writeEntry(i->category(), i->currentSelection()); + pConfigGroup.writeEntry(i->category() + "_filter", i->filterText()); + } +} + +void BBCollectionTab::readSession(KConfigGroup &pConfigGroup) +{ + mJobTracker = new KUiServerJobTracker(this); + Weaver::instance()->setMaximumNumberOfThreads(QThread::idealThreadCount()); + mDBPool = new DBPool(QThread::idealThreadCount()); + + mDatabase.connect(BBSettings::fileNameDB()); + + mConfig = KGlobal::config(); + KConfigGroup lListGroup(mConfig, "PlaylistSystem"); + QStringList lDefaultPlaylistNames; + lDefaultPlaylistNames << i18n("Default Playlist"); + mPlaylistComboBox->addItems(lListGroup.readEntry("ListNames", lDefaultPlaylistNames)); + + connect(mPlaylistComboBox, SIGNAL(currentIndexChanged(QString)), SLOT(loadPlaylist(QString))); + + mRefreshInhibited = true; + mPlaylistComboBox->setCurrentIndex(pConfigGroup.readEntry("chosen_playlist", 0)); + + foreach(BBFilterDock *i, mDocks) + { + i->clearFilterText(); + i->clearSelection(); + i->forceSelection(pConfigGroup.readEntry(i->category(), QStringList())); + i->setFilterText(pConfigGroup.readEntry(i->category() + "_filter", QString())); + } + mRefreshInhibited = false; + JobCollection *lRefreshJob = getRefreshJob(NULL); + connect(lRefreshJob, SIGNAL(done(ThreadWeaver::Job*)), SLOT(finishLoadingPlaylist())); + Weaver::instance()->enqueue(lRefreshJob); +} + + +void BBCollectionTab::refreshAllViewsAndUpdateItem(QModelIndex pIndex) +{ + QSet lSongToUpdate; + lSongToUpdate.insert(mModel->findSongItem(pIndex)); + mModel->updateSongs(lSongToUpdate); + JobCollection *lRefreshJob = getRefreshJob(NULL); + connect(lRefreshJob, SIGNAL(done(ThreadWeaver::Job*)), SLOT(finishLoadingPlaylist())); + Weaver::instance()->enqueue(lRefreshJob); +} + + diff --git a/boombox/src/bbcollectiontab.h b/boombox/src/bbcollectiontab.h new file mode 100644 index 00000000..5ef71415 --- /dev/null +++ b/boombox/src/bbcollectiontab.h @@ -0,0 +1,126 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBCOLLECTIONTAB_H +#define BBCOLLECTIONTAB_H + +#include "bbdatabase.h" +#include "bbplaylistsystem.h" + +#include +#include +#include + +#include + +class QHBoxLayout; +class QComboBox; + +class KUiServerJobTracker; +class KActionCollection; +class KJob; +class KToolBar; +class BBPlaylistSystem; +class BBTabMenuBar; +class DBPool; +class BBFilterDock; +class BBResultView; +class BBAlbumSongModel; +class BBSongListItem; +class BBAlbumListItem; + +Q_DECLARE_METATYPE(QPersistentModelIndex) + +namespace ThreadWeaver +{ + class Job; + class JobCollection; +} + +class BBCollectionTab : public BBPlaylistSystem +{ + Q_OBJECT +public: + explicit BBCollectionTab(int pTabNumber); + BBTabMenuBar *getTabMenuBar(); + + virtual void queryNextSong(BBSongQueryJob &pJob); + virtual void queryPreviousSong(BBSongQueryJob &pJob); + virtual QVariant currentSong(); + virtual void setCurrentSong(const QVariant &pSong); + virtual QString displayString(const QVariant &pSong); + virtual KUrl songUrl(const QVariant &pSong); + + virtual void readSession(KConfigGroup &pConfigGroup); + virtual void saveSession(KConfigGroup &pConfigGroup); + virtual void embedControls(QWidget *pControls); + + void refreshAllExcept(BBFilterDock *pSubject); + void changeFiles(const QString &pField, const QString &pOldValue); + KActionCollection *actionCollection() + { + return mActionCollection; + } + +public slots: + void savePlaylist(const QString &pName); + void loadPlaylist(const QString &pName); + void refreshAllViewsAndUpdateItem(QModelIndex pIndex); + +protected slots: + void deleteJob(ThreadWeaver::Job *pJob); + void fileChangeJobDone(KJob *pJob); + void resetAllFilters(); + void finishLoadingPlaylist(); + void createNewPlaylist(); + void savePlaylist(); + void updateDatabase(); + void databaseUpdated(KJob *); + void deletePlaylist(); + +protected: + void setupActions(); + void setupToolBars(); + ThreadWeaver::JobCollection *getRefreshJob(BBFilterDock *pNotThisOne); + QString getQueryFor(BBFilterDock *pSubject); + QString getSongsCondition(); + + DBPool *mDBPool; + QList mDocks; + BBResultView *mResultView; + BBAlbumSongModel *mModel; + int mCurrentSong; + bool mRefreshInhibited; + QString mPlaylistName; + QComboBox *mPlaylistComboBox; + KToolBar *mControlToolBar; + KToolBar *mPlaylistToolBar; + QWidget *mControlsContainer; + QHBoxLayout *mControlsLayout; + + KUiServerJobTracker *mJobTracker; + BBPlaylistSystem *mPlaylistSystem; + + KSharedConfigPtr mConfig; + KActionCollection *mActionCollection; + + BBDatabase mDatabase; +}; + +#endif diff --git a/boombox/src/bbdatabase.cpp b/boombox/src/bbdatabase.cpp new file mode 100644 index 00000000..05a31979 --- /dev/null +++ b/boombox/src/bbdatabase.cpp @@ -0,0 +1,120 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbdatabase.h" +#include "bbmetadata.h" + +#include +#include + +BBDatabase::BBDatabase() + : mConnected(false), mSqlite(NULL) +{ +} + +BBDatabase::~BBDatabase() +{ + if(mConnected) + disconnect(); +} + +bool BBDatabase::connect(const QString &pFileName) +{ + if(mConnected) + disconnect(); + + int lError = sqlite3_open(pFileName.toUtf8(), &mSqlite); + + if(lError != SQLITE_OK) + { + qCritical("error opening: %i", lError); + return false; + } + executeStatement("CREATE TABLE IF NOT EXISTS albums (album_ID INTEGER PRIMARY KEY, path STRING, " + "album STRING, is_VA INTEGER, cover_art_path STRING)"); + executeStatement("CREATE TABLE IF NOT EXISTS songs (file_ID INTEGER PRIMARY KEY, " + "album_ID INTEGER, file_name STRING, " + "artist STRING, title STRING, track INTEGER, year INTEGER, genre STRING, " + "comment STRING, length STRING)"); + executeStatement("CREATE VIEW IF NOT EXISTS dead_album_view AS SELECT * FROM albums " + "LEFT OUTER JOIN songs USING (album_ID) WHERE file_name IS NULL"); + executeStatement("CREATE TRIGGER IF NOT EXISTS delete_album_trigger INSTEAD OF DELETE ON dead_album_view BEGIN " + "DELETE FROM albums WHERE albums.album_ID = old.album_ID; END"); + mConnected = true; + return true; +} + +void BBDatabase::disconnect() +{ + if(mConnected) + { + mConnected = false; + sqlite3_close(mSqlite); + mSqlite = NULL; + } +} + +bool BBDatabase::executeStatement(const QString &pStatement) +{ + char *lError = NULL; + int lReturnCode; + qDebug("Executing statement: %s", (const char *)pStatement.toUtf8()); + lReturnCode = sqlite3_exec(mSqlite, pStatement.toUtf8(), NULL, NULL, &lError); + if(lError != NULL) + { + qWarning("database error: %s", lError); + sqlite3_free(lError); + } + return lReturnCode == SQLITE_OK; +} + +bool BBDatabase::getTable(const QString &pQuery, BBResultTable &pTable) +{ + char *lError = NULL; + int lReturnCode; + qDebug("Executing statement: %s", (const char *)pQuery.toUtf8()); + lReturnCode = sqlite3_get_table(mSqlite, pQuery.toUtf8(), &pTable.mTable, &pTable.mNumRows, &pTable.mNumCols, &lError); + if(lError != NULL) + { + qWarning("database error: %s", lError); + sqlite3_free(lError); + } + qDebug("Number of rows in result: %i", pTable.mNumRows); + return lReturnCode == SQLITE_OK; +} + +void BBDatabase::freeTable(BBResultTable &pTable) +{ + sqlite3_free_table(pTable.mTable); + pTable.mTable = NULL; + pTable.mNumRows = 0; + pTable.mNumCols = 0; +} + +QString BBDatabase::prepareList(const QStringList &pList) +{ + if(pList.isEmpty()) + return QString(); + QStringList lTmp; + QStringListIterator i(pList); + while(i.hasNext()) + lTmp << prepareString(i.next()); + + return "(\'" + lTmp.join("\', \'") + "\')"; +} diff --git a/boombox/src/bbdatabase.h b/boombox/src/bbdatabase.h new file mode 100644 index 00000000..18c7f3ba --- /dev/null +++ b/boombox/src/bbdatabase.h @@ -0,0 +1,71 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBDATABASE_H +#define BBDATABASE_H + +#include +#include +#include + +class BBMetaData; +class BBStringListItem; +class BBDataListItem; +struct sqlite3; + +class BBResultTable +{ +public: + int mNumRows; + int mNumCols; + char **mTable; + char *at(int pRow, int pColumn) + { + return mTable[pRow*mNumCols + pColumn]; + } +}; + +class BBDatabase +{ +public: + BBDatabase(); + ~BBDatabase(); + + bool connect(const QString &pFileName); + void disconnect(); + bool isConnected(){return mConnected;} + sqlite3 *getDatabase(){return mSqlite;} + bool executeStatement(const QString &pStatement); + bool getTable(const QString &pQuery, BBResultTable &pTable); + static void freeTable(BBResultTable &pTable); + void insertLine(BBMetaData &pMetaData); + + static QString prepareString(const QString &pStr) + { + return QString(pStr).replace('\'', "''"); + } + + static QString prepareList(const QStringList &pList); + +protected: + bool mConnected; + sqlite3 *mSqlite; +}; + +#endif diff --git a/boombox/src/bbfilechangejob.cpp b/boombox/src/bbfilechangejob.cpp new file mode 100644 index 00000000..89f76c74 --- /dev/null +++ b/boombox/src/bbfilechangejob.cpp @@ -0,0 +1,105 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbfilechangejob.h" +#include "bbmetadata.h" +#include "bbsettings.h" + +#include +#include +#include + +BBFileChangeJob::BBFileChangeJob(QList pFiles, const QString &pField, const QString &pNewValue) + : mFiles(pFiles), mField(pField), mNewValue(pNewValue) +{ + setCapabilities(Killable | Suspendable); +} + + +void BBFileChangeJob::doWork() +{ + mDatabase.connect(BBSettings::fileNameDB()); + + setTotalAmount(Files, mFiles.count()); + int lFileCount = 0; + + foreach(int lFileID, mFiles) + { + if(checkForDeathOrSuspend()) + return; + BBResultTable lResult; + mDatabase.getTable(QString("SELECT path || '/' || file_name FROM albums JOIN songs USING (album_ID) " + "WHERE file_ID = '%1'").arg(lFileID), lResult); + QString lPath = QString::fromUtf8(lResult.at(1, 0)); + mDatabase.freeTable(lResult); + + emit description(this, i18n("Writing change to files"), qMakePair(i18n("Path"), lPath)); + + QFileInfo lInfo(lPath); + if(!lPath.contains("://")) + BBMetaData::ChangeFile(lPath, mField, mNewValue); + + if(mField == "album") + { + int lAlbumID; + mDatabase.getTable(QString( + "SELECT DISTINCT album_ID FROM songs JOIN albums USING (album_ID) " + "WHERE album = '%1' AND path = '%2'").arg(BBDatabase::prepareString(mNewValue), + BBDatabase::prepareString(lInfo.absolutePath())), + lResult); + if(lResult.mNumRows == 0) + { + mDatabase.executeStatement(QString( + "INSERT INTO albums (path, album, is_VA, cover_art_path) " + "VALUES ('%1','%2','%3','%4')").arg(BBDatabase::prepareString(lInfo.absolutePath()), + BBDatabase::prepareString(mNewValue), + QString::number(0), QString()) + ); + lAlbumID = sqlite3_last_insert_rowid(mDatabase.getDatabase()); + } + else + { + lAlbumID = QString::fromUtf8(lResult.at(1, 0)).toInt(); + } + mDatabase.freeTable(lResult); + mDatabase.executeStatement( + QString("UPDATE songs SET album_ID = %1 WHERE file_ID = %2").arg(lAlbumID).arg(lFileID) + ); + } + else + { + mDatabase.executeStatement( + QString("UPDATE songs SET %1 = '%2' WHERE file_ID = '%3'").arg(mField, BBDatabase::prepareString(mNewValue)).arg(lFileID) + ); + } + lFileCount++; + setProcessedAmount(Files, lFileCount); + setPercent((ulong)(100*processedAmount(Files))/totalAmount(Files)); + } + + mDatabase.executeStatement("DELETE FROM dead_album_view"); + mDatabase.executeStatement("UPDATE albums SET is_VA = 1 WHERE album_ID IN " + "(SELECT album_ID FROM songs JOIN albums USING (album_ID) " + "GROUP BY album_ID HAVING count(DISTINCT artist) > 1)"); + mDatabase.executeStatement("UPDATE albums SET is_VA = 0 WHERE album_ID IN " + "(SELECT album_ID FROM songs JOIN albums USING (album_ID) " + "GROUP BY album_ID HAVING count(DISTINCT artist) = 1)"); + + emitResult(); +} diff --git a/boombox/src/bbfilechangejob.h b/boombox/src/bbfilechangejob.h new file mode 100644 index 00000000..e5c3fea4 --- /dev/null +++ b/boombox/src/bbfilechangejob.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBFILECHANGEJOB_H +#define BBFILECHANGEJOB_H + +#include + +#include "threadjob.h" +#include "bbdatabase.h" + +class BBFileChangeJob : public ThreadJob +{ + Q_OBJECT + public: + BBFileChangeJob(QList pFiles, const QString &pField, const QString &pNewValue); + + QList mFiles; + QString mField; + QString mNewValue; + BBDatabase mDatabase; + + public slots: + void doWork(); +}; + +#endif // BBFILECHANGEJOB_H diff --git a/boombox/src/bbfilesystemtab.cpp b/boombox/src/bbfilesystemtab.cpp new file mode 100644 index 00000000..e5d9fd9f --- /dev/null +++ b/boombox/src/bbfilesystemtab.cpp @@ -0,0 +1,307 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbfilesystemtab.h" +#include "bbmainwindow.h" +#include "bbmetadata.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +BBFileSystemTab::BBFileSystemTab(int pTabNumber) + :BBPlaylistSystem(pTabNumber) +{ + setWindowTitle(i18n("File System")); +} + +void BBFileSystemTab::queryNextSong(BBSongQueryJob &pJob) +{ + mSongQuery = pJob; + mSearchStack.clear(); + mSearchingForward = true; + buildSearchStack(); + findSongInSearchStack(); +} + +void BBFileSystemTab::queryPreviousSong(BBSongQueryJob &pJob) +{ + mSongQuery = pJob; + mSearchStack.clear(); + mSearchingForward = false; + buildSearchStack(); + findSongInSearchStack(); +} + +void BBFileSystemTab::buildSearchStack() +{ + QModelIndex lCurrent; + if(!mCurrentSong.isNull()) + lCurrent = mProxyModel->mapFromSource(mModel->indexForItem(mCurrentSong)); + if(lCurrent.isValid()) + { + if(mSearchingForward) + { + do + { + int lStartRow = lCurrent.row() + 1; + lCurrent = lCurrent.parent(); + int lMax = mProxyModel->rowCount(lCurrent); + for(int i = lStartRow; i < lMax; ++i) + mSearchStack.append(mModel->itemForIndex(mProxyModel->mapToSource(mProxyModel->index(i, 0, lCurrent)))); + }while(lCurrent.isValid()); + } + else + { + do + { + int lStartRow = lCurrent.row() - 1; + lCurrent = lCurrent.parent(); + for(int i = lStartRow; i >= 0; --i) + mSearchStack.append(mModel->itemForIndex(mProxyModel->mapToSource(mProxyModel->index(i, 0, lCurrent)))); + }while(lCurrent.isValid()); + } + } + else + { + if(mSearchingForward) + { + int lMax = mProxyModel->rowCount(); + for(int i = 0; i < lMax; ++i) + mSearchStack.append(mModel->itemForIndex(mProxyModel->mapToSource(mProxyModel->index(i, 0)))); + } + else + { + for(int i = mProxyModel->rowCount() - 1; i >= 0; --i) + mSearchStack.append(mModel->itemForIndex(mProxyModel->mapToSource(mProxyModel->index(i, 0)))); + } + } +} + +void BBFileSystemTab::findSongInSearchStack() +{ + while(!mSearchStack.isEmpty()) + { + KFileItem lCurrent = mSearchStack.takeFirst(); + if((lCurrent.isFile() || lCurrent.isLink()) && lCurrent.isReadable()) + { + mSongQuery.mSong = lCurrent; + emit songQueryReady(mSongQuery); + mSearchStack.clear(); + return; + } + if(lCurrent.isDir() && lCurrent.isReadable()) + { + QModelIndex lCurrentIndex = mModel->indexForItem(lCurrent); + if(!lCurrentIndex.isValid()) + continue; + if(mModel->canFetchMore(lCurrentIndex)) + { + mModel->fetchMore(lCurrentIndex); + mSearchStack.prepend(lCurrent); + return; + } + QModelIndex lCurrentProxyIndex = mProxyModel->mapFromSource(lCurrentIndex); + if(mSearchingForward) + { + for(int i = mProxyModel->rowCount(lCurrentProxyIndex) - 1; i >= 0; --i) + mSearchStack.prepend(mModel->itemForIndex(mProxyModel->mapToSource(mProxyModel->index(i, 0, lCurrentProxyIndex)))); + } + else + { + for(int i = 0; i < mProxyModel->rowCount(lCurrentProxyIndex); ++i) + mSearchStack.prepend(mModel->itemForIndex(mProxyModel->mapToSource(mProxyModel->index(i, 0, lCurrentProxyIndex)))); + } + } + } +} + +QVariant BBFileSystemTab::currentSong() +{ + return mCurrentSong; +} + +void BBFileSystemTab::setCurrentSong(const QVariant &pSong) +{ + mCurrentSong = pSong.value(); + KUrl lUrl(mCurrentSong.url().directory()); + QString lDirectory = lUrl.url(); + QString lDirectory2 = mDirOperator->url().url(); + if(!lDirectory.startsWith(lDirectory2)) + { + mDirOperator->setUrl(lDirectory, true); + mFilePlacesView->setUrl(lDirectory); + mUrlNavigator->setLocationUrl(lDirectory); + } + mDirOperator->setCurrentItem(mCurrentSong); +} + +QString BBFileSystemTab::displayString(const QVariant &pSong) +{ + KFileItem lFileItem = pSong.value(); + if(!lFileItem.isLocalFile()) + return QString(); + BBMetaData lMetaData; + QString lPath = lFileItem.localPath(); + QString lFileName = lFileItem.name(); + lPath.chop(lFileName.length()); + lMetaData.GetInfoFrom(lPath, lFileName); + return QString("%1 - %2").arg(lMetaData.mArtist, lMetaData.mTitle); +} + +KUrl BBFileSystemTab::songUrl(const QVariant &pSong) +{ + KFileItem lSong = pSong.value(); + if(lSong.isNull()) + return KUrl(); + else + return lSong.targetUrl(); +} + +void BBFileSystemTab::readSession(KConfigGroup &pConfigGroup) +{ + mFilePlacesModel = new KFilePlacesModel(this); + mCenterWidget = new QWidget(this); + setCentralWidget(mCenterWidget); + mLayout = new QVBoxLayout(mCenterWidget); + + mFilePlacesView = new KFilePlacesView(); + mFilePlacesView->setModel(mFilePlacesModel); + mDirOperator = new KDirOperator(KGlobalSettings::musicPath(), this); + QString lMimeTypes = QString("application/x-ogg;audio/basic;audio/vnd.rn-realaudio;audio/x-aiff;" + "audio/x-flac;audio/x-matroska;audio/x-mp3;audio/mpeg;audio/ogg;" + "audio/x-flac+ogg;audio/x-vorbis+ogg;audio/x-ms-wma;" + "audio/x-pn-realaudio;audio/x-wav;inode/directory"); + QStringList lMimeFilter = lMimeTypes.split(";"); + mDirOperator->setMimeFilter(lMimeFilter); + mDirOperator->readConfig(pConfigGroup); + mDirOperator->setView(KFile::DetailTree); + mDirOperator->view()->setAlternatingRowColors(true); + mDirOperator->actionCollection()->action("preview")->setChecked(false); + + KAction *lEnqueueAction = new KAction(i18n("Add to Queue"), this); + lEnqueueAction->setShortcut(Qt::CTRL + Qt::Key_E); + lEnqueueAction->setShortcutContext(Qt::WidgetShortcut); + connect(lEnqueueAction, SIGNAL(triggered()), SLOT(enqueueTriggered())); + mDirOperator->view()->addAction(lEnqueueAction); + + KMenu *lPopupMenu = new KMenu(this); + lPopupMenu->addAction(lEnqueueAction); + lPopupMenu->addSeparator(); + lPopupMenu->addAction(mDirOperator->actionCollection()->action("sorting menu")); + lPopupMenu->addAction(mDirOperator->actionCollection()->action("view menu")); + lPopupMenu->addSeparator(); + lPopupMenu->addAction(mDirOperator->actionCollection()->action("properties")); + KActionMenu *lActionMenu = (KActionMenu *)mDirOperator->actionCollection()->action("popupMenu"); + lActionMenu->setMenu(lPopupMenu); + + mUrlNavigator = new KUrlNavigator(mFilePlacesModel, mDirOperator->url(), this); + + connect(mUrlNavigator, SIGNAL(urlChanged(KUrl)), this, SLOT(setOperatorUrl(KUrl))); + connect(mFilePlacesView, SIGNAL(urlChanged(KUrl)),this, SLOT(setOperatorUrl(KUrl))); + connect(mDirOperator, SIGNAL(urlEntered(KUrl)), this, SLOT(setCurrentPath(KUrl))); + connect(mDirOperator, SIGNAL(fileSelected(KFileItem)), this, SLOT(selectFile(KFileItem))); + mLayout->addWidget(mUrlNavigator); + mLayout->addWidget(mDirOperator); + + QDockWidget *lPlacesDock = new QDockWidget(i18n("Places"), this); + lPlacesDock->setObjectName("fs_places_dock"); + lPlacesDock->setFeatures(QDockWidget::DockWidgetMovable); + lPlacesDock->setWidget(mFilePlacesView); + addDockWidget(Qt::LeftDockWidgetArea, lPlacesDock); + + mControlToolBar = toolBar("fs_control_toolbar"); + mControlToolBar->setWindowTitle(i18n("Playback Control Toolbar")); + mControlToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); + mControlsContainer = new QWidget(this); + mControlsLayout = new QHBoxLayout(mControlsContainer); + mControlToolBar->addWidget(mControlsContainer); + + setAutoSaveSettings("FileSystemTab"); + mProxyModel = (KCategorizedSortFilterProxyModel *)mDirOperator->view()->model(); + mModel = (KDirModel *)mProxyModel->sourceModel(); + connect(mModel->dirLister(), SIGNAL(completed()), this, SLOT(findSongInSearchStack())); + setCurrentPath(pConfigGroup.readEntry("filesystemtab_path", KGlobalSettings::musicPath())); +} + +void BBFileSystemTab::saveSession(KConfigGroup &pConfigGroup) +{ + mDirOperator->writeConfig(pConfigGroup); + pConfigGroup.writePathEntry("filesystemtab_path", mDirOperator->url().toEncoded()); +} + +void BBFileSystemTab::embedControls(QWidget *pControls) +{ + pControls->setParent(mControlsContainer); + mControlsLayout->addWidget(pControls); + pControls->show(); +} + +void BBFileSystemTab::setOperatorUrl(const KUrl &pUrl) +{ + mDirOperator->setUrl(pUrl, true); +} + +void BBFileSystemTab::selectFile(const KFileItem &pFileItem) +{ + if(pFileItem.isFile() && pFileItem.isReadable() && pFileItem != mCurrentSong) + { + gMainWindow->setCurrentPlaylistSystem(tabNumber()); + gMainWindow->setCurrentSong(pFileItem); + } +} + +void BBFileSystemTab::enqueueTriggered() +{ + KFileItemList lSelectedItems = mDirOperator->selectedItems(); + foreach(KFileItem lItem, lSelectedItems) + { + if(lItem.isFile() && lItem.isReadable()) + gMainWindow->addToPlayQueue(lItem, displayString(lItem), tabNumber()); + } +} + +void BBFileSystemTab::setCurrentPath(const KUrl &pUrl) +{ + mUrlNavigator->setLocationUrl(pUrl); + mFilePlacesView->setUrl(pUrl); +} + +void BBFileSystemTab::addManualUrl(const KUrl &pUrl) +{ + KFileItem lItem(KFileItem::Unknown, KFileItem::Unknown, pUrl, true); + if(lItem.isFile() && lItem.isReadable()) + gMainWindow->addToPlayQueue(lItem, displayString(lItem), tabNumber()); +} diff --git a/boombox/src/bbfilesystemtab.h b/boombox/src/bbfilesystemtab.h new file mode 100644 index 00000000..3f39743a --- /dev/null +++ b/boombox/src/bbfilesystemtab.h @@ -0,0 +1,91 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBFILESYSTEMTAB_H +#define BBFILESYSTEMTAB_H + +#include "bbplaylistsystem.h" + +#include + +#include +#include + +class QFileSystemModel; +class QHBoxLayout; +class QModelIndex; +class QVBoxLayout; +class KCategorizedSortFilterProxyModel; +class KDirModel; +class KFilePlacesModel; +class KFilePlacesView; +class KToolBar; +class KUrl; +class KUrlNavigator; + +class BBFileSystemTab : public BBPlaylistSystem +{ +Q_OBJECT +public: + explicit BBFileSystemTab(int pTabNumber); + virtual void queryNextSong(BBSongQueryJob &pJob); + virtual void queryPreviousSong(BBSongQueryJob &pJob); + virtual QVariant currentSong(); + virtual void setCurrentSong(const QVariant &pSong); + virtual QString displayString(const QVariant &pSong); + virtual KUrl songUrl(const QVariant &pSong); + + void addManualUrl(const KUrl &pUrl); + + virtual void readSession(KConfigGroup &pConfigGroup); + virtual void saveSession(KConfigGroup &pConfigGroup); + virtual void embedControls(QWidget *pControls); + + KActionCollection *actionCollection() + { + return mDirOperator->actionCollection(); + } + +protected slots: + void setOperatorUrl(const KUrl &pUrl); + void selectFile(const KFileItem &pFileItem); + void setCurrentPath(const KUrl &pUrl); + void buildSearchStack(); + void findSongInSearchStack(); + void enqueueTriggered(); + +protected: + QVBoxLayout *mLayout; + KFilePlacesModel *mFilePlacesModel; + KFilePlacesView *mFilePlacesView; + KUrlNavigator *mUrlNavigator; + KDirOperator *mDirOperator; + QWidget *mCenterWidget; + KToolBar *mControlToolBar; + QWidget *mControlsContainer; + QHBoxLayout *mControlsLayout; + KFileItem mCurrentSong; + KDirModel *mModel; + KCategorizedSortFilterProxyModel *mProxyModel; + QList mSearchStack; + BBSongQueryJob mSongQuery; + bool mSearchingForward; +}; + +#endif // BBFILESYSTEMTAB_H diff --git a/boombox/src/bbfilterdock.cpp b/boombox/src/bbfilterdock.cpp new file mode 100644 index 00000000..e24f3cd9 --- /dev/null +++ b/boombox/src/bbfilterdock.cpp @@ -0,0 +1,167 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbfilterdock.h" +#include "bbdatabase.h" +#include "bbcollectiontab.h" +#include "bbsetmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +BBFilterDock::BBFilterDock(const QString &pCategory, const QString &pTitle, BBCollectionTab *pCollectionTab) + : QDockWidget(pTitle), mCategory(pCategory), mCollectionTab(pCollectionTab) +{ + setObjectName(pCategory); + setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); + QWidget *lWidget = new QWidget(this); + QVBoxLayout *lLayout = new QVBoxLayout; + mLineEdit = new KLineEdit; + mListView = new QListView; + mPushButton = new KPushButton(i18n("Deselect All")); + + lLayout->addWidget(mLineEdit); + lLayout->addWidget(mPushButton); + lLayout->addWidget(mListView); + lLayout->setSpacing(1); + + lWidget->setLayout(lLayout); + setWidget(lWidget); + + mLineEdit->setClearButtonShown(true); + mLineEdit->setClickMessage(i18n("Enter filtering text here...")); + + mModel = new BBStringSetModel(this); + mListView->setModel(mModel); + mSelectionModel = mListView->selectionModel(); + mListView->setUniformItemSizes(true); + mListView->setSelectionMode(QAbstractItemView::ExtendedSelection); + mListView->setSelectionBehavior(QAbstractItemView::SelectItems); + mListView->setContextMenuPolicy(Qt::CustomContextMenu); + + KAction *lEditAction = new KAction(i18n("Edit"), this); + lEditAction->setShortcut(Qt::Key_F2); + lEditAction->setShortcutContext(Qt::WidgetShortcut); + mListView->addAction(lEditAction); + connect(lEditAction, SIGNAL(triggered()), SLOT(editFilesTriggered())); + mContextMenu = new QMenu(this); + mContextMenu->addAction(lEditAction); + + connect(mListView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showPopupMenu(QPoint))); + connect(mSelectionModel, SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), + this, SLOT(updateSelection(const QItemSelection &, const QItemSelection &))); + connect(mLineEdit, SIGNAL(textChanged(const QString &)), SLOT(lineEditChanged(const QString &))); + connect(mPushButton, SIGNAL(clicked(bool)), mSelectionModel, SLOT(clearSelection())); + +} + +void BBFilterDock::lineEditChanged(const QString &pNewText) +{ + mFilterTextCondition = QString("%1 LIKE '%%2%'").arg(mCategory, pNewText); + mCollectionTab->refreshAllExcept(NULL); +} + +void BBFilterDock::updateSelection(const QItemSelection &pSelected, const QItemSelection &pDeselected) +{ + foreach(QModelIndex i, pDeselected.indexes()) + { + int lIndex = mSelection.indexOf(i.data().toString()); + if(lIndex != -1) + mSelection.removeAt(lIndex); + } + foreach(QModelIndex i, pSelected.indexes()) + { + QString lString = i.data().toString(); + if(mSelection.indexOf(lString) == -1) + mSelection.append(lString); + } + mQuotedSelection = QString("%1 IN %2").arg(mCategory, BBDatabase::prepareList(mSelection)); + + mCollectionTab->refreshAllExcept(this); +} + +void BBFilterDock::addCondition(bool pUseSelection, QStringList &pList) +{ + if(!pUseSelection) + { + if(!mFilterTextCondition.isEmpty()) + pList << mFilterTextCondition; + } + else + { + if(!mSelection.isEmpty()) + pList << mQuotedSelection; + else + { + if(!mFilterTextCondition.isEmpty()) + pList << mFilterTextCondition; + } + } +} + +void BBFilterDock::processResult(QSet pResult) +{ + mModel->setNewSet(pResult); + QModelIndexList lList = mSelectionModel->selectedIndexes(); + if(!lList.isEmpty()) + mListView->scrollTo(lList.first()); +} + +void BBFilterDock::showPopupMenu(const QPoint &pPoint) +{ + if(mListView->indexAt(pPoint).isValid()) + mContextMenu->popup(mListView->mapToGlobal(pPoint)); +} + +void BBFilterDock::editFilesTriggered() +{ + QModelIndexList lList = mSelectionModel->selectedIndexes(); + if(lList.isEmpty()) + return; //should never happen... + + QString lOldValue = lList.first().data().toString(); + mCollectionTab->changeFiles(mCategory, lOldValue); +} + +void BBFilterDock::forceSelection(const QStringList &pList) +{ + mSelection = pList; + mQuotedSelection = QString("%1 IN %2").arg(mCategory, BBDatabase::prepareList(mSelection)); +} + +void BBFilterDock::syncSelection() +{ + foreach(const QString &lEntry, mSelection) + { + int lIndex = mModel->findStringPos(lEntry); + if(lIndex != -1) + { + mSelectionModel->select(mModel->index(lIndex), QItemSelectionModel::Select); + } + } + QModelIndexList lList = mSelectionModel->selectedIndexes(); + if(!lList.isEmpty()) + mListView->scrollTo(lList.first()); +} diff --git a/boombox/src/bbfilterdock.h b/boombox/src/bbfilterdock.h new file mode 100644 index 00000000..c620574e --- /dev/null +++ b/boombox/src/bbfilterdock.h @@ -0,0 +1,73 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBFILTERDOCK_H +#define BBFILTERDOCK_H + +#include +#include +#include +#include + +#include "bblistitem.h" + +class QListView; +class BBStringSetModel; +class KPushButton; +class BBCollectionTab; +class QMenu; + +class BBFilterDock: public QDockWidget +{ + Q_OBJECT + public: + BBFilterDock(const QString &pCategory, const QString &pTitle, BBCollectionTab *pCollectionTab); + const QString & category() const {return mCategory;} + void addCondition(bool pUseSelection, QStringList &pList); + QStringList currentSelection() { return mSelection; } + QString filterText() { return mLineEdit->text(); } + + public slots: + void clearSelection() { mSelectionModel->clearSelection(); } + void clearFilterText() { mLineEdit->clear(); } + void setFilterText(const QString &pString) { mLineEdit->setText(pString); } + void processResult(QSet pResult); + void updateSelection(const QItemSelection &pSelected, const QItemSelection &pDeselected); + void lineEditChanged(const QString &pNewText); + void forceSelection(const QStringList &pList); + void syncSelection(); + + protected slots: + void showPopupMenu(const QPoint &pPoint); + void editFilesTriggered(); + + protected: + QString mCategory; + QString mQuotedSelection; + QString mFilterTextCondition; + QStringList mSelection; + QListView *mListView; + KLineEdit *mLineEdit; + KPushButton *mPushButton; + BBStringSetModel *mModel; + QItemSelectionModel *mSelectionModel; + BBCollectionTab *mCollectionTab; + QMenu *mContextMenu; +}; +#endif diff --git a/boombox/src/bblistitem.h b/boombox/src/bblistitem.h new file mode 100644 index 00000000..543ff227 --- /dev/null +++ b/boombox/src/bblistitem.h @@ -0,0 +1,139 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBLISTITEM_H +#define BBLISTITEM_H + +#include +#include + +class BBIndex +{ + public: + BBIndex() + : mAlbumPos(-1), mSongPos(-1) + {} + BBIndex(int pAlbumPos, int pSongPos) + : mAlbumPos(pAlbumPos), mSongPos(pSongPos) + {} + bool isValid() {return mAlbumPos >= 0 && mSongPos >= 0;} + int mAlbumPos, mSongPos; +}; + +class BBSongData +{ + public: + BBSongData() + : mNextShuffled(), mPrevShuffled() + { + mFileID = -1; + mAlbumID = -1; + } + QString mTitle, mArtist, mLength, mFileName; + int mFileID, mAlbumID; + BBIndex mNextShuffled, mPrevShuffled; +}; + +class BBAlbumData +{ + public: + QString mAlbum, mFolderPath, mCoverArtPath, mArtist; + int mAlbumID; + bool mIsVA; + bool mManuallyExpanded; + QList mSongs; +}; + +class BBListItem +{ + public: + BBListItem(int pPosition) + : mPosition(pPosition) + {} + BBListItem() + : mPosition(-1) + {} + + int mPosition; +}; + +class BBStringListItem : public BBListItem +{ + public: + BBStringListItem(const QString & pString, int pPosition) + : BBListItem(pPosition), mString(pString) + {} + + QString mString; +}; + +class BBSongListItem : public BBListItem +{ + public: + BBSongData mData; + BBIndex mIndex; +}; + +class BBAlbumListItem : public BBListItem +{ + public: + BBAlbumData mData; +}; + +inline bool operator< (const BBListItem &a, const BBListItem &b) +{ + return a.mPosition < b.mPosition; +} + +inline bool operator> (const BBListItem &a, const BBListItem &b) +{ + return a.mPosition > b.mPosition; +} + +inline uint qHash(const BBStringListItem &a) +{ + return qHash(a.mString); +} + +inline bool operator==(const BBStringListItem &a, const BBStringListItem &b) +{ + return a.mString == b.mString; +} + +inline uint qHash(const BBAlbumListItem &a) +{ + return a.mData.mAlbumID; +} + +inline bool operator==(const BBAlbumListItem &a, const BBAlbumListItem &b) +{ + return a.mData.mAlbumID == b.mData.mAlbumID; +} + +inline uint qHash(const BBSongListItem &a) +{ + return a.mData.mFileID; +} + +inline bool operator==(const BBSongListItem &a, const BBSongListItem &b) +{ + return a.mData.mFileID == b.mData.mFileID; +} + +#endif diff --git a/boombox/src/bbmainwindow.cpp b/boombox/src/bbmainwindow.cpp new file mode 100644 index 00000000..a28783dc --- /dev/null +++ b/boombox/src/bbmainwindow.cpp @@ -0,0 +1,738 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbmainwindow.h" +#include "bbcollectiontab.h" +#include "bbfilesystemtab.h" +#include "bbstreamstab.h" +#include "mpris2player.h" +#include "bbsettings.h" +#include "ui_directoriespage.h" +#include "playeradaptor.h" +#include "mediaplayer2adaptor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + // maximum entries in the playback history +const int gMaxHistory = 10; + +class BBCenteredMenuBar : public QWidget +{ +public: + BBCenteredMenuBar(QWidget *pParent = 0); + KMenuBar *mMenuBar; + virtual QSize sizeHint() const; +}; + +BBCenteredMenuBar::BBCenteredMenuBar(QWidget *pParent) + : QWidget(pParent) +{ + QHBoxLayout *lLayout = new QHBoxLayout(this); + lLayout->setMargin(0); + mMenuBar = new KMenuBar(); + lLayout->addWidget(mMenuBar, 0, Qt::AlignVCenter); + lLayout->addSpacing(14); +} + +QSize BBCenteredMenuBar::sizeHint() const +{ + return QWidget::sizeHint() + QSize(0, 10); //trick to make oxygen show it correctly. +} + +BBTitleLabel::BBTitleLabel(QWidget *pParent) + : QLabel(pParent), mMargin(30), mUpdateInterval(40), mSpeed(0.025) +{ + mTimer = new QTimer(this); + connect(mTimer, SIGNAL(timeout()), this, SLOT(advanceAnimation())); +} + +void BBTitleLabel::setNewText(const QString &pText) +{ + setText(pText); + mVx = -mSpeed; + + QRect lRect = contentsRect(); + lRect.setWidth(fontMetrics().boundingRect(pText).width() + mMargin); + mSecondPic = QPixmap(lRect.size()); + mSecondPic.fill(Qt::transparent); + QPainter lPainter(&mSecondPic); + lPainter.setPen(palette().color(QPalette::Active, QPalette::WindowText)); + lPainter.drawText(lRect, Qt::AlignHCenter, text()); + + if(lRect.width() > width() + mMargin) + { + mX = -mMargin/2.0; + mTimer->start(mUpdateInterval); + } + else + { + mX = (width() - lRect.width())/2.0; + mTimer->stop(); + } +} + +void BBTitleLabel::advanceAnimation() +{ + mX += mVx*mUpdateInterval; + if((mX > 0 && mVx > 0) || (mX + mSecondPic.width() < width() && mVx < 0)) + mVx = -mVx; + update(); +} + +void BBTitleLabel::paintEvent(QPaintEvent *) +{ + QPainter lPainter(this); + lPainter.drawPixmap(mX, 0, mSecondPic); +} + +void BBTitleLabel::resizeEvent(QResizeEvent *pEvent) +{ + QLabel::resizeEvent(pEvent); + + if(mSecondPic.width() > width() + mMargin) + mTimer->start(40); + else + { + mX = (width() - mSecondPic.width())/2.0; + mTimer->stop(); + } +} + +BBMainWindow::BBMainWindow(const KAboutData *pAboutData, QWidget *parent) + : KTabWidget(parent,Qt::Window), mAboutData(pAboutData), + mPlaylistSystem(0), mPlayingPlaylistSystem(0), mLastUpdatedTime(0), mMpris2Player(0) +{ + mBufferingStatusTimer = new QTimer(this); + connect(mBufferingStatusTimer, SIGNAL(timeout()), this, SLOT(setBufferingStatus())); + + setWindowTitle("BoomBox"); + setDocumentMode(true); + + setupActions(); + setupControls(); + + connect(tabBar(), SIGNAL(currentChanged(int)), this, SLOT(setActiveTab(int))); + + mCollectionTab = new BBCollectionTab(0); + addTab(mCollectionTab, mCollectionTab->windowTitle()); + tabBar()->setTabButton(0, QTabBar::RightSide, mCollectionTab->getTabMenuBar()); + + mFileSystemTab = new BBFileSystemTab(1); + addTab(mFileSystemTab, mFileSystemTab->windowTitle()); + + mStreamsTab = new BBStreamsTab(2); + addTab(mStreamsTab, mStreamsTab->windowTitle()); + + mAudioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, this); + mVolumeSlider->setAudioOutput(mAudioOutput); + mMediaObject = new Phonon::MediaObject(this); + mMediaObject->setTickInterval(1000); + connect(mMediaObject, SIGNAL(tick(qint64)), SLOT(updateElapsedTime(qint64))); + connect(mMediaObject, SIGNAL(currentSourceChanged(Phonon::MediaSource)), SLOT(updateCurrentSong(Phonon::MediaSource))); + connect(mMediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)), + SLOT(updatePhononState(Phonon::State, Phonon::State))); + connect(mMediaObject, SIGNAL(aboutToFinish()), SLOT(queueNextSong())); + connect(mMediaObject, SIGNAL(metaDataChanged()), SLOT(updateMetaDataDisplay())); + + mPositionSlider->setMediaObject(mMediaObject); + Phonon::createPath(mMediaObject, mAudioOutput); +} + +BBMainWindow::~BBMainWindow() +{ +} + +void BBMainWindow::setupActions() +{ + mActionCollection = new KActionCollection(this); + + BBCenteredMenuBar *lLeftMenu = new BBCenteredMenuBar(); + KMenu *lBBMenu = new KMenu(i18n("BoomBox")); + lBBMenu->addAction((QAction *)KStandardAction::preferences(this, SLOT(showConfigDialog()), mActionCollection)); + lBBMenu->addAction((QAction *)KStandardAction::keyBindings(this, SLOT(showShortcutsDialog()), mActionCollection)); + lBBMenu->addSeparator(); + lBBMenu->addAction((QAction *)KStandardAction::help(this, SLOT(showHelp()), mActionCollection)); + lBBMenu->addAction((QAction *)KStandardAction::aboutApp(this, SLOT(showAboutDialog()), mActionCollection)); + lBBMenu->addSeparator(); + lBBMenu->addAction((QAction *)KStandardAction::quit(this, SLOT(close()), mActionCollection)); + lLeftMenu->mMenuBar->addMenu(lBBMenu); + setCornerWidget(lLeftMenu, Qt::TopLeftCorner); + + mPlayPauseAction = new KAction(KIcon("media-playback-start"), i18n("Play/Pause"), this); + mActionCollection->addAction("play_pause", mPlayPauseAction); + mPlayPauseAction->setGlobalShortcut(KShortcut(Qt::META + Qt::Key_X)); + connect(mPlayPauseAction, SIGNAL(triggered()), SLOT(togglePlayback())); + + mNextAction = new KAction(KIcon("media-skip-forward"), i18n("Next Song"), this); + mActionCollection->addAction("next_song", mNextAction); + mNextAction->setGlobalShortcut(KShortcut(Qt::META + Qt::Key_C)); + connect(mNextAction, SIGNAL(triggered()), SLOT(jumpToNextSong())); + + mPreviousAction = new KAction(KIcon("media-skip-backward"), i18n("Previous Song"), this); + mActionCollection->addAction("previous_song", mPreviousAction); + mPreviousAction->setGlobalShortcut(KShortcut(Qt::META + Qt::Key_Z)); + connect(mPreviousAction, SIGNAL(triggered()), SLOT(jumpToPreviousSong())); + + KAction *lNextTabAction = new KAction(i18n("Activate Next Tab"), this); + lNextTabAction->setShortcut(KStandardShortcut::tabNext()); + mActionCollection->addAction("next_tab", lNextTabAction); + addAction((QAction *)lNextTabAction); + connect(lNextTabAction, SIGNAL(triggered()), this, SLOT(activateNextTab())); + + KAction *lPreviousTabAction = new KAction(i18n("Activate Previous Tab"), this); + lPreviousTabAction->setShortcut(KStandardShortcut::tabPrev()); + mActionCollection->addAction("previous_tab", lPreviousTabAction); + addAction((QAction *)lPreviousTabAction); + connect(lPreviousTabAction, SIGNAL(triggered()), this, SLOT(activatePreviousTab())); + + mActionCollection->readSettings(); +} + +void BBMainWindow::setupControls() +{ + mPlaybackControls = new QWidget(); + + mHistoryMenu = new QMenu(this); + + mPreviousButton = new QToolButton(mPlaybackControls); + mPreviousButton->setIconSize(QSize(32,32)); + mPreviousButton->setDefaultAction(mPreviousAction); + mPreviousButton->setAutoRaise(true); + mPreviousButton->setPopupMode(QToolButton::MenuButtonPopup); + mPreviousButton->setMenu(mHistoryMenu); + mPreviousButton->setEnabled(false); + + mPlayPauseButton = new QToolButton(mPlaybackControls); + mPlayPauseButton->setIconSize(QSize(32,32)); + mPlayPauseButton->setAutoRaise(true); + mPlayPauseButton->setDefaultAction(mPlayPauseAction); + + mPlayQueueMenu = new QMenu(this); + + mNextButton = new QToolButton(mPlaybackControls); + mNextButton->setIconSize(QSize(32,32)); + mNextButton->setDefaultAction(mNextAction); + mNextButton->setAutoRaise(true); + mNextButton->setPopupMode(QToolButton::MenuButtonPopup); + mNextButton->setMenu(0); + + mPositionSlider = new Phonon::SeekSlider(mPlaybackControls); + mPositionSlider->setIconVisible(false); + mShuffleCheckBox = new QCheckBox(i18n("Shuffle"), mPlaybackControls); + connect(mShuffleCheckBox, SIGNAL(toggled(bool)), SLOT(updateShuffleStatus())); + + mVolumeSlider = new Phonon::VolumeSlider(mPlaybackControls); + mVolumeSlider->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + mVolumeSlider->setMuteVisible(false); + + mElapsedTimeLabel = new QLabel("00:00", mPlaybackControls); + mElapsedTimeLabel->setFont(KGlobalSettings::generalFont()); + mElapsedTimeLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + mCurrentSongLabel = new BBTitleLabel(mPlaybackControls); + mCurrentSongLabel->setFont(KGlobalSettings::generalFont()); + mCurrentSongLabel->setNewText(i18n("Hey ho! Let's go!")); + + QHBoxLayout *lHLayout = new QHBoxLayout; + lHLayout->addWidget(mPreviousButton); + lHLayout->addWidget(mPlayPauseButton); + lHLayout->addWidget(mNextButton); + QVBoxLayout *lVLayout = new QVBoxLayout; + lHLayout->addLayout(lVLayout); + QHBoxLayout *lHLayout2 = new QHBoxLayout; + lVLayout->addLayout(lHLayout2); + lHLayout2->addWidget(mCurrentSongLabel); + lHLayout2->addWidget(mElapsedTimeLabel); + QHBoxLayout *lHLayout3 = new QHBoxLayout; + lVLayout->addLayout(lHLayout3); + lHLayout3->addWidget(mShuffleCheckBox); + lHLayout3->addWidget(mPositionSlider); + lHLayout3->addWidget(mVolumeSlider); + + mPlaybackControls->setLayout(lHLayout); +} + +void BBMainWindow::closeEvent(QCloseEvent *pEvent) +{ + saveSession(); + pEvent->accept(); + kapp->quit(); +} + +void BBMainWindow::setActiveTab(int pCurrentIndex) +{ + int lCount = count(); + for(int i = 0; i < lCount; ++i) + { + QWidget *lMenuBar = tabBar()->tabButton(i, QTabBar::RightSide); + if(lMenuBar) + lMenuBar->setEnabled(i == pCurrentIndex); + } + if(mCollectionTab->tabNumber() == pCurrentIndex) + mCollectionTab->embedControls(mPlaybackControls); + else if(mFileSystemTab->tabNumber() == pCurrentIndex) + mFileSystemTab->embedControls(mPlaybackControls); + else if(mStreamsTab->tabNumber() == pCurrentIndex) + mStreamsTab->embedControls(mPlaybackControls); +} + +void BBMainWindow::setCurrentPlaylistSystem(int pTabNumber) +{ + if(mPlaylistSystem) + mPlaylistSystem->disconnect(); + + if(pTabNumber == mCollectionTab->tabNumber()) + mPlaylistSystem = mCollectionTab; + if(pTabNumber == mFileSystemTab->tabNumber()) + mPlaylistSystem = mFileSystemTab; + if(pTabNumber == mStreamsTab->tabNumber()) + mPlaylistSystem = mStreamsTab; + + connect(mPlaylistSystem, SIGNAL(songQueryReady(BBSongQueryJob)), this, SLOT(playNextSong(BBSongQueryJob))); +} + +void BBMainWindow::showConfigDialog() +{ + if(KConfigDialog::showDialog("settings")) + return; + + KConfigDialog *dialog = new KConfigDialog(this, "settings", BBSettings::self()); + QWidget *generalSettingsDlg = new QWidget; + Ui_DirectoriesPage lDirPage; + lDirPage.setupUi(generalSettingsDlg); + dialog->addPage(generalSettingsDlg, i18n("File locations"), "folder-sound"); + //connect(dialog, SIGNAL(settingsChanged(QString)), m_view, SLOT(settingsChanged())); + dialog->show(); +} + +void BBMainWindow::showHelp() +{ + KToolInvocation::invokeHelp(); +} + +void BBMainWindow::showAboutDialog() +{ + KAboutApplicationDialog *lDialog = new KAboutApplicationDialog(mAboutData, this); + lDialog->show(); + connect(lDialog, SIGNAL(finished()), lDialog, SLOT(deleteLater())); +} + +void BBMainWindow::showShortcutsDialog() +{ + KShortcutsDialog lDialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); + lDialog.addCollection(mActionCollection, "BoomBox"); + lDialog.addCollection(mCollectionTab->actionCollection(), i18n("Music Collection Tab")); + lDialog.addCollection(mFileSystemTab->actionCollection(), i18n("File System Tab")); + lDialog.configure(); +} + +void BBMainWindow::togglePlayback() +{ + if(mMediaObject->currentSource().type() == Phonon::MediaSource::Empty) + jumpToNextSong(true); + else + { + Phonon::State lState = mMediaObject->state(); + if(lState == Phonon::PlayingState || lState == Phonon::BufferingState) + mMediaObject->pause(); + else + mMediaObject->play(); + } +} + +void BBMainWindow::queueNextSong() +{ + QList lQueue = mPlayQueueMenu->actions(); + if(!lQueue.isEmpty()) + { + QAction *lNext = lQueue.takeFirst(); + mPlayQueueMenu->removeAction(lNext); + BBSongRef *lSongRef = (BBSongRef *)lNext->data().value(); + setCurrentIndex(lSongRef->mTabIndex); + setCurrentPlaylistSystem(lSongRef->mTabIndex); + + BBSongQueryJob lJob; + lJob.mOnlyPlaceInQueue = true; + lJob.mResumePlaying = true; + lJob.mSong = lSongRef->mSong; + playNextSong(lJob); + + delete lSongRef; + delete lNext; + if(lQueue.isEmpty()) + mNextButton->setMenu(0); + } + else + { + BBSongQueryJob lJob; + lJob.mShuffle = mShuffleCheckBox->checkState(); + lJob.mOnlyPlaceInQueue = true; + lJob.mResumePlaying = true; + mPlaylistSystem->queryNextSong(lJob); + } +} + +void BBMainWindow::jumpToNextSong(bool pStartPlayback) +{ + QList lQueue = mPlayQueueMenu->actions(); + if(!lQueue.isEmpty()) + { + QAction *lNext = lQueue.takeFirst(); + mPlayQueueMenu->removeAction(lNext); + BBSongRef *lSongRef = (BBSongRef *)lNext->data().value(); + setCurrentIndex(lSongRef->mTabIndex); + setCurrentPlaylistSystem(lSongRef->mTabIndex); + setCurrentSong(lSongRef->mSong, pStartPlayback); + delete lSongRef; + delete lNext; + if(lQueue.isEmpty()) + mNextButton->setMenu(0); + } + else + { + BBSongQueryJob lJob; + lJob.mShuffle = mShuffleCheckBox->checkState(); + lJob.mOnlyPlaceInQueue = false; + lJob.mResumePlaying = pStartPlayback || mMediaObject->state() == Phonon::PlayingState;; + mPlaylistSystem->queryNextSong(lJob); + } +} + +void BBMainWindow::jumpToPreviousSong() +{ + QList lHistory = mHistoryMenu->actions(); + if(lHistory.isEmpty()) + return; + + QAction *lPrevious = lHistory.takeFirst(); + mHistoryMenu->removeAction(lPrevious); + BBSongRef *lSongRef = (BBSongRef *)lPrevious->data().value(); + setCurrentIndex(lSongRef->mTabIndex); + setCurrentPlaylistSystem(lSongRef->mTabIndex); + setCurrentSong(lSongRef->mSong, false, false); + delete lSongRef; + delete lPrevious; + if(lHistory.isEmpty()) + mPreviousButton->setEnabled(false); +} + +void BBMainWindow::playNextSong(const BBSongQueryJob &pJob) +{ + Phonon::MediaSource lMediaSource = mMediaObject->currentSource(); + if(lMediaSource.type() == Phonon::MediaSource::Url + || lMediaSource.type() == Phonon::MediaSource::LocalFile) + { + addCurrentSongToHistory(); + } + + mNextSong = pJob.mSong; + KUrl lSongUrl = mPlaylistSystem->songUrl(pJob.mSong); + if(pJob.mOnlyPlaceInQueue && mMediaObject->state() == Phonon::PlayingState) + mMediaObject->enqueue(lSongUrl); + else + { + mMediaObject->stop(); + mMediaObject->clearQueue(); + mMediaObject->setCurrentSource(lSongUrl); + if(pJob.mResumePlaying) + mMediaObject->play(); + } +} + +void BBMainWindow::setCurrentSong(QVariant pSong, bool pStartPlayback, bool pAddToHistory) +{ + KUrl lNewSongUrl = mPlaylistSystem->songUrl(pSong); + if(lNewSongUrl.isEmpty()) + return; + + Phonon::MediaSource lMediaSource = mMediaObject->currentSource(); + if(pAddToHistory && (lMediaSource.type() == Phonon::MediaSource::Url + || lMediaSource.type() == Phonon::MediaSource::LocalFile)) + { + addCurrentSongToHistory(); + } + + bool lResumePlaying = mMediaObject->state() == Phonon::PlayingState; + mNextSong = pSong; + mMediaObject->stop(); + mMediaObject->clearQueue(); + mMediaObject->setCurrentSource(lNewSongUrl); + if(pStartPlayback || lResumePlaying) + mMediaObject->play(); +} + +void BBMainWindow::addToPlayQueue(const QVariant &pSong, const QString &pDisplayText, int pTabNumber) +{ + QAction *lQueueAction = new QAction(this); + BBSongRef *lSongRef = new BBSongRef(); + lSongRef->mSong = pSong; + lSongRef->mTabIndex = pTabNumber; + lQueueAction->setData(QVariant::fromValue((void *)lSongRef)); + lQueueAction->setText(pDisplayText); + connect(lQueueAction, SIGNAL(triggered()), this, SLOT(playSongFromQueue())); + mPlayQueueMenu->addAction(lQueueAction); + mNextButton->setMenu(mPlayQueueMenu); +} + +void BBMainWindow::addCurrentSongToHistory() +{ + QAction *lHistoryAction = new QAction(this); + BBSongRef *lSongRef = new BBSongRef(); + lSongRef->mSong = mPlayingPlaylistSystem->currentSong(); + lSongRef->mTabIndex = mPlayingPlaylistSystem->tabNumber(); + lHistoryAction->setData(QVariant::fromValue((void *)lSongRef)); + lHistoryAction->setText(mCurrentSongLabel->text()); + connect(lHistoryAction, SIGNAL(triggered()), this, SLOT(playSongFromHistory())); + QList lHistory = mHistoryMenu->actions(); + mHistoryMenu->insertAction(lHistory.isEmpty() ? 0 : lHistory.first(), lHistoryAction); + if(lHistory.count() == gMaxHistory) + { + mHistoryMenu->removeAction(lHistory.last()); + delete lHistory.last(); + } + mPreviousButton->setEnabled(true); +} + +void BBMainWindow::updateElapsedTime(qint64 pElapsedTime) +{ + QTime lTime(0, pElapsedTime/60000, (pElapsedTime/1000) % 60); + mElapsedTimeLabel->setText(lTime.toString("mm:ss")); + if(qAbs(pElapsedTime - mLastUpdatedTime) > 1500) { + mMpris2Player->notifySeeked(pElapsedTime * 1000); + } + mLastUpdatedTime = pElapsedTime; +} + +void BBMainWindow::updateCurrentSong(const Phonon::MediaSource &pNewSource) +{ + Q_UNUSED(pNewSource) + mPlaylistSystem->setCurrentSong(mNextSong); + mCurrentSongLabel->setNewText(mPlaylistSystem->displayString(mNextSong)); + //at this time we can be sure the origin for current song is the current playlist system + mPlayingPlaylistSystem = mPlaylistSystem; +} + +void BBMainWindow::updatePhononState(Phonon::State pNewState, Phonon::State pOldState) +{ + switch (pNewState) + { + case Phonon::ErrorState: + if(mMediaObject->errorType() == Phonon::FatalError) + QMessageBox::warning(this, i18n("Fatal Error"), mMediaObject->errorString()); + else + QMessageBox::warning(this, i18n("Error"), mMediaObject->errorString()); + break; + case Phonon::PlayingState: + mPlayPauseAction->setIcon(KIcon("media-playback-pause")); + break; + case Phonon::StoppedState: + mPlayPauseAction->setIcon(KIcon("media-playback-start")); + mElapsedTimeLabel->setText("00:00"); + break; + case Phonon::PausedState: + mPlayPauseAction->setIcon(KIcon("media-playback-start")); + break; + case Phonon::BufferingState: + mBufferingStatusTimer->start(200); + break; + default: + break; + } + + if(pOldState == Phonon::BufferingState) + { + if(mPlaylistSystem == mStreamsTab) + updateMetaDataDisplay(); + else + mCurrentSongLabel->setNewText(mPlaylistSystem->displayString(mPlaylistSystem->currentSong())); + mBufferingStatusTimer->stop(); + } + if(mMpris2Player) { + mMpris2Player->notifyChangedProperty(QLatin1String("PlaybackStatus")); + } +} + +void BBMainWindow::playSongFromHistory() +{ + QAction *lSenderAction = qobject_cast(sender()); + if(lSenderAction) + { + BBSongRef *lSongRef = (BBSongRef *)lSenderAction->data().value(); + setCurrentIndex(lSongRef->mTabIndex); + setCurrentPlaylistSystem(lSongRef->mTabIndex); + setCurrentSong(lSongRef->mSong, false, false); + delete lSongRef; + + QList lHistory = mHistoryMenu->actions(); + QAction *lAction; + while((lAction = lHistory.takeFirst()) != lSenderAction) + { + mHistoryMenu->removeAction(lAction); + lAction->deleteLater(); + } + lSenderAction->deleteLater(); + + if(lHistory.isEmpty()) + mPreviousButton->setEnabled(false); + } +} + +void BBMainWindow::playSongFromQueue() +{ + QAction *lSenderAction = qobject_cast(sender()); + if(lSenderAction) + { + BBSongRef *lSongRef = (BBSongRef *)lSenderAction->data().value(); + setCurrentIndex(lSongRef->mTabIndex); + setCurrentPlaylistSystem(lSongRef->mTabIndex); + setCurrentSong(lSongRef->mSong, false); + delete lSongRef; + + QList lQueue = mPlayQueueMenu->actions(); + QAction *lAction; + while((lAction = lQueue.takeFirst()) != lSenderAction) + { + mPlayQueueMenu->removeAction(lAction); + lAction->deleteLater(); + } + lSenderAction->deleteLater(); + if(lQueue.isEmpty()) + mNextButton->setMenu(0); + } +} + +void BBMainWindow::changeEvent(QEvent *pEvent) +{ + if(pEvent->type() == QEvent::FontChange) + { + mCurrentSongLabel->setFont(KGlobalSettings::generalFont()); + } + KTabWidget::changeEvent(pEvent); +} + +void BBMainWindow::saveSession() +{ + KSharedConfigPtr lConfig = KGlobal::config(); + KConfigGroup lSessionGroup(lConfig, "session"); + + lSessionGroup.writeEntry("shuffle_enabled", mShuffleCheckBox->isChecked()); + lSessionGroup.writeEntry("active_tab", this->currentIndex()); + mCollectionTab->saveSession(lSessionGroup); + mFileSystemTab->saveSession(lSessionGroup); + mStreamsTab->saveSession(lSessionGroup); + lConfig->sync(); +} + +void BBMainWindow::readSession() +{ + KSharedConfigPtr lConfig = KGlobal::config(); + KConfigGroup lSessionGroup(lConfig, "session"); + mCollectionTab->readSession(lSessionGroup); + mFileSystemTab->readSession(lSessionGroup); + mStreamsTab->readSession(lSessionGroup); + mShuffleCheckBox->setChecked(lSessionGroup.readEntry("shuffle_enabled", false)); + int lActiveTab = lSessionGroup.readEntry("active_tab", 0); + setCurrentIndex(lActiveTab); + setCurrentPlaylistSystem(lActiveTab); + mPlayingPlaylistSystem = mPlaylistSystem; + + + // only create mpris interface after everything else is constructed. + // doing otherwise causes dbus deadlock with the "now playing" mpris + // daemon from ktp, running in kded + QDBusConnection lConnection = QDBusConnection::sessionBus(); + lConnection.registerService("org.mpris.MediaPlayer2.BoomBox"); + mMpris2Player = new Mpris2Player(this); + new MediaPlayer2Adaptor(mMpris2Player); + new PlayerAdaptor(mMpris2Player); + lConnection.registerObject("/org/mpris/MediaPlayer2", mMpris2Player); +} + +void BBMainWindow::activateNextTab() +{ + setCurrentIndex((currentIndex() + 1) % count()); +} + +void BBMainWindow::activatePreviousTab() +{ + setCurrentIndex((currentIndex() + count() - 1) % count()); +} + +void BBMainWindow::updateMetaDataDisplay() +{ + // Streams can have changing metadata + if(!mMediaObject->isSeekable()) { + QMultiMap lMetaData = mMediaObject->metaData(); + QString lArtist = lMetaData.value("ARTIST"); + QString lTitle = lMetaData.value("TITLE"); + QString lNewName; + if(!lArtist.isEmpty()) + { + if(!lTitle.isEmpty()) + lNewName = QString::fromAscii("%1 - %2").arg(lArtist, lTitle); + else + lNewName = lArtist; + } + else + { + if(!lTitle.isEmpty()) + lNewName = lTitle; + else + lNewName = mPlaylistSystem->displayString(mPlaylistSystem->currentSong()); + } + mCurrentSongLabel->setNewText(lNewName); + } + // always update mpris clients + if(mMpris2Player) { + mMpris2Player->notifyChangedProperty(QLatin1String("Metadata")); + } +} + +void BBMainWindow::updateShuffleStatus() { + if(mMpris2Player) { + mMpris2Player->notifyChangedProperty(QLatin1String("Shuffle")); + } +} + +void BBMainWindow::setBufferingStatus() +{ + mCurrentSongLabel->setNewText(i18n("Buffering...")); +} diff --git a/boombox/src/bbmainwindow.h b/boombox/src/bbmainwindow.h new file mode 100644 index 00000000..24905ceb --- /dev/null +++ b/boombox/src/bbmainwindow.h @@ -0,0 +1,188 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBMAINWINDOW_H +#define BBMAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Phonon +{ +class SeekSlider; +class VolumeSlider; +class AudioOutput; +class MediaObject; +class MediaSource; +} + +class BBCollectionTab; +class BBFileSystemTab; +class BBStreamsTab; +class BBPlaylistSystem; +class Mpris2Player; +class KActionCollection; +class KAction; +class QToolButton; + +class BBTitleLabel : public QLabel +{ +Q_OBJECT +public: + explicit BBTitleLabel(QWidget *pParent = 0); + virtual QSize minimumSizeHint() const {return QSize(1, 1);} + +public slots: + void advanceAnimation(); + void setNewText(const QString &pText); + +protected: + virtual void paintEvent(QPaintEvent *); + virtual void resizeEvent(QResizeEvent *pEvent); + float mX, mVx; + QTimer *mTimer; + QPixmap mSecondPic; + const int mMargin, mUpdateInterval; + const float mSpeed; +}; + +class BBTabMenuBar : public KMenuBar +{ +public: + BBTabMenuBar(QWidget *pParent = 0) : KMenuBar(pParent) {} + virtual bool event(QEvent *pEvent) + { + if(!isEnabled() && pEvent->type() == QEvent::MouseButtonPress) + { + pEvent->ignore(); + return false; + } + else + return KMenuBar::event(pEvent); + } +}; + +struct BBSongQueryJob +{ + //the query + bool mShuffle; + + //the result + QVariant mSong; + + //from mainwindow, to be returned again along with result + bool mOnlyPlaceInQueue; + bool mResumePlaying; +}; + +struct BBSongRef +{ + QVariant mSong; + int mTabIndex; +}; + +class BBMainWindow : public KTabWidget +{ +Q_OBJECT + +public: + BBMainWindow(const KAboutData *pAboutData, QWidget *parent = 0); + ~BBMainWindow(); + bool shuffleActive() { return mShuffleCheckBox->isChecked(); } + void setShuffle(bool pEnable) { mShuffleCheckBox->setChecked(pEnable); } + void saveSession(); + void readSession(); + void setCurrentSong(QVariant pSong, bool pStartPlayback = true, bool pAddToHistory = true); + void addToPlayQueue(const QVariant &pSong, const QString &pDisplayText, int pTabNumber); + void addCurrentSongToHistory(); + void setCurrentPlaylistSystem(int pTabNumber); + +public slots: + void setActiveTab(int pCurrentIndex); + void showConfigDialog(); + void showHelp(); + void showAboutDialog(); + void showShortcutsDialog(); + void updatePhononState(Phonon::State pNewState, Phonon::State pOldState); + void togglePlayback(); + void jumpToNextSong(bool pStartPlayback = false); + void jumpToPreviousSong(); + void updateElapsedTime(qint64 pElapsedTime); + void updateCurrentSong(const Phonon::MediaSource &pNewSource); + void queueNextSong(); + void playNextSong(const BBSongQueryJob &pJob); + void activateNextTab(); + void activatePreviousTab(); + virtual void changeEvent(QEvent *); + void updateMetaDataDisplay(); + void updateShuffleStatus(); + void setBufferingStatus(); + void playSongFromHistory(); + void playSongFromQueue(); + +protected: + void closeEvent(QCloseEvent *pEvent); + + void setupActions(); + void setupControls(); + + BBCollectionTab *mCollectionTab; + BBFileSystemTab *mFileSystemTab; + BBStreamsTab *mStreamsTab; + KActionCollection *mActionCollection; + Phonon::SeekSlider *mPositionSlider; + Phonon::MediaObject *mMediaObject; + Phonon::AudioOutput *mAudioOutput; + Phonon::VolumeSlider *mVolumeSlider; + QVariant mNextSong; + KAction *mPlayPauseAction; + KAction *mNextAction; + KAction *mPreviousAction; + const KAboutData *mAboutData; + + QLabel *mElapsedTimeLabel; + BBTitleLabel *mCurrentSongLabel; + QCheckBox *mShuffleCheckBox; + QToolButton *mPlayPauseButton; + QToolButton *mNextButton; + QToolButton *mPreviousButton; + QWidget *mPlaybackControls; + QMenu *mHistoryMenu; + QMenu *mPlayQueueMenu; + + BBPlaylistSystem *mPlaylistSystem; + BBPlaylistSystem *mPlayingPlaylistSystem; + + QTimer *mBufferingStatusTimer; + + qint64 mLastUpdatedTime; + Mpris2Player *mMpris2Player; + friend class Mpris2Player; +}; + +extern BBMainWindow *gMainWindow; + +#endif // BBMAINWINDOW_H diff --git a/boombox/src/bbmetadata.cpp b/boombox/src/bbmetadata.cpp new file mode 100644 index 00000000..f9795a8c --- /dev/null +++ b/boombox/src/bbmetadata.cpp @@ -0,0 +1,141 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbmetadata.h" + +#include +#include +#include +#include + +BBMetaData::BBMetaData(const BBMetaData &pMetaData) +{ + mPath = pMetaData.mPath; + mFileID = pMetaData.mFileID; + mTitle = pMetaData.mTitle; + mArtist = pMetaData.mArtist; + mAlbum = pMetaData.mAlbum; + mLength = pMetaData.mLength; + mYear = pMetaData.mYear; + mGenre = pMetaData.mGenre; + mTrack = pMetaData.mTrack; + mComment = pMetaData.mComment; +} + +void BBMetaData::GetInfoFrom(const QString &pPath, const QString &pFileName) +{ + mPath = pPath; + mFileName = pFileName; + QString lTemp = pFileName.toLower(); + if(lTemp.endsWith(".ogg") || lTemp.endsWith(".mp3") || lTemp.endsWith(".mpc") || lTemp.endsWith(".flac")) + { + TagLib::FileRef lFile(QFile::encodeName(QString("%1/%2").arg(pPath, pFileName))); + TagLib::Tag *lTag = lFile.tag(); + TagLib::AudioProperties *lAudio = lFile.audioProperties(); + + if(lTag != NULL) + { + mTitle = QString::fromUtf8(lTag->title().toCString(true)).simplified(); + mArtist = QString::fromUtf8(lTag->artist().toCString(true)).simplified(); + mAlbum = QString::fromUtf8(lTag->album().toCString(true)).simplified(); + mGenre = QString::fromUtf8(lTag->genre().toCString(true)).simplified(); + mComment = QString::fromUtf8(lTag->comment().toCString(true)).simplified(); + mYear = QString::number(lTag->year()); + mTrack = QString::number(lTag->track()); + } + if(lAudio != NULL) + { + QTime lTime(0, lAudio->length()/60, lAudio->length()%60); + mLength = lTime.toString("mm:ss"); + } + } + CheckInfo(); +} + +void BBMetaData::CheckInfo() +{ + QString lReplacer("#-UNKNOWN-#"); + + if(mTitle.isEmpty()) + { + QFileInfo fi(mPath); + mTitle = fi.baseName(); + mTitle.replace('_', " "); + } + if(mArtist.isEmpty()) + mArtist = lReplacer; + if(mAlbum.isEmpty()) + mAlbum = lReplacer; + if(mGenre.isEmpty()) + mGenre = lReplacer; + if(mYear == "0") + mYear = lReplacer; + if(mTrack == "0") + mTrack = lReplacer; +} + +void BBMetaData::ChangeFile(const QString &pPath, const QString &pCategory, const QString &pNewValue) +{ + QString lTemp = pPath.toLower(); + if(!(lTemp.endsWith(".ogg") || lTemp.endsWith(".mp3") || lTemp.endsWith(".mpc") || lTemp.endsWith(".flac"))) + return; + + TagLib::FileRef lFile(QFile::encodeName(pPath)); + TagLib::Tag *lTag = lFile.tag(); + + if(lTag != NULL) + { + TagLib::String lNewValue(pNewValue.toUtf8().data(), TagLib::String::UTF8); + + if(pCategory == "title") + lTag->setTitle(lNewValue); + else if(pCategory == "artist") + lTag->setArtist(lNewValue); + else if(pCategory == "album") + lTag->setAlbum(lNewValue); + else if(pCategory == "comment") + lTag->setComment(lNewValue); + else if(pCategory == "genre") + lTag->setGenre(lNewValue); + else if(pCategory == "year") + lTag->setYear(pNewValue.toUInt()); + else if(pCategory == "track") + lTag->setTrack(pNewValue.toUInt()); + + lFile.save(); + } +} + +void BBMetaData::WriteInfoToFile() +{ + TagLib::FileRef lFile(QFile::encodeName(QString("%1/%2").arg(mPath, mFileName))); + TagLib::Tag *lTag = lFile.tag(); + + if(lTag != NULL) + { + lTag->setTitle(TagLib::String(mTitle.toUtf8().data(), TagLib::String::UTF8)); + lTag->setArtist(TagLib::String(mArtist.toUtf8().data(), TagLib::String::UTF8)); + lTag->setAlbum(TagLib::String(mAlbum.toUtf8().data(), TagLib::String::UTF8)); + lTag->setComment(TagLib::String(mComment.toUtf8().data(), TagLib::String::UTF8)); + lTag->setGenre(TagLib::String(mGenre.toUtf8().data(), TagLib::String::UTF8)); + lTag->setYear(mYear.toUInt()); + lTag->setTrack(mTrack.toUInt()); + lFile.save(); + } +} diff --git a/boombox/src/bbmetadata.h b/boombox/src/bbmetadata.h new file mode 100644 index 00000000..c565a16b --- /dev/null +++ b/boombox/src/bbmetadata.h @@ -0,0 +1,40 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBMETADATA_H +#define BBMETADATA_H + +#include + +class BBMetaData +{ + public: + BBMetaData() {} + BBMetaData(const BBMetaData &pMetaData); + void GetInfoFrom(const QString &pPath, const QString& pFileName); + void WriteInfoToFile(); + static void ChangeFile(const QString &pPath, const QString &pCategory, const QString &pNewValue); + void CheckInfo(); + + QString mTitle, mArtist, mAlbum, mLength, mYear, mGenre, mTrack, mComment, mPath, mFileName; + int mFileID; +}; + + +#endif diff --git a/boombox/src/bbmetainfodialog.cpp b/boombox/src/bbmetainfodialog.cpp new file mode 100644 index 00000000..245b60b3 --- /dev/null +++ b/boombox/src/bbmetainfodialog.cpp @@ -0,0 +1,438 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbmetainfodialog.h" +#include "kmessagebox.h" +#include "ui_bbmetainfodialog.h" +#include "bbmetadata.h" +#include "bbsetmodel.h" +#include "bbsettings.h" +#include "bbcollectiontab.h" + +#include + +BBMetaInfoDialog::BBMetaInfoDialog(QWidget *parent, BBCollectionTab *pCollectionTab): + QDialog(parent), mUI(new Ui::BBMetaInfoDialog), mCollectionTab(pCollectionTab) +{ + mUI->setupUi(this); + connect(this, SIGNAL(accepted()), this, SLOT(saveValues())); + connect(mUI->mGuessButton, SIGNAL(clicked()), this, SLOT(guessFieldNames())); +} + +BBMetaInfoDialog::~BBMetaInfoDialog() +{ + delete mUI; +} + +void BBMetaInfoDialog::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + mUI->retranslateUi(this); + break; + default: + break; + } +} + +bool BBMetaInfoDialog::fillInValues(const QModelIndex &pModelIndex) +{ + mIndex = pModelIndex; + mFileInfo = QFileInfo(QString("%1/%2").arg(pModelIndex.data(BBPathRole).toString(), + pModelIndex.data(BBFileNameRole).toString())); + if(!mFileInfo.exists()) + { + KMessageBox::sorry(this, i18n("Sorry, the file does not exist anymore. You should run an update to keep the BoomBox database in sync with the filesystem."), mFileInfo.canonicalFilePath()); + return false; + } + if(!mFileInfo.isWritable()) + { + KMessageBox::sorry(this, i18n("Sorry, the file is not writable. Make sure you have correct permissions to alter the file and then try again."), mFileInfo.canonicalFilePath()); + return false; + } + mMetaData.GetInfoFrom(mFileInfo.absolutePath(), mFileInfo.fileName()); + mUI->mPathDisplay->setText(QString("%1/%2").arg(mMetaData.mPath, mMetaData.mFileName)); + mUI->mArtistEdit->setText(mMetaData.mArtist); + mUI->mAlbumEdit->setText(mMetaData.mAlbum); + mUI->mTitleEdit->setText(mMetaData.mTitle); + mUI->mTrackSpinner->setValue(mMetaData.mTrack.toInt()); + mUI->mYearSpinner->setValue(mMetaData.mYear.toInt()); + mUI->mGenreEdit->setText(mMetaData.mGenre); + mUI->mCommentEdit->setPlainText(mMetaData.mComment); + return true; +} + +void BBMetaInfoDialog::saveValues() +{ + int lFileID = mIndex.data(BBFileIDRole).toInt(); + bool lValuesChanged = false; + mDatabase.connect(BBSettings::fileNameDB()); + if(mUI->mArtistEdit->text() != mMetaData.mArtist) + { + lValuesChanged = true; + mMetaData.mArtist = mUI->mArtistEdit->text(); + mDatabase.executeStatement( + QString("UPDATE songs SET artist = '%1' WHERE file_ID = '%2'").arg(BBDatabase::prepareString(mMetaData.mArtist)).arg(lFileID) + ); + } + if(mUI->mTitleEdit->text() != mMetaData.mTitle) + { + lValuesChanged = true; + mMetaData.mTitle = mUI->mTitleEdit->text(); + mDatabase.executeStatement( + QString("UPDATE songs SET title = '%1' WHERE file_ID = '%2'").arg(BBDatabase::prepareString(mMetaData.mTitle)).arg(lFileID) + ); + } + if(mUI->mGenreEdit->text() != mMetaData.mGenre) + { + lValuesChanged = true; + mMetaData.mGenre = mUI->mGenreEdit->text(); + mDatabase.executeStatement( + QString("UPDATE songs SET genre = '%1' WHERE file_ID = '%2'").arg(BBDatabase::prepareString(mMetaData.mGenre)).arg(lFileID) + ); + } + QString lComment = mUI->mCommentEdit->document()->toPlainText(); + if(lComment != mMetaData.mComment) + { + lValuesChanged = true; + mMetaData.mComment = lComment; + mDatabase.executeStatement( + QString("UPDATE songs SET comment = '%1' WHERE file_ID = '%2'").arg(BBDatabase::prepareString(mMetaData.mComment)).arg(lFileID) + ); + } + QString lTrack = QString::number(mUI->mTrackSpinner->value()); + if(lTrack != mMetaData.mTrack) + { + lValuesChanged = true; + mMetaData.mTrack = lTrack; + mDatabase.executeStatement( + QString("UPDATE songs SET track = '%1' WHERE file_ID = '%2'").arg(BBDatabase::prepareString(mMetaData.mTrack)).arg(lFileID) + ); + } + QString lYear = QString::number(mUI->mYearSpinner->value()); + if(lYear != mMetaData.mYear) + { + lValuesChanged = true; + mMetaData.mYear = lYear; + mDatabase.executeStatement( + QString("UPDATE songs SET year = '%1' WHERE file_ID = '%2'").arg(BBDatabase::prepareString(mMetaData.mYear)).arg(lFileID) + ); + } + if(mUI->mAlbumEdit->text() != mMetaData.mAlbum) + { + lValuesChanged = true; + mMetaData.mAlbum = mUI->mAlbumEdit->text(); + int lAlbumID; + BBResultTable lResult; + mDatabase.getTable(QString( + "SELECT DISTINCT album_ID FROM songs JOIN albums USING (album_ID) " + "WHERE album = '%1' AND path = '%2'").arg(BBDatabase::prepareString(mMetaData.mAlbum), + BBDatabase::prepareString(mMetaData.mPath)), + lResult); + if(lResult.mNumRows == 0) + { + mDatabase.executeStatement(QString( + "INSERT INTO albums (path, album, is_VA, cover_art_path) " + "VALUES ('%1','%2','%3','%4')").arg(BBDatabase::prepareString(mMetaData.mPath), + BBDatabase::prepareString(mMetaData.mAlbum), + QString::number(0), QString()) + ); + lAlbumID = sqlite3_last_insert_rowid(mDatabase.getDatabase()); + } + else + { + lAlbumID = QString::fromUtf8(lResult.at(1, 0)).toInt(); + } + mDatabase.freeTable(lResult); + mDatabase.executeStatement( + QString("UPDATE songs SET album_ID = %1 WHERE file_ID = %2").arg(lAlbumID).arg(lFileID) + ); + } + if(lValuesChanged) + { + mMetaData.WriteInfoToFile(); + mCollectionTab->refreshAllViewsAndUpdateItem(mIndex); + } +} + +QStringList BBMetaInfoDialog::splitName(QString lName) +{ + QStringList lSplitFileName = lName.split(QRegExp("[\\.-]")); //split on dot or hyphen + + QStringList lFields; + foreach (QString lField, lSplitFileName) + { + QString lSimp = lField.replace('_', ' ').simplified(); + QRegExp lRegExp("^\\d\\d\\s"); + if(lSimp.contains(lRegExp)) + { + lFields << lSimp.left(2); + lSimp = lSimp.right(lSimp.length() - 2).simplified(); + } + + lRegExp = QRegExp("\\b(\\w+)\\b"); + QStringList lDontTouch; + lDontTouch <<"a" <<"an" <<"the" <<"it" <<"this" <<"that" <<"but" <<"and" <<"or" <<"for" <<"so" <<"yet" + <<"in" <<"out" <<"on" <<"over" <<"of" <<"off" <<"to" <<"from" <<"by" <<"with"; + int lPos = 0; + while((lPos = lRegExp.indexIn(lSimp, lPos)) != -1) + { + QString lMatch = lRegExp.cap(1).toLower(); + if(!lDontTouch.contains(lMatch) || lPos == 0) + { + lMatch.replace(0, 1, lMatch.at(0).toUpper()); + } + lSimp.replace(lPos, lMatch.length(), lMatch); + lPos += lRegExp.matchedLength(); + } + + lRegExp = QRegExp("\\s\\d\\d$"); + if(lSimp.contains(lRegExp)) + { + lFields << lSimp.left(lSimp.length() - 2).simplified(); + lSimp = lSimp.right(2); + } + + lFields << lSimp; + } + + if(lFields.last().contains("mp3", Qt::CaseInsensitive) || lFields.last().contains("ogg", Qt::CaseInsensitive) + || lFields.last().contains("flac", Qt::CaseInsensitive)) + { + lFields.takeLast(); + } + return lFields; +} + +void BBMetaInfoDialog::guessFieldNames() +{ + int lYear = -1, lTrack = -1; + QString lArtist, lAlbum, lTitle; + + QStringList lFileFields = splitName(mFileInfo.fileName()); + QString lFolderName = mFileInfo.canonicalPath(); + int lLastSlash = lFolderName.lastIndexOf('/'); + if(lLastSlash != -1) + lFolderName.remove(0, lLastSlash + 1); + QStringList lFolderFields = splitName(lFolderName); + + bool lIsNumber; + int lFileNumberBitField = 0; + int lFolderNumberBitField = 0; + for(int i = 0; i < lFileFields.count(); ++i) + { + lFileFields.at(i).toInt(&lIsNumber); + if(lIsNumber) + lFileNumberBitField |= 1 << i; + } + for(int i = 0; i < lFolderFields.count(); ++i) + { + lFolderFields.at(i).toInt(&lIsNumber); + if(lIsNumber) + lFolderNumberBitField |= 1 << i; + } + + switch(lFileFields.count()) + { + case 1: + if(lFileNumberBitField != 0) + lTrack = lFileFields.at(0).toInt(); + else + lTitle = lFileFields.at(0); + break; + case 2: + switch(lFileNumberBitField) + { + case 0: + lArtist = lFileFields.at(0); + lTitle = lFileFields.at(1); + break; + case 1: + case 3: + lTrack = lFileFields.at(0).toInt(); + lTitle = lFileFields.at(1); + break; + case 2: + lAlbum = lFileFields.at(0); + lTrack = lFileFields.at(1).toInt(); + break; + } + break; + case 3: + switch(lFileNumberBitField & 3) //only expect number in first two fields, otherwise maybe numeric title or such. + { + case 0: + lArtist = lFileFields.at(0); + lAlbum = lFileFields.at(1); + lTitle = lFileFields.at(2); + break; + case 1: + case 3: + lTrack = lFileFields.at(0).toInt(); + lTitle = lFileFields.at(1); + break; + case 2: + lArtist = lFileFields.at(0); + lTrack = lFileFields.at(1).toInt(); + lTitle = lFileFields.at(2); + break; + } + break; + case 4: + case 5: + case 6: + case 7: + switch(lFileNumberBitField & 7) //only expect number in first three fields, otherwise maybe numeric title or such. + { + case 0: + lArtist = lFileFields.at(0); + lAlbum = lFileFields.at(1); + lTitle = lFileFields.at(2); + break; + case 1: + case 3: + case 5: + case 7: + lTrack = lFileFields.at(0).toInt(); + lArtist = lFileFields.at(1); + lTitle = lFileFields.at(2); + break; + case 2: + case 6: + lArtist = lFileFields.at(0); + lYear = lFileFields.at(1).toInt(); + lAlbum = lFileFields.at(2); + lTitle = lFileFields.at(3); + break; + case 4: + lArtist = lFileFields.at(0); + lAlbum = lFileFields.at(1); + lTrack = lFileFields.at(2).toInt(); + lTitle = lFileFields.at(3); + break; + } + break; + } + + switch(lFolderFields.count()) + { + case 1: + if(lFolderNumberBitField == 0) + lAlbum = lFolderFields.at(0); + break; + case 2: + switch(lFolderNumberBitField) + { + case 0: + if(lArtist.isEmpty()) + lArtist = lFolderFields.at(0); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(1); + break; + case 1: + if(lYear == -1) + lYear = lFolderFields.at(0).toInt(); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(1); + break; + case 2: + case 3: + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(0); + if(lYear == -1) + lYear = lFolderFields.at(1).toInt(); + break; + } + break; + case 3: + case 4: + case 5: + case 6: + switch(lFolderNumberBitField & 15) + { + case 0: + if(lArtist.isEmpty()) + lArtist = lFolderFields.at(0); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(1) + " - " + lFolderFields.at(2); + break; + case 1: + if(lYear == -1) + lYear = lFolderFields.at(0).toInt(); + if(lArtist.isEmpty()) + lArtist = lFolderFields.at(1); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(2); + break; + case 2: + case 3: + if(lArtist.isEmpty()) + lArtist = lFolderFields.at(0); + if(lYear == -1) + lYear = lFolderFields.at(1).toInt(); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(2); + break; + case 4: + case 5: + case 6: + case 7: + if(lArtist.isEmpty()) + lArtist = lFolderFields.at(0); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(1); + if(lYear == -1) + lYear = lFolderFields.at(2).toInt(); + break; + case 8: + if(lArtist.isEmpty()) + lArtist = lFolderFields.at(0); + if(lAlbum.isEmpty()) + lAlbum = lFolderFields.at(1) + " - " + lFolderFields.at(2); + if(lYear == -1) + lYear = lFolderFields.at(3).toInt(); + break; + } + break; + } + + int lCDNumber = 0; + if(lYear != -1) + mUI->mYearSpinner->setValue(lYear); + if(lTrack != -1) + { + lCDNumber = lTrack / 100; + lTrack = lTrack - lCDNumber*100; + mUI->mTrackSpinner->setValue(lTrack); + + if(lCDNumber > 0 && !lAlbum.isEmpty()) + lAlbum.append(QString::fromAscii(" CD %1").arg(lCDNumber)); + } + if(!lArtist.isNull()) + mUI->mArtistEdit->setText(lArtist); + if(!lAlbum.isNull()) + mUI->mAlbumEdit->setText(lAlbum); + if(!lTitle.isNull()) + mUI->mTitleEdit->setText(lTitle); + + //TODO: if some fields are not filled out now, check folder name too. + +} + diff --git a/boombox/src/bbmetainfodialog.h b/boombox/src/bbmetainfodialog.h new file mode 100644 index 00000000..988cef10 --- /dev/null +++ b/boombox/src/bbmetainfodialog.h @@ -0,0 +1,62 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBMETAINFODIALOG_H +#define BBMETAINFODIALOG_H + +#include "bbmetadata.h" +#include "bbdatabase.h" +#include +#include +#include + +class BBCollectionTab; + +namespace Ui { + class BBMetaInfoDialog; +} + +class BBMetaInfoDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY(BBMetaInfoDialog) + public: + BBMetaInfoDialog(QWidget *parent, BBCollectionTab *pCollectionTab); + virtual ~BBMetaInfoDialog(); + bool fillInValues(const QModelIndex &pModelIndex); + + protected: + virtual void changeEvent(QEvent *e); + + protected slots: + void saveValues(); + void guessFieldNames(); + + private: + QStringList splitName(QString lName); + + Ui::BBMetaInfoDialog *mUI; + BBMetaData mMetaData; + BBDatabase mDatabase; + QModelIndex mIndex; + QFileInfo mFileInfo; + BBCollectionTab *mCollectionTab; +}; + +#endif // BBMETAINFODIALOG_H diff --git a/boombox/src/bbmetainfodialog.ui b/boombox/src/bbmetainfodialog.ui new file mode 100644 index 00000000..a8c40c3a --- /dev/null +++ b/boombox/src/bbmetainfodialog.ui @@ -0,0 +1,261 @@ + + + Simon Persson + BBMetaInfoDialog + + + + 0 + 0 + 284 + 324 + + + + Edit Song Tags + + + + + + + + Path: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + Guess Values From File Name + + + + + + + Artist: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + mArtistEdit + + + + + + + true + + + + + + + Album: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + mAlbumEdit + + + + + + + true + + + + + + + Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + mTitleEdit + + + + + + + true + + + + + + + Track: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Year: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + mYearSpinner + + + + + + + 10000 + + + + + + + Genre: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + mGenreEdit + + + + + + + true + + + + + + + Comment: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + false + + + + + + + + KIntSpinBox + QSpinBox +
knuminput.h
+
+ + KLineEdit + QLineEdit +
klineedit.h
+
+
+ + mGuessButton + mArtistEdit + mAlbumEdit + mTitleEdit + mTrackSpinner + mYearSpinner + mGenreEdit + mCommentEdit + mButtonBox + + + + + mButtonBox + rejected() + BBMetaInfoDialog + reject() + + + 320 + 295 + + + 286 + 274 + + + + + mButtonBox + accepted() + BBMetaInfoDialog + accept() + + + 252 + 295 + + + 157 + 274 + + + + +
diff --git a/boombox/src/bbplaylistsystem.cpp b/boombox/src/bbplaylistsystem.cpp new file mode 100644 index 00000000..b19693a7 --- /dev/null +++ b/boombox/src/bbplaylistsystem.cpp @@ -0,0 +1,3 @@ +#include "bbplaylistsystem.h" + +//this dummy file will trick cmake to compile and link the moc file. diff --git a/boombox/src/bbplaylistsystem.h b/boombox/src/bbplaylistsystem.h new file mode 100644 index 00000000..37032da6 --- /dev/null +++ b/boombox/src/bbplaylistsystem.h @@ -0,0 +1,67 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBPLAYLISTSYSTEM_H +#define BBPLAYLISTSYSTEM_H + +#include "bbmainwindow.h" + +#include +#include +#include +#include + +class BBPlaylistSystem: public KMainWindow +{ +Q_OBJECT +public: + BBPlaylistSystem(int pTabNumber) : mTabNumber(pTabNumber) {} + virtual ~BBPlaylistSystem() {} + virtual void queryNextSong(BBSongQueryJob &pJob) = 0; + virtual void queryPreviousSong(BBSongQueryJob &pJob) = 0; + + virtual QVariant currentSong() = 0; + virtual void setCurrentSong(const QVariant &pSong) = 0; + + virtual QString displayString(const QVariant &pSong) = 0; + virtual KUrl songUrl(const QVariant &pSong) = 0; + + virtual void readSession(KConfigGroup &pConfigGroup) = 0; + virtual void saveSession(KConfigGroup &pConfigGroup) = 0; + + virtual void embedControls(QWidget *pControls) = 0; + + int tabNumber() {return mTabNumber;} + +signals: + void songQueryReady(const BBSongQueryJob &pJob); + +protected: + int mTabNumber; +}; + + +enum BBPlaylistRoles +{ + BBPathRole = Qt::UserRole + 1, BBFileNameRole, + BBArtistRole, BBTitleRole, BBAlbumRole, BBYearRole, BBLengthRole, BBFileIDRole, + BBIsFolderRole, BBUrlRole, BBAlbumIDRole +}; + +#endif diff --git a/boombox/src/bbresultview.cpp b/boombox/src/bbresultview.cpp new file mode 100644 index 00000000..393b0f8e --- /dev/null +++ b/boombox/src/bbresultview.cpp @@ -0,0 +1,243 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbresultview.h" +#include "bbplaylistsystem.h" +#include "bbsetmodel.h" +#include "bblistitem.h" +#include "bbcollectiontab.h" +#include "bbmetainfodialog.h" +#include "bbmainwindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int gTextMargin = 2; + +class BBSongItemDelegate : public QStyledItemDelegate +{ + public: + virtual void paint(QPainter *pPainter, const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const; + virtual QSize sizeHint(const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const; +}; + +void BBSongItemDelegate::paint(QPainter *pPainter, const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const +{ + QStyleOptionViewItemV4 lOption = pOption; + initStyleOption(&lOption, pIndex); + QStyle *style = QApplication::style(); + + pPainter->save(); + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &lOption, pPainter); + QRectF r = pOption.rect; + r.adjust(gTextMargin, 0, -gTextMargin, 0); + pPainter->setFont(lOption.font); + if(lOption.state & QStyle::State_Selected) + pPainter->setPen(lOption.palette.color(QPalette::Active, QPalette::HighlightedText)); + else + pPainter->setPen(lOption.palette.color(QPalette::Active, QPalette::Text)); + + if(pIndex.internalPointer() != NULL) + { + QRectF lLengthRect; + pPainter->drawText(r, Qt::AlignVCenter | Qt::AlignRight, pIndex.data(BBLengthRole).toString(), &lLengthRect); + r.setRight(lLengthRect.left() - gTextMargin); + pPainter->drawText(r, Qt::AlignVCenter, pIndex.data(Qt::DisplayRole).toString()); + } + else + { + int lOriginalFontSize = lOption.font.pointSize(); + lOption.font.setPointSize(lOriginalFontSize + 2); + pPainter->setFont(lOption.font); + QFontMetrics lFontMetric(lOption.font); + QString lArtist = pIndex.data(BBArtistRole).toString(); + pPainter->drawText(r.left() + gTextMargin, r.top() + lFontMetric.ascent(), lArtist); + lOption.font.setPointSize(lOriginalFontSize + 1); + pPainter->setFont(lOption.font); + pPainter->drawText(r.left() + lFontMetric.width(lArtist) + 6*gTextMargin, r.top() + lFontMetric.ascent(), + pIndex.data(BBAlbumRole).toString()); + } + pPainter->restore(); +} + +QSize BBSongItemDelegate::sizeHint(const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const +{ + QSize lSize = QStyledItemDelegate::sizeHint(pOption, pIndex); + if(!pIndex.parent().isValid()) + { + QFont lFont = pOption.font; + lFont.setPointSize(lFont.pointSize() + 2); + QFontMetrics lFontMetrics(lFont); + lSize.setHeight(lFontMetrics.height()); + } + return lSize; +} + +BBResultView::BBResultView(BBCollectionTab *pCollectionTab, BBAlbumSongModel *pModel) + : QWidget(pCollectionTab), mModel(pModel), mCollectionTab(pCollectionTab) +{ + QVBoxLayout *lLayout = new QVBoxLayout; + mLineEdit = new KLineEdit; + mTreeView = new QTreeView; + QHBoxLayout *lHLayout = new QHBoxLayout; + mResetAllButton = new QPushButton(i18n("Reset All Filters")); + + lHLayout->addWidget(mLineEdit); + lHLayout->addWidget(mResetAllButton); + lLayout->addLayout(lHLayout); + lLayout->addWidget(mTreeView); + lLayout->setSpacing(1); + + setLayout(lLayout); + + mLineEdit->setClearButtonShown(true); + mLineEdit->setClickMessage(i18n("Enter filtering text here...")); + + mTreeView->setModel(pModel); + mTreeView->setSelectionMode(QAbstractItemView::ExtendedSelection); + mTreeView->setHeaderHidden(true); + mTreeView->setAlternatingRowColors(true); + mTreeView->setAnimated(true); + mTreeView->setItemDelegate(new BBSongItemDelegate); + mTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + + KAction *lEditAction = new KAction(i18n("Edit"), this); + lEditAction->setShortcut(Qt::Key_F2); + lEditAction->setShortcutContext(Qt::WidgetShortcut); + mTreeView->addAction(lEditAction); + connect(lEditAction, SIGNAL(triggered()), SLOT(editFilesTriggered())); + + KAction *lEnqueueAction = new KAction(i18n("Add to Queue"), this); + lEnqueueAction->setShortcut(Qt::CTRL + Qt::Key_E); + lEnqueueAction->setShortcutContext(Qt::WidgetShortcut); + mTreeView->addAction(lEnqueueAction); + connect(lEnqueueAction, SIGNAL(triggered()), SLOT(enqueueTriggered())); + + mContextMenu = new QMenu(this); + mContextMenu->addAction(lEnqueueAction); + mContextMenu->addAction(lEditAction); + + mAutoExpandInProgress = false; + + connect(mLineEdit, SIGNAL(textChanged(const QString &)), SLOT(lineEditChanged(const QString &))); + connect(mTreeView, SIGNAL(activated(QModelIndex)), SLOT(itemClicked(QModelIndex))); + connect(mTreeView, SIGNAL(expanded(QModelIndex)), SLOT(itemExpanded(QModelIndex))); + connect(mTreeView, SIGNAL(collapsed(QModelIndex)), SLOT(itemCollapsed(QModelIndex))); + connect(mTreeView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showPopupMenu(QPoint))); + connect(mResetAllButton, SIGNAL(clicked()), mCollectionTab, SLOT(resetAllFilters())); +} + +void BBResultView::lineEditChanged(const QString &) +{ + mCollectionTab->refreshAllExcept(NULL); +} + +QSet BBResultView::filesToUpdate() +{ + QSet lSet; + QItemSelectionModel *lSelectionModel = mTreeView->selectionModel(); + if(lSelectionModel->hasSelection()) + { + QModelIndexList lIndexes = lSelectionModel->selectedIndexes(); + foreach(QModelIndex i, lIndexes) + { + if(i.parent().isValid()) //only song items, not albums + lSet.insert(mModel->findSongItem(i)); + } + } + else + { + lSet = mModel->allSongs(); + } + return lSet; +} + +void BBResultView::itemClicked(QModelIndex pIndex) +{ + if(pIndex.parent().isValid()) + { + gMainWindow->setCurrentPlaylistSystem(mCollectionTab->tabNumber()); + gMainWindow->setCurrentSong(pIndex.data(BBFileIDRole)); + } +} + +void BBResultView::scrollToNewSong(const QModelIndex &pIndexCurrent, const QModelIndex &pIndexPrevious) +{ + mTreeView->setAnimated(false); + QModelIndex lPreviousAlbum(pIndexPrevious.parent()); + if(lPreviousAlbum != pIndexCurrent.parent() && !mModel->isManuallyExpanded(lPreviousAlbum)) + mTreeView->collapse(lPreviousAlbum); + + mAutoExpandInProgress = true; + mTreeView->scrollTo(pIndexCurrent); + mAutoExpandInProgress = false; + mTreeView->setAnimated(true); +} + +void BBResultView::itemExpanded(const QModelIndex &pIndex) +{ + if(!mAutoExpandInProgress) + mModel->setManuallyExpanded(pIndex, true); +} + +void BBResultView::itemCollapsed(const QModelIndex &pIndex) +{ + mModel->setManuallyExpanded(pIndex, false); +} + +void BBResultView::showPopupMenu(const QPoint &pPoint) +{ + QModelIndex lIndex = mTreeView->indexAt(pPoint); + if(lIndex.isValid() && lIndex.parent().isValid()) + mContextMenu->popup(mTreeView->mapToGlobal(pPoint)); +} + +void BBResultView::editFilesTriggered() +{ + QItemSelectionModel *lSelectionModel = mTreeView->selectionModel(); + QModelIndex lIndex = lSelectionModel->currentIndex(); + if(!lIndex.isValid() || !lIndex.parent().isValid()) + return; + + BBMetaInfoDialog *lDialog = new BBMetaInfoDialog(mCollectionTab, mCollectionTab); + connect(lDialog, SIGNAL(finished(int)), lDialog, SLOT(deleteLater())); + if(lDialog->fillInValues(lIndex)) + lDialog->show(); +} + +void BBResultView::enqueueTriggered() +{ + QItemSelectionModel *lSelectionModel = mTreeView->selectionModel(); + QModelIndex lIndex = lSelectionModel->currentIndex(); + if(!lIndex.isValid() || !lIndex.parent().isValid()) + return; + + QString lDisplayText = QString("%1 - %2").arg(lIndex.data(BBArtistRole).toString(), lIndex.data(BBTitleRole).toString()); + gMainWindow->addToPlayQueue(lIndex.data(BBFileIDRole), lDisplayText, mCollectionTab->tabNumber()); +} + diff --git a/boombox/src/bbresultview.h b/boombox/src/bbresultview.h new file mode 100644 index 00000000..9c24afb5 --- /dev/null +++ b/boombox/src/bbresultview.h @@ -0,0 +1,64 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBRESULTVIEW_H +#define BBRESULTVIEW_H + +#include +#include +#include + +class QTreeView; +class QPushButton; +class QMenu; +class BBCollectionTab; +class BBAlbumSongModel; +class BBSongListItem; + +class BBResultView : public QWidget +{ + Q_OBJECT + public: + BBResultView(BBCollectionTab *pCollectionTab, BBAlbumSongModel *pModel); + + QString filterText() {return mLineEdit->text();} + QSet filesToUpdate(); + void scrollToNewSong(const QModelIndex &pIndexCurrent, const QModelIndex &pIndexPrevious); + + protected slots: + void lineEditChanged(const QString &pNewText); + void itemClicked(QModelIndex pIndex); + void itemExpanded(const QModelIndex &pIndex); + void itemCollapsed(const QModelIndex &pIndex); + void showPopupMenu(const QPoint &pPoint); + void editFilesTriggered(); + void enqueueTriggered(); + + protected: + QTreeView *mTreeView; + KLineEdit *mLineEdit; + QPushButton *mResetAllButton; + BBAlbumSongModel *mModel; + BBCollectionTab *mCollectionTab; + QMenu *mContextMenu; + + bool mAutoExpandInProgress; +}; + +#endif // BBRESULTVIEW_H diff --git a/boombox/src/bbsetmodel.cpp b/boombox/src/bbsetmodel.cpp new file mode 100644 index 00000000..c3f527e7 --- /dev/null +++ b/boombox/src/bbsetmodel.cpp @@ -0,0 +1,552 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbsetmodel.h" +#include "bbcollectiontab.h" + +#include +#include +#include +#include + +QVariant BBStringSetModel::data(const QModelIndex &pIndex, int pRole) const +{ + if(!pIndex.isValid()) + return QVariant(); + + if(pIndex.row() >= mList.size()) + return QVariant(); + + if(pRole == Qt::DisplayRole) + return mList.at(pIndex.row()); + else + return QVariant(); +} + +void BBStringSetModel::setNewSet(const QSet &pNewSet) +{ + QList lOldSongs = (mSet - pNewSet).toList(); + qSort(lOldSongs.begin(), lOldSongs.end(), qGreater()); + + int lStartPos = -1, lEndPos = -1; + QListIterator i(lOldSongs); + if(i.hasNext()) + lStartPos = lEndPos = i.next().mPosition; + while(i.hasNext()) + { + int lNextPos = i.next().mPosition; + if(lNextPos == lStartPos - 1) + lStartPos--; + else + { + beginRemoveRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lEndPos; j>=lStartPos; --j) + mList.removeAt(j); + endRemoveRows(); + lStartPos = lEndPos = lNextPos; + } + } + if(lStartPos >= 0) + { + beginRemoveRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lEndPos; j>=lStartPos; --j) + mList.removeAt(j); + endRemoveRows(); + } + + QList lNewSongs = (pNewSet - mSet).toList(); + qSort(lNewSongs.begin(), lNewSongs.end()); + + lStartPos = -1, lEndPos = -1; + i = lNewSongs; + if(i.hasNext()) + lStartPos = lEndPos = i.next().mPosition; + while(i.hasNext()) + { + int lNextPos = i.next().mPosition; + if(lNextPos == lEndPos + 1) + lEndPos++; + else + { + beginInsertRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lStartPos; j<=lEndPos; ++j) + mList.insert(j, lNewSongs.takeFirst().mString); + endInsertRows(); + lStartPos = lEndPos = lNextPos; + } + } + if(lStartPos >= 0) + { + beginInsertRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lStartPos; j<=lEndPos; ++j) + mList.insert(j, lNewSongs.takeFirst().mString); + endInsertRows(); + } + + mSet = pNewSet; +} + +int BBAlbumSongModel::rowCount(const QModelIndex & pParent) const +{ + if(!pParent.isValid()) + return mAlbumList.count(); + else + { + if(pParent.internalPointer() != NULL || pParent.row() >= mAlbumList.count() || pParent.row() < 0) + return 0; + return mAlbumList.at(pParent.row()).mSongs.count(); + } +} + +QVariant BBAlbumSongModel::data(const QModelIndex &pIndex, int pRole) const +{ + if(!pIndex.isValid() || pIndex.column() != 0) + return QVariant(); + + if(pIndex.internalPointer() == NULL) //this is an album item + { + if(pIndex.row() >= mAlbumList.size() || pIndex.row() < 0) + return QVariant(); + + const BBAlbumData &lData = mAlbumList.at(pIndex.row()); + switch(pRole) + { + case Qt::FontRole: + if(lData.mAlbumID == mCurrentlyPlayingAlbumID) + { + QFont lFont = mCollectionTab->font(); + lFont.setBold(true); + return lFont; + } + else + return QVariant(); + case BBAlbumRole: + return lData.mAlbum; + case BBArtistRole: + return lData.mArtist; + case BBPathRole: + return lData.mFolderPath; + default: + return QVariant(); + } + } + else // this is a song item + { + BBAlbumData *lAlbumData = (BBAlbumData *) pIndex.internalPointer(); + if(pIndex.row() >= lAlbumData->mSongs.count()) + return QVariant(); + + const BBSongData &lSongData = lAlbumData->mSongs.at(pIndex.row()); + switch(pRole) + { + case Qt::FontRole: + if(lSongData.mFileID == mCurrentlyPlayingSongID) + { + QFont lFont = mCollectionTab->font(); + lFont.setBold(true); + return lFont; + } + else + return QVariant(); + case Qt::DisplayRole: + if(lAlbumData->mIsVA) + return QString("%1 - %2").arg(lSongData.mArtist, lSongData.mTitle); + case BBTitleRole: //fall through + return lSongData.mTitle; + case BBArtistRole: + if(lAlbumData->mIsVA) + return lSongData.mArtist; + else + return lAlbumData->mArtist; + case BBPathRole: + return lAlbumData->mFolderPath; + case BBFileNameRole: + return lSongData.mFileName; + case BBLengthRole: + return lSongData.mLength; + case BBFileIDRole: + return lSongData.mFileID; + case BBAlbumIDRole: + return lAlbumData->mAlbumID; + default: + return QVariant(); + } + } +} + +QModelIndex BBAlbumSongModel::index(int pRow, int pColumn, const QModelIndex &pParent) const +{ + if(!pParent.isValid()) //this is an album item + { + if(pRow >= mAlbumList.count() || pRow < 0 || pColumn != 0) + return QModelIndex(); + return createIndex(pRow, pColumn, (void *)NULL); + } + else //this is a song item + { + if(pParent.row() >= mAlbumList.count()) + return QModelIndex(); + const BBAlbumData &lAlbumData = mAlbumList.at(pParent.row()); + if(pRow >= lAlbumData.mSongs.count() || pRow < 0 || pColumn != 0) + return QModelIndex(); + return createIndex(pRow, pColumn, (void *)&lAlbumData); + } +} + +QModelIndex BBAlbumSongModel::parent(const QModelIndex &pIndex) const +{ + if(!pIndex.isValid() || pIndex.internalPointer() == NULL) + return QModelIndex(); + else + { + BBAlbumData *lAlbumData = (BBAlbumData *)pIndex.internalPointer(); + return createIndex(findAlbumRow(lAlbumData->mAlbumID), 0, (void *)NULL); + } +} + +void BBAlbumSongModel::setNewSongs(const QSet &pNewSongs, const QSet &pNewAlbums) +{ + //remove songs to be updated now, added again later in this method. + QList lOldSongs = (mSongSet - pNewSongs + mSongsToUpdate).toList(); + // sort in reverse order so that removal starts in end of list, keeps indexes correct. + qSort(lOldSongs.begin(), lOldSongs.end(), qGreater()); + + int lStartPos = -1, lEndPos = -1, lCurrentAlbumPos = -1; + + QListIterator i(lOldSongs); + if(i.hasNext()) + { + const BBSongListItem &lNext = i.next(); + lStartPos = lEndPos = lNext.mIndex.mSongPos; + lCurrentAlbumPos = lNext.mIndex.mAlbumPos; + } + while(i.hasNext()) + { + const BBSongListItem &lNext = i.next(); + int lNextPos = lNext.mIndex.mSongPos; + int lNextAlbumPos = lNext.mIndex.mAlbumPos; + + if(lNextPos == lStartPos - 1 && lCurrentAlbumPos == lNextAlbumPos) + lStartPos--; + else + { + beginRemoveRows(createIndex(lCurrentAlbumPos, 0, (void *)NULL), lStartPos, lEndPos); + for(int j = lEndPos; j>=lStartPos; --j) + mAlbumList[lCurrentAlbumPos].mSongs.removeAt(j); + endRemoveRows(); + lStartPos = lEndPos = lNextPos; + lCurrentAlbumPos = lNextAlbumPos; + } + } + if(lStartPos >= 0) + { + beginRemoveRows(createIndex(lCurrentAlbumPos, 0, (void *)NULL), lStartPos, lEndPos); + for(int j = lEndPos; j>=lStartPos; --j) + mAlbumList[lCurrentAlbumPos].mSongs.removeAt(j); + endRemoveRows(); + } + + QList lOldAlbums = (mAlbumSet - pNewAlbums).toList(); + // sort in reverse order so that removal starts in end of list, keeps indexes correct. + qSort(lOldAlbums.begin(), lOldAlbums.end(), qGreater()); + + lStartPos = -1; + lEndPos = -1; + + QListIterator i2(lOldAlbums); + if(i2.hasNext()) + lStartPos = lEndPos = i2.next().mPosition; + while(i2.hasNext()) + { + int lNextPos = i2.next().mPosition; + if(lNextPos == lStartPos - 1) + lStartPos--; + else + { + beginRemoveRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lEndPos; j>=lStartPos; --j) + mAlbumList.removeAt(j); + endRemoveRows(); + lStartPos = lEndPos = lNextPos; + } + } + if(lStartPos >= 0) + { + beginRemoveRows(QModelIndex(),lStartPos, lEndPos); + for(int j = lEndPos; j>=lStartPos; --j) + mAlbumList.removeAt(j); + endRemoveRows(); + } + + //removing updated songs from sets here will cause them to be added + mSongSet -= mSongsToUpdate; + + QList lNewAlbums = (pNewAlbums- mAlbumSet).toList(); + qSort(lNewAlbums.begin(), lNewAlbums.end()); + + lStartPos = -1; + lEndPos = -1; + i2 = lNewAlbums; + if(i2.hasNext()) + lStartPos = lEndPos = i2.next().mPosition; + while(i2.hasNext()) + { + int lNextPos = i2.next().mPosition; + if(lNextPos == lEndPos + 1) + lEndPos++; + else + { + beginInsertRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lStartPos; j<=lEndPos; ++j) + mAlbumList.insert(j, lNewAlbums.takeFirst().mData); + endInsertRows(); + lStartPos = lEndPos = lNextPos; + } + } + if(lStartPos >= 0) + { + beginInsertRows(QModelIndex(), lStartPos, lEndPos); + for(int j = lStartPos; j<=lEndPos; ++j) + mAlbumList.insert(j, lNewAlbums.takeFirst().mData); + endInsertRows(); + } + + QList lNewSongs = (pNewSongs - mSongSet).toList(); + qSort(lNewSongs.begin(), lNewSongs.end()); + + lStartPos = -1; + lEndPos = -1; + lCurrentAlbumPos = -1; + i = lNewSongs; + if(i.hasNext()) + { + const BBSongListItem &lNext = i.next(); + lStartPos = lEndPos = lNext.mIndex.mSongPos; + lCurrentAlbumPos = lNext.mIndex.mAlbumPos; + } + while(i.hasNext()) + { + const BBSongListItem &lNext = i.next(); + int lNextPos = lNext.mIndex.mSongPos; + int lNextAlbumPos = lNext.mIndex.mAlbumPos; + + if(lNextPos == lEndPos + 1 && lCurrentAlbumPos == lNextAlbumPos) + lEndPos++; + else + { + beginInsertRows(createIndex(lCurrentAlbumPos, 0, (void *)NULL), lStartPos, lEndPos); + for(int j = lStartPos; j<=lEndPos; ++j) + mAlbumList[lCurrentAlbumPos].mSongs.insert(j, lNewSongs.takeFirst().mData); + endInsertRows(); + lStartPos = lEndPos = lNextPos; + lCurrentAlbumPos = lNextAlbumPos; + } + } + if(lStartPos >= 0) + { + beginInsertRows(createIndex(lCurrentAlbumPos, 0, (void *)NULL), lStartPos, lEndPos); + for(int j = lStartPos; j<=lEndPos; ++j) + mAlbumList[lCurrentAlbumPos].mSongs.insert(j, lNewSongs.takeFirst().mData); + endInsertRows(); + } + + mAlbumSet = pNewAlbums; + mSongSet = pNewSongs; + shuffle(); +} + +void BBAlbumSongModel::shuffle() +{ + if(mSongSet.isEmpty()) + { + mLastShuffleItem = mFirstShuffleItem = BBIndex(); + return; + } + QList lTempList; + for(int i = 0; i < mAlbumList.count(); ++i) + { + int max = mAlbumList.at(i).mSongs.count(); + for(int j = 0; j < max; ++j) + lTempList.append(BBIndex(i, j)); + } + + BBIndex lRandomPos = lTempList.takeAt(KRandom::random() % lTempList.count()); + mLastShuffleItem = mFirstShuffleItem = lRandomPos; + findSongData(mFirstShuffleItem).mPrevShuffled = BBIndex(); + + while(!lTempList.isEmpty()) + { + lRandomPos = lTempList.takeAt(KRandom::random() % lTempList.count()); + findSongData(mLastShuffleItem).mNextShuffled = lRandomPos; + findSongData(lRandomPos).mPrevShuffled = mLastShuffleItem; + mLastShuffleItem = lRandomPos; + } + findSongData(mLastShuffleItem).mNextShuffled = BBIndex(); +} + +QPersistentModelIndex BBAlbumSongModel::nextLinear(const QPersistentModelIndex &pIndex) +{ + if(!pIndex.isValid()) + return QModelIndex(); + BBAlbumData *lAlbumData = (BBAlbumData *)pIndex.internalPointer(); + int lNextSongPos = pIndex.row() + 1; + if(lNextSongPos >= lAlbumData->mSongs.count()) + { + lNextSongPos = 0; + int lNextAlbumPos = findAlbumRow(lAlbumData->mAlbumID) + 1; + if(lNextAlbumPos >= mAlbumList.count()) + return QModelIndex(); + lAlbumData = (BBAlbumData *) &mAlbumList.at(lNextAlbumPos); + } + return createIndex(lNextSongPos, 0, (void *)lAlbumData); +} + +QPersistentModelIndex BBAlbumSongModel::nextShuffled(const QPersistentModelIndex &pIndex) +{ + if(!pIndex.isValid()) + return QModelIndex(); + BBAlbumData *lAlbumData = (BBAlbumData *)pIndex.internalPointer(); + BBIndex lNextPos = lAlbumData->mSongs.at(pIndex.row()).mNextShuffled; + if(!lNextPos.isValid()) + return QModelIndex(); + return createIndex(lNextPos.mSongPos, 0, (void *)&mAlbumList.at(lNextPos.mAlbumPos)); +} + +QPersistentModelIndex BBAlbumSongModel::prevLinear(const QPersistentModelIndex &pIndex) +{ + if(!pIndex.isValid()) + return QModelIndex(); + BBAlbumData *lAlbumData = (BBAlbumData *)pIndex.internalPointer(); + int lNextSongPos = pIndex.row() - 1; + if(lNextSongPos < 0) + { + int lNextAlbumPos = findAlbumRow(lAlbumData->mAlbumID) - 1; + if(lNextAlbumPos < 0) + return QModelIndex(); + lAlbumData = (BBAlbumData *) &mAlbumList.at(lNextAlbumPos); + lNextSongPos = lAlbumData->mSongs.count() - 1; + } + return createIndex(lNextSongPos, 0, (void *)lAlbumData); +} + +QPersistentModelIndex BBAlbumSongModel::prevShuffled(const QPersistentModelIndex &pIndex) +{ + if(!pIndex.isValid()) + return QModelIndex(); + BBAlbumData *lAlbumData = (BBAlbumData *)pIndex.internalPointer(); + BBIndex lNextPos = lAlbumData->mSongs.at(pIndex.row()).mPrevShuffled; + if(!lNextPos.isValid()) + return QModelIndex(); + return createIndex(lNextPos.mSongPos, 0, (void *)&mAlbumList.at(lNextPos.mAlbumPos)); +} + +QPersistentModelIndex BBAlbumSongModel::firstLinear() +{ + if(mSongSet.isEmpty()) + return QModelIndex(); + return createIndex(0, 0, (void *)&mAlbumList.first()); +} + +QPersistentModelIndex BBAlbumSongModel::firstShuffled() +{ + if(!mFirstShuffleItem.isValid()) + return QModelIndex(); + return createIndex(mFirstShuffleItem.mSongPos, 0, (void *)&mAlbumList.at(mFirstShuffleItem.mAlbumPos)); +} + +QPersistentModelIndex BBAlbumSongModel::lastLinear() +{ + if(mSongSet.isEmpty()) + return QModelIndex(); + + int lSongPos = mAlbumList.last().mSongs.count() - 1; + return createIndex(lSongPos, 0, (void *)&mAlbumList.last()); +} + +QPersistentModelIndex BBAlbumSongModel::lastShuffled() +{ + if(!mLastShuffleItem.isValid()) + return QModelIndex(); + return createIndex(mLastShuffleItem.mSongPos, 0, (void *)&mAlbumList.at(mLastShuffleItem.mAlbumPos)); +} + +void BBAlbumSongModel::setCurrentSong(int pCurrentSong, int pCurrentAlbum, QModelIndex pPreviousIndex) +{ + mCurrentlyPlayingSongID = pCurrentSong; + mCurrentlyPlayingAlbumID = pCurrentAlbum; + + QModelIndex lCurrentIndex = indexForFileID(pCurrentSong); + if(lCurrentIndex.isValid() && lCurrentIndex.column() == 0 && lCurrentIndex.internalPointer() != NULL) + { + emit dataChanged(lCurrentIndex, lCurrentIndex); + QModelIndex lAlbumIndex = lCurrentIndex.parent(); + emit dataChanged(lAlbumIndex, lAlbumIndex); + } + if(pPreviousIndex.isValid() && pPreviousIndex.column() == 0 && pPreviousIndex.internalPointer() != NULL) + { + emit dataChanged(pPreviousIndex, pPreviousIndex); + QModelIndex lAlbumIndex = pPreviousIndex.parent(); + emit dataChanged(lAlbumIndex, lAlbumIndex); + } +} + +void BBAlbumSongModel::setManuallyExpanded(const QModelIndex &pIndex, bool pManuallyExpanded) +{ + if(!pIndex.isValid() || pIndex.column() != 0 || pIndex.internalPointer() != NULL) + return; + if(pIndex.row() >= mAlbumList.count()) + return; + mAlbumList[pIndex.row()].mManuallyExpanded = pManuallyExpanded; +} + +bool BBAlbumSongModel::isManuallyExpanded(const QModelIndex &pIndex) +{ + if(!pIndex.isValid() || pIndex.column() != 0 || pIndex.internalPointer() != NULL) + return false; + if(pIndex.row() >= mAlbumList.count()) + return false; + return mAlbumList[pIndex.row()].mManuallyExpanded; +} + +int BBAlbumSongModel::findAlbumRow(int pAlbumID) const +{ + BBAlbumListItem lItem; + lItem.mData.mAlbumID = pAlbumID; + QSet::const_iterator lIter = mAlbumSet.find(lItem); + return (*lIter).mPosition; +} + +BBSongListItem BBAlbumSongModel::findSongItem(QModelIndex pIndex) +{ + BBSongListItem lItem; + lItem.mData.mFileID = pIndex.data(BBFileIDRole).toInt(); + QSet::const_iterator lIter = mSongSet.find(lItem); + return *lIter; +} + +QModelIndex BBAlbumSongModel::indexForFileID(int pFileID) +{ + BBSongListItem lItem; + lItem.mData.mFileID = pFileID; + QSet::const_iterator lIter = mSongSet.find(lItem); + if(lIter == mSongSet.constEnd()) + return QModelIndex(); + else + return index((*lIter).mIndex.mSongPos, 0, index((*lIter).mIndex.mAlbumPos)); +} diff --git a/boombox/src/bbsetmodel.h b/boombox/src/bbsetmodel.h new file mode 100644 index 00000000..d6474f72 --- /dev/null +++ b/boombox/src/bbsetmodel.h @@ -0,0 +1,120 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBSETMODEL_H +#define BBSETMODEL_H + +#include +#include +#include +#include + +#include "bblistitem.h" + +class BBCollectionTab; + + +class BBStringSetModel : public QAbstractListModel +{ +public: + BBStringSetModel(QObject *pParent) + : QAbstractListModel(pParent) + {} + + virtual int rowCount(const QModelIndex & /*parent = QModelIndex()*/ ) const + { return mList.count();} + virtual QVariant data(const QModelIndex &pIndex, int pRole = Qt::DisplayRole) const; + void setNewSet(const QSet &pNewSet); + int findStringPos(const QString &pString) + { + return mList.indexOf(pString); + } + +protected: + QSet mSet; + QStringList mList; +}; + + +class BBAlbumSongModel : public QAbstractItemModel +{ + Q_OBJECT +public: + BBAlbumSongModel(QObject *pParent, BBCollectionTab *pCollectionTab) + : QAbstractItemModel(pParent), mLastShuffleItem(), mFirstShuffleItem(), + mCurrentlyPlayingSongID(-1), mCurrentlyPlayingAlbumID(-1), mCollectionTab(pCollectionTab) + {} + + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const + { + Q_UNUSED(section); + Q_UNUSED(orientation); + Q_UNUSED(role); + return QVariant(); + } + + virtual int columnCount(const QModelIndex &pParent = QModelIndex()) const + { + Q_UNUSED(pParent); + return 1; + } + virtual QModelIndex index(int pRow, int pColumn = 0, const QModelIndex &pParent = QModelIndex()) const; + virtual QModelIndex parent(const QModelIndex & pIndex) const; + virtual int rowCount(const QModelIndex & pParent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &pIndex, int pRole = Qt::DisplayRole) const; + + void setCurrentSong(int pCurrentSong, int pCurrentAlbum, QModelIndex pPreviousIndex); + void updateSongs(const QSet &pSongsToUpdate) { mSongsToUpdate = pSongsToUpdate; } + void setManuallyExpanded(const QModelIndex &pIndex, bool pManuallyExpanded); + bool isManuallyExpanded(const QModelIndex &pIndex); + + QSet allSongs() {return mSongSet;} + BBSongListItem findSongItem(QModelIndex pIndex); + QModelIndex indexForFileID(int pFileID); + + QPersistentModelIndex nextLinear(const QPersistentModelIndex &pIndex); + QPersistentModelIndex nextShuffled(const QPersistentModelIndex &pIndex); + QPersistentModelIndex prevLinear(const QPersistentModelIndex &pIndex); + QPersistentModelIndex prevShuffled(const QPersistentModelIndex &pIndex); + QPersistentModelIndex firstLinear(); + QPersistentModelIndex firstShuffled(); + QPersistentModelIndex lastLinear(); + QPersistentModelIndex lastShuffled(); + +public slots: + void setNewSongs(const QSet &pNewSongs, const QSet &pNewAlbums); + +protected: + int findAlbumRow(int pAlbumID) const; + BBSongData &findSongData(const BBIndex &pIndex) + { + return mAlbumList[pIndex.mAlbumPos].mSongs[pIndex.mSongPos]; + } + void shuffle(); + + QSet mSongSet; + QSet mAlbumSet; + QList mAlbumList; + BBIndex mLastShuffleItem, mFirstShuffleItem; + int mCurrentlyPlayingSongID, mCurrentlyPlayingAlbumID; + BBCollectionTab *mCollectionTab; + QSet mSongsToUpdate; +}; + +#endif diff --git a/boombox/src/bbsettings.kcfgc b/boombox/src/bbsettings.kcfgc new file mode 100644 index 00000000..9ac1a153 --- /dev/null +++ b/boombox/src/bbsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=boombox.kcfg +ClassName=BBSettings +Singleton=true +Mutators=true +ItemAccessors=true +SetUserTexts=true diff --git a/boombox/src/bbstreameditform.cpp b/boombox/src/bbstreameditform.cpp new file mode 100644 index 00000000..7cd50bbd --- /dev/null +++ b/boombox/src/bbstreameditform.cpp @@ -0,0 +1,53 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbstreameditform.h" +#include "ui_bbstreameditform.h" + +BBStreamEditForm::BBStreamEditForm(const QString &pName, const QString &pUrl, QWidget *parent) + :QDialog(parent), ui(new Ui::BBStreamEditForm) +{ + ui->setupUi(this); + ui->nameEdit->setText(pName); + ui->urlEdit->setText(pUrl); +} + +BBStreamEditForm::~BBStreamEditForm() +{ + delete ui; +} + +void BBStreamEditForm::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +void BBStreamEditForm::accept() +{ + mName = ui->nameEdit->text(); + mUrl = ui->urlEdit->text(); + QDialog::accept(); +} diff --git a/boombox/src/bbstreameditform.h b/boombox/src/bbstreameditform.h new file mode 100644 index 00000000..d454f341 --- /dev/null +++ b/boombox/src/bbstreameditform.h @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBSTREAMEDITFORM_H +#define BBSTREAMEDITFORM_H + +#include + +namespace Ui { + class BBStreamEditForm; +} + +class BBStreamEditForm : public QDialog +{ +Q_OBJECT + +public: + explicit BBStreamEditForm(const QString &pName, const QString &pUrl, QWidget *parent = 0); + ~BBStreamEditForm(); + + QString mName; + QString mUrl; + +public slots: + virtual void accept(); + +protected: + void changeEvent(QEvent *e); + +private: + Ui::BBStreamEditForm *ui; +}; + +#endif // BBSTREAMEDITFORM_H diff --git a/boombox/src/bbstreameditform.ui b/boombox/src/bbstreameditform.ui new file mode 100644 index 00000000..f0efbd37 --- /dev/null +++ b/boombox/src/bbstreameditform.ui @@ -0,0 +1,95 @@ + + + BBStreamEditForm + + + + 0 + 0 + 182 + 89 + + + + Internet Stream + + + + + + Please provide details for the stream: + + + + + + + + + + + + Name: + + + + + + + + + + URL: + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + BBStreamEditForm + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + BBStreamEditForm + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/boombox/src/bbstreamstab.cpp b/boombox/src/bbstreamstab.cpp new file mode 100644 index 00000000..c4fd8732 --- /dev/null +++ b/boombox/src/bbstreamstab.cpp @@ -0,0 +1,719 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "bbstreamstab.h" +#include "bbsettings.h" +#include "bbstreameditform.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(QPersistentModelIndex) + +class BBStreamItem +{ +public: + explicit BBStreamItem(BBStreamItem *pParent, int pRow, QDomElement &pElement); + ~BBStreamItem(); + + BBStreamItem *mParent; + int mRow; + QDomElement mElement; + QList mChildren; +}; + +BBStreamItem::BBStreamItem(BBStreamItem *pParent, int pRow, QDomElement &pElement) + : mParent(pParent), mRow(pRow), mElement(pElement) +{ + int lCurrentRow = 0; + QDomElement lCurrent = pElement.firstChildElement(); + for(; !lCurrent.isNull(); lCurrent = lCurrent.nextSiblingElement()) + mChildren.append(new BBStreamItem(this, lCurrentRow++, lCurrent)); +} + +BBStreamItem::~BBStreamItem() +{ + foreach(BBStreamItem *lCurrent, mChildren) + delete lCurrent; +} + +BBStreamModel::BBStreamModel(const QString &pFileName, QObject *pParent) + : QAbstractItemModel(pParent), mFileName(pFileName) +{ + QFile lXmlFile(pFileName); + if(!lXmlFile.exists() || !lXmlFile.open(QIODevice::ReadOnly) || !mDocument.setContent(&lXmlFile)) + { + lXmlFile.close(); + if(!lXmlFile.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) + return; + QTextStream lTextStream(&lXmlFile); + lTextStream << "" << endl; + lTextStream << "" << endl; + lTextStream << "" << endl; + lTextStream << "\t" << endl; + lTextStream << "\t" << endl; + lTextStream << "\t" << endl; + lTextStream << "\t" << endl; + lTextStream << "" << endl; + lTextStream << "" << endl; + lXmlFile.seek(0); + if(!mDocument.setContent(&lXmlFile)) + { + qWarning() <<"Giving up getting a valid XML file."; + return; + } + } + QDomElement lDocElement = mDocument.documentElement(); + mRoot = new BBStreamItem(0, 0, lDocElement); +} + +BBStreamModel::~BBStreamModel() +{ + delete mRoot; +} + +QModelIndex BBStreamModel::index(int pRow, int pColumn, const QModelIndex &pParent) const +{ + BBStreamItem *lParentItem; + if(!pParent.isValid()) + lParentItem = mRoot; + else + lParentItem = static_cast(pParent.internalPointer()); + + if(pRow < 0 || pRow >= lParentItem->mChildren.count() || pColumn != 0) + return QModelIndex(); + else + return createIndex(pRow, pColumn, lParentItem->mChildren.at(pRow)); +} + +QModelIndex BBStreamModel::parent(const QModelIndex &pChild) const +{ + if(!pChild.isValid()) + return QModelIndex(); + else + { + BBStreamItem *lChildItem = static_cast(pChild.internalPointer()); + if(lChildItem->mParent == mRoot) + return QModelIndex(); + else + return createIndex(lChildItem->mParent->mRow, 0, lChildItem->mParent); + } +} + +int BBStreamModel::rowCount(const QModelIndex &pParent) const +{ + BBStreamItem *lParentItem; + if(!pParent.isValid()) + lParentItem = mRoot; + else + lParentItem = static_cast(pParent.internalPointer()); + + return lParentItem->mChildren.count(); +} + +int BBStreamModel::columnCount(const QModelIndex &pParent) const +{ + Q_UNUSED(pParent) + return 1; +} + +QVariant BBStreamModel::data(const QModelIndex &pIndex, int pRole) const +{ + if(!pIndex.isValid()) + return QVariant(); + BBStreamItem *lStreamItem = static_cast(pIndex.internalPointer()); + QString lType = lStreamItem->mElement.tagName(); + if(lType == "folder") + { + switch(pRole) + { + case Qt::DisplayRole: + return lStreamItem->mElement.attribute("name"); + case BBIsFolderRole: + return true; + case Qt::FontRole: + if(mCurrentSong.parent() == pIndex) + { + QFont lFont = QApplication::font(); + lFont.setBold(true); + return lFont; + } + else + return QVariant(); + } + } + else if(lType == "entry") + { + switch(pRole) + { + case Qt::DisplayRole: + return lStreamItem->mElement.attribute("name"); + case BBIsFolderRole: + return false; + case BBUrlRole: + return lStreamItem->mElement.attribute("href"); + case Qt::FontRole: + if(mCurrentSong == pIndex) + { + QFont lFont = QApplication::font(); + lFont.setBold(true); + return lFont; + } + else + return QVariant(); + } + } + return QVariant(); +} + +QModelIndex BBStreamModel::createNewEntry(const QModelIndex &pParent, const QString &pName, const QString &pUrl, int pRow) +{ + QDomElement lNewEntry = mDocument.createElement("entry"); + lNewEntry.setAttribute("name", pName); + lNewEntry.setAttribute("href", pUrl); + + BBStreamItem *lParentItem; + QModelIndex lParentIndex = pParent; + if(!lParentIndex.isValid()) + lParentItem = mRoot; + else + { + lParentItem = static_cast(pParent.internalPointer()); + if(lParentItem->mElement.tagName() == "entry") + { + lParentItem = lParentItem->mParent; + lParentIndex = lParentIndex.parent(); + } + } + int lRow = (pRow == -1 ? rowCount(lParentIndex): pRow); + beginInsertRows(lParentIndex, lRow, lRow); + if(pRow == -1) + { + lParentItem->mElement.appendChild(lNewEntry); + lParentItem->mChildren.append(new BBStreamItem(lParentItem, lRow, lNewEntry)); + } + else + { + lParentItem->mElement.insertBefore(lNewEntry, lParentItem->mElement.childNodes().item(lRow)); + lParentItem->mChildren.insert(lRow, new BBStreamItem(lParentItem, lRow, lNewEntry)); + for(int i = lRow + 1; i < lParentItem->mChildren.count(); ++i) + lParentItem->mChildren[i]->mRow++; + } + endInsertRows(); + return index(lRow, 0, lParentIndex); +} + +void BBStreamModel::editEntry(const QModelIndex &pEntry, const QString &pName, const QString &pUrl) +{ + if(!pEntry.isValid()) + return; + BBStreamItem *lItem = static_cast(pEntry.internalPointer()); + if(!pName.isEmpty()) + lItem->mElement.setAttribute("name", pName); + if(!pUrl.isEmpty()) + lItem->mElement.setAttribute("href", pUrl); +} + +void BBStreamModel::deleteEntry(const QModelIndex &pEntry) +{ + if(!pEntry.isValid()) + return; + BBStreamItem *lItem = static_cast(pEntry.internalPointer()); + beginRemoveRows(pEntry.parent(), lItem->mRow, lItem->mRow); + lItem->mParent->mChildren.removeAt(lItem->mRow); + lItem->mParent->mElement.removeChild(lItem->mElement); + for(int i = lItem->mRow; i < lItem->mParent->mChildren.count(); ++i) + lItem->mParent->mChildren[i]->mRow--; + delete lItem; + endRemoveRows(); +} + +QModelIndex BBStreamModel::createNewFolder(const QModelIndex &pParent, const QString &pName, int pRow) +{ + QDomElement lNewEntry = mDocument.createElement("folder"); + lNewEntry.setAttribute("name", pName); + + BBStreamItem *lParentItem; + QModelIndex lParentIndex = pParent; + if(!lParentIndex.isValid()) + lParentItem = mRoot; + else + { + lParentItem = static_cast(pParent.internalPointer()); + if(lParentItem->mElement.tagName() == "entry") + { + lParentItem = lParentItem->mParent; + lParentIndex = lParentIndex.parent(); + } + } + int lRow = (pRow == -1 ? rowCount(lParentIndex) : pRow); + beginInsertRows(lParentIndex, lRow, lRow); + if(pRow == -1) + { + lParentItem->mElement.appendChild(lNewEntry); + lParentItem->mChildren.append(new BBStreamItem(lParentItem, lRow, lNewEntry)); + } + else + { + lParentItem->mElement.insertBefore(lNewEntry, lParentItem->mElement.childNodes().item(lRow)); + lParentItem->mChildren.insert(pRow, new BBStreamItem(lParentItem, lRow, lNewEntry)); + } + endInsertRows(); + return index(lRow, 0, lParentIndex); +} + +void BBStreamModel::saveDocumentToFile() +{ + QFile lXmlFile(mFileName); + if(lXmlFile.open(QIODevice::WriteOnly)) + { + QTextStream lTextStream(&lXmlFile); + mDocument.save(lTextStream, 4); + } +} + +void BBStreamModel::getInfo(const QModelIndex &pModelIndex, QString &pName, QString &pUrl) +{ + if(!pModelIndex.isValid()) + return; + BBStreamItem *lItem = static_cast(pModelIndex.internalPointer()); + + pName = lItem->mElement.attribute("name"); + pUrl = lItem->mElement.attribute("href"); +} + +Qt::ItemFlags BBStreamModel::flags(const QModelIndex &pIndex) const +{ + Qt::ItemFlags lFlags = QAbstractItemModel::flags(pIndex); + if(pIndex.isValid()) + { + lFlags |= Qt::ItemIsDragEnabled; + if(pIndex.data(BBIsFolderRole).toBool()) + lFlags |= Qt::ItemIsDropEnabled; + return lFlags; + } + else + return Qt::ItemIsDropEnabled | lFlags; +} + +Qt::DropActions BBStreamModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +bool BBStreamModel::removeRows(int pRow, int pCount, const QModelIndex &pParent) +{ + BBStreamItem *lParentItem; + if(!pParent.isValid()) + lParentItem = mRoot; + else + lParentItem = static_cast(pParent.internalPointer()); + + beginRemoveRows(pParent, pRow, pRow + pCount - 1); + for(int i = pRow; i < pRow + pCount && i < lParentItem->mChildren.count(); ++i) + { + BBStreamItem *lItem = lParentItem->mChildren.takeAt(i); + lParentItem->mElement.removeChild(lItem->mElement); + delete lItem; + } + for(int i = pRow + pCount - 1; i < lParentItem->mChildren.count(); ++i) + lParentItem->mChildren[i]->mRow -= pCount; + + endRemoveRows(); + saveDocumentToFile(); + return true; +} + +void BBStreamModel::encodeNode(QDataStream &lDataStream, QModelIndex pIndex) const +{ + if(pIndex.data(BBIsFolderRole).toBool()) + { + lDataStream << QString::fromAscii("folder"); + lDataStream << pIndex.data(Qt::DisplayRole).toString(); + qint32 lRowCount = rowCount(pIndex); + lDataStream << lRowCount; + for(qint32 i = 0; i < lRowCount; ++i) + encodeNode(lDataStream, index(i, 0, pIndex)); + } + else + { + lDataStream << QString::fromAscii("entry"); + lDataStream << pIndex.data(Qt::DisplayRole).toString(); + lDataStream << pIndex.data(BBUrlRole).toString(); + } +} + +bool BBStreamModel::decodeNode(QDataStream &lDataStream, QModelIndex pParent, int pRow) +{ + qint32 lRowCount; + QString lType, lUrl, lName; + lDataStream >> lType; + if(lType == "entry") + { + lDataStream >> lName; + lDataStream >> lUrl; + createNewEntry(pParent, lName, lUrl, pRow); + } + else + { + lDataStream >> lName; + QModelIndex i = pParent; + while(i.isValid()) + { + if(i.data().toString() == lName) + return false; + i = i.parent(); + } + QModelIndex lNewFolderIndex = createNewFolder(pParent, lName, pRow); + lDataStream >> lRowCount; + for(qint32 i = 0; i < lRowCount; ++i) + decodeNode(lDataStream, lNewFolderIndex, i); + } + return true; +} + + +QMimeData *BBStreamModel::mimeData(const QModelIndexList &pIndexes) const +{ + QMimeData *lMimeData = new QMimeData(); + QByteArray lByteArray; + QDataStream lDataStream(&lByteArray, QIODevice::WriteOnly); + foreach(QModelIndex lIndex, pIndexes) + encodeNode(lDataStream, lIndex); + lMimeData->setData("application/x-boombox-streamtab", lByteArray); + return lMimeData; +} + +bool BBStreamModel::dropMimeData(const QMimeData *pData, Qt::DropAction pAction, int pRow, int pColumn, const QModelIndex &pParent) +{ + Q_UNUSED(pAction) + Q_UNUSED(pColumn) + if(!pData->hasFormat("application/x-boombox-streamtab")) + return false; + QByteArray lByteArray = pData->data("application/x-boombox-streamtab"); + QDataStream lDataStream(&lByteArray, QIODevice::ReadOnly); + return decodeNode(lDataStream, pParent, pRow); +} + +QStringList BBStreamModel::mimeTypes() const +{ + QStringList lList; + lList << "application/x-boombox-streamtab"; + return lList; +} + +BBStreamsTab::BBStreamsTab(int pTabNumber) + :BBPlaylistSystem(pTabNumber) +{ + setWindowTitle(i18n("Internet Streams")); +} + +void BBStreamsTab::readSession(KConfigGroup &pConfigGroup) +{ + Q_UNUSED(pConfigGroup) + mActionCollection = new KActionCollection(this); + KAction *lNewStreamAction = mActionCollection->addAction("new_stream"); + lNewStreamAction->setText(i18n("New Stream")); + lNewStreamAction->setIcon(KIcon("document-new")); + connect(lNewStreamAction, SIGNAL(triggered()), this, SLOT(createNewStream())); + + KAction *lNewFolderAction = mActionCollection->addAction("new_folder"); + lNewFolderAction->setText(i18n("New Folder")); + lNewFolderAction->setIcon(KIcon("folder-new")); + connect(lNewFolderAction, SIGNAL(triggered()), this, SLOT(createNewFolder())); + + + KAction *lEnqueueAction = new KAction(i18n("Add to Queue"), this); + lEnqueueAction->setShortcut(Qt::CTRL + Qt::Key_E); + lEnqueueAction->setShortcutContext(Qt::WidgetShortcut); + connect(lEnqueueAction, SIGNAL(triggered()), SLOT(enqueueTriggered())); + + KAction *lEditAction = new KAction(i18n("Edit"), this); + lEditAction->setShortcut(Qt::Key_F2); + lEditAction->setShortcutContext(Qt::WidgetShortcut); + connect(lEditAction, SIGNAL(triggered()), SLOT(editTriggered())); + + KAction *lDeleteAction = new KAction(i18n("Delete"), this); + lDeleteAction->setShortcut(Qt::Key_Delete); + lDeleteAction->setShortcutContext(Qt::WidgetShortcut); + connect(lDeleteAction, SIGNAL(triggered()), SLOT(deleteTriggered())); + + mContextMenu = new QMenu(this); + mContextMenu->addAction(lEnqueueAction); + mContextMenu->addAction(lEditAction); + mContextMenu->addAction(lDeleteAction); + + mControlToolBar = toolBar("fs_control_toolbar"); + mControlToolBar->setWindowTitle(i18n("Playback Control Toolbar")); + mControlToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); + mControlsContainer = new QWidget(this); + mControlsLayout = new QHBoxLayout(mControlsContainer); + mControlToolBar->addAction(lNewStreamAction); + mControlToolBar->addAction(lNewFolderAction); + mControlToolBar->addWidget(mControlsContainer); + + mCenterWidget = new QWidget(this); + mTreeView = new QTreeView(this); + mTreeView->setHeaderHidden(true); + mTreeView->setSelectionMode(QAbstractItemView::SingleSelection); + mTreeView->setDragDropMode(QAbstractItemView::InternalMove); + mTreeView->setAcceptDrops(true); + mTreeView->setDragEnabled(true); + mTreeView->setDropIndicatorShown(true); + QVBoxLayout *lCenterLayout = new QVBoxLayout(mCenterWidget); + mModel = new BBStreamModel(BBSettings::fileNameStreams(), this); + mTreeView->setModel(mModel); + mTreeView->expandAll(); + mTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + mTreeView->addAction(lEnqueueAction); + mTreeView->addAction(lEditAction); + mTreeView->addAction(lDeleteAction); + lCenterLayout->addWidget(mTreeView); + setCentralWidget(mCenterWidget); + + connect(mTreeView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showPopupMenu(QPoint))); + connect(mTreeView, SIGNAL(activated(QModelIndex)), this, SLOT(entryClicked(QModelIndex))); +} + +void BBStreamsTab::saveSession(KConfigGroup &pConfigGroup) +{ + Q_UNUSED(pConfigGroup) + mModel->saveDocumentToFile(); +} + +void BBStreamsTab::embedControls(QWidget *pControls) +{ + pControls->setParent(mControlsContainer); + mControlsLayout->addWidget(pControls); + pControls->show(); +} + +void BBStreamsTab::buildSearchStack(bool pSearchForward) +{ + QModelIndex lCurrent = mModel->mCurrentSong; + if(lCurrent.isValid()) + { + if(pSearchForward) + { + do + { + int lStartRow = lCurrent.row() + 1; + lCurrent = lCurrent.parent(); + int lMax = mModel->rowCount(lCurrent); + for(int i = lStartRow; i < lMax; ++i) + mSearchStack.append(mModel->index(i, 0, lCurrent)); + }while(lCurrent.isValid()); + } + else + { + do + { + int lStartRow = lCurrent.row() - 1; + lCurrent = lCurrent.parent(); + for(int i = lStartRow; i >= 0; --i) + mSearchStack.append(mModel->index(i, 0, lCurrent)); + }while(lCurrent.isValid()); + } + } + else + { + if(pSearchForward) + { + int lMax = mModel->rowCount(); + for(int i = 0; i < lMax; ++i) + mSearchStack.append(mModel->index(i, 0)); + } + else + { + for(int i = mModel->rowCount() - 1; i >= 0; --i) + mSearchStack.append(mModel->index(i, 0)); + } + } +} + +void BBStreamsTab::findSongInSearchStack(bool pSearchForward, BBSongQueryJob &pJob) +{ + while(!mSearchStack.isEmpty()) + { + QPersistentModelIndex lCurrent = mSearchStack.takeFirst(); + if(!lCurrent.isValid()) + continue; + if(!lCurrent.data(BBIsFolderRole).toBool()) + { + pJob.mSong = QVariant::fromValue(lCurrent); + emit songQueryReady(pJob); + mSearchStack.clear(); + return; + } + else + { + if(pSearchForward) + { + for(int i = mModel->rowCount(lCurrent) - 1; i >= 0; --i) + mSearchStack.prepend(mModel->index(i, 0, lCurrent)); + } + else + { + for(int i = 0; i < mModel->rowCount(lCurrent); ++i) + mSearchStack.prepend(mModel->index(i, 0, lCurrent)); + } + } + } +} + +void BBStreamsTab::queryNextSong(BBSongQueryJob &pJob) +{ + mSearchStack.clear(); + buildSearchStack(true); + findSongInSearchStack(true, pJob); +} + +void BBStreamsTab::queryPreviousSong(BBSongQueryJob &pJob) +{ + mSearchStack.clear(); + buildSearchStack(false); + findSongInSearchStack(false, pJob); +} + +QVariant BBStreamsTab::currentSong() +{ + return QVariant::fromValue(mModel->mCurrentSong); +} + +void BBStreamsTab::setCurrentSong(const QVariant &pSong) +{ + mTreeView->update(mModel->mCurrentSong); + mTreeView->update(mModel->mCurrentSong.parent()); + mModel->mCurrentSong = pSong.value(); + mTreeView->scrollTo(mModel->mCurrentSong); + mTreeView->update(mModel->mCurrentSong); + mTreeView->update(mModel->mCurrentSong.parent()); +} + +QString BBStreamsTab::displayString(const QVariant &pSong) +{ + return pSong.value().data(Qt::DisplayRole).toString(); +} + +KUrl BBStreamsTab::songUrl(const QVariant &pSong) +{ + QPersistentModelIndex lSong = pSong.value(); + if(!lSong.isValid()) + return KUrl(); + else + return KUrl(lSong.data(BBUrlRole).toString()); +} + +void BBStreamsTab::createNewStream() +{ + QString lName, lUrl; + BBStreamEditForm *lDialog = new BBStreamEditForm(lName, lUrl, this); + if(QDialog::Accepted == lDialog->exec()) + { + mModel->createNewEntry(mTreeView->selectionModel()->currentIndex(), lDialog->mName, lDialog->mUrl); + mModel->saveDocumentToFile(); + } + delete lDialog; +} + +void BBStreamsTab::createNewFolder() +{ + QString lNewName = KInputDialog::getText(i18nc("dialog caption", "New Folder"), + i18n("Please provide a name for the new folder:"), + i18nc("default text", "New Folder")); + if(lNewName.isNull()) + return; + mModel->createNewFolder(mTreeView->selectionModel()->currentIndex(), lNewName); + mModel->saveDocumentToFile(); +} + +void BBStreamsTab::showPopupMenu(const QPoint &pPoint) +{ + QModelIndex lIndex = mTreeView->indexAt(pPoint); + if(lIndex.isValid()) + mContextMenu->popup(mTreeView->mapToGlobal(pPoint)); +} + +void BBStreamsTab::editTriggered() +{ + QString lName, lUrl; + mModel->getInfo(mTreeView->selectionModel()->currentIndex(), lName, lUrl); + if(mTreeView->selectionModel()->currentIndex().data(BBIsFolderRole).toBool()) + { + QString lNewName = KInputDialog::getText(i18nc("dialog caption", "New Folder"), + i18n("Please provide a new name for the folder:"), lName); + if(lNewName.isNull()) + return; + mModel->editEntry(mTreeView->selectionModel()->currentIndex(), lNewName, lUrl); + mModel->saveDocumentToFile(); + } + else + { + BBStreamEditForm *lDialog = new BBStreamEditForm(lName, lUrl, this); + if(QDialog::Accepted == lDialog->exec()) + { + mModel->editEntry(mTreeView->selectionModel()->currentIndex(), lDialog->mName, lDialog->mUrl); + mModel->saveDocumentToFile(); + } + delete lDialog; + } +} + +void BBStreamsTab::deleteTriggered() +{ + mModel->deleteEntry(mTreeView->selectionModel()->currentIndex()); + mModel->saveDocumentToFile(); +} + +void BBStreamsTab::entryClicked(QModelIndex pIndex) +{ + if(pIndex.isValid()) + { + QPersistentModelIndex lIndex = pIndex; + gMainWindow->setCurrentPlaylistSystem(tabNumber()); + gMainWindow->setCurrentSong(QVariant::fromValue(lIndex)); + } +} + +void BBStreamsTab::enqueueTriggered() +{ + QModelIndex lIndex = mTreeView->selectionModel()->currentIndex(); + if(!lIndex.data(BBIsFolderRole).toBool()) + { + gMainWindow->addToPlayQueue(QVariant::fromValue(QPersistentModelIndex(lIndex)), + lIndex.data().toString(), tabNumber()); + } +} + +void BBStreamsTab::addManualUrl(const KUrl &pUrl, const QString &pTitle) +{ //TODO: search for URL match in existing entries, don't add if already there. + QModelIndex lIndex = mModel->createNewEntry(QModelIndex(), pTitle, pUrl.url()); + gMainWindow->addToPlayQueue(QVariant::fromValue(QPersistentModelIndex(lIndex)), pTitle, tabNumber()); +} diff --git a/boombox/src/bbstreamstab.h b/boombox/src/bbstreamstab.h new file mode 100644 index 00000000..21bdb4dc --- /dev/null +++ b/boombox/src/bbstreamstab.h @@ -0,0 +1,113 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef BBSTREAMSTAB_H +#define BBSTREAMSTAB_H + +#include "bbplaylistsystem.h" + +#include +#include + +class QHBoxLayout; +class QTreeView; +class KActionCollection; +class KToolBar; + +class BBStreamItem; + +class BBStreamModel : public QAbstractItemModel +{ +public: + explicit BBStreamModel(const QString &pFileName, QObject *pParent = 0); + virtual ~BBStreamModel(); + + virtual QModelIndex index(int pRow, int pColumn, const QModelIndex &pParent = QModelIndex()) const; + virtual QModelIndex parent(const QModelIndex &pChild) const; + virtual int rowCount(const QModelIndex &pParent = QModelIndex()) const; + virtual int columnCount(const QModelIndex &pParent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &pIndex, int pRole = Qt::DisplayRole) const; + virtual Qt::ItemFlags flags(const QModelIndex &pIndex) const; + virtual Qt::DropActions supportedDropActions() const; + virtual bool removeRows(int pRow, int pCount, const QModelIndex &pParent); + virtual QMimeData *mimeData(const QModelIndexList &pIndexes) const; + virtual bool dropMimeData(const QMimeData *pData, Qt::DropAction pAction, int pRow, int pColumn, const QModelIndex &pParent); + virtual QStringList mimeTypes() const; + + QPersistentModelIndex mCurrentSong; + + QModelIndex createNewEntry(const QModelIndex &pParent, const QString &pName, const QString &pUrl, int pRow = -1); + void editEntry(const QModelIndex &pEntry, const QString &pName, const QString &pUrl); + void deleteEntry(const QModelIndex &pEntry); + QModelIndex createNewFolder(const QModelIndex &pParent, const QString &pName, int pRow = -1); + void saveDocumentToFile(); + void getInfo(const QModelIndex &pModelIndex, QString &pName, QString &pUrl); + +protected: + void encodeNode(QDataStream &lDataStream, QModelIndex pIndex) const; + bool decodeNode(QDataStream &lDataStream, QModelIndex pParent, int pRow); + + QDomDocument mDocument; + QString mFileName; + BBStreamItem *mRoot; +}; + +class BBStreamsTab : public BBPlaylistSystem +{ +Q_OBJECT +public: + explicit BBStreamsTab(int pTabNumber); + virtual void queryNextSong(BBSongQueryJob &pJob); + virtual void queryPreviousSong(BBSongQueryJob &pJob); + virtual QVariant currentSong(); + virtual void setCurrentSong(const QVariant &pSong); + virtual QString displayString(const QVariant &pSong); + virtual KUrl songUrl(const QVariant &pSong); + + void addManualUrl(const KUrl &pUrl, const QString &pTitle); + + virtual void readSession(KConfigGroup &pConfigGroup); + virtual void saveSession(KConfigGroup &pConfigGroup); + virtual void embedControls(QWidget *pControls); + +protected slots: + void createNewStream(); + void createNewFolder(); + void showPopupMenu(const QPoint &pPoint); + void editTriggered(); + void deleteTriggered(); + void entryClicked(QModelIndex pIndex); + void enqueueTriggered(); + +protected: + void buildSearchStack(bool pSearchForward); + void findSongInSearchStack(bool pSearchForward, BBSongQueryJob &pJob); + QList mSearchStack; + + KToolBar *mControlToolBar; + QWidget *mControlsContainer; + QHBoxLayout *mControlsLayout; + QWidget *mCenterWidget; + QTreeView *mTreeView; + BBStreamModel *mModel; + KActionCollection *mActionCollection; + QMenu *mContextMenu; +}; + +#endif // BBSTREAMSTAB_H diff --git a/boombox/src/boombox.desktop b/boombox/src/boombox.desktop new file mode 100644 index 00000000..c83a7476 --- /dev/null +++ b/boombox/src/boombox.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=BoomBox +Name[x-test]=xxBoomBoxxx +Exec=boombox %i -graphicssystem raster -caption "%c" %U +Icon=boombox +Categories=KDE;Qt;AudioVideo;Audio;Player; +GenericName=Music player +GenericName[sv]=Musikspelare +GenericName[en]=Music Player +MimeType=application/x-ogg;audio/basic;audio/vnd.rn-realaudio;audio/x-aiff;audio/x-flac;audio/x-matroska;audio/x-mp3;audio/mpeg;audio/mp4;audio/ogg;audio/x-flac+ogg;audio/x-vorbis+ogg;audio/x-mpegurl;audio/x-ms-wma;audio/x-pn-realaudio;audio/x-scpls;audio/x-wav; +DocPath=boombox/index.html +Terminal=false +X-KDE-StartupNotify=true diff --git a/boombox/src/boombox.kcfg b/boombox/src/boombox.kcfg new file mode 100644 index 00000000..811fb25a --- /dev/null +++ b/boombox/src/boombox.kcfg @@ -0,0 +1,31 @@ + + + + +KStandardDirs +KGlobalSettings +KGlobal + + + + QStringList(KGlobalSettings::musicPath()) + + + + KGlobal::dirs()->saveLocation("appdata") + "music.db" + + + + KGlobal::dirs()->saveLocation("appdata") + "streams.xml" + + + + + + + + + diff --git a/boombox/src/dbqueryjob.cpp b/boombox/src/dbqueryjob.cpp new file mode 100644 index 00000000..011c4353 --- /dev/null +++ b/boombox/src/dbqueryjob.cpp @@ -0,0 +1,179 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "dbqueryjob.h" +#include "bbdatabase.h" +#include "bbsettings.h" + +#include + +DBPool::DBPool(int pNumConnections) + : mNumConnections(pNumConnections) +{ + for(int i = 0; i < mNumConnections; ++i) + mAvailable.push(new BBDatabase()); +} + +DBPool::~DBPool() +{ + foreach(BBDatabase *i, mAvailable) + { + delete i; + } +} + +bool DBPool::canRun(ThreadWeaver::Job *pJob) +{ + if(mAvailable.isEmpty()) + return false; + else + { + DBQueryJob * lJob = qobject_cast(pJob); + if(lJob == NULL) + return false; + if(lJob->mDatabase != NULL) + return true; + lJob->mDatabase = mAvailable.pop(); + if(!lJob->mDatabase->isConnected()) + { + if(!lJob->mDatabase->connect(BBSettings::self()->fileNameDB())) + return false; + } + return true; + } +} + +void DBPool::free(ThreadWeaver::Job *pJob) +{ + DBQueryJob * lJob = qobject_cast(pJob); + if(lJob == NULL) + return; + if(lJob->mDatabase != NULL) + mAvailable.push(lJob->mDatabase); + lJob->mDatabase = NULL; +} + +void FilterQueryJob::run() +{ + BBResultTable lDBResult; + QSet lResult; + + if(mDatabase->getTable(QString("SELECT DISTINCT %1 FROM songs JOIN albums USING (album_ID) %2 ORDER BY %1 ASC" + ).arg(mCategory, mCondition.isEmpty() ? mCondition: QString("WHERE %1").arg(mCondition)), lDBResult)) + { + for(int i = 1; i <= lDBResult.mNumRows; ++i)//start with 1 since first row is column names + { + if(mShouldAbort) + { + BBDatabase::freeTable(lDBResult); + return; + } + lResult.insert(BBStringListItem(QString::fromUtf8(lDBResult.at(i, 0)), i-1)); + } + BBDatabase::freeTable(lDBResult); + } + + if(!mShouldAbort) + emit resultReady(lResult); +} + +void SongQueryJob::getSongs(bool pVA) +{ + BBResultTable lDBResult; + QString lQuery; + if(pVA) + { + lQuery = QString("SELECT title, artist, length, file_name, file_ID, album_ID, album, " + "path, cover_art_path FROM songs JOIN albums USING (album_ID) WHERE is_VA != 0 %1 " + "ORDER BY album, path, track, file_name" + ).arg(mCondition.isEmpty() ? mCondition : QString("AND %1").arg(mCondition)); + } + else + { + lQuery = QString("SELECT title, artist, length, file_name, file_ID, album_ID, album, " + "path, cover_art_path FROM songs JOIN albums USING (album_ID) WHERE is_VA = 0 %1 " + "ORDER BY artist, album, path, track, file_name" + ).arg(mCondition.isEmpty() ? mCondition : QString("AND %1").arg(mCondition)); + } + if(mDatabase->getTable(lQuery, lDBResult)) + { + int lCurrentAlbumId = -1; //for use in first run of loop + int lSongPosition = 0; + QString lTemp; + BBAlbumListItem lAlbum; + BBSongListItem lSong; + lAlbum.mPosition = -1; //for use in first run of loop + for(int i = 1; i <= lDBResult.mNumRows; ++i)//start with 1 since first row is column names + { + if(mShouldAbort) + { + BBDatabase::freeTable(lDBResult); + return; + } + lSong.mData.mTitle = QString::fromUtf8(lDBResult.at(i,0)); + if(pVA) + lSong.mData.mArtist = QString::fromUtf8(lDBResult.at(i,1)); + lSong.mData.mLength = QString::fromUtf8(lDBResult.at(i,2)); + lSong.mData.mFileName = QString::fromUtf8(lDBResult.at(i,3)); + lTemp = QString::fromUtf8(lDBResult.at(i,4)); + lSong.mData.mFileID = lTemp.toInt(); + lTemp = QString::fromUtf8(lDBResult.at(i,5)); + lSong.mData.mAlbumID = lTemp.toInt(); + + if(lCurrentAlbumId != lSong.mData.mAlbumID) + { + if(lAlbum.mPosition >= 0) + { + mAlbumSet.insert(lAlbum); + lSongPosition = 0; + } + lAlbum.mData.mAlbum = QString::fromUtf8(lDBResult.at(i,6)); + lAlbum.mData.mFolderPath = QString::fromUtf8(lDBResult.at(i,7)); + lAlbum.mData.mCoverArtPath= QString::fromUtf8(lDBResult.at(i,8)); + lAlbum.mData.mIsVA = pVA; + lAlbum.mData.mArtist = pVA ? i18n("Various Artists") : QString::fromUtf8(lDBResult.at(i,1)); + lAlbum.mPosition = mAlbumSet.count(); + lAlbum.mData.mAlbumID = lSong.mData.mAlbumID; + lCurrentAlbumId = lSong.mData.mAlbumID; + lAlbum.mData.mManuallyExpanded = false; //initially all albums are not expanded + } + + lSong.mIndex.mSongPos = lSongPosition++; + lSong.mIndex.mAlbumPos = mAlbumSet.count(); + lSong.mPosition = mSongSet.count(); + mSongSet.insert(lSong); + } + if(lAlbum.mPosition >= 0) + mAlbumSet.insert(lAlbum); + + BBDatabase::freeTable(lDBResult); + } +} + +void SongQueryJob::run() +{ + getSongs(false); + if(mShouldAbort) + return; + getSongs(true); + if(!mShouldAbort) + emit resultReady(mSongSet, mAlbumSet); +} + +#include "dbqueryjob.moc" diff --git a/boombox/src/dbqueryjob.h b/boombox/src/dbqueryjob.h new file mode 100644 index 00000000..554f2e96 --- /dev/null +++ b/boombox/src/dbqueryjob.h @@ -0,0 +1,114 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef DBQUERYJOB_H +#define DBQUERYJOB_H + +#include +#include +#include +#include +#include +#include + +#include "bblistitem.h" + +class BBDatabase; + +class DBPool: public ThreadWeaver::QueuePolicy +{ + public: + DBPool(int pNumConnections); + virtual ~DBPool(); + + virtual bool canRun(ThreadWeaver::Job *pJob); + virtual void free(ThreadWeaver::Job *pJob); + virtual void release(ThreadWeaver::Job *pJob) + { + free(pJob); + } + virtual void destructed(ThreadWeaver::Job *pJob) + { + free(pJob); + } + + protected: + int mNumConnections; + QStack mAvailable; +}; + +class DBQueryJob: public ThreadWeaver::Job +{ + Q_OBJECT + public: + DBQueryJob() + : mDatabase(NULL) + {} + + protected: + BBDatabase *mDatabase; + + friend class DBPool; +}; + +class FilterQueryJob: public DBQueryJob +{ + Q_OBJECT + public: + FilterQueryJob(const QString &pCategory, const QString &pCondition) + : mCategory(pCategory), mCondition(pCondition), mShouldAbort(false) + {} + + virtual void requestAbort() {mShouldAbort = true;} + + signals: + void resultReady(QSet); + + protected: + virtual void run(); + + QString mCategory, mCondition; + bool mShouldAbort; +}; + +class SongQueryJob: public DBQueryJob +{ + Q_OBJECT + public: + SongQueryJob(const QString &pCondition) + : mCondition(pCondition), mShouldAbort(false) + {} + + virtual void requestAbort() {mShouldAbort = true;} + + signals: + void resultReady(QSet, QSet); + + protected: + virtual void run(); + void getSongs(bool pVA); + + QString mCategory, mCondition; + bool mShouldAbort; + QSet mSongSet; + QSet mAlbumSet; + +}; + +#endif diff --git a/boombox/src/dbupdatejob.cpp b/boombox/src/dbupdatejob.cpp new file mode 100644 index 00000000..5500ae6c --- /dev/null +++ b/boombox/src/dbupdatejob.cpp @@ -0,0 +1,201 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "dbupdatejob.h" +#include "bbdatabase.h" +#include "bbmetadata.h" +#include "bbsettings.h" + +#include +#include +#include +#include +#include + +DBUpdateJob::DBUpdateJob() +{ + mFileTypes << "*.mp3" << "*.MP3" <<"*.ogg" <<"*.OGG" <<"*.flac" <<"*.FLAC" <<"*.mpc" << "*.MPC"; + setCapabilities(Killable | Suspendable); +} + +void DBUpdateJob::aboutToDie() +{ + mDatabase.executeStatement("ROLLBACK"); +} + +void DBUpdateJob::doWork() +{ + QMap lDatabaseSet, lFileSystemSet; + + emit infoMessage(this, i18n("Reading list of files from database")); + + mDatabase.connect(BBSettings::fileNameDB()); + + BBResultTable lResult; + mDatabase.getTable("SELECT path || '/' || file_name, file_ID FROM songs JOIN albums USING (album_ID)", lResult); + + for(int i = 1; i <= lResult.mNumRows; ++i) + { + if(checkForDeathOrSuspend()) + return; + + lDatabaseSet.insert(QString::fromUtf8(lResult.at(i,0)), QString::fromUtf8(lResult.at(i,1)).toInt()); + } + + mDatabase.freeTable(lResult); + + emit infoMessage(this, i18n("Scanning directories for music files")); + + QStringList lPathList = BBSettings::musicDirectories(); + QString lPath; + foreach(lPath, lPathList) + { + if(!addPathToSet(lPath, lFileSystemSet)) + return; + } + + mDatabase.executeStatement("BEGIN"); + QMapIterator i(lDatabaseSet); + while(i.hasNext()) + { + if(checkForDeathOrSuspend()) + return; + i.next(); + if(!lFileSystemSet.contains(i.key()) && !i.key().contains("://")) + { + emit description(this, i18n("Removing old entries"), qMakePair(i18n("Path"), i.key())); + + mDatabase.executeStatement(QString("DELETE FROM songs WHERE file_ID = %1").arg(i.value())); + } + } + + mDatabase.executeStatement("COMMIT"); + mDatabase.executeStatement("BEGIN"); + setTotalAmount(Files, lFileSystemSet.count()); + + BBMetaData lMetaData; + int lInsertCount = 0, lFileCount=0; + i = lFileSystemSet; + while(i.hasNext()) + { + if(checkForDeathOrSuspend()) + return; + i.next(); + QFileInfo lFileInfo(i.key()); + bool lSongInDatabase = lDatabaseSet.contains(i.key()); + + if(lFileInfo.isFile() && + (!lSongInDatabase || lFileInfo.lastModified() > BBSettings::databaseUpdateTime())) + { + if(lSongInDatabase) + mDatabase.executeStatement(QString("DELETE FROM songs WHERE file_ID = %1").arg(lDatabaseSet.value(i.key()))); + lMetaData.GetInfoFrom(lFileInfo.absolutePath(), lFileInfo.fileName()); + emit description(this, i18n("Adding new entries"), + qMakePair(i18n("Path"), lFileInfo.absoluteFilePath()), + qMakePair(i18n("Info"), QString("%1 - %2").arg(lMetaData.mArtist, lMetaData.mTitle))); + addSongToDatabase(lMetaData); + lInsertCount++; + } + + if(lInsertCount > 99) //commit to db every hundred inserts. + { + mDatabase.executeStatement("COMMIT"); + mDatabase.executeStatement("BEGIN"); + lInsertCount = 0; + } + lFileCount++; + setProcessedAmount(Files, lFileCount); + setPercent((ulong)(100*processedAmount(Files))/totalAmount(Files)); //BUG in KJob?? + } + + mDatabase.executeStatement("COMMIT"); + mDatabase.executeStatement("DELETE FROM dead_album_view"); + + mDatabase.executeStatement("UPDATE albums SET is_VA = 1 WHERE album_ID IN " + "(SELECT album_ID FROM songs JOIN albums USING (album_ID) " + "GROUP BY album_ID HAVING count(DISTINCT artist) > 1)"); + mDatabase.executeStatement("UPDATE albums SET is_VA = 0 WHERE album_ID IN " + "(SELECT album_ID FROM songs JOIN albums USING (album_ID) " + "GROUP BY album_ID HAVING count(DISTINCT artist) = 1)"); + emitResult(); +} + +void DBUpdateJob::addSongToDatabase(BBMetaData &pMetaData) +{ + int lAlbumID; + BBResultTable lResult; + mDatabase.getTable(QString("SELECT album_ID FROM albums WHERE path = '%1' AND album = '%2'" + ).arg(BBDatabase::prepareString(pMetaData.mPath), + BBDatabase::prepareString(pMetaData.mAlbum)), lResult); + if(lResult.mNumRows == 0) + { + mDatabase.executeStatement(QString("INSERT INTO albums (path, album, is_VA, cover_art_path) VALUES ('%1','%2','%3','%4')" + ).arg(BBDatabase::prepareString(pMetaData.mPath), + BBDatabase::prepareString(pMetaData.mAlbum), + QString::number(0), QString())); + lAlbumID = sqlite3_last_insert_rowid(mDatabase.getDatabase()); + } + else + { + lAlbumID = QString::fromUtf8(lResult.at(1, 0)).toInt(); + } + mDatabase.freeTable(lResult); + + mDatabase.executeStatement(QString("INSERT INTO songs (album_ID, file_name, artist, title, track, year, genre, " + "comment, length) VALUES ('%1', '%2', '%3', '%4', '%5', '%6', '%7', '%8', '%9')" + ).arg(lAlbumID).arg(BBDatabase::prepareString(pMetaData.mFileName), + BBDatabase::prepareString(pMetaData.mArtist), + BBDatabase::prepareString(pMetaData.mTitle), + BBDatabase::prepareString(pMetaData.mTrack), + BBDatabase::prepareString(pMetaData.mYear), + BBDatabase::prepareString(pMetaData.mGenre), + BBDatabase::prepareString(pMetaData.mComment), + BBDatabase::prepareString(pMetaData.mLength))); + + pMetaData.mFileID = sqlite3_last_insert_rowid(mDatabase.getDatabase()); +} + +bool DBUpdateJob::addPathToSet(const QString &pPath, QMap &pSet) +{ + QDir lDir(pPath); + if(!lDir.isReadable()) + return true; + + if(checkForDeathOrSuspend()) + return false; + + QFileInfoList lInfoList = lDir.entryInfoList(QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot); + QFileInfo lFileInfo; + foreach(lFileInfo, lInfoList) + { + if(!addPathToSet(lFileInfo.absoluteFilePath(), pSet)) + return false; + } + lInfoList = lDir.entryInfoList(mFileTypes, QDir::Files | QDir::Readable); + foreach(lFileInfo, lInfoList) + { + if(checkForDeathOrSuspend()) + return false; + + pSet.insert(lFileInfo.absoluteFilePath(), 0); + } + return true; +} + +#include "dbupdatejob.moc" diff --git a/boombox/src/dbupdatejob.h b/boombox/src/dbupdatejob.h new file mode 100644 index 00000000..d80d1fa4 --- /dev/null +++ b/boombox/src/dbupdatejob.h @@ -0,0 +1,48 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef DBUPDATEJOB_H +#define DBUPDATEJOB_H + +#include "threadjob.h" +#include "bbdatabase.h" + +#include + +class DBUpdateJob: public ThreadJob +{ + Q_OBJECT + public: + DBUpdateJob(); + + public slots: + void doWork(); + + protected: + void aboutToDie(); + + private: + bool addPathToSet(const QString &pPath, QMap &pSet); + void addSongToDatabase(BBMetaData &pMetaData); + + BBDatabase mDatabase; + QStringList mFileTypes; +}; + +#endif diff --git a/boombox/src/dbus/org.mpris.MediaPlayer2.Player.xml b/boombox/src/dbus/org.mpris.MediaPlayer2.Player.xml new file mode 100644 index 00000000..3c11567f --- /dev/null +++ b/boombox/src/dbus/org.mpris.MediaPlayer2.Player.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/boombox/src/dbus/org.mpris.MediaPlayer2.TrackList.xml b/boombox/src/dbus/org.mpris.MediaPlayer2.TrackList.xml new file mode 100644 index 00000000..51b4bbc6 --- /dev/null +++ b/boombox/src/dbus/org.mpris.MediaPlayer2.TrackList.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/boombox/src/dbus/org.mpris.MediaPlayer2.xml b/boombox/src/dbus/org.mpris.MediaPlayer2.xml new file mode 100644 index 00000000..74208a73 --- /dev/null +++ b/boombox/src/dbus/org.mpris.MediaPlayer2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/boombox/src/directoriespage.ui b/boombox/src/directoriespage.ui new file mode 100644 index 00000000..4476199d --- /dev/null +++ b/boombox/src/directoriespage.ui @@ -0,0 +1,48 @@ + + + DirectoriesPage + + + + 0 + 0 + 412 + 425 + + + + Form + + + + + + + + KEditListWidget::Add|KEditListWidget::Remove + + + + + + + + + + + + + + KEditListWidget + QWidget +
keditlistwidget.h
+
+ + KLineEdit + QLineEdit +
klineedit.h
+
+
+ + +
diff --git a/boombox/src/main.cpp b/boombox/src/main.cpp new file mode 100644 index 00000000..41681e09 --- /dev/null +++ b/boombox/src/main.cpp @@ -0,0 +1,88 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "bbmainwindow.h" +#include "mpris2playerclient.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char description[] = I18N_NOOP("Music player with dynamic playlists."); + +static const char version[] = "0.4"; + +BBMainWindow *gMainWindow; + +extern "C" int main(int argc, char **argv) +{ + KAboutData lAbout("boombox", 0, ki18n("BoomBox"), version, ki18n(description), + KAboutData::License_GPL, ki18n("(C) 2009 Simon Persson"), + KLocalizedString(), 0, "simonop@spray.se"); + lAbout.addAuthor( ki18n("Simon Persson"), KLocalizedString(), "simonop@spray.se" ); + KCmdLineArgs::init(argc, argv, &lAbout); + + KCmdLineOptions lOptions; + lOptions.add("+[URL]", ki18n("File or URL to add to playlist")); + KCmdLineArgs::addCmdLineOptions(lOptions); + KApplication lApp; + + QDBusConnection lConnection = QDBusConnection::sessionBus(); + + QDBusConnectionInterface *lSessionInterface = lConnection.interface(); + QDBusReply lIsRegistered = lSessionInterface->isServiceRegistered(QLatin1String("org.mpris.MediaPlayer2.BoomBox")); + if(!lIsRegistered.value()) + { + lApp.setAttribute(Qt::AA_DontUseNativeMenuBar); + gMainWindow = new BBMainWindow(&lAbout); + gMainWindow->readSession(); + gMainWindow->show(); + } + + QDBusPendingReply<> lReply; + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + if (args->count() > 0) + { + org::mpris::MediaPlayer2::Player *lInterface; + lInterface = new org::mpris::MediaPlayer2::Player(QLatin1String("org.mpris.MediaPlayer2.BoomBox"), + QLatin1String("/org/mpris/MediaPlayer2"), lConnection); + for(int i = 0; i < args->count(); ++i) + lReply = lInterface->OpenUri(args->arg(i)); + } + + if(lIsRegistered.value()) + { + KStartupInfo::appStarted(); //make startup notification go away. + if(args->count() > 0) + lReply.waitForFinished(); + return 0; //one instance already running in this session. + } + else + { + if(args->count() > 0) + gMainWindow->togglePlayback(); + return lApp.exec(); + } + +} diff --git a/boombox/src/mpris2player.cpp b/boombox/src/mpris2player.cpp new file mode 100644 index 00000000..1e20877f --- /dev/null +++ b/boombox/src/mpris2player.cpp @@ -0,0 +1,465 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "mpris2player.h" + +#include "bbmainwindow.h" +#include "bbstreamstab.h" +#include "bbfilesystemtab.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Mpris2Player::Mpris2Player(QObject *pParent) + : QObject(pParent) +{ +} + +bool Mpris2Player::canQuit() const +{ + return true; +} + +bool Mpris2Player::canRaise() const +{ + return true; +} + +QString Mpris2Player::desktopEntry() const +{ + return QString::fromAscii("boombox"); +} + +bool Mpris2Player::hasTrackList() const +{ + return false; +} + +QString Mpris2Player::identity() const +{ + return QString::fromAscii("BoomBox"); +} + +QStringList Mpris2Player::supportedMimeTypes() const +{ + QString lList = QString::fromAscii("application/x-ogg;audio/basic;audio/vnd.rn-realaudio;" + "audio/x-aiff;audio/x-flac;audio/x-matroska;audio/x-mp3;" + "audio/mpeg;audio/ogg;audio/x-flac+ogg;audio/x-vorbis+ogg;" + "audio/x-mpegurl;audio/x-ms-wma;audio/x-pn-realaudio;" + "audio/x-scpls;audio/x-wav"); + return lList.split(';'); +} + +QStringList Mpris2Player::supportedUriSchemes() const +{ + QStringList lList; + lList << "http://" << "file://"; + return lList; +} + +void Mpris2Player::Quit() +{ + gMainWindow->close(); +} + +void Mpris2Player::Raise() +{ + gMainWindow->activateWindow(); + gMainWindow->raise(); +} + + + + + +bool Mpris2Player::canControl() const +{ + return true; +} + +bool Mpris2Player::canGoNext() const +{ + return true; +} + +bool Mpris2Player::canGoPrevious() const +{ + return true; +} + +bool Mpris2Player::canPause() const +{ + return true; +} + +bool Mpris2Player::canPlay() const +{ + return true; +} + +bool Mpris2Player::canSeek() const +{ + return gMainWindow->mMediaObject->isSeekable(); +} + +QString Mpris2Player::loopStatus() const +{ + return QString::fromAscii("Playlist"); +} + +void Mpris2Player::setLoopStatus(const QString &value) +{ + Q_UNUSED(value) +} + +double Mpris2Player::maximumRate() const +{ + return 1.0; +} + +QVariantMap Mpris2Player::metadata() const +{ + QMultiMap lMap = gMainWindow->mMediaObject->metaData(); + QVariantMap lMap2; + QString lUrl = gMainWindow->mMediaObject->currentSource().url().toString(); + if(lUrl.isEmpty()) { + lUrl = "org/mpris/MediaPlayer2/TrackList/NoTrack"; + } else { + lUrl.prepend("boombox/"); + } + lMap2.insert("mpris:trackid", lUrl); + lMap2.insert("mpris:length", gMainWindow->mMediaObject->totalTime() * 1000); + lMap2.insert("xesam:artist", QStringList(lMap.value("ARTIST"))); + lMap2.insert("xesam:album", lMap.value("ALBUM")); + lMap2.insert("xesam:title", lMap.value("TITLE")); + lMap2.insert("xesam:genre", QStringList(lMap.value("GENRE"))); + return lMap2; +} + +double Mpris2Player::minimumRate() const +{ + return 1.0; +} + +QString Mpris2Player::playbackStatus() const +{ + Phonon::State lState = gMainWindow->mMediaObject->state(); + if(lState == Phonon::PlayingState || lState == Phonon::BufferingState) + return "Playing"; + else if(lState == Phonon::PausedState) + return "Paused"; + else + return "Stopped"; +} + +qlonglong Mpris2Player::position() const +{ + return gMainWindow->mMediaObject->currentTime() * 1000; // return position in microseconds +} + +double Mpris2Player::rate() const +{ + return 1.0; +} + +void Mpris2Player::setRate(double value) +{ + Q_UNUSED(value) +} + +bool Mpris2Player::shuffle() const +{ + return gMainWindow->shuffleActive(); +} + +void Mpris2Player::setShuffle(bool value) +{ + gMainWindow->setShuffle(value); +} + +double Mpris2Player::volume() const +{ + return gMainWindow->mAudioOutput->volume(); +} + +void Mpris2Player::setVolume(double value) +{ + gMainWindow->mAudioOutput->setVolume(value); +} + +void Mpris2Player::Next() +{ + gMainWindow->jumpToNextSong(); +} + +void Mpris2Player::OpenUri(const QString &Uri) +{ + KUrl lUrl(Uri); + KMimeType::Ptr lMimeType = KMimeType::findByUrl(lUrl); + if(lMimeType->is("application/octet-stream")) + { + lMimeType = KMimeType::mimeType(KIO::NetAccess::mimetype(lUrl, gMainWindow)); + } + if(lMimeType.isNull()) + return; + + if(lMimeType->is("audio/x-mpegurl") || lMimeType->is("audio/x-scpls")) + { + QString lTmpFile; + if(KIO::NetAccess::download(lUrl, lTmpFile, gMainWindow)) + { + if(lMimeType->is("audio/x-mpegurl")) + openM3U(lTmpFile); + else if(lMimeType->is("audio/x-scpls")) + openPLS(lTmpFile); + KIO::NetAccess::removeTempFile(lTmpFile); + } + else + KMessageBox::error(gMainWindow, KIO::NetAccess::lastErrorString()); + return; + } + + if(lUrl.protocol() == "http") + gMainWindow->mStreamsTab->addManualUrl(lUrl, lUrl.prettyUrl()); + else + gMainWindow->mFileSystemTab->addManualUrl(lUrl); +} + +void Mpris2Player::Pause() +{ + Phonon::State lState = gMainWindow->mMediaObject->state(); + if(lState == Phonon::PlayingState || lState == Phonon::BufferingState) + gMainWindow->togglePlayback(); +} + +void Mpris2Player::Play() +{ + Phonon::State lState = gMainWindow->mMediaObject->state(); + if(lState != Phonon::PlayingState && lState != Phonon::BufferingState) + gMainWindow->togglePlayback(); +} + +void Mpris2Player::PlayPause() +{ + gMainWindow->togglePlayback(); +} + +void Mpris2Player::Previous() +{ + gMainWindow->jumpToPreviousSong(); +} + +void Mpris2Player::Seek(qlonglong Offset) +{ + gMainWindow->mMediaObject->seek(gMainWindow->mMediaObject->currentTime() + Offset * 1000); +} + +void Mpris2Player::SetPosition(const QDBusObjectPath &TrackId, qlonglong Position) +{ + if(!canSeek() || TrackId == QDBusObjectPath("org/mpris/MediaPlayer2/TrackList/NoTrack") || + Position < 0 || Position > gMainWindow->mMediaObject->totalTime() * 1000) { + return; + } + gMainWindow->mMediaObject->seek(Position/1000); +} + +void Mpris2Player::Stop() +{ + Phonon::State lState = gMainWindow->mMediaObject->state(); + if(lState == Phonon::PlayingState || lState == Phonon::BufferingState) + gMainWindow->togglePlayback(); + gMainWindow->mMediaObject->seek(0); +} + +void Mpris2Player::openM3U(const QString &pPath) +{ + QFile lFile(pPath); + QString lReadPath; + QFileInfo lInfo(pPath); + + QString lCurrentPath = lInfo.dir().absolutePath(); + if(!lFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return; + QTextStream lTextStream(&lFile); + + while(!lTextStream.atEnd()) + { + lReadPath = lTextStream.readLine(4096).trimmed(); + if(!lReadPath.startsWith('#')) + { + KUrl lUrl(lReadPath); + if(lUrl.protocol() == "http") + gMainWindow->mStreamsTab->addManualUrl(lUrl, lUrl.prettyUrl()); + else + { + if(lUrl.isRelative()) + lUrl = KUrl(lCurrentPath, lReadPath); + gMainWindow->mFileSystemTab->addManualUrl(lUrl); + } + } + } +} + +void Mpris2Player::notifySeeked(qlonglong pTimeInUs) { + emit Seeked(pTimeInUs); +} + +void Mpris2Player::openPLS(const QString &pPath) +{ + QFile file(pPath); + if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return; + + QTextStream stream(&file); + + // Better Handling of pls playlists - Taken from amaroK - amarok.kde.org + // Counted number of "File#=" lines. + uint entryCnt = 0; + // Value of the "NumberOfEntries=#" line. + uint numberOfEntries = 0; + bool havePlaylistSection = false; + QString tmp; + QStringList lines; + + const QRegExp regExp_NumberOfEntries("^NumberOfEntries\\s*=\\s*\\d+$", Qt::CaseInsensitive); + const QRegExp regExp_File("^File\\d+\\s*=", Qt::CaseInsensitive); + const QRegExp regExp_Title("^Title\\d+\\s*=", Qt::CaseInsensitive); + const QRegExp regExp_Length("^Length\\d+\\s*=\\s*-?\\d+$", Qt::CaseInsensitive); + const QRegExp regExp_Version("^Version\\s*=\\s*\\d+$", Qt::CaseInsensitive); + const QString section_playlist("[playlist]"); + + /* Preprocess the input data. + * Read the lines into a buffer; Cleanup the line strings; + * Count the entries manually and read "NumberOfEntries". + */ + while (!stream.atEnd()) { + tmp = stream.readLine(4096).simplified(); + if (tmp.isEmpty()) + continue; + lines.append(tmp); + + if (tmp == section_playlist) { + havePlaylistSection = true; + continue; + } + + if (tmp.contains(regExp_File)) { + entryCnt++; + continue; + } + + if (tmp.contains(regExp_NumberOfEntries)) { + numberOfEntries = tmp.section('=', -1).simplified().toUInt(); + continue; + } + } + if (numberOfEntries != entryCnt) + numberOfEntries = entryCnt; + + if(numberOfEntries == 0) + return; + + uint index; + bool inPlaylistSection = false; + QString* files = new QString[entryCnt]; + QString *titles = new QString[entryCnt]; + + QStringList::const_iterator i = lines.begin(), end = lines.end(); + for( ; i != end; ++i) + { + if (!inPlaylistSection && havePlaylistSection) { + /* The playlist begins with the "[playlist]" tag. + * Skip everything before this. + */ + if ((*i) == section_playlist) + inPlaylistSection = true; + continue; + } + if ((*i).contains(regExp_File)) { + // Have a "File#=XYZ" line. + index = extractIndex(*i); + if (index <= numberOfEntries && index != 0) + files[index-1] = (*i).section('=', 1).trimmed(); + } + if ((*i).contains(regExp_Title)) { + // Have a "Title#=XYZ" line. + index = extractIndex(*i); + if (index <= numberOfEntries && index != 0) + titles[index-1] = (*i).section('=', 1).trimmed(); + } + } + + QFileInfo lInfo(pPath); + QString lCurrentPath = lInfo.dir().absolutePath(); + for (uint i=0; imStreamsTab->addManualUrl(lUrl, titles[i]); + else + { + if(lUrl.isRelative()) + lUrl = KUrl(lCurrentPath, files[i]); + gMainWindow->mFileSystemTab->addManualUrl(lUrl); + } + } + delete[] files; + delete[] titles; +} + +uint Mpris2Player::extractIndex(const QString &pStr) +{ + /* Extract the index number out of a .pls line. + * Example: + * extractIndex("File2=foobar") == 2 + */ + bool ok = false; + unsigned int ret; + QString tmp(pStr.section('=', 0, 0)); + tmp.remove(QRegExp("^\\D*")); + ret = tmp.simplified().toUInt(&ok); + if (!ok) + { + qWarning("error extracting index, corrupt pls file."); + ret = 0; + } + return ret; +} + +void Mpris2Player::notifyChangedProperty(const QLatin1String &pPropertyName) { + QDBusMessage signal = QDBusMessage::createSignal("/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged"); + signal << QString("org.mpris.MediaPlayer2.Player"); + QVariantMap lChanged; + lChanged.insert(pPropertyName, property(pPropertyName.latin1())); + signal << lChanged; + signal << QStringList(); + QDBusConnection::sessionBus().send(signal); +} diff --git a/boombox/src/mpris2player.h b/boombox/src/mpris2player.h new file mode 100644 index 00000000..64388c13 --- /dev/null +++ b/boombox/src/mpris2player.h @@ -0,0 +1,135 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef MPRIS2PLAYER_H +#define MPRIS2PLAYER_H +#include +#include +#include + +class Mpris2Player: public QObject +{ + Q_OBJECT + +public: + Mpris2Player(QObject *pParent); + +public: // Properties for MediaPlayer2 interface. + Q_PROPERTY(bool CanQuit READ canQuit) + bool canQuit() const; + + Q_PROPERTY(bool CanRaise READ canRaise) + bool canRaise() const; + + Q_PROPERTY(QString DesktopEntry READ desktopEntry) + QString desktopEntry() const; + + Q_PROPERTY(bool HasTrackList READ hasTrackList) + bool hasTrackList() const; + + Q_PROPERTY(QString Identity READ identity) + QString identity() const; + + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes) + QStringList supportedMimeTypes() const; + + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes) + QStringList supportedUriSchemes() const; + +public: // Properties for MediaPlayer2.Player interface. + Q_PROPERTY(bool CanControl READ canControl) + bool canControl() const; + + Q_PROPERTY(bool CanGoNext READ canGoNext) + bool canGoNext() const; + + Q_PROPERTY(bool CanGoPrevious READ canGoPrevious) + bool canGoPrevious() const; + + Q_PROPERTY(bool CanPause READ canPause) + bool canPause() const; + + Q_PROPERTY(bool CanPlay READ canPlay) + bool canPlay() const; + + Q_PROPERTY(bool CanSeek READ canSeek) + bool canSeek() const; + + Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus) + QString loopStatus() const; + void setLoopStatus(const QString &value); + + Q_PROPERTY(double MaximumRate READ maximumRate) + double maximumRate() const; + + Q_PROPERTY(QVariantMap Metadata READ metadata) + QVariantMap metadata() const; + + Q_PROPERTY(double MinimumRate READ minimumRate) + double minimumRate() const; + + Q_PROPERTY(QString PlaybackStatus READ playbackStatus) + QString playbackStatus() const; + + Q_PROPERTY(qlonglong Position READ position) + qlonglong position() const; + + Q_PROPERTY(double Rate READ rate WRITE setRate) + double rate() const; + void setRate(double value); + + Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle) + bool shuffle() const; + void setShuffle(bool value); + + Q_PROPERTY(double Volume READ volume WRITE setVolume) + double volume() const; + void setVolume(double value); + +public slots: //Slots for MediaPlayer2 interface + void Quit(); + void Raise(); + +public Q_SLOTS: // Slots for MediaPlayer2.Player interface. + void Next(); + void OpenUri(const QString &Uri); + void Pause(); + void Play(); + void PlayPause(); + void Previous(); + void Seek(qlonglong Offset); + void SetPosition(const QDBusObjectPath &TrackId, qlonglong Position); + void Stop(); + +Q_SIGNALS: //Signals for MediaPlayer2.Player interface. + void Seeked(qlonglong Position); + +public: + void notifyChangedProperty(const QLatin1String &pPropertyName); + +public slots: + void openPLS(const QString &pPath); + void openM3U(const QString &pPath); + void notifySeeked(qlonglong pTimeInUs); + +protected: + uint extractIndex(const QString &pStr); +}; + +#endif // MPRIS2PLAYER_H diff --git a/boombox/src/threadjob.cpp b/boombox/src/threadjob.cpp new file mode 100644 index 00000000..aaebb883 --- /dev/null +++ b/boombox/src/threadjob.cpp @@ -0,0 +1,128 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#include "threadjob.h" + +#include +#include +#include + +void ThreadJobExecuter::run() +{ + QTimer::singleShot(0, mJob, SLOT(doWork())); + connect(mJob, SIGNAL(finished(KJob*)), SLOT(quit())); + exec(); +} + +ThreadJob::ThreadJob() + :mShouldDie(false), mShouldSuspend(false), mHasStarted(false) +{ + static bool onlyOnce = false; + if(!onlyOnce) + { + qRegisterMetaType >("QPair"); + onlyOnce = true; + } +} + +void ThreadJob::start() +{ + mThread = new ThreadJobExecuter(this); + moveToThread(mThread); + mThread->start(); + mHasStarted = true; +} + +void ThreadJob::emitResult() +{ + moveToThread(QApplication::instance()->thread()); + KJob::emitResult(); +} + +bool ThreadJob::doKill() +{ + if(!(capabilities() & Killable) || !mHasStarted) + return false; + + mShouldDie = true; + return true; +} + +bool ThreadJob::checkForDeath() +{ + if(!mShouldDie) + QCoreApplication::processEvents(); + + if(mShouldDie) + { + aboutToDie(); + mThread->quit(); + moveToThread(QApplication::instance()->thread()); + } + return mShouldDie; +} + +bool ThreadJob::doSuspend() +{ + if(!(capabilities() & Suspendable) || isSuspended()) + return false; + + mShouldSuspend = true; + return true; +} + +bool ThreadJob::checkForDeathOrSuspend() +{ + if(!mShouldSuspend && !mShouldDie) + QCoreApplication::processEvents(); + + if(mShouldDie) + { + aboutToDie(); + mThread->quit(); + moveToThread(QApplication::instance()->thread()); + return true; + } + + if(mShouldSuspend) + { + aboutToSuspend(); + while(mShouldSuspend && !mShouldDie) + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + + if(mShouldDie) + { + aboutToDie(); + mThread->quit(); + moveToThread(QApplication::instance()->thread()); + return true; + } + aboutToResume(); + return false; + } + return false; +} + +bool ThreadJob::doResume() +{ + if(!(capabilities() & Suspendable) || !isSuspended()) + return false; + mShouldSuspend = false; + return true; +} diff --git a/boombox/src/threadjob.h b/boombox/src/threadjob.h new file mode 100644 index 00000000..6afa834d --- /dev/null +++ b/boombox/src/threadjob.h @@ -0,0 +1,71 @@ +/*************************************************************************** + * Copyright (C) by Simon Persson * + * simonop@spray.se * + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#ifndef THREADJOB_H +#define THREADJOB_H + +#include +#include + +class ThreadJob; + +class ThreadJobExecuter : public QThread +{ + Q_OBJECT + public: + ThreadJobExecuter(ThreadJob *pJob) {mJob = pJob;} + void run(); + + private: + ThreadJob *mJob; +}; + +class ThreadJob: public KJob +{ + Q_OBJECT + public: + ThreadJob(); + + virtual void start(); + + public slots: + virtual void doWork()=0; + + protected: + virtual void emitResult(); + virtual bool doKill(); + virtual bool doSuspend(); + virtual bool doResume(); + + virtual bool checkForDeathOrSuspend(); + virtual bool checkForDeath(); + + virtual void aboutToDie(){} + virtual void aboutToSuspend(){} + virtual void aboutToResume(){} + + ThreadJobExecuter *mThread; + + bool mShouldDie; + bool mShouldSuspend; + bool mHasStarted; +}; + + +#endif