├── Procfile ├── views ├── index.slim └── layout.slim ├── .fonts ├── ipag.ttf └── ipagp.ttf ├── public ├── images │ └── ajax-loader.gif └── stylesheets │ └── main.css ├── .buildpacks ├── config.ru ├── Gemfile ├── README.md ├── app.rb ├── package.json ├── gulpfile.js ├── bin └── screenshot.js ├── .gitignore ├── Gemfile.lock └── jsx └── application.jsx /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rackup config.ru -p $PORT -------------------------------------------------------------------------------- /views/index.slim: -------------------------------------------------------------------------------- 1 | div#app-container 2 | script src="/js/application.js" 3 | -------------------------------------------------------------------------------- /.fonts/ipag.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naoya/sukushokun/HEAD/.fonts/ipag.ttf -------------------------------------------------------------------------------- /.fonts/ipagp.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naoya/sukushokun/HEAD/.fonts/ipagp.ttf -------------------------------------------------------------------------------- /public/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naoya/sukushokun/HEAD/public/images/ajax-loader.gif -------------------------------------------------------------------------------- /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/heroku/heroku-buildpack-ruby.git 2 | https://github.com/leesei/heroku-buildpack-casperjs -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/reloader' if development? 3 | require 'slim' 4 | require './app' 5 | 6 | run Sinatra::Application 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'sinatra-reloader' 5 | gem 'sinatra-contrib' 6 | gem 'slim' 7 | gem 'thin' 8 | gem 'foreman' 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # スクショ撮る君 2 | 3 | 指定したURLのスクリーンショット画像を撮ります。画像は保存できます。 4 | 5 | https://sukushokun.herokuapp.com/ 6 | 7 | ![](https://cloud.githubusercontent.com/assets/8991/6650421/3c997f46-ca54-11e4-900f-1d189915aeb2.png) 8 | -------------------------------------------------------------------------------- /views/layout.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html lang="ja" 4 | head 5 | title スクショ撮る君 6 | link href="/stylesheets/main.css" rel="stylesheet" type="text/css" 7 | meta name="viewport" content="width=device-width, initial-scale=1.0" 8 | 9 | body 10 | == yield 11 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | set :port, 3000 2 | 3 | get '/' do 4 | slim :index 5 | end 6 | 7 | get '/screenshot' do 8 | @url = params[:url] 9 | command = "casperjs bin/screenshot.js '#{@url}'" 10 | if params[:mobile] 11 | command = command + " mobile" 12 | end 13 | base64 = `#{command}` 14 | halt 500, 'error' unless base64 15 | return base64 16 | end 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sukushokun", 3 | "bin": { 4 | "sukushokun": "screenshot.js" 5 | }, 6 | "scripts": { 7 | "react": "gulp build", 8 | "watch": "gulp watch", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "browserify": "^9.0.3", 15 | "fluxxor": "^1.5.2", 16 | "gulp": "^3.8.11", 17 | "gulp-util": "^3.0.4", 18 | "react": "^0.12.0", 19 | "react-router": "^0.12.4", 20 | "reactify": "^1.1.0", 21 | "superagent": "^1.1.0", 22 | "vinyl-source-stream": "^1.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var util = require('gulp-util'); 3 | var browserify = require('browserify'); 4 | var reactify = require('reactify'); 5 | var source = require('vinyl-source-stream'); 6 | 7 | function errorHandler (err) { 8 | util.log(util.colors.red('Error'), err.message); 9 | this.end(); 10 | } 11 | 12 | gulp.task('build', function() { 13 | browserify({ 14 | entries: [ './jsx/application.jsx' ], 15 | transform: [reactify] 16 | }) 17 | .bundle() 18 | .on('error', errorHandler) 19 | .pipe(source('application.js')) 20 | .pipe(gulp.dest('./public/js')); 21 | }); 22 | 23 | gulp.task('watch', function () { 24 | gulp.watch(['./jsx/**/*.jsx'], ['build']); 25 | }); 26 | -------------------------------------------------------------------------------- /bin/screenshot.js: -------------------------------------------------------------------------------- 1 | var props = { 2 | // Chrome 3 | pc:{ 4 | ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", 5 | viewport: { width: 2560, height: 1440 } 6 | }, 7 | // iPhone 5, iOS 8.0.2 8 | mobile: { 9 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", 10 | viewport: { width: 640, height: 1136 } 11 | } 12 | }; 13 | 14 | var casper = require('casper').create(); 15 | 16 | var prop = casper.cli.args[1] == "mobile" ? props.mobile : props.pc; 17 | 18 | casper.start(); 19 | casper.userAgent(prop.ua); 20 | casper.zoom(2); 21 | casper.open(casper.cli.args[0]).viewport(prop.viewport.width, prop.viewport.height).then(function () { 22 | console.log(this.captureBase64('png')); 23 | }); 24 | 25 | casper.run(); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/f31b319dca10213163411cb710c27dc37ed3eac5/Global/osx.gitignore 2 | 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | Icon 7 | 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear on external disk 13 | .Spotlight-V100 14 | .Trashes 15 | 16 | 17 | ### https://raw.github.com/github/gitignore/f31b319dca10213163411cb710c27dc37ed3eac5/ruby.gitignore 18 | 19 | *.gem 20 | *.rbc 21 | .bundle 22 | .config 23 | coverage 24 | InstalledFiles 25 | lib/bundler/man 26 | pkg 27 | rdoc 28 | spec/reports 29 | test/tmp 30 | test/version_tmp 31 | tmp 32 | 33 | # YARD artifacts 34 | .yardoc 35 | _yardoc 36 | doc/ 37 | ### https://raw.github.com/github/gitignore/f31b319dca10213163411cb710c27dc37ed3eac5/node.gitignore 38 | 39 | lib-cov 40 | *.seed 41 | *.log 42 | *.csv 43 | *.dat 44 | *.out 45 | *.pid 46 | *.gz 47 | 48 | pids 49 | logs 50 | results 51 | 52 | npm-debug.log 53 | node_modules 54 | 55 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | backports (3.6.4) 5 | daemons (1.1.9) 6 | dotenv (1.0.2) 7 | eventmachine (1.0.7) 8 | foreman (0.77.0) 9 | dotenv (~> 1.0.2) 10 | thor (~> 0.19.1) 11 | multi_json (1.10.1) 12 | rack (1.6.0) 13 | rack-protection (1.5.3) 14 | rack 15 | rack-test (0.6.3) 16 | rack (>= 1.0) 17 | sinatra (1.4.5) 18 | rack (~> 1.4) 19 | rack-protection (~> 1.4) 20 | tilt (~> 1.3, >= 1.3.4) 21 | sinatra-contrib (1.4.2) 22 | backports (>= 2.0) 23 | multi_json 24 | rack-protection 25 | rack-test 26 | sinatra (~> 1.4.0) 27 | tilt (~> 1.3) 28 | sinatra-reloader (1.0) 29 | sinatra-contrib 30 | slim (3.0.2) 31 | temple (~> 0.7.3) 32 | tilt (>= 1.3.3, < 2.1) 33 | temple (0.7.5) 34 | thin (1.6.3) 35 | daemons (~> 1.0, >= 1.0.9) 36 | eventmachine (~> 1.0) 37 | rack (~> 1.0) 38 | thor (0.19.1) 39 | tilt (1.4.1) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | foreman 46 | sinatra 47 | sinatra-contrib 48 | sinatra-reloader 49 | slim 50 | thin 51 | -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue","Helvetica"; 3 | } 4 | 5 | h1 { 6 | font-size: 40px; 7 | letter-spacing: -1px; 8 | font-weight: 200; 9 | margin: 0; 10 | } 11 | 12 | #app-container { 13 | width: 100%; 14 | } 15 | 16 | p { 17 | color: #666; 18 | font-weight: 200; 19 | } 20 | 21 | .intro p { 22 | margin: 0; 23 | } 24 | 25 | .control { 26 | margin: 0 auto; 27 | margin-top: 5px; 28 | } 29 | 30 | #url { 31 | width: 60%; 32 | padding: 10px; 33 | font-size: 1.2em; 34 | } 35 | 36 | #btn-screenshot { 37 | margin-left: 10px; 38 | font-size: 1.2em; 39 | padding: 10px; 40 | 41 | outline: none; 42 | color: #333; 43 | border: 1px solid #d4d4d4; 44 | border-bottom-color: #bcbcbc; 45 | display: inline-block; 46 | font-weight: bold; 47 | line-height: 24px; 48 | white-space: nowrap; 49 | cursor: pointer; 50 | -webkit-touch-callout: none; 51 | -webkit-user-select: none; 52 | -khtml-user-select: none; 53 | -moz-user-select: none; 54 | -ms-user-select: none; 55 | user-select: none; 56 | text-shadow: 0 1px 0 #ffffff; 57 | -webkit-border-radius: 3px; 58 | -moz-border-radius: 3px; 59 | border-radius: 3px; 60 | background-color: #eaeaea; 61 | background-image: -webkit-linear-gradient(#fafafa, #eaeaea); 62 | background-image: moz-linear-gradient(#fafafa, #eaeaea); 63 | background-image: linear-gradient(#fafafa, #eaeaea); 64 | background-repeat: repat-x; 65 | } 66 | 67 | #result { 68 | margin-top: 1.2em; 69 | border-top: 1px solid #ccc; 70 | } 71 | 72 | #result img { 73 | max-width: 320px; 74 | border: 1px solid #999; 75 | display: block; 76 | margin: 1em; 77 | } 78 | 79 | label { 80 | color: #666; 81 | } -------------------------------------------------------------------------------- /jsx/application.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Fluxxor = require('fluxxor'); 3 | var request = require('superagent'); 4 | var Router = require('react-router'); 5 | 6 | var Route = Router.Route; 7 | var RouteHandler = Router.RouteHandler; 8 | 9 | var constants = { 10 | LOAD_SCREENSHOT: "LOAD_SCREENSHOT", 11 | LOAD_SCREENSHOT_SUCCESS: "LOAD_SCREENSHOT_SUCCESS" 12 | }; 13 | 14 | var ScreenshotClient = { 15 | isMobile: false, 16 | load: function(url, callback) { 17 | var endpoint = '/screenshot?url=' + encodeURIComponent(url); 18 | if (this.isMobile) 19 | endpoint += '&mobile=true'; 20 | request.get(endpoint).end(callback); 21 | } 22 | }; 23 | 24 | var actions = { 25 | takeScreenshot: function(url, isMobile) { 26 | this.dispatch(constants.LOAD_SCREENSHOT); 27 | ScreenshotClient.isMobile = isMobile; 28 | ScreenshotClient.load(url, function(err, response) { 29 | // FIXME: erro handling 30 | this.dispatch(constants.LOAD_SCREENSHOT_SUCCESS, {data:response.text}); 31 | }.bind(this)); 32 | } 33 | }; 34 | 35 | var ScreenshotStore = Fluxxor.createStore({ 36 | initialize: function() { 37 | this.screenshots = []; 38 | this.loading = false; 39 | this.bindActions( 40 | constants.LOAD_SCREENSHOT, this.onLoadScreenshot, 41 | constants.LOAD_SCREENSHOT_SUCCESS, this.onLoadScreenshotSuccess 42 | ); 43 | }, 44 | onLoadScreenshot: function() { 45 | this.loading = true; 46 | this.emit('change'); 47 | }, 48 | onLoadScreenshotSuccess: function(payload) { 49 | this.loading = false; 50 | var screenshot = { id:this.screenshots.length + 1, data:payload.data }; 51 | this.screenshots = [screenshot].concat(this.screenshots); 52 | this.emit('change'); 53 | }, 54 | getState: function() { 55 | return { 56 | screenshots: this.screenshots, 57 | loading: this.loading 58 | }; 59 | } 60 | }); 61 | 62 | var FluxMixin = Fluxxor.FluxMixin(React), 63 | StoreWatchMixin = Fluxxor.StoreWatchMixin; 64 | 65 | var App = React.createClass({ 66 | mixins: [ Router.State, FluxMixin, StoreWatchMixin('ScreenshotStore') ], 67 | getInitialState: function() { 68 | return { 69 | url: null, 70 | isMobile: false, 71 | }; 72 | }, 73 | getStateFromFlux: function() { 74 | return this.getFlux().store("ScreenshotStore").getState(); 75 | }, 76 | componentDidMount: function() { 77 | this.handleUrl(this.getQuery().url); 78 | }, 79 | takeScreenShot: function() { 80 | if (!this.state.url) { 81 | window.alert('URLを入力してね'); 82 | return; 83 | } 84 | return this.getFlux().actions.takeScreenshot(this.state.url, this.state.isMobile); 85 | }, 86 | handleUrl: function(value) { 87 | this.setState({ url: value }); 88 | }, 89 | handleMobileFlag: function(value) { 90 | this.setState({ isMobile: value }); 91 | }, 92 | render: function() { 93 | return ( 94 |
95 |

スクショ撮る君

96 |
100 | 101 | 102 | 103 |
104 | ); 105 | } 106 | }); 107 | 108 | var Form = React.createClass({ 109 | propTypes: { 110 | url: React.PropTypes.string, 111 | onUrlChange: React.PropTypes.func.isRequired, 112 | onMobileFlagChange: React.PropTypes.func.isRequired, 113 | onSubmit: React.PropTypes.func.isRequired, 114 | }, 115 | handleUrlChange: function(e) { 116 | this.props.onUrlChange(e.target.value); 117 | }, 118 | handleMobileFlagChange: function(e) { 119 | this.props.onMobileFlagChange(e.target.checked); 120 | }, 121 | handleButton: function() { 122 | this.props.onSubmit(); 123 | }, 124 | render: function() { 125 | return ( 126 |
127 | 128 | 129 |
130 | 131 | 132 |
133 |
134 | ); 135 | } 136 | }); 137 | 138 | var Indicator = React.createClass({ 139 | propTypes: { 140 | loading: React.PropTypes.bool.isRequired 141 | }, 142 | render: function() { 143 | if (this.props.loading) { 144 | return( 145 | 146 | ); 147 | } else { 148 | return ( 149 | 150 | ); 151 | } 152 | } 153 | }); 154 | 155 | var ScreenShotList = React.createClass({ 156 | render: function() { 157 | var screenshots = this.props.screenshots.map(function (screenshot) { 158 | return ( 159 | 160 | ); 161 | }.bind(this)); 162 | return ( 163 |
{screenshots}
164 | ); 165 | } 166 | }); 167 | 168 | var ScreenShot = React.createClass({ 169 | propTypes: { 170 | screenshot: React.PropTypes.shape({ 171 | id: React.PropTypes.number.isRequired, 172 | data: React.PropTypes.string.isRequired 173 | }) 174 | }, 175 | render: function() { 176 | var src = "data:image/png;base64," + this.props.screenshot.data; 177 | return ( 178 | 179 | ); 180 | } 181 | }); 182 | 183 | var routes = ( 184 | 185 | ); 186 | 187 | var stores = { 188 | ScreenshotStore: new ScreenshotStore() 189 | }; 190 | var flux = new Fluxxor.Flux(stores, actions); 191 | 192 | Router.run(routes, Router.HistoryLocation, function (Handler) { 193 | React.render(, document.getElementById('app-container')); 194 | }); 195 | --------------------------------------------------------------------------------