diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 918a3731..e76d1c13 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,493 +1,494 @@ include_directories(${elisa_BINARY_DIR}) set(elisaLib_SOURCES mediaplaylist.cpp musicaudiotrack.cpp progressindicator.cpp databaseinterface.cpp musiclistenersmanager.cpp managemediaplayercontrol.cpp manageheaderbar.cpp manageaudioplayer.cpp trackslistener.cpp elisaapplication.cpp modeldataloader.cpp notificationitem.cpp topnotificationmanager.cpp elisautils.cpp datatype.cpp abstractfile/abstractfilelistener.cpp abstractfile/abstractfilelisting.cpp filescanner.cpp viewmanager.cpp powermanagementinterface.cpp file/filelistener.cpp file/localfilelisting.cpp models/datamodel.cpp models/abstractmediaproxymodel.cpp models/gridviewproxymodel.cpp models/alltracksproxymodel.cpp models/singlealbumproxymodel.cpp models/trackmetadatamodel.cpp models/trackcontextmetadatamodel.cpp models/viewsmodel.cpp ) ecm_qt_declare_logging_category(elisaLib_SOURCES HEADER "indexersManager.h" IDENTIFIER "orgKdeElisaIndexersManager" CATEGORY_NAME "org.kde.elisa.indexers.manager" DEFAULT_SEVERITY Info ) ecm_qt_declare_logging_category(elisaLib_SOURCES HEADER "databaseLogging.h" IDENTIFIER "orgKdeElisaDatabase" CATEGORY_NAME "org.kde.elisa.database" DEFAULT_SEVERITY Info ) ecm_qt_declare_logging_category(elisaLib_SOURCES HEADER "abstractfile/indexercommon.h" IDENTIFIER "orgKdeElisaIndexer" CATEGORY_NAME "org.kde.elisa.indexer" DEFAULT_SEVERITY Info ) if (LIBVLC_FOUND) ecm_qt_declare_logging_category(elisaLib_SOURCES HEADER "vlcLogging.h" IDENTIFIER "orgKdeElisaPlayerVlc" CATEGORY_NAME "org.kde.elisa.player.vlc" DEFAULT_SEVERITY Info ) set(elisaLib_SOURCES ${elisaLib_SOURCES} audiowrapper_libvlc.cpp ) else() ecm_qt_declare_logging_category(elisaLib_SOURCES HEADER "qtMultimediaLogging.h" IDENTIFIER "orgKdeElisaPlayerQtMultimedia" CATEGORY_NAME "org.kde.elisa.player.qtMultimedia" DEFAULT_SEVERITY Info ) set(elisaLib_SOURCES ${elisaLib_SOURCES} audiowrapper_qtmultimedia.cpp ) endif() if (ANDROID) set(elisaLib_SOURCES ${elisaLib_SOURCES} android/androidmusiclistener.cpp ) endif() if (KF5KIO_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} models/filebrowsermodel.cpp models/filebrowserproxymodel.cpp ) endif() if (KF5Baloo_FOUND) if (Qt5DBus_FOUND) ecm_qt_declare_logging_category(elisaLib_SOURCES HEADER "baloo/baloocommon.h" IDENTIFIER "orgKdeElisaBaloo" CATEGORY_NAME "org.kde.elisa.baloo" DEFAULT_SEVERITY Info ) set(elisaLib_SOURCES ${elisaLib_SOURCES} baloo/localbaloofilelisting.cpp baloo/baloolistener.cpp baloo/baloodetector.cpp ) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.main.xml baloo/main) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.fileindexer.xml baloo/fileindexer) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.scheduler.xml baloo/scheduler) qt5_add_dbus_adaptor(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.BalooWatcherApplication.xml baloo/localbaloofilelisting.h LocalBalooFileListing) endif() endif() if (Qt5DBus_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} mpris2/mpris2.cpp mpris2/mediaplayer2.cpp mpris2/mediaplayer2player.cpp ) endif() if (UPNPQT_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} upnp/upnpcontrolcontentdirectory.cpp upnp/upnpcontentdirectorymodel.cpp upnp/upnpcontrolconnectionmanager.cpp upnp/upnpcontrolmediaserver.cpp upnp/didlparser.cpp upnp/upnplistener.cpp upnp/upnpdiscoverallmusic.cpp ) endif() if (KF5Baloo_FOUND) if (Qt5DBus_FOUND) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.fileindexer.xml baloo/fileindexer) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.scheduler.xml baloo/scheduler) set(elisaLib_SOURCES ${elisaLib_SOURCES} ../src/baloo/baloolistener.cpp ../src/baloo/localbaloofilelisting.cpp ) endif() endif() kconfig_add_kcfg_files(elisaLib_SOURCES ../src/elisa_settings.kcfgc ) set(elisaLib_SOURCES ${elisaLib_SOURCES} ../src/elisa_core.kcfg ) add_library(elisaLib ${elisaLib_SOURCES}) target_link_libraries(elisaLib LINK_PUBLIC Qt5::Multimedia LINK_PRIVATE Qt5::Core Qt5::Sql Qt5::Widgets Qt5::Concurrent Qt5::Qml KF5::I18n KF5::CoreAddons KF5::ConfigCore KF5::ConfigGui) if (KF5FileMetaData_FOUND) target_link_libraries(elisaLib LINK_PRIVATE KF5::FileMetaData ) endif() if (KF5KIO_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets ) endif() if (KF5XmlGui_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::XmlGui ) endif() if (KF5ConfigWidgets_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::ConfigWidgets ) endif() if (KF5KCMUtils_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::KCMUtils ) endif() if (KF5Baloo_FOUND) if (Qt5DBus_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::Baloo ) endif() endif() if (Qt5DBus_FOUND) target_link_libraries(elisaLib LINK_PUBLIC Qt5::DBus ) if (KF5DBusAddons_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::DBusAddons ) endif() endif() if (LIBVLC_FOUND) target_include_directories(elisaLib PRIVATE ${LIBVLC_INCLUDE_DIR} ) target_link_libraries(elisaLib LINK_PRIVATE ${LIBVLC_LIBRARY} ) endif() if (ANDROID) target_link_libraries(elisaLib LINK_PUBLIC Qt5::AndroidExtras ) endif() generate_export_header(elisaLib BASE_NAME ElisaLib EXPORT_FILE_NAME elisaLib_export.h) set_target_properties(elisaLib PROPERTIES VERSION 0.1 SOVERSION 0 EXPORT_NAME ElisaLib ) if (NOT APPLE AND NOT WIN32) install(TARGETS elisaLib LIBRARY DESTINATION ${KDE_INSTALL_FULL_LIBDIR}/elisa NAMELINK_SKIP RUNTIME DESTINATION ${KDE_INSTALL_FULL_LIBDIR}/elisa BUNDLE DESTINATION ${KDE_INSTALL_FULL_LIBDIR}/elisa ) else() install(TARGETS elisaLib ${INSTALL_TARGETS_DEFAULT_ARGS}) endif() set(elisaqmlplugin_SOURCES elisaqmlplugin.cpp datatype.cpp elisautils.cpp ) if (KF5FileMetaData_FOUND) set(elisaqmlplugin_SOURCES ${elisaqmlplugin_SOURCES} embeddedcoverageimageprovider.cpp ) endif() add_library(elisaqmlplugin SHARED ${elisaqmlplugin_SOURCES}) target_link_libraries(elisaqmlplugin LINK_PRIVATE Qt5::Quick Qt5::Widgets KF5::ConfigCore KF5::ConfigGui elisaLib ) if (KF5FileMetaData_FOUND) target_link_libraries(elisaqmlplugin LINK_PRIVATE KF5::FileMetaData ) endif() set_target_properties(elisaqmlplugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/org/kde/elisa ) if (NOT APPLE AND NOT WIN32) set_target_properties(elisaqmlplugin PROPERTIES INSTALL_RPATH "${KDE_INSTALL_FULL_LIBDIR}/elisa;${CMAKE_INSTALL_RPATH}" ) endif() install(TARGETS elisaqmlplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/elisa/) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/elisa) add_custom_target(copy) add_custom_target(copy2) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin/org/kde/elisa) add_custom_command(TARGET copy PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/qmldir ${CMAKE_BINARY_DIR}/bin/org/kde/elisa/) add_custom_command(TARGET copy2 PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/plugins.qmltypes ${CMAKE_BINARY_DIR}/bin/org/kde/elisa/) add_dependencies(elisaqmlplugin copy copy2) if (Qt5Quick_FOUND AND Qt5Widgets_FOUND) set(elisa_SOURCES main.cpp windows/WindowsTheme.qml windows/PlatformIntegration.qml android/ElisaMainWindow.qml android/AndroidTheme.qml android/PlatformIntegration.qml android/AlbumsView.qml android/ArtistsView.qml android/TracksView.qml android/GenresView.qml qml/ElisaMainWindow.qml qml/ApplicationMenu.qml qml/BaseTheme.qml qml/Theme.qml qml/PlatformIntegration.qml qml/LabelWithToolTip.qml qml/RatingStar.qml qml/DraggableItem.qml qml/PassiveNotification.qml qml/TopNotification.qml qml/TopNotificationItem.qml qml/TrackImportNotification.qml qml/HeaderBar.qml qml/NavigationActionBar.qml qml/MediaPlayerControl.qml qml/ContextView.qml qml/ContextViewLyrics.qml qml/ContentView.qml qml/ViewSelector.qml qml/ViewSelectorDelegate.qml qml/DataGridView.qml qml/TracksView.qml qml/AlbumView.qml qml/RecentlyPlayedTracks.qml qml/FrequentlyPlayedTracks.qml qml/MediaPlayListView.qml qml/PlayListBasicView.qml qml/PlayListEntry.qml qml/SimplePlayListView.qml qml/SimplePlayListEntry.qml qml/PlayListAlbumHeader.qml qml/BasicPlayListAlbumHeader.qml qml/MetaDataDelegate.qml qml/MediaTrackDelegate.qml qml/MediaAlbumTrackDelegate.qml qml/MediaTrackMetadataView.qml qml/GridBrowserView.qml qml/GridBrowserDelegate.qml qml/ListBrowserView.qml qml/FileBrowserDelegate.qml qml/FileBrowserView.qml qml/ScrollHelper.qml qml/FlatButtonWithToolTip.qml + qml/HeaderFooterToolbar.qml ) qt5_add_resources(elisa_SOURCES resources.qrc) set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON) set(elisa_ICONS_PNG ../icons/128-apps-elisa.png ../icons/64-apps-elisa.png ../icons/48-apps-elisa.png ../icons/32-apps-elisa.png ../icons/22-apps-elisa.png ../icons/16-apps-elisa.png ) # add icons to application sources, to have them bundled ecm_add_app_icon(elisa_SOURCES ICONS ${elisa_ICONS_PNG}) add_executable(elisa ${elisa_SOURCES}) target_include_directories(elisa PRIVATE ${KDSoap_INCLUDE_DIRS}) target_link_libraries(elisa LINK_PRIVATE elisaLib Qt5::Widgets Qt5::QuickControls2 KF5::I18n KF5::CoreAddons KF5::ConfigCore KF5::ConfigGui ) if (ANDROID) target_link_libraries(elisa LINK_PRIVATE Qt5::AndroidExtras Qt5::Svg Qt5::Sql Qt5::Concurrent KF5::Kirigami2 ) endif() if (KF5Crash_FOUND) target_link_libraries(elisa LINK_PRIVATE KF5::Crash ) endif() if (KF5Declarative_FOUND) target_link_libraries(elisa LINK_PRIVATE KF5::Declarative ) endif() if (NOT APPLE AND NOT WIN32) set_target_properties(elisa PROPERTIES INSTALL_RPATH "${KDE_INSTALL_FULL_LIBDIR}/elisa;${CMAKE_INSTALL_RPATH}" ) endif() install(TARGETS elisa ${INSTALL_TARGETS_DEFAULT_ARGS}) endif() if (KF5ConfigWidgets_FOUND AND KF5Declarative_FOUND) add_subdirectory(localFileConfiguration) endif() set(elisaImport_SOURCES elisaimport.cpp elisaimportapplication.cpp ) kconfig_add_kcfg_files(elisaImport_SOURCES ../src/elisa_settings.kcfgc ) set(elisaImport_SOURCES ${elisaImport_SOURCES} ../src/elisa_core.kcfg ) add_executable(elisaImport ${elisaImport_SOURCES}) target_link_libraries(elisaImport LINK_PRIVATE KF5::ConfigCore KF5::ConfigGui elisaLib ) if (KF5FileMetaData_FOUND) target_link_libraries(elisaImport LINK_PRIVATE KF5::FileMetaData ) endif() set(QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/bin CACHE INTERNAL "qml import path" FORCE) if (ANDROID) kirigami_package_breeze_icons(ICONS elisa) endif() diff --git a/src/qml/BaseTheme.qml b/src/qml/BaseTheme.qml index 55217893..b7f41c6c 100644 --- a/src/qml/BaseTheme.qml +++ b/src/qml/BaseTheme.qml @@ -1,114 +1,116 @@ /* * Copyright 2017 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.7 import QtQuick.Controls 2.2 Item { function dp(pixel) { // 96 - common, "base" DPI value return Math.round(pixel * logicalDpi / 96); } property string defaultAlbumImage: 'image://icon/media-optical-audio' property string defaultArtistImage: 'image://icon/view-media-artist' property string defaultBackgroundImage: 'qrc:///background.png' property string artistIcon: 'image://icon/view-media-artist' property string albumIcon: 'image://icon/view-media-album-cover' property string albumCoverIcon: 'image://icon/media-optical-audio' property string playlistIcon: 'image://icon/view-media-playlist' property string tracksIcon: 'image://icon/view-media-track' property string genresIcon: 'image://icon/view-media-genre' property string clearIcon: 'image://icon/edit-clear' property string recentlyPlayedTracksIcon: 'image://icon/media-playlist-play' property string frequentlyPlayedTracksIcon: 'image://icon/amarok_playcount' property string pausedIndicatorIcon: 'image://icon/media-playback-paused' property string playingIndicatorIcon: 'image://icon/media-playback-playing' property string ratingIcon: 'image://icon/rating' property string ratingUnratedIcon: 'image://icon/rating-unrated' property string errorIcon: 'image://icon/error' property string folderIcon: 'image://icon/document-open-folder' property int layoutHorizontalMargin: dp(8) property int layoutVerticalMargin: dp(6) property int delegateHeight: dp(28) FontMetrics { id: playListAuthorTextHeight font.weight: Font.Light } FontMetrics { id: playListAlbumTextHeight font.weight: Font.Bold font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) } FontMetrics { id: playListTrackTextHeight font.weight: Font.Bold } property int playListDelegateHeight: (playListTrackTextHeight.height > dp(28)) ? playListTrackTextHeight.height : dp(28) property int playListHeaderHeight: elisaTheme.layoutVerticalMargin * 5 + playListAuthorTextHeight.height + playListAlbumTextHeight.height property int trackDelegateHeight: dp(45) property int coverImageSize: dp(180) property int contextCoverImageSize: dp(100) property int smallImageSize: dp(32) property int maximumMetadataWidth: dp(300) property int tooltipRadius: dp(3) property int shadowOffset: dp(2) property int delegateToolButtonSize: dp(34) property int smallDelegateToolButtonSize: dp(20) property int ratingStarSize: dp(15) property int mediaPlayerControlHeight: dp(42) property int mediaPlayerHorizontalMargin: dp(10) property real mediaPlayerControlOpacity: 0.6 property int smallControlButtonSize: dp(22) property int volumeSliderWidth: dp(100) property int dragDropPlaceholderHeight: dp(28) property int navigationBarHeight: dp(100) property int navigationBarFilterHeight: dp(44) property int gridDelegateHeight: dp(170) + layoutVerticalMargin + fontSize.height * 2 property int gridDelegateWidth: dp(170) property int viewSelectorDelegateHeight: dp(24) property int filterClearButtonMargin: layoutVerticalMargin property alias defaultFontPointSize: fontSize.font.pointSize + property int headerTitleFontSize: defaultFontPointSize * 2 + property int viewSelectorSmallSizeThreshold: 800 Label { id: fontSize } } diff --git a/src/qml/ContentView.qml b/src/qml/ContentView.qml index 35e57b39..9d4976da 100644 --- a/src/qml/ContentView.qml +++ b/src/qml/ContentView.qml @@ -1,514 +1,512 @@ /* * Copyright 2016-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.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 import org.kde.elisa 1.0 RowLayout { id: contentViewContainer spacing: 0 property bool showPlaylist property alias currentViewIndex: listViews.currentIndex signal toggleSearch() function goBack() { viewManager.goBack() } function openArtist(name) { viewManager.openChildView(name, '', elisaTheme.artistIcon, 0, ElisaUtils.Artist) } function openAlbum(album, artist, image, albumID) { image = !image ? elisaTheme.defaultAlbumImage : image; viewManager.openChildView(album, artist, image, albumID, ElisaUtils.Album); } function openNowPlaying() { viewManager.closeAllViews(); } ViewManager { id: viewManager onSwitchOffAllViews: { listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) while(browseStackView.depth > 1) { browseStackView.pop() } } onSwitchRecentlyPlayedTracksView: { listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) while(browseStackView.depth > expectedDepth) { browseStackView.pop() } browseStackView.push(allRecentlyPlayedTracksView, { viewType: viewType, mainTitle: mainTitle, image: imageUrl, modelType: dataType, stackView: browseStackView, opacity: 0, }) } onSwitchFrequentlyPlayedTracksView: { listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) while(browseStackView.depth > expectedDepth) { browseStackView.pop() } browseStackView.push(allFrequentlyPlayedTracksView, { viewType: viewType, mainTitle: mainTitle, image: imageUrl, modelType: dataType, stackView: browseStackView, opacity: 0, }) } onOpenGridView: { if (expectedDepth === 1) { listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) } while(browseStackView.depth > expectedDepth) { browseStackView.pop() } browseStackView.push(dataGridView, { viewType: viewType, mainTitle: pageModel.viewMainTitle(viewType, mainTitle), secondaryTitle: secondaryTitle, image: pageModel.viewImageUrl(viewType, imageUrl), modelType: dataType, defaultIcon: viewDefaultIcon, showRating: viewShowRating, delegateDisplaySecondaryText: viewDelegateDisplaySecondaryText, genreFilterText: genreNameFilter, artistFilter: artistNameFilter, isSubPage: (browseStackView.depth >= 2), stackView: browseStackView, opacity: 0, }) } onSwitchOneAlbumView: { while(browseStackView.depth > expectedDepth) { browseStackView.pop() } browseStackView.push(albumView, { viewType: viewType, mainTitle: mainTitle, secondaryTitle: secondaryTitle, image: imageUrl, databaseId: databaseId, stackView: browseStackView, opacity: 0, }) } onSwitchAllTracksView: { listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) while(browseStackView.depth > expectedDepth) { browseStackView.pop() } browseStackView.push(allTracksView, { viewType: viewType, mainTitle: mainTitle, image: imageUrl, modelType: dataType, stackView: browseStackView, opacity: 0, }) } onSwitchFilesBrowserView: { listViews.setCurrentIndex(pageModel.indexFromViewType(viewType)) while(browseStackView.depth > expectedDepth) { browseStackView.pop() } browseStackView.push(filesBrowserView, { viewType: viewType, mainTitle: mainTitle, image: imageUrl, opacity: 0, }) } onPopOneView: { if (browseStackView.depth > 2) { browseStackView.pop() } } } ViewsModel { id: pageModel } ViewSelector { id: listViews model: pageModel Layout.fillHeight: true Behavior on Layout.maximumWidth { NumberAnimation { duration: 150 } } onSwitchView: viewManager.openParentView(viewType, pageModel.viewMainTitle(viewType, ""), pageModel.viewImageUrl(viewType, "")) } Rectangle { id: viewSelectorSeparatorItem Layout.fillHeight: true width: 1 color: myPalette.mid } ColumnLayout { Layout.fillHeight: true Layout.fillWidth: true spacing: 0 TopNotification { id: invalidBalooConfiguration Layout.fillWidth: true musicManager: elisa.musicManager focus: true } Item { Layout.fillHeight: true Layout.fillWidth: true RowLayout { anchors.fill: parent spacing: 0 id: contentZone FocusScope { id: mainContentView focus: true Layout.fillHeight: true Layout.minimumWidth: 0 Layout.maximumWidth: 0 Layout.preferredWidth: 0 visible: Layout.minimumWidth != 0 MouseArea { anchors.fill: parent acceptedButtons: Qt.BackButton onClicked: goBack() } Rectangle { radius: 3 color: myPalette.base anchors.fill: parent StackView { id: browseStackView anchors.fill: parent clip: true initialItem: Item { } popEnter: Transition { OpacityAnimator { from: 0.0 to: 1.0 duration: 300 } } popExit: Transition { OpacityAnimator { from: 1.0 to: 0.0 duration: 300 } } pushEnter: Transition { OpacityAnimator { from: 0.0 to: 1.0 duration: 300 } } pushExit: Transition { OpacityAnimator { from: 1.0 to: 0.0 duration: 300 } } replaceEnter: Transition { OpacityAnimator { from: 0.0 to: 1.0 duration: 300 } } replaceExit: Transition { OpacityAnimator { from: 1.0 to: 0.0 duration: 300 } } } Behavior on border.color { ColorAnimation { duration: 300 } } } } Rectangle { id: firstViewSeparatorItem Layout.fillHeight: true width: 1 color: myPalette.mid } MediaPlayListView { id: playList Layout.fillHeight: true Layout.fillWidth: true onStartPlayback: elisa.audioControl.ensurePlay() onPausePlayback: elisa.audioControl.playPause() onDisplayError: messageNotification.showNotification(errorText) } Rectangle { id: viewSeparatorItem Layout.fillHeight: true width: 1 color: myPalette.mid } Loader { id: albumContext active: Layout.minimumWidth != 0 sourceComponent: ContextView { anchors.fill: parent databaseId: elisa.manageHeaderBar.databaseId title: elisa.manageHeaderBar.title artistName: elisa.manageHeaderBar.artist albumName: elisa.manageHeaderBar.album albumArtUrl: elisa.manageHeaderBar.image fileUrl: elisa.manageHeaderBar.fileName } Layout.fillHeight: true Layout.minimumWidth: 0 Layout.maximumWidth: 0 Layout.preferredWidth: 0 - Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin * 1.5 : 0 - Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin * 1.5 : 0 visible: Layout.minimumWidth != 0 } } } states: [ State { name: 'playList' when: listViews.currentIndex === 0 PropertyChanges { target: mainContentView Layout.minimumWidth: 0 Layout.maximumWidth: 0 Layout.preferredWidth: 0 } PropertyChanges { target: firstViewSeparatorItem Layout.minimumWidth: 0 Layout.maximumWidth: 0 Layout.preferredWidth: 0 } PropertyChanges { target: viewSeparatorItem visible: true } PropertyChanges { target: albumContext Layout.minimumWidth: contentZone.width * 3 / 5 - 2 - 3.5 * elisaTheme.layoutHorizontalMargin Layout.maximumWidth: contentZone.width * 3 / 5 - 2 - 3.5 * elisaTheme.layoutHorizontalMargin Layout.preferredWidth: contentZone.width * 3 / 5 - 2 - 3.5 * elisaTheme.layoutHorizontalMargin } }, State { name: "browsingViewsNoPlaylist" when: listViews.currentIndex !== 0 && contentViewContainer.showPlaylist !== true extend: "browsingViews" PropertyChanges { target: mainContentView Layout.minimumWidth: contentZone.width Layout.maximumWidth: contentZone.width Layout.preferredWidth: contentZone.width } PropertyChanges { target: playList Layout.minimumWidth: 0 Layout.maximumWidth: 0 Layout.preferredWidth: 0 } }, State { name: 'browsingViews' when: listViews.currentIndex !== 0 PropertyChanges { target: mainContentView Layout.minimumWidth: contentZone.width * 0.66 Layout.maximumWidth: contentZone.width * 0.68 Layout.preferredWidth: contentZone.width * 0.68 } PropertyChanges { target: firstViewSeparatorItem Layout.minimumWidth: 1 Layout.maximumWidth: 1 Layout.preferredWidth: 1 } PropertyChanges { target: viewSeparatorItem visible: false } PropertyChanges { target: albumContext Layout.minimumWidth: 0 Layout.maximumWidth: 0 Layout.preferredWidth: 0 } } ] transitions: Transition { NumberAnimation { properties: "Layout.minimumWidth, Layout.maximumWidth, Layout.preferredWidth, opacity" easing.type: Easing.InOutQuad duration: 300 } } } Component { id: allFrequentlyPlayedTracksView FrequentlyPlayedTracks { StackView.onActivated: viewManager.viewIsLoaded(viewType) } } Component { id: allRecentlyPlayedTracksView RecentlyPlayedTracks { StackView.onActivated: viewManager.viewIsLoaded(viewType) } } Component { id: dataGridView DataGridView { StackView.onActivated: viewManager.viewIsLoaded(viewType) } } Component { id: allTracksView TracksView { StackView.onActivated: viewManager.viewIsLoaded(viewType) } } Component { id: albumView AlbumView { StackView.onActivated: viewManager.viewIsLoaded(viewType) } } Component { id: filesBrowserView FileBrowserView { StackView.onActivated: viewManager.viewIsLoaded(viewType) } } } diff --git a/src/qml/ContextView.qml b/src/qml/ContextView.qml index d8cb37c1..72fdbffe 100644 --- a/src/qml/ContextView.qml +++ b/src/qml/ContextView.qml @@ -1,231 +1,253 @@ /* * Copyright 2016 Matthieu Gallien + * Copyright 2019 Nate Graham * * 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.Window 2.2 import QtQuick.Controls 2.2 import QtQml.Models 2.2 import QtQuick.Layouts 1.2 import org.kde.elisa 1.0 FocusScope { id: topItem property int databaseId: 0 property alias title: titleLabel.text property string albumName: '' property string artistName: '' property url albumArtUrl: '' property string fileUrl: '' TrackContextMetaDataModel { id: metaDataModel manager: elisa.musicManager } ColumnLayout { anchors.fill: parent spacing: 0 TextMetrics { id: titleHeight - text: viewTitleHeight.text - font: viewTitleHeight.font + text: viewTitle.text + font: viewTitle.font } - LabelWithToolTip { - id: viewTitleHeight - text: i18nc("Title of the context view related to the currently playing track", "Now Playing") + // Header with title + HeaderFooterToolbar { + type: "header" + contentItems: [ + LabelWithToolTip { + id: viewTitle + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - font.pointSize: elisaTheme.defaultFontPointSize * 2 + text: i18nc("Title of the context view related to the currently playing track", "Now Playing") - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.topMargin: elisaTheme.layoutVerticalMargin * 3 - Layout.bottomMargin: titleHeight.boundingRect.height - titleHeight.boundingRect.y + font.pointSize: elisaTheme.headerTitleFontSize + } + ] } - Image { - id: albumIcon + // Scrollview to hold all the content + Flickable { + id: flickable + clip: true - source: albumArtUrl.toString() === '' ? Qt.resolvedUrl(elisaTheme.defaultAlbumImage) : albumArtUrl + contentWidth: content.width + contentHeight: content.height Layout.fillWidth: true - Layout.maximumHeight: elisaTheme.contextCoverImageSize - Layout.preferredHeight: elisaTheme.contextCoverImageSize - Layout.maximumWidth: topItem.width - - Layout.bottomMargin: elisaTheme.layoutVerticalMargin + Layout.fillHeight: true - sourceSize.width: topItem.width - sourceSize.height: topItem.width + boundsBehavior: Flickable.StopAtBounds - asynchronous: true + ScrollBar.vertical: ScrollBar { + id: scrollBar + policy: ScrollBar.AlwaysOn + } - fillMode: Image.PreserveAspectCrop - } + // Album Art + title + metadata + lyrics + ColumnLayout { + id: content - LabelWithToolTip { - id: titleLabel + width: topItem.width - font.pointSize: elisaTheme.defaultFontPointSize * 2 - font.weight: Font.Bold + // Album art slice + Image { + id: albumIcon + source: albumArtUrl.toString() === '' ? Qt.resolvedUrl(elisaTheme.defaultAlbumImage) : albumArtUrl - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.topMargin: elisaTheme.layoutVerticalMargin - Layout.fillWidth: true - Layout.maximumWidth: topItem.width - } + Layout.maximumHeight: elisaTheme.contextCoverImageSize + Layout.preferredHeight: elisaTheme.contextCoverImageSize + Layout.fillWidth: true + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + Layout.topMargin: elisaTheme.layoutVerticalMargin + Layout.bottomMargin: elisaTheme.layoutVerticalMargin - LabelWithToolTip { - id: albumArtistLabel + sourceSize.width: topItem.width + sourceSize.height: topItem.width - text: (artistName && albumName ? i18nc('display of artist and album in context view', 'by %1 from %2', artistName, albumName) : '') + asynchronous: true - font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) + fillMode: Image.PreserveAspectCrop + } - visible: artistName !== '' && albumName !== '' + // Song title + LabelWithToolTip { + id: titleLabel - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.bottomMargin: elisaTheme.layoutVerticalMargin - Layout.fillWidth: true - Layout.maximumWidth: topItem.width - } + font.pointSize: elisaTheme.headerTitleFontSize + font.weight: Font.Bold - LabelWithToolTip { - id: albumLabel + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + Layout.topMargin: elisaTheme.layoutVerticalMargin + } - text: (albumName ? i18nc('display of album in context view', 'from %1', albumName) : '') + LabelWithToolTip { + id: albumArtistLabel - font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) + text: (artistName && albumName ? i18nc('display of artist and album in context view', 'by %1 from %2', artistName, albumName) : '') - visible: artistName === '' && albumName !== '' + font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.bottomMargin: elisaTheme.layoutVerticalMargin - Layout.fillWidth: true - Layout.maximumWidth: topItem.width - } + visible: artistName !== '' && albumName !== '' - LabelWithToolTip { - id: artistLabel + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + Layout.bottomMargin: elisaTheme.layoutVerticalMargin + } - text: (artistName ? i18nc('display of artist in context view', 'by %1', artistName) : '') + LabelWithToolTip { + id: albumLabel - font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) + text: (albumName ? i18nc('display of album in context view', 'from %1', albumName) : '') - visible: artistName !== '' && albumName === '' + font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.bottomMargin: elisaTheme.layoutVerticalMargin - Layout.fillWidth: true - Layout.maximumWidth: topItem.width - } + visible: artistName === '' && albumName !== '' - Flickable { - id: flickable - clip: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + Layout.bottomMargin: elisaTheme.layoutVerticalMargin + } - contentWidth: topItem.width - contentHeight: allMetaData.height + LabelWithToolTip { + id: artistLabel - Layout.fillWidth: true - Layout.fillHeight: true + text: (artistName ? i18nc('display of artist in context view', 'by %1', artistName) : '') - boundsBehavior: Flickable.StopAtBounds + font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.4) - ScrollBar.vertical: ScrollBar { - id: scrollBar - policy: ScrollBar.AlwaysOn - } + visible: artistName !== '' && albumName === '' - ColumnLayout { - id: allMetaData + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + Layout.bottomMargin: elisaTheme.layoutVerticalMargin + } - spacing: 0 + // Metadata + ColumnLayout { + id: allMetaData - width: topItem.width + spacing: 0 + Layout.fillWidth: true + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin - Repeater { - id: trackData + Repeater { + id: trackData - model: metaDataModel + model: metaDataModel - delegate: MetaDataDelegate { - Layout.fillWidth: true + delegate: MetaDataDelegate { + Layout.fillWidth: true + } } } + // Lyrics ContextViewLyrics { id: lyricsContextView Layout.fillWidth: true + Layout.leftMargin: elisaTheme.layoutHorizontalMargin + Layout.rightMargin: elisaTheme.layoutHorizontalMargin + Layout.bottomMargin: elisaTheme.layoutVerticalMargin visible: metaDataModel.lyrics !== "" lyrics: metaDataModel.lyrics } } } - RowLayout { - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - Layout.topMargin: elisaTheme.layoutVerticalMargin - Layout.bottomMargin: elisaTheme.layoutVerticalMargin - Layout.maximumWidth: topItem.width - - spacing: elisaTheme.layoutHorizontalMargin + // Footer with file path label + HeaderFooterToolbar { + type: "footer" + contentLayoutSpacing: elisaTheme.layoutHorizontalMargin + contentItems: [ + Image { + sourceSize.width: fileNameLabel.height + sourceSize.height: fileNameLabel.height - Image { - sourceSize.width: fileNameLabel.height - sourceSize.height: fileNameLabel.height + source: elisaTheme.folderIcon + }, + LabelWithToolTip { + id: fileNameLabel - source: elisaTheme.folderIcon - } - - LabelWithToolTip { - id: fileNameLabel - - Layout.fillWidth: true - - text: fileUrl + Layout.fillWidth: true - elide: Text.ElideLeft - } + text: fileUrl + elide: Text.ElideLeft + } + ] } } onDatabaseIdChanged: { metaDataModel.initializeByTrackId(databaseId) } Connections { target: elisa onMusicManagerChanged: { metaDataModel.initializeByTrackId(databaseId) } } Component.onCompleted: { if (elisa.musicManager) { metaDataModel.initializeByTrackId(databaseId) } } } diff --git a/src/qml/ElisaMainWindow.qml b/src/qml/ElisaMainWindow.qml index 2b63e1ad..256b088c 100644 --- a/src/qml/ElisaMainWindow.qml +++ b/src/qml/ElisaMainWindow.qml @@ -1,343 +1,349 @@ /* * Copyright 2016-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.7 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.elisa 1.0 import Qt.labs.settings 1.0 ApplicationWindow { id: mainWindow visible: true minimumWidth: contentView.showPlaylist ? 1100 : 700 minimumHeight: 600 LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft LayoutMirroring.childrenInherit: true x: persistentSettings.x y: persistentSettings.y width: persistentSettings.width height: persistentSettings.height title: i18n("Elisa") property var goBackAction: elisa.action("go_back") property var seekAction: elisa.action("Seek") property var scrubAction: elisa.action("Scrub") property var playPauseAction: elisa.action("Play-Pause") Action { shortcut: goBackAction.shortcut onTriggered: contentView.goBack() } Action { shortcut: seekAction.shortcut onTriggered: elisa.audioControl.seek(headerBar.playerControl.position + 10000) } Action { shortcut: scrubAction.shortcut onTriggered: elisa.audioControl.seek(headerBar.playerControl.position - 10000) } Action { shortcut: playPauseAction.shortcut onTriggered: elisa.audioControl.playPause() } ApplicationMenu { id: applicationMenu } SystemPalette { id: myPalette colorGroup: SystemPalette.Active } Theme { id: elisaTheme } Settings { id: persistentSettings property int x property int y property int width : 1100 property int height : 600 property var playListState property var audioPlayerState property double playControlItemVolume : 100.0 property bool playControlItemMuted : false property bool playControlItemRepeat : false property bool playControlItemShuffle : false property bool expandedFilterView: false property bool showPlaylist: true property bool headerBarIsMaximized: false } Connections { target: headerBar.playerControl onOpenMenu: applicationMenu.popup() } Connections { target: Qt.application onAboutToQuit: { persistentSettings.x = mainWindow.x; persistentSettings.y = mainWindow.y; persistentSettings.width = mainWindow.width; persistentSettings.height = mainWindow.height; persistentSettings.playListState = elisa.mediaPlayList.persistentState; persistentSettings.audioPlayerState = elisa.audioControl.persistentState persistentSettings.playControlItemVolume = headerBar.playerControl.volume persistentSettings.playControlItemMuted = headerBar.playerControl.muted persistentSettings.playControlItemRepeat = headerBar.playerControl.repeat persistentSettings.playControlItemShuffle = headerBar.playerControl.shuffle persistentSettings.showPlaylist = contentView.showPlaylist persistentSettings.headerBarIsMaximized = headerBar.isMaximized } } Loader { id: mprisloader active: false sourceComponent: PlatformIntegration { id: platformInterface playListModel: elisa.mediaPlayList audioPlayerManager: elisa.audioControl player: elisa.audioPlayer headerBarManager: elisa.manageHeaderBar manageMediaPlayerControl: elisa.playerControl onRaisePlayer: { mainWindow.show() mainWindow.raise() mainWindow.requestActivate() } } } Connections { target: elisa.audioPlayer onVolumeChanged: headerBar.playerControl.volume = elisa.audioPlayer.volume onMutedChanged: headerBar.playerControl.muted = elisa.audioPlayer.muted } Connections { target: elisa.mediaPlayList onPlayListLoadFailed: { messageNotification.showNotification(i18nc("message of passive notification when playlist load failed", "Load of playlist failed"), 3000) } } PassiveNotification { id: messageNotification } Rectangle { color: myPalette.base anchors.fill: parent ColumnLayout { anchors.fill: parent spacing: 0 Item { id: headerBarParent Layout.minimumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.maximumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.fillWidth: true HeaderBar { id: headerBar focus: true anchors.fill: parent tracksCount: elisa.manageHeaderBar.remainingTracks album: elisa.manageHeaderBar.album title: elisa.manageHeaderBar.title artist: elisa.manageHeaderBar.artist albumArtist: elisa.manageHeaderBar.albumArtist image: elisa.manageHeaderBar.image albumID: elisa.manageHeaderBar.albumId ratingVisible: false playerControl.duration: elisa.audioPlayer.duration playerControl.seekable: elisa.audioPlayer.seekable playerControl.volume: persistentSettings.playControlItemVolume playerControl.muted: persistentSettings.playControlItemMuted playerControl.position: elisa.audioPlayer.position playerControl.skipBackwardEnabled: elisa.playerControl.skipBackwardControlEnabled playerControl.skipForwardEnabled: elisa.playerControl.skipForwardControlEnabled playerControl.playEnabled: elisa.playerControl.playControlEnabled playerControl.isPlaying: elisa.playerControl.musicPlaying playerControl.repeat: persistentSettings.playControlItemRepeat playerControl.shuffle: persistentSettings.playControlItemShuffle playerControl.onSeek: elisa.audioPlayer.seek(position) playerControl.onPlay: elisa.audioControl.playPause() playerControl.onPause: elisa.audioControl.playPause() playerControl.onPlayPrevious: elisa.mediaPlayList.skipPreviousTrack() playerControl.onPlayNext: elisa.mediaPlayList.skipNextTrack() playerControl.isMaximized: persistentSettings.headerBarIsMaximized onOpenArtist: { contentView.openArtist(artist) } onOpenNowPlaying: { contentView.openNowPlaying() } onOpenAlbum: { contentView.openAlbum(album, albumArtist, image, albumID) } TrackImportNotification { id: importedTracksCountNotification anchors { right: headerBar.right top: headerBar.top rightMargin: elisaTheme.layoutHorizontalMargin * 1.75 topMargin: elisaTheme.layoutHorizontalMargin * 3 } } Binding { target: importedTracksCountNotification property: 'musicManager' value: elisa.musicManager when: elisa.musicManager !== undefined } Binding { id: indexerBusyBinding target: importedTracksCountNotification property: 'indexingRunning' value: elisa.musicManager.indexerBusy when: elisa.musicManager !== undefined } Binding { target: importedTracksCountNotification property: 'importedTracksCount' value: elisa.musicManager.importedTracksCount when: elisa.musicManager !== undefined } } } + Rectangle { + Layout.fillWidth: true + height: 1 + color: myPalette.mid + } + ContentView { id: contentView Layout.fillHeight: true Layout.fillWidth: true showPlaylist: persistentSettings.showPlaylist } } } StateGroup { id: mainWindowState states: [ State { name: "headerBarIsNormal" when: !headerBar.isMaximized changes: [ PropertyChanges { target: mainWindow minimumHeight: 600 explicit: true }, PropertyChanges { target: headerBarParent Layout.minimumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.maximumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight } ] }, State { name: "headerBarIsMaximized" when: headerBar.isMaximized changes: [ PropertyChanges { target: mainWindow minimumHeight: 120 + elisaTheme.mediaPlayerControlHeight explicit: true }, PropertyChanges { target: headerBarParent Layout.minimumHeight: mainWindow.height Layout.maximumHeight: mainWindow.height } ] } ] transitions: Transition { NumberAnimation { properties: "Layout.minimumHeight, Layout.maximumHeight, minimumHeight" easing.type: Easing.InOutQuad duration: 300 } } } Component.onCompleted: { elisa.initialize() elisa.mediaPlayList.randomPlay = Qt.binding(function() { return headerBar.playerControl.shuffle }) elisa.mediaPlayList.repeatPlay = Qt.binding(function() { return headerBar.playerControl.repeat }) elisa.playerControl.randomOrContinuePlay = Qt.binding(function() { return headerBar.playerControl.shuffle || headerBar.playerControl.repeat}) if (persistentSettings.playListState) { elisa.mediaPlayList.persistentState = persistentSettings.playListState } if (persistentSettings.audioPlayerState) { elisa.audioControl.persistentState = persistentSettings.audioPlayerState } elisa.audioPlayer.muted = Qt.binding(function() { return headerBar.playerControl.muted }) elisa.audioPlayer.volume = Qt.binding(function() { return headerBar.playerControl.volume }) mprisloader.active = true } } diff --git a/src/qml/HeaderFooterToolbar.qml b/src/qml/HeaderFooterToolbar.qml new file mode 100644 index 00000000..bc11d5f5 --- /dev/null +++ b/src/qml/HeaderFooterToolbar.qml @@ -0,0 +1,84 @@ +/* + * Copyright 2016 Matthieu Gallien + * Copyright 2019 Nate Graham + * + * 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 + +ColumnLayout { + spacing: 0 + Layout.fillWidth: true + + // Is this a header or a footer? Acceptable values are + // "header" (separator drawn on bottom) + // "footer" (separator drawn on top) + // "other" (no separator drawn) + property string type + + // A list of items to be shown within the header or footer + property alias contentItems: contentLayout.children + + // Spacing of content items. Defaults to 0 + property alias contentLayoutSpacing: contentLayout.spacing + + + // Separator line above the header + Rectangle { + visible: type == "footer" && type != "other" + Layout.fillWidth: true + height: 1 + color: myPalette.mid + } + + // Background rectangle + content layout + Rectangle { + id: headerBackground + + color: myPalette.window + + Layout.fillWidth: true + height: contentLayout.height + + // Content layout + RowLayout { + id: contentLayout + + anchors { + left: parent.left + leftMargin: elisaTheme.layoutHorizontalMargin + right: parent.right + rightMargin: elisaTheme.layoutHorizontalMargin + verticalCenter: parent.verticalCenter + } + height: childrenRect.height + (elisaTheme.layoutVerticalMargin * 2) + spacing: 0 + + // Items provided by the contentItems property will go here + } + } + + // Separator line under the header + Rectangle { + visible: type == "header" && type != "other" + Layout.fillWidth: true + height: 1 + color: myPalette.mid + } +} diff --git a/src/qml/MediaPlayListView.qml b/src/qml/MediaPlayListView.qml index f74b7a9d..fd8a346a 100644 --- a/src/qml/MediaPlayListView.qml +++ b/src/qml/MediaPlayListView.qml @@ -1,294 +1,294 @@ /* * Copyright 2016-2017 Matthieu Gallien + * Copyright 2019 Nate Graham * * 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.5 import QtQuick.Controls 2.2 import QtQuick.Controls 1.3 as Controls1 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import Qt.labs.platform 1.0 as PlatformDialog import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { property StackView parentStackView property int placeholderHeight: elisaTheme.dragDropPlaceholderHeight signal startPlayback() signal pausePlayback() signal displayError(var errorText) id: topItem Controls1.Action { id: clearPlayList text: i18nc("Remove all tracks from play list", "Clear Playlist") iconName: 'edit-clear-all' enabled: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false onTriggered: elisa.mediaPlayList.clearPlayList() } Controls1.Action { id: showCurrentTrack text: i18nc("Show currently played track inside playlist", "Show Current Track") iconName: 'media-show-active-track-amarok' enabled: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false onTriggered: { playListView.positionViewAtIndex(elisa.mediaPlayList.currentTrackRow, ListView.Contain) playListView.currentIndex = elisa.mediaPlayList.currentTrackRow playListView.currentItem.forceActiveFocus() } } Controls1.Action { id: loadPlaylist text: i18nc("Load a playlist file", "Load Playlist...") iconName: 'document-open' onTriggered: { fileDialog.fileMode = PlatformDialog.FileDialog.OpenFile fileDialog.file = '' fileDialog.open() } } Controls1.Action { id: savePlaylist text: i18nc("Save a playlist file", "Save Playlist...") iconName: 'document-save' enabled: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false onTriggered: { fileDialog.fileMode = PlatformDialog.FileDialog.SaveFile fileDialog.file = '' fileDialog.open() } } PlatformDialog.FileDialog { id: fileDialog defaultSuffix: 'm3u' folder: PlatformDialog.StandardPaths.writableLocation(PlatformDialog.StandardPaths.MusicLocation) nameFilters: [i18nc("file type (mime type) for m3u playlist", "Playlist (*.m3u)")] onAccepted: { if (fileMode === PlatformDialog.FileDialog.SaveFile) { if (!elisa.mediaPlayList.savePlaylist(fileDialog.file)) { displayError(i18nc("message of passive notification when playlist load failed", "Save of playlist failed")) } } else { elisa.mediaPlayList.loadPlaylist(fileDialog.file) } } } ColumnLayout { anchors.fill: parent spacing: 0 - LabelWithToolTip { - id: viewTitleHeight - text: i18nc("Title of the view of the playlist", "Playlist") - - color: myPalette.text - font.pointSize: elisaTheme.defaultFontPointSize * 2 - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.topMargin: elisaTheme.layoutVerticalMargin * 3 - Layout.leftMargin: elisaTheme.layoutHorizontalMargin - Layout.rightMargin: elisaTheme.layoutHorizontalMargin - } - - - RowLayout { - Layout.fillWidth: true - - Layout.leftMargin: elisaTheme.layoutHorizontalMargin - Layout.rightMargin: elisaTheme.layoutHorizontalMargin - - LabelWithToolTip { - id: playListInfo - - text: i18np("1 track", "%1 tracks", (elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount : 0)) - visible: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false - color: myPalette.text - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - } - - Item { Layout.fillWidth: true } - - Controls1.ToolButton { - action: showCurrentTrack - Keys.onReturnPressed: action.trigger() - } - - Controls1.ToolButton { - action: savePlaylist - Keys.onReturnPressed: action.trigger() - } - - Controls1.ToolButton { - action: loadPlaylist - Keys.onReturnPressed: action.trigger() - } - - Controls1.ToolButton { - action: clearPlayList - Keys.onReturnPressed: action.trigger() - } + // Header with title and toolbar buttons + HeaderFooterToolbar { + type: "header" + contentItems: [ + + // Header title + LabelWithToolTip { + Layout.fillWidth: true + + text: i18nc("Title of the view of the playlist", "Playlist") + + font.pointSize: elisaTheme.headerTitleFontSize + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + }, + + // Toolbar buttons + Controls1.ToolButton { + action: showCurrentTrack + Keys.onReturnPressed: action.trigger() + }, + Controls1.ToolButton { + action: savePlaylist + Keys.onReturnPressed: action.trigger() + }, + Controls1.ToolButton { + action: loadPlaylist + Keys.onReturnPressed: action.trigger() + }, + Controls1.ToolButton { + action: clearPlayList + Keys.onReturnPressed: action.trigger() + } + ] } ColumnLayout { id: emptyPlaylistText spacing: 0 visible: true Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.fillHeight: true Layout.fillWidth: true Item { id: emptyVisible visible: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount === 0 : true Layout.preferredHeight: (emptyPlaylistText.height-emptyImage.height-emptyLabel0.height-emptyLabel1.height)/2 } Image { id: emptyImage visible: emptyVisible.visible Layout.alignment: Qt.AlignHCenter width: elisaTheme.gridDelegateWidth * 5 height: elisaTheme.gridDelegateWidth * 5 source: elisaTheme.playlistIcon opacity: 0.25 sourceSize { width: elisaTheme.viewSelectorDelegateHeight * 5 height: elisaTheme.viewSelectorDelegateHeight * 5 } } Label { id: emptyLabel0 visible: emptyVisible.visible Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin - font.pointSize: elisaTheme.defaultFontPointSize * 2 + font.pointSize: elisaTheme.headerTitleFontSize wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: i18nc("Your playlist is empty", "Your playlist is empty") } Label { id: emptyLabel1 visible: emptyVisible.visible Layout.topMargin: 5 Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: i18nc("Text shown when play list is empty", "Add some songs to get started. You can browse your music using the views on the left.") } Item { visible: emptyVisible.visible Layout.fillHeight: true } PlayListBasicView { id: playListView visible: !emptyVisible.visible Layout.fillWidth: true Layout.fillHeight: true playListModel: elisa.mediaPlayList focus: true onStartPlayback: topItem.startPlayback() onPausePlayback: topItem.pausePlayback() onDisplayError: topItem.displayError(errorText) } Kirigami.InlineMessage { Connections { target: elisa.mediaPlayList onDisplayUndoInline: undoClear.visible = true } Connections { target: elisa.mediaPlayList onHideUndoInline: undoClear.visible = false } Timer { id: autoHideUndoTimer interval: 7000 onTriggered: undoClear.visible = false } id: undoClear text: i18nc("Playlist cleared", "Playlist cleared") type: Kirigami.MessageType.Information showCloseButton: true Layout.topMargin: 5 Layout.fillWidth: true Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin onVisibleChanged: { if (visible) { autoHideUndoTimer.start() } else { autoHideUndoTimer.stop() } } actions: [ Kirigami.Action { text: i18nc("Undo", "Undo") icon.name: "dialog-cancel" onTriggered: elisa.mediaPlayList.undoClearPlayList() } ] } } + + // Footer with number of tracks label + HeaderFooterToolbar { + type: "footer" + contentItems: [ + LabelWithToolTip { + id: trackCountLabel + + Layout.fillWidth: true + + text: i18np("1 track", "%1 tracks", (elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount : 0)) + elide: Text.ElideLeft + } + ] + } } } diff --git a/src/resources.qrc b/src/resources.qrc index d508f4e8..c38dffbc 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -1,63 +1,64 @@ qml/MediaPlayerControl.qml qml/RatingStar.qml qml/MediaPlayListView.qml qml/ElisaMainWindow.qml qml/ApplicationMenu.qml qml/HeaderBar.qml qml/ContextView.qml qml/ContentView.qml qml/DraggableItem.qml qml/PassiveNotification.qml qml/NavigationActionBar.qml qml/PlayListEntry.qml qml/Theme.qml qml/PlatformIntegration.qml qml/LabelWithToolTip.qml qml/TopNotification.qml qml/TrackImportNotification.qml qml/TopNotificationItem.qml qml/MediaTrackDelegate.qml qml/MediaAlbumTrackDelegate.qml qml/MediaTrackMetadataView.qml qml/GridBrowserView.qml qml/GridBrowserDelegate.qml qml/ListBrowserView.qml qml/FileBrowserDelegate.qml qml/FileBrowserView.qml qtquickcontrols2.conf background.png qml/BaseTheme.qml qml/ScrollHelper.qml qml/FlatButtonWithToolTip.qml qml/DataGridView.qml qml/TracksView.qml qml/AlbumView.qml qml/RecentlyPlayedTracks.qml qml/FrequentlyPlayedTracks.qml qml/PlayListBasicView.qml qml/SimplePlayListView.qml qml/SimplePlayListEntry.qml qml/ViewSelector.qml qml/PlayListAlbumHeader.qml qml/BasicPlayListAlbumHeader.qml qml/MetaDataDelegate.qml qml/ContextViewLyrics.qml qml/ViewSelectorDelegate.qml + qml/HeaderFooterToolbar.qml windows/WindowsTheme.qml windows/PlatformIntegration.qml windows/LabelWithToolTip.qml android/ElisaMainWindow.qml android/AndroidTheme.qml android/PlatformIntegration.qml android/AlbumsView.qml android/ArtistsView.qml android/TracksView.qml android/GenresView.qml