Dynamically composite favicons from customizable sources
Summary: Ref T13103. Make favicons customizable, and perform dynamic compositing to add marker to indicate things like "unread messages". Test Plan: Viewed favicons in Safari, Firefox and Chrome. With unread messages, saw pink dot composited into icon. Maniphest Tasks: T13103 Differential Revision: https://secure.phabricator.com/D19209
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
BIN
resources/builtin/favicon/dot-pink-64x64.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
resources/builtin/favicon/dot-red-64x64.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
|
@ -16,7 +16,6 @@ return array(
|
||||||
'differential.pkg.js' => 'f6d809c0',
|
'differential.pkg.js' => 'f6d809c0',
|
||||||
'diffusion.pkg.css' => 'a2d17c7d',
|
'diffusion.pkg.css' => 'a2d17c7d',
|
||||||
'diffusion.pkg.js' => '6134c5a1',
|
'diffusion.pkg.js' => '6134c5a1',
|
||||||
'favicon.ico' => '30672e08',
|
|
||||||
'maniphest.pkg.css' => '4845691a',
|
'maniphest.pkg.css' => '4845691a',
|
||||||
'maniphest.pkg.js' => '4d7e79c8',
|
'maniphest.pkg.js' => '4d7e79c8',
|
||||||
'rsrc/audio/basic/alert.mp3' => '98461568',
|
'rsrc/audio/basic/alert.mp3' => '98461568',
|
||||||
|
@ -270,28 +269,8 @@ return array(
|
||||||
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
|
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
|
||||||
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82',
|
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82',
|
||||||
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa',
|
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa',
|
||||||
'rsrc/favicons/apple-touch-icon-114x114.png' => '12a24178',
|
|
||||||
'rsrc/favicons/apple-touch-icon-120x120.png' => '0d1543c7',
|
|
||||||
'rsrc/favicons/apple-touch-icon-144x144.png' => '8043b5a5',
|
|
||||||
'rsrc/favicons/apple-touch-icon-152x152.png' => '65905ecd',
|
|
||||||
'rsrc/favicons/apple-touch-icon-57x57.png' => '2bfc7b0a',
|
|
||||||
'rsrc/favicons/apple-touch-icon-60x60.png' => '8ff52925',
|
|
||||||
'rsrc/favicons/apple-touch-icon-72x72.png' => 'a2bb65d6',
|
|
||||||
'rsrc/favicons/apple-touch-icon-76x76.png' => '2d061a11',
|
|
||||||
'rsrc/favicons/favicon-128.png' => '72f7e812',
|
|
||||||
'rsrc/favicons/favicon-16x16.png' => 'fc6275ba',
|
'rsrc/favicons/favicon-16x16.png' => 'fc6275ba',
|
||||||
'rsrc/favicons/favicon-196x196.png' => '95db275e',
|
|
||||||
'rsrc/favicons/favicon-32x32.png' => '5bd18b6c',
|
|
||||||
'rsrc/favicons/favicon-96x96.png' => '7242c8e9',
|
|
||||||
'rsrc/favicons/favicon-mention.ico' => '1fdd0fa4',
|
|
||||||
'rsrc/favicons/favicon-message.ico' => '115bc010',
|
|
||||||
'rsrc/favicons/favicon.ico' => 'cdb11121',
|
|
||||||
'rsrc/favicons/mask-icon.svg' => 'e132a80f',
|
'rsrc/favicons/mask-icon.svg' => 'e132a80f',
|
||||||
'rsrc/favicons/mstile-144x144.png' => '310c2ee5',
|
|
||||||
'rsrc/favicons/mstile-150x150.png' => '74bf5133',
|
|
||||||
'rsrc/favicons/mstile-310x150.png' => '4a49d3ee',
|
|
||||||
'rsrc/favicons/mstile-310x310.png' => 'a52ab264',
|
|
||||||
'rsrc/favicons/mstile-70x70.png' => '5edce7b8',
|
|
||||||
'rsrc/image/BFCFDA.png' => 'd5ec91f4',
|
'rsrc/image/BFCFDA.png' => 'd5ec91f4',
|
||||||
'rsrc/image/actions/edit.png' => '2fc41442',
|
'rsrc/image/actions/edit.png' => '2fc41442',
|
||||||
'rsrc/image/avatar.png' => '17d346a4',
|
'rsrc/image/avatar.png' => '17d346a4',
|
||||||
|
|
|
@ -2953,6 +2953,8 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php',
|
'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php',
|
||||||
'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
|
'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
|
||||||
'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
|
'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
|
||||||
|
'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php',
|
||||||
|
'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php',
|
||||||
'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php',
|
'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php',
|
||||||
'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php',
|
'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php',
|
||||||
'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php',
|
'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php',
|
||||||
|
@ -4331,7 +4333,6 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
|
'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
|
||||||
'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
|
'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
|
||||||
'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
|
'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
|
||||||
'PhabricatorSystemFaviconController' => 'applications/system/controller/PhabricatorSystemFaviconController.php',
|
|
||||||
'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php',
|
'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php',
|
||||||
'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
|
'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
|
||||||
'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
|
'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
|
||||||
|
@ -8512,6 +8513,8 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension',
|
'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension',
|
||||||
'PhabricatorFactRaw' => 'PhabricatorFactDAO',
|
'PhabricatorFactRaw' => 'PhabricatorFactDAO',
|
||||||
'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
|
'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
|
||||||
|
'PhabricatorFaviconRef' => 'Phobject',
|
||||||
|
'PhabricatorFaviconRefQuery' => 'Phobject',
|
||||||
'PhabricatorFavoritesApplication' => 'PhabricatorApplication',
|
'PhabricatorFavoritesApplication' => 'PhabricatorApplication',
|
||||||
'PhabricatorFavoritesController' => 'PhabricatorController',
|
'PhabricatorFavoritesController' => 'PhabricatorController',
|
||||||
'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
|
'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
|
||||||
|
@ -10142,7 +10145,6 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
|
'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
|
||||||
'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
|
'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||||
'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
|
'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
|
||||||
'PhabricatorSystemFaviconController' => 'PhabricatorController',
|
|
||||||
'PhabricatorSystemReadOnlyController' => 'PhabricatorController',
|
'PhabricatorSystemReadOnlyController' => 'PhabricatorController',
|
||||||
'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
|
'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
|
||||||
'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
|
'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
|
||||||
|
|
|
@ -64,6 +64,10 @@ EOJSON;
|
||||||
"Phabricator logo in the site header.\n\n".
|
"Phabricator logo in the site header.\n\n".
|
||||||
" - **Wordmark**: Choose new text to display next to the logo. ".
|
" - **Wordmark**: Choose new text to display next to the logo. ".
|
||||||
"By default, the header displays //Phabricator//.\n\n")),
|
"By default, the header displays //Phabricator//.\n\n")),
|
||||||
|
$this->newOption('ui.favicons', 'wild', array())
|
||||||
|
->setSummary(pht('Customize favicons.'))
|
||||||
|
->setDescription(pht('Customize favicons.'))
|
||||||
|
->setLocked(true),
|
||||||
$this->newOption('ui.footer-items', $footer_type, array())
|
$this->newOption('ui.footer-items', $footer_type, array())
|
||||||
->setSummary(
|
->setSummary(
|
||||||
pht(
|
pht(
|
||||||
|
|
447
src/applications/files/favicon/PhabricatorFaviconRef.php
Normal file
|
@ -0,0 +1,447 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorFaviconRef extends Phobject {
|
||||||
|
|
||||||
|
private $viewer;
|
||||||
|
private $width;
|
||||||
|
private $height;
|
||||||
|
private $emblems;
|
||||||
|
private $uri;
|
||||||
|
private $cacheKey;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->emblems = array(null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setViewer(PhabricatorUser $viewer) {
|
||||||
|
$this->viewer = $viewer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViewer() {
|
||||||
|
return $this->viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWidth($width) {
|
||||||
|
$this->width = $width;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWidth() {
|
||||||
|
return $this->width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeight($height) {
|
||||||
|
$this->height = $height;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeight() {
|
||||||
|
return $this->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmblems(array $emblems) {
|
||||||
|
if (count($emblems) !== 4) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Expected four elements in icon emblem list. To omit an emblem, '.
|
||||||
|
'pass "null".'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->emblems = $emblems;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmblems() {
|
||||||
|
return $this->emblems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setURI($uri) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI() {
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCacheKey($cache_key) {
|
||||||
|
$this->cacheKey = $cache_key;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCacheKey() {
|
||||||
|
return $this->cacheKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newDigest() {
|
||||||
|
return PhabricatorHash::digestForIndex(serialize($this->toDictionary()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toDictionary() {
|
||||||
|
return array(
|
||||||
|
'width' => $this->width,
|
||||||
|
'height' => $this->height,
|
||||||
|
'emblems' => $this->emblems,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function newConfigurationDigest() {
|
||||||
|
$all_resources = self::getAllResources();
|
||||||
|
|
||||||
|
// Because we need to access this cache on every page, it's very sticky.
|
||||||
|
// Try to dirty it automatically if any relevant configuration changes.
|
||||||
|
$inputs = array(
|
||||||
|
'resources' => $all_resources,
|
||||||
|
'prod' => PhabricatorEnv::getProductionURI('/'),
|
||||||
|
'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'),
|
||||||
|
'havepng' => function_exists('imagepng'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return PhabricatorHash::digestForIndex(serialize($inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getAllResources() {
|
||||||
|
$custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons');
|
||||||
|
|
||||||
|
foreach ($custom_resources as $key => $custom_resource) {
|
||||||
|
$custom_resources[$key] = array(
|
||||||
|
'source-type' => 'file',
|
||||||
|
'default' => false,
|
||||||
|
) + $custom_resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builtin_resources = self::getBuiltinResources();
|
||||||
|
|
||||||
|
return array_merge($builtin_resources, $custom_resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getBuiltinResources() {
|
||||||
|
return array(
|
||||||
|
array(
|
||||||
|
'source-type' => 'builtin',
|
||||||
|
'source' => 'favicon/default-76x76.png',
|
||||||
|
'version' => 1,
|
||||||
|
'width' => 76,
|
||||||
|
'height' => 76,
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'source-type' => 'builtin',
|
||||||
|
'source' => 'favicon/default-120x120.png',
|
||||||
|
'version' => 1,
|
||||||
|
'width' => 120,
|
||||||
|
'height' => 120,
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'source-type' => 'builtin',
|
||||||
|
'source' => 'favicon/default-128x128.png',
|
||||||
|
'version' => 1,
|
||||||
|
'width' => 128,
|
||||||
|
'height' => 128,
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'source-type' => 'builtin',
|
||||||
|
'source' => 'favicon/default-152x152.png',
|
||||||
|
'version' => 1,
|
||||||
|
'width' => 152,
|
||||||
|
'height' => 152,
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'source-type' => 'builtin',
|
||||||
|
'source' => 'favicon/dot-pink-64x64.png',
|
||||||
|
'version' => 1,
|
||||||
|
'width' => 64,
|
||||||
|
'height' => 64,
|
||||||
|
'emblem' => 'dot-pink',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'source-type' => 'builtin',
|
||||||
|
'source' => 'favicon/dot-red-64x64.png',
|
||||||
|
'version' => 1,
|
||||||
|
'width' => 64,
|
||||||
|
'height' => 64,
|
||||||
|
'emblem' => 'dot-red',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newURI() {
|
||||||
|
$dst_w = $this->getWidth();
|
||||||
|
$dst_h = $this->getHeight();
|
||||||
|
|
||||||
|
$template = $this->newTemplateFile(null, $dst_w, $dst_h);
|
||||||
|
$template_file = $template['file'];
|
||||||
|
|
||||||
|
$cache = $this->loadCachedFile($template_file);
|
||||||
|
if ($cache) {
|
||||||
|
return $cache->getViewURI();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->newCompositedFavicon($template);
|
||||||
|
|
||||||
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||||
|
|
||||||
|
$caught = null;
|
||||||
|
try {
|
||||||
|
$favicon_file = $this->newFaviconFile($data);
|
||||||
|
|
||||||
|
$xform = id(new PhabricatorTransformedFile())
|
||||||
|
->setOriginalPHID($template_file->getPHID())
|
||||||
|
->setTransformedPHID($favicon_file->getPHID())
|
||||||
|
->setTransform($this->getCacheKey());
|
||||||
|
|
||||||
|
try {
|
||||||
|
$xform->save();
|
||||||
|
} catch (AphrontDuplicateKeyQueryException $ex) {
|
||||||
|
unset($unguarded);
|
||||||
|
|
||||||
|
$cache = $this->loadCachedFile($template_file);
|
||||||
|
if (!$cache) {
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
id(new PhabricatorDestructionEngine())
|
||||||
|
->destroyObject($favicon_file);
|
||||||
|
|
||||||
|
return $cache->getViewURI();
|
||||||
|
}
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$caught = $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($unguarded);
|
||||||
|
|
||||||
|
if ($caught) {
|
||||||
|
throw $caught;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $favicon_file->getViewURI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadCachedFile(PhabricatorFile $template_file) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$xform = id(new PhabricatorTransformedFile())->loadOneWhere(
|
||||||
|
'originalPHID = %s AND transform = %s',
|
||||||
|
$template_file->getPHID(),
|
||||||
|
$this->getCacheKey());
|
||||||
|
if (!$xform) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new PhabricatorFileQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($xform->getTransformedPHID()))
|
||||||
|
->executeOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function newCompositedFavicon($template) {
|
||||||
|
$dst_w = $this->getWidth();
|
||||||
|
$dst_h = $this->getHeight();
|
||||||
|
$src_w = $template['width'];
|
||||||
|
$src_h = $template['height'];
|
||||||
|
|
||||||
|
$template_data = $template['file']->loadFileData();
|
||||||
|
|
||||||
|
if (!function_exists('imagecreatefromstring')) {
|
||||||
|
return $template_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$src = @imagecreatefromstring($template_data);
|
||||||
|
if (!$src) {
|
||||||
|
return $template_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dst = imagecreatetruecolor($dst_w, $dst_h);
|
||||||
|
imagesavealpha($dst, true);
|
||||||
|
|
||||||
|
$transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127);
|
||||||
|
imagefill($dst, 0, 0, $transparent);
|
||||||
|
|
||||||
|
imagecopyresampled(
|
||||||
|
$dst,
|
||||||
|
$src,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
$dst_w,
|
||||||
|
$dst_h,
|
||||||
|
$src_w,
|
||||||
|
$src_h);
|
||||||
|
|
||||||
|
// Now, copy any icon emblems on top of the image. These are dots or other
|
||||||
|
// marks used to indicate status information.
|
||||||
|
$emblem_w = (int)floor(min($dst_w, $dst_h) / 2);
|
||||||
|
$emblem_h = $emblem_w;
|
||||||
|
foreach ($this->emblems as $key => $emblem) {
|
||||||
|
if ($emblem === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$emblem_template = $this->newTemplateFile(
|
||||||
|
$emblem,
|
||||||
|
$emblem_w,
|
||||||
|
$emblem_h);
|
||||||
|
|
||||||
|
switch ($key) {
|
||||||
|
case 0:
|
||||||
|
$emblem_x = $dst_w - $emblem_w;
|
||||||
|
$emblem_y = 0;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
$emblem_x = $dst_w - $emblem_w;
|
||||||
|
$emblem_y = $dst_h - $emblem_h;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$emblem_x = 0;
|
||||||
|
$emblem_y = $dst_h - $emblem_h;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
$emblem_x = 0;
|
||||||
|
$emblem_y = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$emblem_data = $emblem_template['file']->loadFileData();
|
||||||
|
|
||||||
|
$src = @imagecreatefromstring($emblem_data);
|
||||||
|
if (!$src) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagecopyresampled(
|
||||||
|
$dst,
|
||||||
|
$src,
|
||||||
|
$emblem_x,
|
||||||
|
$emblem_y,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
$emblem_w,
|
||||||
|
$emblem_h,
|
||||||
|
$emblem_template['width'],
|
||||||
|
$emblem_template['height']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
|
||||||
|
$dst,
|
||||||
|
'image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function newTemplateFile($emblem, $width, $height) {
|
||||||
|
$all_resources = self::getAllResources();
|
||||||
|
|
||||||
|
$scores = array();
|
||||||
|
$ratio = $width / $height;
|
||||||
|
foreach ($all_resources as $key => $resource) {
|
||||||
|
// We can't use an emblem resource for a different emblem, nor for an
|
||||||
|
// icon base. We also can't use an icon base as an emblem. That is, if
|
||||||
|
// we're looking for a picture of a red dot, we have to actually find
|
||||||
|
// a red dot, not just any image which happens to have a similar size.
|
||||||
|
if (idx($resource, 'emblem') !== $emblem) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource_width = $resource['width'];
|
||||||
|
$resource_height = $resource['height'];
|
||||||
|
|
||||||
|
// Never use a resource with a different aspect ratio.
|
||||||
|
if (($resource_width / $resource_height) !== $ratio) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use custom resources instead of default resources.
|
||||||
|
if ($resource['default']) {
|
||||||
|
$default_score = 1;
|
||||||
|
} else {
|
||||||
|
$default_score = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$width_diff = ($resource_width - $width);
|
||||||
|
|
||||||
|
// If we have to resize an image, we'd rather scale a larger image down
|
||||||
|
// than scale a smaller image up.
|
||||||
|
if ($width_diff < 0) {
|
||||||
|
$scale_score = 1;
|
||||||
|
} else {
|
||||||
|
$scale_score = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'd rather scale an image a little bit (ideally, zero)
|
||||||
|
// than scale an image a lot.
|
||||||
|
$width_score = abs($width_diff);
|
||||||
|
|
||||||
|
$scores[$key] = id(new PhutilSortVector())
|
||||||
|
->addInt($default_score)
|
||||||
|
->addInt($scale_score)
|
||||||
|
->addInt($width_score);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scores) {
|
||||||
|
if ($emblem === null) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Found no background template resource for dimensions %dx%d.',
|
||||||
|
$width,
|
||||||
|
$height));
|
||||||
|
} else {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Found no template resource (for emblem "%s") with dimensions '.
|
||||||
|
'%dx%d.',
|
||||||
|
$emblem,
|
||||||
|
$width,
|
||||||
|
$height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scores = msortv($scores, 'getSelf');
|
||||||
|
$best_score = head_key($scores);
|
||||||
|
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$resource = $all_resources[$best_score];
|
||||||
|
if ($resource['source-type'] === 'builtin') {
|
||||||
|
$file = PhabricatorFile::loadBuiltin($viewer, $resource['source']);
|
||||||
|
if (!$file) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Failed to load favicon template builtin "%s".',
|
||||||
|
$resource['source']));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$file = id(new PhabricatorFileQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($resource['source']))
|
||||||
|
->executeOne();
|
||||||
|
if (!$file) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Failed to load favicon template with PHID "%s".',
|
||||||
|
$resource['source']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'width' => $resource['width'],
|
||||||
|
'height' => $resource['height'],
|
||||||
|
'file' => $file,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function newFaviconFile($data) {
|
||||||
|
return PhabricatorFile::newFromFileData(
|
||||||
|
$data,
|
||||||
|
array(
|
||||||
|
'name' => 'favicon',
|
||||||
|
'canCDN' => true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorFaviconRefQuery extends Phobject {
|
||||||
|
|
||||||
|
private $refs;
|
||||||
|
|
||||||
|
public function withRefs(array $refs) {
|
||||||
|
assert_instances_of($refs, 'PhabricatorFaviconRef');
|
||||||
|
$this->refs = $refs;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||||
|
|
||||||
|
$refs = $this->refs;
|
||||||
|
|
||||||
|
$config_digest = PhabricatorFaviconRef::newConfigurationDigest();
|
||||||
|
|
||||||
|
$ref_map = array();
|
||||||
|
foreach ($refs as $ref) {
|
||||||
|
$ref_digest = $ref->newDigest();
|
||||||
|
$ref_key = "favicon({$config_digest},{$ref_digest},8)";
|
||||||
|
|
||||||
|
$ref
|
||||||
|
->setViewer($viewer)
|
||||||
|
->setCacheKey($ref_key);
|
||||||
|
|
||||||
|
$ref_map[$ref_key] = $ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache = PhabricatorCaches::getImmutableCache();
|
||||||
|
$ref_hits = $cache->getKeys(array_keys($ref_map));
|
||||||
|
|
||||||
|
foreach ($ref_hits as $ref_key => $ref_uri) {
|
||||||
|
$ref_map[$ref_key]->setURI($ref_uri);
|
||||||
|
unset($ref_map[$ref_key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ref_map) {
|
||||||
|
$new_map = array();
|
||||||
|
foreach ($ref_map as $ref_key => $ref) {
|
||||||
|
$ref_uri = $ref->newURI();
|
||||||
|
$ref->setURI($ref_uri);
|
||||||
|
$new_map[$ref_key] = $ref_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache->setKeys($new_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1152,7 +1152,6 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
||||||
|
|
||||||
$params = array(
|
$params = array(
|
||||||
'name' => $builtin->getBuiltinDisplayName(),
|
'name' => $builtin->getBuiltinDisplayName(),
|
||||||
'ttl.relative' => phutil_units('7 days in seconds'),
|
|
||||||
'canCDN' => true,
|
'canCDN' => true,
|
||||||
'builtin' => $key,
|
'builtin' => $key,
|
||||||
);
|
);
|
||||||
|
@ -1648,7 +1647,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
||||||
public function getFieldValuesForConduit() {
|
public function getFieldValuesForConduit() {
|
||||||
return array(
|
return array(
|
||||||
'name' => $this->getName(),
|
'name' => $this->getName(),
|
||||||
'dataURI' => $this->getCDNURI(),
|
'dataURI' => $this->getCDNURI('data'),
|
||||||
'size' => (int)$this->getByteSize(),
|
'size' => (int)$this->getByteSize(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ final class PhabricatorSystemApplication extends PhabricatorApplication {
|
||||||
'/readonly/' => array(
|
'/readonly/' => array(
|
||||||
'(?P<reason>[^/]+)/' => 'PhabricatorSystemReadOnlyController',
|
'(?P<reason>[^/]+)/' => 'PhabricatorSystemReadOnlyController',
|
||||||
),
|
),
|
||||||
'/favicon.ico' => 'PhabricatorSystemFaviconController',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
final class PhabricatorSystemFaviconController extends PhabricatorController {
|
|
||||||
|
|
||||||
public function shouldRequireLogin() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function processRequest() {
|
|
||||||
$webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/';
|
|
||||||
$content = Filesystem::readFile($webroot.'/rsrc/favicons/favicon.ico');
|
|
||||||
|
|
||||||
return id(new AphrontFileResponse())
|
|
||||||
->setContent($content)
|
|
||||||
->setMimeType('image/x-icon')
|
|
||||||
->setCacheDurationInSeconds(phutil_units('24 hours in seconds'))
|
|
||||||
->setCanCDN(true);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -71,6 +71,14 @@ class PhabricatorBarePageView extends AphrontPageView {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$referrer_tag = phutil_tag(
|
||||||
|
'meta',
|
||||||
|
array(
|
||||||
|
'name' => 'referrer',
|
||||||
|
'content' => 'no-referrer',
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
$mask_icon = phutil_tag(
|
$mask_icon = phutil_tag(
|
||||||
'link',
|
'link',
|
||||||
array(
|
array(
|
||||||
|
@ -80,47 +88,7 @@ class PhabricatorBarePageView extends AphrontPageView {
|
||||||
'/rsrc/favicons/mask-icon.svg'),
|
'/rsrc/favicons/mask-icon.svg'),
|
||||||
));
|
));
|
||||||
|
|
||||||
$icon_tag_76 = phutil_tag(
|
$favicon_links = $this->newFavicons();
|
||||||
'link',
|
|
||||||
array(
|
|
||||||
'rel' => 'apple-touch-icon',
|
|
||||||
'href' => celerity_get_resource_uri(
|
|
||||||
'/rsrc/favicons/apple-touch-icon-76x76.png'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$icon_tag_120 = phutil_tag(
|
|
||||||
'link',
|
|
||||||
array(
|
|
||||||
'rel' => 'apple-touch-icon',
|
|
||||||
'sizes' => '120x120',
|
|
||||||
'href' => celerity_get_resource_uri(
|
|
||||||
'/rsrc/favicons/apple-touch-icon-120x120.png'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$icon_tag_152 = phutil_tag(
|
|
||||||
'link',
|
|
||||||
array(
|
|
||||||
'rel' => 'apple-touch-icon',
|
|
||||||
'sizes' => '152x152',
|
|
||||||
'href' => celerity_get_resource_uri(
|
|
||||||
'/rsrc/favicons/apple-touch-icon-152x152.png'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$favicon_tag = phutil_tag(
|
|
||||||
'link',
|
|
||||||
array(
|
|
||||||
'id' => 'favicon',
|
|
||||||
'rel' => 'shortcut icon',
|
|
||||||
'href' => celerity_get_resource_uri(
|
|
||||||
'/rsrc/favicons/favicon.ico'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$referrer_tag = phutil_tag(
|
|
||||||
'meta',
|
|
||||||
array(
|
|
||||||
'name' => 'referrer',
|
|
||||||
'content' => 'no-referrer',
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = CelerityAPI::getStaticResourceResponse();
|
$response = CelerityAPI::getStaticResourceResponse();
|
||||||
|
|
||||||
|
@ -136,13 +104,10 @@ class PhabricatorBarePageView extends AphrontPageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
return hsprintf(
|
return hsprintf(
|
||||||
'%s%s%s%s%s%s%s%s',
|
'%s%s%s%s%s',
|
||||||
$viewport_tag,
|
$viewport_tag,
|
||||||
$mask_icon,
|
$mask_icon,
|
||||||
$icon_tag_76,
|
$favicon_links,
|
||||||
$icon_tag_120,
|
|
||||||
$icon_tag_152,
|
|
||||||
$favicon_tag,
|
|
||||||
$referrer_tag,
|
$referrer_tag,
|
||||||
$response->renderResourcesOfType('css'));
|
$response->renderResourcesOfType('css'));
|
||||||
}
|
}
|
||||||
|
@ -156,4 +121,61 @@ class PhabricatorBarePageView extends AphrontPageView {
|
||||||
return $response->renderResourcesOfType('js');
|
return $response->renderResourcesOfType('js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function newFavicons() {
|
||||||
|
$favicon_refs = array(
|
||||||
|
array(
|
||||||
|
'rel' => 'apple-touch-icon',
|
||||||
|
'sizes' => '76x76',
|
||||||
|
'width' => 76,
|
||||||
|
'height' => 76,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'rel' => 'apple-touch-icon',
|
||||||
|
'sizes' => '120x120',
|
||||||
|
'width' => 120,
|
||||||
|
'height' => 120,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'rel' => 'apple-touch-icon',
|
||||||
|
'sizes' => '152x152',
|
||||||
|
'width' => 152,
|
||||||
|
'height' => 152,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'rel' => 'icon',
|
||||||
|
'id' => 'favicon',
|
||||||
|
'width' => 64,
|
||||||
|
'height' => 64,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$fetch_refs = array();
|
||||||
|
foreach ($favicon_refs as $key => $spec) {
|
||||||
|
$ref = id(new PhabricatorFaviconRef())
|
||||||
|
->setWidth($spec['width'])
|
||||||
|
->setHeight($spec['height']);
|
||||||
|
|
||||||
|
$favicon_refs[$key]['ref'] = $ref;
|
||||||
|
$fetch_refs[] = $ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
id(new PhabricatorFaviconRefQuery())
|
||||||
|
->withRefs($fetch_refs)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$favicon_links = array();
|
||||||
|
foreach ($favicon_refs as $spec) {
|
||||||
|
$favicon_links[] = phutil_tag(
|
||||||
|
'link',
|
||||||
|
array(
|
||||||
|
'rel' => $spec['rel'],
|
||||||
|
'sizes' => idx($spec, 'sizes'),
|
||||||
|
'id' => idx($spec, 'id'),
|
||||||
|
'href' => $spec['ref']->getURI(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $favicon_links;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,15 +23,29 @@ final class PhabricatorMainMenuView extends AphrontView {
|
||||||
return $this->controller;
|
return $this->controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getFaviconURI($type = null) {
|
private static function getFavicons() {
|
||||||
switch ($type) {
|
$refs = array();
|
||||||
case 'message':
|
|
||||||
return celerity_get_resource_uri('/rsrc/favicons/favicon-message.ico');
|
$refs['favicon'] = id(new PhabricatorFaviconRef())
|
||||||
case 'mention':
|
->setWidth(64)
|
||||||
return celerity_get_resource_uri('/rsrc/favicons/favicon-mention.ico');
|
->setHeight(64);
|
||||||
default:
|
|
||||||
return celerity_get_resource_uri('/rsrc/favicons/favicon.ico');
|
$refs['message_favicon'] = id(new PhabricatorFaviconRef())
|
||||||
}
|
->setWidth(64)
|
||||||
|
->setHeight(64)
|
||||||
|
->setEmblems(
|
||||||
|
array(
|
||||||
|
'dot-pink',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
));
|
||||||
|
|
||||||
|
id(new PhabricatorFaviconRefQuery())
|
||||||
|
->withRefs($refs)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
return mpull($refs, 'getURI');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
|
@ -428,10 +442,7 @@ final class PhabricatorMainMenuView extends AphrontView {
|
||||||
'countType' => $conpherence_data['countType'],
|
'countType' => $conpherence_data['countType'],
|
||||||
'countNumber' => $message_count_number,
|
'countNumber' => $message_count_number,
|
||||||
'unreadClass' => 'message-unread',
|
'unreadClass' => 'message-unread',
|
||||||
'favicon' => $this->getFaviconURI('default'),
|
) + self::getFavicons());
|
||||||
'message_favicon' => $this->getFaviconURI('message'),
|
|
||||||
'mention_favicon' => $this->getFaviconURI('mention'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$message_notification_dropdown = javelin_tag(
|
$message_notification_dropdown = javelin_tag(
|
||||||
'div',
|
'div',
|
||||||
|
@ -509,10 +520,7 @@ final class PhabricatorMainMenuView extends AphrontView {
|
||||||
'countType' => $notification_data['countType'],
|
'countType' => $notification_data['countType'],
|
||||||
'countNumber' => $count_number,
|
'countNumber' => $count_number,
|
||||||
'unreadClass' => 'alert-unread',
|
'unreadClass' => 'alert-unread',
|
||||||
'favicon' => $this->getFaviconURI('default'),
|
) + self::getFavicons());
|
||||||
'message_favicon' => $this->getFaviconURI('message'),
|
|
||||||
'mention_favicon' => $this->getFaviconURI('mention'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$notification_dropdown = javelin_tag(
|
$notification_dropdown = javelin_tag(
|
||||||
'div',
|
'div',
|
||||||
|
@ -594,10 +602,7 @@ final class PhabricatorMainMenuView extends AphrontView {
|
||||||
'countType' => null,
|
'countType' => null,
|
||||||
'countNumber' => null,
|
'countNumber' => null,
|
||||||
'unreadClass' => 'setup-unread',
|
'unreadClass' => 'setup-unread',
|
||||||
'favicon' => $this->getFaviconURI('default'),
|
) + self::getFavicons());
|
||||||
'message_favicon' => $this->getFaviconURI('message'),
|
|
||||||
'mention_favicon' => $this->getFaviconURI('mention'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$setup_notification_dropdown = javelin_tag(
|
$setup_notification_dropdown = javelin_tag(
|
||||||
'div',
|
'div',
|
||||||
|
|
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 6.4 KiB |