Rewrites the entire player component.
Not finished ! This is still work in progress and fairly broken in this commit. The jquery jplayer is now wrapped in an angular directive. The queue is no longer being managed in rootScope but is a part of the player service. It can be accessed like before and emptied / filled. The player controller also uses the player service now.
This commit is contained in:
parent
e51961c167
commit
88b1e6a6e6
10 changed files with 132 additions and 63 deletions
|
@ -474,7 +474,7 @@
|
||||||
//$scope.ping();
|
//$scope.ping();
|
||||||
if (globals.settings.SaveTrackPosition) {
|
if (globals.settings.SaveTrackPosition) {
|
||||||
loadTrackPosition();
|
loadTrackPosition();
|
||||||
player.startSaveTrackPosition();
|
//FIXME: player.startSaveTrackPosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* End Startup */
|
/* End Startup */
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
-->
|
-->
|
||||||
</div>
|
</div>
|
||||||
<!-- Player -->
|
<!-- Player -->
|
||||||
<jamstash-player ng-controller="PlayerController"></jamstash-player>
|
<div ng-include src="'player/player.html'" ng-controller="PlayerController"></div>
|
||||||
</div> <!-- End container -->
|
</div> <!-- End container -->
|
||||||
<script>
|
<script>
|
||||||
(function (i, s, o, g, r, a, m) {
|
(function (i, s, o, g, r, a, m) {
|
||||||
|
|
|
@ -1,21 +1,79 @@
|
||||||
angular.module('jamstash.player.directive', [])
|
angular.module('jamstash.player.directive', ['jamstash.settings'])
|
||||||
|
|
||||||
.directive('jamstashPlayer', function(){
|
.directive('jplayer', ['player', 'globals', function(playerService, globals) {
|
||||||
'use strict';
|
'use strict';
|
||||||
// Runs during compile
|
return {
|
||||||
return {
|
restrict: 'EA',
|
||||||
name: 'jamstash.player',
|
template: '<div></div>',
|
||||||
// priority: 1,
|
link: function(scope, element, attrs) {
|
||||||
// terminal: true,
|
|
||||||
scope: true, // {} = isolate, true = child, false/undefined = no change
|
var $player = element.children('div'),
|
||||||
// controller: function($scope, $element, $attrs, $transclude) {},
|
cls = 'pause';
|
||||||
// require: 'ngModel', // Array = multiple requires, ? = optional, ^ = check parent elements
|
console.log($player);
|
||||||
restrict: 'E',
|
var audioSolution = 'html,flash';
|
||||||
// template: '',
|
if (globals.settings.ForceFlash) {
|
||||||
templateUrl: 'player/player.html',
|
audioSolution = 'flash,html';
|
||||||
replace: true,
|
}
|
||||||
// transclude: true,
|
|
||||||
// compile: function(tElement, tAttrs, function transclude(function(scope, cloneLinkingFn){ return function linking(scope, elm, attrs){}})),
|
var updatePlayer = function() {
|
||||||
// link: function($scope, iElm, iAttrs, controller) {}
|
$player.jPlayer({
|
||||||
};
|
// Flash fallback for outdated browser not supporting HTML5 audio/video tags
|
||||||
});
|
// http://jplayer.org/download/
|
||||||
|
swfPath: 'bower_components/jplayer/jquery.jplayer/Jplayer.swf',
|
||||||
|
wmode: 'window',
|
||||||
|
solution: audioSolution,
|
||||||
|
supplied: 'mp3',
|
||||||
|
preload: 'auto',
|
||||||
|
cssSelectorAncestor: '#player',
|
||||||
|
cssSelector: {
|
||||||
|
play: '.PlayTrack',
|
||||||
|
pause: '.PauseTrack',
|
||||||
|
seekBar: '#audiocontainer .scrubber',
|
||||||
|
playBar: '#audiocontainer .progress',
|
||||||
|
mute: '#action_Mute',
|
||||||
|
unmute: '#action_UnMute',
|
||||||
|
volumeMax: '#action_VolumeMax',
|
||||||
|
currentTime: '#played',
|
||||||
|
duration: '#duration'
|
||||||
|
},
|
||||||
|
ready: function() {
|
||||||
|
$player
|
||||||
|
.jPlayer('setMedia', {
|
||||||
|
mp3: 'shot_2.mp3'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
play: function() {
|
||||||
|
console.log('jplayer play');
|
||||||
|
element.addClass(cls);
|
||||||
|
},
|
||||||
|
pause: function() {
|
||||||
|
console.log('jplayer pause');
|
||||||
|
element.removeClass(cls);
|
||||||
|
},
|
||||||
|
stop: function() {
|
||||||
|
console.log('jplayer stop');
|
||||||
|
element.removeClass(cls);
|
||||||
|
},
|
||||||
|
ended: function() {
|
||||||
|
console.log('jplayer ended');
|
||||||
|
element.removeClass(cls);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.end()
|
||||||
|
.unbind('click').click(function(e) {
|
||||||
|
$player.jPlayer(element.hasClass(cls) ? 'stop' : 'play');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePlayer();
|
||||||
|
|
||||||
|
scope.$watch(function () {
|
||||||
|
return playerService.playingSong;
|
||||||
|
}, function (newVal) {
|
||||||
|
console.log('playingSong changed !');
|
||||||
|
$player.jPlayer('setMedia', {'mp3': newVal.url})
|
||||||
|
.jPlayer('play');
|
||||||
|
});
|
||||||
|
} //end link
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
|
|
@ -10,37 +10,42 @@ angular.module('jamstash.player.service', ['jamstash.settings'])
|
||||||
|
|
||||||
var player = {
|
var player = {
|
||||||
queue: [],
|
queue: [],
|
||||||
currentlyPlayingIndex: -1,
|
playingIndex: -1,
|
||||||
|
playingSong: {},
|
||||||
|
|
||||||
play: function(song) {
|
play: function(song) {
|
||||||
song.playing = true;
|
//song.playing = true;
|
||||||
console.log('play()');
|
player.playingSong = song;
|
||||||
|
console.log('player service - play()', song);
|
||||||
},
|
},
|
||||||
|
|
||||||
load: function(song) {
|
load: function(song) {
|
||||||
|
console.log('player service - load()');
|
||||||
},
|
},
|
||||||
|
|
||||||
restart: function() {
|
restart: function() {
|
||||||
console.log('restart()');
|
console.log('player service - restart()');
|
||||||
},
|
},
|
||||||
|
|
||||||
nextTrack: function() {
|
nextTrack: function() {
|
||||||
if((player.currentlyPlayingIndex + 1) < player.queue.length) {
|
console.log('player service - nextTrack()');
|
||||||
var nextTrack = player.queue[player.currentlyPlayingIndex + 1];
|
if((player.playingIndex + 1) < player.queue.length) {
|
||||||
player.currentlyPlayingIndex++;
|
var nextTrack = player.queue[player.playingIndex + 1];
|
||||||
|
player.playingIndex++;
|
||||||
player.play(nextTrack);
|
player.play(nextTrack);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
previousTrack: function() {
|
previousTrack: function() {
|
||||||
if((player.currentlyPlayingIndex - 1) > 0) {
|
console.log(('player service - previousTrack()'));
|
||||||
var previousTrack = player.queue[player.currentlyPlayingIndex - 1];
|
if((player.playingIndex - 1) > 0) {
|
||||||
player.currentlyPlayingIndex--;
|
var previousTrack = player.queue[player.playingIndex - 1];
|
||||||
|
player.playingIndex--;
|
||||||
player.play(previousTrack);
|
player.play(previousTrack);
|
||||||
} else if (player.queue.length > 0) {
|
} else if (player.queue.length > 0) {
|
||||||
player.currentlyPlayingIndex = 0;
|
player.playingIndex = 0;
|
||||||
player.play(player.queue[player.currentlyPlayingIndex]);
|
var firstTrack = player.queue[0];
|
||||||
|
player.play(firstTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,25 +39,25 @@ describe("Player service", function() {
|
||||||
it("and no song is playing, it plays the first song", function() {
|
it("and no song is playing, it plays the first song", function() {
|
||||||
player.nextTrack();
|
player.nextTrack();
|
||||||
|
|
||||||
expect(player.currentlyPlayingIndex).toBe(0);
|
expect(player.playingIndex).toBe(0);
|
||||||
expect(player.play).toHaveBeenCalledWith(player.queue[0]);
|
expect(player.play).toHaveBeenCalledWith(player.queue[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("and the first song is playing, it plays the second song", function() {
|
it("and the first song is playing, it plays the second song", function() {
|
||||||
player.currentlyPlayingIndex = 0;
|
player.playingIndex = 0;
|
||||||
|
|
||||||
player.nextTrack();
|
player.nextTrack();
|
||||||
|
|
||||||
expect(player.currentlyPlayingIndex).toBe(1);
|
expect(player.playingIndex).toBe(1);
|
||||||
expect(player.play).toHaveBeenCalledWith(player.queue[1]);
|
expect(player.play).toHaveBeenCalledWith(player.queue[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("and the last song is playing, it does nothing", function() {
|
it("and the last song is playing, it does nothing", function() {
|
||||||
player.currentlyPlayingIndex = 2;
|
player.playingIndex = 2;
|
||||||
|
|
||||||
player.nextTrack();
|
player.nextTrack();
|
||||||
|
|
||||||
expect(player.currentlyPlayingIndex).toBe(2);
|
expect(player.playingIndex).toBe(2);
|
||||||
expect(player.play).not.toHaveBeenCalled();
|
expect(player.play).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -66,25 +66,25 @@ describe("Player service", function() {
|
||||||
it("and no song is playing, it plays the first song", function() {
|
it("and no song is playing, it plays the first song", function() {
|
||||||
player.previousTrack();
|
player.previousTrack();
|
||||||
|
|
||||||
expect(player.currentlyPlayingIndex).toBe(0);
|
expect(player.playingIndex).toBe(0);
|
||||||
expect(player.play).toHaveBeenCalledWith(player.queue[0]);
|
expect(player.play).toHaveBeenCalledWith(player.queue[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("and the first song is playing, it restarts the first song", function() {
|
it("and the first song is playing, it restarts the first song", function() {
|
||||||
player.currentlyPlayingIndex = 0;
|
player.playingIndex = 0;
|
||||||
|
|
||||||
player.previousTrack();
|
player.previousTrack();
|
||||||
|
|
||||||
expect(player.currentlyPlayingIndex).toBe(0);
|
expect(player.playingIndex).toBe(0);
|
||||||
expect(player.play).toHaveBeenCalledWith(player.queue[0]);
|
expect(player.play).toHaveBeenCalledWith(player.queue[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("and the last song is playing, it plays the second song", function() {
|
it("and the last song is playing, it plays the second song", function() {
|
||||||
player.currentlyPlayingIndex = 2;
|
player.playingIndex = 2;
|
||||||
|
|
||||||
player.previousTrack();
|
player.previousTrack();
|
||||||
|
|
||||||
expect(player.currentlyPlayingIndex).toBe(1);
|
expect(player.playingIndex).toBe(1);
|
||||||
expect(player.play).toHaveBeenCalledWith(player.queue[1]);
|
expect(player.play).toHaveBeenCalledWith(player.queue[1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -106,14 +106,17 @@ describe("Player service", function() {
|
||||||
it("when I play it, the song is marked as playing", function() {
|
it("when I play it, the song is marked as playing", function() {
|
||||||
player.play(song);
|
player.play(song);
|
||||||
|
|
||||||
|
expect(player.playingSong).toBe(song);
|
||||||
expect(song.playing).toBeTruthy();
|
expect(song.playing).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I restart playback, the song is still marked as playing", function() {
|
it("when I restart playback, the song is still marked as playing", function() {
|
||||||
song.playing = true;
|
song.playing = true;
|
||||||
|
player.playingSong = song;
|
||||||
|
|
||||||
player.restart();
|
player.restart();
|
||||||
|
|
||||||
|
expect(player.playingSong).toBe(song);
|
||||||
expect(song.playing).toBeTruthy();
|
expect(song.playing).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -122,20 +125,20 @@ describe("Player service", function() {
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
player.queue = [];
|
player.queue = [];
|
||||||
player.currentlyPlayingIndex = -1;
|
player.playingIndex = -1;
|
||||||
spyOn(player, "play").and.stub();
|
spyOn(player, "play").and.stub();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I call nextTrack, it does nothing", function() {
|
it("when I call nextTrack, it does nothing", function() {
|
||||||
player.nextTrack();
|
player.nextTrack();
|
||||||
expect(player.play).not.toHaveBeenCalled();
|
expect(player.play).not.toHaveBeenCalled();
|
||||||
expect(player.currentlyPlayingIndex).toBe(-1);
|
expect(player.playingIndex).toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when I call previousTrack, it does nothing", function() {
|
it("when I call previousTrack, it does nothing", function() {
|
||||||
player.previousTrack();
|
player.previousTrack();
|
||||||
expect(player.play).not.toHaveBeenCalled();
|
expect(player.play).not.toHaveBeenCalled();
|
||||||
expect(player.currentlyPlayingIndex).toBe(-1);
|
expect(player.playingIndex).toBe(-1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
<a class="hover" id="PreviousTrack" title="Previous Track" ng-click="previousTrack()">
|
<a class="hover" id="PreviousTrack" title="Previous Track" ng-click="previousTrack()">
|
||||||
<img src="images/first_alt_24x24.png" height="24" width="24" alt="Previous track" />
|
<img src="images/first_alt_24x24.png" height="24" width="24" alt="Previous track" />
|
||||||
</a>
|
</a>
|
||||||
<a class="hover PlayTrack" title="Play/Pause" ng-click="defaultPlay()">
|
<a class="hover PlayTrack" title="Play/Pause" ng-click="play()">
|
||||||
<img src="images/play_alt_24x24.png" height="24" width="24" alt="Play" />
|
<img src="images/play_alt_24x24.png" height="24" width="24" alt="Play" />
|
||||||
</a>
|
</a>
|
||||||
<a class="hover PauseTrack" title="Play/Pause" ng-click="defaultPlay()" style="display: none;">
|
<a class="hover PauseTrack" title="Play/Pause" ng-click="play()" style="display: none;">
|
||||||
<img src="images/pause_alt_24x24.png" height="24" width="24" alt="Pause" />
|
<img src="images/pause_alt_24x24.png" height="24" width="24" alt="Pause" />
|
||||||
</a>
|
</a>
|
||||||
<a class="hover" id="NextTrack" title="Next Track" ng-click="nextTrack()">
|
<a class="hover" id="NextTrack" title="Next Track" ng-click="nextTrack()">
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
<!--<div class="jp-volume-bar"><div class="jp-volume-bar-value"></div></div><a href="" id="action_VolumeMax" class="volume" title="Max Volume"></a>-->
|
<!--<div class="jp-volume-bar"><div class="jp-volume-bar-value"></div></div><a href="" id="action_VolumeMax" class="volume" title="Max Volume"></a>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="playdeck_1"></div>
|
<div id="playdeck_1" jplayer></div>
|
||||||
<div id="playdeck_2"></div>
|
<div id="playdeck_2"></div>
|
||||||
<div id="submenu_CurrentPlaylist" class="submenu shadow" style="display: none;">
|
<div id="submenu_CurrentPlaylist" class="submenu shadow" style="display: none;">
|
||||||
<table id="CurrentPlaylistPreviewContainer" class="simplelist songlist">
|
<table id="CurrentPlaylistPreviewContainer" class="simplelist songlist">
|
||||||
|
|
|
@ -8,6 +8,8 @@ angular.module('jamstash.player.controller', ['jamstash.player.service', 'jamsta
|
||||||
.controller('PlayerController', ['$scope', 'player', function($scope, player){
|
.controller('PlayerController', ['$scope', 'player', function($scope, player){
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
$scope.playingSong = player.playingSong;
|
||||||
|
|
||||||
$scope.previousTrack = function () {
|
$scope.previousTrack = function () {
|
||||||
player.previousTrack();
|
player.previousTrack();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
angular.module('jamstash.queue.controller', ['jamstash.player.service'])
|
angular.module('jamstash.queue.controller', ['jamstash.player.service'])
|
||||||
|
|
||||||
.controller('QueueController', ['$scope', '$rootScope', 'globals', 'player',
|
.controller('QueueController', ['$scope', 'globals', 'player',
|
||||||
function ($scope, $rootScope, globals, player) {
|
function ($scope, globals, player) {
|
||||||
'use strict';
|
'use strict';
|
||||||
$scope.settings = globals.settings;
|
$scope.settings = globals.settings;
|
||||||
$scope.song = $rootScope.queue;
|
$scope.song = player.queue;
|
||||||
//angular.copy($rootScope.queue, $scope.song);
|
//angular.copy($rootScope.queue, $scope.song);
|
||||||
$scope.itemType = 'pl';
|
$scope.itemType = 'pl';
|
||||||
|
|
||||||
|
|
|
@ -281,11 +281,12 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
|
||||||
mappedSongs.push(map.mapSong(randomStarredSongs[i]));
|
mappedSongs.push(map.mapSong(randomStarredSongs[i]));
|
||||||
}
|
}
|
||||||
if(action === 'play') {
|
if(action === 'play') {
|
||||||
$rootScope.queue = [].concat(mappedSongs);
|
player.queue = [].concat(mappedSongs);
|
||||||
notifications.updateMessage(mappedSongs.length + ' Song(s) Added to Queue', true);
|
notifications.updateMessage(mappedSongs.length + ' Song(s) Added to Queue', true);
|
||||||
player.play($rootScope.queue[0]);
|
console.log('subjs', player.queue);
|
||||||
|
player.play(player.queue[0]);
|
||||||
} else if (action === 'add') {
|
} else if (action === 'add') {
|
||||||
$rootScope.queue = $rootScope.queue.concat(mappedSongs);
|
player.queue = player.queue.concat(mappedSongs);
|
||||||
notifications.updateMessage(mappedSongs.length + ' Song(s) Added to Queue', true);
|
notifications.updateMessage(mappedSongs.length + ' Song(s) Added to Queue', true);
|
||||||
} else if (action === 'display') {
|
} else if (action === 'display') {
|
||||||
$scope.album = [];
|
$scope.album = [];
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe("Subsonic controller", function() {
|
||||||
});
|
});
|
||||||
spyOn(notifications, 'updateMessage').and.stub();
|
spyOn(notifications, 'updateMessage').and.stub();
|
||||||
spyOn(player, 'play').and.stub();
|
spyOn(player, 'play').and.stub();
|
||||||
$rootScope.queue = [];
|
player.queue = [];
|
||||||
|
|
||||||
$controller('SubsonicController', {
|
$controller('SubsonicController', {
|
||||||
$scope: scope,
|
$scope: scope,
|
||||||
|
@ -64,14 +64,14 @@ describe("Subsonic controller", function() {
|
||||||
$rootScope.$apply();
|
$rootScope.$apply();
|
||||||
|
|
||||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||||
expect($rootScope.queue).toEqual([
|
expect(player.queue).toEqual([
|
||||||
{id: "2548"}, {id: "8986"}, {id: "2986"}
|
{id: "2548"}, {id: "8986"}, {id: "2986"}
|
||||||
]);
|
]);
|
||||||
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
|
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when playing random starred songs, it plays the first selected song, empties the queue and fills it with the selected songs, and notifies the user", function() {
|
it("when playing random starred songs, it plays the first selected song, empties the queue and fills it with the selected songs, and notifies the user", function() {
|
||||||
$rootScope.queue = [{id: "7666"}];
|
player.queue = [{id: "7666"}];
|
||||||
|
|
||||||
scope.getRandomStarredSongs('play');
|
scope.getRandomStarredSongs('play');
|
||||||
deferred.resolve(response);
|
deferred.resolve(response);
|
||||||
|
@ -79,7 +79,7 @@ describe("Subsonic controller", function() {
|
||||||
|
|
||||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||||
expect(player.play).toHaveBeenCalledWith({id: "2548"});
|
expect(player.play).toHaveBeenCalledWith({id: "2548"});
|
||||||
expect($rootScope.queue).toEqual([
|
expect(player.queue).toEqual([
|
||||||
{id: "2548"}, {id: "8986"}, {id: "2986"}
|
{id: "2548"}, {id: "8986"}, {id: "2986"}
|
||||||
]);
|
]);
|
||||||
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
|
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
|
||||||
|
@ -88,7 +88,7 @@ describe("Subsonic controller", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("given that I don't have any starred song in my library, when getting random starred songs, it notifies the user with an error message, does not play a song and does not touch the queue", function() {
|
it("given that I don't have any starred song in my library, when getting random starred songs, it notifies the user with an error message, does not play a song and does not touch the queue", function() {
|
||||||
$rootScope.queue = [{id: "7666"}];
|
player.queue = [{id: "7666"}];
|
||||||
|
|
||||||
scope.getRandomStarredSongs('whatever action');
|
scope.getRandomStarredSongs('whatever action');
|
||||||
deferred.reject({reason: 'No starred songs found on the Subsonic server.'});
|
deferred.reject({reason: 'No starred songs found on the Subsonic server.'});
|
||||||
|
@ -96,7 +96,7 @@ describe("Subsonic controller", function() {
|
||||||
|
|
||||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||||
expect(player.play).not.toHaveBeenCalled();
|
expect(player.play).not.toHaveBeenCalled();
|
||||||
expect($rootScope.queue).toEqual([{id: "7666"}]);
|
expect(player.queue).toEqual([{id: "7666"}]);
|
||||||
expect(notifications.updateMessage).toHaveBeenCalledWith('No starred songs found on the Subsonic server.', true);
|
expect(notifications.updateMessage).toHaveBeenCalledWith('No starred songs found on the Subsonic server.', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue