1/* 2 * Copyright (C) 2019 3 * Jean-Luc Barriere <jlbarriere68@gmail.com> 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; version 3. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18import QtQuick 2.9 19import QtQuick.Controls 2.2 20import QtQml.Models 2.3 21import NosonApp 1.0 22import NosonMediaScanner 1.0 23import "../components" 24import "../components/Delegates" 25import "../components/Flickables" 26import "../components/Dialog" 27 28MusicPage { 29 id: songStackPage 30 objectName: "songsPage" 31 visible: false 32 pageFlickable: songList 33 isListView: true 34 listview: songList 35 36 property string line1: "" 37 property string line2: "" 38 property var covers: [] 39 property int coverFlow: 1 40 property bool isAlbum: false 41 property string year: "" 42 property string album: "" 43 property string artist: "" 44 property string genre: "" 45 property string composer: "" 46 47 // enable selection toolbar 48 selectable: true 49 50 onStateChanged: { 51 if (state === "selection") 52 songList.state = "selection" 53 else 54 songList.state = "default" 55 } 56 57 TrackList { 58 id: tracks 59 album: songStackPage.album 60 artist: songStackPage.artist 61 genre: songStackPage.genre 62 composer: songStackPage.composer 63 Component.onCompleted: init() 64 } 65 66 SortFilterModel { 67 id: songsModel 68 model: tracks 69 sort.property: isAlbum ? "albumTrackNo" : "normalized" 70 sort.order: Qt.AscendingOrder 71 sortCaseSensitivity: Qt.CaseInsensitive 72 } 73 74 function makeFileCoverSource(modelItem) { 75 var art = ""; 76 if (modelItem.hasArt) 77 art = player.makeFilePictureLocalURL(modelItem.filePath); 78 return makeCoverSource(art, modelItem.author, modelItem.album); 79 } 80 81 function makeItemPayload(modelItem) { 82 return player.makeFileStreamItem(modelItem.filePath, modelItem.codec, modelItem.title, modelItem.album, 83 modelItem.author, modelItem.duration.toString(), modelItem.hasArt); 84 } 85 86 function makeContainerPayloads(model) { 87 var items = []; 88 for (var i = 0; i < model.count; i++) { 89 var item = model.get(i); 90 items.push({id: item.id, payload: makeItemPayload(item)}); 91 } 92 return items; 93 } 94 95 Repeater { 96 id: songModelRepeater 97 model: songsModel 98 99 delegate: Item { 100 property string art: model.art 101 property string artist: model.author 102 property string album: model.album 103 property string filePath: model.filePath 104 property bool hasArt: model.hasArt 105 } 106 property bool hasCover: covers.length ? true : false 107 108 onItemAdded: { 109 if (!hasCover) { 110 if (item.art !== "") { 111 songStackPage.covers = [{art: item.art}]; 112 hasCover = true; 113 } else if (item.hasArt) { 114 item.art = player.makeFilePictureLocalURL(item.filePath); 115 songStackPage.covers = [{art: item.art}]; 116 hasCover = true; 117 } 118 } 119 } 120 } 121 122 BlurredBackground { 123 id: blurredBackground 124 height: parent.height 125 } 126 127 Component { 128 id: dragDelegate 129 130 SelectMusicListItem { 131 id: listItem 132 listview: songList 133 reorderable: false 134 selectable: true 135 136 onClick: { 137 var item = { 138 id: model.id, title: model.title, album: model.album, author: model.author, albumTrackNo: model.albumTrackNo, 139 payload: makeItemPayload(model) 140 }; 141 var arts = []; 142 if (isAlbum) 143 arts = covers; // header covers 144 else 145 arts = [{art: imageSource}]; // item cover 146 147 dialogSongInfo.open(item, arts, 148 "qrc:/controls2/ThisDevice/ArtistView.qml", 149 { 150 "artist": model.author, 151 "covers": makeCoverSource(undefined, model.author, undefined), 152 "pageTitle": qsTr("Artist") 153 }, 154 true, // force show more 155 true, // can play 156 true // can queue 157 ); 158 } 159 160 color: "transparent" 161 162 noCover: "qrc:/images/no_cover.png" 163 rowNumber: songStackPage.isAlbum ? model.albumTrackNo > 0 ? model.albumTrackNo.toString() : "#" : "" 164 imageSources: !songStackPage.isAlbum ? makeFileCoverSource(model) : [] 165 description: qsTr("Song") 166 167 onImageError: model.art = "" // reset invalid url from model 168 onActionPressed: { 169 var payload = makeItemPayload(model); 170 trackClicked({id: model.id, payload: payload}, true); 171 } 172 actionVisible: true 173 actionIconSource: "qrc:/images/media-preview-start.svg" 174 menuVisible: true 175 176 menuItems: [ 177 AddToQueue { 178 modelItem: model 179 } 180 ] 181 182 coverSize: units.gu(5) 183 184 column: Column { 185 Label { 186 id: trackTitle 187 color: styleMusic.view.primaryColor 188 font.pointSize: units.fs("medium") 189 text: model.title 190 } 191 192 Label { 193 id: trackArtist 194 color: styleMusic.view.secondaryColor 195 font.pointSize: units.fs("x-small") 196 text: model.author 197 visible: !isAlbum 198 } 199 200 Label { 201 id: trackInfo 202 color: styleMusic.view.secondaryColor 203 font.pointSize: units.fs(isAlbum ? "small" : "x-small") 204 text: model.codec + " " + (model.bitRate > 999999 ? model.sampleRate : model.bitRate > 0 ? Math.round(model.bitRate/1000) + "k" : "") 205 } 206 } 207 208 Component.onCompleted: { 209 if (model.year > 0) { 210 songStackPage.year = model.year 211 } 212 } 213 } 214 } 215 216 MultiSelectListView { 217 id: songList 218 anchors.fill: parent 219 220 header: MusicHeader { 221 id: blurredHeader 222 rightColumn: Column { 223 spacing: units.gu(1) 224 ShuffleButton { 225 model: songsModel 226 width: units.gu(24) 227 } 228 QueueAllButton { 229 model: songsModel 230 width: units.gu(24) 231 } 232 PlayAllButton { 233 model: songsModel 234 width: units.gu(24) 235 } 236 } 237 height: contentHeight 238 coverSources: songStackPage.covers 239 coverFlow: songStackPage.coverFlow 240 titleColumn: Column { 241 spacing: units.gu(1) 242 243 Label { 244 id: albumLabel 245 anchors { 246 left: parent.left 247 right: parent.right 248 } 249 color: styleMusic.view.foregroundColor 250 elide: Text.ElideRight 251 font.pointSize: units.fs("x-large") 252 maximumLineCount: 1 253 text: line2 254 wrapMode: Text.NoWrap 255 } 256 257 Label { 258 id: albumArtist 259 anchors { 260 left: parent.left 261 right: parent.right 262 } 263 color: styleMusic.view.secondaryColor 264 elide: Text.ElideRight 265 font.pointSize: units.fs("small") 266 maximumLineCount: 1 267 text: line1 268 visible: line1 !== "" 269 wrapMode: Text.NoWrap 270 } 271 272 Label { 273 id: albumYear 274 anchors { 275 left: parent.left 276 right: parent.right 277 } 278 color: styleMusic.view.secondaryColor 279 elide: Text.ElideRight 280 font.pointSize: units.fs("small") 281 maximumLineCount: 1 282 text: isAlbum 283 ? (year !== "" ? year + " | " : "") + qsTr("%n song(s)", "", songsModel.count) 284 : qsTr("%n song(s)", "", songsModel.count) 285 wrapMode: Text.NoWrap 286 } 287 288 Item { 289 id: spacer 290 width: parent.width 291 height: units.gu(1) 292 } 293 } 294 295 onFirstSourceChanged: { 296 blurredBackground.art = firstSource 297 } 298 } 299 300 model: DelegateModel { 301 id: visualModel 302 model: songsModel 303 delegate: dragDelegate 304 } 305 306 Connections { 307 target: songStackPage 308 function onSelectAllClicked() { songList.selectAll() } 309 function onSelectNoneClicked() { songList.selectNone() } 310 function onAddToQueueClicked() { 311 var indicies = songList.getSelectedIndices(); 312 var items = []; 313 for (var i = 0; i < indicies.length; i++) { 314 var itemModel = songsModel.get(indicies[i]); 315 var payload = makeItemPayload(itemModel); 316 items.push({id: itemModel.id, payload: payload}); 317 } 318 if (addMultipleItemsToQueue(items)) { 319 songList.selectNone() 320 songStackPage.state = "default" 321 } 322 } 323 } 324 325 onHasSelectionChanged: { 326 songStackPage.selectAllVisible = !hasSelection 327 songStackPage.selectNoneVisible = hasSelection 328 songStackPage.addToQueueVisible = hasSelection 329 songStackPage.addToPlaylistVisible = false 330 songStackPage.removeSelectedVisible = false 331 332 } 333 334 Component.onCompleted: { 335 songStackPage.selectAllVisible = !hasSelection 336 songStackPage.selectNoneVisible = hasSelection 337 songStackPage.addToQueueVisible = hasSelection 338 songStackPage.addToPlaylistVisible = false 339 songStackPage.removeSelectedVisible = false 340 } 341 } 342} 343