1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
|
/*
* Copyright (C) 2013, 2014, 2015, 2016
* Andrew Hayzen <ahayzen@gmail.com>
* Daniel Holm <d.holmen@gmail.com>
* Victor Thompson <victor.thompson@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.4
import Ubuntu.Components 1.3
import Ubuntu.Components.Popups 1.3
import Ubuntu.MediaScanner 0.1
import Qt.labs.settings 1.0
import QtMultimedia 5.6
import QtQuick.LocalStorage 2.0
import QtGraphicalEffects 1.0
import "logic/stored-request.js" as StoredRequest
import "logic/meta-database.js" as Library
import "logic/playlists.js" as Playlists
import "components"
import "components/Helpers"
import "ui"
MainView {
objectName: "musicMainView"
applicationName: "com.ubuntu.music"
id: mainView
backgroundColor: styleMusic.mainView.backgroundColor
theme.name: "Ubuntu.Components.Themes.SuruDark"
// Startup settings
Settings {
id: startupSettings
category: "StartupSettings"
property bool firstRun: true
property int queueIndex: 0
property int tabIndex: -1
onFirstRunChanged: {
if (!firstRun) {
StoredRequest.run()
}
}
}
Connections { // save the current queueIndex for when the app restarts
target: player.mediaPlayer.playlist
onCurrentIndexChanged: startupSettings.queueIndex = player.mediaPlayer.playlist.currentIndex
}
// Global keyboard shortcuts
focus: true
Keys.onPressed: {
if(event.key === Qt.Key_Escape) {
if (mainPageStack.currentMusicPage.currentDialog !== null) {
PopupUtils.close(mainPageStack.currentMusicPage.currentDialog)
} else if (mainPageStack.currentMusicPage.searchable && mainPageStack.currentMusicPage.state === "search") {
mainPageStack.currentMusicPage.state = "default"
} else {
mainPageStack.goBack(); // Esc Go back
}
}
else if(event.modifiers === Qt.AltModifier) {
var position;
switch (event.key) {
case Qt.Key_Right: // Alt+Right Seek forward +10secs
position = player.mediaPlayer.position + 10000 < player.mediaPlayer.duration
? player.mediaPlayer.position + 10000 : player.mediaPlayer.duration;
player.mediaPlayer.seek(position);
break;
case Qt.Key_Left: // Alt+Left Seek backwards -10secs
position = player.mediaPlayer.position - 10000 > 0
? player.mediaPlayer.position - 10000 : 0;
player.mediaPlayer.seek(position);
break;
}
}
else if(event.modifiers === Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_Left: // Ctrl+Left Previous Song
player.mediaPlayer.playlist.previousWrapper();
break;
case Qt.Key_Right: // Ctrl+Right Next Song
player.mediaPlayer.playlist.nextWrapper();
break;
case Qt.Key_Up: // Ctrl+Up Volume up
player.mediaPlayer.volume = player.mediaPlayer.volume + .1 > 1 ? 1 : player.mediaPlayer.volume + .1
break;
case Qt.Key_Down: // Ctrl+Down Volume down
player.mediaPlayer.volume = player.mediaPlayer.volume - .1 < 0 ? 0 : player.mediaPlayer.volume - .1
break;
case Qt.Key_R: // Ctrl+R Repeat toggle
player.mediaPlayer.repeat = !player.mediaPlayer.repeat
break;
case Qt.Key_F: // Ctrl+F Show Search popup
if (mainPageStack.currentMusicPage.searchable && mainPageStack.currentMusicPage.state === "default") {
mainPageStack.currentMusicPage.state = "search"
header.show()
}
break;
case Qt.Key_J: // Ctrl+J Jump to playing song
tabs.pushNowPlaying()
mainPageStack.currentPage.setListView(true)
break;
case Qt.Key_N: // Ctrl+N Show Now playing
tabs.pushNowPlaying()
break;
case Qt.Key_P: // Ctrl+P Toggle playing state
player.mediaPlayer.toggle();
break;
case Qt.Key_Q: // Ctrl+Q Quit the app
Qt.quit();
break;
case Qt.Key_U: // Ctrl+U Shuffle toggle
player.mediaPlayer.shuffle = !player.mediaPlayer.shuffle
break;
}
}
}
// Arguments during startup
Arguments {
id: args
//defaultArgument.help: "Expects URI of the track to play." // should be used when bug is resolved
//defaultArgument.valueNames: ["URI"] // should be used when bug is resolved
// grab a file
Argument {
name: "url"
help: "URI for track to run at start."
required: false
valueNames: ["track"]
}
// Debug/development mode
Argument {
name: "debug"
help: "Start Music in a debug mode. Will show more output."
required: false
}
}
Action {
id: nextAction
text: i18n.tr("Next")
keywords: i18n.tr("Next Track")
onTriggered: player.mediaPlayer.playlist.nextWrapper()
}
Action {
id: playsAction
text: player.mediaPlayer.playbackState === MediaPlayer.PlayingState
? i18n.tr("Pause") : i18n.tr("Play")
keywords: player.mediaPlayer.playbackState === MediaPlayer.PlayingState
? i18n.tr("Pause Playback") : i18n.tr("Continue or start playback")
onTriggered: player.mediaPlayer.toggle()
}
Action {
id: backAction
text: i18n.tr("Back")
keywords: i18n.tr("Go back to last page")
onTriggered: mainPageStack.goBack();
}
// With a default Quit action only the first 4 actions are displayed
// until the user searches for them within the HUD
Action {
id: prevAction
text: i18n.tr("Previous")
keywords: i18n.tr("Previous Track")
onTriggered: player.mediaPlayer.playlist.previousWrapper()
}
Action {
id: stopAction
text: i18n.tr("Stop")
keywords: i18n.tr("Stop Playback")
onTriggered: player.mediaPlayer.stop()
}
actions: [nextAction, playsAction, prevAction, stopAction, backAction]
UriHandlerHelper {
id: uriHandler
}
ContentHubHelper {
id: contentHub
}
UserMetricsHelper {
id: userMetrics
}
// Design stuff
Style { id: styleMusic }
width: units.gu(100)
height: units.gu(80)
WorkerWaiter {
id: waitForWorker
}
// Run on startup
Component.onCompleted: {
customdebug("Version "+appVersion) // print the curren version
Library.createRecent() // initialize recent
Library.createQueue() // create queue if it doesn't exist
// initialize playlists
Playlists.initializePlaylist()
if (!args.values.url) {
// load the previous queue as there are no args
// FIXME: load and save do not work yet pad.lv/1510225
// so use our localstorage method for now
// player.mediaPlayer.playlist.load("/home/phablet/.local/share/com.ubuntu.music/queue.m3u")
// use onloaded() and onLoadFailed() to confirm it is complete
if (!Library.isQueueEmpty()) {
console.debug("*** Restoring library queue");
player.mediaPlayer.playlist.addItems(Library.getQueue());
player.mediaPlayer.playlist.setCurrentIndex(queueIndex);
player.mediaPlayer.playlist.setPendingCurrentState(MediaPlayer.PausedState);
}
else {
console.debug("Queue is empty, not loading any recent tracks");
}
}
// everything else
loading.visible = true
// push the page to view
mainPageStack.push(tabs)
// if a tab index exists restore it, otherwise goto Recent if there are items otherwise go to Albums
tabs.selectedTabIndex = startupSettings.tabIndex === -1
? (Library.isRecentEmpty() ? albumsTab.index : 0)
: (startupSettings.tabIndex > tabs.count - 1
? tabs.count - 1 : startupSettings.tabIndex)
loadedUI = true;
// Run post load
tabs.ensurePopulated(tabs.selectedTab);
// Display walkthrough on first run, even if the user has music
if (firstRun) {
mainPageStack.push(Qt.resolvedUrl("components/Walkthrough/FirstRunWalkthrough.qml"), {})
}
if (args.values.url) {
uriHandler.process(args.values.url, true);
}
}
// VARIABLES
property string musicName: i18n.tr("Music")
property string appVersion: '2.5'
property bool toolbarShown: musicToolbar.visible
property bool selectedAlbum: false
property alias firstRun: startupSettings.firstRun
property alias queueIndex: startupSettings.queueIndex
property bool noMusic: allSongsModel.rowCount === 0 && allSongsModelModel.status === SongsModel.Ready && loadedUI
property bool emptyState: noMusic && !firstRun && !contentHub.processing
property Page emptyPage
signal listItemSwiping(int i)
property bool wideAspect: width >= units.gu(95) && loadedUI
property bool loadedUI: false // property to detect if the UI has finished
// FUNCTIONS
onEmptyStateChanged: {
if (emptyState) {
emptyPage = mainPageStack.push(Qt.resolvedUrl("ui/LibraryEmptyState.qml"), {})
} else {
mainPageStack.popPage(emptyPage)
}
}
// Custom debug funtion that's easier to shut off
function customdebug(text) {
var debug = true; // set to "0" for not debugging
//if (args.values.debug) { // *USE LATER*
if (debug) {
console.debug(i18n.tr("Debug: ")+text);
}
}
// Converts an duration in ms to a formated string ("minutes:seconds")
function durationToString(duration) {
var minutes = Math.floor((duration/1000) / 60);
var seconds = Math.floor((duration/1000)) % 60;
// Make sure that we never see "NaN:NaN"
if (minutes.toString() == 'NaN')
minutes = 0;
if (seconds.toString() == 'NaN')
seconds = 0;
return minutes + ":" + (seconds<10 ? "0"+seconds : seconds);
}
// Make dictionary from model item
function makeDict(model) {
return {
album: model.album,
art: model.art,
author: model.author,
filename: model.filename || model.source,
title: model.title
};
}
// Clear the queue, queue this model and play the specific index
function trackClicked(model, index, play) {
// TODO: remove once playlists uses U1DB
if (model.hasOwnProperty("linkLibraryListModel")) {
model = model.linkLibraryListModel;
}
var file = Qt.resolvedUrl(model.get(index, model.RoleModelData).filename);
play = play === undefined ? true : play // default play to true
player.mediaPlayer.playlist.clearWrapper(); // clear the old model
player.mediaPlayer.playlist.setCurrentIndex(index);
player.mediaPlayer.playlist.addItemsFromModel(model);
if (play) {
// Set the pending state for the playlist
// this will be set once the currentIndex has been appened to the playlist
player.mediaPlayer.playlist.setPendingCurrentState(MediaPlayer.PlayingState);
// Show the Now playing page and make sure the track is visible
tabs.pushNowPlaying();
}
}
// Play or pause a current track in the queue
// - the index has been tapped by the user
function trackQueueClick(index) {
if (player.mediaPlayer.playlist.currentIndex === index) {
player.mediaPlayer.toggle();
} else {
player.mediaPlayer.playlist.setCurrentIndex(index);
player.mediaPlayer.playlist.setPendingCurrentState(MediaPlayer.PlayingState);
}
}
// Clear the queue and play a random track from this model
// - user has selected "Shuffle" in album/artists or "Tap to play random"
function playRandomSong(model)
{
// If no model is given use all the tracks
if (model === undefined) {
model = allSongsModel;
}
player.mediaPlayer.playlist.clearWrapper();
player.mediaPlayer.playlist.addItemsFromModel(model);
player.shuffle = true;
// Once the model count has been reached in the queue
// shuffle the model
player.mediaPlayer.playlist.setPendingShuffle(model.count);
tabs.pushNowPlaying();
}
// Wrapper function around decodeURIComponent() to prevent exceptions
// from bubbling up to the app.
function decodeFileURI(filename)
{
var newFilename = "";
try {
newFilename = decodeURIComponent(filename);
} catch (e) {
newFilename = filename;
console.log("Unicode decoding error:", filename, e.message)
}
return newFilename;
}
// Load mediascanner store
MediaStore {
id: musicStore
}
SortFilterModel {
id: allSongsModel
objectName: "allSongsModel"
property alias rowCount: allSongsModelModel.rowCount
model: SongsModel {
id: allSongsModelModel
objectName: "allSongsModelModel"
store: musicStore
// if any tracks are removed from ms2 then check they are not in the queue
onFilled: {
var i
var removed = []
// Find tracks from the queue that aren't in ms2 anymore
for (i=0; i < player.mediaPlayer.playlist.count; i++) {
var file = decodeFileURI(player.mediaPlayer.playlist.itemSource(i));
// ms2 doesn't expect the URI scheme so strip file://
if (file.indexOf("file://") === 0) {
file = file.substr(7);
}
if (musicStore.lookup(file) === null) {
removed.push(i)
}
}
// If there are removed tracks then remove them from the queue and store
if (removed.length > 0) {
console.debug("Removed queue:", JSON.stringify(removed))
player.mediaPlayer.playlist.removeItemsWrapper(removed.slice());
}
// Loop through playlists, getPlaylistTracks will remove any tracks that don't exist
var playlists = Playlists.getPlaylists()
for (i=0; i < playlists.length; i++) {
Playlists.getPlaylistTracks(playlists[i].name)
}
// TODO: improve in refactoring to be able detect when a track is removed
// Update playlists page
if (tabs.selectedTab == playlistsTab) {
playlistModel.filterPlaylists()
}
}
}
sort.property: "title"
sort.order: Qt.AscendingOrder
sortCaseSensitivity: Qt.CaseInsensitive
}
AlbumsModel {
id: allAlbumsModel
store: musicStore
// if any tracks are removed from ms2 then check they are not in recent
onFilled: {
var albums = []
var i
var removed = []
for (i=0; i < allAlbumsModel.count; i++) {
albums.push(allAlbumsModel.get(i, allAlbumsModel.RoleTitle))
}
// Find albums from recent that aren't in ms2 anymore
var recent = Library.getRecent()
for (i=0; i < recent.length; i++) {
if (recent[i].type === "album" && albums.indexOf(recent[i].data) === -1) {
removed.push(recent[i].data)
}
}
// If there are removed tracks then remove them from recent
if (removed.length > 0) {
console.debug("Removed recent:", JSON.stringify(removed))
Library.recentRemoveAlbums(removed)
if (recentPage.visible) {
recentModel.filterRecent()
} else {
recentPage.changed = true
}
}
}
}
SongsModel {
id: songsAlbumArtistModel
store: musicStore
onStatusChanged: {
if (status === SongsModel.Ready) {
// Play album it tracks exist
if (rowCount > 0 && selectedAlbum) {
trackClicked(songsAlbumArtistModel, 0, true);
// Add album to recent list
Library.addRecent(songsAlbumArtistModel.get(0, SongsModel.RoleModelData).album, "album")
recentModel.filterRecent()
} else if (selectedAlbum) {
console.debug("Unknown artist-album " + artist + "/" + album + ", skipping")
}
selectedAlbum = false;
// Clear filter for artist and album
songsAlbumArtistModel.albumArtist = ""
songsAlbumArtistModel.album = ""
}
}
}
// WHERE THE MAGIC HAPPENS
Player {
id: player
}
// TODO: Used by playlisttracks move to U1DB
LibraryListModel {
id: albumTracksModel
}
// TODO: used by recent items move to U1DB
LibraryListModel {
id: recentModel
property bool complete: false
onPreLoadCompleteChanged: {
complete = true;
if (preLoadComplete)
{
loading.visible = false
recentTabRepeater.loading = false
recentTabRepeater.populated = true
// Ensure any active tabs are insync as they use a copy of the repeater state
for (var i=0; i < recentTabRepeater.count; i++) {
recentTabRepeater.itemAt(i).loading = false
recentTabRepeater.itemAt(i).populated = true
}
}
}
}
// TODO: list of playlists move to U1DB
// create the listmodel to use for playlists
LibraryListModel {
id: playlistModel
syncFactor: 1
onPreLoadCompleteChanged: {
if (preLoadComplete)
{
loading.visible = false
playlistsTab.loading = false
playlistsTab.populated = true
}
}
}
PageStack {
id: mainPageStack
anchors {
bottom: parent.bottom
fill: undefined
left: parent.left
right: nowPlayingSidebarLoader.left
top: parent.top
}
clip: true // otherwise listitems actions overflow
// Properties storing the current page info
property Page currentMusicPage: null // currentPage can be Tabs
property bool popping: false
/* Helper functions */
// Go back up the stack if possible
function goBack() {
// Ensure in the case that goBack is called programmatically that any dialogs are closed
if (mainPageStack.currentMusicPage && mainPageStack.currentMusicPage.currentDialog !== null) {
PopupUtils.close(mainPageStack.currentMusicPage.currentDialog)
}
if (depth > 1) {
pop()
}
}
// Pop a specific page in the stack
function popPage(page) {
var tmpPages = []
// Ensure in the case that popPage is called programmatically that any dialogs are closed
if (page.currentDialog !== undefined && page.currentDialog !== null) {
PopupUtils.close(page.currentDialog)
}
popping = true
while (currentPage !== page && depth > 0) {
tmpPages.push(currentPage)
pop()
}
if (depth > 0) {
pop()
}
for (var i=tmpPages.length - 1; i > -1; i--) {
push(tmpPages[i])
}
popping = false
}
// Set the current page, and any parent/stacks
function setPage(childPage) {
if (!popping) {
currentMusicPage = childPage;
}
}
Tabs {
id: tabs
anchors {
fill: parent
}
property Tab lastTab: selectedTab
property list<Action> tabActions: [
Action {
enabled: recentTabRepeater.count > 0
objectName: "recentTabAction"
text: enabled ? recentTabRepeater.itemAt(0).title : ""
visible: enabled
onTriggered: {
if (enabled) {
tabs.selectedTabIndex = recentTabRepeater.itemAt(0).index
}
}
},
Action {
objectName: "artistsTabAction"
text: artistsTab.title
onTriggered: tabs.selectedTabIndex = artistsTab.index
},
Action {
objectName: "albumsTabAction"
text: albumsTab.title
onTriggered: tabs.selectedTabIndex = albumsTab.index
},
Action {
objectName: "genresTabAction"
text: genresTab.title
onTriggered: tabs.selectedTabIndex = genresTab.index
},
Action {
objectName: "songsTabAction"
text: songsTab.title
onTriggered: tabs.selectedTabIndex = songsTab.index
},
Action {
objectName: "playlistsTabAction"
text: playlistsTab.title
onTriggered: tabs.selectedTabIndex = playlistsTab.index
}
]
property bool completed: false
Component.onCompleted: completed = true
onSelectedTabChanged: {
// pause loading of the models in the old tab
if (lastTab !== null && lastTab !== selectedTab) {
allowLoading(lastTab, false);
}
lastTab = selectedTab;
ensurePopulated(selectedTab);
}
onSelectedTabIndexChanged: {
if (loadedUI) { // store the tab index if changed by the user
startupSettings.tabIndex = selectedTabIndex
}
}
// Use a repeater to 'hide' the recent tab when the model is empty
// A repeater is used because the Tabs component respects adds and
// removes. Whereas replacing the list tabChildren does not appear
// to respect removes and setting the page as active: false causes
// the page to be blank but the action to still in the overflow
Repeater {
id: recentTabRepeater
// If the model has not loaded and at startup the db was not empty
// then show recent or
// If the workerlist has been set and it has values then show recent
model: (!recentModel.preLoadComplete && !startupRecentEmpty) ||
(recentModel.workerList !== undefined &&
recentModel.workerList.length > 0) ? 1 : 0
delegate: Component {
// First tab is all music
Tab {
property bool populated: recentTabRepeater.populated
property var loader: [recentModel.filterRecent]
property bool loading: recentTabRepeater.loading
property var model: [recentModel, albumTracksModel]
id: recentTab
objectName: "recentTab"
anchors.fill: parent
title: i18n.tr("Recent")
// Tab content begins here
page: Loader {
width: mainPageStack.width
height: mainPageStack.height
active: tabs.selectedTab == recentTab
source: Qt.resolvedUrl("ui/Recent.qml")
}
}
}
// Store the startup state of the db separately otherwise
// it breaks the binding of model
property bool startupRecentEmpty: Library.isRecentEmpty()
// cached values of the recent model that are copied when
// the tab is created
property bool loading: false
property bool populated: false
onCountChanged: {
if (count === 0 && loadedUI) {
// Jump to the albums tab when recent is empty
tabs.selectedTabIndex = albumsTab.index
} else if (count > 0 && !loadedUI) {
// UI is still loading and recent tab has been inserted
// so move the selected index 'down' as the value is
// not auto updated - this is for the case of loading
// directly to the recent tab (otherwise the content
// appears as the second tab but the tabs think they are
// on the first tab)
tabs.selectedTabIndex -= 1
} else if (count > 0 && loadedUI) {
// tab inserted while the app is running so move the
// selected index 'up' to keep the same position
tabs.selectedTabIndex += 1
}
}
}
// Second tab is arists
Tab {
property bool populated: true
property var loader: []
property bool loading: false
property var model: []
id: artistsTab
objectName: "artistsTab"
anchors.fill: parent
title: i18n.tr("Artists")
// tab content
page: Loader {
width: mainPageStack.width
height: mainPageStack.height
// condition on tabs.completed necessary to avoid QTBUG 54657
// https://bugreports.qt.io/browse/QTBUG-54657
active: tabs.completed && tabs.selectedTab == artistsTab
source: Qt.resolvedUrl("ui/Artists.qml")
}
}
// third tab is albums
Tab {
property bool populated: true
property var loader: []
property bool loading: false
property var model: []
id: albumsTab
objectName: "albumsTab"
anchors.fill: parent
title: i18n.tr("Albums")
// Tab content begins here
page: Loader {
width: mainPageStack.width
height: mainPageStack.height
// condition on tabs.completed necessary to avoid QTBUG 54657
// https://bugreports.qt.io/browse/QTBUG-54657
active: tabs.completed && tabs.selectedTab == albumsTab
source: Qt.resolvedUrl("ui/Albums.qml")
}
}
// forth tab is genres
Tab {
property bool populated: true
property var loader: []
property bool loading: false
property var model: []
id: genresTab
objectName: "genresTab"
anchors.fill: parent
title: i18n.tr("Genres")
// Tab content begins here
page: Loader {
width: mainPageStack.width
height: mainPageStack.height
// condition on tabs.completed necessary to avoid QTBUG 54657
// https://bugreports.qt.io/browse/QTBUG-54657
active: tabs.completed && tabs.selectedTab == genresTab
source: Qt.resolvedUrl("ui/Genres.qml")
}
}
// fourth tab is all songs
Tab {
property bool populated: true
property var loader: []
property bool loading: false
property var model: []
id: songsTab
objectName: "songsTab"
anchors.fill: parent
title: i18n.tr("Tracks")
// Tab content begins here
page: Loader {
width: mainPageStack.width
height: mainPageStack.height
// condition on tabs.completed necessary to avoid QTBUG 54657
// https://bugreports.qt.io/browse/QTBUG-54657
active: tabs.completed && tabs.selectedTab == songsTab
source: Qt.resolvedUrl("ui/Songs.qml")
}
}
// fifth tab is the playlists
Tab {
property bool populated: false
property var loader: [playlistModel.filterPlaylists]
property bool loading: false
property var model: [playlistModel, albumTracksModel]
id: playlistsTab
objectName: "playlistsTab"
anchors.fill: parent
title: i18n.tr("Playlists")
// Tab content begins here
page: Loader {
width: mainPageStack.width
height: mainPageStack.height
// condition on tabs.completed necessary to avoid QTBUG 54657
// https://bugreports.qt.io/browse/QTBUG-54657
active: tabs.completed && tabs.selectedTab == playlistsTab
source: Qt.resolvedUrl("ui/Playlists.qml")
}
}
// Set the models in the tab to allow/disallow loading
function allowLoading(tabToLoad, state)
{
if (tabToLoad && tabToLoad.model !== undefined)
{
for (var i=0; i < tabToLoad.model.length; i++)
{
tabToLoad.model[i].canLoad = state;
}
}
}
function ensurePopulated(selectedTab)
{
if (selectedTab) { // check not null or undefined
allowLoading(selectedTab, true); // allow loading of the models
if (!selectedTab.populated && !selectedTab.loading && loadedUI) {
loading.visible = true
selectedTab.loading = true
if (selectedTab.loader !== undefined)
{
for (var i=0; i < selectedTab.loader.length; i++)
{
selectedTab.loader[i]();
}
}
}
loading.visible = selectedTab.loading || !selectedTab.populated
}
}
function pushNowPlaying()
{
if (!wideAspect) {
// only push if on a different page
if (mainPageStack.currentPage.title !== i18n.tr("Now playing")) {
mainPageStack.push(Qt.resolvedUrl("ui/NowPlaying.qml"), {})
}
if (mainPageStack.currentPage.isListView === true) {
mainPageStack.currentPage.setListView(false); // ensure full view
}
}
}
} // end of tabs
}
//
// Components that are ontop of the PageStack
//
Loader {
id: nowPlayingSidebarLoader
anchors { // start offscreen
bottom: parent.bottom
left: parent.right
leftMargin: shown && status === Loader.Ready ? -width : 0
top: parent.top
}
asynchronous: true
// use source as empty string instead of active false otherwise item
// isn't fully unloaded and appears in autopilot twice
// http://doc.qt.io/qt-5/qml-qtquick-loader.html#source-prop
source: shown || anchors.leftMargin < 0 ? "components/NowPlayingSidebar.qml" : ""
visible: width > 0
width: units.gu(40)
property bool shown: loadedUI && wideAspect && player.mediaPlayer.playlist.itemCount > 0
Behavior on anchors.leftMargin {
NumberAnimation {
}
}
}
Loader {
id: musicToolbar
anchors {
left: parent.left
right: parent.right
top: parent.bottom
topMargin: !wideAspect && status === Loader.Ready ? -height : 0
}
asynchronous: true
// use source as empty string instead of active false otherwise item
// isn't fully unloaded and appears in autopilot twice
// http://doc.qt.io/qt-5/qml-qtquick-loader.html#source-prop
source: !wideAspect || anchors.topMargin < 0 ? "components/MusicToolbar.qml" : ""
visible: (mainPageStack.currentPage && (mainPageStack.currentPage.showToolbar || mainPageStack.currentPage.showToolbar === undefined)) &&
!firstRun &&
!noMusic &&
anchors.topMargin < 0
Behavior on anchors.topMargin {
NumberAnimation {
}
}
}
LoadingSpinnerComponent {
id: loading
}
} // end of main view
|