diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'c218ed53', + 'core.pkg.css' => '6a8ba174', 'core.pkg.js' => '8581cd02', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', @@ -168,7 +168,7 @@ 'rsrc/css/phui/phui-object-box.css' => '9cff003c', 'rsrc/css/phui/phui-pager.css' => 'edcbc226', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-property-list-view.css' => '2dc7993f', + 'rsrc/css/phui/phui-property-list-view.css' => '79fc3a02', 'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863', 'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892', 'rsrc/css/phui/phui-spacing.css' => '042804d6', @@ -848,7 +848,7 @@ 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', - 'phui-property-list-view-css' => '2dc7993f', + 'phui-property-list-view-css' => '79fc3a02', 'phui-remarkup-preview-css' => '54a34863', 'phui-segment-bar-view-css' => 'b1d1b892', 'phui-spacing-css' => '042804d6', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2066,6 +2066,7 @@ 'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php', + 'PhabricatorAudioDocumentEngine' => 'applications/files/document/PhabricatorAudioDocumentEngine.php', 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', 'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php', 'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php', @@ -2808,6 +2809,8 @@ 'PhabricatorDividerEditField' => 'applications/transactions/editfield/PhabricatorDividerEditField.php', 'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php', 'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php', + 'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php', + 'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php', 'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', @@ -3155,6 +3158,7 @@ 'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php', 'PhabricatorIconSetEditField' => 'applications/transactions/editfield/PhabricatorIconSetEditField.php', 'PhabricatorIconSetIcon' => 'applications/files/iconset/PhabricatorIconSetIcon.php', + 'PhabricatorImageDocumentEngine' => 'applications/files/document/PhabricatorImageDocumentEngine.php', 'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php', 'PhabricatorImageRemarkupRule' => 'applications/files/markup/PhabricatorImageRemarkupRule.php', 'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', @@ -4481,7 +4485,9 @@ 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', + 'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php', 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', + 'PhabricatorVoidDocumentEngine' => 'applications/files/document/PhabricatorVoidDocumentEngine.php', 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', 'PhabricatorWebContentSource' => 'infrastructure/contentsource/PhabricatorWebContentSource.php', 'PhabricatorWebServerSetupCheck' => 'applications/config/check/PhabricatorWebServerSetupCheck.php', @@ -7497,6 +7503,7 @@ 'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorAudioDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorAuditActionConstants' => 'Phobject', 'PhabricatorAuditApplication' => 'PhabricatorApplication', 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', @@ -8362,6 +8369,8 @@ 'PhabricatorDividerEditField' => 'PhabricatorEditField', 'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDivinerApplication' => 'PhabricatorApplication', + 'PhabricatorDocumentEngine' => 'Phobject', + 'PhabricatorDocumentRef' => 'Phobject', 'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', @@ -8756,6 +8765,7 @@ 'PhabricatorIconSet' => 'Phobject', 'PhabricatorIconSetEditField' => 'PhabricatorEditField', 'PhabricatorIconSetIcon' => 'Phobject', + 'PhabricatorImageDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageTransformer' => 'Phobject', @@ -10330,7 +10340,9 @@ 'PhabricatorVCSResponse' => 'AphrontResponse', 'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', + 'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorVoidDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorWebContentSource' => 'PhabricatorContentSource', 'PhabricatorWebServerSetupCheck' => 'PhabricatorSetupCheck', diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -23,6 +23,7 @@ } return id(new AphrontRedirectResponse())->setURI($file->getInfoURI()); } + $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withIDs(array($id)) @@ -62,31 +63,34 @@ $timeline = $this->buildTransactionView($file); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( - 'F'.$file->getID(), - $this->getApplicationURI("/info/{$phid}/")); + $file->getMonogram(), + $file->getInfoURI()); $crumbs->setBorder(true); $object_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('File')) + ->setHeaderText(pht('File Metadata')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); $this->buildPropertyViews($object_box, $file); $title = $file->getName(); + $file_content = $this->newFileContent($file); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) - ->setMainColumn(array( - $object_box, - $timeline, - )); + ->setMainColumn( + array( + $object_box, + $file_content, + $timeline, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($file->getPHID())) ->appendChild($view); - } private function buildTransactionView(PhabricatorFile $file) { @@ -325,61 +329,6 @@ $viewer->renderHandleList($phids)); } - if ($file->isViewableImage()) { - $image = phutil_tag( - 'img', - array( - 'src' => $file->getViewURI(), - 'class' => 'phui-property-list-image', - )); - - $linked_image = phutil_tag( - 'a', - array( - 'href' => $file->getViewURI(), - ), - $image); - - $media = id(new PHUIPropertyListView()) - ->addImageContent($linked_image); - - $box->addPropertyList($media); - } else if ($file->isVideo()) { - $video = phutil_tag( - 'video', - array( - 'controls' => 'controls', - 'class' => 'phui-property-list-video', - ), - phutil_tag( - 'source', - array( - 'src' => $file->getViewURI(), - 'type' => $file->getMimeType(), - ))); - $media = id(new PHUIPropertyListView()) - ->addImageContent($video); - - $box->addPropertyList($media); - } else if ($file->isAudio()) { - $audio = phutil_tag( - 'audio', - array( - 'controls' => 'controls', - 'class' => 'phui-property-list-audio', - ), - phutil_tag( - 'source', - array( - 'src' => $file->getViewURI(), - 'type' => $file->getMimeType(), - ))); - $media = id(new PHUIPropertyListView()) - ->addImageContent($audio); - - $box->addPropertyList($media); - } - $engine = $this->loadStorageEngine($file); if ($engine) { if ($engine->isChunkEngine()) { @@ -453,5 +402,52 @@ return $engine; } + private function newFileContent(PhabricatorFile $file) { + $viewer = $this->getViewer(); + $engines = PhabricatorDocumentEngine::getAllEngines(); + + $ref = id(new PhabricatorDocumentRef()) + ->setFile($file); + + foreach ($engines as $key => $engine) { + $engine = id(clone $engine) + ->setViewer($viewer); + + if (!$engine->canRenderDocument($ref)) { + unset($engines[$key]); + continue; + } + + $engines[$key] = $engine; + } + + if (!$engines) { + throw new Exception(pht('No engine can render this document.')); + } + + $vectors = array(); + foreach ($engines as $key => $usable_engine) { + $vectors[$key] = $usable_engine->newSortVector($ref); + } + $vectors = msortv($vectors, 'getSelf'); + + $engine = $engines[head_key($vectors)]; + + $content = $engine->newDocument($ref); + if (!$content) { + return null; + } + + $icon = $engine->newDocumentIcon($ref); + + $header = id(new PHUIHeaderView()) + ->setHeaderIcon($icon) + ->setHeader($ref->getName()); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header) + ->appendChild($content); + } } diff --git a/src/applications/files/document/PhabricatorAudioDocumentEngine.php b/src/applications/files/document/PhabricatorAudioDocumentEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorAudioDocumentEngine.php @@ -0,0 +1,61 @@ +getFile(); + if ($file) { + return $file->isAudio(); + } + + $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); + $viewable_types = array_keys($viewable_types); + + $audio_types = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); + $audio_types = array_keys($audio_types); + + return + $ref->hasAnyMimeType($viewable_types) && + $ref->hasAnyMimeType($audio_types); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + throw new PhutilMethodNotImplementedException(); + } + + $mime_type = $ref->getMimeType(); + + $audio = phutil_tag( + 'audio', + array( + 'controls' => 'controls', + ), + phutil_tag( + 'source', + array( + 'src' => $source_uri, + 'type' => $mime_type, + ))); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-audio', + ), + $audio); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorDocumentEngine.php @@ -0,0 +1,62 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function canRenderDocument(PhabricatorDocumentRef $ref) { + return $this->canRenderDocumentType($ref); + } + + abstract protected function canRenderDocumentType( + PhabricatorDocumentRef $ref); + + final public function newDocument(PhabricatorDocumentRef $ref) { + return $this->newDocumentContent($ref); + } + + final public function newDocumentIcon(PhabricatorDocumentRef $ref) { + return id(new PHUIIconView()) + ->setIcon($this->getDocumentIconIcon($ref)); + } + + abstract protected function newDocumentContent( + PhabricatorDocumentRef $ref); + + protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { + return 'fa-file-o'; + } + + final public function getDocumentEngineKey() { + return $this->getPhobjectClassConstant('ENGINEKEY'); + } + + final public static function getAllEngines() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getDocumentEngineKey') + ->execute(); + } + + final public function newSortVector(PhabricatorDocumentRef $ref) { + $content_score = $this->getContentScore($ref); + + return id(new PhutilSortVector()) + ->addInt(-$content_score); + } + + protected function getContentScore() { + return 2000; + } + +} diff --git a/src/applications/files/document/PhabricatorDocumentRef.php b/src/applications/files/document/PhabricatorDocumentRef.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorDocumentRef.php @@ -0,0 +1,93 @@ +file = $file; + return $this; + } + + public function getFile() { + return $this->file; + } + + public function setMimeType($mime_type) { + $this->mimeType = $mime_type; + return $this; + } + + public function getMimeType() { + if ($this->mimeType !== null) { + return $this->mimeType; + } + + if ($this->file) { + return $this->file->getMimeType(); + } + + return null; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + if ($this->name !== null) { + return $this->name; + } + + if ($this->file) { + return $this->file->getName(); + } + + return null; + } + + public function setByteLength($length) { + $this->byteLength = $length; + return $this; + } + + public function getLength() { + if ($this->byteLength !== null) { + return $this->byteLength; + } + + if ($this->file) { + return (int)$this->file->getByteSize(); + } + + return null; + } + + public function hasAnyMimeType(array $candidate_types) { + $mime_full = $this->getMimeType(); + $mime_parts = explode(';', $mime_full); + + $mime_type = head($mime_parts); + $mime_type = $this->normalizeMimeType($mime_type); + + foreach ($candidate_types as $candidate_type) { + if ($this->normalizeMimeType($candidate_type) === $mime_type) { + return true; + } + } + + return false; + } + + private function normalizeMimeType($mime_type) { + $mime_type = trim($mime_type); + $mime_type = phutil_utf8_strtolower($mime_type); + return $mime_type; + } + +} diff --git a/src/applications/files/document/PhabricatorImageDocumentEngine.php b/src/applications/files/document/PhabricatorImageDocumentEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorImageDocumentEngine.php @@ -0,0 +1,63 @@ +getFile(); + if ($file) { + return $file->isViewableImage(); + } + + $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); + $viewable_types = array_keys($viewable_types); + + $image_types = PhabricatorEnv::getEnvConfig('files.image-mime-types'); + $image_types = array_keys($image_types); + + return + $ref->hasAnyMimeType($viewable_types) && + $ref->hasAnyMimeType($image_types); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + // We could use a "data:" URI here. It's not yet clear if or when we'll + // have a ref but no backing file. + throw new PhutilMethodNotImplementedException(); + } + + $image = phutil_tag( + 'img', + array( + 'src' => $source_uri, + )); + + $linked_image = phutil_tag( + 'a', + array( + 'href' => $source_uri, + 'rel' => 'noreferrer', + ), + $image); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-image', + ), + $linked_image); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorVideoDocumentEngine.php b/src/applications/files/document/PhabricatorVideoDocumentEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorVideoDocumentEngine.php @@ -0,0 +1,61 @@ +getFile(); + if ($file) { + return $file->isVideo(); + } + + $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); + $viewable_types = array_keys($viewable_types); + + $video_types = PhabricatorEnv::getEnvConfig('files.video-mime-types'); + $video_types = array_keys($video_types); + + return + $ref->hasAnyMimeType($viewable_types) && + $ref->hasAnyMimeType($video_types); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + throw new PhutilMethodNotImplementedException(); + } + + $mime_type = $ref->getMimeType(); + + $video = phutil_tag( + 'video', + array( + 'controls' => 'controls', + ), + phutil_tag( + 'source', + array( + 'src' => $source_uri, + 'type' => $mime_type, + ))); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-video', + ), + $video); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorVoidDocumentEngine.php b/src/applications/files/document/PhabricatorVoidDocumentEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorVoidDocumentEngine.php @@ -0,0 +1,34 @@ + 'document-engine-message', + ), + $message); + + return $container; + } + +} diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -307,9 +307,14 @@ $icon = null; if ($this->headerIcon) { - $icon = id(new PHUIIconView()) - ->setIcon($this->headerIcon) - ->addClass('phui-header-icon'); + if ($this->headerIcon instanceof PHUIIconView) { + $icon = id(clone $this->headerIcon) + ->addClass('phui-header-icon'); + } else { + $icon = id(new PHUIIconView()) + ->setIcon($this->headerIcon) + ->addClass('phui-header-icon'); + } } $header_content = $this->header; diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -149,24 +149,6 @@ } -.phui-property-list-image { - margin: auto; - max-width: 95%; -} - -.phui-property-list-audio { - display: block; - margin: 16px auto; - width: 50%; - min-width: 240px; -} - -.phui-property-list-video { - display: block; - margin: 0 auto; - max-width: 95%; -} - /* When tags appear in property lists, give them a little more vertical spacing. */ .phui-property-list-value .phui-tag-view { @@ -220,3 +202,32 @@ border-right: 1px solid {$lightblueborder}; border-bottom: 1px solid {$blueborder}; } + + +.document-engine-image img { + margin: 20px auto; + background: url('/rsrc/image/checker_light.png'); +} + +.device-desktop .document-engine-image img:hover { + background: url('/rsrc/image/checker_dark.png'); +} + +.document-engine-video video { + margin: 20px auto; + display: block; + max-width: 95%; +} + +.document-engine-audio audio { + display: block; + margin: 16px auto; + width: 50%; + min-width: 240px; +} + +.document-engine-message { + margin: 20px auto; + text-align: center; + color: {$greytext}; +}