First complete fonctional version of web interface

This commit is contained in:
BrokenFire 2017-12-28 17:19:25 +01:00
parent 4da9d6f903
commit e466f61413
12 changed files with 337 additions and 145 deletions

View File

@ -5,6 +5,7 @@ import net.Broken.MainBot;
import net.Broken.Outils.EmbedMessageUtils;
import net.Broken.Outils.MessageTimeOut;
import net.Broken.audio.AudioM;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.core.entities.VoiceChannel;
import net.dv8tion.jda.core.events.message.MessageReceivedEvent;
@ -18,8 +19,8 @@ import java.util.List;
public class Music implements Commande {
public AudioM audio;
Logger logger = LogManager.getLogger();
public Music() {
audio = new AudioM();
public Music(Guild guild) {
audio = new AudioM(guild);
}
@Override

View File

@ -54,7 +54,7 @@ public class Init {
MainBot.commandes.put("spam", new Spam());
MainBot.commandes.put("spaminfo", new SpamInfo());
MainBot.commandes.put("flush", new Flush());
MainBot.commandes.put("music", new Music());
MainBot.commandes.put("music", new Music(jda.getGuilds().get(0)));
if (!dev) {
MainBot.commandes.put("ass", new Ass());

View File

@ -3,5 +3,7 @@ package net.Broken.RestApi.Data;
public class CommandPostData {
public String command;
public String data;
public boolean onHead;
public String url;
public int playlistLimit;
}

View File

@ -1,6 +1,10 @@
package net.Broken.RestApi;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import net.Broken.Commandes.Music;
@ -9,12 +13,13 @@ import net.Broken.RestApi.Data.CommandPostData;
import net.Broken.RestApi.Data.CommandResponseData;
import net.Broken.RestApi.Data.CurrentMusicData;
import net.Broken.RestApi.Data.PlaylistData;
import net.Broken.audio.AudioM;
import net.Broken.audio.NotConectedException;
import net.Broken.audio.NullMusicManager;
import net.Broken.audio.WebLoadUtils;
import net.dv8tion.jda.core.events.message.MessageReceivedEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
@ -32,6 +37,8 @@ public class MusicWebController {
@RequestMapping("/currentMusicInfo")
public CurrentMusicData test(){
Music musicCommande = (Music) MainBot.commandes.get("music");
if(musicCommande.audio.getGuild().getAudioManager().isConnected()){
try {
AudioPlayer player = musicCommande.audio.getMusicManager().player;
AudioTrack currentTrack = player.getPlayingTrack();
@ -43,6 +50,10 @@ public class MusicWebController {
} catch (NullMusicManager | NotConectedException nullMusicManager) {
return new CurrentMusicData(null,0, "STOP",false);
}
}else
{
return new CurrentMusicData(null,0, "DISCONNECTED",false);
}
}
@RequestMapping("/getPlaylist")
@ -60,37 +71,40 @@ public class MusicWebController {
@RequestMapping(value = "/command", method = RequestMethod.POST)
public ResponseEntity<CommandResponseData> command(@RequestBody CommandPostData data){
if(data.command != null){
if(data.command != null) {
logger.info("receive command: " + data.command);
Music musicCommande = (Music) MainBot.commandes.get("music");
switch (data.command){
switch (data.command) {
case "PLAY":
try {
musicCommande.getAudioManager().getMusicManager().scheduler.resume();
return new ResponseEntity<>(new CommandResponseData(data.command,"Accepted"), HttpStatus.OK);
return new ResponseEntity<>(new CommandResponseData(data.command, "Accepted"), HttpStatus.OK);
} catch (NullMusicManager | NotConectedException nullMusicManager) {
return new ResponseEntity<>(new CommandResponseData(data.command,"Not connected to vocal!"), HttpStatus.NOT_ACCEPTABLE);
return new ResponseEntity<>(new CommandResponseData(data.command, "Not connected to vocal!"), HttpStatus.NOT_ACCEPTABLE);
}
case "PAUSE":
try {
musicCommande.getAudioManager().getMusicManager().scheduler.pause();
return new ResponseEntity<>(new CommandResponseData(data.command,"Accepted"), HttpStatus.OK);
return new ResponseEntity<>(new CommandResponseData(data.command, "Accepted"), HttpStatus.OK);
} catch (NullMusicManager | NotConectedException nullMusicManager) {
return new ResponseEntity<>(new CommandResponseData(data.command,"Not connected to vocal!"), HttpStatus.NOT_ACCEPTABLE);
return new ResponseEntity<>(new CommandResponseData(data.command, "Not connected to vocal!"), HttpStatus.NOT_ACCEPTABLE);
}
case "NEXT":
try {
musicCommande.getAudioManager().getMusicManager().scheduler.nextTrack();
return new ResponseEntity<>(new CommandResponseData(data.command,"Accepted"), HttpStatus.OK);
return new ResponseEntity<>(new CommandResponseData(data.command, "Accepted"), HttpStatus.OK);
} catch (NullMusicManager | NotConectedException nullMusicManager) {
return new ResponseEntity<>(new CommandResponseData(data.command,"Not connected to vocal!"), HttpStatus.NOT_ACCEPTABLE);
return new ResponseEntity<>(new CommandResponseData(data.command, "Not connected to vocal!"), HttpStatus.NOT_ACCEPTABLE);
}
case "STOP":
musicCommande.getAudioManager().stop((MessageReceivedEvent) null);
return new ResponseEntity<>(new CommandResponseData(data.command,"Accepted"), HttpStatus.OK);
return new ResponseEntity<>(new CommandResponseData(data.command, "Accepted"), HttpStatus.OK);
case "ADD":
return new WebLoadUtils(musicCommande ,data).getResponse();
}
}
@ -98,4 +112,8 @@ public class MusicWebController {
logger.info("Null");
return new ResponseEntity<>(new CommandResponseData(null, null), HttpStatus.NO_CONTENT);
}
}

View File

@ -31,28 +31,34 @@ public class AudioM {
private int listTimeOut = 30;
private int listExtremLimit = 300;
private Logger logger = LogManager.getLogger();
private Guild guild;
public AudioM() {
public AudioM(Guild guild) {
this.playerManager = new DefaultAudioPlayerManager();
AudioSourceManagers.registerRemoteSources(playerManager);
AudioSourceManagers.registerLocalSource(playerManager);
this.guild = guild;
}
public void loadAndPlay(MessageReceivedEvent event, VoiceChannel voiceChannel, final String trackUrl,int playlistLimit,boolean onHead) {
GuildMusicManager musicManager = getGuildAudioPlayer(event.getGuild());
public void loadAndPlay(MessageReceivedEvent event, VoiceChannel voiceChannel, final String trackUrl, int playlistLimit, boolean onHead) {
GuildMusicManager musicManager = getGuildAudioPlayer(guild);
playedChanel = voiceChannel;
playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() {
@Override
public void trackLoaded(AudioTrack track) {
logger.info("Single Track detected!");
Message message = event.getTextChannel().sendMessage(EmbedMessageUtils.getMusicOk("Ajout de "+track.getInfo().title+" à la file d'attente!")).complete();
List<Message> messages = new ArrayList<Message>(){{
add(message);
add(event.getMessage());
}};
new MessageTimeOut(messages, MainBot.messageTimeOut).start();
play(event.getGuild(), voiceChannel, musicManager, track, onHead);
play(guild, voiceChannel, musicManager, track, onHead);
}
@Override
@ -66,13 +72,10 @@ public class AudioM {
add(event.getMessage());
}};
new MessageTimeOut(messages, MainBot.messageTimeOut).start();
int i = 0;
for(AudioTrack track : playlist.getTracks()){
play(event.getGuild(), voiceChannel, musicManager, track,onHead);
i++;
if((i>=playlistLimit && i!=-1) || i>listExtremLimit)
break;
}
playListLoader(playlist, playlistLimit, onHead);
}
@ -101,7 +104,15 @@ public class AudioM {
});
}
public void playListLoader(AudioPlaylist playlist,int playlistLimit, boolean onHead){
int i = 0;
for(AudioTrack track : playlist.getTracks()){
play(guild, playedChanel, musicManager, track, onHead);
i++;
if((i>=playlistLimit && i!=-1) || i>listExtremLimit)
break;
}
}
private GuildMusicManager getGuildAudioPlayer(Guild guild) {
@ -114,7 +125,7 @@ public class AudioM {
return musicManager;
}
private void play(Guild guild, VoiceChannel channel, GuildMusicManager musicManager, AudioTrack track,boolean onHead) {
public void play(Guild guild, VoiceChannel channel, GuildMusicManager musicManager, AudioTrack track,boolean onHead) {
if(!guild.getAudioManager().isConnected())
guild.getAudioManager().openAudioConnection(channel);
if(!onHead)
@ -224,10 +235,9 @@ public class AudioM {
public void stop (MessageReceivedEvent event) {
musicManager.scheduler.stop();
playedChanel = null;
musicManager.scheduler.flush();
if (event != null) {
event.getGuild().getAudioManager().closeAudioConnection();
Message message = event.getTextChannel().sendMessage(EmbedMessageUtils.getMusicOk("Arret de la musique!")).complete();
List<Message> messages = new ArrayList<Message>(){{
add(message);
@ -253,6 +263,14 @@ public class AudioM {
return musicManager;
}
public Guild getGuild() {
return guild;
}
public AudioPlayerManager getPlayerManager() {
return playerManager;
}
public VoiceChannel getPlayedChanel() {
return playedChanel;
}
}

View File

@ -24,7 +24,6 @@ public class GuildMusicManager {
player = manager.createPlayer();
scheduler = new TrackScheduler(player);
player.addListener(scheduler);
}
/**

View File

@ -0,0 +1,82 @@
package net.Broken.audio;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.Broken.Commandes.Music;
import net.Broken.RestApi.Data.CommandPostData;
import net.Broken.RestApi.Data.CommandResponseData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.TreeMap;
public class WebLoadUtils {
ResponseEntity<CommandResponseData> response;
Logger logger = LogManager.getLogger();
public WebLoadUtils(Music musicCommande, CommandPostData data){
AudioPlayerManager playerM = musicCommande.getAudioManager().getPlayerManager();
try {
AudioM audioM = musicCommande.getAudioManager();
playerM.loadItemOrdered(musicCommande.getAudioManager().getMusicManager(), data.url, new AudioLoadResultHandler() {
@Override
public void trackLoaded(AudioTrack track) {
logger.info("Single Track detected from web!");
try {
audioM.play(audioM.getGuild(), audioM.getPlayedChanel(), audioM.getMusicManager(), track, data.onHead);
response = new ResponseEntity<>(new CommandResponseData("ADD", "Loaded"), HttpStatus.OK);
} catch (NullMusicManager | NotConectedException nullMusicManager) {
nullMusicManager.printStackTrace();
}
}
@Override
public void playlistLoaded(AudioPlaylist playlist) {
logger.info("Playlist detected from web! Limit: " + data.playlistLimit);
audioM.playListLoader(playlist,data.playlistLimit,data.onHead);
response = new ResponseEntity<>(new CommandResponseData("ADD", "Loaded"), HttpStatus.OK);
}
@Override
public void noMatches() {
logger.warn("Cant find media ! (web)");
response = new ResponseEntity<>(new CommandResponseData("ADD", "Can't find media!"), HttpStatus.NOT_FOUND);
}
@Override
public void loadFailed(FriendlyException exception) {
logger.error("Cant load media ! (web)");
response = new ResponseEntity<>(new CommandResponseData("ADD", "Cant load media !"), HttpStatus.INTERNAL_SERVER_ERROR);
}
});
while(response == null)
Thread.sleep(10);
} catch (NullMusicManager | NotConectedException | InterruptedException nullMusicManager) {
nullMusicManager.printStackTrace();
}
}
public ResponseEntity<CommandResponseData> getResponse(){
while(response == null) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>
<title>Starter Template - Materialize</title>
<title>Discord Bot</title>
<!-- CSS -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
@ -15,23 +15,23 @@
<nav class="blue-grey darken-4 z-depth-3" role="navigation">
<div class="nav-wrapper container">
<a id="logo-container" href="#" class="brand-logo">Discrod IMERIR Social Club</a>
<a id="logo-container" href="#" class="brand-logo">Discord Bot</a>
<ul class="right hide-on-med-and-down">
<li>
<a href="#" data-target="slide-out" class="sidenav-trigger">Home</a>
</li>
<li class="active">
<a href="#" >Music Control</a>
<a href="/" data-target="slide-out" class="sidenav-trigger">Home</a>
</li>
<li>
<a href="/music" >Music Control</a>
</li>
</ul>
<ul id="nav-mobile" class="side-nav">
<li>
<a href="#" data-target="slide-out" class="sidenav-trigger">Home</a>
<li class="active">
<a href="/" data-target="slide-out" class="sidenav-trigger">Home</a>
</li>
<li>
<a href="#" data-activates="slide-out" class="button-collapse-1">Playlist</a>
<a href="/music" data-target="slide-out" class="sidenav-trigger">Music Control</a>
</li>
</ul>
<a href="#" data-activates="nav-mobile" class="button-collapse-1 button-collapse"><i class="material-icons">menu</i></a>
@ -39,80 +39,8 @@
</nav>
<div class="section no-pad-bot main" id="index-banner">
<div class="row">
<div class="col s8">
<div class="row center" >
<img class="responsive-img z-depth-3" id="music_img" style="max-width: 30%" src=""/>
</div>
<h4 class="center" id="music_text"></h4>
<div class="row center">
<div class="progress col s6 offset-s3 z-depth-3">
<div class="determinate" id="music_progress" style="width: 0%"></div>
</div>
</div>
<div class="row center">
<div class="col s2 offset-s3 center">
<a class="btn-large blue-grey darken-4 z-depth-3 waves-effect waves-light" id="btn_stop">
<i class="material-icons medium">stop</i>
</a>
</div>
<div class="col s2 center">
<a class="btn-large blue-grey darken-4 z-depth-3 waves-effect waves-light" id="btn_play">
<i class="material-icons medium">play_arrow</i>
</a>
</div>
<div class="col s2 center">
<a class="btn-large blue-grey darken-4 z-depth-3 waves-effect waves-light" id="btn_next">
<i class="material-icons">skip_next</i>
</a>
</div>
</div>
<div class="row center">
<div class="col offset-s5 s2 center">
<a class="btn blue-grey darken-4 z-depth-3 waves-effect waves-light modal-trigger" href="#modal1" id="btn_info">
<i class="material-icons">info</i>
</a>
</div>
</div>
</div>
<div class="col s4" >
<ul id="playlist_list" class="collapsible popout" data-collapsible="accordion" style="margin: 0px">
</ul>
</div>
</div>
<!-- Playlist -->
<!-- Modal Structure -->
<div id="modal1" class="modal bottom-sheet">
<div class="modal-content">
<ul class="collection">
<li class="collection-item " id="modal_title"></li>
<li class="collection-item " id="modal_author"></li>
<li class="collection-item " id="modal_lenght"></li>
<li class="collection-item " id="modal_url"></li>
</ul>
</div>
</div>
</div>
<li id="playlist_template" style="visibility: hidden">
<div class="collapsible-header"><i class="material-icons">drag_handle</i>@title</div>
<div class="collapsible-body">
<ul class="collection">
<li class="collection-item">Author: @author</li>
<li class="collection-item">Duration: @lenght</li>
<li class="collection-item">URL: <a>@url</a></li>
</ul>
</div>
</li>
<!-- Scripts-->
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>

View File

@ -17,21 +17,71 @@ $(document).ready(function() {
$('#btn_play').click(function () {
switch (state){
case "PLAYING":
sendCommand("PAUSE")
sendCommand(JSON.stringify({ command: "PAUSE"}))
break;
case "PAUSE":
sendCommand("PLAY")
sendCommand(JSON.stringify({ command: "PLAY"}))
break;
}
})
$('#btn_next').click(function () {
sendCommand("NEXT");
sendCommand(JSON.stringify({ command: "NEXT"}));
})
$('#btn_stop').click(function () {
sendCommand("STOP");
sendCommand(JSON.stringify({ command: "STOP"}));
})
$('.dropdown-button').dropdown({
inDuration: 300,
outDuration: 225,
constrainWidth: false, // Does not change width of dropdown to that of the activator
hover: false, // Activate on hover
gutter: 0, // Spacing from edge
belowOrigin: false, // Displays dropdown below the button
alignment: 'left', // Displays dropdown with edge aligned to the left of button
stopPropagation: false // Stops event propagation
}
);
$('#input_link').on("input", function () {
if($('#input_link').val() == ""){
if (!$('#btn_add_bottom').hasClass("disabled")) {
$('#btn_add_bottom').addClass("disabled");
}
if (!$('#btn_add_top').hasClass("disabled")) {
$('#btn_add_top').addClass("disabled");
}
}
else{
if ($('#btn_add_bottom').hasClass("disabled")) {
$('#btn_add_bottom').removeClass("disabled");
}
if ($('#btn_add_top').hasClass("disabled")) {
$('#btn_add_top').removeClass("disabled");
}
}
});
$('#btn_add_top').click(function () {
var command = {
command: "ADD",
url: $('#input_link').val(),
playlistLimit: $('#limit_range').val(),
onHead: true
};
sendCommand(JSON.stringify(command));
})
$('#btn_add_bottom').click(function () {
var command = {
command: "ADD",
url: $('#input_link').val(),
playlistLimit: $('#limit_range').val(),
onHead: false
};
sendCommand(JSON.stringify(command));
})
})
@ -50,7 +100,7 @@ function getCurentMusic() {
state = data.state;
switch (data.state) {
case "STOP":
$('#music_text').text("No Music");
$('#music_text').text("Connected on Vocal Channel");
if (!$('#btn_info').hasClass("indeterminate")) {
$('#btn_info').addClass("determinate").removeClass("indeterminate");
@ -64,6 +114,9 @@ function getCurentMusic() {
if (!$('#btn_info').hasClass("disabled")) {
$('#btn_info').addClass("disabled");
}
if ($('#add_btn').hasClass("disabled")) {
$('#add_btn').removeClass("disabled");
}
$('#music_img').attr("src","/img/no_music.jpg");
@ -87,12 +140,41 @@ function getCurentMusic() {
$('#btn_info').addClass("indeterminate").removeClass("determinate");
}
break;
case "DISCONNECTED":
$('#music_text').text("Disconnected from Vocal");
if (!$('#btn_info').hasClass("indeterminate")) {
$('#btn_info').addClass("determinate").removeClass("indeterminate");
}
$('#music_progress').width("0%");
$('#btn_play').children().text("play_arrow");
if (!$('#btn_play').hasClass("disabled")) {
$('#btn_play').addClass("disabled");
}
if (!$('#btn_stop').hasClass("disabled")) {
$('#btn_stop').addClass("disabled");
}
if (!$('#btn_next').hasClass("disabled")) {
$('#btn_next').addClass("disabled");
}
if (!$('#btn_info').hasClass("disabled")) {
$('#btn_info').addClass("disabled");
}
if (!$('#add_btn').hasClass("disabled")) {
$('#add_btn').addClass("disabled");
}
$('#music_img').attr("src","/img/disconnected.png");
break;
}
getPlayList();
})
.fail(function (data) {
if(!error){
alert("error");
alert("Connection lost, I keep trying to refresh!");
error = true;
}
@ -115,7 +197,6 @@ function getPlayList() {
template.removeAttr("id");
template.removeAttr("style");
var content = template.html();
console.log(content);
content = content.replace("@title", element.title);
content = content.replace("@author", element.author);
content = content.replace("@lenght", msToTime(element.length));
@ -156,13 +237,22 @@ function updateControl(data){
}
$('#music_progress').width(percent + "%");
if ($('#btn_play').hasClass("disabled")) {
$('#btn_play').removeClass("disabled");
}
if ($('#btn_stop').hasClass("disabled")) {
$('#btn_stop').removeClass("disabled");
}
if ($('#btn_info').hasClass("disabled")) {
$('#btn_info').removeClass("disabled");
}
if ($('#add_btn').hasClass("disabled")) {
$('#add_btn').removeClass("disabled");
}
if ($('#btn_next').hasClass("disabled")) {
$('#btn_next').removeClass("disabled");
}
$('#music_img').attr("src","http://img.youtube.com/vi/"+data.info.identifier+"/hqdefault.jpg");
updateModal(data);
@ -174,33 +264,29 @@ function sendCommand(commandStr){
dataType: 'json',
contentType: 'application/json',
url: "/api/music/command",
data: JSON.stringify({ command: commandStr}),
data: commandStr,
success: function (data) {
console.log(data);
}
}).fail(function (data) {
console.log(data);
alert(data.responseJSON.Message);
});
}
function comparePlaylist(list1, list2){
if(list1 == null || list2 == null){
console.log(list1);
console.log(list2);
console.log("False From null")
return false;
}
if(list1.length != list2.length){
console.log("False from length");
return false;
}
for(var i = 0; i++; i < list1.length){
if(list1[i].uri != list2[i].uri)
console.log("false from compare")
return false
}
return true;

View File

@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>
<title>Starter Template - Materialize</title>
<title>Discord Bot - Music Control</title>
<!-- CSS -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
@ -15,23 +15,23 @@
<nav class="blue-grey darken-4 z-depth-3" role="navigation">
<div class="nav-wrapper container">
<a id="logo-container" href="#" class="brand-logo">Discrod IMERIR Social Club</a>
<a id="logo-container" href="#" class="brand-logo">Discord Bot</a>
<ul class="right hide-on-med-and-down">
<li>
<a href="#" data-target="slide-out" class="sidenav-trigger">Home</a>
<li >
<a href="/" data-target="slide-out" class="sidenav-trigger">Home</a>
</li>
<li class="active">
<a href="#" >Music Control</a>
<a href="/music" >Music Control</a>
</li>
</ul>
<ul id="nav-mobile" class="side-nav">
<li>
<a href="#" data-target="slide-out" class="sidenav-trigger">Home</a>
<li >
<a href="/" data-target="slide-out" class="sidenav-trigger">Home</a>
</li>
<li>
<a href="#" data-activates="slide-out" class="button-collapse-1">Playlist</a>
<li class="active">
<a href="/music" data-target="slide-out" class="sidenav-trigger">Music Control</a>
</li>
</ul>
<a href="#" data-activates="nav-mobile" class="button-collapse-1 button-collapse"><i class="material-icons">menu</i></a>
@ -77,9 +77,67 @@
</div>
</div>
<div class="col s4" >
<table>
<thead>
<tr>
<th style="padding: 0px;">
<div class="row center valign-wrapper" style="margin: 0px">
<div class="col s5 center"><h5>Playlist</h5></div>
<div class="col s5 offset-s2 center">
<!-- Modal Trigger -->
<a class="waves-effect waves-light btn modal-trigger blue-grey darken-4" id="add_btn" href="#modalAdd">Add</a>
<!-- Modal Structure -->
<div id="modalAdd" class="modal disabled">
<div class="modal-content" style="padding-bottom: 0px">
<div class="row" style="margin-bottom: 0px">
<h3 class="col s12"> Add Music</h3>
<form class="col s12">
<div class="row" style="margin-bottom: 0px">
<div class="input-field col s12" style="padding-left: 0px; padding-right: 0px">
<!--<i class="material-icons prefix">link</i>-->
<input id="input_link" type="text" class="validate"/>
<label for="input_link">Link</label>
</div>
</div>
<div class="row" style="margin-bottom: 0px">
<div class="col s12 center">
Playlist Limit
</div>
</div>
<div class="row" style="margin-bottom: 0px">
<p class="range-field">
<input type="range" id="limit_range" min="1" max="300" step="1" value="30" />
</p>
</div>
</form>
</div>
</div>
<div class="modal-footer">
<a href="#!" class="modal-action modal-close waves-effect waves-green btn-flat">Cancel</a>
<a href="#!" id="btn_add_top" class="modal-action modal-close waves-effect waves-green btn-flat disabled">Add On Top</a>
<a href="#!" id="btn_add_bottom" class="modal-action modal-close waves-effect waves-green btn-flat disabled">Add On Bottom</a>
</div>
</div>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<ul id="playlist_list" class="collapsible popout" data-collapsible="accordion" style="margin: 0px">
</ul>
</td>
</tr>
</tbody>
</table>
</div>