├── .gitignore ├── screenshot.png ├── CHANGELOG.md ├── package.json ├── README.md ├── styles ├── devices.less └── ionic-preview.less ├── LICENSE.md └── lib ├── ionic.coffee ├── views ├── dropdown-devices.coffee └── frame-view.coffee └── ionic-view.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabaag/ionic-preview/HEAD/screenshot.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ##0.2.0 2 | * Added auto start server 3 | 4 | ## 0.1.0 - First Release 5 | * Every feature added 6 | * Every bug fixed 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-preview", 3 | "main": "./lib/ionic", 4 | "version": "0.7.0", 5 | "description": "Show a preview of your ionic apps", 6 | "activationCommands": { 7 | "atom-workspace": "ionic:preview" 8 | }, 9 | "repository": "https://github.com/cabaag/ionic-preview", 10 | "license": "MIT", 11 | "engines": { 12 | "atom": ">=0.174.0 <2.0.0" 13 | }, 14 | "dependencies": { 15 | "atom-space-pen-views": "^2.0.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ionic Preview 2 | 3 | Preview a live version of your [Ionic](http://www.ionicframework.com) application 4 | in an atom pane. 5 | 6 | ![A screenshot of your package](https://raw.githubusercontent.com/cabaag/ionic-preview/master/screenshot.png) 7 | 8 | # Getting start 9 | 10 | In your terminal: 11 | ''' 12 | apm install ionic-preview 13 | ionic serve 14 | ''' 15 | 16 | Then hit `Cmd + Shift + P` in atom and use the command `Ionic: preview` 17 | 18 | (note: if you don't see `Ionic: Preview` in the list you may need to restart atom) 19 | 20 | This will open a new pane with your app displayed in it. 21 | 22 | Use `ionic serve -b` to start the livereload service without opening your browser 23 | 24 | ## TODO 25 | * Multiple views 26 | * Specs 27 | * Device orientation 28 | -------------------------------------------------------------------------------- /styles/devices.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Array of devices avaliable on ionic-preview 3 | * name width height 4 | */ 5 | 6 | @devices: 7 | iphone-5 320 568, 8 | iphone-6 375 667, 9 | iphone-6-plus 414 763, 10 | 11 | nexus-5x 412 732, 12 | nexus-6p 412 732, 13 | samsung-galaxy-s6 360 640, 14 | ipad 768 1024, 15 | ipad-pro 1024 1366; 16 | 17 | .make-devices-classes(@i: length(@devices)) when (@i > 0) { 18 | .make-devices-classes(@i - 1); 19 | @device: extract(@devices, @i); 20 | @class: extract(@device, 1); 21 | @width: extract(@device, 2); 22 | @height: extract(@device, 3); 23 | &.@{class} { 24 | width: ~'@{width}px'; 25 | height: ~'@{height}px'; 26 | &.landscape { 27 | width: ~'@{height}px'; 28 | height: ~'@{width}px'; 29 | } 30 | } 31 | } 32 | 33 | .ionic-preview { 34 | #frame { 35 | border: none; 36 | .make-devices-classes(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/ionic.coffee: -------------------------------------------------------------------------------- 1 | { CompositeDisposable } = require 'atom' 2 | url = require 'url' 3 | WebBrowserPreview = require './ionic-view' 4 | 5 | CMD_TOOGLE = "ionic:preview" 6 | 7 | view = undefined 8 | pane = undefined 9 | item = undefined 10 | 11 | module.exports = IonicPreview = 12 | 13 | activate: (state)-> 14 | atom.commands.add 'atom-workspace', CMD_TOOGLE, => @toggleView() 15 | return 16 | 17 | deactivate: -> 18 | return 19 | 20 | toggleView: -> 21 | unless view and view.active 22 | address = atom.config.get('ionic-preview.customAddress') 23 | port = atom.config.get('ionic-preview.customPort') 24 | uri = "http://#{address}:#{port}" 25 | 26 | view = new WebBrowserPreview(url: uri) 27 | 28 | atom.workspace.getActivePane().splitRight() 29 | pane = atom.workspace.getActivePane() 30 | item = pane.addItem(view, {index: 0}) 31 | 32 | pane.activateItem(item) 33 | else 34 | pane.destroyItem(item) 35 | return 36 | 37 | serialize: -> 38 | 39 | config: 40 | autoStartServe: 41 | title: 'Autostart Serve' 42 | description: 'Automatically start ionic serve' 43 | type: 'boolean' 44 | default: false 45 | customAddress: 46 | title: 'Address' 47 | description: 'Changes address for ionic-view' 48 | type: 'string' 49 | default: 'localhost' 50 | customPort: 51 | title: 'Port' 52 | description: 'Changes port for ionic-view' 53 | type: 'integer' 54 | default: 8100 55 | -------------------------------------------------------------------------------- /lib/views/dropdown-devices.coffee: -------------------------------------------------------------------------------- 1 | { View, $ } = require 'atom-space-pen-views' 2 | # WebBrowserPreview = require '../ionic-view' 3 | 4 | devices = [ 5 | {id: 'iphone-5', name: 'iPhone 5', width: 320, height: 568} 6 | {id: 'iphone-6', name: 'iPhone 6', width: 375, height: 667} 7 | {id: 'iphone-6-plus', name: 'iPhone 6 Plus', width: 414, height: 763}, 8 | # {id: 'nexus-5x', name: 'Nexus 5X', width: 412, height: 732}, 9 | {id: 'nexus-6p', name: 'Nexus 6P', width: 412, height: 732}, 10 | {id: 'samsung-galaxy-s6', name: 'Samsung Galxy S6', width: 360, height: 640}, 11 | {id: 'ipad', name: 'iPad', width: 768, height: 1024}, 12 | {id: 'ipad-pro', name: 'iPad Pro', width: 1024, height: 1366}, 13 | ]; 14 | 15 | 16 | class Device extends View 17 | @content: (device)-> 18 | @li click: 'click', device.name 19 | 20 | initialize: (device)-> 21 | @device = device 22 | 23 | click: -> 24 | @parentView.click(@device) 25 | 26 | module.exports = 27 | class DropdownDevices extends View 28 | @content: -> 29 | @div class: 'dropdown-devices', => 30 | @ul => 31 | for device in devices 32 | @subview device.id, new Device(device) 33 | 34 | initialize: -> 35 | @me = this 36 | $(document).ready => 37 | top = $('.ionic-preview .header').offsetHeight 38 | @me.css('top', top) 39 | 40 | open: => 41 | @me.addClass('active') 42 | 43 | close: => 44 | @me.removeClass('active') 45 | 46 | toggle: => 47 | @me.toggleClass('active') 48 | 49 | click: (device)-> 50 | @parentView.changeDeviceView(device) 51 | -------------------------------------------------------------------------------- /lib/views/frame-view.coffee: -------------------------------------------------------------------------------- 1 | { View, $ } = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | class FrameView extends View 5 | @content: (url)-> 6 | @div class: 'frame-wrapper', => 7 | @iframe 8 | id: "frame", 9 | class: "iphone-5", 10 | src: url, 11 | sandbox: "allow-same-origin allow-scripts", 12 | outlet: "frame" 13 | 14 | initialize: -> 15 | @address = atom.config.get('ionic-preview.customAddress') 16 | @port = atom.config.get('ionic-preview.customPort') 17 | @watchLocation() 18 | $(window).resize => 19 | @resizeFrame(@parentView) 20 | 21 | destroy: => 22 | clearInterval(@locationWatcher) 23 | 24 | init: => 25 | setTimeout => @resizeFrame() 26 | 27 | # Resize the iframe to match the container 28 | resizeFrame: => 29 | maxHeight = @parentView.height() - 100 30 | if maxHeight? and maxHeight < @frame.height() 31 | @frame.css("transform", "scale(#{maxHeight / @frame.height()})") 32 | 33 | # Get location of iframe 34 | getLocation: => 35 | (@frame[0].contentWindow || @frame[0].contentDocument)?.location.href 36 | 37 | # Iframe navigates to location 38 | navigateTo: (url)=> 39 | @frame.attr('src', url) 40 | return 41 | 42 | # Add watcher when location of frame changes 43 | watchLocation: -> 44 | @locationWatcher = setInterval => 45 | location = @getLocation() 46 | if @actualLocation isnt location 47 | @actualLocation = location 48 | @parentView.addressBar.val(location) 49 | , 200 50 | return 51 | 52 | changeDeviceView: (id)=> 53 | @frame.removeClass() 54 | @frame.addClass(id) 55 | # @resizeFrame() 56 | 57 | rotate: -> 58 | @frame.toggleClass('landscape') 59 | -------------------------------------------------------------------------------- /styles/ionic-preview.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .ionic-preview { 8 | display: flex; 9 | flex-direction: column; 10 | 11 | .header { 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: space-between; 15 | margin: 4px 0; 16 | min-height: 38px; 17 | 18 | .address-bar { 19 | padding: 8px; 20 | background-color: #3d3d3d; 21 | text-align: center; 22 | font-size: 12px; 23 | flex-grow: 1; 24 | border: none; 25 | 26 | &:focus { 27 | // text-align: left;; 28 | // color: @text-color; 29 | background: @app-background-color; 30 | } 31 | } 32 | 33 | button { 34 | background: transparent; 35 | border: 1px solid transparent; 36 | border-radius: 4px; 37 | color: @text-color; 38 | margin: 8px; 39 | 40 | &:hover { 41 | color: @text-color-highlight; 42 | } 43 | &.rotate-device::before{ 44 | content: "\f0ec"; 45 | } 46 | &.shutdown::before{ 47 | content: "\f057"; 48 | } 49 | } 50 | } 51 | 52 | .dropdown-devices { 53 | max-height: 0; 54 | position: absolute; 55 | top: 40px; 56 | overflow: hidden; 57 | 58 | background-color: @app-background-color; 59 | color: @text-color; 60 | z-index: 1000; 61 | transition: max-height 250ms linear; 62 | 63 | &.active { 64 | overflow: visible; 65 | max-height: 200px; 66 | } 67 | 68 | ul { 69 | margin: 0; 70 | padding: 0; 71 | display: flex; 72 | list-style: none; 73 | flex-direction: column; 74 | max-height: inherit; 75 | overflow: hidden; 76 | 77 | li { 78 | padding: 8px 16px; 79 | cursor: pointer; 80 | 81 | &:hover { 82 | background-color: @background-color-highlight; 83 | } 84 | } 85 | } 86 | 87 | } 88 | 89 | .frame-wrapper { 90 | display: flex; 91 | flex-grow: 1; 92 | align-self: center; 93 | overflow: hidden;; 94 | padding-top: 10px; 95 | } 96 | 97 | .footer { 98 | min-height: 38px; 99 | display: flex; 100 | justify-content: center; 101 | align-content: center; 102 | align-items: center; 103 | background-color: @background-color-highlight; 104 | } 105 | 106 | } 107 | // atom-pane:not(.active) { 108 | // .address-bar { 109 | // background-color: #3d3d3d !important; 110 | // } 111 | // } 112 | -------------------------------------------------------------------------------- /lib/ionic-view.coffee: -------------------------------------------------------------------------------- 1 | { View, $, $$ } = require 'atom-space-pen-views' 2 | { BufferedProcess } = require 'atom' 3 | http = require "http" 4 | url = require "url" 5 | 6 | FrameView = require './views/frame-view.coffee' 7 | DropdownDevices = require './views/dropdown-devices.coffee' 8 | 9 | module.exports = 10 | class WebBrowserPreview extends View 11 | @content: (params) -> 12 | @div class: "ionic-preview", => 13 | @div class: 'header', => 14 | @button class: 'icon icon-device-mobile', click: "toggleDropdownDevices" 15 | @button class: 'fa rotate-device', click: "rotateDevice" 16 | @input 17 | class: 'native-key-bindings address-bar', 18 | type: 'text', 19 | keyup: 'keyUp', 20 | outlet: 'addressBar' 21 | @button class: 'icon icon-home', click: "goToDefault" 22 | @button class: 'fa shutdown', click: "clickShutdownButton" 23 | @div class: 'footer', outlet: 'footer' 24 | @subview 'dropdownDevices', new DropdownDevices() 25 | @subview 'frameView', new FrameView(params.url) 26 | 27 | serialize: -> 28 | 29 | # Initialize pane 30 | initialize: (params) -> 31 | @active = true 32 | @actualLocation = @url = params.url 33 | @address = atom.config.get('ionic-preview.customAddress') 34 | @port = atom.config.get('ionic-preview.customPort') 35 | 36 | @device = {id: 'iphone-5', name: 'iPhone 5', width: 320, height: 568} 37 | 38 | # Open browser if auto start serve but theres and external existing 39 | # process of ionic serve else open browser or destroy pane 40 | if atom.config.get 'ionic-preview.autoStartServe' 41 | http.get(@url).on 'error', (error)=> 42 | alert "Starting serve" 43 | @startServe() 44 | else 45 | http.get(@url).on 'error', (error)-> 46 | alert("First start ionic serve") 47 | atom.workspace.destroyActivePaneItem() 48 | 49 | $(document).ready => 50 | @footer.text(@device.name + " - " + @device.height + "x" + @device.width) 51 | @frameView.init() 52 | 53 | destroy: => 54 | @frameView.destroy() 55 | @shutdownServe() 56 | @active = false 57 | return 58 | 59 | getTitle: -> 60 | "Ionic: Preview" 61 | 62 | # Go to default location 63 | goToDefault: -> 64 | @frameView.navigateTo("http://#{@address}:#{@port}") 65 | 66 | toggleDropdownDevices: -> 67 | @dropdownDevices.toggle() 68 | 69 | changeDeviceView: (device)-> 70 | @device = device 71 | @footer.text(@device.name + " - " + @device.height + "x" + @device.width) 72 | @dropdownDevices.close() 73 | @frameView.changeDeviceView(device.id) 74 | 75 | rotateDevice: -> 76 | @footer.text(@device.name + " - " + @device.height + "x" + @device.width) 77 | @frameView.rotate() 78 | 79 | # if ENTER reload the frame with the address provided 80 | keyUp: (event)-> 81 | if event.keyCode is 13 82 | @frameView.navigateTo(@addressBar.val()) 83 | @addressBar.blur() 84 | return 85 | 86 | # Start serve with commands 87 | startServe: -> 88 | command = 'ionic' 89 | args = ['serve', '-b', '--address', @address, '-p', @port] 90 | path = atom.project.getPaths()[0] 91 | options = { cwd: path } 92 | startedServer = new RegExp("Running dev server") 93 | 94 | stdout = (output) => 95 | if /error/ig.exec(output) 96 | alert output 97 | 98 | if startedServer.test(output) 99 | setTimeout => 100 | @frameView.navigateTo("http://#{@address}:#{@port}") 101 | , 500 102 | 103 | exit = (code) => 104 | alert "Your first directory must be an ionic app" 105 | @clickShutdownButton() 106 | 107 | @bufferedProcess = new BufferedProcess({command, args, options, stdout, exit}) 108 | return 109 | 110 | clickShutdownButton: => 111 | atom.workspace.destroyActivePaneItem() 112 | 113 | shutdownServe: => 114 | @bufferedProcess.kill() if @bufferedProcess 115 | return 116 | --------------------------------------------------------------------------------