diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2019-04-28 15:54:41 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2019-04-28 15:54:41 +0200 |
| commit | aa5638a1c31ce56d59696580f33733dcf0d7764c (patch) | |
| tree | c2d1bd0e480b9bae113ce9af706927c5b7c55952 /client | |
| parent | a72ecc91db39ac5a2d60aefc6d767da457500dde (diff) | |
refactor frontend, add threshold slider
Diffstat (limited to 'client')
| -rw-r--r-- | client/actions.js | 27 | ||||
| -rw-r--r-- | client/app.js | 159 | ||||
| -rw-r--r-- | client/components/index.js | 11 | ||||
| -rw-r--r-- | client/components/query.component.js | 103 | ||||
| -rw-r--r-- | client/components/results.component.js | 56 | ||||
| -rw-r--r-- | client/components/timing.component.js | 13 | ||||
| -rw-r--r-- | client/components/upload.helpers.js (renamed from client/lib/upload.helpers.js) | 0 | ||||
| -rw-r--r-- | client/components/uploadImage.component.js (renamed from client/lib/uploadImage.component.js) | 0 | ||||
| -rw-r--r-- | client/index.js | 7 | ||||
| -rw-r--r-- | client/store.js | 61 | ||||
| -rw-r--r-- | client/types.js | 16 |
11 files changed, 303 insertions, 150 deletions
diff --git a/client/actions.js b/client/actions.js new file mode 100644 index 0000000..dfcfa09 --- /dev/null +++ b/client/actions.js @@ -0,0 +1,27 @@ +import { get, post } from './util' +import * as types from './types' + +export const api = (dispatch, method, tag, url, params, after) => { + dispatch({ type: types.api.loading, tag }) + return method(url, params).then(data => { + if (after) data = after(data) + dispatch({ type: types.api.loaded, tag, data }) + return data + }).catch(err => { + dispatch({ type: types.api.error, tag, err }) + }) +} + +export const upload = (blob, threshold) => dispatch => { + const params = new FormData() + params.append('q', blob) + params.append('threshold', threshold) + api(dispatch, post, 'similar', '/api/v1/similar', params) +} + +export const submit = (url, threshold) => dispatch => { + const params = new FormData() + params.append('url', url) + params.append('threshold', threshold) + api(dispatch, post, 'similar', '/api/v1/similar', params) +} diff --git a/client/app.js b/client/app.js index 8a07f34..c9a479c 100644 --- a/client/app.js +++ b/client/app.js @@ -1,155 +1,16 @@ import React, { Component } from 'react' -import UploadImage from './lib/uploadImage.component' -import { post } from './util' +import { Query, Results, Timing } from './components' import './app.css' -const initialState = { - image: null, - url: "", - res: null, - loading: false, +export default function App () { + return ( + <div className='app'> + <h1>Search by Image</h1> + <Query /> + <Results /> + <Timing /> + </div> + ) } - -export default class PhashApp extends Component { - state = { ...initialState } - - upload(blob) { - if (this.state.image) { - URL.revokeObjectURL(this.state.image) - } - const url = URL.createObjectURL(blob) - this.setState({ image: url, loading: true }) - - const fd = new FormData() - fd.append('q', blob) - post('/api/v1/match', fd) - .then(res => { - console.log(res) - this.setState({ res, loading: false }) - }) - .catch(err => { - console.log(err) - this.setState({ loading: false }) - }) - } - - submit() { - const { url } = this.state - if (!url || url.indexOf('http') !== 0) return - this.setState({ image: url, loading: true }) - - const fd = new FormData() - fd.append('url', url) - post('/api/v1/match', fd) - .then(res => { - console.log(res) - this.setState({ res, loading: false }) - }) - .catch(err => { - console.log(err) - this.setState({ loading: false }) - }) - } - - render() { - const { res } = this.state - return ( - <div className='app'> - <h1>Search by Image</h1> - {this.renderQuery()} - {this.renderResults()} - {this.renderTiming()} - </div> - ) - } - - renderQuery() { - const { image } = this.state - const style = {} - if (image) { - style.backgroundImage = 'url(' + image + ')' - style.backgroundSize = 'cover' - style.opacity = 1 - } - return ( - <div className='query'> - <label> - <span>Query image</span> - <UploadImage onUpload={this.upload.bind(this)} /> - </label> - <br /> - <label> - <span>Add a URL</span> - <input - type='text' - value={this.state.url} - onChange={e => this.setState({ url: e.target.value })} - onKeyDown={e => e.keyCode === 13 && this.submit()} - placeholder='https://' - /> - </label> - {image && <div style={style} />} - </div> - ) - } - renderResults() { - const { loading, res } = this.state - if (!res) { - return ( - <div className='results'> - </div> - ) - } - if (loading) { - return ( - <div className='results'> - <i>Loading...</i> - </div> - ) - } - - const { success, error, match, results } = res - if (!success) { - return ( - <div className='results'> - <b>Error: {error.replace(/_/g, ' ')}</b> - </div> - ) - } - - if (!match) { - return ( - <div className='results'> - No match, image added to database - </div> - ) - } - - return ( - <div className='results'> - {results.map(({ phash, score, sha256, url }) => ( - <div className='result' key={sha256}> - <div className='img'><img src={url} /></div> - <br /> - {score == 0 - ? <span className='score'><b>Exact match</b></span> - : <span className='score'>Score: {score}</span> - }<br /> - <span className='sha256'>{sha256}</span> - Phash: {phash.toString(16)} - </div> - ))} - </div> - ) - } - - renderTiming() { - const { res } = this.state - if (res && res.timing) { - return <small>Query completed in {Math.round(res.timing * 1000)} ms</small> - } - return null - } -}
\ No newline at end of file diff --git a/client/components/index.js b/client/components/index.js new file mode 100644 index 0000000..8c3f07a --- /dev/null +++ b/client/components/index.js @@ -0,0 +1,11 @@ +import Query from './query.component' +import Results from './results.component' +import Timing from './timing.component' +import UploadImage from './uploadImage.component' + +export { + Query, + Results, + Timing, + UploadImage +}
\ No newline at end of file diff --git a/client/components/query.component.js b/client/components/query.component.js new file mode 100644 index 0000000..bd98ce8 --- /dev/null +++ b/client/components/query.component.js @@ -0,0 +1,103 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from '../actions' + +import UploadImage from './uploadImage.component' +class Query extends Component { + state = { + image: null, + blob: null, + url: "", + threshold: 20, + } + + handleUpload(blob) { + const { image, threshold } = this.state + if (image) { + URL.revokeObjectURL(image) + } + const url = URL.createObjectURL(blob) + this.setState({ image: url, blob }) + this.props.actions.upload(blob, threshold) + } + + handleURL() { + const { url, threshold } = this.state + if (!url || url.indexOf('http') !== 0) return + this.setState({ image: url, blob: null }) + this.props.actions.submit(url, threshold) + } + + resubmit() { + const { image, blob } = this.state + if (blob) { + this.handleUpload(blob) + } else { + this.handleURL() + } + } + + render() { + const { url, image, threshold } = this.state + const style = {} + if (image) { + style.maxWidth = 200 + style.maxHeight = 200 + style.backgroundImage = 'url(' + image + ')' + style.backgroundSize = 'cover' + style.opacity = 1 + } + return ( + <div className='query'> + <label> + <span>Query image</span> + <UploadImage onUpload={this.handleUpload.bind(this)} /> + </label> + {/* + <label> + <span>Add a URL</span> + <input + type='text' + value={url} + onChange={e => this.setState({ url: e.target.value })} + onKeyDown={e => e.keyCode === 13 && this.handleURL()} + placeholder='https://' + /> + </label> + */} + <label> + <span>Threshold</span> + <input + type='number' + value={threshold} + min={0} + max={64} + step={1} + onChange={e => this.setState({ threshold: parseInt(e.target.value) }) } + /> + <input + type='range' + value={threshold} + min={0} + max={64} + step={1} + onChange={e => this.setState({ threshold: parseInt(e.target.value) }) } + /> + <button onClick={this.resubmit}>Update</button> + </label> + {image && <div className='image' style={style} />} + </div> + ) + } +} + +const mapStateToProps = state => ({ + ...state.api, +}) +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(Query) diff --git a/client/components/results.component.js b/client/components/results.component.js new file mode 100644 index 0000000..a6f3052 --- /dev/null +++ b/client/components/results.component.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +function Results({ loading, res }) { + if (!res) { + return ( + <div className='results'> + </div> + ) + } + if (loading) { + return ( + <div className='results'> + <i>Loading...</i> + </div> + ) + } + + const { success, error, match, results } = res + if (!success) { + return ( + <div className='results'> + <b>Error: {error.replace(/_/g, ' ')}</b> + </div> + ) + } + + if (!match) { + return ( + <div className='results'> + No match, image added to database + </div> + ) + } + + return ( + <div className='results'> + {results.map(({ phash, score, sha256, url }) => ( + <div className='result' key={sha256}> + <div className='img'><img src={url} /></div> + <br /> + {score == 0 + ? <span className='score'><b>Exact match</b></span> + : <span className='score'>Score: {score}</span> + }<br /> + <span className='sha256'>{sha256}</span> + Phash: {phash.toString(16)} + </div> + ))} + </div> + ) +} + +const mapStateToProps = state => state.api + +export default connect(mapStateToProps)(Results) diff --git a/client/components/timing.component.js b/client/components/timing.component.js new file mode 100644 index 0000000..7473a51 --- /dev/null +++ b/client/components/timing.component.js @@ -0,0 +1,13 @@ +import React from 'react' +import { connect } from 'react-redux' + +function Timing({ timing }) { + if (timing) { + return <small>Query completed in {Math.round(timing * 1000)} ms</small> + } + return null +} + +const mapStateToProps = state => state.api + +export default connect(mapStateToProps)(Timing) diff --git a/client/lib/upload.helpers.js b/client/components/upload.helpers.js index d0baab4..d0baab4 100644 --- a/client/lib/upload.helpers.js +++ b/client/components/upload.helpers.js diff --git a/client/lib/uploadImage.component.js b/client/components/uploadImage.component.js index 690c0dc..690c0dc 100644 --- a/client/lib/uploadImage.component.js +++ b/client/components/uploadImage.component.js diff --git a/client/index.js b/client/index.js index 1a71e55..b9d3a1a 100644 --- a/client/index.js +++ b/client/index.js @@ -1,8 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' import App from './app' +import { store, history } from './store' + ReactDOM.render( - <App />, document.querySelector('#container') + <Provider store={store}> + <App history={history} /> + </Provider>, document.querySelector('#container') ) diff --git a/client/store.js b/client/store.js new file mode 100644 index 0000000..c5f5555 --- /dev/null +++ b/client/store.js @@ -0,0 +1,61 @@ +import { applyMiddleware, compose, combineReducers, createStore } from 'redux' +import { connectRouter, routerMiddleware } from 'connected-react-router' +import { createBrowserHistory } from 'history' +import thunk from 'redux-thunk' +import * as types from './types' + +const initialState = () => ({ + api: null, +}) + +export default function apiReducer(state = initialState(), action) { + // console.log(action.type, action) + switch (action.type) { + case types.api.loading: + return { + ...state, + [action.tag]: { loading: true }, + } + + case types.api.loaded: + return { + ...state, + [action.tag]: action.data, + } + + case types.api.error: + return { + ...state, + [action.tag]: { error: action.err }, + } + + default: + return state + } +} + +const rootReducer = combineReducers({ + api: apiReducer, +}) + +function configureStore(initialState = {}, history) { + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose + + const store = createStore( + connectRouter(history)(rootReducer), // new root reducer with router state + initialState, + composeEnhancers( + applyMiddleware( + thunk, + routerMiddleware(history) + ), + ), + ) + + return store +} + +const history = createBrowserHistory() +const store = configureStore({}, history) + +export { store, history } diff --git a/client/types.js b/client/types.js new file mode 100644 index 0000000..6311816 --- /dev/null +++ b/client/types.js @@ -0,0 +1,16 @@ +export const asType = (type, name) => [type, name].join('_').toUpperCase() +export const tagAsType = (type, names) => ( + names.reduce((tags, name) => { + tags[name] = asType(type, name) + return tags + }, {}) +) + +export const api = tagAsType('api', [ + 'loading', 'loaded', 'error', +]) + +export const system = tagAsType('system', [ +]) + +export const init = '@@INIT' |
