summaryrefslogtreecommitdiff
path: root/app/client/common
diff options
context:
space:
mode:
Diffstat (limited to 'app/client/common')
-rw-r--r--app/client/common/currentTask.component.js2
-rw-r--r--app/client/common/fileList.component.js2
-rw-r--r--app/client/common/fileViewer.component.js87
-rw-r--r--app/client/common/index.js5
-rw-r--r--app/client/common/taskList.component.js2
-rw-r--r--app/client/common/timeline.component.js166
6 files changed, 232 insertions, 32 deletions
diff --git a/app/client/common/currentTask.component.js b/app/client/common/currentTask.component.js
index ef976bc..c053a13 100644
--- a/app/client/common/currentTask.component.js
+++ b/app/client/common/currentTask.component.js
@@ -31,7 +31,7 @@ function CurrentTask ({ cpu, gpu, processor }) {
? <span>(currently #{epoch})</span>
: ""}
<br/><br/>
- <div className='quiet'>{last_message}</div>
+ <div className='quiet lastMessage'>{last_message}</div>
</div>
)
}
diff --git a/app/client/common/fileList.component.js b/app/client/common/fileList.component.js
index 8f79148..12f6865 100644
--- a/app/client/common/fileList.component.js
+++ b/app/client/common/fileList.component.js
@@ -119,7 +119,7 @@ export const FileRow = props => {
<div className={"epoch " + util.hush_null(epoch)[0]}>{epoch > 0 ? 'ep. ' + epoch : ''}</div>
}
{fields.has('date') &&
- <div className={"date " + util.carbon_date(date)}>{moment(date).format("YYYY-MM-DD")}</div>
+ <div className={"date " + util.carbon_date(date)} title={moment(date).format("YYYY-MM-DD HH:mm")}>{moment(date).format("YYYY-MM-DD")}</div>
}
{fields.has('datetime') &&
<div className={"datetime"}>
diff --git a/app/client/common/fileViewer.component.js b/app/client/common/fileViewer.component.js
index d98073b..4938650 100644
--- a/app/client/common/fileViewer.component.js
+++ b/app/client/common/fileViewer.component.js
@@ -21,11 +21,37 @@ const video_types = {
'mp4': 'video/mp4',
}
+const THROTTLE_FETCH_TIME = 200
+
class FileViewer extends Component {
state = {
loading: false,
stale: false,
- buffer: {}
+ buffer: {},
+ url: null,
+ }
+
+ componentDidMount(){
+ this.fetch()
+ }
+
+ componentDidUpdate(prevProps){
+ if (this.props.file !== prevProps.file) {
+ this.deferFetch()
+ }
+ }
+
+ componentWillUnmount(){
+ if (this.state.url) {
+ window.URL.revokeObjectURL(this.state.url)
+ }
+ }
+
+ deferFetch(){
+ clearTimeout(this.timeout)
+ this.timeout = setTimeout(() => {
+ this.fetch()
+ }, THROTTLE_FETCH_TIME)
}
fetch() {
@@ -40,48 +66,56 @@ class FileViewer extends Component {
this.setState({ buffer: null, loading: true })
if (thumbnail) {
- console.log('fetch thumbnail', fn)
+ // console.log('fetch thumbnail', fn)
const size = parseInt(thumbnail) || 200
actions.socket
.thumbnail({ module, fn, size })
.then(this.loadBuffer.bind(this))
} else {
- console.log('fetch file', fn)
+ // console.log('fetch file', fn)
actions.socket
.read_file({ module, fn })
.then(this.loadBuffer.bind(this))
}
}
+
loadBuffer(buffer) {
- console.log('fetched buffer', buffer)
+ // console.log('fetched buffer', buffer)
+ const { name, buf } = buffer
+ const ext = extension(name)
+ if (this.state.url) {
+ window.URL.revokeObjectURL(this.state.url)
+ }
+ let url
+ if (buf) {
+ if (ext in image_types) {
+ url = getURLFor(buf, image_types[ext])
+ } else if (ext in audio_types) {
+ url = getURLFor(buf, audio_types[ext])
+ } else if (ext in video_types) {
+ url = getURLFor(buf, video_types[ext])
+ } else {
+ url = ab2str(buf)
+ }
+ }
const { stale } = this.state
- this.setState({ buffer, loading: false, stale: false, }, () => {
- console.log('loaded')
+ this.setState({ ext, url, buffer, loading: false, stale: false, }, () => {
+ // console.log('loaded')
if (stale) {
- console.log('stale, fetching...')
+ // console.log('stale, fetching...')
this.fetch()
}
})
}
- componentDidMount(){
- this.fetch()
- }
-
- componentDidUpdate(nextProps){
- if (this.props.file !== nextProps.file) {
- this.fetch()
- }
- }
-
render() {
const { file } = this.props
if (!file) {
return <div className='fileViewer'></div>
}
- const { loading, buffer } = this.state
+ const { loading, buffer, url, ext } = this.state
if (loading) {
- return <div className='fileViewer'>Loading...</div>
+ return <div className='fileViewer'><span>Loading...</span></div>
}
const {
error,
@@ -90,24 +124,23 @@ class FileViewer extends Component {
buf,
} = buffer
if (error) {
- return <div className='fileViewer'>{error}</div>
+ return <div className='fileViewer'><span>{error}</span></div>
}
if (!name) {
return <div className='fileViewer'></div>
}
- if (!buf) {
- return <div className='fileViewer'>File empty</div>
+ if (!buf || !url) {
+ return <div className='fileViewer'><span>File empty</span></div>
}
- const ext = extension(name)
let tag;
if (ext in image_types) {
- tag = <img src={getURLFor(buf, image_types[ext])} />
+ tag = <img src={url} />
} else if (ext in audio_types) {
- tag = <audio src={getURLFor(buf, audio_types[ext])} controls autoplay />
+ tag = <audio src={url} controls autoplay />
} else if (ext in video_types) {
- tag = <video src={getURLFor(buf, video_types[ext])} controls autoplay />
+ tag = <video src={url} controls autoplay />
} else {
- tag = <div className='text'>{ab2str(buf)}</div>
+ tag = <div className='text'>{url}</div>
}
return (
<div className='fileViewer'>{tag}</div>
diff --git a/app/client/common/index.js b/app/client/common/index.js
index e120597..15511fd 100644
--- a/app/client/common/index.js
+++ b/app/client/common/index.js
@@ -21,8 +21,9 @@ import Progress from './progress.component'
import Select from './select.component'
import SelectGroup from './selectGroup.component'
import Slider from './slider.component'
-import TextInput from './textInput.component'
import TaskList from './taskList.component'
+import TextInput from './textInput.component'
+import Timeline from './timeline.component'
import * as Views from './views'
export {
@@ -34,5 +35,5 @@ export {
TextInput, NumberInput,
Slider, Select, SelectGroup, Button, Checkbox,
CurrentTask, TaskList,
- ButtonGrid, AugmentationGrid,
+ ButtonGrid, AugmentationGrid, Timeline,
} \ No newline at end of file
diff --git a/app/client/common/taskList.component.js b/app/client/common/taskList.component.js
index 272ff80..a2f1df0 100644
--- a/app/client/common/taskList.component.js
+++ b/app/client/common/taskList.component.js
@@ -59,7 +59,7 @@ class TaskList extends Component {
<div className='row'>
<div className='activity'>{task.activity} {task.module}</div>
<div className='dataset'>{dataset_link}</div>
- <div className={"age " + util.carbon_date(task.updated_at)}>{util.get_age(task.updated_at)}</div>
+ <div className={"age " + util.carbon_date(task.updated_at)} title={task.updated_at}>{util.get_age(task.updated_at)}</div>
<div className='options'>
<span className='destroy' onClick={() => this.handleDestroy(task)}>x</span>
</div>
diff --git a/app/client/common/timeline.component.js b/app/client/common/timeline.component.js
new file mode 100644
index 0000000..d3739b1
--- /dev/null
+++ b/app/client/common/timeline.component.js
@@ -0,0 +1,166 @@
+import { h, Component } from 'preact'
+import { clamp, norm } from '../util/math'
+
+const initialState = {
+ dir: '/',
+ start: null,
+ end: null,
+ cursor: null,
+ selection: null,
+ width: 0,
+ ratio: 0,
+ loading: true
+}
+
+export default class Timeline extends Component {
+ state = { ...initialState }
+
+ constructor() {
+ super()
+ this.handleMouseDown = this.handleMouseDown.bind(this)
+ this.handleMouseMove = this.handleMouseMove.bind(this)
+ this.handleMouseUp = this.handleMouseUp.bind(this)
+ this.computeOffset = this.computeOffset.bind(this)
+ }
+
+ componentDidMount() {
+ const { sequence } = this.props
+ window.addEventListener('resize', this.computeOffset)
+ window.addEventListener('mousemove', this.handleMouseMove)
+ window.addEventListener('mouseup', this.handleMouseUp)
+ this.computeOffset()
+ if (sequence) {
+ this.reset()
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { sequence } = this.props
+ if (sequence !== prevProps.sequence) {
+ this.reset()
+ }
+ }
+
+ componentWillUnmount(){
+ window.removeEventListener('resize', this.computeOffset)
+ window.removeEventListener('mousemove', this.handleMouseMove)
+ window.removeEventListener('mouseup', this.handleMouseUp)
+ }
+
+ reset(){
+ const { sequence } = this.props
+ if (!sequence || !sequence.length) return
+ const len = sequence.length - 1
+ const start = { frame: sequence[0], i: 0 }
+ const end = { frame: sequence[len], i: len }
+ const width = Math.sqrt(clamp(sequence.length / 15000, 0.3, 1.0)) * Math.min(window.innerWidth - 40, 1000)
+ const ratio = width / sequence.length
+ setTimeout(() => this.computeOffset())
+ this.setState({
+ ...initialState,
+ width,
+ ratio,
+ start,
+ end,
+ })
+ }
+
+ computeOffset(){
+ if (this.ref) {
+ this.offset = this.ref.getBoundingClientRect()
+ }
+ }
+
+ computeFrame(e){
+ const { sequence } = this.props
+ if (!sequence || !sequence.length) return null
+ let x = (e.pageX - this.offset.left) / this.offset.width
+ let y = (e.pageY - this.offset.top) / this.offset.height
+ if (this.state.dragging) {
+ x = clamp(x, 0, 1)
+ y = clamp(y, 0, 1)
+ }
+ if (0 <= x && x <= 1 && 0 <= y && y <= 1) {
+ const index = Math.floor(x * (sequence.length-1))
+ return { frame: sequence[index], i: index }
+ }
+ return null
+ }
+
+ handleMouseDown(e) {
+ this.setState({ dragging: true })
+ const frame = this.computeFrame(e)
+ if (frame) {
+ this.props.onPick && this.props.onPick(frame)
+ this.props.onSelect && this.props.onSelect({ start: frame, end: frame })
+ this.setState({
+ start: frame,
+ end: frame,
+ })
+ }
+ }
+
+ handleMouseMove(e) {
+ const frame = this.computeFrame(e)
+ if (frame) {
+ this.props.onCursor && this.props.onCursor(frame)
+ if (this.state.dragging) {
+ let start = this.state.start
+ let end = frame
+ this.setState({
+ cursor: frame,
+ end: frame,
+ })
+ if (this.props.onSelect) {
+ if (end.i < start.i) {
+ [start, end] = [end, start]
+ }
+ this.props.onSelect({ start, end })
+ }
+ } else {
+ this.setState({
+ cursor: frame
+ })
+ }
+ }
+ }
+
+ handleMouseUp(e) {
+ if (!this.state.dragging) return
+ let { start, end } = this.state
+ if (end.i < start.i) {
+ [start, end] = [end, start]
+ }
+ this.props.onSelect && this.props.onSelect({ start, end })
+ this.setState({ dragging: false })
+ }
+
+ render() {
+ const { sequence } = this.props
+ const { loading, start, end, cursor, width, ratio } = this.state
+ return (
+ <div
+ className='timeline'
+ style={{ width }}
+ ref={ref => this.ref = ref}
+ onMouseDown={this.handleMouseDown}
+ >
+ {ratio && start && end && this.renderSelection(start, end, ratio)}
+ {ratio && cursor && this.renderCursor(cursor, ratio)}
+ </div>
+ )
+ }
+ renderCursor(cursor, ratio){
+ const left = cursor.i * ratio
+ return (
+ <div key='cursor' className='cursor' style={{ left }} />
+ )
+ }
+ renderSelection(start, end, ratio){
+ const left = Math.min(start.i, end.i) * ratio
+ const width = Math.max(Math.abs(start.i - end.i) * ratio, 1)
+ return (
+ <div key='selection' className='selection' style={{ left, width }} />
+ )
+ }
+}