mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 00:32:42 +01:00
Add a "Play Sound" workboard trigger rule
Summary: Ref T5474. Allow columns to play a sound when tasks are dropped. This is a little tricky because Safari has changed somewhat recently to require some gymnastics to play sounds when the user didn't explicitly click something. Preloading the sound on the first mouse interaction, then playing and immediately pausing it seems to work, though. Test Plan: Added a trigger with 5 sounds. In Safari, Chrome, and Firefox, dropped a card into the column. In all browsers, heard a nice sequence of 5 sounds played one after the other. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20306
This commit is contained in:
parent
66c1d623c3
commit
bfa5ffe8a1
12 changed files with 267 additions and 38 deletions
|
@ -10,7 +10,7 @@ return array(
|
|||
'conpherence.pkg.css' => '3c8a0668',
|
||||
'conpherence.pkg.js' => '020aebcf',
|
||||
'core.pkg.css' => 'b797945d',
|
||||
'core.pkg.js' => 'eaca003c',
|
||||
'core.pkg.js' => 'eb53fc5b',
|
||||
'differential.pkg.css' => '8d8360fb',
|
||||
'differential.pkg.js' => '67e02996',
|
||||
'diffusion.pkg.css' => '42c75c37',
|
||||
|
@ -249,7 +249,7 @@ return array(
|
|||
'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e',
|
||||
'rsrc/externals/javelin/lib/Router.js' => '32755edb',
|
||||
'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae',
|
||||
'rsrc/externals/javelin/lib/Sound.js' => 'e562708c',
|
||||
'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a',
|
||||
'rsrc/externals/javelin/lib/URI.js' => '2e255291',
|
||||
'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb',
|
||||
'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e',
|
||||
|
@ -409,7 +409,7 @@ return array(
|
|||
'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f',
|
||||
'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9',
|
||||
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172',
|
||||
'rsrc/js/application/projects/WorkboardBoard.js' => '65afb173',
|
||||
'rsrc/js/application/projects/WorkboardBoard.js' => '3ba8e6ad',
|
||||
'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8',
|
||||
'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4',
|
||||
'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63',
|
||||
|
@ -418,7 +418,7 @@ return array(
|
|||
'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d',
|
||||
'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b',
|
||||
'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f',
|
||||
'rsrc/js/application/projects/behavior-project-boards.js' => '8512e4ea',
|
||||
'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445',
|
||||
'rsrc/js/application/projects/behavior-project-create.js' => '34c53422',
|
||||
'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9',
|
||||
'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
|
||||
|
@ -664,7 +664,7 @@ return array(
|
|||
'javelin-behavior-phuix-example' => 'c2c500a7',
|
||||
'javelin-behavior-policy-control' => '0eaa33a9',
|
||||
'javelin-behavior-policy-rule-editor' => '9347f172',
|
||||
'javelin-behavior-project-boards' => '8512e4ea',
|
||||
'javelin-behavior-project-boards' => 'aad45445',
|
||||
'javelin-behavior-project-create' => '34c53422',
|
||||
'javelin-behavior-quicksand-blacklist' => '5a6f6a06',
|
||||
'javelin-behavior-read-only-warning' => 'b9109f8f',
|
||||
|
@ -718,7 +718,7 @@ return array(
|
|||
'javelin-routable' => '6a18c42e',
|
||||
'javelin-router' => '32755edb',
|
||||
'javelin-scrollbar' => 'a43ae2ae',
|
||||
'javelin-sound' => 'e562708c',
|
||||
'javelin-sound' => 'd4cc2d2a',
|
||||
'javelin-stratcom' => '0889b835',
|
||||
'javelin-tokenizer' => '89a1ae3a',
|
||||
'javelin-typeahead' => 'a4356cde',
|
||||
|
@ -737,7 +737,7 @@ return array(
|
|||
'javelin-view-renderer' => '9aae2b66',
|
||||
'javelin-view-visitor' => '308f9fe4',
|
||||
'javelin-websocket' => 'fdc13e4e',
|
||||
'javelin-workboard-board' => '65afb173',
|
||||
'javelin-workboard-board' => '3ba8e6ad',
|
||||
'javelin-workboard-card' => '0392a5d8',
|
||||
'javelin-workboard-card-template' => '2a61f8d4',
|
||||
'javelin-workboard-column' => 'c3d24e63',
|
||||
|
@ -1227,6 +1227,18 @@ return array(
|
|||
'javelin-behavior',
|
||||
'phabricator-prefab',
|
||||
),
|
||||
'3ba8e6ad' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
'javelin-workboard-column',
|
||||
'javelin-workboard-header-template',
|
||||
'javelin-workboard-card-template',
|
||||
'javelin-workboard-order-template',
|
||||
),
|
||||
'3dc5ad43' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
@ -1456,18 +1468,6 @@ return array(
|
|||
'60cd9241' => array(
|
||||
'javelin-behavior',
|
||||
),
|
||||
'65afb173' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
'javelin-workboard-column',
|
||||
'javelin-workboard-header-template',
|
||||
'javelin-workboard-card-template',
|
||||
'javelin-workboard-order-template',
|
||||
),
|
||||
'65bb0011' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1594,16 +1594,6 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-vector',
|
||||
),
|
||||
'8512e4ea' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'javelin-workboard-controller',
|
||||
'javelin-workboard-drop-effect',
|
||||
),
|
||||
'87428eb2' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-diffusion-locate-file-source',
|
||||
|
@ -1848,6 +1838,16 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-util',
|
||||
),
|
||||
'aad45445' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'javelin-workboard-controller',
|
||||
'javelin-workboard-drop-effect',
|
||||
),
|
||||
'ab85e184' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
|
@ -2041,6 +2041,9 @@ return array(
|
|||
'd3799cb4' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'd4cc2d2a' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'd8a86cfb' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -2075,9 +2078,6 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-history',
|
||||
),
|
||||
'e562708c' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'e5bdb730' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
|
|
@ -4183,6 +4183,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php',
|
||||
'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php',
|
||||
'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php',
|
||||
'PhabricatorProjectTriggerPlaySoundRule' => 'applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php',
|
||||
'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php',
|
||||
'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php',
|
||||
'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php',
|
||||
|
@ -10317,6 +10318,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType',
|
||||
'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType',
|
||||
'PhabricatorProjectTriggerPlaySoundRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorProjectTriggerRule' => 'Phobject',
|
||||
'PhabricatorProjectTriggerRuleRecord' => 'Phobject',
|
||||
|
|
|
@ -542,6 +542,7 @@ final class PhabricatorProjectBoardViewController
|
|||
$templates = array();
|
||||
$all_tasks = array();
|
||||
$column_templates = array();
|
||||
$sounds = array();
|
||||
foreach ($visible_columns as $column_phid => $column) {
|
||||
$column_tasks = $column_phids[$column_phid];
|
||||
|
||||
|
@ -629,6 +630,10 @@ final class PhabricatorProjectBoardViewController
|
|||
if ($trigger) {
|
||||
$preview_effect = $trigger->getPreviewEffect()
|
||||
->toDictionary();
|
||||
|
||||
foreach ($trigger->getSoundEffects() as $sound) {
|
||||
$sounds[] = $sound;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -685,6 +690,7 @@ final class PhabricatorProjectBoardViewController
|
|||
|
||||
'boardID' => $board_id,
|
||||
'projectPHID' => $project->getPHID(),
|
||||
'preloadSounds' => $sounds,
|
||||
);
|
||||
$this->initBehavior('project-boards', $behavior_config);
|
||||
|
||||
|
|
|
@ -152,7 +152,8 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
protected function newCardResponse(
|
||||
$board_phid,
|
||||
$object_phid,
|
||||
PhabricatorProjectColumnOrder $ordering = null) {
|
||||
PhabricatorProjectColumnOrder $ordering = null,
|
||||
$sounds = array()) {
|
||||
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
|
@ -166,7 +167,8 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
->setViewer($viewer)
|
||||
->setBoardPHID($board_phid)
|
||||
->setObjectPHID($object_phid)
|
||||
->setVisiblePHIDs($visible_phids);
|
||||
->setVisiblePHIDs($visible_phids)
|
||||
->setSounds($sounds);
|
||||
|
||||
if ($ordering) {
|
||||
$engine->setOrdering($ordering);
|
||||
|
|
|
@ -111,6 +111,7 @@ final class PhabricatorProjectMoveController
|
|||
$xactions[] = $header_xaction;
|
||||
}
|
||||
|
||||
$sounds = array();
|
||||
if ($column->canHaveTrigger()) {
|
||||
$trigger = $column->getTrigger();
|
||||
if ($trigger) {
|
||||
|
@ -121,6 +122,10 @@ final class PhabricatorProjectMoveController
|
|||
foreach ($trigger_xactions as $trigger_xaction) {
|
||||
$xactions[] = $trigger_xaction;
|
||||
}
|
||||
|
||||
foreach ($trigger->getSoundEffects() as $effect) {
|
||||
$sounds[] = $effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,7 +138,11 @@ final class PhabricatorProjectMoveController
|
|||
|
||||
$editor->applyTransactions($object, $xactions);
|
||||
|
||||
return $this->newCardResponse($board_phid, $object_phid, $ordering);
|
||||
return $this->newCardResponse(
|
||||
$board_phid,
|
||||
$object_phid,
|
||||
$ordering,
|
||||
$sounds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
private $objectPHID;
|
||||
private $visiblePHIDs;
|
||||
private $ordering;
|
||||
private $sounds;
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
|
@ -53,6 +54,15 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
return $this->ordering;
|
||||
}
|
||||
|
||||
public function setSounds(array $sounds) {
|
||||
$this->sounds = $sounds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSounds() {
|
||||
return $this->sounds;
|
||||
}
|
||||
|
||||
public function buildResponse() {
|
||||
$viewer = $this->getViewer();
|
||||
$object_phid = $this->getObjectPHID();
|
||||
|
@ -150,6 +160,7 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
'columnMaps' => $natural,
|
||||
'cards' => $cards,
|
||||
'headers' => $headers,
|
||||
'sounds' => $this->getSounds(),
|
||||
);
|
||||
|
||||
return id(new AphrontAjaxResponse())
|
||||
|
|
|
@ -245,6 +245,18 @@ final class PhabricatorProjectTrigger
|
|||
->setContent($header);
|
||||
}
|
||||
|
||||
public function getSoundEffects() {
|
||||
$sounds = array();
|
||||
|
||||
foreach ($this->getTriggerRules() as $rule) {
|
||||
foreach ($rule->getSoundEffects() as $effect) {
|
||||
$sounds[] = $effect;
|
||||
}
|
||||
}
|
||||
|
||||
return $sounds;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerPlaySoundRule
|
||||
extends PhabricatorProjectTriggerRule {
|
||||
|
||||
const TRIGGERTYPE = 'sound';
|
||||
|
||||
public function getSelectControlName() {
|
||||
return pht('Play sound');
|
||||
}
|
||||
|
||||
protected function assertValidRuleValue($value) {
|
||||
if (!is_string($value)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Status rule value should be a string, but is not (value is "%s").',
|
||||
phutil_describe_type($value)));
|
||||
}
|
||||
|
||||
$map = self::getSoundMap();
|
||||
|
||||
if (!isset($map[$value])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Rule value ("%s") is not a valid sound.',
|
||||
$value));
|
||||
}
|
||||
}
|
||||
|
||||
protected function newDropTransactions($object, $value) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function newDropEffects($value) {
|
||||
$sound_icon = 'fa-volume-up';
|
||||
$sound_color = 'blue';
|
||||
$sound_name = self::getSoundName($value);
|
||||
|
||||
$content = pht(
|
||||
'Play sound %s.',
|
||||
phutil_tag('strong', array(), $sound_name));
|
||||
|
||||
return array(
|
||||
$this->newEffect()
|
||||
->setIcon($sound_icon)
|
||||
->setColor($sound_color)
|
||||
->setContent($content),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultValue() {
|
||||
return head_key(self::getSoundMap());
|
||||
}
|
||||
|
||||
protected function getPHUIXControlType() {
|
||||
return 'select';
|
||||
}
|
||||
|
||||
protected function getPHUIXControlSpecification() {
|
||||
$map = self::getSoundMap();
|
||||
$map = ipull($map, 'name');
|
||||
|
||||
return array(
|
||||
'options' => $map,
|
||||
'order' => array_keys($map),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRuleViewLabel() {
|
||||
return pht('Play Sound');
|
||||
}
|
||||
|
||||
public function getRuleViewDescription($value) {
|
||||
$sound_name = self::getSoundName($value);
|
||||
|
||||
return pht(
|
||||
'Play sound %s.',
|
||||
phutil_tag('strong', array(), $sound_name));
|
||||
}
|
||||
|
||||
public function getRuleViewIcon($value) {
|
||||
$sound_icon = 'fa-volume-up';
|
||||
$sound_color = 'blue';
|
||||
|
||||
return id(new PHUIIconView())
|
||||
->setIcon($sound_icon, $sound_color);
|
||||
}
|
||||
|
||||
private static function getSoundName($value) {
|
||||
$map = self::getSoundMap();
|
||||
$spec = idx($map, $value, array());
|
||||
return idx($spec, 'name', $value);
|
||||
}
|
||||
|
||||
private static function getSoundMap() {
|
||||
return array(
|
||||
'bing' => array(
|
||||
'name' => pht('Bing'),
|
||||
'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'),
|
||||
),
|
||||
'glass' => array(
|
||||
'name' => pht('Glass'),
|
||||
'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSoundEffects() {
|
||||
$value = $this->getValue();
|
||||
|
||||
$map = self::getSoundMap();
|
||||
$spec = idx($map, $value, array());
|
||||
|
||||
$uris = array();
|
||||
if (isset($spec['uri'])) {
|
||||
$uris[] = $spec['uri'];
|
||||
}
|
||||
|
||||
return $uris;
|
||||
}
|
||||
|
||||
}
|
|
@ -60,6 +60,10 @@ abstract class PhabricatorProjectTriggerRule
|
|||
return null;
|
||||
}
|
||||
|
||||
public function getSoundEffects() {
|
||||
return array();
|
||||
}
|
||||
|
||||
final public function getDropTransactions($object, $value) {
|
||||
return $this->newDropTransactions($object, $value);
|
||||
}
|
||||
|
|
50
webroot/rsrc/externals/javelin/lib/Sound.js
vendored
50
webroot/rsrc/externals/javelin/lib/Sound.js
vendored
|
@ -8,31 +8,75 @@
|
|||
JX.install('Sound', {
|
||||
statics: {
|
||||
_sounds: {},
|
||||
_queue: [],
|
||||
_playingQueue: false,
|
||||
|
||||
load: function(uri) {
|
||||
var self = JX.Sound;
|
||||
|
||||
if (!(uri in self._sounds)) {
|
||||
self._sounds[uri] = JX.$N(
|
||||
var audio = JX.$N(
|
||||
'audio',
|
||||
{
|
||||
src: uri,
|
||||
preload: 'auto'
|
||||
});
|
||||
|
||||
// In Safari, it isn't good enough to just load a sound in response
|
||||
// to a click: we must also play it. Once we've played it once, we
|
||||
// can continue to play it freely.
|
||||
|
||||
// Play the sound, then immediately pause it. This rejects the "play()"
|
||||
// promise but marks the audio as playable, so our "play()" method will
|
||||
// work correctly later.
|
||||
if (window.webkitAudioContext) {
|
||||
audio.play().then(JX.bag, JX.bag);
|
||||
audio.pause();
|
||||
}
|
||||
|
||||
self._sounds[uri] = audio;
|
||||
}
|
||||
},
|
||||
|
||||
play: function(uri) {
|
||||
play: function(uri, callback) {
|
||||
var self = JX.Sound;
|
||||
self.load(uri);
|
||||
|
||||
var sound = self._sounds[uri];
|
||||
|
||||
try {
|
||||
sound.play();
|
||||
sound.onended = callback || JX.bag;
|
||||
sound.play().then(JX.bag, callback || JX.bag);
|
||||
} catch (ex) {
|
||||
JX.log(ex);
|
||||
}
|
||||
},
|
||||
|
||||
queue: function(uri) {
|
||||
var self = JX.Sound;
|
||||
self._queue.push(uri);
|
||||
self._playQueue();
|
||||
},
|
||||
|
||||
_playQueue: function() {
|
||||
var self = JX.Sound;
|
||||
if (self._playingQueue) {
|
||||
return;
|
||||
}
|
||||
self._playingQueue = true;
|
||||
self._nextQueue();
|
||||
},
|
||||
|
||||
_nextQueue: function() {
|
||||
var self = JX.Sound;
|
||||
if (self._queue.length) {
|
||||
var next = self._queue[0];
|
||||
self._queue.splice(0, 1);
|
||||
self.play(next, self._nextQueue);
|
||||
} else {
|
||||
self._playingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
|
|
@ -529,6 +529,11 @@ JX.install('WorkboardBoard', {
|
|||
|
||||
this.updateCard(response);
|
||||
|
||||
var sounds = response.sounds || [];
|
||||
for (var ii = 0; ii < sounds.length; ii++) {
|
||||
JX.Sound.queue(sounds[ii]);
|
||||
}
|
||||
|
||||
list.unlock();
|
||||
},
|
||||
|
||||
|
|
|
@ -166,4 +166,16 @@ JX.behavior('project-boards', function(config, statics) {
|
|||
|
||||
board.start();
|
||||
|
||||
// In Safari, we can only play sounds that we've already loaded, and we can
|
||||
// only load them in response to an explicit user interaction like a click.
|
||||
var sounds = config.preloadSounds;
|
||||
var listener = JX.Stratcom.listen('mousedown', null, function() {
|
||||
for (var ii = 0; ii < sounds.length; ii++) {
|
||||
JX.Sound.load(sounds[ii]);
|
||||
}
|
||||
|
||||
// Remove this callback once it has run once.
|
||||
listener.remove();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue