diff --git a/autotests/localfilelistingtest.cpp b/autotests/localfilelistingtest.cpp --- a/autotests/localfilelistingtest.cpp +++ b/autotests/localfilelistingtest.cpp @@ -42,7 +42,7 @@ #include #if !defined Q_OS_FREEBSD && !defined Q_OS_MACOS -#include +#include #endif class LocalFileListingTests: public QObject, public DatabaseTestData diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -377,6 +377,7 @@ qml/AlbumView.qml qml/RecentlyPlayedTracks.qml qml/FrequentlyPlayedTracks.qml + qml/RadiosView.qml qml/MediaPlayListView.qml qml/PlayListBasicView.qml diff --git a/src/audiowrapper.h b/src/audiowrapper.h --- a/src/audiowrapper.h +++ b/src/audiowrapper.h @@ -122,6 +122,8 @@ void positionChanged(qint64 position); + void currentPlayingForRadiosChanged(QString title, QString nowPlaying); + void seekableChanged(bool seekable); void playing(); diff --git a/src/audiowrapper_libvlc.cpp b/src/audiowrapper_libvlc.cpp --- a/src/audiowrapper_libvlc.cpp +++ b/src/audiowrapper_libvlc.cpp @@ -231,7 +231,15 @@ void AudioWrapper::setSource(const QUrl &source) { - d->mMedia = libvlc_media_new_path(d->mInstance, QDir::toNativeSeparators(source.toLocalFile()).toUtf8().constData()); + if(source.isLocalFile()){ + qCDebug(orgKdeElisaPlayerVlc) << "AudioWrapper::setSource reading local resource"; + d->mMedia = libvlc_media_new_path(d->mInstance, QDir::toNativeSeparators(source.toLocalFile()).toUtf8().constData()); + }else{ + qCDebug(orgKdeElisaPlayerVlc) << "AudioWrapper::setSource reading remote resource"; + const char * charUrl = source.url().toUtf8().constData(); + d->mMedia = libvlc_media_new_location(d->mInstance, charUrl); + } + if (!d->mMedia) { qCDebug(orgKdeElisaPlayerVlc) << "AudioWrapper::setSource" << "failed creating media" @@ -555,6 +563,13 @@ mParent->playerPositionSignalChanges(mPreviousPosition); } + + if(this->mMedia){ + QString title = QLatin1String(libvlc_media_get_meta(this->mMedia, libvlc_meta_Title)); + QString nowPlaying = QLatin1String(libvlc_media_get_meta(this->mMedia, libvlc_meta_NowPlaying)); + + Q_EMIT mParent->currentPlayingForRadiosChanged(title, nowPlaying); + } } void AudioWrapperPrivate::signalSeekableChange(bool isSeekable) diff --git a/src/databaseinterface.h b/src/databaseinterface.h --- a/src/databaseinterface.h +++ b/src/databaseinterface.h @@ -86,6 +86,7 @@ PlayFrequency, ElementTypeRole, LyricsRole, + HttpAddressRole, }; Q_ENUM(ColumnsRoles) @@ -226,6 +227,11 @@ { return operator[](key_type::FileModificationTime).toDateTime(); } + + QString httpAddress() const + { + return operator[](key_type::HttpAddressRole).toString(); + } }; using ListTrackDataType = QList; @@ -341,6 +347,8 @@ ListTrackDataType allTracksData(); + ListTrackDataType allRadiosData(); + ListTrackDataType recentlyPlayedTracksData(int count); ListTrackDataType frequentlyPlayedTracksData(int count); @@ -367,6 +375,8 @@ TrackDataType trackDataFromDatabaseId(qulonglong id); + TrackDataType radioDataFromDatabaseId(qulonglong id); + qulonglong trackIdFromTitleAlbumTrackDiscNumber(const QString &title, const QString &artist, const QString &album, int trackNumber, int discNumber); @@ -500,6 +510,8 @@ TrackDataType buildTrackDataFromDatabaseRecord(const QSqlRecord &trackRecord) const; + TrackDataType buildRadioDataFromDatabaseRecord(const QSqlRecord &trackRecord) const; + void internalRemoveTracksList(const QList &removedTracks); void internalRemoveTracksList(const QHash &removedTracks, qulonglong sourceId); @@ -524,12 +536,16 @@ ListTrackDataType internalAllTracksPartialData(); + ListTrackDataType internalAllRadiosPartialData(); + ListTrackDataType internalRecentlyPlayedTracksData(int count); ListTrackDataType internalFrequentlyPlayedTracksData(int count); TrackDataType internalOneTrackPartialData(qulonglong databaseId); + TrackDataType internalOneRadioPartialData(qulonglong databaseId); + ListGenreDataType internalAllGenresPartialData(); ListArtistDataType internalAllComposersPartialData(); @@ -553,6 +569,8 @@ void upgradeDatabaseV12(); + void upgradeDatabaseV13(); + void checkDatabaseSchema(); void checkAlbumsTableSchema(); diff --git a/src/databaseinterface.cpp b/src/databaseinterface.cpp --- a/src/databaseinterface.cpp +++ b/src/databaseinterface.cpp @@ -49,13 +49,15 @@ mSelectTrackQuery(mTracksDatabase), mSelectAlbumIdFromTitleQuery(mTracksDatabase), mInsertAlbumQuery(mTracksDatabase), mSelectTrackIdFromTitleAlbumIdArtistQuery(mTracksDatabase), mInsertTrackQuery(mTracksDatabase), mSelectTracksFromArtist(mTracksDatabase), - mSelectTrackFromIdQuery(mTracksDatabase), mSelectCountAlbumsForArtistQuery(mTracksDatabase), + mSelectTrackFromIdQuery(mTracksDatabase), mSelectRadioFromIdQuery(mTracksDatabase), + mSelectCountAlbumsForArtistQuery(mTracksDatabase), mSelectTrackIdFromTitleArtistAlbumTrackDiscNumberQuery(mTracksDatabase), mSelectAllAlbumsFromArtistQuery(mTracksDatabase), mSelectAllArtistsQuery(mTracksDatabase), mInsertArtistsQuery(mTracksDatabase), mSelectArtistByNameQuery(mTracksDatabase), mSelectArtistQuery(mTracksDatabase), mUpdateTrackStatistics(mTracksDatabase), mRemoveTrackQuery(mTracksDatabase), mRemoveAlbumQuery(mTracksDatabase), mRemoveArtistQuery(mTracksDatabase), mSelectAllTracksQuery(mTracksDatabase), + mSelectAllRadiosQuery(mTracksDatabase), mInsertTrackMapping(mTracksDatabase), mUpdateTrackFirstPlayStatistics(mTracksDatabase), mInsertMusicSource(mTracksDatabase), mSelectMusicSource(mTracksDatabase), mUpdateTrackPriority(mTracksDatabase), mUpdateTrackFileModifiedTime(mTracksDatabase), @@ -84,7 +86,8 @@ mSelectAllRecentlyPlayedTracksQuery(mTracksDatabase), mSelectAllFrequentlyPlayedTracksQuery(mTracksDatabase), mClearTracksTable(mTracksDatabase), mClearAlbumsTable(mTracksDatabase), mClearArtistsTable(mTracksDatabase), mClearComposerTable(mTracksDatabase), mClearGenreTable(mTracksDatabase), mClearLyricistTable(mTracksDatabase), - mArtistMatchGenreQuery(mTracksDatabase), mSelectTrackIdQuery(mTracksDatabase) + mArtistMatchGenreQuery(mTracksDatabase), mSelectTrackIdQuery(mTracksDatabase), + mInsertRadiosQuery(mTracksDatabase) { } @@ -106,6 +109,8 @@ QSqlQuery mSelectTrackFromIdQuery; + QSqlQuery mSelectRadioFromIdQuery; + QSqlQuery mSelectCountAlbumsForArtistQuery; QSqlQuery mSelectTrackIdFromTitleArtistAlbumTrackDiscNumberQuery; @@ -130,6 +135,8 @@ QSqlQuery mSelectAllTracksQuery; + QSqlQuery mSelectAllRadiosQuery; + QSqlQuery mInsertTrackMapping; QSqlQuery mUpdateTrackFirstPlayStatistics; @@ -248,6 +255,8 @@ QSqlQuery mSelectTrackIdQuery; + QSqlQuery mInsertRadiosQuery; + QSet mModifiedTrackIds; QSet mModifiedAlbumIds; @@ -364,6 +373,29 @@ return result; } +DatabaseInterface::ListTrackDataType DatabaseInterface::allRadiosData() +{ + auto result = ListTrackDataType{}; + + if (!d) { + return result; + } + + auto transactionResult = startTransaction(); + if (!transactionResult) { + return result; + } + + result = internalAllRadiosPartialData(); + + transactionResult = finishTransaction(); + if (!transactionResult) { + return result; + } + + return result; +} + DatabaseInterface::ListTrackDataType DatabaseInterface::recentlyPlayedTracksData(int count) { auto result = ListTrackDataType{}; @@ -702,6 +734,29 @@ return result; } +DatabaseInterface::TrackDataType DatabaseInterface::radioDataFromDatabaseId(qulonglong id) +{ + auto result = TrackDataType(); + + if (!d) { + return result; + } + + auto transactionResult = startTransaction(); + if (!transactionResult) { + return result; + } + + result = internalOneRadioPartialData(id); + + transactionResult = finishTransaction(); + if (!transactionResult) { + return result; + } + + return result; +} + qulonglong DatabaseInterface::trackIdFromTitleAlbumTrackDiscNumber(const QString &title, const QString &artist, const QString &album, int trackNumber, int discNumber) { @@ -1153,17 +1208,22 @@ upgradeDatabaseV9(); upgradeDatabaseV11(); upgradeDatabaseV12(); + upgradeDatabaseV13(); } else if (listTables.contains(QStringLiteral("DatabaseVersionV9"))) { if (!listTables.contains(QStringLiteral("DatabaseVersionV11"))) { upgradeDatabaseV11(); } if (!listTables.contains(QStringLiteral("DatabaseVersionV12"))) { upgradeDatabaseV12(); } + if (!listTables.contains(QStringLiteral("DatabaseVersionV13"))) { + upgradeDatabaseV13(); + } } else { createDatabaseV9(); upgradeDatabaseV11(); upgradeDatabaseV12(); + upgradeDatabaseV13(); } } @@ -2467,6 +2527,105 @@ qCInfo(orgKdeElisaDatabase) << "finished update to v12 of database schema"; } +void DatabaseInterface::upgradeDatabaseV13() +{ + qCInfo(orgKdeElisaDatabase) << "begin update to v13 of database schema"; + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + const auto &result = createSchemaQuery.exec(QStringLiteral("CREATE TABLE `DatabaseVersionV13` (`Version` INTEGER PRIMARY KEY NOT NULL)")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + QSqlQuery disableForeignKeys(d->mTracksDatabase); + + auto result = disableForeignKeys.exec(QStringLiteral(" PRAGMA foreign_keys=OFF")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << disableForeignKeys.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << disableForeignKeys.lastError(); + + Q_EMIT databaseError(); + } + } + + d->mTracksDatabase.transaction(); + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + const auto &result = createSchemaQuery.exec(QStringLiteral("CREATE TABLE `Radios` (" + "`ID` INTEGER PRIMARY KEY AUTOINCREMENT, " + "`HttpAddress` VARCHAR(255) NOT NULL, " + "`Priority` INTEGER NOT NULL, " + "`Title` VARCHAR(85) NOT NULL, " + "`Rating` INTEGER NOT NULL DEFAULT 0, " + "`Genre` VARCHAR(55), " + "`Comment` VARCHAR(255), " + "UNIQUE (" + "`HttpAddress`" + "), " + "UNIQUE (" + "`Priority`, `Title`, `HttpAddress`" + ") " + "CONSTRAINT fk_tracks_genre FOREIGN KEY (`Genre`) REFERENCES `Genre`(`Name`))" + )); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << createSchemaQuery.lastError(); + } + } + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + //Find webradios (french): https://doc.ubuntu-fr.org/liste_radio_france + //English: https://www.radio.fr/language/english (to get the link play a radio and look for streamUrl in the html elements page). + const auto &result = createSchemaQuery.exec(QStringLiteral("INSERT INTO `Radios` (`HttpAddress`, `Priority`, `Title`) " + "SELECT 'http://classicrock.stream.ouifm.fr/ouifm3.mp3', 1, 'OuiFM_Classic_Rock' UNION ALL " + "SELECT 'http://rock70s.stream.ouifm.fr/ouifmseventies.mp3', 1, 'OuiFM_70s' UNION ALL " + "SELECT 'http://jazzradio.ice.infomaniak.ch/jazzradio-high.mp3', 2 , 'Jazz_Radio' UNION ALL " + "SELECT 'http://cdn.nrjaudio.fm/audio1/fr/30601/mp3_128.mp3?origine=playerweb', 1, 'Nostalgie' UNION ALL " + "SELECT 'https://scdn.nrjaudio.fm/audio1/fr/30713/aac_64.mp3?origine=playerweb', 1, 'Nostalgie Johnny' UNION ALL " + "SELECT 'http://sc-classrock.1.fm:8200', 1, 'Classic rock replay' UNION ALL " + "SELECT 'http://agnes.torontocast.com:8151/stream', 1, 'Instrumentals Forever' UNION ALL " + "SELECT 'https://stream.laut.fm/jahfari', 1, 'Jahfari'" + )); + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + d->mTracksDatabase.commit(); + + { + QSqlQuery enableForeignKeys(d->mTracksDatabase); + + auto result = enableForeignKeys.exec(QStringLiteral(" PRAGMA foreign_keys=ON")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << enableForeignKeys.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV13" << enableForeignKeys.lastError(); + + Q_EMIT databaseError(); + } + } + + qCInfo(orgKdeElisaDatabase) << "finished update to v13 of database schema"; +} + void DatabaseInterface::checkDatabaseSchema() { checkAlbumsTableSchema(); @@ -3047,6 +3206,29 @@ } } + { + auto selectAllRadiosText = QStringLiteral("SELECT " + "radios.`ID`, " + "radios.`Title`, " + "radios.`HttpAddress`, " + "radios.`Rating`, " + "trackGenre.`Name`, " + "radios.`Comment` " + "FROM " + "`Radios` radios " + "LEFT JOIN `Genre` trackGenre ON trackGenre.`Name` = radios.`Genre` " + ""); + + auto result = prepareQuery(d->mSelectAllRadiosQuery, selectAllRadiosText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectAllRadiosQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectAllRadiosQuery.lastError(); + + Q_EMIT databaseError(); + } + } + { auto selectAllTracksText = QStringLiteral("SELECT " "tracks.`ID`, " @@ -3645,6 +3827,31 @@ Q_EMIT databaseError(); } } + + { + auto selectRadioFromIdQueryText = QStringLiteral("SELECT " + "radios.`ID`, " + "radios.`Title`, " + "radios.`HttpAddress`, " + "radios.`Rating`, " + "trackGenre.`Name`, " + "radios.`Comment` " + "FROM " + "`Radios` radios " + "LEFT JOIN `Genre` trackGenre ON trackGenre.`Name` = radios.`Genre` " + "WHERE " + "radios.`ID` = :radioId " + ""); + + auto result = prepareQuery(d->mSelectRadioFromIdQuery, selectRadioFromIdQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioFromIdQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioFromIdQuery.lastError(); + + Q_EMIT databaseError(); + } + } { auto selectCountAlbumsQueryText = QStringLiteral("SELECT count(*) " "FROM `Albums` album " @@ -5401,6 +5608,28 @@ return result; } +DatabaseInterface::TrackDataType DatabaseInterface::buildRadioDataFromDatabaseRecord(const QSqlRecord &trackRecord) const +{ + TrackDataType result; + + result[TrackDataType::key_type::DatabaseIdRole] = trackRecord.value(0); + result[TrackDataType::key_type::TitleRole] = trackRecord.value(1); + + result[TrackDataType::key_type::AlbumRole] = QStringLiteral("Radios"); + result[TrackDataType::key_type::ArtistRole] = trackRecord.value(1); + + result[TrackDataType::key_type::HttpAddressRole] = trackRecord.value(2); + result[TrackDataType::key_type::ResourceRole] = trackRecord.value(2); + result[TrackDataType::key_type::RatingRole] = trackRecord.value(3); + if (!trackRecord.value(4).isNull()) { + result[TrackDataType::key_type::GenreRole] = trackRecord.value(4); + } + result[TrackDataType::key_type::CommentRole] = trackRecord.value(5); + result[DataType::key_type::ElementTypeRole] = ElisaUtils::Radio; + + return result; +} + void DatabaseInterface::internalRemoveTracksList(const QList &removedTracks) { QSet modifiedAlbums; @@ -6303,6 +6532,27 @@ return result; } +DatabaseInterface::ListTrackDataType DatabaseInterface::internalAllRadiosPartialData() +{ + auto result = ListTrackDataType{}; + + if (!internalGenericPartialData(d->mSelectAllRadiosQuery)) { + return result; + } + + while(d->mSelectAllRadiosQuery.next()) { + const auto ¤tRecord = d->mSelectAllRadiosQuery.record(); + + auto newData = buildRadioDataFromDatabaseRecord(currentRecord); + + result.push_back(newData); + } + + d->mSelectAllRadiosQuery.finish(); + + return result; +} + DatabaseInterface::ListTrackDataType DatabaseInterface::internalRecentlyPlayedTracksData(int count) { auto result = ListTrackDataType{}; @@ -6370,6 +6620,27 @@ return result; } +DatabaseInterface::TrackDataType DatabaseInterface::internalOneRadioPartialData(qulonglong databaseId) +{ + auto result = TrackDataType{}; + + d->mSelectRadioFromIdQuery.bindValue(QStringLiteral(":radioId"), databaseId); + + if (!internalGenericPartialData(d->mSelectRadioFromIdQuery)) { + return result; + } + + if (d->mSelectRadioFromIdQuery.next()) { + const auto ¤tRecord = d->mSelectRadioFromIdQuery.record(); + + result = buildRadioDataFromDatabaseRecord(currentRecord); + } + + d->mSelectRadioFromIdQuery.finish(); + + return result; +} + DatabaseInterface::ListGenreDataType DatabaseInterface::internalAllGenresPartialData() { ListGenreDataType result; diff --git a/src/elisaapplication.cpp b/src/elisaapplication.cpp --- a/src/elisaapplication.cpp +++ b/src/elisaapplication.cpp @@ -361,21 +361,22 @@ QObject::connect(d->mAudioControl.get(), &ManageAudioPlayer::playerSourceChanged, d->mAudioWrapper.get(), &AudioWrapper::setSource); QObject::connect(d->mAudioControl.get(), &ManageAudioPlayer::startedPlayingTrack, d->mMusicManager->viewDatabase(), &DatabaseInterface::trackHasStartedPlaying); + QObject::connect(d->mAudioControl.get(), &ManageAudioPlayer::currentPlayingForRadiosChanged, d->mMediaPlayList.get(), &MediaPlayList::updateRadioData); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::ensurePlay, d->mAudioControl.get(), &ManageAudioPlayer::ensurePlay); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::playListFinished, d->mAudioControl.get(), &ManageAudioPlayer::playListFinished); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::currentTrackChanged, d->mAudioControl.get(), &ManageAudioPlayer::setCurrentTrack); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::clearPlayListPlayer, d->mAudioControl.get(), &ManageAudioPlayer::saveForUndoClearPlaylist); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::undoClearPlayListPlayer, d->mAudioControl.get(), &ManageAudioPlayer::restoreForUndoClearPlaylist); - QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::playbackStateChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerPlaybackState); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::statusChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerStatus); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::errorChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerError); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::durationChanged, d->mAudioControl.get(), &ManageAudioPlayer::setAudioDuration); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::seekableChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerIsSeekable); QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::positionChanged, d->mAudioControl.get(), &ManageAudioPlayer::setPlayerPosition); + QObject::connect(d->mAudioWrapper.get(), &AudioWrapper::currentPlayingForRadiosChanged, d->mAudioControl.get(), &ManageAudioPlayer::setCurrentPlayingForRadios); d->mPlayerControl->setPlayListModel(d->mMediaPlayList.get()); QObject::connect(d->mMediaPlayList.get(), &MediaPlayList::currentTrackChanged, d->mPlayerControl.get(), &ManageMediaPlayerControl::setCurrentTrack); diff --git a/src/elisautils.h b/src/elisautils.h --- a/src/elisautils.h +++ b/src/elisautils.h @@ -55,6 +55,7 @@ Track, FileName, Unknown, + Radio, }; Q_ENUM_NS(PlayListEntryType) diff --git a/src/manageaudioplayer.h b/src/manageaudioplayer.h --- a/src/manageaudioplayer.h +++ b/src/manageaudioplayer.h @@ -203,6 +203,8 @@ void startedPlayingTrack(const QUrl &fileName, const QDateTime &time); + void currentPlayingForRadiosChanged(const QVariant &value, int role); + public Q_SLOTS: void setCurrentTrack(const QPersistentModelIndex ¤tTrack); @@ -237,6 +239,8 @@ void setPlayerPosition(qint64 playerPosition); + void setCurrentPlayingForRadios(const QString title, const QString nowPlaying); + void setPlayControlPosition(int playerPosition); void setPersistentState(const QVariantMap &persistentStateValue); diff --git a/src/manageaudioplayer.cpp b/src/manageaudioplayer.cpp --- a/src/manageaudioplayer.cpp +++ b/src/manageaudioplayer.cpp @@ -413,6 +413,13 @@ QTimer::singleShot(0, [this]() {Q_EMIT playControlPositionChanged();}); } +void ManageAudioPlayer::setCurrentPlayingForRadios(const QString title, const QString nowPlaying){ + if(mPlayListModel && mCurrentTrack.isValid()){ + Q_EMIT currentPlayingForRadiosChanged(title, MediaPlayList::TitleRole); + Q_EMIT currentPlayingForRadiosChanged(nowPlaying, MediaPlayList::ArtistRole); + } +} + void ManageAudioPlayer::setPlayControlPosition(int playerPosition) { Q_EMIT seek(playerPosition); diff --git a/src/mediaplaylist.h b/src/mediaplaylist.h --- a/src/mediaplaylist.h +++ b/src/mediaplaylist.h @@ -118,6 +118,7 @@ CountRole, IsPlayingRole, AlbumSectionRole, + HttpAddressRole, }; Q_ENUM(ColumnsRoles) @@ -265,6 +266,8 @@ void undoClearPlayList(); + void updateRadioData(const QVariant &value, int role); + private Q_SLOTS: void loadPlayListLoaded(); @@ -290,7 +293,7 @@ void enqueueFilesList(const ElisaUtils::EntryDataList &newEntries); - void enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries); + void enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries, ElisaUtils::PlayListEntryType type); void enqueueOneEntry(const ElisaUtils::EntryData &entryData, ElisaUtils::PlayListEntryType type); diff --git a/src/mediaplaylist.cpp b/src/mediaplaylist.cpp --- a/src/mediaplaylist.cpp +++ b/src/mediaplaylist.cpp @@ -88,6 +88,7 @@ roles[static_cast(ColumnsRoles::IsValidRole)] = "isValid"; roles[static_cast(ColumnsRoles::DatabaseIdRole)] = "databaseId"; roles[static_cast(ColumnsRoles::TitleRole)] = "title"; + roles[static_cast(ColumnsRoles::HttpAddressRole)] = "httpAddress"; roles[static_cast(ColumnsRoles::StringDurationRole)] = "duration"; roles[static_cast(ColumnsRoles::ArtistRole)] = "artist"; roles[static_cast(ColumnsRoles::AlbumArtistRole)] = "albumArtist"; @@ -209,7 +210,8 @@ return modelModified; } - if (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::IsPlayingRole) { + if ((role != ColumnsRoles::TitleRole && role != ColumnsRoles::ArtistRole) && + (role < ColumnsRoles::IsValidRole || role > ColumnsRoles::IsPlayingRole)) { return modelModified; } @@ -220,10 +222,44 @@ case ColumnsRoles::IsPlayingRole: { modelModified = true; + auto before = d->mCurrentTrack.data(role); auto newState = static_cast(value.toInt()); d->mData[index.row()].mIsPlaying = newState; Q_EMIT dataChanged(index, index, {role}); + auto after = d->mCurrentTrack.data(role); + + if (!d->mCurrentTrack.isValid()) { + resetCurrentTrack(); + } + + break; + } + case ColumnsRoles::TitleRole: + { + modelModified = true; + d->mData[index.row()].mTitle = value; + d->mTrackData[index.row()][static_cast(role)] = value; + Q_EMIT dataChanged(index, index, {role}); + + auto test = d->mData[index.row()].mTitle; + auto myNewIndex = this->index(d->mCurrentPlayListPosition,0); + auto automyNewTitle = myNewIndex.data(convertedRole); + auto after = d->mCurrentTrack.data(convertedRole); + + if (!d->mCurrentTrack.isValid()) { + resetCurrentTrack(); + } + + break; + } + case ColumnsRoles::ArtistRole: + { + modelModified = true; + d->mData[index.row()].mArtist = value; + d->mTrackData[index.row()][static_cast(role)] = value; + Q_EMIT dataChanged(index, index, {role}); + if (!d->mCurrentTrack.isValid()) { resetCurrentTrack(); } @@ -414,13 +450,13 @@ Q_EMIT dataChanged(index(rowCount() - 1, 0), index(rowCount() - 1, 0), {MediaPlayList::IsPlayingRole}); } -void MediaPlayList::enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries) +void MediaPlayList::enqueueTracksListById(const ElisaUtils::EntryDataList &newEntries, ElisaUtils::PlayListEntryType type) { enqueueCommon(); beginInsertRows(QModelIndex(), d->mData.size(), d->mData.size() + newEntries.size() - 1); for (const auto &newTrack : newEntries) { - auto newMediaPlayListEntry = MediaPlayListEntry{std::get<0>(newTrack), std::get<1>(newTrack), ElisaUtils::Track}; + auto newMediaPlayListEntry = MediaPlayListEntry{std::get<0>(newTrack), std::get<1>(newTrack), type}; d->mData.push_back(newMediaPlayListEntry); d->mTrackData.push_back({}); Q_EMIT newEntryInList(newMediaPlayListEntry.mId, newMediaPlayListEntry.mTitle.toString(), newMediaPlayListEntry.mEntryType); @@ -554,6 +590,14 @@ Q_EMIT undoClearPlayListPlayer(); } +void MediaPlayList::updateRadioData(const QVariant &value, int role) +{ + auto convertedRole = static_cast(role); + if(d->mCurrentTrack.data(convertedRole) != value){ + this->setData(d->mCurrentTrack, value, role); + } +} + void MediaPlayList::enqueueCommon() { displayOrHideUndoInline(false); @@ -616,6 +660,7 @@ case ElisaUtils::Artist: case ElisaUtils::Genre: case ElisaUtils::Track: + case ElisaUtils::Radio: enqueueOneEntry(newEntry, databaseIdType); break; case ElisaUtils::FileName: @@ -657,7 +702,8 @@ switch (databaseIdType) { case ElisaUtils::Track: - enqueueTracksListById(newEntries); + case ElisaUtils::Radio: + enqueueTracksListById(newEntries, databaseIdType); break; case ElisaUtils::FileName: enqueueFilesList(newEntries); @@ -892,6 +938,24 @@ resetCurrentTrack(); } continue; + } else if(oneEntry.mEntryType == ElisaUtils::Radio ){ + if (track.databaseId() != oneEntry.mId) { + continue; + } + + d->mTrackData[i] = track; + oneEntry.mId = track.databaseId(); + oneEntry.mIsValid = true; + + Q_EMIT dataChanged(index(i, 0), index(i, 0), {}); + + restorePlayListPosition(); + + if (!d->mCurrentTrack.isValid()) { + resetCurrentTrack(); + } + + break; } else if (oneEntry.mEntryType != ElisaUtils::Artist && !oneEntry.mIsValid && !oneEntry.mTrackUrl.isValid()) { if (track.title() != oneEntry.mTitle) { continue; diff --git a/src/modeldataloader.h b/src/modeldataloader.h --- a/src/modeldataloader.h +++ b/src/modeldataloader.h @@ -62,6 +62,8 @@ void allTracksData(const ModelDataLoader::ListTrackDataType &allData); + void allRadiosData(const ModelDataLoader::ListTrackDataType &radiosData); + void allTrackData(const ModelDataLoader::TrackDataType &allData); void tracksAdded(ModelDataLoader::ListTrackDataType newData); diff --git a/src/modeldataloader.cpp b/src/modeldataloader.cpp --- a/src/modeldataloader.cpp +++ b/src/modeldataloader.cpp @@ -106,6 +106,9 @@ case ElisaUtils::FileName: case ElisaUtils::Unknown: break; + case ElisaUtils::Radio: + Q_EMIT allRadiosData(d->mDatabase->allRadiosData()); + break; } } diff --git a/src/models/datamodel.h b/src/models/datamodel.h --- a/src/models/datamodel.h +++ b/src/models/datamodel.h @@ -128,6 +128,8 @@ void tracksAdded(DataModel::ListTrackDataType newData); + void radiosAdded(DataModel::ListTrackDataType newData); + void trackModified(const DataModel::TrackDataType &modifiedTrack); void trackRemoved(qulonglong removedTrackId); diff --git a/src/models/datamodel.cpp b/src/models/datamodel.cpp --- a/src/models/datamodel.cpp +++ b/src/models/datamodel.cpp @@ -34,6 +34,8 @@ DataModel::ListTrackDataType mAllTrackData; + DataModel::ListTrackDataType mAllRadiosData; + DataModel::ListAlbumDataType mAllAlbumData; DataModel::ListArtistDataType mAllArtistData; @@ -90,6 +92,8 @@ roles[static_cast(DatabaseInterface::ColumnsRoles::DatabaseIdRole)] = "databaseId"; roles[static_cast(DatabaseInterface::ColumnsRoles::ElementTypeRole)] = "dataType"; + roles[static_cast(DatabaseInterface::ColumnsRoles::HttpAddressRole)] = "httpAddress"; + roles[static_cast(DatabaseInterface::ColumnsRoles::ArtistRole)] = "artist"; roles[static_cast(DatabaseInterface::ColumnsRoles::AllArtistsRole)] = "allArtists"; roles[static_cast(DatabaseInterface::ColumnsRoles::HighestTrackRating)] = "highestTrackRating"; @@ -123,11 +127,14 @@ Q_ASSERT(index.isValid()); Q_ASSERT(index.column() == 0); - Q_ASSERT(index.row() >= 0 && index.row() < dataCount); Q_ASSERT(!index.parent().isValid()); Q_ASSERT(index.model() == this); Q_ASSERT(index.internalId() == 0); + if(d->mModelType != ElisaUtils::Radio){ + Q_ASSERT(index.row() >= 0 && index.row() < dataCount); + } + switch(role) { case Qt::DisplayRole: @@ -145,6 +152,9 @@ case ElisaUtils::Genre: result = d->mAllGenreData[index.row()][GenreDataType::key_type::TitleRole]; break; + case ElisaUtils::Radio: + result = d->mAllRadiosData[index.row()][GenreDataType::key_type::TitleRole]; + break; case ElisaUtils::Lyricist: case ElisaUtils::Composer: case ElisaUtils::FileName: @@ -179,6 +189,9 @@ case ElisaUtils::Genre: result = d->mAllGenreData[index.row()][static_cast(role)]; break; + case ElisaUtils::Radio: + result = d->mAllRadiosData[index.row()][static_cast(role)]; + break; case ElisaUtils::Lyricist: case ElisaUtils::Composer: case ElisaUtils::FileName: @@ -401,6 +414,8 @@ connect(&d->mDataLoader, &ModelDataLoader::allTracksData, this, &DataModel::tracksAdded); + connect(&d->mDataLoader, &ModelDataLoader::allRadiosData, + this, &DataModel::radiosAdded); connect(&d->mDataLoader, &ModelDataLoader::allAlbumsData, this, &DataModel::albumsAdded); connect(&d->mDataLoader, &ModelDataLoader::allArtistsData, @@ -433,6 +448,10 @@ setBusy(false); } + if(d->mModelType == ElisaUtils::Radio){ + setBusy(false); + } + if (newData.isEmpty() || d->mModelType != ElisaUtils::Track) { return; } @@ -488,6 +507,67 @@ } } +void DataModel::radiosAdded(ListTrackDataType newData) +{ + if (newData.isEmpty() && d->mModelType == ElisaUtils::Radio) { + setBusy(false); + } + + if (newData.isEmpty() || d->mModelType != ElisaUtils::Radio) { + return; + } + + if (d->mFilterType == FilterById && !d->mAllRadiosData.isEmpty()) { + for (const auto &newTrack : newData) { + auto trackIndex = trackIndexFromId(newTrack.databaseId()); + + if (trackIndex != -1) { + continue; + } + + bool trackInserted = false; + for (int trackIndex = 0; trackIndex < d->mAllRadiosData.count(); ++trackIndex) { + const auto &oneTrack = d->mAllRadiosData[trackIndex]; + + if (oneTrack.trackNumber() > newTrack.trackNumber()) { + beginInsertRows({}, trackIndex, trackIndex); + d->mAllRadiosData.insert(trackIndex, newTrack); + endInsertRows(); + + if (d->mAllRadiosData.size() == 1) { + setBusy(false); + } + + trackInserted = true; + break; + } + } + + if (!trackInserted) { + beginInsertRows({}, d->mAllRadiosData.count(), d->mAllRadiosData.count()); + d->mAllRadiosData.insert(d->mAllRadiosData.count(), newTrack); + endInsertRows(); + + if (d->mAllRadiosData.size() == 1) { + setBusy(false); + } + } + } + } else { + if (d->mAllRadiosData.isEmpty()) { + beginInsertRows({}, 0, newData.size() - 1); + d->mAllRadiosData.swap(newData); + endInsertRows(); + + setBusy(false); + } else { + beginInsertRows({}, d->mAllRadiosData.size(), d->mAllRadiosData.size() + newData.size() - 1); + d->mAllRadiosData.append(newData); + endInsertRows(); + } + } +} + void DataModel::trackModified(const TrackDataType &modifiedTrack) { if (d->mModelType != ElisaUtils::Track) { diff --git a/src/models/viewsmodel.cpp b/src/models/viewsmodel.cpp --- a/src/models/viewsmodel.cpp +++ b/src/models/viewsmodel.cpp @@ -39,25 +39,30 @@ mTypes = {ViewManager::NoViews, ViewManager::RecentlyPlayedTracks, ViewManager::FrequentlyPlayedTracks, ViewManager::AllAlbums, ViewManager::AllArtists, ViewManager::AllTracks, - ViewManager::AllGenres, ViewManager::FilesBrowser}; + ViewManager::AllGenres, ViewManager::FilesBrowser, + ViewManager::RadiosBrowser}; mNames = {{ViewManager::NoViews, {i18nc("Title of the view of the playlist", "Now Playing")}}, {ViewManager::RecentlyPlayedTracks, {i18nc("Title of the view of recently played tracks", "Recently Played")}}, {ViewManager::FrequentlyPlayedTracks, {i18nc("Title of the view of frequently played tracks", "Frequently Played")}}, {ViewManager::AllAlbums, {i18nc("Title of the view of all albums", "Albums")}}, {ViewManager::AllArtists, {i18nc("Title of the view of all artists", "Artists")}}, {ViewManager::AllTracks, {i18nc("Title of the view of all tracks", "Tracks")}}, {ViewManager::AllGenres, {i18nc("Title of the view of all genres", "Genres")}}, - {ViewManager::FilesBrowser, {i18nc("Title of the file browser view", "Files")}}}; + {ViewManager::FilesBrowser, {i18nc("Title of the file browser view", "Files")}}, + {ViewManager::RadiosBrowser, {i18nc("Title of the file radios browser view", "Radios")}}}; + mIcons = {{ViewManager::NoViews, QUrl{QStringLiteral("image://icon/view-media-playlist")}}, {ViewManager::RecentlyPlayedTracks, QUrl{QStringLiteral("image://icon/media-playlist-play")}}, {ViewManager::FrequentlyPlayedTracks, QUrl{QStringLiteral("image://icon/amarok_playcount")}}, {ViewManager::AllAlbums, QUrl{QStringLiteral("image://icon/view-media-album-cover")}}, {ViewManager::AllArtists, QUrl{QStringLiteral("image://icon/view-media-artist")}}, {ViewManager::AllTracks, QUrl{QStringLiteral("image://icon/view-media-track")}}, {ViewManager::AllGenres, QUrl{QStringLiteral("image://icon/view-media-genre")}}, - {ViewManager::FilesBrowser, QUrl{QStringLiteral("image://icon/document-open-folder")}}}; + {ViewManager::FilesBrowser, QUrl{QStringLiteral("image://icon/document-open-folder")}}, + {ViewManager::RadiosBrowser, QUrl{QStringLiteral("image://icon/radio")}}}; + } }; @@ -167,6 +172,8 @@ return 6; case ViewManager::FilesBrowser: return 7; + case ViewManager::RadiosBrowser: + return 8; case ViewManager::OneAlbum: case ViewManager::OneArtist: case ViewManager::OneAlbumFromArtist: diff --git a/src/qml/ContentView.qml b/src/qml/ContentView.qml --- a/src/qml/ContentView.qml +++ b/src/qml/ContentView.qml @@ -166,6 +166,23 @@ }) } + onSwitchRadiosView: { + listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) + + while(browseStackView.depth > expectedDepth) { + browseStackView.pop() + } + + browseStackView.push(radiosView, { + viewType: viewType, + mainTitle: mainTitle, + image: imageUrl, + modelType: dataType, + stackView: browseStackView, + opacity: 0, + }) + } + onPopOneView: { if (browseStackView.depth > 2) { browseStackView.pop() } @@ -559,6 +576,14 @@ } } + Component { + id: radiosView + + RadiosView { + StackView.onActivated: viewManager.viewIsLoaded(viewType) + } + } + Component { id: filesBrowserView diff --git a/src/qml/MediaRadioDelegate.qml b/src/qml/MediaRadioDelegate.qml new file mode 100644 --- /dev/null +++ b/src/qml/MediaRadioDelegate.qml @@ -0,0 +1,443 @@ +/* + * Copyright 2016-2017 Matthieu Gallien + * Copyright 2017 Alexander Stippich + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import QtQuick.Controls 1.4 as Controls1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.0 +import org.kde.elisa 1.0 + +FocusScope { + id: mediaTrack + + property var databaseId + property string title + property string httpAddress + property string artist + property string album + property string albumArtist + property string duration + property url imageUrl + property int trackNumber + property int discNumber + property int rating + property bool isFirstTrackOfDisc + property bool isSingleDiscAlbum + property bool isAlternateColor + property bool detailedView: true + + signal clicked() + signal enqueue(var databaseId, var name) + signal replaceAndPlay(var databaseId, var name) + + Controls1.Action { + id: enqueueAction + text: i18nc("Enqueue current track", "Enqueue") + iconName: "media-track-add-amarok" + onTriggered: enqueue(databaseId, title) + } + + Controls1.Action { + id: viewDetailsAction + text: i18nc("Show track metadata", "View Details") + iconName: "help-about" + onTriggered: { + if (metadataLoader.active === false) { + metadataLoader.active = true + } + else { + metadataLoader.item.close(); + metadataLoader.active = false + } + } + } + + Controls1.Action { + id: replaceAndPlayAction + text: i18nc("Clear play list and enqueue current track", "Play Now and Replace Play List") + iconName: "media-playback-start" + onTriggered: replaceAndPlay(databaseId, title) + } + + Keys.onReturnPressed: enqueue(databaseId, title) + Keys.onEnterPressed: enqueue(databaseId, title) + + Loader { + id: metadataLoader + active: false + onLoaded: item.show() + + sourceComponent: MediaTrackMetadataView { + databaseId: mediaTrack.databaseId + onRejected: metadataLoader.active = false; + } + } + + Rectangle { + id: rowRoot + + anchors.fill: parent + + color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) + + MouseArea { + id: hoverArea + + anchors.fill: parent + + hoverEnabled: true + focus: true + acceptedButtons: Qt.LeftButton + + onClicked: { + hoverArea.forceActiveFocus() + mediaTrack.clicked() + } + + onDoubleClicked: enqueue(databaseId, title) + + RowLayout { + anchors.fill: parent + spacing: 0 + + LabelWithToolTip { + id: mainLabel + + visible: !detailedView + + text: { + if (trackNumber !== 0) { + if (artist !== albumArtist) + return i18nc("%1: track number. %2: track title. %3: artist name", + "%1 - %2 - %3", + trackNumber.toLocaleString(Qt.locale(), 'f', 0), + title, artist); + else + return i18nc("%1: track number. %2: track title.", + "%1 - %2", + trackNumber.toLocaleString(Qt.locale(), 'f', 0), + title); + } else { + if (artist !== albumArtist) + return i18nc("%1: track title. %2: artist name", + "%1 - %2", + title, artist); + else + return i18nc("%1: track title", + "%1", + title); + } + } + + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + + color: myPalette.text + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + Layout.leftMargin: { + if (!LayoutMirroring.enabled) + return (!isSingleDiscAlbum ? elisaTheme.layoutHorizontalMargin * 4 : elisaTheme.layoutHorizontalMargin) + else + return 0 + } + Layout.rightMargin: { + if (LayoutMirroring.enabled) + return (!isSingleDiscAlbum ? elisaTheme.layoutHorizontalMargin * 4 : elisaTheme.layoutHorizontalMargin) + else + return 0 + } + } + + Item { + Layout.preferredHeight: mediaTrack.height * 0.9 + Layout.preferredWidth: mediaTrack.height * 0.9 + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + visible: detailedView + + Image { + id: coverImageElement + + anchors.fill: parent + + sourceSize.width: mediaTrack.height * 0.9 + sourceSize.height: mediaTrack.height * 0.9 + fillMode: Image.PreserveAspectFit + smooth: true + + source: (imageUrl != '' ? imageUrl : Qt.resolvedUrl(elisaTheme.defaultAlbumImage)) + + asynchronous: true + + layer.enabled: imageUrl != '' + + layer.effect: DropShadow { + source: coverImageElement + + radius: 10 + spread: 0.1 + samples: 21 + + color: myPalette.shadow + } + } + } + + ColumnLayout { + visible: detailedView + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignLeft + + spacing: 0 + + LabelWithToolTip { + id: mainLabelDetailed + +// text: { +// if (trackNumber !== 0) { +// return i18nc("%1: track number. %2: track title", "%1 - %2", +// trackNumber.toLocaleString(Qt.locale(), 'f', 0), title); +// } else { +// return title; +// } +// } + text: title + + horizontalAlignment: Text.AlignLeft + + font.weight: Font.Bold + color: myPalette.text + + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.fillWidth: true + Layout.topMargin: elisaTheme.layoutVerticalMargin / 2 + + elide: Text.ElideRight + } + + Item { + Layout.fillHeight: true + } + + LabelWithToolTip { + id: artistLabel + +// text: { +// var labelText = "" +// if (artist) { +// labelText += artist +// } +// if (album !== '') { +// labelText += ' - ' + album +// if (!isSingleDiscAlbum) { +// labelText += ' - CD ' + discNumber +// } +// } +// return labelText; +// } + + text: httpAddress + horizontalAlignment: Text.AlignLeft + + font.weight: Font.Light + font.italic: true + color: myPalette.text + + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.fillWidth: true + Layout.bottomMargin: elisaTheme.layoutVerticalMargin / 2 + + elide: Text.ElideRight + } + } + + Loader { + id: hoverLoader + active: false + + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.rightMargin: 10 + + z: 1 + opacity: 0 + + sourceComponent: Row { + anchors.centerIn: parent + + Controls1.ToolButton { + id: detailsButton + + height: elisaTheme.delegateHeight + width: elisaTheme.delegateHeight + + action: viewDetailsAction + } + + Controls1.ToolButton { + id: enqueueButton + + height: elisaTheme.delegateHeight + width: elisaTheme.delegateHeight + + action: enqueueAction + } + + Controls1.ToolButton { + id: clearAndEnqueueButton + + scale: LayoutMirroring.enabled ? -1 : 1 + + height: elisaTheme.delegateHeight + width: elisaTheme.delegateHeight + + action: replaceAndPlayAction + } + } + } + + RatingStar { + id: ratingWidget + + starSize: elisaTheme.ratingStarSize + + starRating: rating + + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + } + + TextMetrics { + id: durationTextMetrics + text: i18nc("This is used to preserve a fixed width for the duration text.", "00:00") + } + + LabelWithToolTip { + id: durationLabel + + text: duration + + font.weight: Font.Light + color: myPalette.text + + horizontalAlignment: Text.AlignRight + + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.preferredWidth: durationTextMetrics.width + 1 + } + } + } + } + + states: [ + State { + name: 'notSelected' + when: !hoverArea.containsMouse && !mediaTrack.activeFocus + PropertyChanges { + target: hoverLoader + active: false + } + PropertyChanges { + target: hoverLoader + opacity: 0.0 + } + PropertyChanges { + target: ratingWidget + hoverWidgetOpacity: 0.0 + } + PropertyChanges { + target: rowRoot + color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) + } + }, + State { + name: 'hoveredOrSelected' + when: hoverArea.containsMouse || mediaTrack.activeFocus + PropertyChanges { + target: hoverLoader + active: true + } + PropertyChanges { + target: hoverLoader + opacity: 1.0 + } + PropertyChanges { + target: ratingWidget + hoverWidgetOpacity: 1.0 + } + PropertyChanges { + target: rowRoot + color: myPalette.mid + } + } + ] + + transitions: [ + Transition { + to: 'hoveredOrSelected' + SequentialAnimation { + PropertyAction { + properties: "active" + } + ParallelAnimation { + NumberAnimation { + properties: "opacity, hoverWidgetOpacity" + easing.type: Easing.InOutQuad + duration: 250 + } + ColorAnimation { + properties: "color" + duration: 250 + } + } + } + }, + Transition { + to: 'notSelected' + SequentialAnimation { + ParallelAnimation { + NumberAnimation { + properties: "opacity, hoverWidgetOpacity" + easing.type: Easing.InOutQuad + duration: 250 + } + ColorAnimation { + properties: "color" + duration: 250 + } + } + PropertyAction { + properties: "active" + } + } + } + ] +} diff --git a/src/qml/PlayListBasicView.qml b/src/qml/PlayListBasicView.qml --- a/src/qml/PlayListBasicView.qml +++ b/src/qml/PlayListBasicView.qml @@ -114,18 +114,19 @@ databaseId: (model.databaseId ? model.databaseId : 0) title: model.title - artist: model.artist - album: model.album + artist: (model.artist ? model.artist : '') + album: (model.album ? model.album : '') albumArtist: (model.albumArtist ? model.albumArtist : '') duration: (model.duration ? model.duration : '') fileName: (model.trackResource ? model.trackResource : '') - imageUrl: model.imageUrl + imageUrl: (model.imageUrl ? model.imageUrl : '') trackNumber: (model.trackNumber ? model.trackNumber : -1) discNumber: (model.discNumber ? model.discNumber : -1) rating: (model.rating ? model.rating : 0) isSingleDiscAlbum: (model.isSingleDiscAlbum ? model.isSingleDiscAlbum : true) isValid: model.isValid isPlaying: model.isPlaying + httpAddress: (model.httpAddress ? model.httpAddress : '') onStartPlayback: playListView.startPlayback() onPausePlayback: playListView.pausePlayback() diff --git a/src/qml/PlayListEntry.qml b/src/qml/PlayListEntry.qml --- a/src/qml/PlayListEntry.qml +++ b/src/qml/PlayListEntry.qml @@ -39,6 +39,7 @@ property string album property string albumArtist property string duration + property string httpAddress property url fileName property url imageUrl property int trackNumber diff --git a/src/qml/RadiosView.qml b/src/qml/RadiosView.qml new file mode 100644 --- /dev/null +++ b/src/qml/RadiosView.qml @@ -0,0 +1,114 @@ +/* + * Copyright 2018 Matthieu Gallien + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.10 +import QtQuick.Controls 2.3 +import org.kde.elisa 1.0 + +FocusScope { + id: viewHeader + + property var viewType + property alias mainTitle: listView.mainTitle + property alias image: listView.image + property var modelType + + focus: true + + DataModel { + id: realModel + } + + AllTracksProxyModel { + id: proxyModel + + sortRole: Qt.DisplayRole + sourceModel: realModel + + onEntriesToEnqueue: elisa.mediaPlayList.enqueue(newEntries, viewHeader.modelType, enqueueMode, triggerPlay) + } + + ListBrowserView { + id: listView + + focus: true + activeFocusOnTab: true + + anchors.fill: parent + + contentModel: proxyModel + + delegate: MediaRadioDelegate { + id: entry + + width: listView.delegateWidth + height: elisaTheme.trackDelegateHeight + + focus: true + + databaseId: model.databaseId + title: model.title + httpAddress: model.httpAddress +// artist: model.artist +// album: (model.album !== undefined && model.album !== '' ? model.album : '') +// albumArtist: model.albumArtist +// duration: model.duration +// imageUrl: (model.imageUrl !== undefined && model.imageUrl !== '' ? model.imageUrl : '') +// trackNumber: model.trackNumber +// discNumber: model.discNumber +// rating: model.rating + isFirstTrackOfDisc: false +// isSingleDiscAlbum: model.isSingleDiscAlbum + + onEnqueue: elisa.mediaPlayList.enqueue(databaseId, name, modelType, + ElisaUtils.AppendPlayList, + ElisaUtils.DoNotTriggerPlay) + + onReplaceAndPlay: elisa.mediaPlayList.enqueue(databaseId, name, modelType, + ElisaUtils.ReplacePlayList, + ElisaUtils.TriggerPlay) + + onClicked: contentDirectoryView.currentIndex = index + } + + Loader { + anchors.fill: parent + + visible: realModel.isBusy + active: realModel.isBusy + + sourceComponent: BusyIndicator { + anchors.fill: parent + } + } + } + + Connections { + target: elisa + + onMusicManagerChanged: realModel.initialize(elisa.musicManager, + elisa.musicManager.viewDatabase, + modelType) + } + + Component.onCompleted: { + if (elisa.musicManager) { + realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, + modelType) + } + } +} diff --git a/src/resources.qrc b/src/resources.qrc --- a/src/resources.qrc +++ b/src/resources.qrc @@ -44,6 +44,8 @@ qml/BasicPlayListAlbumHeader.qml qml/MetaDataDelegate.qml qml/ContextViewLyrics.qml + qml/RadiosView.qml + qml/MediaRadioDelegate.qml windows/WindowsTheme.qml diff --git a/src/trackslistener.cpp b/src/trackslistener.cpp --- a/src/trackslistener.cpp +++ b/src/trackslistener.cpp @@ -34,6 +34,8 @@ QSet mTracksByIdSet; + QSet mRadiosByIdSet; + QList> mTracksByNameSet; QList mTracksByFileNameSet; @@ -198,6 +200,16 @@ } break; } + case ElisaUtils::Radio: + { + d->mRadiosByIdSet.insert(newDatabaseId); + + auto newTrack = d->mDatabase->radioDataFromDatabaseId(newDatabaseId); + if (!newTrack.isEmpty()) { + Q_EMIT trackHasChanged(newTrack); + } + break; + } case ElisaUtils::Artist: newArtistInList(newDatabaseId, entryTitle); break; diff --git a/src/viewmanager.h b/src/viewmanager.h --- a/src/viewmanager.h +++ b/src/viewmanager.h @@ -44,7 +44,8 @@ OneAlbumFromArtistAndGenre, FrequentlyPlayedTracks, RecentlyPlayedTracks, - FilesBrowser + FilesBrowser, + RadiosBrowser }; Q_ENUM(ViewsType) @@ -76,6 +77,9 @@ void switchFilesBrowserView(ViewManager::ViewsType viewType, int expectedDepth, const QString &mainTitle, const QUrl &imageUrl); + void switchRadiosView(ViewManager::ViewsType viewType, int expectedDepth, const QString &mainTitle, + const QUrl &imageUrl, ElisaUtils::PlayListEntryType dataType); + void switchOffAllViews(ViewManager::ViewsType viewType); void popOneView(); @@ -110,6 +114,8 @@ void openFilesBrowser(const QString &mainTitle, const QUrl &imageUrl); + void openRadiosBrowser(const QString &mainTitle, const QUrl &imageUrl); + void openOneAlbum(const QString &albumTitle, const QString &albumAuthor, const QUrl &albumCover, qulonglong albumDatabaseId); @@ -137,6 +143,8 @@ void filesBrowserViewIsLoaded(); + void radiosBrowserViewIsLoaded(); + ViewsType mCurrentView = ViewsType::NoViews; QString mCurrentAlbumTitle; QString mCurrentAlbumAuthor; diff --git a/src/viewmanager.cpp b/src/viewmanager.cpp --- a/src/viewmanager.cpp +++ b/src/viewmanager.cpp @@ -56,6 +56,9 @@ case FilesBrowser: openFilesBrowser(mainTitle, mainImage); break; + case RadiosBrowser: + openRadiosBrowser(mainTitle, mainImage); + break; case OneAlbum: case OneArtist: case OneAlbumFromArtist: @@ -133,6 +136,9 @@ case ViewsType::FilesBrowser: filesBrowserViewIsLoaded(); break; + case ViewsType::RadiosBrowser: + radiosBrowserViewIsLoaded(); + break; case ViewsType::NoViews: break; } @@ -278,6 +284,14 @@ } } +void ViewManager::openRadiosBrowser(const QString &mainTitle, const QUrl &imageUrl) +{ + mTargetView = ViewsType::RadiosBrowser; + if (mCurrentView != mTargetView) { + Q_EMIT switchRadiosView(mTargetView, 1, mainTitle, imageUrl, ElisaUtils::Radio); + } +} + void ViewManager::recentlyPlayedTracksIsLoaded() { mCurrentView = ViewsType::RecentlyPlayedTracks; @@ -359,6 +373,11 @@ mCurrentView = ViewsType::FilesBrowser; } +void ViewManager::radiosBrowserViewIsLoaded() +{ + mCurrentView = ViewsType::RadiosBrowser; +} + void ViewManager::goBack() { Q_EMIT popOneView();