diff --git a/package.json b/package.json index 7130b8b8..87685b24 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,14 @@ "name": "pttchrome", "version": "1.2.0", "dependencies": { + "base58": "^1.0.1", "classnames": "^2.2.5" }, "devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.24.1", "cross-env": "^5.1.0", @@ -39,9 +42,11 @@ "modules": false } ], - [ - "react" - ] + ["react"] + ], + "plugins": [ + "transform-class-properties", + "transform-object-rest-spread" ] }, "production": { @@ -52,9 +57,11 @@ "modules": false } ], - [ - "react" - ] + ["react"] + ], + "plugins": [ + "transform-class-properties", + "transform-object-rest-spread" ] } } diff --git a/src/components/ImagePreviewer.js b/src/components/ImagePreviewer.js new file mode 100644 index 00000000..f015d387 --- /dev/null +++ b/src/components/ImagePreviewer.js @@ -0,0 +1,207 @@ +import { stringify } from "querystring"; +import { decode } from "base58"; + +const noop = () => {}; + +export const of = src => Promise.resolve({ src }); + +export const resolveSrcToImageUrl = ({ src }) => + imageUrlResolvers.find(r => r.test(src)).request(src); + +export const resolveWithImageDOM = ({ src }) => + new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => + resolve({ + src, + height: img.height + }); + img.onerror = reject; + img.src = src; + }); + +export class ImagePreviewer extends React.PureComponent { + state = { + pending: undefined, + value: undefined, + error: undefined + }; + + componentDidMount() { + this.handleStart(); + } + + componentDidUpdate(prevProps) { + if (this.props.request !== prevProps.request) { + this.handleStart(); + } + } + + handleStart(props) { + this.setState((state, { request }) => { + request.then(this.handleResolve, this.handleReject); + return { + pending: request, + value: undefined, + error: undefined + }; + }); + } + + handleResolve = value => { + this.setState(({ pending }, { request }) => { + if (pending !== request) { + return; + } + return { value }; + }); + }; + + handleReject = error => { + this.setState(({ pending }, { request }) => { + if (pending !== request) { + return; + } + return { error }; + }); + }; + + render() { + return React.createElement(this.props.component, { + ...this.props, + component: undefined, + request: undefined, + value: this.state.value, + error: this.state.error + }); + } +} + +const getTop = (top, height) => { + const pageHeight = $(window).height(); + + // opening image would pass the bottom of the page + if (top + height / 2 > pageHeight - 20) { + if (height / 2 < top) { + return pageHeight - 20 - height; + } + } else if (top - 20 > height / 2) { + return top - height / 2; + } + return 20; +}; + +ImagePreviewer.OnHover = ({ left, top, value, error }) => { + if (error) { + return false; + } else if (value) { + return ( + + ); + } else { + return ( + + ); + } +}; + +ImagePreviewer.Inline = ({ value, error }) => { + if (error) { + return false; + } else if (value) { + return ; + } else { + return ( + + ); + } +}; + +const imageUrlResolvers = [ + { + /* + * Default + */ + test() { + return true; + }, + request() { + return Promise.reject(new Error("Unimplemented")); + } + } +]; + +const registerImageUrlResolver = imageUrlResolvers.unshift.bind( + imageUrlResolvers +); + +registerImageUrlResolver({ + /* + * Flic.kr + */ + regex: /flic\.kr\/p\/(\w+)|flickr\.com\/photos\/[\w@]+\/(\d+)/, + test(src) { + return this.regex.test(src); + }, + request(src) { + const [, flickrBase58Id, flickrPhotoId] = src.match(this.regex); + const photoId = flickrBase58Id ? decode(flickrBase58Id) : flickrPhotoId; + + const apiURL = `https://api.flickr.com/services/rest/?${stringify({ + method: "flickr.photos.getInfo", + api_key: "c8c95356e465b8d7398ff2847152740e", + photo_id: photoId, + format: "json", + nojsoncallback: 1 + })}`; + return fetch(apiURL, { + mode: "cors" + }) + .then(r => r.json()) + .then(data => { + if (!data.photo) { + throw new Error("Not found"); + } + const { farm, server: svr, id, secret } = data.photo; + return { + src: `https://farm${farm}.staticflickr.com/${svr}/${id}_${secret}.jpg` + }; + }); + } +}); + +registerImageUrlResolver({ + /* + * imgur.com + */ + regex: /^https?:\/\/(i\.)?imgur\.com/, + test(src) { + return this.regex.test(src); + }, + request(src) { + return Promise.resolve({ + src: `${src.replace(this.regex, "https://i.imgur.com")}.jpg` + }); + } +}); + +export default ImagePreviewer; diff --git a/src/components/Row/HyperLink.js b/src/components/Row/HyperLink.js index cd7d2b0a..598a5a02 100644 --- a/src/components/Row/HyperLink.js +++ b/src/components/Row/HyperLink.js @@ -1,5 +1,14 @@ -export const HyperLink = ({ col, row, href, inner }) => ( +export const HyperLink = ({ + col, + row, + href, + inner, + onMouseOver, + onMouseOut +}) => ( ); // TODO: Modularize this. - if (this.linkPreviews) { - this.linkPreviews.push( - + if (this.inlineLinkPreviews) { + this.inlineLinkPreviews.push( + ); } } else { @@ -61,14 +76,13 @@ export class LinkSegmentBuilder { return (
{this.segs} -
{this.linkPreviews}
+
{this.inlineLinkPreviews}
); } diff --git a/src/components/Row/index.js b/src/components/Row/index.js index ed878878..23849a16 100644 --- a/src/components/Row/index.js +++ b/src/components/Row/index.js @@ -1,30 +1,29 @@ import LinkSegmentBuilder from "./LinkSegmentBuilder"; -export class Row extends React.Component { - constructor() { - super(); - this.state = { highlighted: false }; - } - - render() { - return this.props.chars +export const Row = ({ + chars, + row, + enableLinkInlinePreview, + forceWidth, + highlighted, + onHyperLinkMouseOver, + onHyperLinkMouseOut +}) => ( + + {chars .reduce( LinkSegmentBuilder.accumulator, new LinkSegmentBuilder( - this.props.row, - this.props.showsLinkPreviews, - this.props.forceWidth, - this.state.highlighted + row, + enableLinkInlinePreview, + forceWidth, + highlighted, + onHyperLinkMouseOver, + onHyperLinkMouseOut ) ) - .build(); - } - - setHighlight(shouldHighlight) { - if (this.state.highlighted != shouldHighlight) { - this.setState({ highlighted: shouldHighlight }); - } - } -} + .build()} + +); export default Row; diff --git a/src/components/Screen.js b/src/components/Screen.js new file mode 100644 index 00000000..d4d20e77 --- /dev/null +++ b/src/components/Screen.js @@ -0,0 +1,77 @@ +import Row from "./Row"; +import ImagePreviewer, { + of, + resolveSrcToImageUrl, + resolveWithImageDOM +} from "./ImagePreviewer"; + +export class Screen extends React.Component { + setCurrentHighlighted = currentHighlighted => { + this.setState({ currentHighlighted }); + }; + + state = { + currentHighlighted: undefined, + currentImagePreview: undefined, + left: undefined, + top: undefined + }; + + componentWillReceiveProps(nextProps) { + if (this.props.lines !== nextProps.lines) { + this.setState({ currentImagePreview: undefined }); + } + } + + handleMouseMove = ({ clientX, clientY }) => { + if (this.state.currentImagePreview) { + this.setState({ + left: clientX, + top: clientY + }); + } + }; + + handleHyperLinkMouseOver = ({ currentTarget: { href } }) => { + if (this.props.enableLinkHoverPreview) { + this.setState({ + currentImagePreview: of(href) + .then(resolveSrcToImageUrl) + .then(resolveWithImageDOM) + }); + } + }; + + handleHyperLinkMouseOut = () => { + this.setState({ currentImagePreview: undefined }); + }; + + render() { + return ( +
+ {this.props.lines.map((chars, row) => ( + + ))} + {this.state.currentImagePreview && ( + + )} +
+ ); + } +} + +export default Screen; diff --git a/src/dev.html b/src/dev.html index 8c98def1..a672f57d 100644 --- a/src/dev.html +++ b/src/dev.html @@ -173,7 +173,6 @@

-