Skip to content

Commit

Permalink
Save initial work
Browse files Browse the repository at this point in the history
  • Loading branch information
rsimmons committed Jun 7, 2016
0 parents commit 873bb7b
Show file tree
Hide file tree
Showing 12 changed files with 561 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 Russel Simmons

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
46 changes: 46 additions & 0 deletions NOTES.txt
@@ -0,0 +1,46 @@

Next steps:
- Reconsider how we store media/subs; rather than by language, might want just uids. Could mark whichever media/textChunks as 'native' vs. 'translation'
- Check style guide on curlies spacing, etc.
- Get eslint going?
- Display the computed current subs somewhere
- Jump-back button and kbd shortcut
- Un-halfwidth subs on import
- Display more summary info (sub tracks, media presence)
- Integrate JS-mecab
- Try JS-mecab on simple OCR output?
- Consume OCR result
- 0028.jpg is good, has errors/confusion
- Do we need to ignore summary result and go off pieces alone? Is summary result always the concat of pieces? Seems like we want to go through and make spans with uids, and make corresponding divs over image.

Keyboard shortcuts:
- general idea is to keep one hand in position on keyboard. let's say left hand, since most people mouse with right. and let's say home row, so asdf+space
- controls:
- SPACE: toggle play/pause for video, go to to next page for comic
- A: jump-back N seconds for video, go to previous page for comic
- S: ?
- D: toggle transcription
- F: toggle translation

What are controls?
- new document (of given type, language)
- import media
- import subs
- re-analyze current section?
- media nav (depends on document type, has keyboard shortcuts)
- comics
- forward/back pages
- video/audio
- pause/resume
- rewind, replay last section
- auto-pause checkbox

What contraints should hold for text?
- For a video, it seems that all text should be inside a timed chunk? And the times should be sequential (in start times at least, overlaps could be allowed). Times could also be nested perhaps(?), but then they should still be in order.
- For a comic, it seems that similarly all text should be inside a "page" chunk, and pages must be sequential. Pages could be skipped of course. Pages shouldn't nest.
- For just-text (novel, article), it doesn't seem like we have any basic structural constraints.
- For videos and comics and such, should match the media.

Could we allow videos and comics to have text outisde chunks/pages, but just emit warnings?

Should we wrap videos and comics and such in different top-level custom elements? Maybe just a data attribute or something to indicate which 'type' it is? Or do we just infer that by the top-level setting for the whole 'document'.
8 changes: 8 additions & 0 deletions README.md
@@ -0,0 +1,8 @@
# Immersion Player

Immersion Player (*needs a real name*) is a power tool for foreign language learners to get the most out of watching videos, readings comics, etc. It lets you easily:
- toggle the display of transcriptions/translations
- look up definitions of unknown words
- save excerpts for later SRS study
- (for video) quickly rewind to hear tricky speech again
- (for comics) extract text from images with OCR
46 changes: 46 additions & 0 deletions actions/index.js
@@ -0,0 +1,46 @@
import {parseSRT} from '../util/subtitles'

export const incrementCount = () => ({
type: 'incrementCount',
});

export const newDoc = (kind, language) => ({
type: 'newDoc',
kind,
language,
});

export const importVideoFile = (file) => ({
type: 'importVideoFile',
file,
});

export const importSubsParsed = (subChunks, language) => ({
type: 'importSubsParsed',
subChunks,
language,
});

export const importSubsFile = (file, language) => (
(dispatch) => { // return thunk
// Start async file load and parse
const reader = new FileReader();
reader.onload = (e) => {
// Parse loaded file data and dispatch action for result
dispatch(importSubsParsed(parseSRT(e.target.result), language));
}
reader.readAsText(file);
}
);

export const videoTimeUpdate = (time) => ({
type: 'videoTimeUpdate',
time,
});

export const controlBack = () => {

return {
type: 'controlBack',
}
};
194 changes: 194 additions & 0 deletions components/index.js
@@ -0,0 +1,194 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'

import { incrementCount, newDoc, importVideoFile, importSubsFile, videoTimeUpdate, controlBack } from '../actions'

// Counter
const Counter = connect(
(state) => ({ // mapStateToProps
value: state.count,
}),
(dispatch) => ({ // mapDispatchToProps
onIncreaseClick: () => dispatch(incrementCount()),
})
)(({value, onIncreaseClick}) => (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
));

const languageOptions = [
{value: 'ja', label: 'Japanese'},
{value: 'en', label: 'English'},
];

// Select, "uncontrolled" but watches changes
class Select extends Component {
componentWillMount() {
const {options, onSet} = this.props;
if (options.length > 0) {
onSet(options[0].value);
}
}

render() {
const {options, onSet} = this.props;
return (
<select onChange={e => onSet(e.target.value)}>
{options.map((o, i) => <option key={i} value={o.value}>{o.label}</option>)}
</select>
)
}
}

// NewDocForm
const NewDocForm = connect()(
class extends Component {
render() {
const kindOptions = [
{value: 'video', label: 'Video'},
{value: 'comic', label: 'Comic'},
];
return (
<form onSubmit={e => {
e.preventDefault();
this.props.dispatch(newDoc(this.kindVal, this.languageVal));
}}>
<Select options={kindOptions} onSet={v => { this.kindVal = v; }} />
<Select options={languageOptions} onSet={v => { this.languageVal = v; }} />
<button type="submit">New Document</button>
</form>
);
}
}
);

// FileChooserForm
const FileChooser = ({label, accept, onChoose}) => (
<label>{label} <input type="file" accept={accept} onChange={e => { onChoose(e.target.files[0]); e.target.value = null; }}/></label>
);

// VideoImportControls
class VideoImportControls extends Component {
render() {
const {dispatch} = this.props;
return (
<div>
<form>
<FileChooser label="Import Video" accept="video/*" onChoose={(file) => { dispatch(importVideoFile(file)); }} />
</form>
<form>
<FileChooser label="Import Subs (SRT)" accept=".srt" onChoose={(file) => { dispatch(importSubsFile(file, this.subLanguageVal)); }} />
<Select options={languageOptions} onSet={v => { this.subLanguageVal = v; }} />
</form>
</div>
);
}
}

class VideoMedia extends Component {
render() {
const {media, onTimeUpdate, mountedVideoElement} = this.props;
return (
<div>{ media ? (
<video src={media.videoURL} controls onTimeUpdate={e => {onTimeUpdate(e.target.currentTime)}} ref={(el) => { mountedVideoElement(el); }}/>
) : "No video media"
}</div>
);
}
}

class PlayControls extends Component {
constructor(props) {
super(props);
this.handleKeyDown = this.handleKeyDown.bind(this);
}

handleKeyDown(e) {
const {onBack, onTogglePause} = this.props;

// console.log(e);
if (!e.repeat) {
switch (e.keyCode) {
case 65: // a
onBack();
break;

case 32: // space
onTogglePause();
break;
}
}
}

componentDidMount() {
document.body.addEventListener('keydown', this.handleKeyDown);
}

componentWillUnmount() {
document.body.removeEventListener('keydown', this.handleKeyDown);
}

render() {
return null;
}
}

// Doc
class Doc extends Component {
constructor(props) {
super(props);
this.videoElement = null;
}

render() {
const {doc, dispatch} = this.props;
return (
<div>
<div>Kind: { doc.kind }, Language: { doc.language }</div>
<VideoImportControls dispatch={dispatch}/>
<VideoMedia media={doc.media} onTimeUpdate={time => { dispatch(videoTimeUpdate(time)); }} mountedVideoElement={(el) => { this.videoElement = el; }} />
<PlayControls dispatch={dispatch} onBack={
() => {
if (this.videoElement) {
const nt = this.videoElement.currentTime - 3.0;
this.videoElement.currentTime = nt >= 0 ? nt : 0;
}
}
} onTogglePause={
() => {
if (this.videoElement) {
if (this.videoElement.paused) {
this.videoElement.play();
} else {
this.videoElement.pause();
}
}
}
} />
<div>{JSON.stringify(doc.currentTextChunks)}</div>
</div>
);
}
}

// MaybeDoc
const MaybeDoc = connect(
(state) => ({
doc: state.doc,
})
)(({doc, dispatch}) => (
<div>{ doc ? <Doc doc={doc} dispatch={dispatch} /> : "No document" }</div>
));

// App
const App = () => (
<div>
<Counter />
<NewDocForm />
<MaybeDoc />
</div>
);

export default App;
10 changes: 10 additions & 0 deletions index.html
@@ -0,0 +1,10 @@
<html>
<head>
<title>Immersion Player</title>
<link href="main.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions index.js
@@ -0,0 +1,24 @@
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import ReduxThunk from 'redux-thunk'
import createReduxLogger from 'redux-logger'

import rootReducer from './reducers'
import RootComponent from './components'

const middlewares = [ReduxThunk];

if (process.env.NODE_ENV === 'development') {
middlewares.push(createReduxLogger());
}

const store = createStore(rootReducer, applyMiddleware(...middlewares));

render(
<Provider store={store}>
<RootComponent />
</Provider>,
document.getElementById('root')
);
8 changes: 8 additions & 0 deletions main.css
@@ -0,0 +1,8 @@
html, body {
margin: 0;
padding: 0;
}

body {
font-family: Helvetica, Arial, "Lucida Grande", sans-serif;
}

0 comments on commit 873bb7b

Please sign in to comment.