summaryrefslogtreecommitdiff
path: root/client/common
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2018-12-15 21:32:51 +0100
committerJules Laplace <julescarbon@gmail.com>2018-12-15 21:32:51 +0100
commite0b0b2f976c61225a178c7715caf2656a1f6741f (patch)
tree78a0e5c861462822d212c065f0825c906209bfe9 /client/common
parentc5b02ffab8d388e8a2925e51736b902a48a95e71 (diff)
moving stuff
Diffstat (limited to 'client/common')
-rw-r--r--client/common/activeLink.component.js16
-rw-r--r--client/common/classifier.component.js99
-rw-r--r--client/common/common.css347
-rw-r--r--client/common/detectionBoxes.component.js15
-rw-r--r--client/common/detectionList.component.js16
-rw-r--r--client/common/footer.component.js10
-rw-r--r--client/common/gate.component.js21
-rw-r--r--client/common/header.component.js1
-rw-r--r--client/common/index.js36
-rw-r--r--client/common/keyframe.component.js118
-rw-r--r--client/common/keyframes.component.js95
-rw-r--r--client/common/loader.component.js10
-rw-r--r--client/common/sidebar.component.js37
-rw-r--r--client/common/table.component.js121
-rw-r--r--client/common/video.component.js47
15 files changed, 989 insertions, 0 deletions
diff --git a/client/common/activeLink.component.js b/client/common/activeLink.component.js
new file mode 100644
index 00000000..59f63881
--- /dev/null
+++ b/client/common/activeLink.component.js
@@ -0,0 +1,16 @@
+import React from 'react'
+import { NavLink } from 'react-router-dom'
+
+export default function ActiveLink({
+ to,
+ className = 'navlink',
+ children
+}) {
+ return (
+ <span className={className}>
+ <NavLink to={to}>
+ {children}
+ </NavLink>
+ </span>
+ )
+}
diff --git a/client/common/classifier.component.js b/client/common/classifier.component.js
new file mode 100644
index 00000000..af6a4934
--- /dev/null
+++ b/client/common/classifier.component.js
@@ -0,0 +1,99 @@
+import React, { Component } from 'react'
+import { courtesyS } from '../util'
+
+import { TableTuples, DetectionList, Keyframe } from '.'
+
+export default class Classifier extends Component {
+ render() {
+ const {
+ tag,
+ sha256,
+ verified,
+ keyframes = {},
+ labels,
+ summary,
+ aspectRatio = 1.777,
+ showAll,
+ } = this.props
+ let totalDetections = 0
+ const keys = Object
+ .keys(keyframes)
+ .map(s => parseInt(s, 10))
+ const validKeyframes = keys
+ .sort((a, b) => a - b)
+ .map(frame => {
+ const detections = keyframes[frame]
+ if (detections.length || showAll) {
+ totalDetections += detections.length
+ return { frame, detections }
+ }
+ return null
+ })
+ .filter(f => !!f)
+ const detectionLookup = validKeyframes
+ .reduce((a, b) => {
+ b.detections.reduce((aa, { idx }) => {
+ if (!(idx in aa)) aa[idx] = [labels[idx], 0]
+ aa[idx][1] += 1
+ return aa
+ }, a)
+ return a
+ }, {})
+ const detectionCounts = Object.keys(detectionLookup)
+ .map(idx => detectionLookup[idx])
+ .sort((a, b) => b[1] - a[1])
+
+ if (summary) {
+ return (
+ <div>
+ <h3>{tag}{' Detections'}</h3>
+ <TableTuples
+ list={detectionCounts}
+ />
+ </div>
+ )
+ }
+ return (
+ <div>
+ <h2>{tag}</h2>
+ <h3>Detections</h3>
+ <TableTuples
+ list={detectionCounts}
+ />
+ <h3>Frames</h3>
+ <ul className='meta'>
+ <li>
+ {'Displaying '}{validKeyframes.length}{' / '}{courtesyS(keys.length, 'frame')}
+ </li>
+ <li>
+ {courtesyS(totalDetections, 'detection')}{' found'}
+ </li>
+ </ul>
+ <div className='thumbnails'>
+ {validKeyframes.map(({ frame, detections }) => (
+ <Keyframe
+ key={frame}
+ sha256={sha256}
+ frame={frame}
+ verified={verified}
+ size='th'
+ showFrame
+ showTimestamp
+ aspectRatio={aspectRatio}
+ detectionList={[
+ { labels, detections }
+ ]}
+ >
+ <DetectionList
+ labels={labels}
+ detections={detections}
+ width={160}
+ height={90}
+ />
+ </Keyframe>
+ ))}
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/client/common/common.css b/client/common/common.css
new file mode 100644
index 00000000..4b939df0
--- /dev/null
+++ b/client/common/common.css
@@ -0,0 +1,347 @@
+/* css boilerplate */
+
+* { box-sizing: border-box; }
+html,body {
+ margin: 0; padding: 0;
+ width: 100%; height: 100%;
+}
+body {
+ font-family: Helvetica, sans-serif;
+ font-weight: 300;
+}
+
+h1 {
+
+}
+h2 {
+ font-weight: normal;
+ margin: 10px 0;
+ padding: 3px;
+ font-size: 24px;
+}
+h3 {
+ font-weight: normal;
+ margin: 10px 0 0 0;
+ padding: 3px;
+ font-size: 18px;
+}
+h4 {
+ font-weight: 300;
+ font-size: 12px;
+ letter-spacing: 2px;
+ color: #888;
+ text-transform: uppercase;
+ margin: 5px 10px;
+ margin-top: 20px;
+}
+h4:first-child {
+ margin-top: 10px;
+}
+
+.app {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: flex-start;
+}
+
+/* header stuff */
+
+header {
+ width: 100%;
+ background: #11f;
+ color: white;
+ align-items: stretch;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ z-index: 3;
+}
+header > section {
+ justify-content: flex-start;
+ align-items: center;
+ display: flex;
+ flex: 1 0;
+ font-weight: bold;
+}
+header > section:last-of-type {
+ justify-content: flex-end;
+}
+
+/* sidebar / body columns */
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ height: 100%;
+ float: left;
+ width: 200px;
+ flex: 0 0 200px;
+ padding: 10px;
+ margin-right: 10px;
+}
+.sidebar a {
+ display: block;
+ padding: 10px 10px;
+ text-decoration: none;
+ color: #444;
+}
+.sidebar a.active {
+ font-weight: bold;
+ color: #222;
+}
+.body {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+ flex-grow: 1;
+}
+.body > div {
+ padding-bottom: 40px;
+}
+
+/* buttons / forms */
+
+.btn:focus, .btn:hover {
+ background: #f1f1fc;
+ color: #4b48d6 !important;
+ text-decoration: none;
+}
+.btn {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: #fff;
+ border: .05rem solid;
+ border-radius: 2px;
+ margin-right: 5px;
+ color: #11f;
+ cursor: pointer;
+ display: inline-block;
+ font-size: .8rem;
+ height: 1.8rem;
+ line-height: 1rem;
+ outline: none;
+ padding: .35rem .4rem;
+ text-align: center;
+ text-decoration: none;
+ -webkit-transition: all .2s ease;
+ -o-transition: all .2s ease;
+ transition: all .2s ease;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+.btn.reset,
+.btn.panic {
+ color: #b00;
+}
+.btn.btn-primary {
+ background: #11f;
+ border-color: #11f;
+ color: white;
+}
+.btn[disabled] {
+ color: #bbb !important;
+ border-color: #bbb !important;
+ background: white !important;
+ cursor: default;
+}
+.btn.btn-primary:focus,
+.btn.btn-primary:hover {
+ background: #0808ee;
+ color: white !important;
+}
+.row .btn {
+ margin: 0 5px 0 0;
+}
+input[type=text] {
+ border: 1px solid #888;
+ padding: 4px;
+ font-size: 15px;
+}
+
+
+/* tables on metadata pages */
+
+table {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ border-spacing: 0;
+}
+.tableObject td,
+.tableObject th {
+ padding: 3px;
+ vertical-align: top;
+}
+.tableObject hr {
+ width: 100%;
+ color: transparent;
+ border: 0;
+ border-bottom: 1px solid #bbb;
+ align: left;
+ margin: 3px 0;
+ padding: 0;
+}
+.tableObject th,
+.tableTuples th {
+ min-width: 145px;
+ text-align: left;
+ text-transform: capitalize;
+ padding: 3px;
+ padding-right: 10px;
+ font-weight: 300;
+ color: #333;
+}
+.tableTuples td {
+ text-align: right;
+ padding: 3px;
+}
+.tableObject td {
+ font-weight: normal;
+ color: #000;
+}
+.tableObject .tableObject {
+ border: 1px solid #ddd;
+}
+.tableArray {
+ border: 1px solid #ddd;
+ border-spacing: 0;
+}
+.tableArray td {
+ border-bottom: 1px solid #ddd;
+}
+.gray {
+ font-size: 12px;
+ color: #888;
+ display: block;
+}
+.sha256.heading {
+ margin: 20px 0 0px;
+}
+.gray span {
+ padding-right: 5px;
+}
+.gray {
+ margin-bottom: 10px;
+}
+.gray a {
+ color: #666;
+}
+
+.verified {
+ color: #080;
+ font-weight: bold;
+}
+.unverified {
+ color: #f00;
+ font-weight: 300;
+}
+
+.loading, .error {
+ font-weight: normal;
+ margin: 10px 0;
+ padding: 3px;
+ font-size: 24px;
+}
+
+.title {
+ text-transform: capitalize;
+}
+.rect {
+ position: absolute;
+}
+.rect { border: 1px solid rgba(0,0,255); background-color: rgba(0,0,255,0.1); }
+
+/* videos / video preloader */
+
+video {
+ max-width: 640px;
+ margin: 10px 0;
+}
+.video {
+ margin: 0 0 10px 0;
+}
+.video .bg {
+ cursor: pointer;
+ position: relative;
+ background-size: cover;
+}
+.video .play {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate3d(-50%, -50%, 0);
+ width: 20%;
+ height: 20%;
+ background-image: url(/search/static/img/play.png);
+ background-position: center center;
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+.desktop .video .play:hover {
+ -webkit-filter: invert(60%) sepia(100%) saturate(500%) hue-rotate(160deg);
+}
+
+/* spectre.css loader */
+
+.loaderWrapper {
+ display: inline-block;
+ position: relative;
+ width: .8rem;
+ height: .8rem;
+ padding: 10px;
+}
+.loader {
+ color: transparent !important;
+ min-height: .8rem;
+ pointer-events: none;
+ position: relative;
+}
+
+.loader::after {
+ animation: loader 500ms infinite linear;
+ border: .1rem solid #5755d9;
+ border-radius: 50%;
+ border-right-color: transparent;
+ border-top-color: transparent;
+ content: "";
+ display: block;
+ height: .8rem;
+ left: 50%;
+ margin-left: -.4rem;
+ margin-top: -.4rem;
+ position: absolute;
+ top: 50%;
+ width: .8rem;
+ z-index: 1;
+}
+
+.loader.loader-lg {
+ min-height: 2rem;
+}
+
+.loader.loader-lg::after {
+ height: 1.6rem;
+ margin-left: -.8rem;
+ margin-top: -.8rem;
+ width: 1.6rem;
+}
+
+@keyframes loader {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+} \ No newline at end of file
diff --git a/client/common/detectionBoxes.component.js b/client/common/detectionBoxes.component.js
new file mode 100644
index 00000000..c4872ea8
--- /dev/null
+++ b/client/common/detectionBoxes.component.js
@@ -0,0 +1,15 @@
+import React from 'react'
+
+import { px } from '../util'
+
+export default function DetectionBoxes({ detections, width, height }) {
+ return detections.map(({ rect }, i) => (
+ rect &&
+ <div className='rect' key={i} style={{
+ left: px(rect[0], width),
+ top: px(rect[1], height),
+ width: px(rect[2] - rect[0], width),
+ height: px(rect[3] - rect[1], height),
+ }} />
+ ))
+}
diff --git a/client/common/detectionList.component.js b/client/common/detectionList.component.js
new file mode 100644
index 00000000..416e66d8
--- /dev/null
+++ b/client/common/detectionList.component.js
@@ -0,0 +1,16 @@
+import React from 'react'
+
+export default function DetectionList({ detections, labels, tag, showEmpty }) {
+ return (
+ <span className='detectionList'>
+ {tag && <h3>{tag}</h3>}
+ {!detections.length && showEmpty && <label><small>No detections</small></label>}
+ {detections.map(({ idx, score, rect }, i) => (
+ <label key={i}>
+ <small className='title'>{(labels[idx] || 'Unknown').replace(/_/, ' ')}</small>
+ <small>{score.toFixed(2)}</small>
+ </label>
+ ))}
+ </span>
+ )
+}
diff --git a/client/common/footer.component.js b/client/common/footer.component.js
new file mode 100644
index 00000000..7c82b44b
--- /dev/null
+++ b/client/common/footer.component.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { connect } from 'react-redux'
+
+export default function Footer(props) {
+ return (
+ <footer>
+ </footer>
+ );
+}
diff --git a/client/common/gate.component.js b/client/common/gate.component.js
new file mode 100644
index 00000000..9bf9287b
--- /dev/null
+++ b/client/common/gate.component.js
@@ -0,0 +1,21 @@
+import React from 'react'
+import { connect } from 'react-redux'
+
+function Gate(props) {
+ const { app, tag, View } = props
+ const data = app[tag]
+ if (!data) return null
+ if (data === 'loading') {
+ return <div className='tableObject loading'>{tag}{': Loading'}</div>
+ }
+ if (data.err) {
+ return <div className='tableObject error'>{tag}{' Error: '}{data.err}</div>
+ }
+ return <View data={data} {...props} />
+}
+
+const mapStateToProps = state => ({
+ app: state.metadata
+})
+
+export default connect(mapStateToProps)(Gate)
diff --git a/client/common/header.component.js b/client/common/header.component.js
new file mode 100644
index 00000000..84fe306f
--- /dev/null
+++ b/client/common/header.component.js
@@ -0,0 +1 @@
+/* imported from main vcat application */
diff --git a/client/common/index.js b/client/common/index.js
new file mode 100644
index 00000000..ad9fe5e1
--- /dev/null
+++ b/client/common/index.js
@@ -0,0 +1,36 @@
+import Header from 'vcat-header'
+
+import ActiveLink from './activeLink.component'
+import Classifier from './classifier.component'
+import DetectionBoxes from './detectionBoxes.component'
+import DetectionList from './detectionList.component'
+// import Header from './header.component'
+import Footer from './footer.component'
+import Loader from './loader.component'
+import Sidebar from './sidebar.component'
+import Gate from './gate.component'
+import Keyframe from './keyframe.component'
+import Keyframes from './keyframes.component'
+import Video from './video.component'
+import { TableObject, TableArray, TableTuples, TableRow, TableCell } from './table.component'
+import './common.css'
+
+export {
+ Header,
+ Footer,
+ Sidebar,
+ Loader,
+ Gate,
+ TableObject,
+ TableArray,
+ TableTuples,
+ TableRow,
+ TableCell,
+ ActiveLink,
+ Classifier,
+ DetectionList,
+ DetectionBoxes,
+ Keyframe,
+ Keyframes,
+ Video,
+}
diff --git a/client/common/keyframe.component.js b/client/common/keyframe.component.js
new file mode 100644
index 00000000..c77db3ac
--- /dev/null
+++ b/client/common/keyframe.component.js
@@ -0,0 +1,118 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { imageUrl, timestamp, keyframeUri, widths, verify } from '../util'
+import { DetectionBoxes } from '.'
+
+import * as searchActions from '../search/search.actions'
+
+export default function Keyframe({
+ verified,
+ sha256,
+ frame,
+ score,
+ isSaved,
+ fps = 25,
+ size = 'th',
+ className,
+ showHash,
+ showFrame,
+ showTimestamp,
+ showScore,
+ showSearchButton,
+ showSaveButton,
+ to,
+ children,
+ detectionList = [],
+ aspectRatio = 1.777,
+ onClick,
+ reviewActions,
+}) {
+ if (!sha256) return null
+ const width = widths[size]
+ const height = Math.round(width / aspectRatio)
+ return (
+ <div className={(className || 'keyframe') + (isSaved ? ' isSaved' : '')}>
+ <div className="thumbnail">
+ <PossiblyExternalLink to={to || keyframeUri(sha256, frame)} onClick={onClick}>
+ <img
+ alt={'Frame #' + frame}
+ src={imageUrl(verified, sha256, frame, size)}
+ width={width}
+ height={height}
+ onClick={onClick}
+ />
+ {detectionList.map(({ labels, detections }, i) => (
+ <DetectionBoxes
+ key={i}
+ labels={labels}
+ detections={detections}
+ width={width}
+ height={height}
+ />
+ ))}
+ </PossiblyExternalLink>
+ {(reviewActions && (showSearchButton || showSaveButton)) &&
+ <label className='searchButtons'>
+ {showSearchButton &&
+ <Link
+ to={searchActions.publicUrl.searchByVerifiedFrame(verified, sha256, frame)}
+ className='btn'
+ >
+ Search
+ </Link>
+ }
+ {showSaveButton && (isSaved
+ ? <button
+ onClick={() => reviewActions.unsave({ hash: sha256, frame, verified })}
+ className={'btn btn-primary saved'}
+ >
+ {'Saved'}
+ </button>
+ : <button
+ onClick={() => reviewActions.save({ hash: sha256, frame, verified })}
+ className={'btn btn save'}
+ >
+ {'Save'}
+ </button>
+ )}
+ </label>
+ }
+ </div>
+ {(showHash || showFrame || showTimestamp || showScore) &&
+ <label>
+ {showHash &&
+ <small>
+ <Link to={searchActions.publicUrl.browse(sha256)}>
+ <span
+ title={sha256}
+ className={'sha256 ' + verify(verified)}
+ >
+ {'▶ '}
+ {sha256.substr(0, 6)}
+ </span>
+ </Link>
+ </small>
+ }
+ {showFrame &&
+ <small>
+ <span>{'Frame #'}{frame}</span>
+ </small>
+ }
+ {showTimestamp && <small>{timestamp(frame, fps)}</small>}
+ {showScore && !!score && <small>{score}</small>}
+ </label>
+ }
+ {children}
+ </div>
+ )
+}
+
+const PossiblyExternalLink = props => {
+ if (props.onClick) {
+ return props.children
+ }
+ if (props.to.match(/^http/)) {
+ return <a href={props.to} target='_blank' rel='noopener noreferrer'>{props.children}</a>
+ }
+ return <Link {...props} />
+}
diff --git a/client/common/keyframes.component.js b/client/common/keyframes.component.js
new file mode 100644
index 00000000..62eda45e
--- /dev/null
+++ b/client/common/keyframes.component.js
@@ -0,0 +1,95 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import { Keyframe } from '.'
+import * as reviewActions from '../review/review.actions'
+import * as searchActions from '../search/search.actions'
+
+function Keyframes(props) {
+ // console.log(props)
+ let {
+ frames,
+ groupByHash,
+ } = props
+ let minDistance = 0
+ if (frames && frames.length) {
+ minDistance = frames[0].distance || 0
+ }
+ if (!groupByHash) {
+ return (
+ <KeyframeList
+ minDistance={minDistance}
+ {...props}
+ />
+ )
+ }
+ const frameGroups = frames.reduce((a, b) => {
+ if (a[b.hash]) {
+ a[b.hash].push(b)
+ } else {
+ a[b.hash] = [b]
+ }
+ return a
+ }, {})
+ return Object.keys(frameGroups)
+ .map(hash => [frameGroups[hash].length, hash])
+ .sort((a, b) => b[0] - a[0])
+ .map(([count, hash]) => (
+ <KeyframeList
+ {...props}
+ count={count}
+ key={hash}
+ minDistance={minDistance}
+ frames={frameGroups[hash]}
+ label={hash}
+ />
+ ))
+}
+
+function KeyframeList(props) {
+ let {
+ saved = {},
+ frames,
+ options,
+ review,
+ search,
+ minDistance,
+ label,
+ count,
+ ...frameProps
+ } = props
+ if (!frames) return null
+ return (
+ <div className={label ? 'keyframes keyframeGroup' : 'keyframes'}>
+ {label && <h4><Link to={searchActions.publicUrl.browse(label)}>{label}</Link> ({count})</h4>}
+ {frames.map(({ hash, frame, verified, distance }) => (
+ <Keyframe
+ key={hash + '_' + frame}
+ sha256={hash}
+ frame={frame}
+ score={100 - Math.round(distance - minDistance) + '%'}
+ verified={verified}
+ isSaved={!!saved[hash] && !!saved[hash].frames && !!saved[hash].frames[parseInt(frame, 10)]}
+ size={options.thumbnailSize}
+ onClick={() => review.toggleSaved({ verified, hash, frame })}
+ reviewActions={review}
+ {...frameProps}
+ />
+ ))}
+ </div>
+ )
+}
+
+const mapStateToProps = state => ({
+ saved: state.review.saved,
+ options: state.search.options,
+})
+
+const mapDispatchToProps = dispatch => ({
+ review: bindActionCreators({ ...reviewActions }, dispatch),
+ search: bindActionCreators({ ...searchActions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(Keyframes)
diff --git a/client/common/loader.component.js b/client/common/loader.component.js
new file mode 100644
index 00000000..6795424b
--- /dev/null
+++ b/client/common/loader.component.js
@@ -0,0 +1,10 @@
+import React, { Component } from 'react'
+
+export default function Loader() {
+ return (
+ <div className='loaderWrapper'>
+ <div className='loader'>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/client/common/sidebar.component.js b/client/common/sidebar.component.js
new file mode 100644
index 00000000..487f3289
--- /dev/null
+++ b/client/common/sidebar.component.js
@@ -0,0 +1,37 @@
+import React, { Component } from 'react'
+import { NavLink } from 'react-router-dom'
+import { connect } from 'react-redux'
+
+class Sidebar extends Component {
+ render() {
+ const { hash } = this.props
+ if (!hash) {
+ return (
+ <div className="sidebar">
+ </div>
+ )
+ }
+ return (
+ <div className="sidebar">
+ <h4>Media</h4>
+ <NavLink to={'/metadata/' + hash + '/summary/'}>Summary</NavLink>
+ <NavLink to={'/metadata/' + hash + '/mediaRecord/'}>Media Record</NavLink>
+ <NavLink to={'/metadata/' + hash + '/mediaInfo/'}>Media Info</NavLink>
+ <NavLink to={'/metadata/' + hash + '/sugarcube/'}>Sugarcube</NavLink>
+
+ <h4>Keyframes</h4>
+ <NavLink to={'/metadata/' + hash + '/keyframe/'}>Keyframe</NavLink>
+
+ <h4>Detectors</h4>
+ <NavLink to={'/metadata/' + hash + '/places365/'}>Places 365</NavLink>
+ <NavLink to={'/metadata/' + hash + '/coco/'}>Coco</NavLink>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ hash: state.metadata.hash,
+})
+
+export default connect(mapStateToProps)(Sidebar)
diff --git a/client/common/table.component.js b/client/common/table.component.js
new file mode 100644
index 00000000..76a1d57c
--- /dev/null
+++ b/client/common/table.component.js
@@ -0,0 +1,121 @@
+import React from 'react'
+
+import { formatName } from '../util'
+
+const __HR__ = '__HR__'
+
+export function TableObject({ tag, object, order, summary }) {
+ if (!object) return null
+ if (object === 'loading') {
+ return <div className='tableObject loading'>{tag}{': Loading'}</div>
+ }
+ if (object.err) {
+ return <div className='tableObject error'>{tag}{' Error: '}{object.err}</div>
+ }
+ let objects = Object.keys(object)
+ if (order) {
+ const grouped = objects.reduce((a, b) => {
+ const index = order.indexOf(b)
+ if (index !== -1) {
+ a.order.push([index, b])
+ } else {
+ a.alpha.push(b)
+ }
+ return a
+ }, { order: [], alpha: [] })
+ objects = grouped.order
+ .sort((a, b) => a[0] - b[0])
+ .map(([i, s]) => s)
+ if (!summary) {
+ objects = objects
+ // .concat([__HR__])
+ .concat(grouped.alpha.sort())
+ }
+ } else {
+ objects = objects.sort()
+ }
+ return (
+ <div>
+ {tag && <h3>{tag}</h3>}
+ <table className={'tableObject ' + tag}>
+ <tbody>
+ {objects.map((key, i) => (
+ <TableRow key={key + '_' + i} name={key} value={object[key]} />
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )
+}
+
+export function TableArray({ tag, list }) {
+ if (!list) return null
+ return (
+ <div>
+ {tag && <h3>{tag}</h3>}
+ <table className={'tableArray ' + tag}>
+ <tbody>
+ {list.map((value, i) => (
+ <tr key={tag + '_' + i}>
+ <TableCell value={value} />
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )
+}
+
+export function TableTuples({ tag, list }) {
+ if (!list) return null
+ return (
+ <div>
+ {tag && <h3>{tag}</h3>}
+ <table className={'tableTuples ' + tag}>
+ <tbody>
+ {list.map(([key, ...values], i) => (
+ <tr key={tag + '_' + i}>
+ <th>{formatName(key)}</th>
+ {values.map((value, j) => (
+ <TableCell key={i + '_' + j} value={value} />
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )
+}
+
+export function TableRow({ name, value }) {
+ if (name === __HR__) {
+ return (
+ <tr>
+ <th className='tr'>
+ <hr />
+ </th>
+ </tr>
+ )
+ }
+ return (
+ <tr>
+ <th>{formatName(name)}</th>
+ <TableCell name={name} value={value} />
+ </tr>
+ )
+}
+
+export function TableCell({ value }) {
+ if (value && typeof value === 'object') {
+ if (value._raw) {
+ value = value.value
+ } else if (value.length) {
+ value = <TableArray nested tag={''} list={value} />
+ } else {
+ value = <TableObject nested tag={''} object={value} />
+ }
+ }
+ return (
+ <td>{value}</td>
+ )
+}
diff --git a/client/common/video.component.js b/client/common/video.component.js
new file mode 100644
index 00000000..e5525bf6
--- /dev/null
+++ b/client/common/video.component.js
@@ -0,0 +1,47 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { imageUrl, widths } from '../util'
+
+import { Gate } from '.'
+
+class Video extends Component {
+ state = {
+ playing: false,
+ }
+
+ render() {
+ const { app, data, size } = this.props
+ const { playing } = this.state
+ const { sugarcube } = data.metadata
+ const url = sugarcube.fp.replace('/var/www/files/', 'https://cube.syrianarchive.org/')
+ const { sha256, verified } = app.mediainfo
+ const { video } = app.mediainfo.metadata.mediainfo
+ const keyframe = app.keyframe.metadata.keyframe.basic[0]
+ return (
+ <div className='video'>
+ {playing
+ ? <video src={url} autoPlay controls muted />
+ : <div
+ className='bg'
+ style={{
+ width: widths[size || 'sm'],
+ height: widths[size || 'sm'] / video.aspect_ratio,
+ backgroundImage: 'url(' + imageUrl(verified, sha256, keyframe, size) + ')',
+ }}
+ onClick={() => this.setState({ playing: true })}
+ >
+ <div className='play'></div>
+ </div>
+ }
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = () => ({
+ tag: 'sugarcube',
+})
+
+export default connect(mapStateToProps)(props => (
+ <Gate View={Video} {...props} />
+))