diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,7 @@ models/alltracksproxymodel.cpp models/singlealbumproxymodel.cpp models/trackmetadatamodel.cpp + models/radiometadatamodel.cpp models/trackcontextmetadatamodel.cpp models/viewsmodel.cpp ) @@ -378,6 +379,7 @@ qml/AlbumView.qml qml/RecentlyPlayedTracks.qml qml/FrequentlyPlayedTracks.qml + qml/RadiosView.qml qml/MediaPlayListView.qml qml/PlayListBasicView.qml @@ -388,6 +390,7 @@ qml/BasicPlayListAlbumHeader.qml qml/MetaDataDelegate.qml + qml/MetaRadioDataDelegate.qml qml/MediaTrackDelegate.qml qml/MediaAlbumTrackDelegate.qml qml/MediaTrackMetadataView.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 @@ -233,7 +233,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" @@ -557,6 +565,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); @@ -410,6 +420,10 @@ void finishRemovingTracksList(); + void updateUIAfterRadioInsertOrUpdate(TrackDataType radio, bool isInsertion); + + void updateUIAfterRadioDeleted(qulonglong radioId); + public Q_SLOTS: void insertTracksList(const QList &tracks, const QHash &covers); @@ -422,6 +436,10 @@ void clearData(); + void updateRadioInDatabase(const TrackDataType &oneTrack); + + void deleteRadioInDatabase(qulonglong radioId); + private: enum class TrackFileInsertType { @@ -456,6 +474,8 @@ qulonglong internalTrackIdFromFileName(const QUrl &fileName); + qulonglong internalRadioIdFromHttpAddress(const QString &httpAddress); + ListTrackDataType internalTracksFromAuthor(const QString &artistName); QList internalAlbumIdsFromAuthor(const QString &artistName); @@ -501,6 +521,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); @@ -525,12 +547,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(); @@ -556,6 +582,8 @@ void upgradeDatabaseV13(); + void upgradeDatabaseV14(); + void checkDatabaseSchema(); void checkAlbumsTableSchema(); diff --git a/src/databaseinterface.cpp b/src/databaseinterface.cpp --- a/src/databaseinterface.cpp +++ b/src/databaseinterface.cpp @@ -49,17 +49,20 @@ 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), mSelectTracksMapping(mTracksDatabase), mSelectTracksMappingPriority(mTracksDatabase), + mSelectRadioIdFromHttpAddress(mTracksDatabase), mUpdateAlbumArtUriFromAlbumIdQuery(mTracksDatabase), mSelectTracksMappingPriorityByTrackId(mTracksDatabase), mSelectAlbumIdsFromArtist(mTracksDatabase), mSelectAllTrackFilesQuery(mTracksDatabase), mRemoveTracksMappingFromSource(mTracksDatabase), mRemoveTracksMapping(mTracksDatabase), @@ -76,15 +79,17 @@ mSelectCountAlbumsForLyricistQuery(mTracksDatabase), mSelectAllGenresQuery(mTracksDatabase), mSelectGenreForArtistQuery(mTracksDatabase), mSelectGenreForAlbumQuery(mTracksDatabase), mUpdateTrackQuery(mTracksDatabase), mUpdateAlbumArtistQuery(mTracksDatabase), + mUpdateRadioQuery(mTracksDatabase), mUpdateAlbumArtistInTracksQuery(mTracksDatabase), mQueryMaximumTrackIdQuery(mTracksDatabase), mQueryMaximumAlbumIdQuery(mTracksDatabase), mQueryMaximumArtistIdQuery(mTracksDatabase), mQueryMaximumLyricistIdQuery(mTracksDatabase), mQueryMaximumComposerIdQuery(mTracksDatabase), mQueryMaximumGenreIdQuery(mTracksDatabase), mSelectAllArtistsWithGenreFilterQuery(mTracksDatabase), mSelectAllAlbumsShortWithGenreArtistFilterQuery(mTracksDatabase), mSelectAllAlbumsShortWithArtistFilterQuery(mTracksDatabase), mSelectAllRecentlyPlayedTracksQuery(mTracksDatabase), mSelectAllFrequentlyPlayedTracksQuery(mTracksDatabase), mClearTracksTable(mTracksDatabase), mClearAlbumsTable(mTracksDatabase), mClearArtistsTable(mTracksDatabase), mClearComposerTable(mTracksDatabase), mClearGenreTable(mTracksDatabase), mClearLyricistTable(mTracksDatabase), - mArtistMatchGenreQuery(mTracksDatabase), mSelectTrackIdQuery(mTracksDatabase) + mArtistMatchGenreQuery(mTracksDatabase), mSelectTrackIdQuery(mTracksDatabase), + mInsertRadioQuery(mTracksDatabase), mDeleteRadioQuery(mTracksDatabase) { } @@ -106,6 +111,8 @@ QSqlQuery mSelectTrackFromIdQuery; + QSqlQuery mSelectRadioFromIdQuery; + QSqlQuery mSelectCountAlbumsForArtistQuery; QSqlQuery mSelectTrackIdFromTitleArtistAlbumTrackDiscNumberQuery; @@ -130,6 +137,8 @@ QSqlQuery mSelectAllTracksQuery; + QSqlQuery mSelectAllRadiosQuery; + QSqlQuery mInsertTrackMapping; QSqlQuery mUpdateTrackFirstPlayStatistics; @@ -144,6 +153,8 @@ QSqlQuery mSelectTracksMapping; + QSqlQuery mSelectRadioIdFromHttpAddress; + QSqlQuery mSelectTracksMappingPriority; QSqlQuery mUpdateAlbumArtUriFromAlbumIdQuery; @@ -206,6 +217,8 @@ QSqlQuery mUpdateTrackQuery; + QSqlQuery mUpdateRadioQuery; + QSqlQuery mUpdateAlbumArtistQuery; QSqlQuery mUpdateAlbumArtistInTracksQuery; @@ -248,6 +261,10 @@ QSqlQuery mSelectTrackIdQuery; + QSqlQuery mInsertRadioQuery; + + QSqlQuery mDeleteRadioQuery; + QSet mModifiedTrackIds; QSet mModifiedAlbumIds; @@ -364,6 +381,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 +742,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,6 +1216,7 @@ upgradeDatabaseV11(); upgradeDatabaseV12(); upgradeDatabaseV13(); + upgradeDatabaseV14(); checkDatabaseSchema(); } else if (listTables.contains(QStringLiteral("DatabaseVersionV9"))) { @@ -1165,13 +1229,17 @@ if (!listTables.contains(QStringLiteral("DatabaseVersionV13"))) { upgradeDatabaseV13(); } + if(!listTables.contains(QStringLiteral("DatabaseVersionV14"))){ + upgradeDatabaseV14(); + } checkDatabaseSchema(); } else { createDatabaseV9(); upgradeDatabaseV11(); upgradeDatabaseV12(); upgradeDatabaseV13(); + upgradeDatabaseV14(); } } @@ -2735,6 +2803,107 @@ qCInfo(orgKdeElisaDatabase) << "finished update to v13 of database schema"; } +void DatabaseInterface::upgradeDatabaseV14() +{ + qCInfo(orgKdeElisaDatabase) << "begin update to v14 of database schema"; + + { + QSqlQuery createSchemaQuery(d->mTracksDatabase); + + const auto &result = createSchemaQuery.exec(QStringLiteral("CREATE TABLE `DatabaseVersionV14` (`Version` INTEGER PRIMARY KEY NOT NULL)")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + QSqlQuery disableForeignKeys(d->mTracksDatabase); + + auto result = disableForeignKeys.exec(QStringLiteral(" PRAGMA foreign_keys=OFF")); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << disableForeignKeys.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << 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::upgradeDatabaseV14" << createSchemaQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << createSchemaQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + 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::upgradeDatabaseV14" << enableForeignKeys.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::upgradeDatabaseV14" << enableForeignKeys.lastError(); + + Q_EMIT databaseError(); + } + } + + qCInfo(orgKdeElisaDatabase) << "finished update to v14 of database schema"; +} + void DatabaseInterface::checkDatabaseSchema() { checkAlbumsTableSchema(); @@ -3385,6 +3554,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`, " @@ -4039,6 +4231,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 " @@ -4416,6 +4633,24 @@ } } + { + auto selectRadioIdFromHttpAddress = QStringLiteral("SELECT " + "`ID` " + "FROM " + "`Radios` " + "WHERE " + "`HttpAddress` = :httpAddress"); + + auto result = prepareQuery(d->mSelectRadioIdFromHttpAddress, selectRadioIdFromHttpAddress); + + if (!result) { + qCInfo(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioIdFromHttpAddress.lastQuery(); + qCInfo(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectRadioIdFromHttpAddress.lastError(); + + Q_EMIT databaseError(); + } + } + { auto selectTracksMappingPriorityQueryText = QStringLiteral("SELECT " "max(tracks.`Priority`) AS Priority " @@ -4637,6 +4872,66 @@ } } + { + auto insertRadioQueryText = QStringLiteral("INSERT INTO `Radios` " + "(" + "`Title`, " + "`httpAddress`, " + "`Comment`, " + "`Rating`, " + "`Priority`) " + "VALUES " + "(" + ":title, " + ":httpAddress, " + ":comment, " + ":trackRating," + "1)"); + + auto result = prepareQuery(d->mInsertRadioQuery, insertRadioQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mInsertRadioQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mInsertRadioQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + auto deleteRadioQueryText = QStringLiteral("DELETE FROM `Radios` " + "WHERE `ID` = :radioId"); + + auto result = prepareQuery(d->mDeleteRadioQuery, deleteRadioQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mDeleteRadioQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mDeleteRadioQuery.lastError(); + + Q_EMIT databaseError(); + } + } + + { + auto updateRadioQueryText = QStringLiteral("UPDATE `Radios` " + "SET " + "`HttpAddress` = :httpAddress, " + "`Title` = :title, " + "`Comment` = :comment, " + "`Rating` = :trackRating " + "WHERE " + "`ID` = :radioId"); + + auto result = prepareQuery(d->mUpdateRadioQuery, updateRadioQueryText); + + if (!result) { + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mUpdateRadioQuery.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mUpdateRadioQuery.lastError(); + + Q_EMIT databaseError(); + } + } + { auto updateAlbumArtistQueryText = QStringLiteral("UPDATE `Albums` " "SET " @@ -5834,6 +6129,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; @@ -6221,6 +6538,60 @@ d->mUpdateTrackQuery.finish(); } +void DatabaseInterface::updateRadioInDatabase(const TrackDataType &oneTrack) +{ + QSqlQuery query = d->mUpdateRadioQuery; + bool isInsertion = false; + + if(oneTrack.databaseId() == -1ull){ + query = d->mInsertRadioQuery; + isInsertion = true; + } + + query.bindValue(QStringLiteral(":httpAddress"), oneTrack.httpAddress()); + query.bindValue(QStringLiteral(":radioId"), oneTrack.databaseId()); + query.bindValue(QStringLiteral(":title"), oneTrack.title()); + query.bindValue(QStringLiteral(":comment"), oneTrack.comment()); + query.bindValue(QStringLiteral(":trackRating"), oneTrack.rating()); + + auto result = execQuery(query); + + if (!result || !query.isActive()) { + Q_EMIT databaseError(); + + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.boundValues(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastError(); + }else{ + TrackDataType radio(oneTrack); + radio[TrackDataType::key_type::DatabaseIdRole] = internalRadioIdFromHttpAddress(oneTrack.httpAddress()); + Q_EMIT updateUIAfterRadioInsertOrUpdate(radio, isInsertion); + } + + query.finish(); +} + +void DatabaseInterface::deleteRadioInDatabase(qulonglong radioId) +{ + QSqlQuery query = d->mDeleteRadioQuery; + + query.bindValue(QStringLiteral(":radioId"), radioId); + + auto result = execQuery(query); + + if (!result || !query.isActive()) { + Q_EMIT databaseError(); + + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.boundValues(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::updateTrackInDatabase" << query.lastError(); + }else{ + Q_EMIT updateUIAfterRadioDeleted(radioId); + } + + query.finish(); +} + void DatabaseInterface::removeAlbumInDatabase(qulonglong albumId) { d->mRemoveAlbumQuery.bindValue(QStringLiteral(":albumId"), albumId); @@ -6572,6 +6943,42 @@ return result; } +qulonglong DatabaseInterface::internalRadioIdFromHttpAddress(const QString &httpAddress) +{ + auto result = qulonglong(0); + + if (!d) { + return result; + } + + d->mSelectRadioIdFromHttpAddress.bindValue(QStringLiteral(":httpAddress"), httpAddress); + + auto queryResult = execQuery(d->mSelectRadioIdFromHttpAddress); + + if (!queryResult || !d->mSelectRadioIdFromHttpAddress.isSelect() || !d->mSelectRadioIdFromHttpAddress.isActive()) { + Q_EMIT databaseError(); + + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalTrackIdFromFileName" << d->mSelectRadioIdFromHttpAddress.lastQuery(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalTrackIdFromFileName" << d->mSelectRadioIdFromHttpAddress.boundValues(); + qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalTrackIdFromFileName" << d->mSelectRadioIdFromHttpAddress.lastError(); + + d->mSelectRadioIdFromHttpAddress.finish(); + + return result; + } + + if (d->mSelectRadioIdFromHttpAddress.next()) { + const auto ¤tRecordValue = d->mSelectRadioIdFromHttpAddress.record().value(0); + if (currentRecordValue.isValid()) { + result = currentRecordValue.toULongLong(); + } + } + + d->mSelectRadioIdFromHttpAddress.finish(); + + return result; +} + DatabaseInterface::ListTrackDataType DatabaseInterface::internalTracksFromAuthor(const QString &ArtistName) { auto allTracks = ListTrackDataType{}; @@ -6747,6 +7154,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{}; @@ -6814,6 +7242,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/elisaqmlplugin.cpp b/src/elisaqmlplugin.cpp --- a/src/elisaqmlplugin.cpp +++ b/src/elisaqmlplugin.cpp @@ -47,6 +47,7 @@ #include "databaseinterface.h" #include "models/datamodel.h" #include "models/trackmetadatamodel.h" +#include "models/radiometadatamodel.h" #include "models/trackcontextmetadatamodel.h" #include "models/viewsmodel.h" #include "models/gridviewproxymodel.h" @@ -129,6 +130,7 @@ qmlRegisterType(uri, 1, 0, "GridViewProxyModel"); qmlRegisterType(uri, 1, 0, "AllTracksProxyModel"); qmlRegisterType(uri, 1, 0, "SingleAlbumProxyModel"); + qmlRegisterType(uri, 1, 0, "RadioMetadataModel"); #if defined KF5KIO_FOUND && KF5KIO_FOUND qmlRegisterType(uri, 1, 0, "FileBrowserModel"); @@ -172,6 +174,7 @@ qRegisterMetaType("EntryData"); qRegisterMetaType("EntryDataList"); qRegisterMetaType("DatabaseInterface::TrackDataType"); + qRegisterMetaType("TrackDataType"); qRegisterMetaType("DatabaseInterface::AlbumDataType"); qRegisterMetaType("DatabaseInterface::ArtistDataType"); qRegisterMetaType("DatabaseInterface::GenreDataType"); 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; } @@ -230,6 +232,32 @@ 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}); + + 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(); + } + + break; + } default: modelModified = false; } @@ -414,13 +442,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 +582,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 +652,7 @@ case ElisaUtils::Artist: case ElisaUtils::Genre: case ElisaUtils::Track: + case ElisaUtils::Radio: enqueueOneEntry(newEntry, databaseIdType); break; case ElisaUtils::FileName: @@ -657,7 +694,8 @@ switch (databaseIdType) { case ElisaUtils::Track: - enqueueTracksListById(newEntries); + case ElisaUtils::Radio: + enqueueTracksListById(newEntries, databaseIdType); break; case ElisaUtils::FileName: enqueueFilesList(newEntries); @@ -892,6 +930,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,12 +62,20 @@ void allTracksData(const ModelDataLoader::ListTrackDataType &allData); + void allRadiosData(const ModelDataLoader::ListTrackDataType &radiosData); + + void allRadiosDataUIUpdate(TrackDataType radiosData, bool isInsertion); + void allTrackData(const ModelDataLoader::TrackDataType &allData); + void allRadioData(const ModelDataLoader::TrackDataType &allData); + void tracksAdded(ModelDataLoader::ListTrackDataType newData); void trackModified(const ModelDataLoader::TrackDataType &modifiedTrack); + void radioModified(const ModelDataLoader::TrackDataType &modifiedTrack); + void trackRemoved(qulonglong removedTrackId); void genresAdded(ModelDataLoader::ListGenreDataType newData); @@ -82,6 +90,12 @@ void albumModified(const ModelDataLoader::AlbumDataType &modifiedAlbum); + void saveRadioModified(TrackDataType trackDataType); + + void deleteRadioInDatabase(qulonglong radioId); + + void radioRemoved(qulonglong radioId); + public Q_SLOTS: void loadData(ElisaUtils::PlayListEntryType dataType); @@ -107,6 +121,14 @@ void loadFrequentlyPlayedData(ElisaUtils::PlayListEntryType dataType); + void updateRadioData(TrackDataType trackDataType); + + void deleteRadioData(qulonglong radioId); + + void updateUIAfterRadioInsertOrUpdate(TrackDataType radio, bool isInsertion); + + void updateUIAfterRadioDeleted(qulonglong radioId); + private: void databaseTracksAdded(const ListTrackDataType &newData); diff --git a/src/modeldataloader.cpp b/src/modeldataloader.cpp --- a/src/modeldataloader.cpp +++ b/src/modeldataloader.cpp @@ -75,6 +75,14 @@ this, &ModelDataLoader::databaseArtistsAdded); connect(database, &DatabaseInterface::artistRemoved, this, &ModelDataLoader::databaseArtistRemoved); + connect(this, &ModelDataLoader::saveRadioModified, + database, &DatabaseInterface::updateRadioInDatabase); + connect(this, &ModelDataLoader::deleteRadioInDatabase, + database, &DatabaseInterface::deleteRadioInDatabase); + connect(database, &DatabaseInterface::updateUIAfterRadioInsertOrUpdate, + this, &ModelDataLoader::updateUIAfterRadioInsertOrUpdate); + connect(database, &DatabaseInterface::updateUIAfterRadioDeleted, + this, &ModelDataLoader::updateUIAfterRadioDeleted); } void ModelDataLoader::loadData(ElisaUtils::PlayListEntryType dataType) @@ -106,6 +114,9 @@ case ElisaUtils::FileName: case ElisaUtils::Unknown: break; + case ElisaUtils::Radio: + Q_EMIT allRadiosData(d->mDatabase->allRadiosData()); + break; } } @@ -135,6 +146,7 @@ break; case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -160,6 +172,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -185,6 +198,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -211,6 +225,7 @@ case ElisaUtils::Track: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -229,6 +244,9 @@ case ElisaUtils::Track: Q_EMIT allTrackData(d->mDatabase->trackDataFromDatabaseId(databaseId)); break; + case ElisaUtils::Radio: + Q_EMIT allRadioData(d->mDatabase->radioDataFromDatabaseId(databaseId)); + break; case ElisaUtils::Album: case ElisaUtils::Artist: case ElisaUtils::Composer: @@ -263,6 +281,7 @@ case ElisaUtils::Genre: case ElisaUtils::Lyricist: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -287,6 +306,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -311,6 +331,7 @@ case ElisaUtils::Lyricist: case ElisaUtils::FileName: case ElisaUtils::Unknown: + case ElisaUtils::Radio: break; } } @@ -439,5 +460,20 @@ Q_EMIT albumModified(modifiedAlbum); } +void ModelDataLoader::updateRadioData(TrackDataType trackDataType){ + Q_EMIT saveRadioModified(trackDataType); +} + +void ModelDataLoader::deleteRadioData(qulonglong radioId){ + Q_EMIT deleteRadioInDatabase(radioId); +} + +void ModelDataLoader::updateUIAfterRadioInsertOrUpdate(TrackDataType radio, bool isInsertion){ + Q_EMIT allRadiosDataUIUpdate(radio, isInsertion); +} + +void ModelDataLoader::updateUIAfterRadioDeleted(qulonglong radioId){ + Q_EMIT radioRemoved(radioId); +} #include "moc_modeldataloader.cpp" diff --git a/src/models/datamodel.h b/src/models/datamodel.h --- a/src/models/datamodel.h +++ b/src/models/datamodel.h @@ -128,10 +128,16 @@ void tracksAdded(DataModel::ListTrackDataType newData); + void radiosAdded(DataModel::ListTrackDataType newData); + void trackModified(const DataModel::TrackDataType &modifiedTrack); + void radioModified(const TrackDataType &modifiedRadio); + void trackRemoved(qulonglong removedTrackId); + void radioRemoved(qulonglong removedRadioId); + void genresAdded(DataModel::ListGenreDataType newData); void artistsAdded(DataModel::ListArtistDataType newData); @@ -166,14 +172,18 @@ void initializeFrequentlyPlayed(MusicListenersManager *manager, DatabaseInterface *database, ElisaUtils::PlayListEntryType modelType); + void radiosModifiedUI(const DataModel::TrackDataType radiosData, bool isInsertion); + private Q_SLOTS: void cleanedDatabase(); private: int trackIndexFromId(qulonglong id) const; + int radioIndexFromId(qulonglong id) const; + void connectModel(DatabaseInterface *database); void setBusy(bool value); @@ -183,6 +193,8 @@ void askModelData(); + void removeRadios(); + std::unique_ptr d; }; 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: @@ -395,12 +408,31 @@ return result; } +int DataModel::radioIndexFromId(qulonglong id) const +{ + int result; + + for (result = 0; result < d->mAllRadiosData.size(); ++result) { + if (d->mAllRadiosData[result].databaseId() == id) { + return result; + } + } + + result = -1; + + return result; +} + void DataModel::connectModel(DatabaseInterface *database) { d->mDataLoader.setDatabase(database); connect(&d->mDataLoader, &ModelDataLoader::allTracksData, this, &DataModel::tracksAdded); + connect(&d->mDataLoader, &ModelDataLoader::allRadiosData, + this, &DataModel::radiosAdded); + connect(&d->mDataLoader, &ModelDataLoader::allRadiosDataUIUpdate, + this, &DataModel::radiosModifiedUI); connect(&d->mDataLoader, &ModelDataLoader::allAlbumsData, this, &DataModel::albumsAdded); connect(&d->mDataLoader, &ModelDataLoader::allArtistsData, @@ -419,12 +451,16 @@ this, &DataModel::tracksAdded); connect(&d->mDataLoader, &ModelDataLoader::trackModified, this, &DataModel::trackModified); + connect(&d->mDataLoader, &ModelDataLoader::radioModified, + this, &DataModel::radioModified); connect(&d->mDataLoader, &ModelDataLoader::trackRemoved, this, &DataModel::trackRemoved); connect(&d->mDataLoader, &ModelDataLoader::artistsAdded, this, &DataModel::artistsAdded); connect(&d->mDataLoader, &ModelDataLoader::artistRemoved, this, &DataModel::artistRemoved); + connect(&d->mDataLoader, &ModelDataLoader::radioRemoved, + this, &DataModel::radioRemoved); } void DataModel::tracksAdded(ListTrackDataType newData) @@ -488,6 +524,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) { @@ -525,6 +622,22 @@ } } +void DataModel::radioModified(const TrackDataType &modifiedRadio) +{ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + auto trackIndex = radioIndexFromId(modifiedRadio.databaseId()); + + if (trackIndex == -1) { + return; + } + + d->mAllRadiosData[trackIndex] = modifiedRadio; + Q_EMIT dataChanged(index(trackIndex, 0), index(trackIndex, 0)); +} + void DataModel::trackRemoved(qulonglong removedTrackId) { if (d->mModelType != ElisaUtils::Track) { @@ -557,6 +670,52 @@ } } +void DataModel::radioRemoved(qulonglong removedRadioId) +{ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + + auto itRadio = std::find_if(d->mAllRadiosData.begin(), d->mAllRadiosData.end(), + [removedRadioId](auto track) {return track.databaseId() == removedRadioId;}); + + if (itRadio == d->mAllRadiosData.end()) { + return; + } + + auto position = itRadio - d->mAllRadiosData.begin(); + + beginRemoveRows({}, position, position); + d->mAllRadiosData.erase(itRadio); + endRemoveRows(); +} + +void DataModel::radiosModifiedUI(const DataModel::TrackDataType radioData, bool isInsertion){ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + if(isInsertion){ + ListTrackDataType list; + list.append(radioData); + radiosAdded(list); + }else{ + radioModified(radioData); + } +} + +void DataModel::removeRadios() +{ + if (d->mModelType != ElisaUtils::Radio) { + return; + } + + beginRemoveRows({}, 0, d->mAllRadiosData.size()); + d->mAllRadiosData.clear(); + endRemoveRows(); +} + void DataModel::genresAdded(DataModel::ListGenreDataType newData) { if (newData.isEmpty() && d->mModelType == ElisaUtils::Genre) { diff --git a/src/models/radiometadatamodel.h b/src/models/radiometadatamodel.h new file mode 100644 --- /dev/null +++ b/src/models/radiometadatamodel.h @@ -0,0 +1,165 @@ +#ifndef RADIOMETADATAMODEL_H +#define RADIOMETADATAMODEL_H + +#include "elisaLib_export.h" + +#include "elisautils.h" +#include "databaseinterface.h" +#include "modeldataloader.h" +#include "filescanner.h" + +#include +#include +#include +#include + +class MusicListenersManager; + +class ELISALIB_EXPORT RadioMetadataModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(MusicListenersManager* manager + READ manager + WRITE setManager + NOTIFY managerChanged) + + Q_PROPERTY(QString lyrics + READ lyrics + NOTIFY lyricsChanged) + +public: + + enum ColumnRoles + { + ItemNameRole = Qt::UserRole + 1, + ItemTypeRole, + }; + + enum ItemType + { + TextEntry, + IntegerEntry, + RatingEntry, + DateEntry, + LongTextEntry, + }; + + Q_ENUM(ItemType) + + using TrackDataType = DatabaseInterface::TrackDataType; + + explicit RadioMetadataModel(QObject *parent = nullptr); + + ~RadioMetadataModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) override; + + QHash roleNames() const override; + + const QUrl& coverUrl() const; + + QString fileUrl() const; + + MusicListenersManager* manager() const; + + QString lyrics() const; + +Q_SIGNALS: + + void needDataByDatabaseId(ElisaUtils::PlayListEntryType dataType, qulonglong databaseId); + + void needDataByFileName(ElisaUtils::PlayListEntryType dataType, const QUrl &fileName); + + void coverUrlChanged(); + + void fileUrlChanged(); + + void managerChanged(); + + void lyricsChanged(); + + void saveRadioData(TrackDataType trackDataType); + + void deleteRadioData(qulonglong radioId); + + void disableApplyButton(); + + void hideDeleteButton(); + + void showDeleteButton(); + + void closeWindow(); + +public Q_SLOTS: + + void trackData(const RadioMetadataModel::TrackDataType &trackData); + + void initializeByTrackId(qulonglong databaseId); + + void initializeByTrackFileName(const QUrl &fileName); + + void initializeForNewRadio(); + + void setManager(MusicListenersManager *newManager); + + void setDatabase(DatabaseInterface *trackDatabase); + + void saveData(); + + void deleteRadio(); + + void updateUI(TrackDataType radiosData, bool isInsertion); + + void radioRemoved(qulonglong radioId); + +protected: + + void fillDataFromTrackData(const RadioMetadataModel::TrackDataType &trackData); + + void fillDataForNewRadio(); + + virtual void filterDataFromTrackData(); + + void removeMetaData(DatabaseInterface::ColumnsRoles metaData); + + TrackDataType::mapped_type dataFromType(TrackDataType::key_type metaData) const; + + virtual void fillLyricsDataFromTrack(); + +private Q_SLOTS: + + void lyricsValueIsReady(); + +private: + + void initialize(MusicListenersManager *newManager, + DatabaseInterface *trackDatabase); + + void fetchLyrics(); + + TrackDataType mFullData; + + TrackDataType mTrackData; + + QList mTrackKeys; + + ModelDataLoader mDataLoader; + + MusicListenersManager *mManager = nullptr; + + FileScanner mFileScanner; + + QMimeDatabase mMimeDatabase; + + QFutureWatcher mLyricsValueWatcher; + +}; + +#endif // RADIOMETADATAMODEL_H + diff --git a/src/models/trackmetadatamodel.cpp b/src/models/radiometadatamodel.cpp copy from src/models/trackmetadatamodel.cpp copy to src/models/radiometadatamodel.cpp --- a/src/models/trackmetadatamodel.cpp +++ b/src/models/radiometadatamodel.cpp @@ -1,5 +1,6 @@ /* * Copyright 2018 Matthieu Gallien + * Copyright 2019 Jérôme Guidon * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -15,38 +16,38 @@ * along with this program. If not, see . */ -#include "trackmetadatamodel.h" +#include "radiometadatamodel.h" #include "musiclistenersmanager.h" #include #include -TrackMetadataModel::TrackMetadataModel(QObject *parent) +RadioMetadataModel::RadioMetadataModel(QObject *parent) : QAbstractListModel(parent) { connect(&mLyricsValueWatcher, &QFutureWatcher::finished, - this, &TrackMetadataModel::lyricsValueIsReady); + this, &RadioMetadataModel::lyricsValueIsReady); } -TrackMetadataModel::~TrackMetadataModel() +RadioMetadataModel::~RadioMetadataModel() { if (mLyricsValueWatcher.isRunning() && !mLyricsValueWatcher.isFinished()) { mLyricsValueWatcher.waitForFinished(); } } -int TrackMetadataModel::rowCount(const QModelIndex &parent) const +int RadioMetadataModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return mTrackData.count(); } -QVariant TrackMetadataModel::data(const QModelIndex &index, int role) const +QVariant RadioMetadataModel::data(const QModelIndex &index, int role) const { auto result = QVariant{}; @@ -63,6 +64,9 @@ case DatabaseInterface::TitleRole: result = i18nc("Track title for track metadata view", "Title"); break; + case DatabaseInterface::HttpAddressRole: + result = i18nc("Http Address for radio metadata view", "Http Address"); + break; case DatabaseInterface::DurationRole: result = i18nc("Duration label for track metadata view", "Duration"); break; @@ -146,6 +150,9 @@ case DatabaseInterface::TitleRole: result = TextEntry; break; + case DatabaseInterface::HttpAddressRole: + result = TextEntry; + break; case DatabaseInterface::ArtistRole: result = TextEntry; break; @@ -220,17 +227,21 @@ return result; } -bool TrackMetadataModel::setData(const QModelIndex &index, const QVariant &value, int role) +bool RadioMetadataModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (data(index, role) != value) { - // FIXME: Implement me! + const auto currentKey = mTrackKeys[index.row()]; + auto result = mTrackData[currentKey]; + + mTrackData[currentKey] = value; + emit dataChanged(index, index, QVector() << role); return true; } return false; } -QHash TrackMetadataModel::roleNames() const +QHash RadioMetadataModel::roleNames() const { auto names = QAbstractListModel::roleNames(); @@ -240,47 +251,38 @@ return names; } -QString TrackMetadataModel::fileUrl() const -{ - return mFileUrl; -} - -const QUrl &TrackMetadataModel::coverUrl() const -{ - return mCoverImage; -} - -MusicListenersManager *TrackMetadataModel::manager() const +MusicListenersManager *RadioMetadataModel::manager() const { return mManager; } -QString TrackMetadataModel::lyrics() const +QString RadioMetadataModel::lyrics() const { return mFullData[TrackDataType::key_type::LyricsRole].toString(); } -void TrackMetadataModel::trackData(const TrackMetadataModel::TrackDataType &trackData) +void RadioMetadataModel::trackData(const RadioMetadataModel::TrackDataType &trackData) { if (!mFullData.isEmpty() && trackData.databaseId() != mFullData.databaseId()) { return; } fillDataFromTrackData(trackData); } -void TrackMetadataModel::fillDataFromTrackData(const TrackMetadataModel::TrackDataType &trackData) +void RadioMetadataModel::fillDataFromTrackData(const RadioMetadataModel::TrackDataType &trackData) { beginResetModel(); mFullData = trackData; mTrackData.clear(); mTrackKeys.clear(); - for (auto role : {DatabaseInterface::TitleRole, DatabaseInterface::ArtistRole, DatabaseInterface::AlbumRole, - DatabaseInterface::AlbumArtistRole, DatabaseInterface::TrackNumberRole, DatabaseInterface::DiscNumberRole, - DatabaseInterface::RatingRole, DatabaseInterface::GenreRole, DatabaseInterface::LyricistRole, - DatabaseInterface::ComposerRole, DatabaseInterface::CommentRole, DatabaseInterface::YearRole, - DatabaseInterface::LastPlayDate, DatabaseInterface::PlayCounter}) { + for (auto role : { + DatabaseInterface::TitleRole, + DatabaseInterface::HttpAddressRole, + DatabaseInterface::CommentRole, + DatabaseInterface::DatabaseIdRole + }) { if (trackData.constFind(role) != trackData.constEnd()) { if (role == DatabaseInterface::RatingRole) { if (trackData[role].toInt() == 0) { @@ -294,27 +296,13 @@ } filterDataFromTrackData(); endResetModel(); - - fetchLyrics(); - - mCoverImage = trackData[DatabaseInterface::ImageUrlRole].toUrl(); - Q_EMIT coverUrlChanged(); - - auto rawFileUrl = trackData[DatabaseInterface::ResourceRole].toUrl(); - - if (rawFileUrl.isLocalFile()) { - mFileUrl = rawFileUrl.toLocalFile(); - } else { - mFileUrl = rawFileUrl.toString(); - } - Q_EMIT fileUrlChanged(); } -void TrackMetadataModel::filterDataFromTrackData() +void RadioMetadataModel::filterDataFromTrackData() { } -void TrackMetadataModel::removeMetaData(DatabaseInterface::ColumnsRoles metaData) +void RadioMetadataModel::removeMetaData(DatabaseInterface::ColumnsRoles metaData) { auto itMetaData = std::find(mTrackKeys.begin(), mTrackKeys.end(), metaData); if (itMetaData == mTrackKeys.end()) { @@ -325,20 +313,20 @@ mTrackData.remove(metaData); } -TrackMetadataModel::TrackDataType::mapped_type TrackMetadataModel::dataFromType(TrackDataType::key_type metaData) const +RadioMetadataModel::TrackDataType::mapped_type RadioMetadataModel::dataFromType(TrackDataType::key_type metaData) const { return mFullData[metaData]; } -void TrackMetadataModel::fillLyricsDataFromTrack() +void RadioMetadataModel::fillLyricsDataFromTrack() { beginInsertRows({}, mTrackData.size(), mTrackData.size()); mTrackKeys.push_back(DatabaseInterface::LyricsRole); mTrackData[DatabaseInterface::LyricsRole] = mLyricsValueWatcher.result(); endInsertRows(); } -void TrackMetadataModel::lyricsValueIsReady() +void RadioMetadataModel::lyricsValueIsReady() { if (!mLyricsValueWatcher.result().isEmpty()) { fillLyricsDataFromTrack(); @@ -349,7 +337,7 @@ } } -void TrackMetadataModel::initialize(MusicListenersManager *newManager, DatabaseInterface *trackDatabase) +void RadioMetadataModel::initialize(MusicListenersManager *newManager, DatabaseInterface *trackDatabase) { mManager = newManager; Q_EMIT managerChanged(); @@ -364,17 +352,25 @@ mManager->connectModel(&mDataLoader); } - connect(this, &TrackMetadataModel::needDataByDatabaseId, + connect(this, &RadioMetadataModel::needDataByDatabaseId, &mDataLoader, &ModelDataLoader::loadDataByDatabaseId); - connect(this, &TrackMetadataModel::needDataByFileName, + connect(this, &RadioMetadataModel::needDataByFileName, &mDataLoader, &ModelDataLoader::loadDataByFileName); - connect(&mDataLoader, &ModelDataLoader::allTrackData, - this, &TrackMetadataModel::trackData); + connect(&mDataLoader, &ModelDataLoader::allRadioData, + this, &RadioMetadataModel::trackData); connect(&mDataLoader, &ModelDataLoader::trackModified, - this, &TrackMetadataModel::trackData); + this, &RadioMetadataModel::trackData); + connect(this, &RadioMetadataModel::saveRadioData, + &mDataLoader, &ModelDataLoader::updateRadioData); + connect(this, &RadioMetadataModel::deleteRadioData, + &mDataLoader, &ModelDataLoader::deleteRadioData); + connect(&mDataLoader, &ModelDataLoader::allRadiosDataUIUpdate, + this, &RadioMetadataModel::updateUI); + connect(&mDataLoader, &ModelDataLoader::radioRemoved, + this, &RadioMetadataModel::radioRemoved); } -void TrackMetadataModel::fetchLyrics() +void RadioMetadataModel::fetchLyrics() { auto lyricicsValue = QtConcurrent::run(QThreadPool::globalInstance(), [=]() { auto trackData = mFileScanner.scanOneFile(mFullData[DatabaseInterface::ResourceRole].toUrl(), mMimeDatabase); @@ -387,39 +383,90 @@ mLyricsValueWatcher.setFuture(lyricicsValue); } -void TrackMetadataModel::initializeByTrackId(qulonglong databaseId) +void RadioMetadataModel::initializeByTrackId(qulonglong databaseId) { mFullData.clear(); mTrackData.clear(); - mCoverImage.clear(); - mFileUrl.clear(); Q_EMIT lyricsChanged(); - Q_EMIT needDataByDatabaseId(ElisaUtils::Track, databaseId); + Q_EMIT needDataByDatabaseId(ElisaUtils::Radio, databaseId); +} + +void RadioMetadataModel::initializeForNewRadio() +{ + mFullData.clear(); + mTrackData.clear(); + + fillDataForNewRadio(); } -void TrackMetadataModel::initializeByTrackFileName(const QUrl &fileName) +void RadioMetadataModel::fillDataForNewRadio() +{ + beginResetModel(); + mTrackData.clear(); + mTrackKeys.clear(); + + for (auto role : { + DatabaseInterface::TitleRole, + DatabaseInterface::HttpAddressRole, + DatabaseInterface::CommentRole, + DatabaseInterface::DatabaseIdRole + + }) { + mTrackKeys.push_back(role); + if(role == DatabaseInterface::DatabaseIdRole){ + mTrackData[role] = -1; + Q_EMIT hideDeleteButton(); + }else{ + mTrackData[role] = QStringLiteral(""); + } + + } + filterDataFromTrackData(); + endResetModel(); +} + +void RadioMetadataModel::initializeByTrackFileName(const QUrl &fileName) { mFullData.clear(); mTrackData.clear(); - mCoverImage.clear(); - mFileUrl.clear(); Q_EMIT lyricsChanged(); Q_EMIT needDataByFileName(ElisaUtils::FileName, fileName); } -void TrackMetadataModel::setManager(MusicListenersManager *newManager) +void RadioMetadataModel::setManager(MusicListenersManager *newManager) { initialize(newManager, nullptr); } -void TrackMetadataModel::setDatabase(DatabaseInterface *trackDatabase) +void RadioMetadataModel::setDatabase(DatabaseInterface *trackDatabase) { initialize(nullptr, trackDatabase); } +void RadioMetadataModel::saveData() +{ + Q_EMIT saveRadioData(mTrackData); +} -#include "moc_trackmetadatamodel.cpp" +void RadioMetadataModel::deleteRadio() +{ + if(mTrackData[DatabaseInterface::DatabaseIdRole]>=0){ + Q_EMIT deleteRadioData(mTrackData[DatabaseInterface::DatabaseIdRole].toULongLong()); + } +} + +void RadioMetadataModel::updateUI(TrackDataType radiosData, bool isInsertion){ + if(isInsertion){ + mTrackData[DatabaseInterface::DatabaseIdRole] = radiosData[DatabaseInterface::DatabaseIdRole]; + Q_EMIT showDeleteButton(); + } + Q_EMIT disableApplyButton(); +} + +void RadioMetadataModel::radioRemoved(qulonglong radioId){ + Q_EMIT closeWindow(); +} diff --git a/src/models/trackmetadatamodel.cpp b/src/models/trackmetadatamodel.cpp --- a/src/models/trackmetadatamodel.cpp +++ b/src/models/trackmetadatamodel.cpp @@ -117,6 +117,9 @@ case DatabaseInterface::LyricsRole: result = i18nc("Lyrics label for track metadata view", "Lyrics"); break; + case DatabaseInterface::HttpAddressRole: + result = i18nc("Radio HTTP address for radio metadata view", "HttpAddress"); + break; case DatabaseInterface::SecondaryTextRole: case DatabaseInterface::ImageUrlRole: case DatabaseInterface::ShadowForImageRole: 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() } @@ -556,6 +573,14 @@ } } + Component { + id: radiosView + + RadiosView { + StackView.onActivated: viewManager.viewIsLoaded(viewType) + } + } + Component { id: filesBrowserView diff --git a/src/qml/ListBrowserView.qml b/src/qml/ListBrowserView.qml --- a/src/qml/ListBrowserView.qml +++ b/src/qml/ListBrowserView.qml @@ -41,6 +41,9 @@ property alias currentIndex: contentDirectoryView.currentIndex property alias enableSorting: navigationBar.enableSorting property var stackView + property bool showEnqueueButton: true + property bool showCreateRadioButton: false + property alias navigationBar: navigationBar signal goBack() signal showArtist(var name) @@ -70,6 +73,9 @@ Layout.maximumHeight: height Layout.fillWidth: true + showEnqueueButton: listView.showEnqueueButton + showCreateRadioButton: listView.showCreateRadioButton + Binding { target: contentModel property: 'filterText' 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,423 @@ +/* + * Copyright 2016-2017 Matthieu Gallien + * Copyright 2017 Alexander Stippich + * Copyright 2019 Jérôme Guidon + * + * 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 + property NavigationActionBar navigationBar + + 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: MediaRadioMetadataView { + 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: 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: 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/MediaRadioMetadataView.qml b/src/qml/MediaRadioMetadataView.qml new file mode 100644 --- /dev/null +++ b/src/qml/MediaRadioMetadataView.qml @@ -0,0 +1,172 @@ +/* + * Copyright 2017 Alexander Stippich + * Copyright 2018 Matthieu Gallien + * Copyright 2019 Jérôme Guidon + * + * 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.Controls 2.2 +import QtQuick.Window 2.2 +import QtQml.Models 2.2 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import org.kde.elisa 1.0 + +Window { + id: trackMetadata + + property int databaseId: 0 + property url fileName + + signal rejected() + + LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + title: i18nc("Window title for track metadata", "View Details") + + RadioMetadataModel { + id: realModel + + manager: elisa.musicManager + } + + modality: Qt.NonModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + color: myPalette.window + + minimumHeight: elisaTheme.coverImageSize * 1.8 + minimumWidth: elisaTheme.coverImageSize * 5 + + ColumnLayout { + anchors.fill: parent + anchors.margins: elisaTheme.layoutVerticalMargin + + spacing: elisaTheme.layoutVerticalMargin + + RowLayout { + id: metadataView + + Layout.fillHeight: true + Layout.fillWidth: true + + spacing: 0 + + ListView { + id: trackData + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 2 * elisaTheme.layoutHorizontalMargin + + focus: true + + ScrollBar.vertical: ScrollBar { + id: scrollBar + } + boundsBehavior: Flickable.StopAtBounds + clip: true + + ScrollHelper { + id: scrollHelper + flickable: trackData + anchors.fill: trackData + } + model: realModel + + delegate: MetaRadioDataDelegate { + width: scrollBar.visible ? (!LayoutMirroring.enabled ? trackData.width - scrollBar.width : trackData.width) : trackData.width + onRadioEdited: applyButton.enabled = true + } + } + } + + RowLayout{ + spacing: elisaTheme.layoutVerticalMargin + + DialogButtonBox { + Layout.minimumHeight: implicitHeight + alignment: Qt.AlignLeft + + Button { + id: deleteButton + text: qsTr("Delete") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: realModel.deleteRadio() + } + } + + DialogButtonBox { + id: buttons + + Layout.fillWidth: true + Layout.minimumHeight: implicitHeight + alignment: Qt.AlignRight + + Button { + id: applyButton + + text: qsTr("Apply") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + onClicked: realModel.saveData() + enabled: false + } + Button { + text: qsTr("Close") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: trackMetadata.rejected() + } + } + } + } + + Connections { + target: elisa + + onMusicManagerChanged: { + if (databaseId == -1) { + realModel.initializeForNewRadio() + } else if (databaseId !== 0) { + realModel.initializeByTrackId(databaseId) + } + else { + realModel.initializeByTrackFileName(fileName) + } + } + } + + Connections{ + target: realModel + + onDisableApplyButton: applyButton.enabled = false + onShowDeleteButton: deleteButton.visible = true + onHideDeleteButton: deleteButton.visible = false + onCloseWindow: trackMetadata.rejected() + } + + Component.onCompleted: { + if (elisa.musicManager) { + if (databaseId == -1) { + realModel.initializeForNewRadio() + } else if (databaseId !== 0) { + realModel.initializeByTrackId(databaseId) + } else { + realModel.initializeByTrackFileName(fileName) + } + } + } +} diff --git a/src/qml/MetaRadioDataDelegate.qml b/src/qml/MetaRadioDataDelegate.qml new file mode 100644 --- /dev/null +++ b/src/qml/MetaRadioDataDelegate.qml @@ -0,0 +1,140 @@ +/* + * Copyright 2016 Matthieu Gallien + * Copyright 2019 Jérôme Guidon + * + * 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.2 +import QtQuick.Layouts 1.2 + +import org.kde.elisa 1.0 + +RowLayout { + id: delegateRow + spacing: 0 + + signal radioEdited() + + TextMetrics { + id: metaDataLabelMetric + + text: 'Metadata Name' + + font.weight: Font.Bold + } + + Label { + id: metaDataLabels + + text: model.name + + font.weight: Font.Bold + + horizontalAlignment: Text.AlignLeft + + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: 0.8 * elisaTheme.coverImageSize + Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin * 2 : 0 + Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin * 2 : 0 + } + + Loader { + id: textDisplayLoader + + active: model.type === RadioMetadataModel.TextEntry || model.type === RadioMetadataModel.IntegerEntry + visible: model.type === RadioMetadataModel.TextEntry || model.type === RadioMetadataModel.IntegerEntry + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + sourceComponent: TextField { + text: model.display + + horizontalAlignment: Text.AlignLeft + + anchors.fill: parent + + onEditingFinished: { + if(model.display !== text){ + model.display = text + delegateRow.radioEdited() + } + } + } + } + + Loader { + id: longTextDisplayLoader + + active: model.type === RadioMetadataModel.LongTextEntry + visible: model.type === RadioMetadataModel.LongTextEntry + + Layout.fillWidth: true + Layout.maximumWidth: delegateRow.width - (0.8 * elisaTheme.coverImageSize + elisaTheme.layoutHorizontalMargin * 2) + Layout.alignment: Qt.AlignTop + + sourceComponent: Label { + text: model.display + + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + + anchors.fill: parent + + wrapMode: Text.WordWrap + } + } + + Loader { + active: model.type === RadioMetadataModel.DateEntry + visible: model.type === RadioMetadataModel.DateEntry + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + sourceComponent: LabelWithToolTip { + text: rawDate.toLocaleDateString() + + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + + anchors.fill: parent + + property date rawDate: new Date(model.display) + } + } + + Loader { + active: model.type === RadioMetadataModel.RatingEntry + visible: model.type === RadioMetadataModel.RatingEntry + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + sourceComponent: RatingStar { + starRating: model.display + starSize: elisaTheme.ratingStarSize + + readOnly: true + + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + } + } +} diff --git a/src/qml/NavigationActionBar.qml b/src/qml/NavigationActionBar.qml --- a/src/qml/NavigationActionBar.qml +++ b/src/qml/NavigationActionBar.qml @@ -33,6 +33,8 @@ property string secondaryTitle property url image property bool allowArtistNavigation: false + property bool showEnqueueButton: true + property bool showCreateRadioButton: false property string labelText property bool showRating: true @@ -46,6 +48,7 @@ signal enqueue(); signal replaceAndPlay(); + signal createRadio(); signal goBack(); signal showArtist(); signal sort(var order); @@ -193,6 +196,8 @@ Layout.leftMargin: 0 Layout.rightMargin: 0 + + visible: navigationBar.showEnqueueButton } Controls1.Button { @@ -208,6 +213,25 @@ Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + + visible: navigationBar.showEnqueueButton + } + + Controls1.Button { + objectName: 'createRadioButton' + text: i18nc("Create a new radio", "Create a radio") + tooltip: i18nc("Create a new radio", "Create a new radio") + iconName: "media-track-add-amarok" + + activeFocusOnTab: true + + onClicked: createRadio() + Keys.onReturnPressed: createRadio() + + Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 + + visible: navigationBar.showCreateRadioButton } Controls1.Button { diff --git a/src/qml/PlayListBasicView.qml b/src/qml/PlayListBasicView.qml --- a/src/qml/PlayListBasicView.qml +++ b/src/qml/PlayListBasicView.qml @@ -116,18 +116,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,142 @@ +/* + * Copyright 2018 Matthieu Gallien + * Copyright 2019 Jérôme Guidon + * + * 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) + } + + Action { + id: viewDetailsAction + text: i18nc("Create a new radio", "Create Radio") + onTriggered: { + if (metadataLoader.active === false) { + metadataLoader.active = true + } + else { + metadataLoader.item.close(); + metadataLoader.active = false + } + } + } + + Loader { + id: metadataLoader + active: false + onLoaded: item.show() + + sourceComponent: MediaRadioMetadataView { + databaseId: -1 + onRejected: metadataLoader.active = false; + } + } + + ListBrowserView { + id: listView + + focus: true + activeFocusOnTab: true + + anchors.fill: parent + + contentModel: proxyModel + + showEnqueueButton: false + showCreateRadioButton:true + + delegate: MediaRadioDelegate { + id: entry + + width: listView.delegateWidth + height: elisaTheme.trackDelegateHeight + + focus: true + + databaseId: model.databaseId + title: model.title + httpAddress: model.httpAddress + isFirstTrackOfDisc: false + + 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) + } + + Connections { + target: listView.navigationBar + + onCreateRadio: { + viewDetailsAction.trigger() + } + } + + 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 @@ -45,6 +45,10 @@ qml/MetaDataDelegate.qml qml/ContextViewLyrics.qml qml/ViewSelectorDelegate.qml + qml/RadiosView.qml + qml/MediaRadioDelegate.qml + qml/MediaRadioMetadataView.qml + qml/MetaRadioDataDelegate.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 newRadio = d->mDatabase->radioDataFromDatabaseId(newDatabaseId); + if (!newRadio.isEmpty()) { + Q_EMIT trackHasChanged(newRadio); + } + 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();