Commit 7bb83f95 authored by Devin Lin's avatar Devin Lin 🎨
Browse files

Add mobile layout

This commit is the culmination of !205

Notable Changes:
- Added mobile folder with components needed for mobile (and are separator from desktop components)
- Changed tst_NavigationActionBar test so that it does not check for artist button when it is invisible (loaders are used now so the button wouldn't be detected)
- Added loaders in ContentView.qml to switch between the mobile and desktop sidebars
- Changed ContextView.qml to use FormLayout, and also add mobile navigation buttons
- Collapsed the two list delegates (detailed and non detailed) together, along with edits for mobile usage
- Lowered the size of text in list delegates
- Extracted out the settings form to be shared between mobile and desktop
- Edited the grid delegate to be mobile friendly
- Removed clip from both listviews and gridviews for performance
- Extracted out the volume slider and duration slider to be shared for mobile and desktop
- Add mobile and desktop com...
parent fefb6e59
Pipeline #54058 canceled with stage
......@@ -217,10 +217,7 @@ FocusScope {
verify(showArtistButtonItem1 !== null, "valid showArtistButton")
mouseClick(showArtistButtonItem1);
compare(showArtistSpy1.count, 1);
var showArtistButtonItem2 = findChild(navigationActionBar2, "showArtistButton");
verify(showArtistButtonItem2 !== null, "valid showArtistButton")
mouseClick(showArtistButtonItem2);
compare(showArtistSpy2.count, 0);
// artist button is unloaded in navigationActionBar2
}
function test_filterRating() {
......
......@@ -414,14 +414,16 @@ if (Qt5Quick_FOUND AND Qt5Widgets_FOUND)
qml/ViewSelectorDelegate.qml
qml/DataGridView.qml
qml/DataListView.qml
qml/DurationSlider.qml
qml/VolumeSlider.qml
qml/MediaPlayListView.qml
qml/PlayListEntry.qml
qml/SimplePlayListView.qml
qml/BasicPlayListAlbumHeader.qml
qml/MetaDataDelegate.qml
qml/EditableMetaDataDelegate.qml
qml/MediaTrackMetadataDelegate.qml
qml/MediaTrackMetadataForm.qml
qml/TracksDiscHeader.qml
qml/MediaTrackMetadataView.qml
qml/GridBrowserView.qml
......@@ -431,7 +433,19 @@ if (Qt5Quick_FOUND AND Qt5Widgets_FOUND)
qml/FlatButtonWithToolTip.qml
qml/HeaderFooterToolbar.qml
qml/SettingsForm.qml
qml/ElisaConfigurationDialog.qml
qml/mobile/MobileContextMenuEntry.qml
qml/mobile/MobileContextMenuSheet.qml
qml/mobile/MobileFooterBar.qml
qml/mobile/MobileMediaTrackMetadataView.qml
qml/mobile/MobileMinimizedPlayerControl.qml
qml/mobile/MobilePlayListDelegate.qml
qml/mobile/MobileSettingsPage.qml
qml/mobile/MobileSidebar.qml
qml/mobile/MobileTrackPlayer.qml
qml/mobile/MobileVolumeButton.qml
)
qt5_add_resources(elisa_SOURCES resources.qrc)
......
......@@ -5,7 +5,7 @@
*/
import QtQuick 2.7
import org.kde.kirigami 2.0 as Kirigami
import org.kde.kirigami 2.12 as Kirigami
Item {
property string defaultAlbumImage: 'image://icon/media-optical-audio'
......@@ -41,7 +41,7 @@ Item {
property int tooltipRadius: 3
property int shadowOffset: 2
property int mediaPlayerControlHeight: 42
property int mediaPlayerControlHeight: Kirigami.Settings.isMobile? Math.round(Kirigami.Units.gridUnit * 3.5) : Math.round(Kirigami.Units.gridUnit * 2.5)
property real mediaPlayerControlOpacity: 0.6
property int volumeSliderWidth: 100
......
......@@ -11,16 +11,23 @@ import QtQuick.Window 2.2
import org.kde.elisa 1.0
import org.kde.kirigami 2.8 as Kirigami
import "mobile"
RowLayout {
id: contentViewContainer
spacing: 0
property bool showPlaylist
property bool showExpandedFilterView
property alias currentViewIndex: listViews.currentIndex
property int currentViewIndex: getCurrentViewIndex()
property Kirigami.ContextDrawer playlistDrawer
property alias initialIndex: viewManager.initialIndex
property alias sidebar: mobileSidebar.item
// setCurrentViewIndex be called before loaders are loaded, so store the value
property int preloadIndex: -1
function goBack() {
viewManager.goBack()
}
......@@ -38,6 +45,24 @@ RowLayout {
viewManager.openNowPlaying();
}
function getCurrentViewIndex() {
if (mobileSidebar.item != null) {
return mobileSidebar.item.viewIndex;
} else if (desktopSidebar.item != null) {
return desktopSidebar.item.currentIndex;
}
}
function setCurrentViewIndex(index) {
if (mobileSidebar.item != null) {
mobileSidebar.item.switchView(index);
} else if (desktopSidebar.item != null) {
desktopSidebar.item.setCurrentIndex(index);
} else {
contentViewContainer.preloadIndex = index;
}
}
ViewManager {
id: viewManager
......@@ -45,7 +70,7 @@ RowLayout {
onOpenGridView: {
if (configurationData.expectedDepth === 1) {
listViews.setCurrentIndex(viewManager.viewIndex)
contentViewContainer.setCurrentViewIndex(viewManager.viewIndex)
}
while(browseStackView.depth > configurationData.expectedDepth) {
......@@ -79,7 +104,7 @@ RowLayout {
onOpenListView: {
if (configurationData.expectedDepth === 1) {
listViews.setCurrentIndex(viewManager.viewIndex)
contentViewContainer.setCurrentViewIndex(viewManager.viewIndex)
}
while(browseStackView.depth > configurationData.expectedDepth) {
......@@ -114,7 +139,10 @@ RowLayout {
}
onSwitchContextView: {
listViews.setCurrentIndex(viewManager.viewIndex)
if (preloadIndex == -1) {
// prevent changing page to viewManager.viewIndex if there is a pending page change
contentViewContainer.setCurrentViewIndex(viewManager.viewIndex)
}
while(browseStackView.depth > expectedDepth) {
browseStackView.pop()
......@@ -158,18 +186,45 @@ RowLayout {
embeddedCategory: ElisaApplication.embeddedView
}
ViewSelector {
id: listViews
model: pageProxyModel
// sidebar used on desktop
Loader {
id: desktopSidebar
active: !Kirigami.Settings.isMobile
Layout.fillHeight: true
onSwitchView: viewManager.openView(viewIndex)
onLoaded: {
if (contentViewContainer.preloadIndex != -1) {
item.setCurrentIndex(contentViewContainer.preloadIndex);
viewManager.openView(contentViewContainer.preloadIndex);
contentViewContainer.preloadIndex = -1;
}
}
sourceComponent: ViewSelector {
model: pageProxyModel
onSwitchView: viewManager.openView(viewIndex)
}
}
// sidebar used on mobile
Loader {
id: mobileSidebar
active: Kirigami.Settings.isMobile
onLoaded: {
if (contentViewContainer.preloadIndex != -1) {
item.switchView(contentViewContainer.preloadIndex);
viewManager.openView(contentViewContainer.preloadIndex);
contentViewContainer.preloadIndex = -1;
}
}
sourceComponent: MobileSidebar {
model: pageProxyModel
onSwitchView: viewManager.openView(viewIndex)
}
}
Kirigami.Separator {
id: viewSelectorSeparatorItem
visible: !Kirigami.Settings.isMobile
Layout.fillHeight: true
}
......@@ -259,11 +314,10 @@ RowLayout {
Layout.fillHeight: true
}
// playlist right sidebar
MediaPlayListView {
id: playList
Layout.fillHeight: true
onStartPlayback: ElisaApplication.audioControl.ensurePlay()
onPausePlayback: ElisaApplication.audioControl.playPause()
}
......@@ -277,6 +331,7 @@ RowLayout {
Layout.minimumWidth: 0
Layout.maximumWidth: 0
Layout.preferredWidth: 0
visible: false
}
PropertyChanges {
target: playListSeparatorItem
......
......@@ -39,6 +39,8 @@ Kirigami.Page {
title: i18nc("Title of the context view related to the currently playing track", "Now Playing")
padding: 0
property bool isWidescreen: mainWindow.width >= elisaTheme.viewSelectorSmallSizeThreshold
TrackContextMetaDataModel {
id: metaDataModel
......@@ -50,6 +52,7 @@ Kirigami.Page {
// "Kirigami.ApplicationHeaderStyle.None" and remove the custom header
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
header: ToolBar {
implicitHeight: Math.round(Kirigami.Units.gridUnit * 2.5)
Layout.fillWidth: true
// Override color to use standard window colors, not header colors
......@@ -61,10 +64,18 @@ Kirigami.Page {
anchors.fill: parent
spacing: Kirigami.Units.largeSpacing
FlatButtonWithToolTip {
id: showSidebarButton
objectName: 'showSidebarButton'
visible: Kirigami.Settings.isMobile
text: i18nc("open the sidebar", "Open sidebar")
icon.name: "application-menu"
onClicked: mainWindow.globalDrawer.open()
}
Image {
id: mainIcon
source: elisaTheme.nowPlayingIcon
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
......@@ -73,7 +84,7 @@ Kirigami.Page {
fillMode: Image.PreserveAspectFit
asynchronous: true
visible: !Kirigami.Settings.isMobile
}
Kirigami.Heading {
Layout.fillWidth: true
......@@ -87,6 +98,21 @@ Kirigami.Page {
icon.name: "edit-paste"
opacity: 0
}
FlatButtonWithToolTip {
id: showPlaylistButton
visible: Kirigami.Settings.isMobile
text: i18nc("show the playlist", "Show Playlist")
icon.name: "view-media-playlist"
display: topItem.isWidescreen ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
onClicked: {
if (topItem.isWidescreen) {
contentView.showPlaylist = !contentView.showPlaylist;
} else {
playlistDrawer.open();
}
}
}
}
}
......@@ -250,33 +276,33 @@ Kirigami.Page {
// Horizontal line separating title and subtitle from metadata
Kirigami.Separator {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing* 5
Layout.leftMargin: Kirigami.Units.largeSpacing * 5
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing * 5
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
// Metadata
ColumnLayout {
Kirigami.FormLayout {
id: allMetaData
spacing: 0
Layout.fillWidth: true
//Layout.leftMargin:Kirigami.Units.largeSpacing
//Layout.rightMargin:Kirigami.Units.largeSpacing
Repeater {
id: trackData
model: metaDataModel
delegate: MetaDataDelegate {
index: model.index
name: model.name
display: model.display
type: model.type
Layout.fillWidth: true
delegate: RowLayout {
Kirigami.FormData.label: "<b>" + model.name + ":</b>"
MediaTrackMetadataDelegate {
index: model.index
name: model.name
display: model.display
type: model.type
readOnly: true
Layout.fillWidth: true
}
}
}
}
......@@ -315,6 +341,7 @@ Kirigami.Page {
width: parent.width - (Kirigami.Units.largeSpacing * 4)
visible: topItem.nothingPlaying
text: i18n("Nothing playing")
icon.name: "view-media-track"
}
}
......
......@@ -35,7 +35,7 @@ FocusScope {
property alias viewManager: listView.viewManager
function openMetaDataView(databaseId, url, entryType) {
metadataLoader.setSource("MediaTrackMetadataView.qml",
metadataLoader.setSource(Kirigami.Settings.isMobile ? "mobile/MobileMediaTrackMetadataView.qml" : "MediaTrackMetadataView.qml",
{
"fileName": url,
"modelType": entryType,
......@@ -49,7 +49,7 @@ FocusScope {
}
function openCreateRadioView()
{
metadataLoader.setSource("MediaTrackMetadataView.qml",
metadataLoader.setSource(Kirigami.Settings.isMobile ? "mobile/MobileMediaTrackMetadataView.qml" : "MediaTrackMetadataView.qml",
{
"modelType": ElisaUtils.Radio,
"isCreation": true,
......@@ -100,11 +100,20 @@ FocusScope {
Loader {
id: metadataLoader
active: false
onLoaded: item.show()
onLoaded: {
// on mobile, the metadata loader is a page
// on desktop, it's a window
if (Kirigami.Settings.isMobile) {
mainWindow.pageStack.layers.push(item);
} else {
item.show();
}
}
}
// desktop delegates
Component {
id: albumDelegate
id: trackDelegate
ListBrowserDelegate {
id: entry
......@@ -124,9 +133,10 @@ FocusScope {
trackNumber: model.trackNumber ? model.trackNumber : -1
discNumber: model.discNumber ? model.discNumber : -1
rating: model.rating
hideDiscNumber: !viewHeader.displaySingleAlbum && model.isSingleDiscAlbum
isSelected: listView.currentIndex === index
isAlternateColor: (index % 2) === 1
detailedView: false
detailedView: !viewHeader.displaySingleAlbum
onEnqueue: ElisaApplication.mediaPlayListProxyModel.enqueue(model.fullData, model.display,
ElisaUtils.AppendPlayList,
......@@ -136,8 +146,10 @@ FocusScope {
ElisaUtils.ReplacePlayList,
ElisaUtils.TriggerPlay)
onClicked: listView.currentIndex = index
onClicked: {
listView.currentIndex = index;
entry.forceActiveFocus();
}
onActiveFocusChanged: {
if (activeFocus && listView.currentIndex !== index) {
......@@ -151,50 +163,6 @@ FocusScope {
}
}
Component {
id: detailedTrackDelegate
ListBrowserDelegate {
id: entry
width: listView.delegateWidth
focus: true
trackUrl: model.url
dataType: model.dataType
title: model.display ? model.display : ''
artist: model.artist ? model.artist : ''
album: model.album ? model.album : ''
albumArtist: model.albumArtist ? model.albumArtist : ''
duration: model.duration ? model.duration : ''
imageUrl: model.imageUrl ? model.imageUrl : ''
trackNumber: model.trackNumber ? model.trackNumber : -1
discNumber: model.discNumber ? model.discNumber : -1
rating: model.rating
hideDiscNumber: model.isSingleDiscAlbum
isSelected: listView.currentIndex === index
isAlternateColor: (index % 2) === 1
onEnqueue: ElisaApplication.mediaPlayListProxyModel.enqueue(model.fullData, model.display,
ElisaUtils.AppendPlayList,
ElisaUtils.DoNotTriggerPlay)
onReplaceAndPlay: ElisaApplication.mediaPlayListProxyModel.enqueue(model.fullData, model.display,
ElisaUtils.ReplacePlayList,
ElisaUtils.TriggerPlay)
onClicked: {
listView.currentIndex = index
entry.forceActiveFocus()
}
onCallOpenMetaDataView: {
openMetaDataView(databaseId, url, entryType)
}
}
}
ListBrowserView {
id: listView
......@@ -204,7 +172,7 @@ FocusScope {
contentModel: proxyModel
delegate: (displaySingleAlbum ? albumDelegate : detailedTrackDelegate)
delegate: trackDelegate
enableSorting: !displaySingleAlbum
......
/*
SPDX-FileCopyrightText: 2020 (c) Devin Lin <espidev@gmail.com>
SPDX-License-Identifier: LGPL-3.0-or-later
*/
import QtQuick 2.7
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.3
import org.kde.elisa 1.0
import org.kde.kirigami 2.5 as Kirigami
import ".."
RowLayout {
id: durationSlider
property int position
property int duration
property bool seekable
property bool playEnabled
property color labelColor: "white"
property color sliderElapsedColor: "white"
property color sliderRemainingColor: "grey"
property color sliderHandleColor: "white"
property color sliderBorderInactiveColor: "white"
property color sliderBorderActiveColor: "grey"
property int sliderBackgroundHeight: Kirigami.Settings.isMobile ? Math.floor(Kirigami.Units.smallSpacing / 2) : 6
property int sliderHandleSize: Kirigami.Settings.isMobile ? Math.floor(Kirigami.Units.gridUnit * 0.75) : 18
signal seek(int position)
onPositionChanged: {
if (!musicProgress.seekStarted) {
musicProgress.value = position
}
}
onDurationChanged: {
musicProgress.to = durationSlider.duration
musicProgress.value = Qt.binding(function() { return durationSlider.position })
}
spacing: 0
TextMetrics {
id: durationTextMetrics
text: i18nc("This is used to preserve a fixed width for the duration text.", "00:00:00")
}
LabelWithToolTip {
id: positionLabel
text: timeIndicator.progressDuration
color: durationSlider.labelColor
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
Layout.rightMargin: !LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing * 2
Layout.preferredWidth: (durationTextMetrics.boundingRect.width - durationTextMetrics.boundingRect.x) + Kirigami.Units.smallSpacing
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignRight
ProgressIndicator {
id: timeIndicator
position: durationSlider.position
}
}
MouseArea {
id: seekWheelHandler
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.rightMargin: !LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0
Layout.leftMargin: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0
acceptedButtons: Qt.NoButton
onWheel: {
if (wheel.angleDelta.y > 0) {
durationSlider.seek(position + 10000)
} else {
durationSlider.seek(position - 10000)
}
}
// Synthesized slider background that's not actually a part of the
// slider. This is done so the slider's own background can be full
// height yet transparent, for easier clicking
Rectangle {
anchors.left: musicProgress.left
anchors.verticalCenter: musicProgress.verticalCenter
implicitWidth: seekWheelHandler.width
implicitHeight: sliderBackgroundHeight
color: sliderRemainingColor
radius: height / 2
}
Slider {
property bool seekStarted: false
property int seekValue
id: musicProgress
anchors.fill: parent
from: 0
to: durationSlider.duration
enabled: durationSlider.seekable && durationSlider.playEnabled
live: true
onValueChanged: {
if (seekStarted) {
seekValue = value
}
}
onPressedChanged: {
if (pressed) {
seekStarted = true;
seekValue = value
} else {
durationSlider.seek(seekValue)
seekStarted = false;
}
}
// This only provides a full-height area for clicking; see
// https://bugs.kde.org/show_bug.cgi?id=408703. The actual visual
// background is generated above ^^
background: Rectangle {
anchors.fill: parent
implicitWidth: seekWheelHandler.width
implicitHeight: seekWheelHandler.height
color: "transparent"
Rectangle {
anchors.verticalCenter: parent.verticalCenter
x: (LayoutMirroring.enabled ? musicProgress.visualPosition * parent.width : 0)
width: LayoutMirroring.enabled ? parent.width - musicProgress.visualPosition * parent.width: musicProgress.handle.x + radius
height: sliderBackgroundHeight
color: sliderElapsedColor
radius: height / 2
}
}