зеркало из https://github.com/mozilla/moz-rockbot.git
better UI interactivity
This commit is contained in:
Родитель
805d83fd1e
Коммит
ed7ea48b29
49
src/main.css
49
src/main.css
|
@ -21,10 +21,13 @@ body {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
a:link, a:visited, a:active {
|
||||
a:link, a:visited, a:active, a:hover {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.active {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
/* App */
|
||||
#app {
|
||||
|
@ -64,6 +67,7 @@ a:link, a:visited, a:active {
|
|||
.queue {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.queue-item {
|
||||
|
@ -206,14 +210,20 @@ header > h1 {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 2em;
|
||||
padding-right: .5em;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.searchForm .throb {
|
||||
color: #000;
|
||||
}
|
||||
.query {
|
||||
flex: 1;
|
||||
font: inherit;
|
||||
font-size: 1.5rem;
|
||||
background: transparent;
|
||||
border: 0 none;
|
||||
padding: .5rem 2rem;
|
||||
border-radius: 2em;
|
||||
border: 0;
|
||||
}
|
||||
.results-container {
|
||||
overflow: hidden;
|
||||
|
@ -222,15 +232,24 @@ header > h1 {
|
|||
}
|
||||
.results {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: 100%;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.results li {
|
||||
padding: .5rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
.results h2 {
|
||||
font-style: italic;
|
||||
}
|
||||
.results a {
|
||||
display: block;
|
||||
flex: 1;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
@ -238,6 +257,28 @@ h2 {
|
|||
font-size: 2.25rem;
|
||||
color: #ddd;
|
||||
}
|
||||
.results .throb {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* loading */
|
||||
.throb {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
background-color: red;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border: .25em solid currentColor;
|
||||
border-radius: 2em;
|
||||
border-color: currentColor transparent currentColor transparent;
|
||||
animation: 500ms spin linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-device-width: 480px) {
|
||||
html {
|
||||
|
|
121
src/main.js
121
src/main.js
|
@ -5,6 +5,16 @@ function extend(o, n) {
|
|||
return o;
|
||||
}
|
||||
|
||||
function classSet(o) {
|
||||
var c = [];
|
||||
for (var k in o) {
|
||||
if (!!o[k]) {
|
||||
c.push(k);
|
||||
}
|
||||
}
|
||||
return c.join(' ');
|
||||
}
|
||||
|
||||
var App = React.createClass({
|
||||
setMode: function (mode) {
|
||||
this.setState(extend(this.state, {
|
||||
|
@ -196,7 +206,8 @@ var SearchPanel = React.createClass({
|
|||
return {
|
||||
artist: [],
|
||||
song: [],
|
||||
query: ''
|
||||
query: '',
|
||||
pending: false
|
||||
};
|
||||
},
|
||||
searchArtist: function (artistId) {
|
||||
|
@ -211,17 +222,25 @@ var SearchPanel = React.createClass({
|
|||
venue: this.props.zone.id
|
||||
});
|
||||
},
|
||||
onChange: function (e) {
|
||||
var q = e.target.value;
|
||||
this.setState(extend(this.state, {
|
||||
query: q
|
||||
}));
|
||||
if (q.length > 2) {
|
||||
updateQuery: function (q) {
|
||||
if (q.length > 0) {
|
||||
this.setState(extend(this.state, {
|
||||
query: q
|
||||
}));
|
||||
this.performSearch();
|
||||
}
|
||||
},
|
||||
onChange: function (e) {
|
||||
console.log(e);
|
||||
var q = e.target.value;
|
||||
clearTimeout(this.to);
|
||||
this.to = setTimeout(this.updateQuery.bind(this, q), 200);
|
||||
},
|
||||
performSearch: function () {
|
||||
console.log('search', this.state.query);
|
||||
this.setState(extend(this.state, {
|
||||
pending: true
|
||||
}));
|
||||
socket.emit('search', {
|
||||
query: this.state.query,
|
||||
venue: this.props.zone.id
|
||||
|
@ -230,25 +249,37 @@ var SearchPanel = React.createClass({
|
|||
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));
|
||||
console.log('results', data);
|
||||
self.setState(extend(self.state, {
|
||||
artist: data.artist,
|
||||
song: data.song,
|
||||
pending: false
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
if (!this.props.zone) {
|
||||
return <div></div>;
|
||||
}
|
||||
var pending = '';
|
||||
if (this.state.pending) {
|
||||
pending = 'pending';
|
||||
}
|
||||
return (
|
||||
<section className="search">
|
||||
<header>
|
||||
<a href="#" onClick={this.go.bind(this, 'detail')}>Back</a>
|
||||
<h1>Search</h1>
|
||||
</header>
|
||||
<div className="searchForm">
|
||||
<input className="query" onChange={this.onChange} value={this.state.query} />
|
||||
<div className={"searchForm " + pending}>
|
||||
<input className="query" onChange={this.onChange} />
|
||||
{this.state.pending ? <Throbber /> : ''}
|
||||
</div>
|
||||
<div className="results-container">
|
||||
<SearchResults results={this.state}
|
||||
queue={this.props.zone.aData.aQueue}
|
||||
searchArtist={this.searchArtist}
|
||||
pickSong={this.pickSong} />
|
||||
</div>
|
||||
|
@ -258,15 +289,22 @@ var SearchPanel = React.createClass({
|
|||
});
|
||||
|
||||
var SearchResults = React.createClass({
|
||||
pickSong: function (songId, e) {
|
||||
pickSong: function (songId) {
|
||||
this.props.pickSong(songId);
|
||||
},
|
||||
listArtist: function (artistId, e) {
|
||||
e.preventDefault();
|
||||
this.props.searchArtist(artistId);
|
||||
},
|
||||
render: function () {
|
||||
var artists = this.props.results.artist;
|
||||
var songs = this.props.results.song;
|
||||
if (!artists) {
|
||||
artists = [];
|
||||
}
|
||||
if (!songs) {
|
||||
songs = [];
|
||||
}
|
||||
return (
|
||||
<div className="results">
|
||||
<h2>Artists ({artists.length})</h2>
|
||||
|
@ -274,7 +312,14 @@ var SearchResults = React.createClass({
|
|||
{artists.filter(function (r) {
|
||||
return r.bEnabled !== false;
|
||||
}).map(function (r) {
|
||||
return <li onClick={this.listArtist.bind(this, r.idArtist)}>{r.sName}</li>;
|
||||
return (
|
||||
<li key={r.idArtist}>
|
||||
<a href="#"
|
||||
onClick={this.listArtist.bind(this, r.idArtist)}>
|
||||
{r.sName}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
<h2>Songs ({songs.length})</h2>
|
||||
|
@ -282,7 +327,15 @@ var SearchResults = React.createClass({
|
|||
{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>;
|
||||
var picked = false;
|
||||
if (this.props.queue) {
|
||||
this.props.queue.forEach(function (q) {
|
||||
if (q.sSong === r.sName && q.sArtist === r.sArtist) {
|
||||
picked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return <SongResult key={r.idSong} picked={picked} song={r} pick={this.pickSong}/>;
|
||||
}, this)}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -290,6 +343,44 @@ var SearchResults = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
var SongResult = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
pending: false
|
||||
};
|
||||
},
|
||||
pick: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.pick(this.props.song.idSong);
|
||||
this.setState({
|
||||
pending: true
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var song = this.props.song;
|
||||
var pending = this.state.pending && !this.props.picked;
|
||||
var classes = classSet({
|
||||
'picked': this.props.picked,
|
||||
'pending': pending
|
||||
});
|
||||
return (
|
||||
<li key={song.idSong} className={classes}>
|
||||
<a href="#"
|
||||
onClick={this.pick}>
|
||||
{song.sArtist} - {song.sName}
|
||||
</a>
|
||||
{pending ? <Throbber /> : ''}
|
||||
{this.props.picked ? <div>✓</div> : ''}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Throbber = React.createClass({
|
||||
render: function () {
|
||||
return <div className="throb"></div>;
|
||||
}
|
||||
});
|
||||
|
||||
var socket = io.connect('/');
|
||||
var o = React.createElement(Overview);
|
||||
|
|
|
@ -21,10 +21,13 @@ body {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
a:link, a:visited, a:active {
|
||||
a:link, a:visited, a:active, a:hover {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.active {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
/* App */
|
||||
#app {
|
||||
|
@ -84,6 +87,7 @@ a:link, a:visited, a:active {
|
|||
.queue {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.queue-item {
|
||||
|
@ -320,6 +324,13 @@ header > h1 {
|
|||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 2em;
|
||||
padding-right: .5em;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.searchForm .throb {
|
||||
color: #000;
|
||||
}
|
||||
.query {
|
||||
-webkit-box-flex: 1;
|
||||
|
@ -327,10 +338,9 @@ header > h1 {
|
|||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
font: inherit;
|
||||
font-size: 1.5rem;
|
||||
background: transparent;
|
||||
border: 0 none;
|
||||
padding: .5rem 2rem;
|
||||
border-radius: 2em;
|
||||
border: 0;
|
||||
}
|
||||
.results-container {
|
||||
overflow: hidden;
|
||||
|
@ -342,15 +352,34 @@ header > h1 {
|
|||
}
|
||||
.results {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: 100%;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.results li {
|
||||
padding: .5rem 0;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
.results h2 {
|
||||
font-style: italic;
|
||||
}
|
||||
.results a {
|
||||
display: block;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
@ -358,6 +387,40 @@ h2 {
|
|||
font-size: 2.25rem;
|
||||
color: #ddd;
|
||||
}
|
||||
.results .throb {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* loading */
|
||||
.throb {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
background-color: red;
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border: .25em solid currentColor;
|
||||
border-radius: 2em;
|
||||
border-color: currentColor transparent currentColor transparent;
|
||||
-webkit-animation: 500ms spin linear infinite;
|
||||
animation: 500ms spin linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-device-width: 480px) {
|
||||
html {
|
||||
|
|
121
static/main.js
121
static/main.js
|
@ -5,6 +5,16 @@ function extend(o, n) {
|
|||
return o;
|
||||
}
|
||||
|
||||
function classSet(o) {
|
||||
var c = [];
|
||||
for (var k in o) {
|
||||
if (!!o[k]) {
|
||||
c.push(k);
|
||||
}
|
||||
}
|
||||
return c.join(' ');
|
||||
}
|
||||
|
||||
var App = React.createClass({displayName: "App",
|
||||
setMode: function (mode) {
|
||||
this.setState(extend(this.state, {
|
||||
|
@ -196,7 +206,8 @@ var SearchPanel = React.createClass({displayName: "SearchPanel",
|
|||
return {
|
||||
artist: [],
|
||||
song: [],
|
||||
query: ''
|
||||
query: '',
|
||||
pending: false
|
||||
};
|
||||
},
|
||||
searchArtist: function (artistId) {
|
||||
|
@ -211,17 +222,25 @@ var SearchPanel = React.createClass({displayName: "SearchPanel",
|
|||
venue: this.props.zone.id
|
||||
});
|
||||
},
|
||||
onChange: function (e) {
|
||||
var q = e.target.value;
|
||||
this.setState(extend(this.state, {
|
||||
query: q
|
||||
}));
|
||||
if (q.length > 2) {
|
||||
updateQuery: function (q) {
|
||||
if (q.length > 0) {
|
||||
this.setState(extend(this.state, {
|
||||
query: q
|
||||
}));
|
||||
this.performSearch();
|
||||
}
|
||||
},
|
||||
onChange: function (e) {
|
||||
console.log(e);
|
||||
var q = e.target.value;
|
||||
clearTimeout(this.to);
|
||||
this.to = setTimeout(this.updateQuery.bind(this, q), 200);
|
||||
},
|
||||
performSearch: function () {
|
||||
console.log('search', this.state.query);
|
||||
this.setState(extend(this.state, {
|
||||
pending: true
|
||||
}));
|
||||
socket.emit('search', {
|
||||
query: this.state.query,
|
||||
venue: this.props.zone.id
|
||||
|
@ -230,25 +249,37 @@ var SearchPanel = React.createClass({displayName: "SearchPanel",
|
|||
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));
|
||||
console.log('results', data);
|
||||
self.setState(extend(self.state, {
|
||||
artist: data.artist,
|
||||
song: data.song,
|
||||
pending: false
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
if (!this.props.zone) {
|
||||
return React.createElement("div", null);
|
||||
}
|
||||
var pending = '';
|
||||
if (this.state.pending) {
|
||||
pending = 'pending';
|
||||
}
|
||||
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("div", {className: "searchForm"},
|
||||
React.createElement("input", {className: "query", onChange: this.onChange, value: this.state.query})
|
||||
React.createElement("div", {className: "searchForm " + pending},
|
||||
React.createElement("input", {className: "query", onChange: this.onChange}),
|
||||
this.state.pending ? React.createElement(Throbber, null) : ''
|
||||
),
|
||||
React.createElement("div", {className: "results-container"},
|
||||
React.createElement(SearchResults, {results: this.state,
|
||||
queue: this.props.zone.aData.aQueue,
|
||||
searchArtist: this.searchArtist,
|
||||
pickSong: this.pickSong})
|
||||
)
|
||||
|
@ -258,15 +289,22 @@ var SearchPanel = React.createClass({displayName: "SearchPanel",
|
|||
});
|
||||
|
||||
var SearchResults = React.createClass({displayName: "SearchResults",
|
||||
pickSong: function (songId, e) {
|
||||
pickSong: function (songId) {
|
||||
this.props.pickSong(songId);
|
||||
},
|
||||
listArtist: function (artistId, e) {
|
||||
e.preventDefault();
|
||||
this.props.searchArtist(artistId);
|
||||
},
|
||||
render: function () {
|
||||
var artists = this.props.results.artist;
|
||||
var songs = this.props.results.song;
|
||||
if (!artists) {
|
||||
artists = [];
|
||||
}
|
||||
if (!songs) {
|
||||
songs = [];
|
||||
}
|
||||
return (
|
||||
React.createElement("div", {className: "results"},
|
||||
React.createElement("h2", null, "Artists (", artists.length, ")"),
|
||||
|
@ -274,7 +312,14 @@ var SearchResults = React.createClass({displayName: "SearchResults",
|
|||
artists.filter(function (r) {
|
||||
return r.bEnabled !== false;
|
||||
}).map(function (r) {
|
||||
return React.createElement("li", {onClick: this.listArtist.bind(this, r.idArtist)}, r.sName);
|
||||
return (
|
||||
React.createElement("li", {key: r.idArtist},
|
||||
React.createElement("a", {href: "#",
|
||||
onClick: this.listArtist.bind(this, r.idArtist)},
|
||||
r.sName
|
||||
)
|
||||
)
|
||||
);
|
||||
}, this)
|
||||
),
|
||||
React.createElement("h2", null, "Songs (", songs.length, ")"),
|
||||
|
@ -282,7 +327,15 @@ var SearchResults = React.createClass({displayName: "SearchResults",
|
|||
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);
|
||||
var picked = false;
|
||||
if (this.props.queue) {
|
||||
this.props.queue.forEach(function (q) {
|
||||
if (q.sSong === r.sName && q.sArtist === r.sArtist) {
|
||||
picked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return React.createElement(SongResult, {key: r.idSong, picked: picked, song: r, pick: this.pickSong});
|
||||
}, this)
|
||||
)
|
||||
)
|
||||
|
@ -290,6 +343,44 @@ var SearchResults = React.createClass({displayName: "SearchResults",
|
|||
}
|
||||
});
|
||||
|
||||
var SongResult = React.createClass({displayName: "SongResult",
|
||||
getInitialState: function () {
|
||||
return {
|
||||
pending: false
|
||||
};
|
||||
},
|
||||
pick: function (e) {
|
||||
e.preventDefault();
|
||||
this.props.pick(this.props.song.idSong);
|
||||
this.setState({
|
||||
pending: true
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var song = this.props.song;
|
||||
var pending = this.state.pending && !this.props.picked;
|
||||
var classes = classSet({
|
||||
'picked': this.props.picked,
|
||||
'pending': pending
|
||||
});
|
||||
return (
|
||||
React.createElement("li", {key: song.idSong, className: classes},
|
||||
React.createElement("a", {href: "#",
|
||||
onClick: this.pick},
|
||||
song.sArtist, " - ", song.sName
|
||||
),
|
||||
pending ? React.createElement(Throbber, null) : '',
|
||||
this.props.picked ? React.createElement("div", null, "✓") : ''
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Throbber = React.createClass({displayName: "Throbber",
|
||||
render: function () {
|
||||
return React.createElement("div", {className: "throb"});
|
||||
}
|
||||
});
|
||||
|
||||
var socket = io.connect('/');
|
||||
var o = React.createElement(Overview);
|
||||
|
|
Загрузка…
Ссылка в новой задаче