Moving commands out of Snapcast
This commit is contained in:
2
src/js/bootstrap.js
vendored
2
src/js/bootstrap.js
vendored
@ -89,7 +89,7 @@ var initialState = {
|
||||
groups: {},
|
||||
clients: {},
|
||||
server: null,
|
||||
client_commands: {}
|
||||
commands: {}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -76,21 +76,16 @@ class OutputControl extends React.Component{
|
||||
clients.map(client => {
|
||||
|
||||
var commands = {};
|
||||
if (this.props.snapcast_client_commands[client.id]){
|
||||
commands = this.props.snapcast_client_commands[client.id];
|
||||
if (this.props.snapcast_commands[client.id]){
|
||||
commands = this.props.snapcast_commands[client.id];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"output-control__item outputs__item--snapcast"+(commands && commands.power ? " output-control__item--has-power-button": '')} key={client.id}>
|
||||
<div className="output-control__item outputs__item--snapcast" key={client.id}>
|
||||
<div className="output-control__item__name">
|
||||
{client.name}
|
||||
</div>
|
||||
<div className="output-control__item__controls">
|
||||
{commands && commands.power ? <SnapcastPowerButton
|
||||
className="output-control__item__power"
|
||||
client={client}
|
||||
onClick={e => this.props.snapcastActions.sendClientCommand(client.id, commands.power)}
|
||||
/> : null}
|
||||
<MuteControl
|
||||
className="output-control__item__mute"
|
||||
noTooltip={true}
|
||||
@ -189,7 +184,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
pusher_connected: state.pusher.connected,
|
||||
snapcast_enabled: (state.pusher.config ? state.pusher.config.snapcast_enabled : null),
|
||||
snapcast_clients: state.snapcast.clients,
|
||||
snapcast_client_commands: (state.snapcast.client_commands ? state.snapcast.client_commands : {})
|
||||
snapcast_commands: (state.snapcast.commands ? state.snapcast.commands : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,10 +29,6 @@ export default class Notifications extends React.Component{
|
||||
this.props.geniusActions.importAuthorization(configuration.genius.authorization, configuration.genius.me);
|
||||
}
|
||||
|
||||
if (configuration.snapcast_client_commands){
|
||||
this.props.snapcastActions.clientCommandsUpdated(configuration.snapcast_client_commands);
|
||||
}
|
||||
|
||||
this.props.uiActions.removeNotification(notification_key, true);
|
||||
this.props.uiActions.createNotification({type: 'info', content: 'Import successful'});
|
||||
}
|
||||
@ -72,7 +68,6 @@ export default class Notifications extends React.Component{
|
||||
{notification.configuration.spotify ? <li>Spotify</li> : null}
|
||||
{notification.configuration.lastfm ? <li>LastFM</li> : null}
|
||||
{notification.configuration.genius ? <li>Genius</li> : null}
|
||||
{notification.configuration.snapcast_client_commands ? <li>Snapcast</li> : null}
|
||||
</ul>
|
||||
<p>Do you want to import this?</p>
|
||||
</div>
|
||||
|
||||
@ -89,8 +89,8 @@ class Snapcast extends React.Component{
|
||||
if (this.state.clients_expanded.includes(client.id)){
|
||||
|
||||
var commands = {};
|
||||
if (this.props.client_commands[client.id]){
|
||||
commands = this.props.client_commands[client.id];
|
||||
if (this.props.commands[client.id]){
|
||||
commands = this.props.commands[client.id];
|
||||
}
|
||||
|
||||
return (
|
||||
@ -176,7 +176,7 @@ class Snapcast extends React.Component{
|
||||
</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
onChange={value => this.props.snapcastActions.setClientCommand(client.id, {power: value})}
|
||||
onChange={value => this.props.snapcastActions.setCommand(client.id, {power: value})}
|
||||
value={commands.power ? commands.power : ""}
|
||||
placeholder='{"url":"https://myserver.local:8080/sendCommand/power"}'
|
||||
/>
|
||||
@ -335,8 +335,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
show_disconnected_clients: (state.ui.snapcast_show_disconnected_clients !== undefined ? state.ui.snapcast_show_disconnected_clients : false),
|
||||
streams: (state.snapcast.streams ? state.snapcast.streams : null),
|
||||
groups: (state.snapcast.groups ? state.snapcast.groups : null),
|
||||
clients: (state.snapcast.clients ? state.snapcast.clients : null),
|
||||
client_commands: (state.snapcast.client_commands ? state.snapcast.client_commands : {})
|
||||
clients: (state.snapcast.clients ? state.snapcast.clients : null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ import KioskMode from './views/modals/KioskMode';
|
||||
import ShareConfiguration from './views/modals/ShareConfiguration';
|
||||
import AddToPlaylist from './views/modals/AddToPlaylist';
|
||||
import ImageZoom from './views/modals/ImageZoom';
|
||||
import EditCommand from './views/modals/EditCommand';
|
||||
|
||||
global.baseURL = '/';
|
||||
|
||||
@ -59,6 +60,7 @@ ReactDOM.render(
|
||||
<Route path="add-to-playlist/:uris" component={AddToPlaylist} />
|
||||
<Route path="image-zoom" component={ImageZoom} />
|
||||
<Route path="share-configuration" component={ShareConfiguration} />
|
||||
<Route path="edit-command(/:id)" component={EditCommand} />
|
||||
|
||||
<Route path="queue" component={Queue} />
|
||||
<Route path="queue/history" component={QueueHistory} />
|
||||
|
||||
@ -240,11 +240,11 @@ const localstorageMiddleware = (function(){
|
||||
);
|
||||
break;
|
||||
|
||||
case 'SNAPCAST_CLIENT_COMMANDS_UPDATED':
|
||||
case 'SNAPCAST_COMMANDS_UPDATED':
|
||||
helpers.setStorage(
|
||||
'snapcast',
|
||||
{
|
||||
client_commands: action.client_commands
|
||||
commands: action.commands
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
@ -192,97 +192,28 @@ export function addQueueMetadata(tlids = [], from_uri = null){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Snapcast actions
|
||||
* TODO: Figure out how to cleanly split this out to it's own service
|
||||
* but still share the pusher middleware connection
|
||||
* Commands (buttons)
|
||||
**/
|
||||
|
||||
export function getSnapcast(){
|
||||
return {
|
||||
type: 'PUSHER_GET_SNAPCAST',
|
||||
data: {
|
||||
method: 'Server.GetStatus'
|
||||
}
|
||||
}
|
||||
export function setCommand(command){
|
||||
return {
|
||||
type: 'PUSHER_SET_COMMAND',
|
||||
command: command
|
||||
}
|
||||
}
|
||||
|
||||
export function snapcastServerLoaded(server){
|
||||
return {
|
||||
type: 'PUSHER_SNAPCAST_SERVER_LOADED',
|
||||
server: server
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientName(id, name){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_NAME',
|
||||
id: id,
|
||||
name: name
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientMute(id, mute){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_MUTE',
|
||||
id: id,
|
||||
mute: mute
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientVolume(id, percent){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_VOLUME',
|
||||
id: id,
|
||||
percent: percent
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientLatency(id, latency){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_LATENCY',
|
||||
id: id,
|
||||
latency: latency
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientGroup(id, group_id){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_GROUP',
|
||||
id: id,
|
||||
group_id: group_id
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteSnapcastClient(id){
|
||||
return {
|
||||
type: 'PUSHER_DELETE_SNAPCAST_CLIENT',
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastGroupStream(id, stream_id){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_GROUP_STREAM',
|
||||
id: id,
|
||||
stream_id: stream_id
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastGroupMute(id, mute){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_GROUP_MUTE',
|
||||
id: id,
|
||||
mute: mute
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastGroupVolume(id, percent, old_percent = 0){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_GROUP_VOLUME',
|
||||
id: id,
|
||||
percent: percent,
|
||||
old_percent: old_percent
|
||||
}
|
||||
export function sendCommand(id){
|
||||
return {
|
||||
type: 'PUSHER_SEND_COMMAND',
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
export function commandsUpdated(commands){
|
||||
return {
|
||||
type: 'PUSHER_COMMANDS_UPDATED',
|
||||
commands: commands
|
||||
}
|
||||
}
|
||||
@ -387,6 +387,51 @@ const PusherMiddleware = (function(){
|
||||
return next(action);
|
||||
break;
|
||||
|
||||
|
||||
/**
|
||||
* Commands
|
||||
**/
|
||||
|
||||
case 'PUSHER_SET_COMMAND':
|
||||
var commands_index = Object.assign({}, store.getState().pusher.commands);
|
||||
|
||||
if (commands_index[action.command.id]){
|
||||
var command = Object.assign({}, commands_index[action.command.id], action.command);
|
||||
} else {
|
||||
var command = action.command;
|
||||
}
|
||||
commands_index[action.command.id] = command;
|
||||
|
||||
store.dispatch(pusherActions.commandsUpdated(commands_index));
|
||||
|
||||
next(action);
|
||||
break
|
||||
|
||||
case 'PUSHER_SEND_COMMAND':
|
||||
var commands_index = Object.assign({}, pusher.commands);
|
||||
|
||||
// Prepare our command
|
||||
// We try and make it cross-origin compatible
|
||||
var command = JSON.parse(commands_index[action.id]);
|
||||
command.crossDomain = true;
|
||||
command.dataType = "jsonp";
|
||||
command.success = function(response){
|
||||
store.dispatch(uiActions.createNotification({type: 'info', content: 'Command sent', description: command.url}));
|
||||
}
|
||||
command.fail = function(xhr, status, error){
|
||||
store.dispatch(uiActions.createNotification({type: 'bad', content: 'Command failed', description: error}));
|
||||
}
|
||||
|
||||
// Actually send the request
|
||||
$.ajax(command);
|
||||
|
||||
break
|
||||
|
||||
|
||||
/**
|
||||
* Radio
|
||||
**/
|
||||
|
||||
case 'PUSHER_GET_RADIO':
|
||||
request(store, 'get_radio')
|
||||
.then(
|
||||
|
||||
@ -54,30 +54,8 @@ export default function reducer(pusher = {}, action){
|
||||
case 'PUSHER_CONFIG':
|
||||
return Object.assign({}, pusher, { config: action.config });
|
||||
|
||||
case 'PUSHER_SNAPCAST':
|
||||
return Object.assign({}, pusher, {
|
||||
snapcast_streams: action.snapcast_streams,
|
||||
snapcast_groups: action.snapcast_groups,
|
||||
snapcast_clients: action.snapcast_clients
|
||||
});
|
||||
|
||||
case 'PUSHER_SNAPCAST_GROUP_UPDATED':
|
||||
var snapcast_groups = Object.assign({}, pusher.snapcast_groups);
|
||||
var group = snapcast_groups[action.key];
|
||||
snapcast_groups[action.key] = Object.assign({}, group, action.group);
|
||||
return Object.assign({}, pusher, { snapcast_groups: snapcast_groups });
|
||||
|
||||
case 'PUSHER_SNAPCAST_CLIENT_UPDATED':
|
||||
var snapcast_clients = Object.assign({}, pusher.snapcast_clients);
|
||||
var client = snapcast_clients[action.key];
|
||||
client.config = Object.assign({}, client.config, action.client.config);
|
||||
snapcast_clients[action.key] = client;
|
||||
return Object.assign({}, pusher, { snapcast_clients: snapcast_clients });
|
||||
|
||||
case 'PUSHER_SNAPCAST_CLIENT_REMOVED':
|
||||
var snapcast_clients = Object.assign({}, pusher.snapcast_clients);
|
||||
delete snapcast_clients[action.key];
|
||||
return Object.assign({}, pusher, { snapcast_clients: snapcast_clients });
|
||||
case 'PUSHER_COMMANDS_UPDATED':
|
||||
return Object.assign({}, pusher, { commands: action.commands });
|
||||
|
||||
default:
|
||||
return pusher
|
||||
|
||||
@ -37,29 +37,6 @@ export function setClientLatency(id, latency){
|
||||
}
|
||||
}
|
||||
|
||||
export function setClientCommand(id, command){
|
||||
return {
|
||||
type: 'SNAPCAST_SET_CLIENT_COMMAND',
|
||||
id: id,
|
||||
command: command
|
||||
}
|
||||
}
|
||||
|
||||
export function sendClientCommand(id, command){
|
||||
return {
|
||||
type: 'SNAPCAST_SEND_CLIENT_COMMAND',
|
||||
id: id,
|
||||
command: command
|
||||
}
|
||||
}
|
||||
|
||||
export function clientCommandsUpdated(client_commands){
|
||||
return {
|
||||
type: 'SNAPCAST_CLIENT_COMMANDS_UPDATED',
|
||||
client_commands: client_commands
|
||||
}
|
||||
}
|
||||
|
||||
export function setClientGroup(id, group_id){
|
||||
return {
|
||||
type: 'SNAPCAST_SET_CLIENT_GROUP',
|
||||
|
||||
@ -228,37 +228,6 @@ const SnapcastMiddleware = (function(){
|
||||
);
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SET_CLIENT_COMMAND':
|
||||
var client_commands_index = Object.assign({}, snapcast.client_commands);
|
||||
|
||||
if (client_commands_index[action.id]){
|
||||
var client_command = Object.assign({}, client_commands_index[action.id], action.command);
|
||||
} else {
|
||||
var client_command = action.command;
|
||||
}
|
||||
client_commands_index[action.id] = client_command;
|
||||
|
||||
store.dispatch(snapcastActions.clientCommandsUpdated(client_commands_index));
|
||||
|
||||
next(action);
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SEND_CLIENT_COMMAND':
|
||||
|
||||
// Prepare our command
|
||||
// We try and make it cross-origin compatible
|
||||
var command = JSON.parse(action.command);
|
||||
command.crossDomain = true;
|
||||
command.dataType = "jsonp";
|
||||
command.success = function(response){
|
||||
store.dispatch(uiActions.createNotification({type: 'info', content: 'Command sent', description: command.url}));
|
||||
}
|
||||
|
||||
// Actually send the request
|
||||
$.ajax(command);
|
||||
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SET_CLIENT_GROUP':
|
||||
|
||||
var group = snapcast.groups[action.group_id];
|
||||
|
||||
@ -6,9 +6,6 @@ export default function reducer(snapcast = {}, action){
|
||||
var server = Object.assign({}, action.server);
|
||||
return Object.assign({}, snapcast, { server: server });
|
||||
|
||||
case 'SNAPCAST_CLIENT_COMMANDS_UPDATED':
|
||||
return Object.assign({}, snapcast, { client_commands: action.client_commands });
|
||||
|
||||
case 'SNAPCAST_CLIENTS_LOADED':
|
||||
if (action.flush){
|
||||
var clients = {};
|
||||
|
||||
@ -166,6 +166,15 @@ class Settings extends React.Component {
|
||||
var upgrade_button = null;
|
||||
}
|
||||
|
||||
var commands = [];
|
||||
if (this.props.pusher.commands){
|
||||
for (var id in this.props.pusher.commands){
|
||||
if (this.props.pusher.commands.hasOwnProperty(id)){
|
||||
commands.push(this.props.pusher.commands[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="view settings-view">
|
||||
<Header options={options} uiActions={this.props.uiActions}>
|
||||
@ -325,10 +334,28 @@ class Settings extends React.Component {
|
||||
Allow reporting of anonymous usage statistics
|
||||
</span>
|
||||
</label>
|
||||
<div className="description">Anonymous usage data is used to identify errors and potential features that make Iris better for everyone. Read the <a href="https://github.com/jaedb/Iris/wiki/Terms-of-use#privacy-policy" target="_blank">privacy policy</a>.</div>
|
||||
<div className="description">This helps identify errors and potential features that make Iris better for everyone. See <a href="https://github.com/jaedb/Iris/wiki/Terms-of-use#privacy-policy" target="_blank">privacy policy</a>.</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className="field commands-setup">
|
||||
<div className="name">
|
||||
Commands
|
||||
</div>
|
||||
<div className="input">
|
||||
{
|
||||
commands.map(command => {
|
||||
return (
|
||||
<Link className="command__item commands-setup__item" key={command.id} to={global.baseURL+'edit-command/'+command.id}>
|
||||
<Icon name={command.icon} />
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
}
|
||||
<Link to={global.baseURL+'edit-command'} className="button">Add new</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="underline">Advanced</h4>
|
||||
|
||||
<div className="field">
|
||||
|
||||
111
src/js/views/modals/EditCommand.js
Executable file
111
src/js/views/modals/EditCommand.js
Executable file
@ -0,0 +1,111 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
import ReactGA from 'react-ga'
|
||||
|
||||
import Modal from './Modal';
|
||||
import Icon from '../../components/Icon';
|
||||
import * as pusherActions from '../../services/pusher/actions';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as helpers from '../../helpers';
|
||||
|
||||
class EditCommand extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
id: helpers.generateGuid(),
|
||||
icon: 'power_settings_new',
|
||||
command: '{"url":"https://myserver.local:8080/sendCommand/power"}'
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (this.props.command){
|
||||
this.props.uiActions.setWindowTitle("Edit command");
|
||||
this.setState(this.props.command);
|
||||
} else {
|
||||
this.props.uiActions.setWindowTitle("Create command");
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e){
|
||||
e.preventDefault();
|
||||
|
||||
this.props.pusherActions.setCommand({
|
||||
id: this.state.id,
|
||||
icon: this.state.icon,
|
||||
command: this.state.command
|
||||
});
|
||||
|
||||
window.history.back();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Modal className="modal--create-command">
|
||||
<h1>Create command</h1>
|
||||
<form onSubmit={(e) => this.handleSubmit(e)}>
|
||||
|
||||
<div className="field text white">
|
||||
<div className="name">
|
||||
Icon
|
||||
</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
name="icon"
|
||||
value={this.state.icon}
|
||||
onChange={ e => this.setState({ icon: e.target.value })}
|
||||
/>
|
||||
<div className="description">
|
||||
Material icon, see <a href="https://material.io/tools/icons/?style=baseline" target="_blank" noopener="true">docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field textarea white">
|
||||
<div className="name tooltip">
|
||||
Command
|
||||
<div className="tooltip__content">
|
||||
Ajax configuration (JSON string)
|
||||
</div>
|
||||
</div>
|
||||
<div className="input">
|
||||
<textarea
|
||||
name="command"
|
||||
value={this.state.command}
|
||||
onChange={ e => this.setState({ command: e.target.value })}>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="actions centered-text">
|
||||
<button type="submit" className="primary large">Save</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
var id = ownProps.params.id;
|
||||
return {
|
||||
command: (id && state.pusher.commands && state.pusher.commands[id] !== undefined ? state.pusher.commands[id] : null)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
pusherActions: bindActionCreators(pusherActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditCommand)
|
||||
@ -21,7 +21,6 @@ class ShareConfiguration extends React.Component {
|
||||
spotify: false,
|
||||
lastfm: false,
|
||||
genius: false,
|
||||
snapcast_client_commands: false,
|
||||
ui: false
|
||||
};
|
||||
}
|
||||
@ -66,9 +65,6 @@ class ShareConfiguration extends React.Component {
|
||||
if (this.state.ui){
|
||||
configuration.ui = this.props.ui;
|
||||
}
|
||||
if (this.state.snapcast_client_commands){
|
||||
configuration.snapcast_client_commands = this.props.snapcast_client_commands;
|
||||
}
|
||||
|
||||
for (var recipient of this.state.recipients){
|
||||
this.props.pusherActions.deliverMessage(
|
||||
@ -149,17 +145,6 @@ class ShareConfiguration extends React.Component {
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{this.props.snapcast_client_commands ? <label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="snapcast_client_commands"
|
||||
checked={this.state.snapcast_client_commands}
|
||||
onChange={ e => this.setState({ snapcast_client_commands: !this.state.snapcast_client_commands })} />
|
||||
<span className="label">
|
||||
Snapcast client commands
|
||||
</span>
|
||||
</label> : null}
|
||||
|
||||
{this.props.spotify_me && this.props.spotify_authorization ? <label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -212,7 +197,6 @@ const mapStateToProps = (state, ownProps) => {
|
||||
genius_me: state.genius.me,
|
||||
lastfm_authorization: state.lastfm.authorization,
|
||||
lastfm_me: state.lastfm.me,
|
||||
snapcast_client_commands: state.snapcast.client_commands,
|
||||
ui: state.ui,
|
||||
connection_id: state.pusher.connection_id,
|
||||
connections: state.pusher.connections
|
||||
|
||||
@ -79,8 +79,8 @@
|
||||
width: 18%;
|
||||
padding-right: 2%;
|
||||
float: left;
|
||||
opacity: 0.5;
|
||||
padding-top: 8px;
|
||||
color: $mid_grey;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
Reference in New Issue
Block a user