Design rework of the station profile page.

This commit is contained in:
Buster Neece 2019-05-10 00:44:47 -05:00
parent a9ff682956
commit 4774b5f29c
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
31 changed files with 3951 additions and 3660 deletions

View File

@ -528,6 +528,10 @@ return function(App $app)
$this->get('/profile', Controller\Stations\ProfileController::class)
->setName('stations:profile:index');
$this->get('/profile/toggle/{feature}/{csrf}', Controller\Stations\ProfileController::class.':toggleAction')
->setName('stations:profile:toggle')
->add([Middleware\Permissions::class, Acl::STATION_PROFILE, true]);
$this->map(['GET', 'POST'], '/profile/edit', Controller\Stations\ProfileController::class.':editAction')
->setName('stations:profile:edit')
->add([Middleware\Permissions::class, Acl::STATION_PROFILE, true]);

View File

@ -168,6 +168,17 @@ return function (\Azura\Container $di)
$view->registerFunction('truncateUrl', function($url) {
return \App\Utilities::truncateUrl($url);
});
$view->registerFunction('link', function($url, $external = true, $truncate = true) {
$url = htmlspecialchars($url, \ENT_QUOTES, 'UTF-8');
$a = ['href="'.$url.'"'];
if ($external) {
$a[] = 'target="_blank"';
}
$a_body = ($truncate) ? \App\Utilities::truncateUrl($url) : $url;
return '<a '.implode(' ', $a).'>'.$a_body.'</a>';
});
$view->addData([
'assets' => $di[\Azura\Assets::class],

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,9 @@ class ProfileController
/** @var StationForm */
protected $station_form;
/** @var string */
protected $csrf_namespace = 'stations_profile';
/**
* @param EntityManager $em
* @param StationForm $station_form
@ -119,7 +122,7 @@ class ProfileController
$np = array_intersect_key($station_np->toArray(), $np) + $np;
}
return $view->renderToResponse($response, 'stations/profile/index', [
$view->addData([
'num_songs' => $num_songs,
'num_playlists' => $num_playlists,
'stream_urls' => $stream_urls,
@ -128,7 +131,10 @@ class ProfileController
'frontend_type' => $station->getFrontendType(),
'frontend_config' => (array)$station->getFrontendConfig(),
'nowplaying' => $np,
'csrf' => $request->getSession()->getCsrf()->generate($this->csrf_namespace),
]);
return $view->renderToResponse($response, 'stations/profile/index');
}
public function editAction(Request $request, Response $response, $station_id): ResponseInterface
@ -143,4 +149,32 @@ class ProfileController
'form' => $this->station_form,
]);
}
public function toggleAction(Request $request, Response $response, $station_id, $feature, $csrf_token): ResponseInterface
{
$request->getSession()->getCsrf()->verify($csrf_token, $this->csrf_namespace);
$station = $request->getStation();
switch($feature) {
case 'requests':
$station->setEnableRequests(!$station->getEnableRequests());
break;
case 'streamers':
$station->setEnableStreamers(!$station->getEnableStreamers());
break;
case 'public':
$station->setEnablePublicPage(!$station->getEnablePublicPage());
break;
}
$this->em->persist($station);
$this->em->flush($station);
$this->em->refresh($station);
return $response->withRedirect($request->getRouter()->fromHere('stations:profile:index'));
}
}

View File

@ -97,21 +97,21 @@ $(function() {
url: '<?=$router->fromHere('api:stations:status') ?>',
success: function(data) {
var backend_status = $('#backend_status');
backend_status.removeClass('text-success text-danger');
backend_status.removeClass('badge-success badge-danger');
if (data.backend_running) {
backend_status.addClass('text-success').text(service_status_lang.running);
backend_status.addClass('badge-success').text(service_status_lang.running);
} else {
backend_status.addClass('text-danger').text(service_status_lang.not_running);
backend_status.addClass('badge-danger').text(service_status_lang.not_running);
}
var frontend_status = $('#frontend_status');
frontend_status.removeClass('text-success text-danger');
frontend_status.removeClass('badge-success badge-danger');
if (data.frontend_running) {
frontend_status.addClass('text-success').text(service_status_lang.running);
frontend_status.addClass('badge-success').text(service_status_lang.running);
} else {
frontend_status.addClass('text-danger').text(service_status_lang.not_running);
frontend_status.addClass('badge-danger').text(service_status_lang.not_running);
}
if (is_first_load) {

View File

@ -1,5 +1,8 @@
<?php
/** @var App\Entity\Station $station */
/**
* @var App\Entity\Station $station
* @var \Azura\Assets $assets
*/
$this->layout('main', [
'title' => __('Profile'),
@ -7,393 +10,49 @@ $this->layout('main', [
'sidebar_tab' => 'profile',
]);
/** @var \Azura\Assets $assets */
$assets
->load('vue')
->load('inline_player')
->addInlineJs($this->fetch('partials/radio_controls.js'), 95)
->load('clipboard')
->addInlineJs($this->fetch('stations/profile/index.js', ['nowplaying' => $nowplaying]), 99);
$user = $request->getUser();
?>
<div class="row">
<div class="col-lg-6">
<section class="card mb-4 nowplaying" role="region" id="profile-nowplaying">
<div class="card-header bg-primary-dark">
<div class="d-flex">
<h3 class="flex-shrink card-title mt-1 mb-0"><?=__('On the Air') ?></h3>
<h6 class="card-subtitle text-right flex-fill mt-1 mb-0" style="line-height: 1;">
<i class="material-icons sm align-middle" aria-hidden="true">headset</i> <span>{{ np.listeners.total }}</span> <span v-if="np.listeners.total == 1"><?=__('Listener') ?></span><span v-else><?=__('Listeners') ?></span><br>
<small>
<span>{{ np.listeners.unique }}</span> <?=__('Unique') ?>
</small>
</h6>
</div>
</div>
<div class="card-body">
<div class="row pt-1">
<div class="col-md-6">
<div class="position-relative" style="margin-left: 20px;">
<div class="position-absolute" style="top: 0; left: -20px;">
<i class="material-icons sm align-top">music_note</i>
</div>
<div class="col-lg-7">
<?=$this->fetch('stations/profile/panel_header') ?>
<h6><?=__('Now Playing') ?>:</h6>
<?=$this->fetch('stations/profile/panel_nowplaying') ?>
<div v-if="np.now_playing.song.title != ''">
<h5 class="media-heading m-0" style="line-height: 1;">
{{ np.now_playing.song.title }}<br>
<small>{{ np.now_playing.song.artist }}</small>
</h5>
</div>
<div v-else>
<h5 class="media-heading m-0" style="line-height: 1;">{{ np.now_playing.song.text }}</h5>
</div>
<div v-if="np.now_playing.playlist">
<small class="text-muted"><?=__('Playlist') ?>: {{ np.now_playing.playlist }}</small>
</div>
<div class="nowplaying-progress" v-if="time_display">
<small>{{ time_display }}</small>
</div>
</div>
</div>
<div class="col-md-6" v-if="!np.live.is_live && np.playing_next">
<div class="position-relative" style="margin-left: 20px;">
<div class="position-absolute" style="top: 0; left: -20px;">
<i class="material-icons sm align-top">skip_next</i>
</div>
<h6><?=__('Playing Next') ?>:</h6>
<div v-if="np.playing_next.song.title != ''">
<h5 class="media-heading m-0" style="line-height: 1;">
{{ np.playing_next.song.title }}<br>
<small>{{ np.playing_next.song.artist }}</small>
</h5>
</div>
<div v-else>
<h5 class="media-heading m-0" style="line-height: 1;">{{ np.playing_next.song.text }}</h5>
</div>
<div v-if="np.playing_next.playlist">
<small class="text-muted"><?=__('Playlist') ?>: {{ np.playing_next.playlist }}</small>
</div>
</div>
</div>
<div class="col-md-6" v-if="np.live.is_live">
<div class="position-relative" style="margin-left: 20px;">
<div class="position-absolute" style="top: 0; left: -20px;">
<i class="material-icons sm align-top">mic</i>
</div>
<h6><?=__('Now Streaming') ?>:</h6>
<h4 class="media-heading">
{{ np.live.streamer_name }}
</h4>
</div>
</div>
</div>
</div>
<?php if ($backend_type === \App\Radio\Adapters::BACKEND_LIQUIDSOAP && $acl->userAllowed($user, \App\Acl::STATION_BROADCASTING, $station->getId())): ?>
<div class="card-actions">
<a id="btn_skip_song" class="btn btn-outline-primary api-call no-reload" role="button" href="<?=$router->fromHere('api:stations:backend', ['do' => 'skip']) ?>" v-if="!np.live.is_live">
<i class="material-icons" aria-hidden="true">skip_next</i>
<?=__('Skip Song') ?>
</a>
<a id="btn_disconnect_streamer" class="btn btn-outline-primary api-call no-reload" role="button" href="<?=$router->fromHere('api:stations:backend', ['do' => 'disconnect']) ?>" v-if="np.live.is_live">
<i class="material-icons" aria-hidden="true">volume_off</i>
<?=__('Disconnect Streamer') ?>
</a>
</div>
<?php endif; ?>
</section>
<section class="card mb-4" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title"><?=__('Profile') ?></h3>
</div>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station->getId())): ?>
<div class="card-actions">
<a class="btn btn-outline-primary" role="button" href="<?=$router->fromHere('stations:profile:edit') ?>">
<i class="material-icons" aria-hidden="true">edit</i>
<?=__('Edit Profile') ?>
</a>
<?php if ($backend::supportsRequests() || $backend::supportsStreamers()): ?>
<div class="row">
<?php if ($backend::supportsRequests()): ?>
<div class="col">
<?=$this->fetch('stations/profile/panel_requests') ?>
</div>
<?php endif; ?>
<table class="table table-striped table-responsive mb-0">
<colgroup>
<col width="30%">
<col width="70%">
</colgroup>
<tbody>
<tr>
<td><?=__('Name') ?></td>
<td><?=$this->e($station->getName()) ?></td>
</tr>
<?php if (!empty($station->getDescription())): ?>
<tr>
<td><?=__('Description') ?></td>
<td><?=$this->e($station->getDescription()) ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($station->getGenre())): ?>
<tr>
<td><?=__('Genre') ?></td>
<td><?=$this->e($station->getGenre()) ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($station->getUrl())): ?>
<tr>
<td><?=__('Web Site URL') ?></td>
<td><a href="<?=$this->e($station->getUrl()) ?>" target="_blank"><?=$this->e($this->truncateUrl($station->getUrl())) ?></a></td>
</tr>
<?php endif; ?>
<?php if ($backend::supportsRequests()): ?>
<tr>
<td><?=__('Song Requests') ?></td>
<td>
<?php if ($station->getEnableRequests()): ?>
<span class="text-success"><?=__('Enabled') ?></span>
<div class="form-field">
<textarea id="request_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embedrequests', ['station' => $station->getShortName()], [], true) ?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 850px; border: 0;"></iframe></textarea>
</div>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#request_embed_url"><i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard') ?></button>
<?php else: ?>
<span class="text-danger"><?=__('Disabled') ?></span>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station->getId())): ?>
<br><small><?=__('<a href="%s">Edit station profile</a> to enable.', $router->fromHere('stations:profile:edit')) ?></small>
<?php endif; ?>
<?php endif; ?>
</td>
</tr>
<?php endif; ?>
<?php if ($backend::supportsStreamers()): ?>
<tr>
<td><?=__('Streamers/DJs') ?></td>
<td>
<?php if ($station->getEnableStreamers()): ?>
<span class="text-success"><?=__('Enabled') ?></span><br>
<a href="<?=$router->fromHere('stations:streamers:index') ?>"><?=__('Manage streamer accounts') ?></a>
<?php else: ?>
<span class="text-danger"><?=__('Disabled') ?></span>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station->getId())): ?>
<br><small><?=__('<a href="%s">Edit station profile</a> to enable.', $router->fromHere('stations:profile:edit')) ?></small>
<?php endif; ?>
<?php endif; ?>
</td>
</tr>
<?php endif; ?>
<tr>
<td><?=__('Base Directory') ?></td>
<td class="text-preformatted"><?=$this->e($station->getRadioBaseDir()) ?></td>
</tr>
<tr>
<td><?=__('Media Directory') ?></td>
<td class="text-preformatted"><?=$this->e($station->getRadioMediaDir()) ?></td>
</tr>
<tr>
<td><?=__('Player Embed Code') ?></td>
<td class="form-field">
<textarea id="player_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embed', ['station' => $station->getShortName()], [], true) ?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 150px; border: 0;"></iframe></textarea>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#player_embed_url"><i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard') ?></button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="col-lg-6">
<?php if (count($stream_urls['local']) > 0 || count($stream_urls['remote']) > 0): ?>
<section class="card mb-4" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title"><?=__('Streams') ?></h3>
<?php if ($backend::supportsStreamers()): ?>
<div class="col">
<?=$this->fetch('stations/profile/panel_streamers') ?>
</div>
<table class="table table-sm table-striped mb-0">
<colgroup>
<col width="10%">
<col width="90%">
</colgroup>
<?php if (count($stream_urls['local']) > 0): ?>
<thead>
<tr>
<th colspan="2"><?=__('Local Streams') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($stream_urls['local'] as [$stream_name, $stream_url]): ?>
<tr class="align-middle">
<td>
<a class="btn-audio" href="#" data-url="<?=$this->e((string)$stream_url) ?>">
<i class="material-icons" aria-hidden="true">play_circle_filled</i>
</a>
</td>
<td>
<h6 class="mb-0"><?=$this->e($stream_name) ?></h6>
<a href="<?=$this->e($stream_url) ?>"><?=$this->truncateUrl($stream_url) ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endif; ?>
<?php if (count($stream_urls['remote']) > 0): ?>
<thead>
<tr>
<th colspan="2"><?=__('Remote Relays') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($stream_urls['remote'] as [$stream_name, $stream_url]): ?>
<tr class="align-middle">
<td>
<a class="btn-audio" href="#" data-url="<?=$this->e((string)$stream_url) ?>">
<i class="material-icons" aria-hidden="true">play_circle_filled</i>
</a>
</td>
<td>
<h6 class="mb-0"><?=$this->e($stream_name) ?></h6>
<a href="<?=$this->e($stream_url) ?>"><?=$this->truncateUrl($stream_url) ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endif; ?>
</table>
<div class="card-actions">
<a class="btn btn-outline-primary" href="<?=$router->fromHere('public:playlist', ['format' => 'pls']) ?>">
<i class="material-icons" aria-hidden="true">file_download</i>
<?=__('Download %s', 'PLS') ?>
</a>
<a class="btn btn-outline-primary" href="<?=$router->fromHere('public:playlist', ['format' => 'm3u']) ?>">
<i class="material-icons" aria-hidden="true">file_download</i>
<?=__('Download %s', 'M3U') ?>
</a>
</div>
</section>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($frontend_type !== \App\Radio\Adapters::FRONTEND_REMOTE): ?>
<section class="card mb-4" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?php if ($frontend_type === \App\Radio\Adapters::FRONTEND_ICECAST): ?>
<?=__('Icecast (Broadcasting Service)') ?>
<?php elseif ($frontend_type === \App\Radio\Adapters::FRONTEND_SHOUTCAST): ?>
<?=__('SHOUTcast DNAS 2 (Broadcasting Service)') ?>
<?php endif; ?>
</h3>
</div>
<?=$this->fetch('stations/profile/panel_publicpages') ?>
</div>
<div class="card-body">
<h5 class="card-subtitle mt-1" id="frontend_status"><?=__('Loading...') ?></h5>
</div>
<div class="col-lg-5">
<?=$this->fetch('stations/profile/panel_streams') ?>
<table class="table table-striped table-responsive mb-0">
<colgroup>
<col width="30%">
<col width="70%">
</colgroup>
<tbody>
<tr>
<td><?=__('Administration URL') ?></td>
<td><a href="<?=$this->e($frontend->getAdminUrl($station)) ?>"><?=$this->e($frontend->getAdminUrl($station)) ?></a></td>
</tr>
<tr>
<td><?=__('Administrator Password') ?></td>
<td>
<span id="frontend_admin_pw"><?=$this->e($frontend_config['admin_pw']) ?></span>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#frontend_admin_pw"><i class="material-icons sm">file_copy</i></i><span class="sr-only"><?=__('Copy to Clipboard') ?></span></button>
</td>
</tr>
<tr>
<td><?=__('Source Password') ?></td>
<td>
<span id="frontend_source_pw"><?=$this->e($frontend_config['source_pw']) ?></span>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#frontend_source_pw"><i class="material-icons sm">file_copy</i><span class="sr-only"><?=__('Copy to Clipboard') ?></span></button>
</td>
</tr>
<?php if ($frontend_type === \App\Radio\Adapters::FRONTEND_ICECAST): ?>
<tr>
<td><?=__('Relay Password') ?></td>
<td>
<span id="frontend_relay_pw"><?=$this->e($frontend_config['relay_pw']) ?></span>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#frontend_relay_pw"><i class="material-icons sm">file_copy</i><span class="sr-only"><?=__('Copy to Clipboard') ?></span></button>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if (\App\Radio\Adapters::FRONTEND_REMOTE !== $frontend_type): ?>
<?=$this->fetch('stations/profile/panel_frontend') ?>
<?php endif; ?>
<?php if ($acl->userAllowed($user, 'manage station broadcasting', $station->getId())): ?>
<div class="card-actions">
<a class="api-call no-reload btn btn-outline-secondary" href="<?=$router->fromHere('api:stations:frontend', ['do' => 'restart']) ?>">
<i class="material-icons" aria-hidden="true">update</i>
<?=__('Restart') ?>
</a>
<a class="api-call no-reload btn btn-success" href="<?=$router->fromHere('api:stations:frontend', ['do' => 'start']) ?>">
<i class="material-icons" aria-hidden="true">play_arrow</i>
<?=__('Start') ?>
</a>
<a class="api-call no-reload btn btn-danger" href="<?=$router->fromHere('api:stations:frontend', ['do' => 'stop']) ?>">
<i class="material-icons" aria-hidden="true">stop</i>
<?=__('Stop') ?>
</a>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<?php if ($backend_type == 'none'): ?>
<section class="card" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title"><?=__('AutoDJ Disabled') ?></h3>
</div>
<div class="card-body">
<p><?=__('AutoDJ has been disabled for this station. No music will automatically be played when a source is not live.') ?></p>
</div>
</section>
<?php else: ?>
<section class="card" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?php if ($backend_type === \App\Radio\Adapters::BACKEND_LIQUIDSOAP): ?>
<?=__('Liquidsoap (AutoDJ Service)') ?><br>
<?php endif; ?>
</h3>
</div>
<div class="card-body">
<h5 class="card-subtitle mt-1" id="backend_status"><?=__('Loading...') ?></h5>
<p><?=sprintf(__('LiquidSoap is currently shuffling from <b>%d uploaded songs</b> in <b>%d playlists</b>.'), $num_songs, $num_playlists) ?></p>
<?php if ($acl->userAllowed($user, 'manage station media', $station->getId())): ?>
<div class="buttons">
<a class="btn btn-primary" href="<?=$router->fromHere('stations:files:index') ?>"><?=__('Music Files') ?></a>
<a class="btn btn-primary" href="<?=$router->fromHere('stations:playlists:index') ?>"><?=__('Playlists') ?></a>
</div>
<?php endif; ?>
</div>
<?php if ($acl->userAllowed($user, 'manage station broadcasting', $station->getId())): ?>
<div class="card-actions">
<a class="api-call no-reload btn btn-outline-secondary" href="<?=$router->fromHere('api:stations:backend', ['do' => 'restart']) ?>">
<i class="material-icons" aria-hidden="true">update</i>
<?=__('Restart') ?>
</a>
<a class="api-call no-reload btn btn-success" href="<?=$router->fromHere('api:stations:backend', ['do' => 'start']) ?>">
<i class="material-icons" aria-hidden="true">play_arrow</i>
<?=__('Start') ?>
</a>
<a class="api-call no-reload btn btn-danger" href="<?=$router->fromHere('api:stations:backend', ['do' => 'stop']) ?>">
<i class="material-icons" aria-hidden="true">stop</i>
<?=__('Stop') ?>
</a>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<?php if (\App\Radio\Adapters::BACKEND_NONE === $backend_type): ?>
<?=$this->fetch('stations/profile/panel_backend_none') ?>
<?php else: ?>
<?=$this->fetch('stations/profile/panel_backend') ?>
<?php endif; ?>
</div>
</div>

View File

@ -0,0 +1,51 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('AutoDJ Service') ?>
<small class="badge badge-pill" id="backend_status"><?=__('Loading...') ?></small>
<br>
<small>
<?php if (\App\Radio\Adapters::BACKEND_LIQUIDSOAP === $backend_type): ?>
Liquidsoap
<?php endif; ?>
</small>
</h3>
</div>
<div class="card-body">
<p><?=sprintf(__('LiquidSoap is currently shuffling from <b>%d uploaded songs</b> in <b>%d playlists</b>.'), $num_songs, $num_playlists) ?></p>
<?php if ($acl->userAllowed($user, 'manage station media', $station->getId())): ?>
<div class="buttons">
<a class="btn btn-primary" href="<?=$router->fromHere('stations:files:index') ?>"><?=__('Music Files') ?></a>
<a class="btn btn-primary" href="<?=$router->fromHere('stations:playlists:index') ?>"><?=__('Playlists') ?></a>
</div>
<?php endif; ?>
</div>
<?php if ($acl->userAllowed($user, 'manage station broadcasting', $station->getId())): ?>
<div class="card-actions">
<a class="api-call no-reload btn btn-outline-secondary" href="<?=$router->fromHere('api:stations:backend', ['do' => 'restart']) ?>">
<i class="material-icons" aria-hidden="true">update</i>
<?=__('Restart') ?>
</a>
<a class="api-call no-reload btn btn-outline-success" href="<?=$router->fromHere('api:stations:backend', ['do' => 'start']) ?>">
<i class="material-icons" aria-hidden="true">play_arrow</i>
<?=__('Start') ?>
</a>
<a class="api-call no-reload btn btn-outline-danger" href="<?=$router->fromHere('api:stations:backend', ['do' => 'stop']) ?>">
<i class="material-icons" aria-hidden="true">stop</i>
<?=__('Stop') ?>
</a>
</div>
<?php endif; ?>
</section>

View File

@ -0,0 +1,19 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title"><?=__('AutoDJ Disabled') ?></h3>
</div>
<div class="card-body">
<p><?=__('AutoDJ has been disabled for this station. No music will automatically be played when a source is not live.') ?></p>
</div>
</section>

View File

@ -0,0 +1,80 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card mb-4" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Broadcasting Service') ?>
<small class="badge badge-pill" id="frontend_status"><?=__('Loading...') ?></small>
<br>
<small>
<?php if (\App\Radio\Adapters::FRONTEND_ICECAST === $frontend_type): ?>
Icecast
<?php elseif (\App\Radio\Adapters::FRONTEND_SHOUTCAST === $frontend_type): ?>
SHOUTcast
<?php endif; ?>
</small>
</h3>
</div>
<table class="table table-striped table-responsive mb-0">
<colgroup>
<col width="30%">
<col width="70%">
</colgroup>
<tbody>
<tr>
<td><?=__('Administration URL') ?></td>
<td><a href="<?=$this->e($frontend->getAdminUrl($station)) ?>"><?=$this->e($frontend->getAdminUrl($station)) ?></a></td>
</tr>
<tr>
<td><?=__('Administrator Password') ?></td>
<td>
<span id="frontend_admin_pw"><?=$this->e($frontend_config['admin_pw']) ?></span>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#frontend_admin_pw"><i class="material-icons sm">file_copy</i></i><span class="sr-only"><?=__('Copy to Clipboard') ?></span></button>
</td>
</tr>
<tr>
<td><?=__('Source Password') ?></td>
<td>
<span id="frontend_source_pw"><?=$this->e($frontend_config['source_pw']) ?></span>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#frontend_source_pw"><i class="material-icons sm">file_copy</i><span class="sr-only"><?=__('Copy to Clipboard') ?></span></button>
</td>
</tr>
<?php if ($frontend_type === \App\Radio\Adapters::FRONTEND_ICECAST): ?>
<tr>
<td><?=__('Relay Password') ?></td>
<td>
<span id="frontend_relay_pw"><?=$this->e($frontend_config['relay_pw']) ?></span>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#frontend_relay_pw"><i class="material-icons sm">file_copy</i><span class="sr-only"><?=__('Copy to Clipboard') ?></span></button>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if ($acl->userAllowed($user, 'manage station broadcasting', $station->getId())): ?>
<div class="card-actions">
<a class="api-call no-reload btn btn-outline-secondary" href="<?=$router->fromHere('api:stations:frontend', ['do' => 'restart']) ?>">
<i class="material-icons" aria-hidden="true">update</i>
<?=__('Restart') ?>
</a>
<a class="api-call no-reload btn btn-outline-success" href="<?=$router->fromHere('api:stations:frontend', ['do' => 'start']) ?>">
<i class="material-icons" aria-hidden="true">play_arrow</i>
<?=__('Start') ?>
</a>
<a class="api-call no-reload btn btn-outline-danger" href="<?=$router->fromHere('api:stations:frontend', ['do' => 'stop']) ?>">
<i class="material-icons" aria-hidden="true">stop</i>
<?=__('Stop') ?>
</a>
</div>
<?php endif; ?>
</section>

View File

@ -0,0 +1,29 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<div class="card mb-4">
<div class="card-header bg-primary-dark d-flex">
<div class="flex-fill">
<h2 class="card-title"><?=$this->e($station->getName()) ?></h2>
<h3 class="card-subtitle">
<?=$this->e($station->getDescription()) ?>
</h3>
</div>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station->getId())): ?>
<div class="flex-shrink-0">
<a class="btn btn-light" role="button" href="<?=$router->fromHere('stations:profile:edit') ?>">
<i class="material-icons" aria-hidden="true">edit</i>
<?=__('Edit Profile') ?>
</a>
</div>
<?php endif; ?>
</div>
</div>

View File

@ -0,0 +1,103 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card mb-4 nowplaying" role="region" id="profile-nowplaying">
<div class="card-header bg-primary-dark">
<div class="d-flex">
<h3 class="flex-shrink card-title mt-1 mb-0"><?=__('On the Air') ?></h3>
<h6 class="card-subtitle text-right flex-fill mt-1 mb-0" style="line-height: 1;">
<i class="material-icons sm align-middle" aria-hidden="true">headset</i> <span>{{ np.listeners.total }}</span> <span v-if="np.listeners.total == 1"><?=__('Listener') ?></span><span v-else><?=__('Listeners') ?></span><br>
<small>
<span>{{ np.listeners.unique }}</span> <?=__('Unique') ?>
</small>
</h6>
</div>
</div>
<div class="card-body">
<div class="row pt-1">
<div class="col-md-6">
<div class="position-relative" style="margin-left: 20px;">
<div class="position-absolute" style="top: 0; left: -20px;">
<i class="material-icons sm align-top">music_note</i>
</div>
<h6><?=__('Now Playing') ?>:</h6>
<div v-if="np.now_playing.song.title != ''">
<h5 class="media-heading m-0" style="line-height: 1;">
{{ np.now_playing.song.title }}<br>
<small>{{ np.now_playing.song.artist }}</small>
</h5>
</div>
<div v-else>
<h5 class="media-heading m-0" style="line-height: 1;">{{ np.now_playing.song.text }}</h5>
</div>
<div v-if="np.now_playing.playlist">
<small class="text-muted"><?=__('Playlist') ?>: {{ np.now_playing.playlist }}</small>
</div>
<div class="nowplaying-progress" v-if="time_display">
<small>{{ time_display }}</small>
</div>
</div>
</div>
<div class="col-md-6" v-if="!np.live.is_live && np.playing_next">
<div class="position-relative" style="margin-left: 20px;">
<div class="position-absolute" style="top: 0; left: -20px;">
<i class="material-icons sm align-top">skip_next</i>
</div>
<h6><?=__('Playing Next') ?>:</h6>
<div v-if="np.playing_next.song.title != ''">
<h5 class="media-heading m-0" style="line-height: 1;">
{{ np.playing_next.song.title }}<br>
<small>{{ np.playing_next.song.artist }}</small>
</h5>
</div>
<div v-else>
<h5 class="media-heading m-0" style="line-height: 1;">{{ np.playing_next.song.text }}</h5>
</div>
<div v-if="np.playing_next.playlist">
<small class="text-muted"><?=__('Playlist') ?>: {{ np.playing_next.playlist }}</small>
</div>
</div>
</div>
<div class="col-md-6" v-if="np.live.is_live">
<div class="position-relative" style="margin-left: 20px;">
<div class="position-absolute" style="top: 0; left: -20px;">
<i class="material-icons sm align-top">mic</i>
</div>
<h6><?=__('Now Streaming') ?>:</h6>
<h4 class="media-heading">
{{ np.live.streamer_name }}
</h4>
</div>
</div>
</div>
</div>
<?php if ($backend_type === \App\Radio\Adapters::BACKEND_LIQUIDSOAP && $acl->userAllowed($user, \App\Acl::STATION_BROADCASTING, $station->getId())): ?>
<div class="card-actions">
<a id="btn_skip_song" class="btn btn-outline-primary api-call no-reload" role="button" href="<?=$router->fromHere('api:stations:backend', ['do' => 'skip']) ?>" v-if="!np.live.is_live">
<i class="material-icons" aria-hidden="true">skip_next</i>
<?=__('Skip Song') ?>
</a>
<a id="btn_disconnect_streamer" class="btn btn-outline-primary api-call no-reload" role="button" href="<?=$router->fromHere('api:stations:backend', ['do' => 'disconnect']) ?>" v-if="np.live.is_live">
<i class="material-icons" aria-hidden="true">volume_off</i>
<?=__('Disconnect Streamer') ?>
</a>
</div>
<?php endif; ?>
</section>

View File

@ -0,0 +1,78 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card mb-4" role="region">
<?php if ($station->getEnablePublicPage()): ?>
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Public Pages') ?>
<small class="badge badge-pill badge-success"><?=__('Enabled') ?></small>
</h3>
</div>
<table class="table table-striped table-responsive mb-0">
<tbody>
<tr>
<td style="width: 30%;"><?=__('Public Page') ?></td>
<td style="width: 70%;">
<?=$this->link($router->named('public:index', ['station' => $station->getShortName()], [], true)) ?>
</td>
</tr>
<?php if ($backend::supportsStreamers() && $station->getEnableStreamers()): ?>
<tr>
<td><?=__('Web DJ') ?></td>
<td>
<?=$this->link($router->named('public:dj', ['station' => $station->getShortName()], [], true)) ?>
</td>
</tr>
<?php endif; ?>
<tr>
<td><?=__('Player Embed Code') ?></td>
<td class="form-field">
<textarea id="player_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embed', ['station' => $station->getShortName()], [], true) ?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 150px; border: 0;"></iframe></textarea>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#player_embed_url"><i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard') ?></button>
</td>
</tr>
<?php if ($backend::supportsRequests() && $station->getEnableRequests()): ?>
<tr>
<td><?=__('Request Embed Code') ?></td>
<td class="form-field">
<textarea id="request_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embedrequests', ['station' => $station->getShortName()], [], true) ?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 850px; border: 0;"></iframe></textarea>
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#request_embed_url"><i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard') ?></button>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station)): ?>
<div class="card-actions">
<a class="btn btn-outline-danger" data-confirm-title=<?=$this->escapeJs(__('Disable public pages?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'public', 'csrf' => $csrf]) ?>">
<i class="material-icons" aria-hidden="true">highlight_off</i>
<?=__('Disable') ?>
</a>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Public Pages') ?>
<small class="badge badge-pill badge-danger"><?=__('Disabled') ?></small>
</h3>
</div>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station)): ?>
<div class="card-actions">
<a class="btn btn-outline-success" data-confirm-title=<?=$this->escapeJs(__('Enable public pages?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'public', 'csrf' => $csrf]) ?>">
<i class="material-icons" aria-hidden="true">check_circle_outline</i>
<?=__('Enable') ?>
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</section>

View File

@ -0,0 +1,50 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card mb-4" role="region">
<?php if ($station->getEnableRequests()): ?>
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Song Requests') ?>
<small class="badge badge-pill badge-success"><?=__('Enabled') ?></small>
</h3>
</div>
<div class="card-actions">
<?php if ($acl->userAllowed($user, \App\Acl::STATION_REPORTS, $station)): ?>
<a class="btn btn-outline-primary" href="<?=$router->fromHere('stations:reports:requests') ?>">
<i class="material-icons" aria-hidden="true">assignment</i>
<?=__('View') ?>
</a>
<?php endif; ?>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station)): ?>
<a class="btn btn-outline-danger" data-confirm-title=<?=$this->escapeJs(__('Disable song requests?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'requests', 'csrf' => $csrf]) ?>">
<i class="material-icons" aria-hidden="true">highlight_off</i>
<?=__('Disable') ?>
</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Song Requests') ?>
<small class="badge badge-pill badge-danger"><?=__('Disabled') ?></small>
</h3>
</div>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station)): ?>
<div class="card-actions">
<a class="btn btn-outline-success" data-confirm-title=<?=$this->escapeJs(__('Enable song requests?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'requests', 'csrf' => $csrf]) ?>">
<i class="material-icons" aria-hidden="true">check_circle_outline</i>
<?=__('Enable') ?>
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</section>

View File

@ -0,0 +1,50 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card mb-4" role="region">
<?php if ($station->getEnableStreamers()): ?>
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Streamers/DJs') ?>
<small class="badge badge-pill badge-success"><?=__('Enabled') ?></small>
</h3>
</div>
<div class="card-actions">
<?php if ($acl->userAllowed($user, \App\Acl::STATION_STREAMERS, $station)): ?>
<a class="btn btn-outline-primary" href="<?=$router->fromHere('stations:streamers:index') ?>">
<i class="material-icons" aria-hidden="true">mic</i>
<?=__('Manage') ?>
</a>
<?php endif; ?>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station)): ?>
<a class="btn btn-outline-danger" data-confirm-title=<?=$this->escapeJs(__('Disable streamers?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'streamers', 'csrf' => $csrf]) ?>">
<i class="material-icons" aria-hidden="true">highlight_off</i>
<?=__('Disable') ?>
</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="card-header bg-primary-dark">
<h3 class="card-title">
<?=__('Streamers/DJs') ?>
<small class="badge badge-pill badge-danger"><?=__('Disabled') ?></small>
</h3>
</div>
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station)): ?>
<div class="card-actions">
<a class="btn btn-outline-success" data-confirm-title=<?=$this->escapeJs(__('Enable streamers?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'streamers', 'csrf' => $csrf]) ?>">
<i class="material-icons" aria-hidden="true">check_circle_outline</i>
<?=__('Enable') ?>
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</section>

View File

@ -0,0 +1,76 @@
<?php
/**
* @var \App\Entity\Station $station
* @var \App\Acl $acl
* @var \App\Http\Router $router
* @var \App\Http\Request $request
*/
$user = $request->getUser();
?>
<section class="card mb-4" role="region">
<div class="card-header bg-primary-dark">
<h3 class="card-title"><?=__('Streams') ?></h3>
</div>
<table class="table table-sm table-striped mb-0">
<colgroup>
<col style="width: 10%;">
<col style="width: 90%;">
</colgroup>
<?php if (count($stream_urls['local']) > 0): ?>
<thead>
<tr>
<th colspan="2"><?=__('Local Streams') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($stream_urls['local'] as [$stream_name, $stream_url]): ?>
<tr class="align-middle">
<td>
<a class="btn-audio" href="#" data-url="<?=$this->e((string)$stream_url) ?>">
<i class="material-icons" aria-hidden="true">play_circle_filled</i>
</a>
</td>
<td>
<h6 class="mb-0"><?=$this->e($stream_name) ?></h6>
<?=$this->link($stream_url) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endif; ?>
<?php if (count($stream_urls['remote']) > 0): ?>
<thead>
<tr>
<th colspan="2"><?=__('Remote Relays') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($stream_urls['remote'] as [$stream_name, $stream_url]): ?>
<tr class="align-middle">
<td>
<a class="btn-audio" href="#" data-url="<?=$this->e((string)$stream_url) ?>">
<i class="material-icons" aria-hidden="true">play_circle_filled</i>
</a>
</td>
<td>
<h6 class="mb-0"><?=$this->e($stream_name) ?></h6>
<?=$this->link($stream_url) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<?php endif; ?>
</table>
<div class="card-actions">
<a class="btn btn-outline-primary" href="<?=$router->fromHere('public:playlist', ['format' => 'pls']) ?>">
<i class="material-icons" aria-hidden="true">file_download</i>
<?=__('Download %s', 'PLS') ?>
</a>
<a class="btn btn-outline-primary" href="<?=$router->fromHere('public:playlist', ['format' => 'm3u']) ?>">
<i class="material-icons" aria-hidden="true">file_download</i>
<?=__('Download %s', 'M3U') ?>
</a>
</div>
</section>

View File

@ -7,26 +7,19 @@ $assets
?>
<div class="card">
<div class="card-header ch-alt">
<h2><?=__('Song Requests') ?></h2>
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=__('Song Requests') ?></h2>
</div>
<div class="table-responsive">
<table class="data-table table table-striped">
<colgroup>
<col width="20%">
<col width="20%">
<col width="30%">
<col width="15%">
<col width="15%">
</colgroup>
<thead>
<tr>
<th data-column-id="timestamp"><?=__('Date Requested') ?></th>
<th data-column-id="played_at"><?=__('Date Played') ?></th>
<th data-column-id="song"><?=__('Song Title') ?></th>
<th data-column-id="ip"><?=__('Requester IP') ?></th>
<th data-column-id="actions"><?=__('Actions') ?></th>
</tr>
<tr>
<th style="width: 20%;" data-column-id="timestamp"><?=__('Date Requested') ?></th>
<th style="width: 20%;" data-column-id="played_at"><?=__('Date Played') ?></th>
<th style="width: 30%;" data-column-id="song"><?=__('Song Title') ?></th>
<th style="width: 15%;" data-column-id="ip"><?=__('Requester IP') ?></th>
<th style="width: 15%;" data-column-id="actions"><?=__('Actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($requests as $request_row): ?>

View File

@ -1,7 +1,7 @@
{
"dist/app.js": "dist/app-f91e2d10e8.js",
"dist/bootgrid.js": "dist/bootgrid-862d128fa2.js",
"dist/dark.css": "dist/dark-cf8feaa57d.css",
"dist/app.js": "dist/app-f230b9c022.js",
"dist/bootgrid.js": "dist/bootgrid-acbc545ec1.js",
"dist/dark.css": "dist/dark-a3097f87be.css",
"dist/inline_player.js": "dist/inline_player-cc8f01f2dd.js",
"dist/lib/autosize/autosize.min.js": "dist/lib/autosize/autosize-ad0656589d.min.js",
"dist/lib/bootstrap-notify/bootstrap-notify.min.js": "dist/lib/bootstrap-notify/bootstrap-notify-a02f92a499.min.js",
@ -38,9 +38,9 @@
"dist/lib/vue/vue.js": "dist/lib/vue/vue-5424a463b5.js",
"dist/lib/vue/vue.min.js": "dist/lib/vue/vue-f15aee8488.min.js",
"dist/lib/zxcvbn/zxcvbn.js": "dist/lib/zxcvbn/zxcvbn-9cf6916dc0.js",
"dist/light.css": "dist/light-9767b83557.css",
"dist/material.js": "dist/material-ad7c414bdf.js",
"dist/light.css": "dist/light-55f3a0bc75.css",
"dist/material.js": "dist/material-4f1d9f4f98.js",
"dist/radio_player.js": "dist/radio_player-7987ae4284.js",
"dist/webcaster.js": "dist/webcaster-6989962a2d.js",
"dist/zxcvbn.js": "dist/zxcvbn-f4433cd930.js"
"dist/zxcvbn.js": "dist/zxcvbn-82c9dedeea.js"
}

2
web/static/dist/app-f230b9c022.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
$(function(){$.extend($.fn.bootgrid.Constructor.defaults.css,{iconRefresh:"refresh",iconColumns:"list",iconSearch:"search",iconDown:"expand_more",iconUp:"expand_less",dropDownMenuItems:"dropdown-menu dropdown-menu-right",paginationButton:"page-link"}),$.extend($.fn.bootgrid.Constructor.defaults.templates,{icon:'<i class="material-icons">{{ctx.iconCss}}</i>',paginationItem:'<li class="paginate_button page-item {{ctx.css}}"><a data-page="{{ctx.page}}" class="{{css.paginationButton}}">{{ctx.text}}</a></li>'})});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJvb3RncmlkLmpzIl0sIm5hbWVzIjpbIiQiLCJleHRlbmQiLCJmbiIsImJvb3RncmlkIiwiQ29uc3RydWN0b3IiLCJkZWZhdWx0cyIsImNzcyIsImljb25SZWZyZXNoIiwiaWNvbkNvbHVtbnMiLCJpY29uU2VhcmNoIiwiaWNvbkRvd24iLCJpY29uVXAiLCJkcm9wRG93bk1lbnVJdGVtcyIsInBhZ2luYXRpb25CdXR0b24iLCJ0ZW1wbGF0ZXMiLCJpY29uIiwicGFnaW5hdGlvbkl0ZW0iXSwibWFwcGluZ3MiOiJBQUFBQSxFQUFFLFdBRUVBLEVBQUVDLE9BQU9ELEVBQUVFLEdBQUdDLFNBQVNDLFlBQVlDLFNBQVNDLElBQUssQ0FDN0NDLFlBQWEsVUFDYkMsWUFBYSxPQUNiQyxXQUFZLFNBQ1pDLFNBQVUsY0FDVkMsT0FBUSxjQUNSQyxrQkFBbUIsb0NBQ25CQyxpQkFBa0IsY0FHdEJiLEVBQUVDLE9BQU9ELEVBQUVFLEdBQUdDLFNBQVNDLFlBQVlDLFNBQVNTLFVBQVcsQ0FDbkRDLEtBQU0sZ0RBQ05DLGVBQWdCIiwiZmlsZSI6ImJvb3RncmlkLmpzIiwic291cmNlc0NvbnRlbnQiOlsiJChmdW5jdGlvbigpIHtcbiAgICAvLyBNYWtlIGpRdWVyeSBCb290Z3JpZCBjb21wYXRpYmxlIHdpdGggQm9vdHN0cmFwIDRcbiAgICAkLmV4dGVuZCgkLmZuLmJvb3RncmlkLkNvbnN0cnVjdG9yLmRlZmF1bHRzLmNzcywge1xuICAgICAgICBpY29uUmVmcmVzaDogXCJyZWZyZXNoXCIsXG4gICAgICAgIGljb25Db2x1bW5zOiBcImxpc3RcIixcbiAgICAgICAgaWNvblNlYXJjaDogXCJzZWFyY2hcIixcbiAgICAgICAgaWNvbkRvd246IFwiZXhwYW5kX21vcmVcIixcbiAgICAgICAgaWNvblVwOiBcImV4cGFuZF9sZXNzXCIsXG4gICAgICAgIGRyb3BEb3duTWVudUl0ZW1zOiBcImRyb3Bkb3duLW1lbnUgZHJvcGRvd24tbWVudS1yaWdodFwiLFxuICAgICAgICBwYWdpbmF0aW9uQnV0dG9uOiBcInBhZ2UtbGlua1wiXG4gICAgfSk7XG5cbiAgICAkLmV4dGVuZCgkLmZuLmJvb3RncmlkLkNvbnN0cnVjdG9yLmRlZmF1bHRzLnRlbXBsYXRlcywge1xuICAgICAgICBpY29uOiBcIjxpIGNsYXNzPVxcXCJtYXRlcmlhbC1pY29uc1xcXCI+e3tjdHguaWNvbkNzc319PC9pPlwiLFxuICAgICAgICBwYWdpbmF0aW9uSXRlbTogXCI8bGkgY2xhc3M9XFxcInBhZ2luYXRlX2J1dHRvbiBwYWdlLWl0ZW0ge3tjdHguY3NzfX1cXFwiPjxhIGRhdGEtcGFnZT1cXFwie3tjdHgucGFnZX19XFxcIiBjbGFzcz1cXFwie3tjc3MucGFnaW5hdGlvbkJ1dHRvbn19XFxcIj57e2N0eC50ZXh0fX08L2E+PC9saT5cIlxuICAgIH0pO1xufSk7XG4iXX0=
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJvb3RncmlkLmpzIl0sIm5hbWVzIjpbIiQiLCJleHRlbmQiLCJmbiIsImJvb3RncmlkIiwiQ29uc3RydWN0b3IiLCJkZWZhdWx0cyIsImNzcyIsImljb25SZWZyZXNoIiwiaWNvbkNvbHVtbnMiLCJpY29uU2VhcmNoIiwiaWNvbkRvd24iLCJpY29uVXAiLCJkcm9wRG93bk1lbnVJdGVtcyIsInBhZ2luYXRpb25CdXR0b24iLCJ0ZW1wbGF0ZXMiLCJpY29uIiwicGFnaW5hdGlvbkl0ZW0iXSwibWFwcGluZ3MiOiJBQUFBQSxFQUFFLFdBRUVBLEVBQUVDLE9BQU9ELEVBQUVFLEdBQUdDLFNBQVNDLFlBQVlDLFNBQVNDLElBQUssQ0FDN0NDLFlBQWEsVUFDYkMsWUFBYSxPQUNiQyxXQUFZLFNBQ1pDLFNBQVUsY0FDVkMsT0FBUSxjQUNSQyxrQkFBbUIsb0NBQ25CQyxpQkFBa0IsY0FHdEJiLEVBQUVDLE9BQU9ELEVBQUVFLEdBQUdDLFNBQVNDLFlBQVlDLFNBQVNTLFVBQVcsQ0FDbkRDLEtBQU0sZ0RBQ05DLGVBQWdCIiwiZmlsZSI6ImJvb3RncmlkLmpzIiwic291cmNlc0NvbnRlbnQiOlsiJChmdW5jdGlvbigpIHtcclxuICAgIC8vIE1ha2UgalF1ZXJ5IEJvb3RncmlkIGNvbXBhdGlibGUgd2l0aCBCb290c3RyYXAgNFxyXG4gICAgJC5leHRlbmQoJC5mbi5ib290Z3JpZC5Db25zdHJ1Y3Rvci5kZWZhdWx0cy5jc3MsIHtcclxuICAgICAgICBpY29uUmVmcmVzaDogXCJyZWZyZXNoXCIsXHJcbiAgICAgICAgaWNvbkNvbHVtbnM6IFwibGlzdFwiLFxyXG4gICAgICAgIGljb25TZWFyY2g6IFwic2VhcmNoXCIsXHJcbiAgICAgICAgaWNvbkRvd246IFwiZXhwYW5kX21vcmVcIixcclxuICAgICAgICBpY29uVXA6IFwiZXhwYW5kX2xlc3NcIixcclxuICAgICAgICBkcm9wRG93bk1lbnVJdGVtczogXCJkcm9wZG93bi1tZW51IGRyb3Bkb3duLW1lbnUtcmlnaHRcIixcclxuICAgICAgICBwYWdpbmF0aW9uQnV0dG9uOiBcInBhZ2UtbGlua1wiXHJcbiAgICB9KTtcclxuXHJcbiAgICAkLmV4dGVuZCgkLmZuLmJvb3RncmlkLkNvbnN0cnVjdG9yLmRlZmF1bHRzLnRlbXBsYXRlcywge1xyXG4gICAgICAgIGljb246IFwiPGkgY2xhc3M9XFxcIm1hdGVyaWFsLWljb25zXFxcIj57e2N0eC5pY29uQ3NzfX08L2k+XCIsXHJcbiAgICAgICAgcGFnaW5hdGlvbkl0ZW06IFwiPGxpIGNsYXNzPVxcXCJwYWdpbmF0ZV9idXR0b24gcGFnZS1pdGVtIHt7Y3R4LmNzc319XFxcIj48YSBkYXRhLXBhZ2U9XFxcInt7Y3R4LnBhZ2V9fVxcXCIgY2xhc3M9XFxcInt7Y3NzLnBhZ2luYXRpb25CdXR0b259fVxcXCI+e3tjdHgudGV4dH19PC9hPjwvbGk+XCJcclxuICAgIH0pO1xyXG59KTtcclxuIl19

2
web/static/dist/dark-a3097f87be.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
web/static/dist/light-55f3a0bc75.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
web/static/dist/zxcvbn-82c9dedeea.js vendored Normal file
View File

@ -0,0 +1,2 @@
$(document).ready(function(){$("input[type=password].strength").on("keyup",function(s){var a=zxcvbn($(this).val()),e=a.score,r=$(this).closest(".form-group");r.length||(r=$(this).closest("div"));var t=r.find(".form-text.password-explanation");if(!t.length){t=$('<small class="form-text password-explanation" />');var n=r.find("label");n.length?n.after(t):$(this).after(t),t=r.find(".form-text.password-explanation")}switch(a.feedback.warning?t.text(a.feedback.warning).show():t.hide(),r.removeClass("has-error has-success has-warning"),e){case 0:case 1:r.addClass("has-error");break;case 2:case 3:r.addClass("has-warning");break;case 4:r.addClass("has-success")}})});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInp4Y3Zibi5qcyJdLCJuYW1lcyI6WyIkIiwiZG9jdW1lbnQiLCJyZWFkeSIsIm9uIiwiZSIsInJlc3VsdCIsInp4Y3ZibiIsInRoaXMiLCJ2YWwiLCJzY29yZSIsImdyb3VwIiwiY2xvc2VzdCIsImxlbmd0aCIsImV4cGxhbmF0aW9uIiwiZmluZCIsImxhYmVsIiwiYWZ0ZXIiLCJmZWVkYmFjayIsIndhcm5pbmciLCJ0ZXh0Iiwic2hvdyIsImhpZGUiLCJyZW1vdmVDbGFzcyIsImFkZENsYXNzIl0sIm1hcHBpbmdzIjoiQUFBQUEsRUFBRUMsVUFBVUMsTUFBTSxXQUVkRixFQUFFLGlDQUFpQ0csR0FBRyxRQUFTLFNBQVNDLEdBRXBELElBQUlDLEVBQVNDLE9BQU9OLEVBQUVPLE1BQU1DLE9BQ3hCQyxFQUFRSixFQUFPSSxNQUVmQyxFQUFRVixFQUFFTyxNQUFNSSxRQUFRLGVBQ3ZCRCxFQUFNRSxTQUNQRixFQUFRVixFQUFFTyxNQUFNSSxRQUFRLFFBRzVCLElBQUlFLEVBQWNILEVBQU1JLEtBQUssbUNBRTdCLElBQUtELEVBQVlELE9BQVEsQ0FDckJDLEVBQWNiLEVBQUUsb0RBRWhCLElBQUllLEVBQVFMLEVBQU1JLEtBQUssU0FDbkJDLEVBQU1ILE9BQ05HLEVBQU1DLE1BQU1ILEdBRVpiLEVBQUVPLE1BQU1TLE1BQU1ILEdBR2xCQSxFQUFjSCxFQUFNSSxLQUFLLG1DQVc3QixPQVJJVCxFQUFPWSxTQUFTQyxRQUNoQkwsRUFBWU0sS0FBS2QsRUFBT1ksU0FBU0MsU0FBU0UsT0FFMUNQLEVBQVlRLE9BR2hCWCxFQUFNWSxZQUFZLHFDQUVWYixHQUNKLEtBQUssRUFDTCxLQUFLLEVBQ0RDLEVBQU1hLFNBQVMsYUFDZixNQUVKLEtBQUssRUFDTCxLQUFLLEVBQ0RiLEVBQU1hLFNBQVMsZUFDZixNQUVKLEtBQUssRUFDRGIsRUFBTWEsU0FBUyIsImZpbGUiOiJ6eGN2Ym4uanMiLCJzb3VyY2VzQ29udGVudCI6WyIkKGRvY3VtZW50KS5yZWFkeShmdW5jdGlvbigpIHtcclxuXHJcbiAgICAkKCdpbnB1dFt0eXBlPXBhc3N3b3JkXS5zdHJlbmd0aCcpLm9uKCdrZXl1cCcsIGZ1bmN0aW9uKGUpIHtcclxuXHJcbiAgICAgICAgdmFyIHJlc3VsdCA9IHp4Y3ZibigkKHRoaXMpLnZhbCgpKSxcclxuICAgICAgICAgICAgc2NvcmUgPSByZXN1bHQuc2NvcmU7XHJcblxyXG4gICAgICAgIHZhciBncm91cCA9ICQodGhpcykuY2xvc2VzdCgnLmZvcm0tZ3JvdXAnKTtcclxuICAgICAgICBpZiAoIWdyb3VwLmxlbmd0aCkge1xyXG4gICAgICAgICAgICBncm91cCA9ICQodGhpcykuY2xvc2VzdCgnZGl2Jyk7XHJcbiAgICAgICAgfVxyXG5cclxuICAgICAgICB2YXIgZXhwbGFuYXRpb24gPSBncm91cC5maW5kKCcuZm9ybS10ZXh0LnBhc3N3b3JkLWV4cGxhbmF0aW9uJyk7XHJcblxyXG4gICAgICAgIGlmICghZXhwbGFuYXRpb24ubGVuZ3RoKSB7XHJcbiAgICAgICAgICAgIGV4cGxhbmF0aW9uID0gJCgnPHNtYWxsIGNsYXNzPVwiZm9ybS10ZXh0IHBhc3N3b3JkLWV4cGxhbmF0aW9uXCIgLz4nKTtcclxuXHJcbiAgICAgICAgICAgIHZhciBsYWJlbCA9IGdyb3VwLmZpbmQoJ2xhYmVsJyk7XHJcbiAgICAgICAgICAgIGlmIChsYWJlbC5sZW5ndGgpIHtcclxuICAgICAgICAgICAgICAgIGxhYmVsLmFmdGVyKGV4cGxhbmF0aW9uKTtcclxuICAgICAgICAgICAgfSBlbHNlIHtcclxuICAgICAgICAgICAgICAgICQodGhpcykuYWZ0ZXIoZXhwbGFuYXRpb24pO1xyXG4gICAgICAgICAgICB9XHJcblxyXG4gICAgICAgICAgICBleHBsYW5hdGlvbiA9IGdyb3VwLmZpbmQoJy5mb3JtLXRleHQucGFzc3dvcmQtZXhwbGFuYXRpb24nKTtcclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIGlmIChyZXN1bHQuZmVlZGJhY2sud2FybmluZykge1xyXG4gICAgICAgICAgICBleHBsYW5hdGlvbi50ZXh0KHJlc3VsdC5mZWVkYmFjay53YXJuaW5nKS5zaG93KCk7XHJcbiAgICAgICAgfSBlbHNlIHtcclxuICAgICAgICAgICAgZXhwbGFuYXRpb24uaGlkZSgpO1xyXG4gICAgICAgIH1cclxuXHJcbiAgICAgICAgZ3JvdXAucmVtb3ZlQ2xhc3MoJ2hhcy1lcnJvciBoYXMtc3VjY2VzcyBoYXMtd2FybmluZycpO1xyXG5cclxuICAgICAgICBzd2l0Y2ggKHNjb3JlKSB7XHJcbiAgICAgICAgICAgIGNhc2UgMDpcclxuICAgICAgICAgICAgY2FzZSAxOlxyXG4gICAgICAgICAgICAgICAgZ3JvdXAuYWRkQ2xhc3MoJ2hhcy1lcnJvcicpO1xyXG4gICAgICAgICAgICAgICAgYnJlYWs7XHJcblxyXG4gICAgICAgICAgICBjYXNlIDI6XHJcbiAgICAgICAgICAgIGNhc2UgMzpcclxuICAgICAgICAgICAgICAgIGdyb3VwLmFkZENsYXNzKCdoYXMtd2FybmluZycpO1xyXG4gICAgICAgICAgICAgICAgYnJlYWs7XHJcblxyXG4gICAgICAgICAgICBjYXNlIDQ6XHJcbiAgICAgICAgICAgICAgICBncm91cC5hZGRDbGFzcygnaGFzLXN1Y2Nlc3MnKTtcclxuICAgICAgICAgICAgICAgIGJyZWFrO1xyXG4gICAgICAgIH1cclxuXHJcbiAgICB9KTtcclxuXHJcbn0pO1xyXG4iXX0=

View File

@ -1,2 +0,0 @@
$(document).ready(function(){$("input[type=password].strength").on("keyup",function(s){var a=zxcvbn($(this).val()),e=a.score,r=$(this).closest(".form-group");r.length||(r=$(this).closest("div"));var t=r.find(".form-text.password-explanation");if(!t.length){t=$('<small class="form-text password-explanation" />');var n=r.find("label");n.length?n.after(t):$(this).after(t),t=r.find(".form-text.password-explanation")}switch(a.feedback.warning?t.text(a.feedback.warning).show():t.hide(),r.removeClass("has-error has-success has-warning"),e){case 0:case 1:r.addClass("has-error");break;case 2:case 3:r.addClass("has-warning");break;case 4:r.addClass("has-success")}})});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInp4Y3Zibi5qcyJdLCJuYW1lcyI6WyIkIiwiZG9jdW1lbnQiLCJyZWFkeSIsIm9uIiwiZSIsInJlc3VsdCIsInp4Y3ZibiIsInRoaXMiLCJ2YWwiLCJzY29yZSIsImdyb3VwIiwiY2xvc2VzdCIsImxlbmd0aCIsImV4cGxhbmF0aW9uIiwiZmluZCIsImxhYmVsIiwiYWZ0ZXIiLCJmZWVkYmFjayIsIndhcm5pbmciLCJ0ZXh0Iiwic2hvdyIsImhpZGUiLCJyZW1vdmVDbGFzcyIsImFkZENsYXNzIl0sIm1hcHBpbmdzIjoiQUFBQUEsRUFBRUMsVUFBVUMsTUFBTSxXQUVkRixFQUFFLGlDQUFpQ0csR0FBRyxRQUFTLFNBQVNDLEdBRXBELElBQUlDLEVBQVNDLE9BQU9OLEVBQUVPLE1BQU1DLE9BQ3hCQyxFQUFRSixFQUFPSSxNQUVmQyxFQUFRVixFQUFFTyxNQUFNSSxRQUFRLGVBQ3ZCRCxFQUFNRSxTQUNQRixFQUFRVixFQUFFTyxNQUFNSSxRQUFRLFFBRzVCLElBQUlFLEVBQWNILEVBQU1JLEtBQUssbUNBRTdCLElBQUtELEVBQVlELE9BQVEsQ0FDckJDLEVBQWNiLEVBQUUsb0RBRWhCLElBQUllLEVBQVFMLEVBQU1JLEtBQUssU0FDbkJDLEVBQU1ILE9BQ05HLEVBQU1DLE1BQU1ILEdBRVpiLEVBQUVPLE1BQU1TLE1BQU1ILEdBR2xCQSxFQUFjSCxFQUFNSSxLQUFLLG1DQVc3QixPQVJJVCxFQUFPWSxTQUFTQyxRQUNoQkwsRUFBWU0sS0FBS2QsRUFBT1ksU0FBU0MsU0FBU0UsT0FFMUNQLEVBQVlRLE9BR2hCWCxFQUFNWSxZQUFZLHFDQUVWYixHQUNKLEtBQUssRUFDTCxLQUFLLEVBQ0RDLEVBQU1hLFNBQVMsYUFDZixNQUVKLEtBQUssRUFDTCxLQUFLLEVBQ0RiLEVBQU1hLFNBQVMsZUFDZixNQUVKLEtBQUssRUFDRGIsRUFBTWEsU0FBUyIsImZpbGUiOiJ6eGN2Ym4uanMiLCJzb3VyY2VzQ29udGVudCI6WyIkKGRvY3VtZW50KS5yZWFkeShmdW5jdGlvbigpIHtcblxuICAgICQoJ2lucHV0W3R5cGU9cGFzc3dvcmRdLnN0cmVuZ3RoJykub24oJ2tleXVwJywgZnVuY3Rpb24oZSkge1xuXG4gICAgICAgIHZhciByZXN1bHQgPSB6eGN2Ym4oJCh0aGlzKS52YWwoKSksXG4gICAgICAgICAgICBzY29yZSA9IHJlc3VsdC5zY29yZTtcblxuICAgICAgICB2YXIgZ3JvdXAgPSAkKHRoaXMpLmNsb3Nlc3QoJy5mb3JtLWdyb3VwJyk7XG4gICAgICAgIGlmICghZ3JvdXAubGVuZ3RoKSB7XG4gICAgICAgICAgICBncm91cCA9ICQodGhpcykuY2xvc2VzdCgnZGl2Jyk7XG4gICAgICAgIH1cblxuICAgICAgICB2YXIgZXhwbGFuYXRpb24gPSBncm91cC5maW5kKCcuZm9ybS10ZXh0LnBhc3N3b3JkLWV4cGxhbmF0aW9uJyk7XG5cbiAgICAgICAgaWYgKCFleHBsYW5hdGlvbi5sZW5ndGgpIHtcbiAgICAgICAgICAgIGV4cGxhbmF0aW9uID0gJCgnPHNtYWxsIGNsYXNzPVwiZm9ybS10ZXh0IHBhc3N3b3JkLWV4cGxhbmF0aW9uXCIgLz4nKTtcblxuICAgICAgICAgICAgdmFyIGxhYmVsID0gZ3JvdXAuZmluZCgnbGFiZWwnKTtcbiAgICAgICAgICAgIGlmIChsYWJlbC5sZW5ndGgpIHtcbiAgICAgICAgICAgICAgICBsYWJlbC5hZnRlcihleHBsYW5hdGlvbik7XG4gICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgICQodGhpcykuYWZ0ZXIoZXhwbGFuYXRpb24pO1xuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICBleHBsYW5hdGlvbiA9IGdyb3VwLmZpbmQoJy5mb3JtLXRleHQucGFzc3dvcmQtZXhwbGFuYXRpb24nKTtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmIChyZXN1bHQuZmVlZGJhY2sud2FybmluZykge1xuICAgICAgICAgICAgZXhwbGFuYXRpb24udGV4dChyZXN1bHQuZmVlZGJhY2sud2FybmluZykuc2hvdygpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgZXhwbGFuYXRpb24uaGlkZSgpO1xuICAgICAgICB9XG5cbiAgICAgICAgZ3JvdXAucmVtb3ZlQ2xhc3MoJ2hhcy1lcnJvciBoYXMtc3VjY2VzcyBoYXMtd2FybmluZycpO1xuXG4gICAgICAgIHN3aXRjaCAoc2NvcmUpIHtcbiAgICAgICAgICAgIGNhc2UgMDpcbiAgICAgICAgICAgIGNhc2UgMTpcbiAgICAgICAgICAgICAgICBncm91cC5hZGRDbGFzcygnaGFzLWVycm9yJyk7XG4gICAgICAgICAgICAgICAgYnJlYWs7XG5cbiAgICAgICAgICAgIGNhc2UgMjpcbiAgICAgICAgICAgIGNhc2UgMzpcbiAgICAgICAgICAgICAgICBncm91cC5hZGRDbGFzcygnaGFzLXdhcm5pbmcnKTtcbiAgICAgICAgICAgICAgICBicmVhaztcblxuICAgICAgICAgICAgY2FzZSA0OlxuICAgICAgICAgICAgICAgIGdyb3VwLmFkZENsYXNzKCdoYXMtc3VjY2VzcycpO1xuICAgICAgICAgICAgICAgIGJyZWFrO1xuICAgICAgICB9XG5cbiAgICB9KTtcblxufSk7XG4iXX0=

View File

@ -4,17 +4,25 @@ function confirmDangerousAction(el) {
confirmTitle = $(el).data('confirm-title');
}
let dangerMode = true;
if ($(el).hasClass('btn-success') || $(el).hasClass('btn-outline-success')) {
dangerMode = false;
}
// jQuery trick to pull an item's text without inner HTML elements.
// https://stackoverflow.com/questions/8624592/how-to-get-only-direct-text-without-tags-with-jquery-in-html
let buttonText = $(el).clone().children().remove().end().text();
return swal({
title: confirmTitle,
type: 'warning',
buttons: [true, $(el).text()],
dangerMode: true
buttons: [true, buttonText],
dangerMode: dangerMode
});
}
$(function() {
$('a.btn-danger').on('click', function(e) {
$('a.btn-danger,a.btn[data-confirm-title]').on('click', function(e) {
e.preventDefault();
const linkUrl = $(this).attr('href');

View File

@ -6147,7 +6147,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@ -6166,7 +6166,7 @@
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"resolved": "http://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
}

View File

@ -1,3 +1,7 @@
small.badge {
font-size: 70%;
}
.badge {
@include border-radius($badge-border-radius);