зеркало из https://github.com/mozilla/moz-rockbot.git
added search/enqueue
This commit is contained in:
Родитель
cb4e0d3789
Коммит
b7cdff67c9
83
index.js
83
index.js
|
@ -52,17 +52,19 @@ function api(v, m, p, cb) {
|
|||
qs: p
|
||||
}, function(err, res, body) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
console.log('api error:', err);
|
||||
return cb(err);
|
||||
}
|
||||
if (body) {
|
||||
try {
|
||||
cb(null, JSON.parse(body));
|
||||
body = JSON.parse(body);
|
||||
return cb(null, body);
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
return cb(e);
|
||||
}
|
||||
} else {
|
||||
cb(null);
|
||||
}
|
||||
throw "whuck";
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -87,21 +89,82 @@ function checkCurrent() {
|
|||
setTimeout(checkCurrent, 10000);
|
||||
}
|
||||
|
||||
io.on('connect', function(socket) {
|
||||
io.on('connect', function (socket) {
|
||||
io.emit('currently', currently);
|
||||
|
||||
socket.on('upvote', function(o) {
|
||||
socket.on('upvote', function (o) {
|
||||
console.log('upvote', o);
|
||||
var v = venues[o.venue];
|
||||
api(o.venue, 'kiosk:add_up_vote', {pick: o.pick}, function (err, res) {
|
||||
console.log(err, res);
|
||||
for (var i = 0; i < currently.length; i++) {
|
||||
if (currently[i].id === o.venue) {
|
||||
res.name = currently[i].name;
|
||||
res.id = o.venue;
|
||||
currently[i] = res;
|
||||
io.emit('currently', currently);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
socket.on('downvote', function(o) {
|
||||
socket.on('downvote', function (o) {
|
||||
console.log('downvote', o);
|
||||
var v = venues[o.venue];
|
||||
api(o.venue, 'kiosk:add_down_vote', {pick: o.pick}, function (err, res) {
|
||||
console.log(err, res);
|
||||
for (var i = 0; i < currently.length; i++) {
|
||||
if (currently[i].id === o.venue) {
|
||||
res.name = currently[i].name;
|
||||
res.id = o.venue;
|
||||
currently[i] = res;
|
||||
io.emit('currently', currently);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
socket.on('search', function (o) {
|
||||
console.log('search', o);
|
||||
var results = {
|
||||
query: o.query,
|
||||
artist: []
|
||||
};
|
||||
var match = o.query.match(/^artist:(\d+)/);
|
||||
if (match) {
|
||||
api(o.venue, 'kiosk:get_artist', {artist: match[1]}, function (err, res) {
|
||||
if (err) {
|
||||
results.song = [];
|
||||
} else {
|
||||
results.song = res.aData;
|
||||
}
|
||||
socket.emit('results', results);
|
||||
});
|
||||
} else {
|
||||
api(o.venue, 'kiosk:search_artists', {query: o.query}, function (err, res) {
|
||||
if (err) {
|
||||
results.artist = [];
|
||||
} else {
|
||||
results.artist = res.aData;
|
||||
}
|
||||
api(o.venue, 'kiosk:search_songs', {query: o.query}, function (err, res) {
|
||||
if (err) {
|
||||
results.song = [];
|
||||
} else {
|
||||
results.song = res.aData;
|
||||
}
|
||||
socket.emit('results', results);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
socket.on('pick', function (o) {
|
||||
api(o.venue, 'kiosk:add_song', {song: o.song}, function (err, res) {
|
||||
console.log(err, res);
|
||||
for (var i = 0; i < currently.length; i++) {
|
||||
if (currently[i].id === o.venue) {
|
||||
res.name = currently[i].name;
|
||||
res.id = o.venue;
|
||||
currently[i] = res;
|
||||
io.emit('currently', currently);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
123
src/main.js
123
src/main.js
|
@ -8,7 +8,7 @@ function extend(o, n) {
|
|||
var App = React.createClass({
|
||||
setMode: function (mode) {
|
||||
this.setState(extend(this.state, {
|
||||
mode: 'mode'
|
||||
mode: mode
|
||||
}));
|
||||
},
|
||||
selectZone: function (zone) {
|
||||
|
@ -37,10 +37,12 @@ var App = React.createClass({
|
|||
});
|
||||
},
|
||||
render: function () {
|
||||
var zone = this.state.zones[this.state.selected];
|
||||
return (
|
||||
<div className="app" data-mode={this.state.mode}>
|
||||
<Overview zones={this.state.zones} selectZone={this.selectZone} />
|
||||
<ZoneDetail zone={this.state.zones[this.state.selected]} go={this.setMode} />
|
||||
<ZoneDetail zone={zone} go={this.setMode} />
|
||||
<SearchPanel zone={zone} go={this.setMode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -102,18 +104,18 @@ var QueueItem = React.createClass({
|
|||
<div className="info">{a.sArtist} - {a.sSong}</div>
|
||||
<User avatar={a.sUserImage} name={a.sUser} />
|
||||
<a className="up" href="#"
|
||||
onClick={this.upVote.bind(this, a.idPick)}>👍 {a.iLikes}</a>
|
||||
onClick={this.upVote.bind(this, a.idPick)}>{a.iLikes}</a>
|
||||
<a className="down" href="#"
|
||||
onClick={this.downVote.bind(this, a.idPick)}>👎 {a.iDislikes}</a>
|
||||
onClick={this.downVote.bind(this, a.idPick)}>{a.iDislikes}</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ZoneDetail = React.createClass({
|
||||
back: function (e) {
|
||||
go: function (mode, e) {
|
||||
e.preventDefault();
|
||||
this.props.go('overview');
|
||||
this.props.go(mode);
|
||||
},
|
||||
upvote: function (pick) {
|
||||
console.log('upvoting pick ' + pick);
|
||||
|
@ -136,8 +138,9 @@ var ZoneDetail = React.createClass({
|
|||
return (
|
||||
<section className="detail">
|
||||
<header>
|
||||
<a href="#" onClick={this.back}>Back</a>
|
||||
<a href="#" onClick={this.go.bind(this, 'overview')}>Back</a>
|
||||
<h1>{z.name}</h1>
|
||||
<a href="#" onClick={this.go.bind(this, 'search')}>Search</a>
|
||||
</header>
|
||||
<div className="nowplaying">
|
||||
<AlbumArt url={now.sArtwork} big={true} />
|
||||
|
@ -146,7 +149,7 @@ var ZoneDetail = React.createClass({
|
|||
<User className="user" avatar={now.sUserImage} name={now.sUser} />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="upnext">Up Next</h1>
|
||||
<h2 className="upnext">Up Next</h2>
|
||||
<div className="queue-container">
|
||||
<ol className="queue">
|
||||
{queue.map(function (item) {
|
||||
|
@ -184,6 +187,110 @@ var User = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
var SearchPanel = React.createClass({
|
||||
go: function (mode, e) {
|
||||
e.preventDefault();
|
||||
this.props.go(mode);
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
artist: [],
|
||||
song: [],
|
||||
query: ''
|
||||
};
|
||||
},
|
||||
searchArtist: function (artistId) {
|
||||
this.setState(extend(this.state, {
|
||||
query: 'artist:' + artistId
|
||||
}));
|
||||
this.performSearch();
|
||||
},
|
||||
pickSong: function (songId) {
|
||||
socket.emit('pick', {
|
||||
song: songId,
|
||||
venue: this.props.zone.id
|
||||
});
|
||||
},
|
||||
onChange: function (e) {
|
||||
var q = e.target.value;
|
||||
this.setState(extend(this.state, {
|
||||
query: q
|
||||
}));
|
||||
if (q.length > 2) {
|
||||
this.performSearch();
|
||||
}
|
||||
},
|
||||
performSearch: function () {
|
||||
console.log('search', this.state.query);
|
||||
socket.emit('search', {
|
||||
query: this.state.query,
|
||||
venue: this.props.zone.id
|
||||
});
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
socket.on('results', function (data) {
|
||||
console.log(data.query, self.state.query);
|
||||
if (data.query === self.state.query) {
|
||||
console.log(data);
|
||||
self.setState(extend(self.state, data));
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<section className="search">
|
||||
<header>
|
||||
<a href="#" onClick={this.go.bind(this, 'detail')}>Back</a>
|
||||
<h1>Search</h1>
|
||||
</header>
|
||||
<form className="searchForm">
|
||||
<input className="query" onChange={this.onChange} value={this.state.query} />
|
||||
<button className="submit">submit</button>
|
||||
</form>
|
||||
<div className="results-container">
|
||||
<SearchResults results={this.state}
|
||||
searchArtist={this.searchArtist}
|
||||
pickSong={this.pickSong} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SearchResults = React.createClass({
|
||||
pickSong: function (songId, e) {
|
||||
this.props.pickSong(songId);
|
||||
},
|
||||
listArtist: function (artistId, e) {
|
||||
this.props.searchArtist(artistId);
|
||||
},
|
||||
render: function () {
|
||||
var artists = this.props.results.artist;
|
||||
var songs = this.props.results.song;
|
||||
return (
|
||||
<div className="results">
|
||||
<h2>Artists ({artists.length})</h2>
|
||||
<ul>
|
||||
{artists.filter(function (r) {
|
||||
return r.bEnabled !== false;
|
||||
}).map(function (r) {
|
||||
return <li onClick={this.listArtist.bind(this, r.idArtist)}>{r.sName}</li>;
|
||||
}, this)}
|
||||
</ul>
|
||||
<h2>Songs ({songs.length})</h2>
|
||||
<ul>
|
||||
{songs.filter(function (r) {
|
||||
return r.bEnabled !== false;
|
||||
}).map(function (r) {
|
||||
return <li onClick={this.pickSong.bind(this, r.idSong)}>{r.sArtist} - {r.sName}</li>;
|
||||
}, this)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var socket = io.connect('/');
|
||||
var o = React.createElement(Overview);
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 2vmin;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
@ -14,7 +18,6 @@ body {
|
|||
color: #fff;
|
||||
font-family: 'Fira Sans', sans-serif;
|
||||
overflow: hidden;
|
||||
font-size: 4vmin;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
@ -47,6 +50,10 @@ a:link, a:visited, a:active {
|
|||
.app[data-mode="detail"] {
|
||||
transform: translate(-100vw, 0);
|
||||
}
|
||||
.app[data-mode="search"] {
|
||||
transform: translate(-200vw, 0);
|
||||
}
|
||||
|
||||
|
||||
/* Queue */
|
||||
.queue-container {
|
||||
|
@ -57,7 +64,7 @@ a:link, a:visited, a:active {
|
|||
.queue {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
font-size: 4vmin;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.queue-item {
|
||||
display: flex;
|
||||
|
@ -78,6 +85,16 @@ a:link, a:visited, a:active {
|
|||
white-space: nowrap;
|
||||
width: 10vmin;
|
||||
text-align: center;
|
||||
padding-left: 1.2em;
|
||||
background-position: left center;
|
||||
background-size: auto 2rem;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.queue .up {
|
||||
background-image: url(/thumbs-up.png);
|
||||
}
|
||||
.queue .down {
|
||||
background-image: url(/thumbs-down.png);
|
||||
}
|
||||
|
||||
/* Detail */
|
||||
|
@ -93,7 +110,6 @@ header {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 3vmin;
|
||||
}
|
||||
header > * + * {
|
||||
margin-left: 1em;
|
||||
|
@ -101,6 +117,7 @@ header > * + * {
|
|||
header a {
|
||||
padding: 1rem;
|
||||
margin: -1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
header > h1 {
|
||||
flex: 1;
|
||||
|
@ -112,7 +129,7 @@ header > h1 {
|
|||
}
|
||||
.upnext {
|
||||
font-style: italic;
|
||||
font-size: 5vmin;
|
||||
font-size: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.nowplaying {
|
||||
|
@ -121,8 +138,8 @@ header > h1 {
|
|||
}
|
||||
.nowplaying .info {
|
||||
display: flex;
|
||||
font-size: 4vmin;
|
||||
line-height: 6vmin;
|
||||
font-size: 2rem;
|
||||
line-height: 3rem;
|
||||
margin-left: 1em;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
@ -151,12 +168,13 @@ header > h1 {
|
|||
}
|
||||
.overview .zone-item {
|
||||
flex: 1;
|
||||
font-size: 4vmin;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.zone-item > div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.zone-item .art {
|
||||
height: 20vh;
|
||||
|
@ -164,3 +182,62 @@ header > h1 {
|
|||
flex-shrink: 0;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.search {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.search > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.submit {
|
||||
background: #000;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
font-size: 1.5rem;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.searchForm {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.query {
|
||||
flex: 1;
|
||||
font: inherit;
|
||||
font-size: 1.5rem;
|
||||
padding: .5rem 2rem;
|
||||
border-radius: 2em;
|
||||
border: 0;
|
||||
}
|
||||
.results-container {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
.results {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.results h2 {
|
||||
font-style: italic;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.25rem;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
@media (max-device-width: 480px) {
|
||||
.queue {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
|
125
static/main.js
125
static/main.js
|
@ -8,7 +8,7 @@ function extend(o, n) {
|
|||
var App = React.createClass({displayName: "App",
|
||||
setMode: function (mode) {
|
||||
this.setState(extend(this.state, {
|
||||
mode: 'mode'
|
||||
mode: mode
|
||||
}));
|
||||
},
|
||||
selectZone: function (zone) {
|
||||
|
@ -37,10 +37,12 @@ var App = React.createClass({displayName: "App",
|
|||
});
|
||||
},
|
||||
render: function () {
|
||||
var zone = this.state.zones[this.state.selected];
|
||||
return (
|
||||
React.createElement("div", {className: "app", "data-mode": this.state.mode},
|
||||
React.createElement(Overview, {zones: this.state.zones, selectZone: this.selectZone}),
|
||||
React.createElement(ZoneDetail, {zone: this.state.zones[this.state.selected], go: this.setMode})
|
||||
React.createElement(ZoneDetail, {zone: zone, go: this.setMode}),
|
||||
React.createElement(SearchPanel, {zone: zone, go: this.setMode})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -102,18 +104,18 @@ var QueueItem = React.createClass({displayName: "QueueItem",
|
|||
React.createElement("div", {className: "info"}, a.sArtist, " - ", a.sSong),
|
||||
React.createElement(User, {avatar: a.sUserImage, name: a.sUser}),
|
||||
React.createElement("a", {className: "up", href: "#",
|
||||
onClick: this.upVote.bind(this, a.idPick)}, "👍 ", a.iLikes),
|
||||
onClick: this.upVote.bind(this, a.idPick)}, a.iLikes),
|
||||
React.createElement("a", {className: "down", href: "#",
|
||||
onClick: this.downVote.bind(this, a.idPick)}, "👎 ", a.iDislikes)
|
||||
onClick: this.downVote.bind(this, a.idPick)}, a.iDislikes)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ZoneDetail = React.createClass({displayName: "ZoneDetail",
|
||||
back: function (e) {
|
||||
go: function (mode, e) {
|
||||
e.preventDefault();
|
||||
this.props.go('overview');
|
||||
this.props.go(mode);
|
||||
},
|
||||
upvote: function (pick) {
|
||||
console.log('upvoting pick ' + pick);
|
||||
|
@ -136,8 +138,9 @@ var ZoneDetail = React.createClass({displayName: "ZoneDetail",
|
|||
return (
|
||||
React.createElement("section", {className: "detail"},
|
||||
React.createElement("header", null,
|
||||
React.createElement("a", {href: "#", onClick: this.back}, "Back"),
|
||||
React.createElement("h1", null, z.name)
|
||||
React.createElement("a", {href: "#", onClick: this.go.bind(this, 'overview')}, "Back"),
|
||||
React.createElement("h1", null, z.name),
|
||||
React.createElement("a", {href: "#", onClick: this.go.bind(this, 'search')}, "Search")
|
||||
),
|
||||
React.createElement("div", {className: "nowplaying"},
|
||||
React.createElement(AlbumArt, {url: now.sArtwork, big: true}),
|
||||
|
@ -146,7 +149,7 @@ var ZoneDetail = React.createClass({displayName: "ZoneDetail",
|
|||
React.createElement(User, {className: "user", avatar: now.sUserImage, name: now.sUser})
|
||||
)
|
||||
),
|
||||
React.createElement("h1", {className: "upnext"}, "Up Next"),
|
||||
React.createElement("h2", {className: "upnext"}, "Up Next"),
|
||||
React.createElement("div", {className: "queue-container"},
|
||||
React.createElement("ol", {className: "queue"},
|
||||
queue.map(function (item) {
|
||||
|
@ -184,6 +187,110 @@ var User = React.createClass({displayName: "User",
|
|||
}
|
||||
});
|
||||
|
||||
var SearchPanel = React.createClass({displayName: "SearchPanel",
|
||||
go: function (mode, e) {
|
||||
e.preventDefault();
|
||||
this.props.go(mode);
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
artist: [],
|
||||
song: [],
|
||||
query: ''
|
||||
};
|
||||
},
|
||||
searchArtist: function (artistId) {
|
||||
this.setState(extend(this.state, {
|
||||
query: 'artist:' + artistId
|
||||
}));
|
||||
this.performSearch();
|
||||
},
|
||||
pickSong: function (songId) {
|
||||
socket.emit('pick', {
|
||||
song: songId,
|
||||
venue: this.props.zone.id
|
||||
});
|
||||
},
|
||||
onChange: function (e) {
|
||||
var q = e.target.value;
|
||||
this.setState(extend(this.state, {
|
||||
query: q
|
||||
}));
|
||||
if (q.length > 2) {
|
||||
this.performSearch();
|
||||
}
|
||||
},
|
||||
performSearch: function () {
|
||||
console.log('search', this.state.query);
|
||||
socket.emit('search', {
|
||||
query: this.state.query,
|
||||
venue: this.props.zone.id
|
||||
});
|
||||
},
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
socket.on('results', function (data) {
|
||||
console.log(data.query, self.state.query);
|
||||
if (data.query === self.state.query) {
|
||||
console.log(data);
|
||||
self.setState(extend(self.state, data));
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
React.createElement("section", {className: "search"},
|
||||
React.createElement("header", null,
|
||||
React.createElement("a", {href: "#", onClick: this.go.bind(this, 'detail')}, "Back"),
|
||||
React.createElement("h1", null, "Search")
|
||||
),
|
||||
React.createElement("form", {className: "searchForm"},
|
||||
React.createElement("input", {className: "query", onChange: this.onChange, value: this.state.query}),
|
||||
React.createElement("button", {className: "submit"}, "submit")
|
||||
),
|
||||
React.createElement("div", {className: "results-container"},
|
||||
React.createElement(SearchResults, {results: this.state,
|
||||
searchArtist: this.searchArtist,
|
||||
pickSong: this.pickSong})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SearchResults = React.createClass({displayName: "SearchResults",
|
||||
pickSong: function (songId, e) {
|
||||
this.props.pickSong(songId);
|
||||
},
|
||||
listArtist: function (artistId, e) {
|
||||
this.props.searchArtist(artistId);
|
||||
},
|
||||
render: function () {
|
||||
var artists = this.props.results.artist;
|
||||
var songs = this.props.results.song;
|
||||
return (
|
||||
React.createElement("div", {className: "results"},
|
||||
React.createElement("h2", null, "Artists (", artists.length, ")"),
|
||||
React.createElement("ul", null,
|
||||
artists.filter(function (r) {
|
||||
return r.bEnabled !== false;
|
||||
}).map(function (r) {
|
||||
return React.createElement("li", {onClick: this.listArtist.bind(this, r.idArtist)}, r.sName);
|
||||
}, this)
|
||||
),
|
||||
React.createElement("h2", null, "Songs (", songs.length, ")"),
|
||||
React.createElement("ul", null,
|
||||
songs.filter(function (r) {
|
||||
return r.bEnabled !== false;
|
||||
}).map(function (r) {
|
||||
return React.createElement("li", {onClick: this.pickSong.bind(this, r.idSong)}, r.sArtist, " - ", r.sName);
|
||||
}, this)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var socket = io.connect('/');
|
||||
var o = React.createElement(Overview);
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 4.2 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 4.1 KiB |
Загрузка…
Ссылка в новой задаче