installed plugin `AudioIgniter` version 1.7.3

This commit is contained in:
KawaiiPunk 2022-04-17 00:39:10 +00:00 committed by Gitium
parent dbc3ae04ae
commit 55ea6cf8dd
56 changed files with 13860 additions and 0 deletions

View File

@ -0,0 +1 @@
player/build

View File

@ -0,0 +1,106 @@
//
// Mixins
//
@mixin clearfix() {
&::after {
content: "";
display: table;
clear: both;
}
}
@keyframes ai-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@mixin spinner($color: #fff, $opacity: .35, $size: 40px) {
border: 6px solid rgba($color, $opacity);
border-top-color: rgba($color, $opacity*2.5);
border-radius: 100%;
height: $size;
width: $size;
animation: ai-spin .8s infinite linear;
}
@mixin btn-reset {
display: inline-block;
font-weight: normal;
margin: 0;
padding: 0;
line-height: normal;
border: 0;
appearance: none;
text-align: center;
box-shadow: none;
vertical-align: middle;
cursor: pointer;
white-space: nowrap;
user-select: none;
border-radius: 0;
min-width: 0;
max-width: 100%;
min-height: 0;
width: auto;
height: auto;
background-image: none;
background-color: transparent;
&::before,
&::after {
display: none;
}
}
@keyframes backgroundPosition {
0% {
background-position: -140px 0
}
100% {
background-position: 140px 0
}
}
@mixin animatedBackground($width: 140px, $height: 8px, $top: 0, $left: 0) {
content: '';
width: $width;
height: $height;
background: linear-gradient(to right, $control-color 8%, lighten($control-color, 6%) 18%, $control-color 33%);
background-size: 500px;
position: absolute;
top: $top;
left: $left;
opacity: 1;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: backgroundPosition;
animation-timing-function: linear;
}
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
@mixin dashicon($icon) {
content: $icon;
display: inline-block;
font: 400 20px/1 dashicons;
speak: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-decoration: none !important;
}

View File

@ -0,0 +1,155 @@
@charset "UTF-8";
@keyframes ai-spin { 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } }
@keyframes backgroundPosition { 0% { background-position: -140px 0; }
100% { background-position: 140px 0; } }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; }
.ai-row { margin-left: -15px; margin-right: -15px; box-sizing: border-box; }
.ai-row::after { content: ""; display: table; clear: both; }
[class^="ai-col"] { float: left; padding-left: 15px; padding-right: 15px; width: 50%; box-sizing: border-box; }
.ai-btn { display: inline-block; font-weight: normal; margin: 0; line-height: normal; border: 0; box-shadow: none; text-align: center; vertical-align: middle; cursor: pointer; white-space: nowrap; user-select: none; border-radius: 2px; width: auto; height: auto; background-image: none; padding: 11px 20px 11px; font-size: 12px; text-transform: uppercase; background-color: #1c4866; color: #ffffff; text-decoration: none; }
.ai-btn:hover, .ai-btn:focus { color: #ffffff; background-color: #173a52; }
.ai-btn-green { background-color: #14b552; }
.ai-btn-green:hover, .ai-btn-green:focus { color: #ffffff; background-color: #119e48; }
.ai-brand-module { background-color: #1c4866; padding: 15px; color: #ffffff; font-size: 12px; }
.ai-brand-module p { font-size: 12px; }
.ai-brand-module a:not(.ai-btn) { color: #ffcc00; text-decoration: none; }
.ai-brand-module-actions { text-align: right; }
.ai-brand-module-actions p { margin: 0; }
.ai-header { margin: 12px 0 -12px; height: 40px; }
.ai-header-actions { text-align: right; }
.ai-logo { display: inline-block; position: relative; top: -2px; }
.ai-logo img { height: 44px; }
.ai-note { font-style: italic; }
.ai-list-inline { margin: 0; padding: 0; list-style: none; }
.ai-list-inline li { display: inline-block; margin: 0; }
.ai-footer-links a::after { content: "\007c"; color: #ffffff; opacity: .5; margin: 0 7px; }
.ai-footer-links li:last-child a::after { display: none; }
.ai-module { border: 1px solid #eeeeee; margin-top: 12px; padding: 15px; }
.ai-module::after { content: ""; display: table; clear: both; }
.ai-container { margin-top: 12px; }
.ai-field-controls-wrap { padding: 15px; border: 1px solid #eeeeee; }
.ai-field-controls-wrap::after { content: ""; display: table; clear: both; }
.ai-field-controls { float: left; }
.ai-field-controls .button { margin-right: 5px; }
.ai-field-controls-visibility { float: right; padding-top: 4px; }
.ai-field-controls-visibility a { text-decoration: none; }
.ai-fields-expand-all { margin-right: 8px; padding-right: 6px; border-right: 1px solid #f1f1f1; }
.ai-fields-container { padding: 15px; border-left: 1px solid #eeeeee; border-right: 1px solid #eeeeee; }
.ai-field-repeatable { margin-bottom: 15px; border: 1px solid #d7d7d7; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.07); }
.ai-field-repeatable:last-child { margin-bottom: 0; }
.ai-field-repeatable:only-child .ai-remove-field { display: none; }
.ai-field-container { padding: 15px; background-color: #ffffff; }
.ai-field-container::after { content: ""; display: table; clear: both; }
.ai-field-head { padding: 8px 15px 5px; line-height: normal; background-color: #d7d7d7; background: linear-gradient(to bottom, #f1f1f1, #d7d7d7); border-bottom: 1px solid #cccccc; }
.ai-field-head::after { content: ""; display: table; clear: both; }
.ai-field-head .toggle-indicator { border-radius: 50%; }
.ai-fields-sortable .ai-field-head { cursor: move; }
.ai-field-sort-handle { position: relative; top: 1px; color: #0073aa; }
.ai-field-sort-handle .dashicons { font-size: 18px; }
.ai-field-title { font-weight: bold; font-size: 1.05em; margin-left: 8px; padding-top: 3px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; max-width: 80%; display: inline-block; }
.ai-field-toggle { float: right; }
.ai-field-cover { float: left; width: 100px; height: 100px; margin-right: 15px; background-color: #eeeeee; border: 1px solid #cccccc; }
.ai-field-split { float: left; width: calc(50% - 71px); margin-right: 15px; }
.ai-field-split:nth-child(2n + 1) { margin-right: 0; }
.ai-container .button .dashicons, .ai-module .button .dashicons { font-size: 1.2em; line-height: 1.7em; }
.ai-form-field-group { padding: 15px; border: 1px solid #f1f1f1; margin-bottom: 15px; }
.ai-form-field-group :last-child { margin-bottom: 0; }
.ai-form-field-group-title { margin-top: 0; }
.ai-form-field { margin-bottom: 15px; }
.ai-form-field label { display: inline-block; font-weight: bold; margin-bottom: 3px; }
.ai-form-field input[type="text"], .ai-form-field input[type="url"], .ai-form-field input[type="search"], .ai-form-field input[type="email"], .ai-form-field input[type="password"], .ai-form-field input[type="number"], .ai-form-field input[type="tel"], .ai-form-field input[type="date"], .ai-form-field textarea { width: 100%; }
.ai-form-field input[type="checkbox"], .ai-form-field input[type="radio"] { display: inline-block; position: relative; top: 1px; }
.ai-module-settings .ai-form-field input[type="text"], .ai-module-settings .ai-form-field input[type="url"], .ai-module-settings .ai-form-field input[type="search"], .ai-module-settings .ai-form-field input[type="email"], .ai-module-settings .ai-form-field input[type="password"], .ai-module-settings .ai-form-field input[type="number"], .ai-module-settings .ai-form-field input[type="tel"], .ai-module-settings .ai-form-field input[type="date"], .ai-module-settings .ai-form-field textarea, .ai-module-settings .ai-form-field select { width: 200px; max-width: 100%; display: block; }
.ai-form-field-addon { position: relative; }
.ai-form-field-addon input { padding-right: 80px; }
.ai-form-field-addon button { position: absolute; top: 0; right: -2px; }
.ai-field-help { margin: 5px 0 0; font-style: italic; color: #999; }
.ai-remove-field { float: right; }
.ai-field-upload-cover { display: block; position: relative; width: 100px; height: 100px; text-decoration: none; color: initial; overflow: hidden; }
.ai-field-upload-cover img { max-width: 100%; display: none; }
.ai-has-cover .ai-remove-cover { display: block; }
.ai-has-cover .ai-field-cover-placeholder { display: none; }
.ai-has-cover img { display: inline-block; }
.ai-field-cover-placeholder { text-align: center; font-style: normal; font-size: .9em; opacity: .8; padding-top: 28px; }
.ai-field-cover-placeholder::before { content: ""; display: inline-block; font: 400 20px/1 dashicons; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-decoration: none !important; display: block; }
.ai-track-loading .ai-field-cover-placeholder::before { content: "\f463"; animation: rotation 1.2s infinite linear; }
.ai-remove-cover { color: #ffffff; background-color: #ff0000; width: 16px; height: 16px; font-size: 12px; cursor: pointer; position: absolute; top: 0; right: 0; opacity: .9; transition: opacity 0.18s ease-in; display: none; text-align: center; }
.ai-remove-cover:hover { opacity: 1; }
.ai-remove-cover .dashicons { font-size: 16px; width: 100%; height: 100%; }
.ai-remove-all-fields .dashicons, .ai-remove-field .dashicons { color: #ff0000; }
.ai-add-field-batch .dashicons, .ai-add-field .dashicons { color: #0073aa; }
.ai-info-box { background: #fffce6; color: #948832; font-size: 12px; border: solid 1px #eeeac9; padding: 15px; margin: 0 0 15px 0; }
.ai-player-type-message { display: none; }
.ai-drop-placeholder { background-color: #f1f1f1; border: 2px dashed #cccccc; opacity: 0.5; margin-bottom: 15px; }
.ai-collapsed .ai-field-container { display: none; }
.ai-collapsed .toggle-indicator::before { content: "\f140" !important; }
.ai-module-shortcode .code { display: block; width: 100%; margin-top: 3px; padding: 10px 10px 8px; font-weight: bold; background: #f1f1f1; }
.ai-sync-soundcloud.button { display: none; }
.ai-sync-soundcloud.button::before { content: "\f463"; color: #d54e21; display: inline-block; font: 400 19px/1 dashicons; speak: none; position: relative; left: -1px; top: 4px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; vertical-align: top; }
.ai-track-loading .ai-sync-soundcloud.button::before { animation: rotation 1.2s infinite linear; }
.ai-soundcloud-track .ai-sync-soundcloud { display: inline-block; }
.ai-soundcloud-track .ai-upload { display: none; }
@media (max-width: 1100px) { .ai-field-controls, .ai-field-controls-visibility { margin: 0; float: none; width: 100%; }
.ai-field-controls { margin-bottom: 5px; }
.ai-field-split { float: none; width: 100%; }
.ai-field-cover { margin-bottom: 15px; }
.ai-footer { text-align: center; }
.ai-footer .ai-brand-module-actions { text-align: center; margin-top: 10px; }
.ai-footer [class^="ai-col"] { width: 100%; } }
@media (max-width: 782px) { .ai-container .button .dashicons, .ai-module .button .dashicons { line-height: 1.2em; }
.ai-form-field-addon .button { top: 2px; } }
@media (max-width: 600px) { .ai-field-controls .button { width: 100%; }
.ai-header { text-align: center; }
.ai-header .ai-brand-module-actions { margin-top: 10px; }
.ai-header .ai-btn { display: block; }
.ai-header [class^="ai-col"] { width: 100%; } }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,660 @@
@import 'mixins';
$brand-color: #1c4866 !default;
$brand-secondary-color: #ffcc00 !default;
$lighter-grey: #f1f1f1 !default;
$light-grey: #eeeeee !default;
$medium-grey: #d7d7d7 !default;
$dark-grey: #cccccc !default;
$white: #ffffff !default;
$blue: #0073aa !default;
$red: #ff0000 !default;
$green: #14b552 !default;
$info-box-bg-color: #fffce6 !default;
$info-box-border-color: #eeeac9 !default;
$info-box-text-color: #948832 !default;
$base-pad: 15px !default;
$transition-time: .18s !default;
$border-color: $lighter-grey !default;
.sr-only {
@include sr-only;
}
.ai-row {
@include clearfix;
margin-left: -15px;
margin-right: -15px;
box-sizing: border-box;
}
[class^="ai-col"] {
float: left;
padding-left: 15px;
padding-right: 15px;
width: 50%;
box-sizing: border-box;
}
.ai-btn {
display: inline-block;
font-weight: normal;
margin: 0;
line-height: normal;
border: 0;
box-shadow: none;
text-align: center;
vertical-align: middle;
cursor: pointer;
white-space: nowrap;
user-select: none;
border-radius: 2px;
width: auto;
height: auto;
background-image: none;
padding: 11px 20px 11px;
font-size: 12px;
text-transform: uppercase;
background-color: $brand-color;
color: $white;
text-decoration: none;
&:hover,
&:focus {
color: $white;
background-color: darken($brand-color, 5%);
}
}
.ai-btn-green {
background-color: $green;
&:hover,
&:focus {
color: $white;
background-color: darken($green, 5%);
}
}
//
// Brand Modules
//
.ai-brand-module {
background-color: $brand-color;
padding: $base-pad;
color: $white;
font-size: 12px;
p {
font-size: 12px;
}
a:not(.ai-btn) {
color: $brand-secondary-color;
text-decoration: none;
}
}
.ai-brand-module-actions {
text-align: right;
p {
margin: 0;
}
}
//
// Header
//
.ai-header {
margin: 12px 0 -12px;
height: 40px;
}
.ai-header-actions {
text-align: right;
}
.ai-logo {
display: inline-block;
position: relative;
top: -2px;
img {
height: 44px;
}
}
//
// Footer
//
.ai-note {
font-style: italic;
}
.ai-list-inline {
margin: 0;
padding: 0;
list-style: none;
li {
display: inline-block;
margin: 0;
}
}
.ai-footer-links {
a {
&::after {
content: "\007c";
color: $white;
opacity: .5;
margin: 0 7px;
}
}
li {
&:last-child {
a::after {
display: none;
}
}
}
}
//
// General
//
.ai-module {
@include clearfix;
border: 1px solid $light-grey;
margin-top: 12px;
padding: $base-pad;
}
.ai-container {
margin-top: 12px;
}
//
// Field Controls
//
.ai-field-controls-wrap {
@include clearfix;
padding: $base-pad;
border: 1px solid $light-grey;
}
.ai-field-controls {
float: left;
.button {
margin-right: 5px;
}
}
.ai-field-controls-visibility {
float: right;
padding-top: 4px;
}
.ai-field-controls-visibility {
a {
text-decoration: none;
}
}
.ai-fields-expand-all {
margin-right: 8px;
padding-right: 6px;
border-right: 1px solid $lighter-grey;
}
//
// Fields general structure
//
.ai-fields-container {
padding: $base-pad;
border-left: 1px solid $light-grey;
border-right: 1px solid $light-grey;
}
.ai-field-repeatable {
margin-bottom: $base-pad;
border: 1px solid $medium-grey;
box-shadow: 1px 1px 2px rgba(black, .07);
&:last-child {
margin-bottom: 0;
}
&:only-child {
.ai-remove-field {
display: none;
}
}
}
.ai-field-container {
@include clearfix;
padding: $base-pad;
background-color: $white;
}
.ai-field-head {
@include clearfix;
padding: 8px $base-pad 5px;
line-height: normal;
background-color: $medium-grey;
background: linear-gradient(to bottom, $lighter-grey, $medium-grey);
border-bottom: 1px solid $dark-grey;
.toggle-indicator {
border-radius: 50%;
}
.ai-fields-sortable & {
cursor: move;
}
}
.ai-field-sort-handle {
position: relative;
top: 1px;
color: $blue;
.dashicons {
font-size: 18px;
}
}
.ai-field-title {
font-weight: bold;
font-size: 1.05em;
margin-left: 8px;
padding-top: 3px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 80%;
display: inline-block;
}
.ai-field-toggle {
float: right;
}
.ai-field-cover {
float: left;
width: 100px;
height: 100px;
margin-right: $base-pad;
background-color: $light-grey;
border: 1px solid $dark-grey;
}
.ai-field-split {
float: left;
width: calc(50% - 71px);
margin-right: $base-pad;
&:nth-child(2n + 1) {
margin-right: 0;
}
}
//
// Form elements
//
.ai-container,
.ai-module {
.button {
.dashicons {
font-size: 1.2em;
line-height: 1.7em;
}
}
}
.ai-form-field-group {
padding: $base-pad;
border: 1px solid $border-color;
margin-bottom: $base-pad;
:last-child {
margin-bottom: 0;
}
&-title {
margin-top: 0;
}
}
.ai-form-field {
margin-bottom: $base-pad;
label {
display: inline-block;
font-weight: bold;
margin-bottom: 3px;
}
input[type="text"],
input[type="url"],
input[type="search"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="date"],
textarea {
width: 100%;
}
input[type="checkbox"],
input[type="radio"] {
display: inline-block;
position: relative;
top: 1px;
}
}
.ai-module-settings {
.ai-form-field {
input[type="text"],
input[type="url"],
input[type="search"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="date"],
textarea,
select {
width: 200px;
max-width: 100%;
display: block;
}
}
}
.ai-form-field-addon {
position: relative;
input {
padding-right: 80px;
}
button {
position: absolute;
top: 0;
right: -2px;
}
}
.ai-field-help {
margin: 5px 0 0;
font-style: italic;
color: #999;
}
.ai-remove-field {
float: right;
}
.ai-field-upload-cover {
display: block;
position: relative;
width: 100px;
height: 100px;
text-decoration: none;
color: initial;
overflow: hidden;
img {
max-width: 100%;
display: none;
}
}
.ai-has-cover {
.ai-remove-cover {
display: block;
}
.ai-field-cover-placeholder {
display: none;
}
img {
display: inline-block;
}
}
.ai-field-cover-placeholder {
text-align: center;
font-style: normal;
font-size: .9em;
opacity: .8;
padding-top: 28px;
&::before {
@include dashicon($icon: "\f128");
display: block;
}
.ai-track-loading & {
&::before {
content: "\f463";
animation: rotation 1.2s infinite linear;
}
}
}
.ai-remove-cover {
color: $white;
background-color: $red;
width: 16px;
height: 16px;
font-size: 12px;
cursor: pointer;
position: absolute;
top: 0;
right: 0;
opacity: .9;
transition: opacity $transition-time ease-in;
display: none;
text-align: center;
&:hover {
opacity: 1;
}
.dashicons {
font-size: 16px;
width: 100%;
height: 100%;
}
}
.ai-remove-all-fields,
.ai-remove-field {
.dashicons {
color: $red;
}
}
.ai-add-field-batch,
.ai-add-field {
.dashicons {
color: $blue;
}
}
.ai-info-box {
background: $info-box-bg-color;
color: $info-box-text-color;
font-size: 12px;
border: solid 1px $info-box-border-color;
padding: 15px;
margin: 0 0 15px 0;
}
.ai-player-type-message {
display: none;
}
//
// Sortable specific
//
.ai-drop-placeholder {
background-color: $lighter-grey;
border: 2px dashed $dark-grey;
opacity: 0.5;
margin-bottom: $base-pad;
}
//
// Collapsible
//
.ai-collapsed {
.ai-field-container {
display: none;
}
.toggle-indicator {
&::before {
content: "\f140" !important;
}
}
}
//
// Shortcode field
//
.ai-module-shortcode {
.code {
display: block;
width: 100%;
margin-top: 3px;
padding: 10px 10px 8px;
font-weight: bold;
background: $lighter-grey;
}
}
//
// Soundcloud module
//
.ai-sync-soundcloud.button { // Overcoming specificity
display: none;
&::before {
content: "\f463";
color: #d54e21;
display: inline-block;
font: 400 19px/1 dashicons;
speak: none;
position: relative;
left: -1px;
top: 4px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: top;
}
.ai-track-loading & {
&::before {
animation: rotation 1.2s infinite linear;
}
}
}
.ai-soundcloud-track {
.ai-sync-soundcloud {
display: inline-block;
}
.ai-upload {
display: none;
}
}
//
// Media queries
//
@media (max-width: 1100px) {
.ai-field-controls,
.ai-field-controls-visibility {
margin: 0;
float: none;
width: 100%;
}
.ai-field-controls {
margin-bottom: 5px;
}
.ai-field-split {
float: none;
width: 100%;
}
.ai-field-cover {
margin-bottom: $base-pad;
}
.ai-footer {
text-align: center;
.ai-brand-module-actions {
text-align: center;
margin-top: 10px;
}
[class^="ai-col"] {
width: 100%;
}
}
}
@media (max-width: 782px) {
.ai-container,
.ai-module {
.button {
.dashicons {
line-height: 1.2em;
}
}
}
.ai-form-field-addon {
.button {
top: 2px;
}
}
}
@media (max-width: 600px) {
.ai-field-controls {
.button {
width: 100%;
}
}
.ai-header {
text-align: center;
.ai-brand-module-actions {
margin-top: 10px;
}
.ai-btn {
display: block;
}
[class^="ai-col"] {
width: 100%;
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,529 @@
/* eslint-env browser, jquery */
/* eslint-disable prefer-arrow-callback, prefer-template, func-names, no-var, object-shorthand, no-alert */
/* global wp ai_scripts */
jQuery(function($) {
// Return early if ai_scripts are not available
if (!ai_scripts) {
// eslint-disable-line camelcase
return;
}
// eslint-disable-next-line vars-on-top
var AudioIgniter = (function() {
var el = {
$trackContainer: $(".ai-fields-container"),
trackFieldClassName: ".ai-field-repeatable",
$addTrackButtonTop: $(".ai-add-field-top"),
$addTrackButtonBottom: $(".ai-add-field-bottom"),
removeFieldButtonClassName: ".ai-remove-field",
$removeAllTracksButton: $(".ai-remove-all-fields"),
$batchUploadButton: $(".ai-add-field-batch"),
audioUploadButtonClassName: ".ai-upload",
coverUploadButtonClassName: ".ai-field-upload-cover",
coverRemoveClassName: ".ai-remove-cover",
fieldTitleClassName: ".ai-field-title",
trackTitleClassName: ".ai-track-title",
trackArtistClassName: ".ai-track-artist",
trackLyricsClassName: ".ai-track-lyrics",
trackUrlClassName: ".ai-track-url",
hasCoverClass: "ai-has-cover",
fieldHeadClassName: ".ai-field-head",
fieldCollapsedClass: "ai-collapsed",
$expandAllButton: $(".ai-fields-expand-all"),
$collapseAllButton: $(".ai-fields-collapse-all"),
$shortcodeInputField: $("#ai_shortcode"),
soundCloudTrackClass: "ai-soundcloud-track"
};
/**
* Generate a rfc4122 version 4 compliant UUID
* http://stackoverflow.com/a/2117523
*
* @returns {string} - UUID
*/
function uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(
c
) {
var r = (Math.random() * 16) | 0;
var v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Check if field is collapsed
*
* @param {Object} $field - jQuery object
* @returns {*|boolean}
*/
function isFieldCollapsed($field) {
return $field.hasClass(el.fieldCollapsedClass);
}
/**
* Collapse a field
*
* @param {Object} $field - jQuery object
*/
function collapseField($field) {
$field.addClass(el.fieldCollapsedClass);
}
/**
* Expand a field
*
* @param {Object} $field - jQuery object
*/
function expandField($field) {
$field.removeClass(el.fieldCollapsedClass);
}
/**
* Resets the cover image placeholder state
*
* @param {Object} $field - the remove button jQuery object
*/
function resetCoverImage($field) {
var $coverWrap = $field.find("." + el.hasCoverClass);
$coverWrap
.removeClass(el.hasCoverClass)
.find("img")
.attr("src", "")
.attr("alt", "");
$coverWrap
.parent()
.find("input")
.val("");
}
/**
* Resets a form field
* - Clears input values
* - Clears thumbnail
*
* @param {object} $field - the field's jQuery object
* @param {string} [hash] - UUID or random hash
*/
function resetField($field, hash) {
var fieldHash = $field.data("uid");
var newHash = hash || uuid();
$field.attr("data-uid", newHash);
$field
.find("input, textarea")
.not(":button")
.each(function() {
var $this = $(this);
$this.attr("id", $this.attr("id").replace(fieldHash, newHash));
$this.attr("name", $this.attr("name").replace(fieldHash, newHash));
$this.val("");
});
$field.find("label").each(function() {
var $this = $(this);
$this.attr("for", $this.attr("for").replace(fieldHash, newHash));
});
$field.find(el.fieldTitleClassName).text("");
$field.removeClass(el.soundCloudTrackClass);
expandField($field);
resetCoverImage($field);
}
/**
* Checks if a track field is clear of values
*
* @param {object} $field - Track field jQuery object
* @returns {boolean}
*/
function isTrackFieldEmpty($field) {
var isEmpty = true;
var $inputs = $field.find("input");
$inputs.each(function() {
if ($(this).val()) {
isEmpty = false;
}
});
return isEmpty;
}
/**
* Gets the first field from $trackContainer
* and appends it back after resetting it
*
* @param {string} [hash] - UUID or random hash
*
* return {Object} - jQuery object
*/
function getNewTrackField(hash) {
var newHash = hash || uuid();
var $clone = el.$trackContainer
.find(el.trackFieldClassName)
.first()
.clone()
.hide()
.fadeIn();
resetField($clone, newHash);
return $clone;
}
/**
* Removes an element (or many) from the DOM
* by fading it out first
*
* @param {Object} $el - jQuery object of the element(s) to be removed
* @param {Function} [callback] - Optional callback
*/
function removeElement($el, callback) {
$el.fadeOut("fast", function() {
$(this).remove();
if (callback && typeof callback === "function") {
callback();
}
});
}
/**
* Populates a track field
*
* @param {Object} $field - The field's jQuery object
* @param {Object} media - WP Media Manager media object
*/
function populateTrackField($field, media) {
var $urlInput = $field.find(el.trackUrlClassName);
var $titleInput = $field.find(el.trackTitleClassName);
var $artistInput = $field.find(el.trackArtistClassName);
var $fieldTitle = $field.find(el.fieldTitleClassName);
if (media.url) {
$urlInput.val(media.url);
}
if (media.title && $titleInput.val() === "") {
$titleInput.val(media.title);
$fieldTitle.text(media.title);
}
if (media.meta && media.meta.artist && $artistInput.val() === "") {
$artistInput.val(media.meta.artist);
}
}
/**
* Sets a cover image for the field
*
* @param $field - The field's jQuery object
* @param {Object} cover - Cover object
* @param {number} cover.id - Image ID
* @param {string} cover.url - Image URL
* @param {string} [cover.alt] - Image alt text
*/
function setTrackFieldCover($field, cover) {
var $coverField = $field.find(el.coverUploadButtonClassName);
if (!cover || !cover.url || !cover.id) {
return;
}
$coverField
.find("img")
.attr("src", cover.url)
.attr("alt", cover.alt || "");
$coverField
.addClass(el.hasCoverClass)
.siblings("input")
.val(cover.id);
}
/**
* Initializes the WordPress Media Manager
*
* @param {Object} opts - Options object
* @param {string} opts.handler - Handler identifier of the media frame,
* this allows multiple media manager frames with different functionalities
* @param {string} [opts.type] - Filter media manager by type (audio, image etc)
* @param {string} [opts.title=Select Media] - Title of the media manager frame
* @param {boolean} [opts.multiple=false] - Accept multiple selections
* @param {Function} [opts.onMediaSelect] - Do something after media selection
*/
function wpMediaInit(opts) {
if (!opts.handler) {
throw new Error("Missing `handler` option");
}
/* eslint-disable */
var multiple = opts.multiple || false;
var title = opts.title || "Select media";
var mediaManager = wp.media.frames[opts.handler];
/* eslint-enable */
if (mediaManager) {
mediaManager.open();
return;
}
mediaManager = wp.media({
title: title,
multiple: multiple,
library: {
type: opts.type
}
});
mediaManager.open();
mediaManager.on("select", function() {
var attachments;
var attachmentModels = mediaManager.state().get("selection");
if (multiple) {
attachments = attachmentModels.map(function(attachment) {
return attachment.toJSON();
});
} else {
attachments = attachmentModels.first().toJSON();
}
if (opts.onMediaSelect && typeof opts.onMediaSelect === "function") {
opts.onMediaSelect(attachments);
}
});
}
/**
* Collapsible bindings
*/
el.$trackContainer.on("click", el.fieldHeadClassName, function(e) {
var $this = $(this);
var $parentField = $this.parents(el.trackFieldClassName);
if (isFieldCollapsed($parentField)) {
expandField($parentField);
} else {
collapseField($parentField);
}
e.preventDefault();
});
el.$expandAllButton.on("click", function(e) {
expandField(el.$trackContainer.find(el.trackFieldClassName));
e.preventDefault();
});
el.$collapseAllButton.on("click", function(e) {
collapseField(el.$trackContainer.find(el.trackFieldClassName));
e.preventDefault();
});
/**
* Field control bindings
* (Add, remove buttons etc)
*/
/* Bind track title to title input value */
el.$trackContainer.on("keyup", el.trackTitleClassName, function() {
var $this = $(this);
var $fieldTitle = $this
.parents(el.trackFieldClassName)
.find(el.fieldTitleClassName);
$fieldTitle.text($this.val());
});
/* Add Track Top*/
el.$addTrackButtonTop.on("click", function() {
el.$trackContainer.prepend(getNewTrackField());
});
/* Add Track Bottom*/
el.$addTrackButtonBottom.on("click", function () {
el.$trackContainer.append(getNewTrackField());
});
/* Remove Track */
el.$trackContainer.on("click", el.removeFieldButtonClassName, function() {
var $this = $(this);
removeElement($this.parents(".ai-field-repeatable"));
});
/* Remove All Tracks */
el.$removeAllTracksButton.on("click", function() {
var $trackFields = el.$trackContainer.find(el.trackFieldClassName);
if (window.confirm(ai_scripts.messages.confirm_clear_tracks)) {
if ($trackFields.length > 1) {
removeElement($trackFields.slice(1));
resetField($trackFields);
} else {
resetField($trackFields);
}
}
});
/**
* Bind media uploaders
*/
/* Audio upload */
el.$trackContainer.on("click", el.audioUploadButtonClassName, function() {
var $this = $(this);
var $parentTrackField = $this.parents(el.trackFieldClassName);
wpMediaInit({
handler: "ai-audio",
title: ai_scripts.messages.media_title_upload,
type: "audio",
onMediaSelect: function(media) {
populateTrackField($parentTrackField, media);
}
});
});
/**
* Cover image upload
*
* Element `coverUploadButtonClassName` *must* have
* an `img` and `coverRemoveClassName` elements
* as children
*/
el.$trackContainer
.on("click", el.coverUploadButtonClassName, function(e) {
var $this = $(this);
wpMediaInit({
handler: "ai-cover",
title: ai_scripts.messages.media_title_upload_cover,
type: "image",
onMediaSelect: function(media) {
setTrackFieldCover($this.parents(el.trackFieldClassName), {
id: media.id,
url: media.sizes.thumbnail.url,
alt: media.alt
});
}
});
e.preventDefault();
})
/* Remove Image */
.on("click", el.coverRemoveClassName, function(e) {
var $this = $(this);
resetCoverImage($this.parents(el.trackFieldClassName));
e.stopPropagation();
e.preventDefault();
});
/**
* Hide / show options based on player type
*
* Different player types support different kind of options.
* E.g. "Simple Player" doesn't support tracklist height, etc.
*/
var $settingsWrap = $(".ai-module-settings");
var $typeSelect = $(".ai-form-select-player-type");
function getUnsupportedSettings($el) {
var settingsString = $el.data("no-support");
if (typeof settingsString !== "string") {
return [];
}
return settingsString
.replace(/\s/g, "") // remove all whitespace
.split(",")
.map(function(x) {
return "_audioigniter_" + x;
});
}
function filterUIBasedOnPlayerType($el) {
var type = $el.val();
// Reset styles
var $shortcodeMetaBox = $("#ai-meta-box-shortcode");
var $messageBox = $(".ai-player-type-message");
var info = $el.data("info");
$shortcodeMetaBox.show();
if (info) {
$messageBox.text(info).show();
} else {
$messageBox.text("").hide();
}
// Player specific controls
switch (type) {
case "global-footer":
$shortcodeMetaBox.hide();
break;
default:
return;
}
}
function filterSettings() {
var $formFields;
var $type = $typeSelect.find(":selected");
var unsupportedSettings = getUnsupportedSettings($type);
filterUIBasedOnPlayerType($type);
if (unsupportedSettings.length === 0) {
$formFields = $settingsWrap.find(".ai-form-field");
$formFields.show();
return;
}
$settingsWrap.find("input", "select", "textarea").each(function() {
var $this = $(this);
var $parent = $this.parents(".ai-form-field");
if (unsupportedSettings.indexOf($this.attr("name")) > -1) {
$parent.hide();
} else {
$parent.show();
}
});
}
filterSettings();
$typeSelect.on("change", filterSettings);
/**
* Shortcode select on click
*/
el.$shortcodeInputField.on("click", function() {
$(this).select();
});
/**
* Export public methods and variables
*/
return {
elements: el,
uuid: uuid,
collapseField: collapseField,
expandField: expandField,
isFieldCollapsed: isFieldCollapsed,
isTrackFieldEmpty: isTrackFieldEmpty,
resetField: resetField,
resetCoverImage: resetCoverImage,
getNewTrackField: getNewTrackField,
removeElement: removeElement,
populateTrackField: populateTrackField,
setTrackFieldCover: setTrackFieldCover,
wpMediaInit: wpMediaInit
};
})();
// Expose the AudioIgniter instance as a global
if (!window.AudioIgniter) {
window.AudioIgniter = AudioIgniter;
}
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,367 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class AudioIgniter_Sanitizer
*
* Provides static sanitization functions.
*
* @since 1.0.0
*/
class AudioIgniter_Sanitizer {
/**
* Sanitizes the player type.
*
* @version 1.4.0
* @since 1.4.0
*
* @uses AudioIgniter->get_player_types()
*
* @param string $value Player type to sanitize.
*
* @return string
*/
public static function player_type( $value ) {
$choices = AudioIgniter()->get_player_types();
if ( array_key_exists( $value, $choices ) ) {
return $value;
}
return 'full';
}
/**
* Sanitizes a playlist (repeatable tracks).
*
* @version 1.2.0
* @since 1.0.0
*
* @uses AudioIgniter_Sanitizer::playlist_track()
*
* @param array $post_tracks Input values to sanitize, as passed by the playlist metabox.
* @param int|null $post_id Optional. Post ID where the track belongs to.
*
* @return array
*/
public static function metabox_playlist( $post_tracks, $post_id = null ) {
if ( empty( $post_tracks ) || ! is_array( $post_tracks ) ) {
return array();
}
$tracks = array();
foreach ( $post_tracks as $uid => $track_data ) {
$track = self::playlist_track( $track_data, $post_id, $uid );
if ( false !== $track ) {
$tracks[] = $track;
}
}
return apply_filters( 'audioigniter_sanitize_playlist', $tracks, $post_tracks, $post_id );
}
/**
* Sanitizes a single playlist track.
*
* @since 1.0.0
*
* @uses AudioIgniter::get_default_track_values()
*
* @param array $track Input values to sanitize.
* @param int|null $post_id Optional. Post ID where the track belongs to.
* @param string $track_uid Optional. UID that identifies the track in the metabox list.
*
* @return array|false Array if at least one field is completed, false otherwise.
*/
public static function playlist_track( $track, $post_id = null, $track_uid = '' ) {
$track = wp_parse_args( $track, AudioIgniter::get_default_track_values() );
$sanitized_track = array();
$sanitized_track['cover_id'] = intval( $track['cover_id'] );
$sanitized_track['title'] = sanitize_text_field( $track['title'] );
$sanitized_track['artist'] = sanitize_text_field( $track['artist'] );
$sanitized_track['track_url'] = esc_url_raw( $track['track_url'] );
$sanitized_track['buy_link'] = esc_url_raw( $track['buy_link'] );
$sanitized_track['download_url'] = esc_url_raw( $track['download_url'] );
$sanitized_track = array_map( 'trim', $sanitized_track );
$tmp = array_filter( $sanitized_track );
if ( empty( $tmp ) ) {
$sanitized_track = false;
}
return apply_filters( 'audioigniter_sanitize_playlist_track', $sanitized_track, $track, $post_id, $track_uid );
}
/**
* Sanitizes a checkbox value.
*
* @since 1.0.0
*
* @param int|string|bool $input Input value to sanitize.
*
* @return int|string Returns 1 if $input evaluates to 1, an empty string otherwise.
*/
public static function checkbox( $input ) {
if ( 1 == $input ) { // WPCS: loose comparison ok.
return 1;
}
return '';
}
/**
* Sanitizes a checkbox value. Value is passed by reference.
*
* Useful when sanitizing form checkboxes. Since browsers don't send any data when a checkbox
* is not checked, checkbox() throws an error.
* checkbox_ref() however evaluates &$input as null so no errors are thrown.
*
* @since 1.0.0
*
* @param int|string|bool &$input Input value to sanitize.
*
* @return int|string Returns 1 if $input evaluates to 1, an empty string otherwise.
*/
public static function checkbox_ref( &$input ) {
if ( 1 == $input ) { // WPCS: loose comparison ok.
return 1;
}
return '';
}
/**
* Sanitizes integer input while differentiating zero from empty string.
*
* @since 1.0.0
*
* @param mixed $input Input value to sanitize.
*
* @return int|string Integer value (including zero), or an empty string otherwise.
*/
public static function intval_or_empty( $input ) {
if ( is_null( $input ) || false === $input || '' === $input ) {
return '';
}
if ( 0 == $input ) { // WPCS: loose comparison ok.
return 0;
}
return intval( $input );
}
/**
* Returns a sanitized hex color code.
*
* @since 1.0.0
*
* @param string $str The color string to be sanitized.
* @param bool $return_hash Whether to return the color code prepended by a hash.
* @param string $return_fail The value to return on failure.
*
* @return string A valid hex color code on success, an empty string on failure.
*/
public static function hex_color( $str, $return_hash = true, $return_fail = '' ) {
if ( false === $str || empty( $str ) || 'false' === $str ) {
return $return_fail;
}
// Allow keywords and predefined colors
if ( in_array( $str, array( 'transparent', 'initial', 'inherit', 'black', 'silver', 'gray', 'grey', 'white', 'maroon', 'red', 'purple', 'fuchsia', 'green', 'lime', 'olive', 'yellow', 'navy', 'blue', 'teal', 'aqua', 'orange', 'aliceblue', 'antiquewhite', 'aquamarine', 'azure', 'beige', 'bisque', 'blanchedalmond', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'limegreen', 'linen', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'oldlace', 'olivedrab', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'whitesmoke', 'yellowgreen', 'rebeccapurple' ), true ) ) {
return $str;
}
// Include the hash if not there.
// The regex below depends on in.
if ( substr( $str, 0, 1 ) !== '#' ) {
$str = '#' . $str;
}
preg_match( '/(#)([0-9a-fA-F]{6})/', $str, $matches );
if ( count( $matches ) === 3 ) {
if ( $return_hash ) {
return $matches[1] . $matches[2];
} else {
return $matches[2];
}
}
return $return_fail;
}
/**
* Sanitizes a CSS color.
*
* Tries to validate and sanitize values in these formats:
* - rgba()
* - 3 and 6 digit hex values, optionally prefixed with `#`
* - Predefined CSS named colors/keywords, such as 'transparent', 'initial', 'inherit', 'black', 'silver', etc.
*
* @since 1.7.1
*
* @param string $color The color value to sanitize
* @param bool $return_hash Whether to return hex color prefixed with a `#`
* @param string $return_fail Value to return when $color fails validation.
*
* @return string
*/
public static function rgba_color( $color, $return_hash = true, $return_fail = '' ) {
if ( false === $color || empty( $color ) || 'false' === $color ) {
return $return_fail;
}
// Allow keywords and predefined colors
if ( in_array( $color, array( 'transparent', 'initial', 'inherit', 'black', 'silver', 'gray', 'grey', 'white', 'maroon', 'red', 'purple', 'fuchsia', 'green', 'lime', 'olive', 'yellow', 'navy', 'blue', 'teal', 'aqua', 'orange', 'aliceblue', 'antiquewhite', 'aquamarine', 'azure', 'beige', 'bisque', 'blanchedalmond', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'limegreen', 'linen', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'oldlace', 'olivedrab', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'whitesmoke', 'yellowgreen', 'rebeccapurple' ), true ) ) {
return $color;
}
preg_match( '/rgba\(\s*(\d{1,3}\.?\d*\%?)\s*,\s*(\d{1,3}\.?\d*\%?)\s*,\s*(\d{1,3}\.?\d*\%?)\s*,\s*(\d{1}\.?\d*\%?)\s*\)/', $color, $rgba_matches );
if ( ! empty( $rgba_matches ) && 5 === count( $rgba_matches ) ) {
for ( $i = 1; $i < 4; $i++ ) {
if ( strpos( $rgba_matches[ $i ], '%' ) !== false ) {
$rgba_matches[ $i ] = self::float_0_100( $rgba_matches[ $i ] );
} else {
$rgba_matches[ $i ] = self::int_0_255( $rgba_matches[ $i ] );
}
}
$rgba_matches[4] = self::float_0_1( $rgba_matches[ $i ] );
return sprintf( 'rgba(%s, %s, %s, %s)', $rgba_matches[1], $rgba_matches[2], $rgba_matches[3], $rgba_matches[4] );
}
// Not a color function either. Let's see if it's a hex color.
// Include the hash if not there.
// The regex below depends on in.
if ( substr( $color, 0, 1 ) !== '#' ) {
$color = '#' . $color;
}
preg_match( '/(#)([0-9a-fA-F]{6})/', $color, $matches );
if ( 3 === count( $matches ) ) {
if ( $return_hash ) {
return $matches[1] . $matches[2];
} else {
return $matches[2];
}
}
return $return_fail;
}
/**
* Sanitizes a percentage value, 0% - 100%
*
* Accepts float values with or without the percentage sign `%`
* Returns a string suffixed with the percentage sign `%`.
*
* @since 1.7.1
*
* @param mixed $value
*
* @return string A percentage value, including the percentage sign.
*/
public static function float_0_100( $value ) {
$value = str_replace( '%', '', $value );
if ( floatval( $value ) > 100 ) {
$value = 100;
} elseif ( floatval( $value ) < 0 ) {
$value = 0;
}
return floatval( $value ) . '%';
}
/**
* Sanitizes a decimal CSS color value, 0 - 255.
*
* Accepts float values with or without the percentage sign `%`
* Returns a string suffixed with the percentage sign `%`.
*
* @since 1.7.1
*
* @param mixed $value
*
* @return int A number between 0-255.
*/
public static function int_0_255( $value ) {
if ( intval( $value ) > 255 ) {
$value = 255;
} elseif ( intval( $value ) < 0 ) {
$value = 0;
}
return intval( $value );
}
/**
* Sanitizes a CSS opacity value, 0 - 1.
*
* @since 1.7.1
*
* @param mixed $value
*
* @return float A number between 0-1.
*/
public static function float_0_1( $value ) {
if ( floatval( $value ) > 1 ) {
$value = 1;
} elseif ( floatval( $value ) < 0 ) {
$value = 0;
}
return floatval( $value );
}
/**
* Removes elements whose keys are not valid data-attribute names.
*
* @since 1.0.0
*
* @param array $array Input array to sanitize.
*
* @return array()
*/
public static function html_data_attributes_array( $array ) {
$keys = array_keys( $array );
$key_prefix = 'data-';
// Remove keys that are not data attributes.
foreach ( $keys as $key ) {
if ( substr( $key, 0, strlen( $key_prefix ) ) !== $key_prefix ) {
unset( $array[ $key ] );
}
}
return $array;
}
/**
* Returns false when value is empty or null.
* Only use with array_filter() or similar, as the naming can lead to confusion.
*
* @since 1.2.0
*
* @param mixed $value Array value to check whether empty or null.
*
* @return bool false if empty or null, true otherwise.
*/
public static function array_filter_empty_null( $value ) {
if ( '' === $value || is_null( $value ) ) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,8 @@
module.exports = {
name: 'audioigniter',
paths: {
src: {
styles: ['./assets/css/*.scss'],
},
},
};

View File

@ -0,0 +1,391 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: AudioIgniter\n"
"POT-Creation-Date: 2021-12-14 12:04+0200\n"
"PO-Revision-Date: 2016-08-29 19:22+0300\n"
"Last-Translator: Anastis Sourgoutsidis <anastis@cssigniter.com>\n"
"Language-Team: Anastis Sourgoutsidis <anastis@cssigniter.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
"X-Generator: Poedit 3.0.1\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-WPHeader: audioigniter.php\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;"
"esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;"
"_nx_noop:3c,1,2;__ngettext_noop:1,2\n"
"X-Poedit-Flags-xgettext: --add-comments=translators:\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPathExcluded-0: *.js\n"
#. translators: %s is the track's title.
#: audioigniter.php:213
#, php-format
msgid "Play %s"
msgstr ""
#. translators: %s is the track's title.
#: audioigniter.php:215
#, php-format
msgid "Pause %s"
msgstr ""
#: audioigniter.php:216
msgid "Previous track"
msgstr ""
#: audioigniter.php:217
msgid "Next track"
msgstr ""
#: audioigniter.php:218
msgid "Toggle track listing repeat"
msgstr ""
#: audioigniter.php:219
msgid "Toggle track repeat"
msgstr ""
#: audioigniter.php:220
msgid "Toggle track listing visibility"
msgstr ""
#: audioigniter.php:221
msgid "Buy this track"
msgstr ""
#: audioigniter.php:222
msgid "Download this track"
msgstr ""
#: audioigniter.php:223
msgid "Volume Up"
msgstr ""
#: audioigniter.php:224
msgid "Volume Down"
msgstr ""
#: audioigniter.php:225
msgid "Open track lyrics"
msgstr ""
#: audioigniter.php:226
msgid "Set playback rate"
msgstr ""
#: audioigniter.php:227
msgid "Skip forward"
msgstr ""
#: audioigniter.php:228
msgid "Skip backward"
msgstr ""
#: audioigniter.php:229
msgid "Shuffle"
msgstr ""
#: audioigniter.php:234
msgid ""
"Do you really want to remove all tracks? (This will not delete your audio "
"files)."
msgstr ""
#: audioigniter.php:235
msgid "Select or upload audio media"
msgstr ""
#: audioigniter.php:236
msgid "Select a cover image"
msgstr ""
#: audioigniter.php:273
msgctxt "post type general name"
msgid "Playlists"
msgstr ""
#: audioigniter.php:274 audioigniter.php:289
msgctxt "post type singular name"
msgid "Playlist"
msgstr ""
#: audioigniter.php:275
msgctxt "admin menu"
msgid "Playlists"
msgstr ""
#: audioigniter.php:276
msgctxt "add new on admin bar"
msgid "Playlist"
msgstr ""
#: audioigniter.php:277 audioigniter.php:278
msgid "Add New Playlist"
msgstr ""
#: audioigniter.php:279
msgid "Edit Playlist"
msgstr ""
#: audioigniter.php:280
msgid "New Playlist"
msgstr ""
#: audioigniter.php:281
msgid "View Playlist"
msgstr ""
#: audioigniter.php:282
msgid "Search Playlists"
msgstr ""
#: audioigniter.php:283
msgid "No playlists found"
msgstr ""
#: audioigniter.php:284
msgid "No playlists found in the trash"
msgstr ""
#: audioigniter.php:309 audioigniter.php:830
msgid "Tracks"
msgstr ""
#: audioigniter.php:310
msgid "Settings"
msgstr ""
#: audioigniter.php:311
msgid "Shortcode"
msgstr ""
#: audioigniter.php:370
msgid "AudioIgniter Logo"
msgstr ""
#: audioigniter.php:379
msgid "Upgrade to Pro"
msgstr ""
#: audioigniter.php:403
msgid "Support"
msgstr ""
#: audioigniter.php:407
msgid "Documentation"
msgstr ""
#: audioigniter.php:411
msgid "Rate this plugin"
msgstr ""
#. translators: %s is a URL.
#: audioigniter.php:434
#, php-format
msgid ""
"Thank you for creating with <a href=\"%s\" target=\"_blank\">AudioIgniter</a>"
msgstr ""
#: audioigniter.php:479
msgid "Toggle track visibility"
msgstr ""
#: audioigniter.php:490
msgid "Remove Cover Image"
msgstr ""
#: audioigniter.php:503
msgid "Upload Cover"
msgstr ""
#: audioigniter.php:521 audioigniter.php:528
msgid "Title"
msgstr ""
#: audioigniter.php:536 audioigniter.php:543
msgid "Artist"
msgstr ""
#: audioigniter.php:552 audioigniter.php:559
msgid "Buy link"
msgstr ""
#: audioigniter.php:572 audioigniter.php:581
msgid "Audio file or radio stream"
msgstr ""
#: audioigniter.php:585
msgid "Upload"
msgstr ""
#: audioigniter.php:596 audioigniter.php:603
msgid "Download URL"
msgstr ""
#: audioigniter.php:612
msgid "Remove Track"
msgstr ""
#: audioigniter.php:627
msgid "Add Track"
msgstr ""
#: audioigniter.php:634
msgid "Clear Playlist"
msgstr ""
#: audioigniter.php:640
msgid "Expand All"
msgstr ""
#: audioigniter.php:643
msgid "Collapse All"
msgstr ""
#: audioigniter.php:682
msgid "Player &amp; Track listing"
msgstr ""
#: audioigniter.php:687
msgid "Player Type"
msgstr ""
#: audioigniter.php:718
msgid "Show track listing by default"
msgstr ""
#: audioigniter.php:732
msgid "Show track listing visibility toggle button"
msgstr ""
#: audioigniter.php:746
msgid "Reverse track order"
msgstr ""
#: audioigniter.php:752
msgid "Starting volume"
msgstr ""
#: audioigniter.php:763
msgid "0-100"
msgstr ""
#: audioigniter.php:768
msgid "Enter a value between 0 and 100 in increments of 10"
msgstr ""
#: audioigniter.php:782
msgid "Limit track listing height"
msgstr ""
#: audioigniter.php:788 audioigniter.php:798
msgid "Track listing height"
msgstr ""
#: audioigniter.php:803
msgid "Set a number of pixels"
msgstr ""
#: audioigniter.php:809
msgid "Maximum player width"
msgstr ""
#: audioigniter.php:817
msgid "Automatic width"
msgstr ""
#: audioigniter.php:822
#, php-format
msgid ""
"Set a number of pixels, or leave empty to automatically cover 100% of the "
"available area (recommended)."
msgstr ""
#: audioigniter.php:842
msgid "Show track numbers in tracklist"
msgstr ""
#: audioigniter.php:856
msgid "Show track covers in tracklist"
msgstr ""
#: audioigniter.php:870
msgid "Show active track's cover"
msgstr ""
#: audioigniter.php:884
msgid "Show artist names"
msgstr ""
#: audioigniter.php:898
msgid "Show track extra buttons (buy link, download button etc)"
msgstr ""
#: audioigniter.php:912
msgid "Open buy links in new window"
msgstr ""
#: audioigniter.php:920
msgid "Track &amp; Track listing repeat"
msgstr ""
#: audioigniter.php:932
msgid "Repeat track listing enabled by default"
msgstr ""
#: audioigniter.php:946
msgid "Show track listing repeat toggle button"
msgstr ""
#: audioigniter.php:963
msgid "Show \"Powered by AudioIgniter\" link"
msgstr ""
#: audioigniter.php:967
msgid ""
"We've put a great deal of effort into building this plugin. If you feel like "
"it, let others know about it by enabling this option."
msgstr ""
#: audioigniter.php:987
msgid "Grab the shortcode"
msgstr ""
#: audioigniter.php:1021
msgid "Full Player"
msgstr ""
#: audioigniter.php:1026
msgid "Simple Player"
msgstr ""
#: audioigniter.php:1248
msgid "ID doesn't match a playlist"
msgstr ""
#. Plugin Name of the plugin/theme
msgid "AudioIgniter"
msgstr ""
#. Plugin URI of the plugin/theme
msgid "https://www.cssigniter.com/plugins/audioigniter/"
msgstr ""
#. Description of the plugin/theme
msgid ""
"AudioIgniter lets you create music playlists and embed them in your "
"WordPress posts, pages or custom post types and serve your audio content in "
"style!"
msgstr ""
#. Author of the plugin/theme
msgid "The CSSIgniter Team"
msgstr ""
#. Author URI of the plugin/theme
msgid "https://www.cssigniter.com"
msgstr ""

View File

@ -0,0 +1,7 @@
{
"presets": [
"es2015",
"react",
"stage-2"
]
}

View File

@ -0,0 +1,18 @@
root = true
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.php]
indent_style = tab
[*.js]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
indent_size = 4

View File

@ -0,0 +1,2 @@
build/
node_modules/

View File

@ -0,0 +1,38 @@
{
"extends": [
"airbnb",
"plugin:prettier/recommended",
"prettier/react"
],
"plugins": [
"import"
],
"globals": {
"aiStrings": true
},
"env": {
"browser": true
},
"rules": {
"arrow-body-style": 0,
"no-confusing-arrow": 0,
"global-require": 0,
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
"import/prefer-default-export": 0,
"react/jsx-filename-extension": 0,
"react/require-default-props": 0,
"react/forbid-prop-types": 0,
"react/default-props-match-prop-types": 0,
"react/prefer-stateless-function": 0,
"react/jsx-curly-spacing": [2, {
"when": "never",
"children": true
}],
"react/no-array-index-key": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/no-static-element-interactions": 0,
"react/destructuring-assignment": 0,
"react/button-has-type": 0,
"jsx-a11y/label-has-for": 0
}
}

View File

@ -0,0 +1 @@
v8.12.0

View File

@ -0,0 +1,13 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"printWidth": 80,
"proseWrap": "never",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AudioIgniter</title>
<style>
body {
padding-bottom: 120px;
}
</style>
<link href="style.css" rel="stylesheet"></head>
<body>
<div
class="audioigniter-root"
data-player-type="full"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="true"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-allow-playback-rate="true"
data-display-track-no="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-cycle-tracks="true"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-skip-amount="15"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="5"
data-timer-countdown="false"
data-shuffle="true"
data-shuffle-default="false"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="simple"
data-tracks-url="/dev-tracks.json"
data-display-credits="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-allow-track-loop="true"
data-volume="50"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="3"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="false"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="global-footer"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="false"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-skip-amount="15"
data-cycle-tracks="false"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="true"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
<script type="text/javascript" src="app.js"></script><script type="text/javascript" src="style.js"></script></body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(n){function r(e){if(t[e])return t[e].exports;var o=t[e]={i:e,l:!1,exports:{}};return n[e].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var t={};r.m=n,r.c=t,r.d=function(n,t,e){r.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:e})},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(t,"a",t),t},r.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},r.p="",r(r.s=28)}({28:function(n,r){}});

View File

@ -0,0 +1,64 @@
[
{
"title": "Sunrise",
"subtitle": "Thoribass",
"audio": "https://www.cssigniter.com/assets/audioigniter/sunrise.mp3",
"buyUrl": "https://www.cssigniter.com",
"downloadUrl": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/Thoribass-Sunrise.jpg",
"lyrics": "Here in my mind\nYou know you might find\nSomething that you\n\nYou thought you once knew\nBut now it's all gone\nAnd you know it's no fun\n\nYeah I know it's no fun\nOh I know it's no fun\nI'm free to be whatever I\nWhatever I choose\nAnd I'll sing the blues if I want\nI'm free to be whatever I\nWhatever I choose\nAnd I'll sing the blues if I want\nWhatever you do\nWhatever you say\nYeah I know it's alright"
},
{
"title": "Seriously long title lorem ipsum dolor sit amet, consectetur adipiscing elit. Non est enim.",
"subtitle": "The Fisherman",
"audio": "https://www.cssigniter.com/assets/audioigniter/sunrise.mp3",
"buyUrl": "https://www.cssigniter.com",
"downloadUrl": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/The-Fisherman-Another-Day.jpg"
},
{
"title": "Remix Safety Guide",
"subtitle": "Rocavaco",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/remix.mp3",
"buyUrl": "https://www.cssigniter.com",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/Rocavaco-Remix-Safety-Guide.jpg",
"lyrics": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. At eius hic illo natus vitae. Assumenda commodi eaque eos est eum excepturi fugiat provident, quidem saepe? Aut doloremque, unde? Delectus, dolorum."
},
{
"title": "Tomorrow",
"subtitle": "MegaEnx",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/tomorrow.mp3",
"buyUrl": "",
"downloadUrl": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3",
"cover": ""
},
{
"title": "Is It Because I'm Black (David August Reconstruction)",
"subtitle": "Syl Johnson (SoundCloud)",
"audio": "https://soundcloud.com/enterofficial/maceo-plex-b2b-richie-hawtin-enterweek-11-sake-bar-space-ibiza-september-10th-2015",
"buyUrl": "",
"cover": "https://www.cssigniter.com/preview/audioigniter/files/2016/08/artworks-000103551140-ez6k4x-t500x500.jpg"
},
{
"title": "Deep House Radio",
"subtitle": "",
"audio": "https://deephouseradio.radioca.st/stream/1/",
"buyUrl": "https://www.cssigniter.com",
"cover": "https://www.cssigniter.com/preview/audioigniter/files/2016/08/Rocavaco-Remix-Safety-Guide.jpg"
},
{
"title": "Flash of Light",
"subtitle": "Kxmode",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/flashlight.mp3",
"buyUrl": "https://www.cssigniter.com",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/Kxmode-Flash-of-Light.jpg",
"lyrics": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. At eius hic illo natus vitae. Assumenda commodi eaque eos est eum excepturi fugiat provident, quidem saepe? Aut doloremque, unde? Delectus, dolorum."
},
{
"title": "We Get Mental",
"subtitle": "BitBurner",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/mental.mp3",
"buyUrl": "",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/BitBurner-We-Get-Mental.jpg"
}
]

View File

@ -0,0 +1,62 @@
{
"name": "audioigniter",
"version": "1.6.1",
"description": "React audio player",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "rm -rf ./build && webpack",
"lint": "eslint ./src --ext .js --ext .jsx --cache || true",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"audio",
"audio player",
"react"
],
"author": "vmasto",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-react-hmre": "1.1.1",
"babel-preset-stage-1": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "^0.28.4",
"eslint": "3.19.0",
"eslint-config-airbnb": "15.0.2",
"eslint-config-airbnb-base": "^11.2.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "2.7.0",
"eslint-plugin-jsx-a11y": "5.1.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "7.1.0",
"extract-text-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^2.29.0",
"node-sass": "^4.5.3",
"postcss-loader": "^2.0.6",
"precss": "^2.0.0",
"prettier": "^1.16.4",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.2",
"webpack": "^3.2.0",
"webpack-dev-server": "^2.5.1",
"webpack-merge": "^4.1.0"
},
"dependencies": {
"classnames": "2.3.0",
"es6-promise": "^4.1.1",
"prop-types": "^15.7.2",
"react": "^16.8.3",
"react-custom-scrollbars": "^4.1.2",
"react-dom": "^16.8.3",
"react-modal": "^3.8.1",
"react-sound": "^1.2.0",
"soundmanager2": "^2.97.20170602",
"sprintf-js": "1.1.1",
"whatwg-fetch": "0.11.1"
}
}

View File

@ -0,0 +1,64 @@
import React, { Fragment, useState, createContext } from 'react';
import PropTypes from 'prop-types';
import Player from './player/Player';
import SimplePlayer from './player/SimplePlayer';
import GlobalFooterPlayer from './player/GlobalFooterPlayer';
import TrackLyricsModal from './player/components/TrackLyricsModal';
export const AppContext = createContext();
const App = ({ type, ...props }) => {
const [modal, setModalState] = useState({
open: false,
track: null,
});
const toggleLyricsModal = (open, track) =>
setModalState(prevState => ({
...prevState,
track,
open,
}));
const { track, open } = modal;
const PlayerActual = (() => {
if (type === 'simple') {
return SimplePlayer;
}
if (type === 'global-footer') {
return GlobalFooterPlayer;
}
return Player;
})();
return (
<Fragment>
<AppContext.Provider
value={{
toggleLyricsModal,
}}
>
<PlayerActual {...props} />
</AppContext.Provider>
{track && track.lyrics && (
<TrackLyricsModal
isOpen={open}
closeModal={() => toggleLyricsModal(false)}
>
{track && track.lyrics}
</TrackLyricsModal>
)}
</Fragment>
);
};
App.propTypes = {
type: PropTypes.string,
};
export default App;

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title>
<style>
body {
padding-bottom: 120px;
}
</style>
</head>
<body>
<div
class="audioigniter-root"
data-player-type="full"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="true"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-allow-playback-rate="true"
data-display-track-no="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-cycle-tracks="true"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-skip-amount="15"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="5"
data-timer-countdown="false"
data-shuffle="true"
data-shuffle-default="false"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="simple"
data-tracks-url="/dev-tracks.json"
data-display-credits="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-allow-track-loop="true"
data-volume="50"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="3"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="false"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="global-footer"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="false"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-skip-amount="15"
data-cycle-tracks="false"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="true"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
</body>
</html>

View File

@ -0,0 +1,97 @@
import React from 'react';
import { render } from 'react-dom';
import 'es6-promise/auto';
import 'whatwg-fetch';
import App from './App';
// Set up translatable strings here
// for development purposes only. The production build
// gets them from WordPress's injection
if (process.env.NODE_ENV !== 'production') {
window.aiStrings = {
play_title: 'Play %s',
pause_title: 'Pause %s',
previous: 'Previous track',
next: 'Next track',
toggle_list_repeat: 'Toggle track listing repeat',
toggle_list_visible: 'Toggle track listing visibility',
toggle_track_repeat: 'Toggle track repeat',
buy_track: 'Buy this track',
download_track: 'Download this track',
volume_up: 'Volume Up',
volume_down: 'Volume Down',
open_track_lyrics: 'Open track lyrics',
set_playback_rate: 'Set playback rate',
skip_forward: 'Skip forward',
skip_backward: 'Skip backward',
shuffle: 'Shuffle',
};
}
const nodes = document.getElementsByClassName('audioigniter-root');
function renderApp(node) {
const type = node.getAttribute('data-player-type');
const props = {
tracksUrl: node.getAttribute('data-tracks-url'),
displayTracklistCovers: JSON.parse(
node.getAttribute('data-display-tracklist-covers'),
),
displayActiveCover: JSON.parse(
node.getAttribute('data-display-active-cover'),
),
displayCredits: JSON.parse(node.getAttribute('data-display-credits')),
displayTracklist: JSON.parse(node.getAttribute('data-display-tracklist')),
allowTracklistToggle: JSON.parse(
node.getAttribute('data-allow-tracklist-toggle'),
),
allowPlaybackRate: JSON.parse(
node.getAttribute('data-allow-playback-rate'),
),
allowTracklistLoop: JSON.parse(
node.getAttribute('data-allow-tracklist-loop'),
),
allowTrackLoop: JSON.parse(node.getAttribute('data-allow-track-loop')),
displayTrackNo: JSON.parse(node.getAttribute('data-display-track-no')),
displayBuyButtons: JSON.parse(
node.getAttribute('data-display-buy-buttons'),
),
buyButtonsTarget: JSON.parse(node.getAttribute('data-buy-buttons-target')),
volume: parseInt(node.getAttribute('data-volume'), 10),
displayArtistNames: JSON.parse(
node.getAttribute('data-display-artist-names'),
),
cycleTracks: JSON.parse(node.getAttribute('data-cycle-tracks')),
limitTracklistHeight: JSON.parse(
node.getAttribute('data-limit-tracklist-height'),
),
tracklistHeight: parseInt(node.getAttribute('data-tracklist-height'), 10),
reverseTrackOrder: JSON.parse(
node.getAttribute('data-reverse-track-order'),
),
maxWidth: node.getAttribute('data-max-width'),
soundcloudClientId: node.getAttribute('data-soundcloud-client-id'),
skipAmount: parseInt(node.getAttribute('data-skip-amount'), 10),
initialTrack: parseInt(node.getAttribute('data-initial-track'), 10),
delayBetweenTracks: parseInt(node.getAttribute('data-tracks-delay'), 10),
stopOnTrackFinish: JSON.parse(node.getAttribute('data-stop-on-finish')),
defaultShuffle: JSON.parse(node.getAttribute('data-shuffle')),
shuffleEnabled: JSON.parse(node.getAttribute('data-shuffle')),
countdownTimerByDefault: JSON.parse(
node.getAttribute('data-timer-countdown'),
),
};
render(<App type={type} {...props} />, node);
}
Array.prototype.slice.call(nodes).forEach(node => {
renderApp(node);
});
// eslint-disable-next-line no-underscore-dangle
window.__CI_AUDIOIGNITER_MANUAL_INIT__ = node => {
renderApp(node);
};

View File

@ -0,0 +1,357 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import { sprintf } from 'sprintf-js';
import classNames from 'classnames';
import soundProvider from './soundProvider';
import Cover from './components/Cover';
import Button from './components/Button';
import ProgressBar from './components/ProgressBar';
import Time from './components/Time';
import VolumeControl from './components/VolumeControl';
import TracklistWrap from './components/TracklistWrap';
import {
PlayIcon,
PauseIcon,
NextIcon,
PreviousIcon,
PlaylistIcon,
RefreshIcon,
LyricsIcon,
} from './components/Icons';
import { AppContext } from '../App';
import typographyDisabled from '../utils/typography-disabled';
class GlobalFooterPlayer extends React.Component {
constructor(props) {
super(props);
this.state = {
isTrackListOpen: this.props.displayTracklist,
};
this.toggleTracklist = this.toggleTracklist.bind(this);
}
toggleTracklist() {
this.setState(state => ({
isTrackListOpen: !state.isTrackListOpen,
}));
}
render() {
const { isTrackListOpen } = this.state;
const {
tracks,
playStatus,
activeIndex,
volume,
position,
duration,
playbackRate,
currentTrack,
playTrack,
togglePlay,
nextTrack,
prevTrack,
setPosition,
setVolume,
toggleTracklistCycling,
cycleTracks,
setTrackCycling,
setPlaybackRate,
allowPlaybackRate,
allowTracklistToggle,
allowTracklistLoop,
allowTrackLoop,
reverseTrackOrder,
displayTrackNo,
displayTracklistCovers,
displayActiveCover,
limitTracklistHeight,
tracklistHeight,
displayBuyButtons,
buyButtonsTarget,
displayArtistNames,
repeatingTrackIndex,
skipAmount,
skipPosition,
countdownTimerByDefault,
buffering,
} = this.props;
const classes = classNames({
'ai-wrap': true,
'ai-type-global-footer': true,
'ai-is-loading': !tracks.length,
'ai-with-typography': !typographyDisabled(),
});
const audioControlClasses = classNames({
'ai-audio-control': true,
'ai-audio-playing': playStatus === Sound.status.PLAYING,
'ai-audio-loading': buffering,
});
return (
<div
ref={ref => (this.root = ref)} // eslint-disable-line no-return-assign
className={classes}
>
<div className="ai-control-wrap">
{displayActiveCover && (
<Cover
className="ai-thumb ai-control-wrap-thumb"
src={currentTrack.cover}
alt={currentTrack.title}
/>
)}
<div className="ai-control-wrap-controls">
<ProgressBar
setPosition={setPosition}
duration={duration}
position={position}
/>
<div className="ai-audio-controls-main">
<Button
onClick={togglePlay}
className={audioControlClasses}
ariaLabel={
playStatus === Sound.status.PLAYING
? sprintf(aiStrings.pause_title, currentTrack.title)
: sprintf(aiStrings.play_title, currentTrack.title)
}
ariaPressed={playStatus === Sound.status.PLAYING}
>
{playStatus === Sound.status.PLAYING ? (
<PauseIcon />
) : (
<PlayIcon />
)}
<span className="ai-control-spinner" />
</Button>
<div className="ai-audio-controls-meta">
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-prev"
onClick={prevTrack}
ariaLabel={aiStrings.previous}
>
<PreviousIcon />
</Button>
)}
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-next"
onClick={nextTrack}
ariaLabel={aiStrings.next}
>
<NextIcon />
</Button>
)}
<VolumeControl
volume={volume}
// eslint-disable-next-line no-shadow
setVolume={setVolume}
/>
{allowTracklistLoop && (
<Button
className={`ai-btn ai-btn-repeat ${cycleTracks &&
'ai-btn-active'}`}
onClick={toggleTracklistCycling}
ariaLabel={aiStrings.toggle_list_repeat}
>
<RefreshIcon />
</Button>
)}
{allowPlaybackRate && (
<Button
className="ai-btn ai-btn-playback-rate"
onClick={setPlaybackRate}
ariaLabel={aiStrings.set_playback_rate}
>
<Fragment>&times;{playbackRate}</Fragment>
</Button>
)}
{skipAmount > 0 && (
<Fragment>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(-1)}
ariaLabel={aiStrings.skip_backward}
>
-{skipAmount}s
</Button>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(1)}
ariaLabel={aiStrings.skip_forward}
>
+{skipAmount}s
</Button>
</Fragment>
)}
{currentTrack && currentTrack.lyrics && !isTrackListOpen && (
<AppContext.Consumer>
{({ toggleLyricsModal }) => (
<Button
className="ai-btn ai-lyrics"
onClick={() => toggleLyricsModal(true, currentTrack)}
ariaLabel={aiStrings.open_track_lyrics}
title={aiStrings.open_track_lyrics}
>
<LyricsIcon />
</Button>
)}
</AppContext.Consumer>
)}
</div>
<div className="ai-track-info">
<p className="ai-track-title">
<span>{currentTrack.title}</span>
</p>
{(tracks.length === 0 || currentTrack.subtitle) &&
displayArtistNames && (
<p className="ai-track-subtitle">
<span>{currentTrack.subtitle}</span>
</p>
)}
</div>
<div className="ai-audio-controls-meta-right">
<Time
duration={duration}
position={position}
countdown={countdownTimerByDefault}
/>
{allowTracklistToggle && (
<Button
className="ai-btn ai-tracklist-toggle"
onClick={this.toggleTracklist}
ariaLabel={aiStrings.toggle_list_visible}
>
<PlaylistIcon />
</Button>
)}
</div>
</div>
</div>
</div>
<div
className={`ai-tracklist-wrap ${
isTrackListOpen ? 'ai-tracklist-open' : ''
}`}
style={{ display: isTrackListOpen ? 'block' : 'none' }}
>
<TracklistWrap
className="ai-tracklist"
trackClassName="ai-track"
tracks={tracks}
activeTrackIndex={activeIndex}
isOpen={isTrackListOpen}
displayTrackNo={displayTrackNo}
displayCovers={displayTracklistCovers}
displayBuyButtons={displayBuyButtons}
buyButtonsTarget={buyButtonsTarget}
displayArtistNames={displayArtistNames}
reverseTrackOrder={reverseTrackOrder}
limitTracklistHeight={limitTracklistHeight}
tracklistHeight={tracklistHeight}
onTrackClick={playTrack}
onTrackLoop={allowTrackLoop ? setTrackCycling : undefined}
repeatingTrackIndex={repeatingTrackIndex}
/>
</div>
</div>
);
}
}
GlobalFooterPlayer.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeIndex: PropTypes.number,
volume: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
currentTrack: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
playTrack: PropTypes.func.isRequired,
togglePlay: PropTypes.func.isRequired,
nextTrack: PropTypes.func.isRequired,
prevTrack: PropTypes.func.isRequired,
setPosition: PropTypes.func.isRequired,
setVolume: PropTypes.func.isRequired,
toggleTracklistCycling: PropTypes.func.isRequired,
cycleTracks: PropTypes.bool.isRequired,
displayTracklist: PropTypes.bool,
allowTracklistToggle: PropTypes.bool,
allowTracklistLoop: PropTypes.bool,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
displayActiveCover: PropTypes.bool,
displayTracklistCovers: PropTypes.bool,
limitTracklistHeight: PropTypes.bool,
tracklistHeight: PropTypes.number,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
setTrackCycling: PropTypes.func.isRequired,
repeatingTrackIndex: PropTypes.number,
allowTrackLoop: PropTypes.bool,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
skipAmount: PropTypes.number,
skipPosition: PropTypes.func.isRequired,
countdownTimerByDefault: PropTypes.bool,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default soundProvider(GlobalFooterPlayer, {
onFinishedPlaying(props) {
const {
repeatingTrackIndex,
cycleTracks,
nextTrack,
activeIndex,
playTrack,
trackQueue,
} = props;
if (repeatingTrackIndex != null) {
playTrack(repeatingTrackIndex);
return;
}
if (cycleTracks) {
nextTrack();
return;
}
// Check if not the last track
if (activeIndex !== trackQueue[trackQueue.length - 1]) {
nextTrack();
}
},
});

View File

@ -0,0 +1,403 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import { sprintf } from 'sprintf-js';
import classNames from 'classnames';
import TracklistWrap from './components/TracklistWrap';
import ProgressBar from './components/ProgressBar';
import Time from './components/Time';
import VolumeControl from './components/VolumeControl';
import Button from './components/Button';
import Cover from './components/Cover';
import {
PlayIcon,
PauseIcon,
NextIcon,
PreviousIcon,
PlaylistIcon,
RefreshIcon,
LyricsIcon,
ShuffleIcon,
} from './components/Icons';
import soundProvider from './soundProvider';
import { AppContext } from '../App';
import typographyDisabled from '../utils/typography-disabled';
class Player extends React.Component {
constructor(props) {
super(props);
this.state = {
isTrackListOpen: this.props.displayTracklist,
};
this.toggleTracklist = this.toggleTracklist.bind(this);
this.isNarrowContext = this.isNarrowContext.bind(this);
}
isNarrowContext() {
return this.root && this.root.offsetWidth < 480 && window.innerWidth > 480;
}
toggleTracklist() {
this.setState(state => ({
isTrackListOpen: !state.isTrackListOpen,
}));
}
render() {
const { isTrackListOpen } = this.state;
const {
tracks,
playStatus,
activeIndex,
volume,
position,
duration,
playbackRate,
shuffle,
shuffleEnabled,
currentTrack,
playTrack,
togglePlay,
nextTrack,
prevTrack,
setPosition,
setVolume,
setPlaybackRate,
toggleTracklistCycling,
cycleTracks,
toggleShuffle,
allowTracklistToggle,
allowTracklistLoop,
allowPlaybackRate,
allowTrackLoop,
setTrackCycling,
reverseTrackOrder,
displayTrackNo,
displayTracklistCovers,
displayActiveCover,
displayCredits,
limitTracklistHeight,
tracklistHeight,
displayBuyButtons,
buyButtonsTarget,
displayArtistNames,
maxWidth,
repeatingTrackIndex,
skipAmount,
skipPosition,
countdownTimerByDefault,
buffering,
} = this.props;
const classes = classNames({
'ai-wrap': true,
'ai-type-full': true,
'ai-is-loading': !tracks.length,
'ai-narrow': this.isNarrowContext(),
'ai-with-typography': !typographyDisabled(),
});
const audioControlClasses = classNames({
'ai-audio-control': true,
'ai-audio-playing': playStatus === Sound.status.PLAYING,
'ai-audio-loading': buffering,
});
return (
<div
ref={ref => (this.root = ref)} // eslint-disable-line no-return-assign
className={classes}
style={{ maxWidth }}
>
<div className="ai-control-wrap">
{displayActiveCover && (
<Cover
className="ai-thumb ai-control-wrap-thumb"
src={currentTrack.cover}
alt={currentTrack.title}
/>
)}
<div className="ai-control-wrap-controls">
<div className="ai-audio-controls-main">
<Button
onClick={togglePlay}
className={audioControlClasses}
ariaLabel={
playStatus === Sound.status.PLAYING
? sprintf(aiStrings.pause_title, currentTrack.title)
: sprintf(aiStrings.play_title, currentTrack.title)
}
ariaPressed={playStatus === Sound.status.PLAYING}
>
{playStatus === Sound.status.PLAYING ? (
<PauseIcon />
) : (
<PlayIcon />
)}
<span className="ai-control-spinner" />
</Button>
<div className="ai-track-info">
<p className="ai-track-title">
<span>{currentTrack.title}</span>
</p>
{(tracks.length === 0 || currentTrack.subtitle) &&
displayArtistNames && (
<p className="ai-track-subtitle">
<span>{currentTrack.subtitle}</span>
</p>
)}
</div>
</div>
<div className="ai-audio-controls-progress">
<ProgressBar
setPosition={setPosition}
duration={duration}
position={position}
/>
<Time
duration={duration}
position={position}
countdown={countdownTimerByDefault}
/>
</div>
<div className="ai-audio-controls-meta">
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-prev"
onClick={prevTrack}
ariaLabel={aiStrings.previous}
title={aiStrings.previous}
>
<PreviousIcon />
</Button>
)}
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-next"
onClick={nextTrack}
ariaLabel={aiStrings.next}
title={aiStrings.next}
>
<NextIcon />
</Button>
)}
<VolumeControl
volume={volume}
// eslint-disable-next-line no-shadow
setVolume={setVolume}
/>
{allowTracklistLoop && (
<Button
className={`ai-btn ai-btn-repeat ${cycleTracks &&
'ai-btn-active'}`}
onClick={toggleTracklistCycling}
ariaLabel={aiStrings.toggle_list_repeat}
>
<RefreshIcon />
</Button>
)}
{shuffleEnabled && (
<Button
className={`ai-btn ai-btn-shuffle ${shuffle &&
'ai-btn-active'}`}
onClick={toggleShuffle}
ariaLabel={aiStrings.shuffle}
>
<ShuffleIcon />
</Button>
)}
{allowPlaybackRate && (
<Button
className="ai-btn ai-btn-playback-rate"
onClick={setPlaybackRate}
ariaLabel={aiStrings.set_playback_rate}
>
<Fragment>&times;{playbackRate}</Fragment>
</Button>
)}
{skipAmount > 0 && (
<Fragment>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(-1)}
ariaLabel={aiStrings.skip_backward}
>
-{skipAmount}s
</Button>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(1)}
ariaLabel={aiStrings.skip_forward}
>
+{skipAmount}s
</Button>
</Fragment>
)}
{currentTrack && currentTrack.lyrics && !isTrackListOpen && (
<AppContext.Consumer>
{({ toggleLyricsModal }) => (
<Button
className="ai-btn ai-lyrics"
onClick={() => toggleLyricsModal(true, currentTrack)}
ariaLabel={aiStrings.open_track_lyrics}
title={aiStrings.open_track_lyrics}
>
<LyricsIcon />
</Button>
)}
</AppContext.Consumer>
)}
{allowTracklistToggle && (
<Button
className="ai-btn ai-tracklist-toggle"
onClick={this.toggleTracklist}
ariaLabel={aiStrings.toggle_list_visible}
ariaExpanded={isTrackListOpen}
>
<PlaylistIcon />
</Button>
)}
</div>
</div>
</div>
<div
className={`ai-tracklist-wrap ${
isTrackListOpen ? 'ai-tracklist-open' : ''
}`}
>
<TracklistWrap
className="ai-tracklist"
trackClassName="ai-track"
tracks={tracks}
activeTrackIndex={activeIndex}
isOpen={isTrackListOpen}
displayTrackNo={displayTrackNo}
displayCovers={displayTracklistCovers}
displayBuyButtons={displayBuyButtons}
buyButtonsTarget={buyButtonsTarget}
displayArtistNames={displayArtistNames}
reverseTrackOrder={reverseTrackOrder}
limitTracklistHeight={limitTracklistHeight}
tracklistHeight={tracklistHeight}
onTrackClick={playTrack}
onTrackLoop={allowTrackLoop ? setTrackCycling : undefined}
repeatingTrackIndex={repeatingTrackIndex}
/>
</div>
{displayCredits && (
<div className="ai-footer">
<p>
Powered by{' '}
<a
href="https://www.cssigniter.com/plugins/audioigniter?utm_source=player&utm_medium=link&utm_content=audioigniter&utm_campaign=footer-link"
target="_blank"
rel="noopener noreferrer"
>
AudioIgniter
</a>
</p>
</div>
)}
</div>
);
}
}
Player.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeIndex: PropTypes.number,
volume: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
currentTrack: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
playTrack: PropTypes.func.isRequired,
togglePlay: PropTypes.func.isRequired,
nextTrack: PropTypes.func.isRequired,
prevTrack: PropTypes.func.isRequired,
setPosition: PropTypes.func.isRequired,
setVolume: PropTypes.func.isRequired,
toggleTracklistCycling: PropTypes.func.isRequired,
setTrackCycling: PropTypes.func.isRequired,
cycleTracks: PropTypes.bool.isRequired,
displayTracklist: PropTypes.bool,
allowTracklistToggle: PropTypes.bool,
allowTracklistLoop: PropTypes.bool,
allowTrackLoop: PropTypes.bool,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
displayCredits: PropTypes.bool,
displayActiveCover: PropTypes.bool,
displayTracklistCovers: PropTypes.bool,
limitTracklistHeight: PropTypes.bool,
tracklistHeight: PropTypes.number,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
maxWidth: PropTypes.string,
repeatingTrackIndex: PropTypes.number,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
skipAmount: PropTypes.number,
skipPosition: PropTypes.func.isRequired,
countdownTimerByDefault: PropTypes.bool,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
shuffleEnabled: PropTypes.bool,
shuffle: PropTypes.bool,
toggleShuffle: PropTypes.func.isRequired,
};
export default soundProvider(Player, {
onFinishedPlaying(props) {
const {
repeatingTrackIndex,
cycleTracks,
nextTrack,
activeIndex,
playTrack,
trackQueue,
} = props;
if (repeatingTrackIndex != null) {
playTrack(repeatingTrackIndex);
return;
}
if (cycleTracks) {
nextTrack();
return;
}
// Check if not the last track
if (activeIndex !== trackQueue[trackQueue.length - 1]) {
nextTrack();
}
},
});

View File

@ -0,0 +1,124 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import classNames from 'classnames';
import soundProvider from './soundProvider';
import Tracklist from './components/Tracklist';
import typographyDisabled from '../utils/typography-disabled';
const SimplePlayer = props => {
const { playStatus } = props;
const activeIndex =
playStatus === Sound.status.PLAYING || playStatus === Sound.status.PAUSED
? props.activeIndex
: undefined;
const classes = classNames({
'ai-wrap': true,
'ai-type-simple': true,
'ai-with-typography': !typographyDisabled(),
});
return (
<div className={classes} style={{ maxWidth: props.maxWidth }}>
<div className="ai-tracklist ai-tracklist-open">
<Tracklist
tracks={props.tracks}
playStatus={props.playStatus}
activeTrackIndex={activeIndex}
onTrackClick={props.togglePlay}
setPosition={props.setPosition}
duration={props.duration}
position={props.position}
playbackRate={props.playbackRate}
className="ai-tracklist"
trackClassName="ai-track"
reverseTrackOrder={props.reverseTrackOrder}
displayTrackNo={props.displayTrackNo}
displayBuyButtons={props.displayBuyButtons}
buyButtonsTarget={props.buyButtonsTarget}
displayArtistNames={props.displayArtistNames}
standaloneTracks
onTrackLoop={props.allowTrackLoop ? props.setTrackCycling : undefined}
repeatingTrackIndex={props.repeatingTrackIndex}
setPlaybackRate={props.setPlaybackRate}
allowPlaybackRate={props.allowPlaybackRate}
buffering={props.buffering}
/>
</div>
{props.displayCredits && (
<div className="ai-footer">
<p>
Powered by{' '}
<a
href="https://www.cssigniter.com/plugins/audioigniter?utm_source=player&utm_medium=link&utm_content=audioigniter&utm_campaign=footer-link"
target="_blank"
rel="noopener noreferrer"
>
AudioIgniter
</a>
</p>
</div>
)}
</div>
);
};
SimplePlayer.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeIndex: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
setPosition: PropTypes.func.isRequired,
togglePlay: PropTypes.func.isRequired,
setTrackCycling: PropTypes.func.isRequired,
allowTrackLoop: PropTypes.bool,
maxWidth: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
displayCredits: PropTypes.bool,
repeatingTrackIndex: PropTypes.number,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default soundProvider(SimplePlayer, {
onFinishedPlaying(props) {
const {
repeatingTrackIndex,
cycleTracks,
nextTrack,
activeIndex,
playTrack,
trackQueue,
} = props;
if (repeatingTrackIndex != null) {
playTrack(repeatingTrackIndex);
return;
}
if (cycleTracks) {
nextTrack();
return;
}
// Check if not the last track
if (activeIndex !== trackQueue[trackQueue.length - 1]) {
nextTrack();
}
},
});

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
const Button = ({
className,
onClick,
children,
ariaLabel,
ariaPressed,
ariaExpanded,
ariaControls,
}) => (
<button
className={className}
onClick={onClick}
aria-label={ariaLabel}
aria-pressed={ariaPressed}
aria-expanded={ariaExpanded}
aria-controls={ariaControls}
>
{children}
</button>
);
Button.propTypes = {
className: PropTypes.string,
onClick: PropTypes.func,
children: PropTypes.node,
ariaLabel: PropTypes.string,
ariaPressed: PropTypes.bool,
ariaExpanded: PropTypes.bool,
ariaControls: PropTypes.string,
};
export default Button;

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MusicNoteIcon } from './Icons';
const Cover = ({ className, title, src, onClick }) => (
<div
className={className + (src ? '' : ' ai-track-no-thumb')}
onClick={onClick}
>
{src ? <img src={src} alt={title || ''} /> : <MusicNoteIcon />}
</div>
);
Cover.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
src: PropTypes.string,
onClick: PropTypes.func,
};
export default Cover;

View File

@ -0,0 +1,105 @@
import React from 'react';
export const PlayIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 24">
<path d="M18 12c0 .712-.37 1.355-.99 1.72L3.159 23.625C2.757 23.889 2.382 24 2 24c-1.103 0-2-.897-2-2V2C0 .897.897 0 2 0c.385 0 .76.111 1.085.323l13.962 9.981c.583.34.953.983.953 1.695z" />
</svg>
);
};
export const PauseIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M9 2v20c0 1.103-.897 2-2 2H2c-1.103 0-2-.897-2-2V2C0 .897.897 0 2 0h5c1.103 0 2 .897 2 2zm13-2h-5c-1.103 0-2 .897-2 2v20c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V2c0-1.103-.897-2-2-2z" />
</svg>
);
};
export const NextIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 1.999v19.989c0 1.102-.897 1.999-2 1.999h-5c-1.103 0-2-.897-2-1.999v-6.837L3.16 23.612C1.597 24.635 0 23.472 0 21.988V1.999C0 .897.897 0 2 0c.384 0 .76.111 1.085.322L15 8.837V1.999C15 .897 15.897 0 17 0h5c1.103 0 2 .897 2 1.999z" />
</svg>
);
};
export const PreviousIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 2.014v19.987C24 23.103 23.103 24 22 24c-.385 0-.76-.111-1.085-.323L9 15.164v6.838c0 1.102-.897 1.999-2 1.999H2c-1.103 0-2-.897-2-1.999V2.015C0 .913.897.016 2 .016h5c1.103 0 2 .897 2 1.999v6.837L20.841.391C22.41-.636 24 .533 24 2.016z" />
</svg>
);
};
export const PlaylistIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M.871 5h10.758c.488 0 .871-.439.871-1s-.383-1-.871-1H.871C.383 3 0 3.439 0 4s.383 1 .871 1zM.871 10.25h10.758c.488 0 .871-.439.871-1s-.383-1-.871-1H.871c-.488 0-.871.439-.871 1s.383 1 .871 1zM23.595 3.129l-.002-.001c-.254-.156-.574-.17-.833-.036l-7.449 3.756c-.291.148-.472.442-.472.77v8.259c-.5-.234-1.055-.356-1.626-.356-1.841 0-3.339 1.229-3.339 2.74s1.498 2.74 3.339 2.74 3.338-1.229 3.338-2.74V8.15l5.736-2.893v8.116c-.5-.233-1.056-.355-1.627-.355-1.841 0-3.338 1.229-3.338 2.739s1.497 2.74 3.338 2.74 3.339-1.229 3.339-2.74V3.862c0-.3-.151-.574-.405-.733zM8.129 13.5H.871c-.488 0-.871.439-.871 1s.383 1 .871 1h7.258c.488 0 .871-.439.871-1s-.383-1-.871-1z" />
</svg>
);
};
export const VolumeUpIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 11v2c0 1.103-.897 2-2 2h-7v7c0 1.103-.897 2-2 2h-2c-1.103 0-2-.897-2-2v-7H2c-1.103 0-2-.897-2-2v-2c0-1.103.897-2 2-2h7V2c0-1.103.897-2 2-2h2c1.103 0 2 .897 2 2v7h7c1.103 0 2 .897 2 2z" />
</svg>
);
};
export const VolumeDownIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 24">
<path d="M24 11v2c0 1.103-.897 2-2 2H2c-1.103 0-2-.897-2-2v-2c0-1.103.897-2 2-2h20c1.103 0 2 .897 2 2z" />
</svg>
);
};
export const MusicNoteIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 24">
<path d="M18 2v16c0 1.654-1.794 3-4 3s-4-1.346-4-3 1.794-3 4-3V4.5L8 6.374V21c0 1.654-1.794 3-4 3s-4-1.346-4-3 1.794-3 4-3V5c0-.966.691-1.793 1.645-1.966L15.238.157c.204-.097.481-.157.763-.157 1.103 0 2 .897 2 2z" />
</svg>
);
};
export const CartIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8.707 15h9.898c1.042 0 1.985-.657 2.346-1.636l2.94-7.979c.072-.196.109-.402.109-.616 0-.976-.794-1.77-1.77-1.77H5.734l-.339-1.188C5.09.744 4.101-.001 2.991-.001H.5c-.276 0-.5.224-.5.5s.224.5.5.5h2.491c.666 0 1.259.447 1.442 1.088l3.505 12.267-2.379 2.379c-.361.36-.56.841-.56 1.356 0 1.054.857 1.91 1.91 1.91h15.59c.276 0 .5-.224.5-.5s-.224-.5-.5-.5H6.909c-.502 0-.91-.408-.91-.916 0-.243.095-.472.267-.644l2.44-2.44zM18 12h-7.5c-.276 0-.5-.224-.5-.5s.224-.5.5-.5H18c.276 0 .5.224.5.5s-.224.5-.5.5zm.5-2.5H10c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h8.5c.276 0 .5.224.5.5s-.224.5-.5.5zM9.5 6H20c.276 0 .5.224.5.5s-.224.5-.5.5H9.5c-.276 0-.5-.224-.5-.5s.224-.5.5-.5zM21 20c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2zM8 20c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2z" />
</svg>
);
};
export const RefreshIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 12c0 2.756-2.243 4.999-5 4.999-.004 0-.02.001-.047.001-.295 0-1.919-.082-3.953-1.398v.397c0 .553-.447 1-1 1s-1-.447-1-1v-2.5c0-.553.447-1 1-1h2.5c.553 0 1 .447 1 1 0 .403-.241.745-.584.903 1.193.589 2.011.604 2.055.597 1.683 0 3.028-1.345 3.028-3s-1.346-3-3-3c-2.151 0-4.213 1.832-6.396 3.772-2.338 2.078-4.756 4.227-7.604 4.227-2.757 0-5-2.243-5-4.999S2.242 7 4.999 7c.046-.002 1.777-.044 4 1.394V8c0-.553.447-1 1-1s1 .447 1 1v2.5c0 .553-.447 1-1 1h-2.5c-.553 0-1-.447-1-1 0-.403.241-.746.585-.904-1.186-.587-1.997-.6-2.056-.596C3.345 9 2 10.346 2 12s1.346 3 3 3c2.089 0 4.122-1.807 6.275-3.722C13.641 9.176 16.087 7.001 19 7.001c2.757 0 5 2.243 5 4.999z" />
</svg>
);
};
export const DownloadIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 15c0 2.757-2.243 5-5 5h-.183c-.177 0-.333-.092-.422-.23-.05-.078-.078-.17-.078-.269 0-.078.018-.153.05-.219.419-.882.632-1.819.632-2.782 0-3.584-2.916-6.5-6.5-6.5s-6.5 2.916-6.5 6.5c0 .923.196 1.823.583 2.676.074.087.119.2.119.324 0 .276-.224.5-.5.5-.005.001-.013 0-.02 0h-.183c-3.309 0-6-2.691-6-6 0-2.158 1.143-4.121 3.003-5.193C3.104 5.036 6.203 2 9.998 2c2.759 0 5.205 1.58 6.35 4.062.227-.042.439-.063.65-.063 2.206 0 4 1.794 4 4 0 .142-.008.283-.024.428 1.825.785 3.024 2.572 3.024 4.572zm-6 1.5c0 3.032-2.468 5.5-5.5 5.5S7 19.532 7 16.5 9.468 11 12.5 11s5.5 2.468 5.5 5.5zm-3.146.646c-.195-.195-.512-.195-.707 0l-1.146 1.146v-4.793c0-.276-.224-.5-.5-.5s-.5.224-.5.5v4.793l-1.146-1.146c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l2 2c.046.046.1.083.161.108.059.025.124.038.192.038.065 0 .129-.013.19-.038h.002c.002-.001.003-.003.005-.004.057-.024.111-.058.157-.105l2-2c.195-.195.195-.512 0-.707z" />
</svg>
);
};
export const LyricsIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 4.5C0 3.673.673 3 1.5 3h21c.827 0 1.5.673 1.5 1.5S23.327 6 22.5 6h-21C.673 6 0 5.327 0 4.5zM1.5 11h15c.827 0 1.5-.673 1.5-1.5S17.327 8 16.5 8h-15C.673 8 0 8.673 0 9.5S.673 11 1.5 11zm15 7h-15c-.827 0-1.5.673-1.5 1.5S.673 21 1.5 21h15c.827 0 1.5-.673 1.5-1.5s-.673-1.5-1.5-1.5zm6-5h-21c-.827 0-1.5.673-1.5 1.5S.673 16 1.5 16h21c.827 0 1.5-.673 1.5-1.5s-.673-1.5-1.5-1.5z" />
</svg>
);
};
export const ShuffleIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.927 16.827c.098.23.098.504-.004.743-.044.111-.119.223-.212.314l-2.876 2.833c-.184.182-.428.282-.688.282s-.506-.101-.69-.283c-.187-.183-.289-.428-.289-.689s.103-.506.29-.69l1.188-1.171h-.86c-1.881 0-3.649-.722-4.979-2.034l-2.14-2.107c-.187-.185-.289-.43-.289-.69 0-.176.062-.336.149-.484l-2.372 2.337c-1.329 1.312-3.098 2.034-4.979 2.034H.98c-.54 0-.979-.436-.979-.972s.438-.972.979-.972h4.196c1.36 0 2.639-.522 3.599-1.469l2.354-2.319c-.148.086-.308.146-.484.146-.26 0-.505-.1-.689-.282l-1.179-1.163c-.962-.947-2.24-1.469-3.601-1.469H.98c-.54 0-.979-.436-.979-.972s.438-.972.979-.972h4.196c1.88 0 3.648.722 4.979 2.033l1.179 1.163c.188.184.29.429.29.69 0 .177-.063.339-.152.487l3.333-3.284c1.33-1.312 3.099-2.034 4.979-2.034h.86l-1.188-1.171c-.188-.184-.29-.429-.29-.69s.103-.506.29-.69c.379-.375.998-.375 1.379.001l2.874 2.833c.096.094.168.202.217.323.098.231.098.505-.004.743-.044.111-.116.219-.21.312l-2.878 2.835c-.363.363-1.013.365-1.38-.001-.186-.182-.288-.428-.288-.689s.104-.506.29-.69l1.188-1.17h-.86c-1.36 0-2.639.521-3.601 1.469l-3.313 3.265c.374-.215.855-.181 1.174.134l2.139 2.108c.963.947 2.241 1.469 3.602 1.469h.86l-1.188-1.171c-.188-.184-.29-.429-.29-.69s.104-.506.29-.69c.379-.374.998-.375 1.379.001l2.877 2.834c.094.094.166.202.214.321z" />
</svg>
);
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ProgressBar extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
const { duration, setPosition } = this.props;
if (setPosition == null) {
return;
}
const offsetX =
event.pageX - event.currentTarget.getBoundingClientRect().left;
const posX = offsetX / event.currentTarget.offsetWidth;
setPosition(posX * duration);
}
render() {
const { position, duration } = this.props;
return (
<span onClick={this.handleClick} className="ai-track-progress-bar">
<span
className="ai-track-progress"
style={{ width: `${(position * 100) / duration}%` }}
/>
</span>
);
}
}
ProgressBar.propTypes = {
setPosition: PropTypes.func,
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
};

View File

@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class Time extends React.Component {
constructor(props) {
super(props);
const { countdown } = this.props;
this.state = {
showRemaining: countdown || false,
};
this.handleClick = this.handleClick.bind(this);
}
/**
* Pretty prints time remaining/elapsed
*
* @param {number} position - Track position in milliseconds
* @param {number} duration - Track duration in milliseconds
* @returns {string} - Time pretty formatted
*/
formatTime(position, duration) {
const { showRemaining } = this.state;
const positionInSeconds = showRemaining
? (duration - position) / 1000
: position / 1000;
const hours = Math.floor(positionInSeconds / 3600);
let min = Math.floor((positionInSeconds % 3600) / 60);
let sec = Math.floor(positionInSeconds % 60);
let time = '00:00';
min = min >= 10 ? min : `0${min}`;
sec = sec >= 10 ? sec : `0${sec}`;
if (!isNaN(sec)) {
if (hours) {
time = `${hours}:${min}:${sec}`;
} else {
time = `${min}:${sec}`;
}
}
return showRemaining ? `-${time}` : time;
}
handleClick() {
const { showRemaining } = this.state;
this.setState({ showRemaining: !showRemaining });
}
render() {
const { position, duration } = this.props;
return (
<span className="ai-track-time" onClick={this.handleClick}>
{this.formatTime(position, duration)}
</span>
);
}
}
Time.propTypes = {
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
countdown: PropTypes.bool.isRequired,
};

View File

@ -0,0 +1,153 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import { sprintf } from 'sprintf-js';
import classNames from 'classnames';
import TrackTitle from './TrackTitle';
import Cover from './Cover';
import TrackButtons from './TrackButtons';
import ProgressBar from './ProgressBar';
import { PlayIcon, PauseIcon } from './Icons';
import { AppContext } from '../../App';
const Track = ({
track,
index,
trackNo,
isActive,
playStatus,
duration,
position,
setPosition,
isStandalone,
buyButtonsTarget,
displayArtistNames,
displayCovers,
displayBuyButtons,
onTrackClick,
onTrackLoop,
className,
isLooping,
playbackRate,
setPlaybackRate,
allowPlaybackRate,
buffering,
}) => {
const { toggleLyricsModal } = useContext(AppContext);
const isPlaying = isActive && playStatus === Sound.status.PLAYING;
const hasProgressBar =
typeof position !== 'undefined' &&
typeof duration !== 'undefined' &&
isActive &&
isStandalone;
const classes = classNames({
[className]: !!className,
'ai-track-active': isActive,
'ai-track-loading': isActive && buffering,
});
return (
<li className={classes}>
{displayCovers && (
<Cover
className="ai-track-thumb"
src={track.cover}
alt={track.title}
onClick={() => onTrackClick(index)}
/>
)}
{isStandalone && (
<button
className={classNames({
'ai-track-btn ai-track-inline-play-btn': true,
'ai-is-loading': isActive && buffering,
})}
onClick={() => onTrackClick(index)}
aria-label={
isPlaying
? sprintf(aiStrings.pause_title, track.title)
: sprintf(aiStrings.play_title, track.title)
}
aria-pressed={isPlaying}
>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
<span className="ai-track-spinner" />
</button>
)}
<div className="ai-track-control" onClick={() => onTrackClick(index)}>
<TrackTitle
className="ai-track-name"
track={track}
trackNo={trackNo}
displayArtistNames={displayArtistNames}
/>
</div>
<TrackButtons
buyButtonsTarget={buyButtonsTarget}
buyUrl={track.buyUrl}
downloadUrl={track.downloadUrl}
downloadFilename={track.downloadFilename}
onTrackLoop={onTrackLoop && (() => onTrackLoop(index))}
isLooping={isLooping}
displayBuyButtons={displayBuyButtons}
onOpenTrackLyrics={
track.lyrics && (() => toggleLyricsModal(true, track))
}
playbackRate={playbackRate}
setPlaybackRate={setPlaybackRate}
allowPlaybackRate={allowPlaybackRate}
isPlaying={isPlaying}
/>
{hasProgressBar && (
<ProgressBar
setPosition={setPosition}
duration={duration}
position={position}
/>
)}
</li>
);
};
Track.propTypes = {
track: PropTypes.shape({
audio: PropTypes.string,
buyUrl: PropTypes.string,
cover: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
lyrics: PropTypes.string,
downloadUrl: PropTypes.string,
}),
index: PropTypes.number.isRequired,
trackNo: PropTypes.number,
isActive: PropTypes.bool,
position: PropTypes.number,
duration: PropTypes.number,
setPosition: PropTypes.func,
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
onTrackClick: PropTypes.func.isRequired,
onTrackLoop: PropTypes.func,
className: PropTypes.string.isRequired,
isStandalone: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
displayCovers: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
isLooping: PropTypes.bool,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default Track;

View File

@ -0,0 +1,131 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { CartIcon, DownloadIcon, LyricsIcon, RefreshIcon } from './Icons';
const TrackButtons = ({
buyButtonsTarget,
buyUrl,
downloadUrl,
downloadFilename,
onTrackLoop,
isLooping,
displayBuyButtons,
onOpenTrackLyrics,
setPlaybackRate,
playbackRate,
allowPlaybackRate,
isPlaying,
}) => {
if (
buyUrl == null &&
downloadUrl == null &&
!onTrackLoop &&
!onOpenTrackLyrics
) {
return null;
}
return (
<div className="ai-track-control-buttons">
{buyUrl && displayBuyButtons && (
<a
href={buyUrl}
className="ai-track-btn"
rel={buyButtonsTarget ? 'noopener noreferrer' : undefined}
target={buyButtonsTarget ? '_blank' : '_self'}
role="button"
aria-label={aiStrings.buy_track}
title={aiStrings.buy_track}
>
<CartIcon />
</a>
)}
{downloadUrl && downloadFilename && displayBuyButtons && (
<a
href={downloadUrl}
download={downloadFilename}
className="ai-track-btn"
role="button"
aria-label={aiStrings.download_track}
title={aiStrings.download_track}
>
<DownloadIcon />
</a>
)}
{onOpenTrackLyrics && (
// eslint-disable-next-line
<a
href="#"
className="ai-track-btn"
role="button"
aria-label={aiStrings.open_track_lyrics}
title={aiStrings.open_track_lyrics}
onClick={event => {
event.preventDefault();
onOpenTrackLyrics();
}}
>
<LyricsIcon />
</a>
)}
{allowPlaybackRate && isPlaying && (
<a
href="#"
className="ai-track-btn ai-btn-playback-rate"
role="button"
aria-label={aiStrings.set_playback_rate}
title={aiStrings.set_playback_rate}
onClick={event => {
event.preventDefault();
setPlaybackRate();
}}
>
<Fragment>&times;{playbackRate}</Fragment>
</a>
)}
{onTrackLoop && (
// eslint-disable-next-line
<a
href="#"
className="ai-track-btn ai-track-btn-repeat"
role="button"
aria-label={aiStrings.toggle_track_repeat}
title={aiStrings.toggle_track_repeat}
onClick={event => {
event.preventDefault();
onTrackLoop();
}}
>
<span
style={{
opacity: isLooping ? 1 : 0.3,
}}
>
<RefreshIcon />
</span>
</a>
)}
</div>
);
};
TrackButtons.propTypes = {
buyButtonsTarget: PropTypes.bool,
buyUrl: PropTypes.string,
downloadUrl: PropTypes.string,
downloadFilename: PropTypes.string,
onTrackLoop: PropTypes.func,
isLooping: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
onOpenTrackLyrics: PropTypes.func,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
isPlaying: PropTypes.bool,
};
export default TrackButtons;

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-modal';
if (document.querySelector('.audioigniter-root')) {
Modal.setAppElement('.audioigniter-root');
}
const TrackLyricsModal = ({ isOpen, closeModal, children }) => {
return (
<Modal
isOpen={isOpen}
closeModal={closeModal}
onRequestClose={closeModal}
overlayClassName="ai-modal-overlay"
className="ai-modal"
>
<div className="ai-modal-wrap">
<div className="ai-modal-header">
<button
className="ai-modal-dismiss"
type="button"
onClick={closeModal}
>
&times;
</button>
</div>
<div className="ai-modal-content">{children}</div>
</div>
</Modal>
);
};
const propTypes = {
isOpen: PropTypes.bool,
closeModal: PropTypes.func.isRequired,
children: PropTypes.any,
};
TrackLyricsModal.propTypes = propTypes;
export default TrackLyricsModal;

View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
const TrackTitle = ({
className,
style,
track,
trackNo,
displayArtistNames,
}) => {
let trackTitle = track.title;
if (displayArtistNames && track.subtitle) {
trackTitle = `${track.title} - ${track.subtitle}`;
}
if (trackNo != null) {
trackTitle = `${trackNo}. ${trackTitle}`;
}
return (
<span className={className} style={style}>
{trackTitle}
</span>
);
};
TrackTitle.propTypes = {
track: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
trackNo: PropTypes.number,
style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
className: PropTypes.string,
displayArtistNames: PropTypes.bool,
};
export default TrackTitle;

View File

@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import Track from './Track';
const Tracklist = ({ ...props }) => {
const { tracks } = props;
return (
<ul className={props.className} aria-expanded="true">
{tracks &&
tracks.map((track, index) => {
const trackNo = props.reverseTrackOrder
? tracks.length - index
: index + 1;
const isLooping = index === props.repeatingTrackIndex;
return (
<Track
key={index}
track={track}
index={index}
trackNo={props.displayTrackNo ? trackNo : undefined}
playStatus={props.playStatus}
isActive={props.activeTrackIndex === index}
buyButtonsTarget={props.buyButtonsTarget}
displayArtistNames={props.displayArtistNames}
displayBuyButtons={props.displayBuyButtons}
displayCovers={props.displayCovers}
onTrackClick={props.onTrackClick}
onTrackLoop={props.onTrackLoop}
setPosition={props.setPosition}
duration={props.duration}
position={props.position}
className={props.trackClassName}
isStandalone={props.standaloneTracks}
isLooping={isLooping}
playbackRate={props.playbackRate}
setPlaybackRate={props.setPlaybackRate}
allowPlaybackRate={props.allowPlaybackRate}
buffering={props.buffering}
/>
);
})}
</ul>
);
};
Tracklist.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeTrackIndex: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
setPosition: PropTypes.func,
standaloneTracks: PropTypes.bool,
onTrackClick: PropTypes.func.isRequired,
onTrackLoop: PropTypes.func,
className: PropTypes.string,
trackClassName: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayCovers: PropTypes.bool,
displayArtistNames: PropTypes.bool,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default Tracklist;

View File

@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Scrollbars } from 'react-custom-scrollbars';
import Tracklist from './Tracklist';
export default class TracklistWrap extends React.Component {
componentWillReceiveProps(nextProps) {
const { activeTrackIndex, limitTracklistHeight } = this.props;
if (
activeTrackIndex !== nextProps.activeTrackIndex &&
limitTracklistHeight
) {
this.scrollToTrack(nextProps.activeTrackIndex);
}
}
scrollToTrack(trackIndex) {
const { tracks } = this.props;
const trackHeight = this.scrollbarsRef.getScrollHeight() / tracks.length;
if (!this.isTrackVisible(trackIndex)) {
this.scrollbarsRef.scrollTop(trackHeight * trackIndex);
}
}
isTrackVisible(trackIndex) {
const { tracks } = this.props;
const trackHeight = this.scrollbarsRef.getScrollHeight() / tracks.length;
const trackPosition = trackHeight * trackIndex;
const scrollTop = this.scrollbarsRef.getScrollTop();
const scrollBottom = scrollTop + this.scrollbarsRef.getClientHeight();
return !(trackPosition < scrollTop || trackPosition > scrollBottom);
}
renderTracklist() {
return (
<Tracklist
tracks={this.props.tracks}
activeTrackIndex={this.props.activeTrackIndex}
onTrackClick={this.props.onTrackClick}
className={this.props.className}
trackClassName={this.props.trackClassName}
reverseTrackOrder={this.props.reverseTrackOrder}
displayTrackNo={this.props.displayTrackNo}
displayBuyButtons={this.props.displayBuyButtons}
buyButtonsTarget={this.props.buyButtonsTarget}
displayCovers={this.props.displayCovers}
displayArtistNames={this.props.displayArtistNames}
onTrackLoop={this.props.onTrackLoop}
repeatingTrackIndex={this.props.repeatingTrackIndex}
/>
);
}
render() {
const { isOpen, limitTracklistHeight, tracklistHeight } = this.props;
return (
<div id="tracklisting" style={{ display: isOpen ? 'block' : 'none' }}>
{limitTracklistHeight ? (
<Scrollbars
className="ai-scroll-wrap"
ref={ref => (this.scrollbarsRef = ref)} // eslint-disable-line no-return-assign
style={{ height: tracklistHeight }}
>
{this.renderTracklist()}
</Scrollbars>
) : (
this.renderTracklist()
)}
</div>
);
}
}
TracklistWrap.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
activeTrackIndex: PropTypes.number.isRequired,
onTrackClick: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
className: PropTypes.string,
trackClassName: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
limitTracklistHeight: PropTypes.bool,
tracklistHeight: PropTypes.number,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayCovers: PropTypes.bool,
displayArtistNames: PropTypes.bool,
onTrackLoop: PropTypes.func,
repeatingTrackIndex: PropTypes.number,
};

View File

@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from './Button';
import { VolumeUpIcon, VolumeDownIcon } from './Icons';
export default class VolumeControl extends React.Component {
renderVolumeBars() {
const { volume, setVolume } = this.props;
return Array(...Array(11)).map((bar, i) => (
<span
key={i} // eslint-disable-line react/no-array-index-key
className={`ai-volume-bar ${
i <= volume / 10 ? 'ai-volume-bar-active' : ''
}`}
onClick={() => setVolume(i * 10)}
/>
));
}
render() {
const { volume, setVolume } = this.props;
return (
<div className="ai-audio-volume-control">
<div className="ai-audio-volume-bars">{this.renderVolumeBars()}</div>
<div className="ai-audio-volume-control-btns">
<Button
className="ai-btn"
onClick={() => setVolume(volume >= 100 ? volume : volume + 10)}
aria-label={aiStrings.volume_up}
>
<VolumeUpIcon />
</Button>
<Button
className="ai-btn"
onClick={() => setVolume(volume <= 0 ? volume : volume - 10)}
aria-label={aiStrings.volume_down}
>
<VolumeDownIcon />
</Button>
</div>
</div>
);
}
}
VolumeControl.propTypes = {
volume: PropTypes.number.isRequired,
setVolume: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,82 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import WaveSurfer from 'wavesurfer.js';
const propTypes = {
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
audio: PropTypes.string,
setPosition: PropTypes.func.isRequired,
};
const WaveformProgressBar = ({ audio, position, duration, setPosition }) => {
const waveFormDomRef = useRef(null);
const wavesurfer = useRef(null);
useEffect(() => {
if (waveFormDomRef.current && audio) {
wavesurfer.current = WaveSurfer.create({
container: waveFormDomRef.current,
mediaControls: false,
height: 40,
barWidth: 2,
barGap: 2,
barRadius: 3,
responsive: true,
cursorWidth: 0,
backgroundColor: 'transparent',
progressColor: '#f70f5d',
waveColor: '#fff',
xhr: {
mode: 'no-cors',
},
});
wavesurfer.current.load(audio);
wavesurfer.current.on('ready', () => {
console.log('wavesurfer loaded');
});
}
return () => {
if (wavesurfer.current) {
wavesurfer.current.destroy();
}
};
}, [audio, waveFormDomRef.current]);
useEffect(() => {
// Sync wavesurfer with current playing position
const progress = position / duration;
if (wavesurfer.current && !Number.isNaN(progress)) {
wavesurfer.current.seekTo(progress || 0);
}
}, [position]);
const handleClick = event => {
if (setPosition == null) {
return;
}
const offsetX =
event.pageX - event.currentTarget.getBoundingClientRect().left;
const posX = offsetX / event.currentTarget.offsetWidth;
setPosition(posX * duration);
};
if (!audio) {
return null;
}
return (
<div className="ai-waveform-bar" onClick={handleClick}>
<div className="ai-waveform" ref={waveFormDomRef} />
<div className="ai-waveform-progress" />
</div>
);
};
WaveformProgressBar.propTypes = propTypes;
export default WaveformProgressBar;

View File

@ -0,0 +1,418 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import SoundCloud from '../utils/soundcloud';
import multiSoundDisabled from '../utils/multi-sound-disabled';
import { getInitialTrackQueueAndIndex } from '../utils/getInitialTrackIndex';
const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
const soundProvider = (Player, events) => {
class EnhancedPlayer extends React.Component {
constructor(props) {
super(props);
const {
volume,
cycleTracks,
defaultShuffle,
shuffleEnabled,
} = this.props;
this.state = {
tracks: [],
activeIndex: 0, // Determine active track by index
// trackQueue: List of track indexes that represents the order of the playlist
// i.e. [0, 1, 2, 3, 4] will play the 1st, 2nd, 3rd, etc track.
// [5, 4, 3, 2, 1] will play the tracks reversed.
// [4, 2, 0, ...] will play the 5th track first, 3rd second, then the 1st, etc.
trackQueue: [],
playStatus: Sound.status.STOPPED,
position: 0,
duration: 0,
playbackRate: 1,
volume: volume == null ? 100 : volume,
cycleTracks,
repeatingTrackIndex: null,
isMultiSoundDisabled: multiSoundDisabled(),
buffering: false,
shuffle: shuffleEnabled && defaultShuffle,
};
this.playTrack = this.playTrack.bind(this);
this.pauseTrack = this.pauseTrack.bind(this);
this.togglePlay = this.togglePlay.bind(this);
this.nextTrack = this.nextTrack.bind(this);
this.prevTrack = this.prevTrack.bind(this);
this.setPosition = this.setPosition.bind(this);
this.setVolume = this.setVolume.bind(this);
this.skipPosition = this.skipPosition.bind(this);
this.setPlaybackRate = this.setPlaybackRate.bind(this);
this.toggleTracklistCycling = this.toggleTracklistCycling.bind(this);
this.toggleShuffle = this.toggleShuffle.bind(this);
this.setTrackCycling = this.setTrackCycling.bind(this);
this.reverseTracks = this.reverseTracks.bind(this);
this.getFinalProps = this.getFinalProps.bind(this);
this.onPlaying = this.onPlaying.bind(this);
this.onFinishedPlaying = this.onFinishedPlaying.bind(this);
}
componentDidMount() {
const {
tracksUrl,
soundcloudClientId,
reverseTrackOrder,
initialTrack,
} = this.props;
const { shuffle } = this.state;
const tracksPromised = fetch(tracksUrl).then(res => res.json());
if (!soundcloudClientId) {
tracksPromised.then(tracks => {
const { trackQueue, activeIndex } = getInitialTrackQueueAndIndex({
tracks,
initialTrack,
reverseTrackOrder,
shuffle,
});
this.setState(
{
tracks,
activeIndex,
trackQueue,
},
() => {
if (reverseTrackOrder) {
this.reverseTracks();
}
},
);
});
return;
}
const sc = new SoundCloud(soundcloudClientId);
const scTracks = tracksPromised
.then(tracks => sc.fetchSoundCloudStreams(tracks))
.catch(err => console.error(err)); // eslint-disable-line no-console
// Make sure if SoundCloud fetching fails
// we delegate and load our tracks anyway
const promiseArray = [tracksPromised, scTracks].map(p =>
p.catch(error => ({
status: 'error',
error,
})),
);
Promise.all(promiseArray).then(res => {
if (res[1].status === 'error') {
return this.setState({ tracks: res[0] });
}
const tracks = sc.mapStreamsToTracks(...res);
const { trackQueue, activeIndex } = getInitialTrackQueueAndIndex({
tracks,
initialTrack,
reverseTrackOrder,
shuffle,
});
return this.setState(
() => ({
tracks,
activeIndex,
trackQueue,
}),
() => {
if (reverseTrackOrder) {
this.reverseTracks();
}
},
);
});
}
// Events
onPlaying({ duration, position }) {
this.setState(
() => ({ duration, position }),
() => {
if (events && events.onPlaying) {
events.onPlaying(this.getFinalProps());
}
},
);
}
onFinishedPlaying() {
const { stopOnTrackFinish, delayBetweenTracks = 0 } = this.props;
const delayBetweenTracksMs = delayBetweenTracks * 1000;
this.setState(() => ({ playStatus: Sound.status.STOPPED }));
if (stopOnTrackFinish) {
return;
}
if (events && events.onFinishedPlaying) {
setTimeout(() => {
events.onFinishedPlaying(this.getFinalProps());
}, delayBetweenTracksMs);
}
}
getFinalProps() {
const { tracks, activeIndex } = this.state;
const currentTrack = tracks[activeIndex] || {};
return {
playTrack: this.playTrack,
pauseTrack: this.pauseTrack,
togglePlay: this.togglePlay,
nextTrack: this.nextTrack,
prevTrack: this.prevTrack,
setPosition: this.setPosition,
skipPosition: this.skipPosition,
setPlaybackRate: this.setPlaybackRate,
setVolume: this.setVolume,
toggleTracklistCycling: this.toggleTracklistCycling,
setTrackCycling: this.setTrackCycling,
toggleShuffle: this.toggleShuffle,
currentTrack,
...this.props,
...this.state,
};
}
setVolume(volume) {
this.setState(() => ({ volume }));
}
setPosition(position) {
this.setState(() => ({ position }));
}
setTrackCycling(index, event) {
if (event) {
event.preventDefault();
}
const { activeIndex, cycleTracks } = this.state;
if (cycleTracks && index != null) {
this.toggleTracklistCycling();
}
this.setState(
({ repeatingTrackIndex }) => ({
repeatingTrackIndex: repeatingTrackIndex === index ? null : index,
}),
() => {
if (index != null && activeIndex !== index) {
this.playTrack(index);
}
},
);
}
setPlaybackRate() {
this.setState(({ playbackRate }) => {
const currentIndex = PLAYBACK_RATES.findIndex(
rate => rate === playbackRate,
);
const nextIndex =
(PLAYBACK_RATES.length + (currentIndex + 1)) % PLAYBACK_RATES.length;
return {
playbackRate: PLAYBACK_RATES[nextIndex],
};
});
}
toggleShuffle() {
const { initialTrack, reverseTrackOrder } = this.props;
const { tracks } = this.state;
this.setState(
prev => ({
shuffle: !prev.shuffle,
}),
() => {
this.setState(() => {
const { trackQueue } = getInitialTrackQueueAndIndex({
tracks,
initialTrack,
reverseTrackOrder,
shuffle: this.state.shuffle,
});
return {
trackQueue,
};
});
if (this.state.shuffle) {
// Shuffle track queue
} else {
// Unshuffle track queue
}
},
);
}
skipPosition(direction = 1) {
const { position } = this.state;
const { skipAmount } = this.props;
const amount = parseInt(skipAmount, 10) * 1000;
this.setPosition(position + amount * direction);
}
playTrack(index, event) {
if (event) {
event.preventDefault();
}
const { repeatingTrackIndex, isMultiSoundDisabled } = this.state;
if (isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
this.setState(() => ({
activeIndex: index,
position: 0,
playStatus: Sound.status.PLAYING,
}));
// Reset repating track index if the track is not the active one.
if (index !== repeatingTrackIndex && repeatingTrackIndex != null) {
this.setTrackCycling(null);
}
}
pauseTrack(event) {
if (event) {
event.preventDefault();
}
const { playStatus } = this.state;
if (playStatus === Sound.status.PLAYING) {
this.setState(() => ({ playStatus: Sound.status.PAUSED }));
}
}
togglePlay(index, event) {
if (event) {
event.preventDefault();
}
const { activeIndex } = this.state;
if (typeof index === 'number' && index !== activeIndex) {
this.playTrack(index);
return;
}
this.setState(({ playStatus, isMultiSoundDisabled }) => {
if (playStatus !== Sound.status.PLAYING && isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
return {
playStatus:
playStatus === Sound.status.PLAYING
? Sound.status.PAUSED
: Sound.status.PLAYING,
};
});
}
nextTrack() {
const { trackQueue, activeIndex } = this.state;
const currentQueueIndex = trackQueue.indexOf(activeIndex);
const nextQueueIndex = (currentQueueIndex + 1) % trackQueue.length;
const nextTrackIndex = trackQueue[nextQueueIndex];
this.playTrack(nextTrackIndex);
}
prevTrack() {
const { trackQueue, activeIndex } = this.state;
const currentQueueIndex = trackQueue.indexOf(activeIndex);
const prevQueueIndex =
(currentQueueIndex + trackQueue.length - 1) % trackQueue.length;
const prevTrackIndex = trackQueue[prevQueueIndex];
this.playTrack(prevTrackIndex);
}
toggleTracklistCycling() {
const { repeatingTrackIndex } = this.state;
if (repeatingTrackIndex !== null) {
this.setTrackCycling(null);
}
this.setState(state => ({
cycleTracks: !state.cycleTracks,
}));
}
reverseTracks() {
this.setState(state => ({
tracks: state.tracks.slice().reverse(),
}));
}
render() {
const { tracks, playStatus, position, volume, playbackRate } = this.state;
const finalProps = this.getFinalProps();
return (
<div className="ai-audioigniter">
<Player {...finalProps} />
{tracks.length > 0 && (
<Sound
url={finalProps.currentTrack.audio}
playStatus={playStatus}
position={position}
volume={volume}
onPlaying={this.onPlaying}
onFinishedPlaying={this.onFinishedPlaying}
onPause={() => this.pauseTrack()}
playbackRate={playbackRate}
onBufferChange={buffering => {
this.setState({ buffering });
}}
/>
)}
</div>
);
}
}
EnhancedPlayer.propTypes = {
volume: PropTypes.number,
cycleTracks: PropTypes.bool,
tracksUrl: PropTypes.string,
soundcloudClientId: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
skipAmount: PropTypes.number,
stopOnTrackFinish: PropTypes.bool,
delayBetweenTracks: PropTypes.number,
initialTrack: PropTypes.number,
shuffleEnabled: PropTypes.bool,
defaultShuffle: PropTypes.bool,
};
return EnhancedPlayer;
};
export default soundProvider;

View File

@ -0,0 +1,14 @@
/**
* Shifts an array to right / left by n positions.
*
* @param {Array} arr The array.
* @param {number} direction The direction - 0 for left 1 for right.
* @param {number} n Number of positions to shift by.
* @returns {any[]}
*/
const arrayShift = (arr, direction, n) => {
const times = n > arr.length ? n % arr.length : n;
return arr.concat(arr.splice(0, direction > 0 ? arr.length - times : times));
};
export default arrayShift;

View File

@ -0,0 +1,24 @@
/**
* Shuffles an array.
* Copied from https://github.com/sindresorhus/array-shuffle
*
* @param {Array} array The array to be shuffled.
* @returns {*[]|*}
*/
const arrayShuffle = array => {
if (!Array.isArray(array)) {
return array;
}
const clone = [...array];
// eslint-disable-next-line no-plusplus
for (let index = clone.length - 1; index > 0; index--) {
const newIndex = Math.floor(Math.random() * (index + 1));
[clone[index], clone[newIndex]] = [clone[newIndex], clone[index]];
}
return clone;
};
export default arrayShuffle;

View File

@ -0,0 +1,76 @@
import arrayShuffle from './array-shuffle';
import arrayShift from './array-shift';
/**
* Fetches the initial track index.
*
* @param {Object} options The options.
* @param {Array} options.tracks The tracks.
* @param {number} [options.initialTrack] The initial track index.
* @param {boolean} options.reverseTrackOrder Whether the track order is reversed.
* @returns {number}
*/
export const getInitialTrackIndex = ({
tracks = [],
initialTrack = 1,
reverseTrackOrder = false,
}) => {
// The user provides a 1-index value.
const initialTrackIndex = initialTrack - 1;
if (!tracks.length || !initialTrack || initialTrack > tracks.length) {
return 0;
}
if (reverseTrackOrder) {
return Math.max(tracks.length - initialTrack, 0);
}
return initialTrackIndex;
};
/**
* Fetches the initial track index and the initial track queue.
*
* @param {Object} options The options.
* @param {Array} options.tracks The tracks.
* @param {Number} options.initialTrack The initial track number (1-indexed).
* @param {Boolean} reverseTrackOrder Whether the track order is reversed.
* @param {Boolean} shuffle Whether the track queue is shuffled.
* @returns {{activeIndex: number, trackQueue: (*[]|*)}|{activeIndex: number, trackQueue: *}}
*/
export const getInitialTrackQueueAndIndex = ({
tracks = [],
initialTrack = 1,
reverseTrackOrder = false,
shuffle = false,
}) => {
const activeIndex = getInitialTrackIndex({
tracks,
initialTrack,
reverseTrackOrder,
});
const orderedTrackIndexes = tracks.map((_, index) => index);
if (!shuffle) {
const shiftAmount = orderedTrackIndexes.indexOf(activeIndex);
return {
activeIndex,
trackQueue: arrayShift(orderedTrackIndexes, 0, shiftAmount),
};
}
const shuffledQueue = arrayShuffle(orderedTrackIndexes);
// Always bring the initial track (activeIndex) to the front of the queue.
shuffledQueue.splice(shuffledQueue.indexOf(activeIndex), 1);
shuffledQueue.unshift(activeIndex);
return {
activeIndex,
trackQueue: shuffledQueue,
};
};
export default getInitialTrackIndex;

View File

@ -0,0 +1,8 @@
const multiSoundDisabled = () => {
return (
window.ai_pro_front_scripts &&
!!window.ai_pro_front_scripts.multi_sound_disabled
);
};
export default multiSoundDisabled;

View File

@ -0,0 +1,88 @@
export default class SoundCloud {
constructor(clientId) {
if (!clientId) {
throw new Error('SoundCloud client ID is required');
}
this.clientId = clientId;
this.baseUrl = 'https://api.soundcloud.com';
}
/**
* Checks if a URL is from SoundCloud
*
* @param {string} url - URL to be checked
*
* @returns {boolean}
*/
static isSoundCloudUrl(url) {
return url.indexOf('soundcloud.com') > -1;
}
/**
* Resolves a SoundCloud URL into a track object
*
* @param {string} url - URL to be resolved
*
* @returns {Promise.<*>}
*/
resolve(url) {
/*
* Tell the SoundCloud API not to serve a redirect. This is to get around
* CORS issues on Safari 7+, which likes to send pre-flight requests
* before following redirects, which has problems.
*
* https://github.com/soundcloud/soundcloud-javascript/issues/27
*/
const statusCodeMap = encodeURIComponent('_status_code_map[302]=200');
return fetch(
`${this.baseUrl}/resolve?url=${url}&client_id=${
this.clientId
}&${statusCodeMap}`,
)
.then(res => res.json())
.then(res => fetch(res.location))
.then(res => res.json());
}
/**
* Resolves and fetches SoundCloud track objects
*
* @param {Object[]} tracks - Tracks object
*
* @returns {Promise.<*>}
*/
fetchSoundCloudStreams(tracks) {
const scTracks = tracks
.filter(track => SoundCloud.isSoundCloudUrl(track.audio))
.map(track => this.resolve(track.audio));
return Promise.all(scTracks);
}
/**
* Maps a SoundCloud tracks object into an AudioIgniter one
* by replacing `track.audio` with `sctrack.stream_url`.
*
* Works *in order* of appearance in the `tracks` object.
*
* @param {Object[]} tracks - AudioIgniter tracks object
* @param {Object[]} scTracks - SoundCloud tracks object
*
* @returns {Object[]}
*/
mapStreamsToTracks(tracks, scTracks) {
let i = 0;
return tracks.map(track => {
if (SoundCloud.isSoundCloudUrl(track.audio)) {
// eslint-disable-next-line no-param-reassign
track.audio = `${scTracks[i].stream_url}?client_id=${this.clientId}`;
i++; // eslint-disable-line no-plusplus
}
return track;
});
}
}

View File

@ -0,0 +1,8 @@
const typographyDisabled = () => {
return (
window.ai_pro_front_scripts &&
!!window.ai_pro_front_scripts.typography_disabled
);
};
export default typographyDisabled;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const parts = require('./webpack.parts');
const TARGET = process.env.npm_lifecycle_event;
process.env.BABEL_ENV = TARGET;
const PATHS = {
app: path.join(__dirname, 'src'),
build: path.join(__dirname, 'build'),
style: path.join(__dirname, 'styles', 'style.scss'),
};
const common = {
entry: {
style: PATHS.style,
app: PATHS.app,
},
output: {
path: PATHS.build,
filename: '[name].js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'AudioIgniter',
template: `${PATHS.app}/index.ejs`,
}),
],
devServer: {
contentBase: path.resolve('assets'),
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.jsx?$/,
use: ['babel-loader?cacheDirectory'],
},
],
},
};
let config;
// Detect how npm is run and branch based on that
switch (TARGET) {
case 'build': {
config = merge(
common,
{
resolve: {
modules: [path.resolve(__dirname), 'node_modules'],
extensions: ['.js', '.jsx'],
},
},
parts.minify(),
parts.extractCSS(PATHS.style),
parts.setFreeVariable('process.env.NODE_ENV', 'production'),
);
break;
}
default: {
config = merge(
common,
{
devtool: 'eval-source-map',
},
parts.setupSass(PATHS.style),
parts.devServer({
host: process.env.HOST,
port: process.env.PORT,
}),
);
}
}
module.exports = config;

View File

@ -0,0 +1,107 @@
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoprefixer = require('autoprefixer');
exports.setupSass = paths => ({
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: paths,
},
],
},
});
exports.extractCSS = paths => ({
module: {
rules: [
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
minimize: true,
},
},
{
loader: 'postcss-loader',
options: {
plugins: () => [
autoprefixer({
browsers: [
'Chrome >= 46',
'Firefox ESR',
'Edge >= 12',
'Explorer >= 9',
'iOS >= 8',
'Safari >= 8',
'Android >= 4',
],
cascade: false,
}),
],
},
},
{
loader: 'sass-loader',
options: {
outputStyle: 'expanded',
},
},
],
}),
include: paths,
},
],
},
plugins: [
new ExtractTextPlugin({
filename: '[name].css',
}),
],
});
exports.devServer = options => ({
devServer: {
contentBase: __dirname,
historyApiFallback: true,
hot: false,
inline: true,
stats: 'errors-only',
host: options.host,
port: options.port,
overlay: {
warnings: true,
errors: true,
},
},
});
exports.minify = () => ({
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: true,
screw_ie8: true,
},
output: {
comments: false,
},
}),
],
});
exports.setFreeVariable = (key, value) => {
const env = {};
env[key] = JSON.stringify(value);
return {
plugins: [new webpack.DefinePlugin(env)],
};
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
=== AudioIgniter Music Player ===
Contributors: cssigniterteam, anastis, silencerius, tsiger
Tags: audio, podcast, audio player, html5 player, mp3 player, music player, music, radio stream, radio player, sound player, player, podcast player
Requires at least: 5.0
Tested up to: 5.9
Stable tag: 1.7.3
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
AudioIgniter lets you create music playlists and embed them in your WordPress posts, pages or custom post types and serve your audio content in style!
== Description ==
Looking for an MP3 music player? AudioIgniter lets you create music playlists and embed them in your WordPress posts, pages or custom post types. By using the standard WordPress media upload functionality, you can create new audio playlists in minutes. Oh, you can use AudioIgniter to stream your radio show too!
https://www.youtube.com/watch?v=AmRDYlVW_3M
Check out [the demo](https://www.cssigniter.com/preview/audioigniter/) now!
**Selling digital music?**
You can combine [AudioIgniter with WooCommerce to easily sell individual tracks](https://www.cssigniter.com/sell-individual-tracks-using-audioigniter-with-woocommerce/).
**Features:**
* Supports audio and radio streaming
* Unlimited playlists
* Unlimited tracks
* 100% Compatible with Elementor
* 100% Compatible with Visual Composer
* 100% Compatible with Gutenberg Block Editor
* Responsive layout
* Embed through shortcode
* Flexible settings per playlist
* Show/Hide track listing
* Show/Hide track numbers in tracklist
* Show numbers in reverse order
* Show/Hide track covers in playlist
* Show/Hide active tracks cover
* Show/Hide artist name
* Custom "Buy track" URL field
* Custom "Download" URL field
* "Full" or "Simple" player mode (Great for podcasts)
* Limit track listing height
* Repeat track listing option
* Maximum player width
* Automatic ID3 Tag extraction from MP3 files
* Heavily tested on the 150 most popular free themes on WordPress.org
**Supported Services:**
* Acast
* Amazon S3
* Anchor
* Art19
* AudioBoom
* Castbox
* Captivate
* Icecast
* Podbean
* Radiojar
* Shoutcast
* Speaker
* Stitcher
* Libsyn
**But wait, there's more!**
A [Pro version](https://www.cssigniter.com/plugins/audioigniter) is also available! Here's what you get if you decide to upgrade:
* Bulk upload functionality
* Rearrange tracks functionality
* Stop Tracks From Other Players (Multiple Players In One Page)
* Track skipping functionality (You can adjust the number of seconds)
* Playback rate (1x, 1.5x, 2x)
* Lyrics per track
* Individual Track Repeat Mode
* Non-continuous playback (Stop each track after playing)
* Optional customizable delay between tracks
* Shuffle playlist mode
* Starting track option
* Default track timer to countdown mode
* Fixed position player (Continuous play must be supported by your theme)
* Internal taxonomy for archiving purposes
* Customize the colors through the Customizer
* Custom block for the Gutenberg Block Editor (With the ability to change colors per player)
* Widget & Shortcode available
**PREMIUM SUPPORT**
You can expect the same level of support for both the free and pro version of our plugin. Average response time: 24 hours.
== Installation ==
1. Upload the plugin files to the `/wp-content/plugins/audioigniter` directory, or install the plugin through the WordPress plugins screen directly.
2. Activate the plugin through the "Plugins" screen in WordPress
3. In the WordPress admin dashboard you should see a new post type named "Playlists"
4. Navigate to the new Playlists post type and add your tracks!
== Screenshots ==
1. The AudioIgniter player
2. Managing your playlists via an intuitive and user friendly interface
3. Advanced player customization
== Changelog ==
= 1.7.3 =
* Top "Add Track" prepends a track, and bottom "Add Track" appends a track.
= 1.7.2 =
* Provide minimized and optimized stylesheet.
* Updated shortcode to support HTML classes via the class="" parameter.
= 1.7.1 =
* Added support for x3 playback rate.
* Added base support for user controlled shuffle button, in playlists that shuffle mode is enabled (Pro feature).
* Fixed a deprecation warning "Required parameter follows optional parameter" that would appear in PHP 8.
* Fixed spinner positioning on simple track listing.
* Introduced method AudioIgniter_Sanitizer::rgba_color()
= 1.7.0 =
* Added a loading spinner while the track is buffering.
* Developer note - Changed: Static property AudioIgniter::$version is now non-static and should be accessed as such.
= 1.6.3 =
* Fixed SVG appearance in TwentyTwenty theme.
= 1.6.2 =
* Fixed an issue where the download buttons would suggest an ugly filename consisting of the URL in a sanitized form.
= 1.6.1 =
* Fixed an issue where reverse track order would not work correctly under some particular option configurations.
= 1.6.0 =
* Change the layout of volume controls to be a bit more spacey.
* Hidden volume controls in mobile devices (improves player appearance in mobile).
= 1.5.1 =
* Minor performance improvements.
= 1.5.0 =
* Added new filters: 'aiStrings', 'audioigniter_get_playlist_data_attributes_array'.
* Added new actions: 'audioigniter_metabox_tracks_repeatable_track_fields_column_1', 'audioigniter_metabox_tracks_repeatable_track_fields_column_2', 'audioigniter_metabox_settings_group_player_track_listing_fields', 'audioigniter_metabox_settings_group_tracks_fields', 'audioigniter_metabox_settings_group_player_track_track_listing_repeat_fields'.
* Upgraded React / ReactDOM and dependencies to latest versions.
* Fixed issue with viewing buy/download buttons vs track repeat button.
* Fixed some untranslatable strings.
* Added base support for per-track Lyrics (requires Pro version).
* Added base support for single track looping (requires Pro version).
* Rearranged track listing settings layout.
= 1.4.2 =
* Accessibility enhancements.
= 1.4.1 =
* Developer enhancements.
= 1.4.0 =
* Code changes to accommodate a new player type, Global Footer Player, available in AudioIgniter Pro.
* Introduced AudioIgniter::is_playlist() for easier playlist ID validation.
* Added some translators comments.
= 1.3.0 =
* Added a new player type! From now on you can use a simpler playlist type if you don't need the full fledged player.
* Player type can now be selected via a simple dropdown.
* Updated some settings' labels to reflect the setting's function more accurately.
* Fixed an issue which prevented the player from working in IE11 sometimes.
* Fixed an issue where reversing a playlist would result in playing the incorrect tracks.
* Dropped IE9 support.
= 1.2.0 =
* Added support for initial volume setting.
* Show the tracklist toggle button when the tracklist is hidden by default.
* Added support for downloading tracks.
* Fixed issue where tracklist wouldn't display when there was only one track.
= 1.1.0 =
* Updated CSSIgniter links to https
* Added a button to enable repeating the playlist. Added admin option for the default state of the repeat button.
* Fixed a bug where the playlist would not get shown if it contained only one track.
* Added option to choose whether track links should open in a new window or not.
= 1.0.1 =
* Stop looping over the tracklist when the player finishes playing the last track.
* A couple of strings could not be translated.
= 1.0.0 =
* Initial release.

View File

@ -0,0 +1,11 @@
<?php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ||
! WP_UNINSTALL_PLUGIN ||
dirname( WP_UNINSTALL_PLUGIN ) != dirname( plugin_basename( __FILE__ ) )
) {
status_header( 404 );
exit;
}
flush_rewrite_rules();