├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── images ├── pause.png ├── play.png ├── repeat.png ├── shuffle.png └── spotify.png ├── keymaps └── atomify.json ├── lib ├── actions │ └── index.js ├── atom │ ├── Main.js │ └── Subscriptions.js ├── components │ ├── App.js │ ├── PlayerDetails.js │ ├── Render.js │ └── TrackDetails.js ├── constants │ ├── ActionType.js │ └── IconUrl.js ├── containers │ ├── CurrentApp.js │ ├── CurrentPlayerDetails.js │ └── CurrentTrackDetails.js └── reducers │ ├── CurrentDisplaySettings.js │ ├── CurrentPlayerDetails.js │ ├── CurrentSpotifyState.js │ ├── CurrentTrackDetails.js │ └── index.js ├── menus └── atomify.json ├── package-lock.json ├── package.json ├── spec └── atomify-spec.js └── styles └── atomify.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | dist: trusty 4 | 5 | env: 6 | global: 7 | - APM_TEST_PACKAGES="" 8 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 9 | 10 | matrix: 11 | - ATOM_CHANNEL=stable 12 | - ATOM_CHANNEL=beta 13 | 14 | os: 15 | - linux 16 | - osx 17 | 18 | ### Generic setup follows ### 19 | script: 20 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 21 | - chmod u+x build-package.sh 22 | - ./build-package.sh 23 | 24 | notifications: 25 | email: 26 | on_success: never 27 | on_failure: change 28 | 29 | branches: 30 | only: 31 | - master 32 | 33 | git: 34 | depth: 10 35 | 36 | sudo: false 37 | 38 | addons: 39 | apt: 40 | packages: 41 | - build-essential 42 | - git 43 | - libgnome-keyring-dev 44 | - fakeroot 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Every feature added 3 | * Every bug fixed 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jaebradley/atomify.svg?branch=master)](https://travis-ci.org/jaebradley/atomify) 2 | 3 | # [Atomify](https://atom.io/packages/atomify) (Where Atom Meets Spotify for Macs) 4 | 5 | View and Control your Spotify experience without leaving Atom on a Mac 6 | 7 | ## Example Usage 8 | 9 | ### Regular View 10 | ![Example Usage](http://g.recordit.co/r2wi8BV6sm.gif) 11 | 12 | ### Shortcuts / Commands 13 | 14 | #### Toggle Package 15 | ![Toggle Package](http://g.recordit.co/KoKWUThEoL.gif) 16 | 17 | ### Toggle Shuffle (Alt + Ctrl + S) 18 | ![Toggle Shuffle](http://g.recordit.co/9i84SWdPIr.gif) 19 | 20 | ### Toggle Repeat (Alt + Ctrl + R) 21 | ![Toggle Repeat](http://g.recordit.co/McLcvYCmuz.gif) 22 | 23 | ### Toggle Play/Pause (Alt + Ctrl + P) 24 | ![Toggle Play/Pause](http://g.recordit.co/hSbIPnbzYT.gif) 25 | 26 | ### Next Track (Alt + Ctrl + L) 27 | ![Next Track](http://g.recordit.co/zpnmtQj0Aa.gif) 28 | 29 | ### Previous Track (Alt + Ctrl + J) 30 | ![Previous Track](http://g.recordit.co/U2Y0qtdwwe.gif) 31 | -------------------------------------------------------------------------------- /images/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaebradley/atomify/a0d5b8f2b9188ef907a6bc8c23eadf1b8538eac0/images/pause.png -------------------------------------------------------------------------------- /images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaebradley/atomify/a0d5b8f2b9188ef907a6bc8c23eadf1b8538eac0/images/play.png -------------------------------------------------------------------------------- /images/repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaebradley/atomify/a0d5b8f2b9188ef907a6bc8c23eadf1b8538eac0/images/repeat.png -------------------------------------------------------------------------------- /images/shuffle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaebradley/atomify/a0d5b8f2b9188ef907a6bc8c23eadf1b8538eac0/images/shuffle.png -------------------------------------------------------------------------------- /images/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaebradley/atomify/a0d5b8f2b9188ef907a6bc8c23eadf1b8538eac0/images/spotify.png -------------------------------------------------------------------------------- /keymaps/atomify.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-workspace": { 3 | "alt-ctrl-s": "atomify:toggleShuffle", 4 | "alt-ctrl-r": "atomify:toggleRepeat", 5 | "alt-ctrl-p": "atomify:togglePlayPause", 6 | "alt-ctrl-l": "atomify:goToNextTrack", 7 | "alt-ctrl-j": "atomify:goToPreviousTrack", 8 | "alt-ctrl-o": "atomify:openSpotify" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/actions/index.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { UPDATE_TRACK_DETAILS, UPDATE_PLAYER_DETAILS, UPDATE_DISPLAY_SETTINGS, UPDATE_SPOTIFY_IS_OPEN } from '../constants/ActionType'; 4 | 5 | export const updateTrackDetails = (song, album, artist, songDuration) => { 6 | return { 7 | type: UPDATE_TRACK_DETAILS, 8 | song, 9 | album, 10 | artist, 11 | songDuration, 12 | }; 13 | }; 14 | 15 | export const updatePlayerDetails = (playerState, songPosition, isShuffling, isRepeating) => { 16 | return { 17 | type: UPDATE_PLAYER_DETAILS, 18 | playerState, 19 | songPosition, 20 | isShuffling, 21 | isRepeating, 22 | }; 23 | }; 24 | 25 | export const updateDisplaySettings = (shouldDisplay) => { 26 | return { 27 | type: UPDATE_DISPLAY_SETTINGS, 28 | shouldDisplay, 29 | }; 30 | }; 31 | 32 | export const updateSpotifyIsOpen = (isSpotifyOpen) => { 33 | return { 34 | type: UPDATE_SPOTIFY_IS_OPEN, 35 | isSpotifyOpen, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/atom/Main.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import * as React from 'react'; 4 | import { SpotifyApplicationClient } from 'spotify-application-client'; 5 | 6 | import { render, initRoot, togglePanel, fetchDetails } from '../components/Render'; 7 | import { onActivate, onDeactivate } from './Subscriptions'; 8 | 9 | class Main { 10 | constructor() { 11 | this.root = initRoot(); 12 | } 13 | 14 | consumeStatusBar(statusBar) { 15 | statusBar.addRightTile({ 16 | item: this.root, 17 | priority: -100 18 | }); 19 | 20 | onActivate(); 21 | 22 | render(this.root); 23 | 24 | setInterval(() => this.poll(), 1000); 25 | } 26 | 27 | deactivate() { 28 | onDeactivate(); 29 | } 30 | 31 | toggle() { 32 | togglePanel(); 33 | } 34 | 35 | poll() { 36 | if (!this.root.hidden) { 37 | return fetchDetails(); 38 | } 39 | } 40 | }; 41 | 42 | export default new Main(); 43 | -------------------------------------------------------------------------------- /lib/atom/Subscriptions.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { CompositeDisposable } from 'atom'; 4 | import { unmount, togglePanel } from '../components/Render'; 5 | 6 | import { SpotifyApplicationClient } from 'spotify-application-client'; 7 | 8 | const subscriptions = new CompositeDisposable; 9 | 10 | export function onActivate() { 11 | // Add subscriptions here 12 | subscriptions.add( 13 | atom.commands.add('atom-workspace', { 14 | 'atomify:toggle': () => togglePanel() 15 | }), 16 | atom.commands.add('atom-workspace', { 17 | 'atomify:toggleShuffle': () => SpotifyApplicationClient.toggleShuffle() 18 | }), 19 | atom.commands.add('atom-workspace', { 20 | 'atomify:toggleRepeat': () => SpotifyApplicationClient.toggleRepeat() 21 | }), 22 | atom.commands.add('atom-workspace', { 23 | 'atomify:togglePlayPause': () => SpotifyApplicationClient.togglePlayPause() 24 | }), 25 | atom.commands.add('atom-workspace', { 26 | 'atomify:goToNextTrack': () => SpotifyApplicationClient.playNextTrack() 27 | }), 28 | atom.commands.add('atom-workspace', { 29 | 'atomify:goToPreviousTrack': () => SpotifyApplicationClient.playPreviousTrack() 30 | }), 31 | atom.commands.add('atom-workspace', { 32 | 'atomify:openSpotify': () => SpotifyApplicationClient.activateApplication() 33 | }), 34 | ); 35 | return subscriptions; 36 | } 37 | 38 | export function onDeactivate() { 39 | // 1. remove window listener 40 | window.onresize = null; 41 | // 2. unmount React 42 | unmount(); 43 | // 3. cleanup subscriptions 44 | subscriptions.dispose(); 45 | } 46 | -------------------------------------------------------------------------------- /lib/components/App.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import CurrentTrackDetails from '../containers/CurrentTrackDetails'; 7 | import CurrentPlayerDetails from '../containers/CurrentPlayerDetails'; 8 | 9 | import { SPOTIFY_ICON_URL } from '../constants/IconUrl'; 10 | 11 | const App = React.createClass({ 12 | propTypes: { 13 | shouldDisplay: PropTypes.bool, 14 | isSpotifyOpen: PropTypes.bool, 15 | }, 16 | 17 | getDefaultProps() { 18 | return { 19 | shouldDisplay: false, 20 | isSpotifyOpen: false, 21 | }; 22 | }, 23 | 24 | getDisplayValue() { 25 | return this.props.shouldDisplay ? null : 'none'; 26 | }, 27 | 28 | render() { 29 | return ( 30 |
31 | { this.props.isSpotifyOpen && 32 | 33 | } 34 | { this.props.isSpotifyOpen && 35 | 36 | } 37 | { !this.props.isSpotifyOpen && 38 |
39 | Spotify is not open 40 |
41 | } 42 |
43 | ); 44 | }, 45 | }); 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /lib/components/PlayerDetails.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { SPOTIFY_ICON_URL, SHUFFLE_ICON_URL, REPEAT_ICON_URL, PLAYING_ICON_URL, PAUSED_ICON_URL } from '../constants/IconUrl'; 7 | 8 | const PlayerDetails = React.createClass({ 9 | propTypes: { 10 | playerState: PropTypes.string.isRequired, 11 | isShuffling: PropTypes.bool.isRequired, 12 | isRepeating: PropTypes.bool.isRequired, 13 | }, 14 | 15 | getDefaultProps() { 16 | return { 17 | playerState: 'playing', 18 | isShuffling: false, 19 | isRepeating: false, 20 | }; 21 | }, 22 | 23 | getStateIconSource() { 24 | switch (this.props.playerState) { 25 | case 'playing': 26 | return PLAYING_ICON_URL; 27 | case 'paused': 28 | return PAUSED_ICON_URL; 29 | default: 30 | return null; 31 | } 32 | }, 33 | 34 | render() { 35 | const stateIconSource = this.getStateIconSource(); 36 | 37 | return ( 38 |
39 | 40 | { stateIconSource != null && 41 | 42 | } 43 | { this.props.isShuffling && 44 | 45 | } 46 | { this.props.isRepeating && 47 | 48 | } 49 |
50 | ); 51 | }, 52 | }); 53 | 54 | export default PlayerDetails; 55 | -------------------------------------------------------------------------------- /lib/components/Render.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import * as React from 'react'; 4 | import * as ReactDOM from 'react-dom'; 5 | import { Provider } from 'react-redux' 6 | import { createStore } from 'redux'; 7 | import { SpotifyApplicationClient } from 'spotify-application-client'; 8 | 9 | import Atomify from '../reducers/'; 10 | import CurrentApp from '../containers/CurrentApp'; 11 | import { updateTrackDetails, updatePlayerDetails, updateDisplaySettings, updateSpotifyIsOpen } from '../actions'; 12 | 13 | const store = createStore(Atomify); 14 | 15 | export function render(target: HTMLElement) { 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | target 21 | ); 22 | } 23 | 24 | let root = null; 25 | const rootName = 'atomify'; 26 | 27 | export function unmount() { 28 | ReactDOM.unmountComponentAtNode(root); 29 | } 30 | 31 | export function initRoot(): HTMLElement { 32 | root = document.createElement('div'); 33 | root.setAttribute('id', rootName); 34 | root.hidden = false; 35 | store.dispatch(updateDisplaySettings(!root.hidden)); 36 | return root; 37 | } 38 | 39 | export function togglePanel() { 40 | root.hidden = !root.hidden; 41 | store.dispatch(updateDisplaySettings(root.hidden)); 42 | } 43 | 44 | export function fetchDetails() { 45 | return SpotifyApplicationClient.isSpotifyRunning() 46 | .then(isSpotifyRunning => { 47 | store.dispatch(updateSpotifyIsOpen(isSpotifyRunning)); 48 | if (isSpotifyRunning) { 49 | return Promise.all([fetchTrackDetails(), fetchPlayerDetails()]); 50 | } 51 | 52 | return Promise.resolve('Spotify is not running'); 53 | }).catch(e => console.error(`Error when fetching Spotify Player Information: ${JSON.stringify(e)}`)); 54 | } 55 | 56 | export function fetchTrackDetails() { 57 | return SpotifyApplicationClient.getTrackDetails() 58 | .then((trackDetails) => store.dispatch(updateTrackDetails(trackDetails.name, trackDetails.albumName, trackDetails.artistName, trackDetails.durationInMilliseconds))); 59 | } 60 | 61 | export function fetchPlayerDetails() { 62 | return SpotifyApplicationClient.getPlayerDetails() 63 | .then((details) => store.dispatch(updatePlayerDetails(details.state.value, details.positionInSeconds * 1000, details.isShuffling, details.isRepeating))); 64 | } 65 | -------------------------------------------------------------------------------- /lib/components/TrackDetails.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import moment from 'moment'; 7 | 8 | const TrackDetails = React.createClass({ 9 | propTypes: { 10 | song: PropTypes.string.isRequired, 11 | album: PropTypes.string.isRequired, 12 | artist: PropTypes.string.isRequired, 13 | songDuration: PropTypes.number.isRequired, 14 | songPosition: PropTypes.number.isRequired, 15 | }, 16 | 17 | getDefaultProps() { 18 | return { 19 | song: '', 20 | album: '', 21 | artist: '', 22 | songDuration: 0, 23 | songPosition: 0, 24 | }; 25 | }, 26 | 27 | formatDuration(duration) { 28 | // Hacky as fuck - I hate this... 29 | return moment.utc(duration).format("m:ss"); 30 | }, 31 | 32 | render() { 33 | return ( 34 | 35 | { this.props.song } by { this.props.artist } from { this.props.album } { this.formatDuration(this.props.songPosition) } / { this.formatDuration(this.props.songDuration) } 36 | 37 | ); 38 | }, 39 | }); 40 | 41 | export default TrackDetails; 42 | -------------------------------------------------------------------------------- /lib/constants/ActionType.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export const UPDATE_TRACK_DETAILS = 'UPDATE_TRACK_DETAILS'; 4 | export const UPDATE_PLAYER_DETAILS = 'UPDATE_PLAYER_DETAILS'; 5 | export const UPDATE_DISPLAY_SETTINGS = 'UPDATE_DISPLAY_SETTINGS'; 6 | export const UPDATE_SPOTIFY_IS_OPEN = 'UPDATE_SPOTIFY_IS_OPEN'; 7 | -------------------------------------------------------------------------------- /lib/constants/IconUrl.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export const SPOTIFY_ICON_URL = 'https://raw.githubusercontent.com/jaebradley/atomify/master/images/spotify.png?raw=true'; 4 | export const SHUFFLE_ICON_URL = 'https://raw.githubusercontent.com/jaebradley/atomify/master/images/shuffle.png?raw=true'; 5 | export const REPEAT_ICON_URL = 'https://raw.githubusercontent.com/jaebradley/atomify/master/images/repeat.png?raw=true'; 6 | export const PLAYING_ICON_URL = 'https://raw.githubusercontent.com/jaebradley/atomify/master/images/play.png?raw=true'; 7 | export const PAUSED_ICON_URL = 'https://raw.githubusercontent.com/jaebradley/atomify/master/images/pause.png?raw=true'; 8 | -------------------------------------------------------------------------------- /lib/containers/CurrentApp.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { connect } from 'react-redux' 4 | import App from '../components/App' 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | shouldDisplay: state.CurrentDisplaySettings.shouldDisplay, 9 | isSpotifyOpen: state.CurrentSpotifyState.isSpotifyOpen, 10 | }; 11 | }; 12 | 13 | const CurrentApp = connect( 14 | mapStateToProps 15 | )(App); 16 | 17 | export default CurrentApp; 18 | -------------------------------------------------------------------------------- /lib/containers/CurrentPlayerDetails.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { connect } from 'react-redux' 4 | import PlayerDetails from '../components/PlayerDetails' 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | playerState: state.CurrentPlayerDetails.playerState, 9 | isShuffling: state.CurrentPlayerDetails.isShuffling, 10 | isRepeating: state.CurrentPlayerDetails.isRepeating, 11 | }; 12 | }; 13 | 14 | const CurrentPlayerDetails = connect( 15 | mapStateToProps 16 | )(PlayerDetails); 17 | 18 | export default CurrentPlayerDetails; 19 | -------------------------------------------------------------------------------- /lib/containers/CurrentTrackDetails.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { connect } from 'react-redux' 4 | import TrackDetails from '../components/TrackDetails' 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | song: state.CurrentTrackDetails.song, 9 | album: state.CurrentTrackDetails.album, 10 | artist: state.CurrentTrackDetails.artist, 11 | songDuration: state.CurrentTrackDetails.songDuration, 12 | songPosition: state.CurrentPlayerDetails.songPosition, 13 | } 14 | }; 15 | 16 | const CurrentTrackDetails = connect( 17 | mapStateToProps 18 | )(TrackDetails); 19 | 20 | export default CurrentTrackDetails; 21 | -------------------------------------------------------------------------------- /lib/reducers/CurrentDisplaySettings.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { UPDATE_DISPLAY_SETTINGS } from '../constants/ActionType'; 4 | 5 | const CurrentDisplaySettings = (state = {}, action) => { 6 | switch (action.type) { 7 | case UPDATE_DISPLAY_SETTINGS: 8 | return { 9 | shouldDisplay: action.shouldDisplay, 10 | } 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default CurrentDisplaySettings; 17 | -------------------------------------------------------------------------------- /lib/reducers/CurrentPlayerDetails.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { UPDATE_PLAYER_DETAILS } from '../constants/ActionType'; 4 | 5 | const CurrentPlayerDetails = (state = {}, action) => { 6 | switch (action.type) { 7 | case UPDATE_PLAYER_DETAILS: 8 | return { 9 | playerState: action.playerState, 10 | songPosition: action.songPosition, 11 | isShuffling: action.isShuffling, 12 | isRepeating: action.isRepeating, 13 | } 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default CurrentPlayerDetails; 20 | -------------------------------------------------------------------------------- /lib/reducers/CurrentSpotifyState.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { UPDATE_SPOTIFY_IS_OPEN } from '../constants/ActionType'; 4 | 5 | const CurrentSpotifyState = (state = {}, action) => { 6 | switch (action.type) { 7 | case UPDATE_SPOTIFY_IS_OPEN: 8 | return { 9 | isSpotifyOpen: action.isSpotifyOpen, 10 | } 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default CurrentSpotifyState; 17 | -------------------------------------------------------------------------------- /lib/reducers/CurrentTrackDetails.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { UPDATE_TRACK_DETAILS } from '../constants/ActionType'; 4 | 5 | const CurrentTrackDetails = (state = {}, action) => { 6 | switch (action.type) { 7 | case UPDATE_TRACK_DETAILS: 8 | return { 9 | song: action.song, 10 | album: action.album, 11 | artist: action.artist, 12 | songDuration: action.songDuration, 13 | } 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default CurrentTrackDetails; 20 | -------------------------------------------------------------------------------- /lib/reducers/index.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { combineReducers } from 'redux'; 4 | import CurrentTrackDetails from './CurrentTrackDetails'; 5 | import CurrentPlayerDetails from './CurrentPlayerDetails'; 6 | import CurrentDisplaySettings from './CurrentDisplaySettings'; 7 | import CurrentSpotifyState from './CurrentSpotifyState'; 8 | 9 | const Atomify = combineReducers({ 10 | CurrentTrackDetails, 11 | CurrentPlayerDetails, 12 | CurrentDisplaySettings, 13 | CurrentSpotifyState, 14 | }); 15 | 16 | export default Atomify; 17 | -------------------------------------------------------------------------------- /menus/atomify.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": { 3 | "atom-text-editor": [ 4 | { 5 | "label": "Toggle atomify", 6 | "command": "atomify:toggle" 7 | }, 8 | { 9 | "label": "Toggle Shuffle", 10 | "command": "atomify:toggleShuffle" 11 | }, 12 | { 13 | "label": "Toggle Repeat", 14 | "command": "atomify:toggleRepeat" 15 | }, 16 | { 17 | "label": "Toggle Play/Pause", 18 | "command": "atomify:togglePlayPause" 19 | }, 20 | { 21 | "label": "Go To Next Track", 22 | "command": "atomify:goToNextTrack" 23 | }, 24 | { 25 | "label": "Go To Previous Track", 26 | "command": "atomify:goToPreviousTrack" 27 | } 28 | ] 29 | }, 30 | "menu": [ 31 | { 32 | "label": "Packages", 33 | "submenu": [ 34 | { 35 | "label": "atomify", 36 | "submenu": [ 37 | { 38 | "label": "Toggle", 39 | "command": "atomify:toggle" 40 | }, 41 | { 42 | "label": "Toggle Shuffle", 43 | "command": "atomify:toggleShuffle" 44 | }, 45 | { 46 | "label": "Toggle Repeat", 47 | "command": "atomify:toggleRepeat" 48 | }, 49 | { 50 | "label": "Toggle Play/Pause", 51 | "command": "atomify:togglePlayPause" 52 | }, 53 | { 54 | "label": "Go To Next Track", 55 | "command": "atomify:goToNextTrack" 56 | }, 57 | { 58 | "label": "Go To Previous Track", 59 | "command": "atomify:goToPreviousTrack" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomify", 3 | "version": "0.3.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asap": { 8 | "version": "2.0.6", 9 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 10 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 11 | }, 12 | "buffers": { 13 | "version": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", 14 | "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" 15 | }, 16 | "core-js": { 17 | "version": "1.2.7", 18 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", 19 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" 20 | }, 21 | "create-react-class": { 22 | "version": "15.6.0", 23 | "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.0.tgz", 24 | "integrity": "sha1-q0SEl8JlZuHilBPogyB9V8/nvtQ=", 25 | "requires": { 26 | "fbjs": "0.8.12", 27 | "loose-envify": "1.3.1", 28 | "object-assign": "4.1.1" 29 | }, 30 | "dependencies": { 31 | "fbjs": { 32 | "version": "0.8.12", 33 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", 34 | "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=", 35 | "requires": { 36 | "core-js": "1.2.7", 37 | "isomorphic-fetch": "2.2.1", 38 | "loose-envify": "1.3.1", 39 | "object-assign": "4.1.1", 40 | "promise": "7.3.1", 41 | "setimmediate": "1.0.5", 42 | "ua-parser-js": "0.7.13" 43 | } 44 | }, 45 | "object-assign": { 46 | "version": "4.1.1", 47 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 48 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 49 | } 50 | } 51 | }, 52 | "encoding": { 53 | "version": "0.1.12", 54 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 55 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 56 | "requires": { 57 | "iconv-lite": "0.4.18" 58 | } 59 | }, 60 | "enumify": { 61 | "version": "https://registry.npmjs.org/enumify/-/enumify-1.0.4.tgz", 62 | "integrity": "sha1-K7YmMHHdRVHlTFV1Vwf60kpAzX4=" 63 | }, 64 | "fbjs": { 65 | "version": "0.8.12", 66 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", 67 | "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=", 68 | "requires": { 69 | "core-js": "1.2.7", 70 | "isomorphic-fetch": "2.2.1", 71 | "loose-envify": "1.3.1", 72 | "object-assign": "4.1.1", 73 | "promise": "7.3.1", 74 | "setimmediate": "1.0.5", 75 | "ua-parser-js": "0.7.13" 76 | } 77 | }, 78 | "hoist-non-react-statics": { 79 | "version": "1.2.0", 80 | "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", 81 | "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" 82 | }, 83 | "iconv-lite": { 84 | "version": "0.4.18", 85 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", 86 | "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" 87 | }, 88 | "immutable": { 89 | "version": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz", 90 | "integrity": "sha1-IAgH8Rqw9ycQ6khVQt4IgHX2jNI=" 91 | }, 92 | "invariant": { 93 | "version": "2.2.2", 94 | "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", 95 | "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", 96 | "requires": { 97 | "loose-envify": "1.3.1" 98 | } 99 | }, 100 | "is-stream": { 101 | "version": "1.1.0", 102 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 103 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 104 | }, 105 | "isomorphic-fetch": { 106 | "version": "2.2.1", 107 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 108 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 109 | "requires": { 110 | "node-fetch": "1.7.1", 111 | "whatwg-fetch": "2.0.3" 112 | }, 113 | "dependencies": { 114 | "whatwg-fetch": { 115 | "version": "2.0.3", 116 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", 117 | "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" 118 | } 119 | } 120 | }, 121 | "js-tokens": { 122 | "version": "3.0.2", 123 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 124 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" 125 | }, 126 | "lodash": { 127 | "version": "4.17.4", 128 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 129 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 130 | }, 131 | "lodash-es": { 132 | "version": "4.17.4", 133 | "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", 134 | "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" 135 | }, 136 | "loose-envify": { 137 | "version": "1.3.1", 138 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", 139 | "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", 140 | "requires": { 141 | "js-tokens": "3.0.2" 142 | } 143 | }, 144 | "moment": { 145 | "version": "https://registry.npmjs.org/moment/-/moment-2.18.0.tgz", 146 | "integrity": "sha1-bP7GpJXsqRXQJgCmcCDtmUk3JSw=" 147 | }, 148 | "node-fetch": { 149 | "version": "1.7.1", 150 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", 151 | "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==", 152 | "requires": { 153 | "encoding": "0.1.12", 154 | "is-stream": "1.1.0" 155 | } 156 | }, 157 | "node-osascript": { 158 | "version": "https://registry.npmjs.org/node-osascript/-/node-osascript-1.0.4.tgz", 159 | "integrity": "sha1-2dW4GhPxulwdCeUk0odWV/hNoxc=", 160 | "requires": { 161 | "buffers": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz" 162 | } 163 | }, 164 | "object-assign": { 165 | "version": "4.1.1", 166 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 167 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 168 | }, 169 | "promise": { 170 | "version": "7.3.1", 171 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 172 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 173 | "requires": { 174 | "asap": "2.0.6" 175 | } 176 | }, 177 | "prop-types": { 178 | "version": "15.5.10", 179 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", 180 | "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", 181 | "requires": { 182 | "fbjs": "0.8.12", 183 | "loose-envify": "1.3.1" 184 | }, 185 | "dependencies": { 186 | "fbjs": { 187 | "version": "0.8.12", 188 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", 189 | "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=", 190 | "requires": { 191 | "core-js": "1.2.7", 192 | "isomorphic-fetch": "2.2.1", 193 | "loose-envify": "1.3.1", 194 | "object-assign": "4.1.1", 195 | "promise": "7.3.1", 196 | "setimmediate": "1.0.5", 197 | "ua-parser-js": "0.7.13" 198 | } 199 | }, 200 | "object-assign": { 201 | "version": "4.1.1", 202 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 203 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 204 | } 205 | } 206 | }, 207 | "react": { 208 | "version": "15.6.1", 209 | "resolved": "https://registry.npmjs.org/react/-/react-15.6.1.tgz", 210 | "integrity": "sha1-uqhDTsZ4C96ZfNw4C3nNM7ljk98=", 211 | "requires": { 212 | "create-react-class": "15.6.0", 213 | "fbjs": "0.8.12", 214 | "loose-envify": "1.3.1", 215 | "object-assign": "4.1.1", 216 | "prop-types": "15.5.10" 217 | } 218 | }, 219 | "react-dom": { 220 | "version": "15.6.1", 221 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.1.tgz", 222 | "integrity": "sha1-LLDtQZEDjlPCCes6eaI+Kkz5lHA=", 223 | "requires": { 224 | "fbjs": "0.8.12", 225 | "loose-envify": "1.3.1", 226 | "object-assign": "4.1.1", 227 | "prop-types": "15.5.10" 228 | } 229 | }, 230 | "react-redux": { 231 | "version": "5.0.5", 232 | "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.5.tgz", 233 | "integrity": "sha1-+OjHsjlCJXblLWt9sGQ5RpvphGo=", 234 | "requires": { 235 | "create-react-class": "15.6.0", 236 | "hoist-non-react-statics": "1.2.0", 237 | "invariant": "2.2.2", 238 | "lodash": "4.17.4", 239 | "lodash-es": "4.17.4", 240 | "loose-envify": "1.3.1", 241 | "prop-types": "15.5.10" 242 | } 243 | }, 244 | "redux": { 245 | "version": "3.7.2", 246 | "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", 247 | "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", 248 | "requires": { 249 | "lodash": "4.17.4", 250 | "lodash-es": "4.17.4", 251 | "loose-envify": "1.3.1", 252 | "symbol-observable": "1.0.4" 253 | } 254 | }, 255 | "setimmediate": { 256 | "version": "1.0.5", 257 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 258 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" 259 | }, 260 | "spotify-application-client": { 261 | "version": "https://registry.npmjs.org/spotify-application-client/-/spotify-application-client-1.0.4.tgz", 262 | "integrity": "sha1-KOq/XcVqd/VoDrnPvubwOLN+J4c=", 263 | "requires": { 264 | "enumify": "https://registry.npmjs.org/enumify/-/enumify-1.0.4.tgz", 265 | "immutable": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz", 266 | "node-osascript": "https://registry.npmjs.org/node-osascript/-/node-osascript-1.0.4.tgz" 267 | } 268 | }, 269 | "symbol-observable": { 270 | "version": "1.0.4", 271 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", 272 | "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" 273 | }, 274 | "ua-parser-js": { 275 | "version": "0.7.13", 276 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.13.tgz", 277 | "integrity": "sha1-zZ3S+GSTs/RNvu7zeA/adMXuFL4=" 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomify", 3 | "description": "Where Atom Meets Spotify (For Macs)", 4 | "version": "0.6.0", 5 | "author": "Jae Bradley", 6 | "bugs": "https://github.com/jaebradley/atomify/issues", 7 | "consumedServices": { 8 | "status-bar": { 9 | "versions": { 10 | "^1.0.0": "consumeStatusBar" 11 | } 12 | } 13 | }, 14 | "dependencies": { 15 | "enumify": "^1.0.4", 16 | "immutable": "^3.8.1", 17 | "moment": "^2.18.0", 18 | "node-osascript": "^1.0.4", 19 | "prop-types": "^15.5.10", 20 | "react": "^15.3.0", 21 | "react-dom": "^15.3.0", 22 | "react-redux": "^5.0.5", 23 | "redux": "^3.7.2", 24 | "spotify-application-client": "^1.0.4" 25 | }, 26 | "engines": { 27 | "atom": ">=1.0.0 <2.0.0" 28 | }, 29 | "homepage": "https://github.com/jaebradley/atomify/", 30 | "keywords": [ 31 | "spotify" 32 | ], 33 | "license": "MIT", 34 | "main": "./lib/atom/Main", 35 | "repository": "https://github.com/jaebradley/atomify" 36 | } 37 | -------------------------------------------------------------------------------- /spec/atomify-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Main from '../lib/atom/Main'; 4 | 5 | // Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 6 | // 7 | // To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 8 | // or `fdescribe`). Remove the `f` to unfocus the block. 9 | 10 | describe('Main', () => { }); 11 | -------------------------------------------------------------------------------- /styles/atomify.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | #atomify { 4 | display: inline-block; 5 | margin-left: 10px; 6 | } 7 | 8 | #atomify-player-details { 9 | display: inline-block; 10 | vertical-align: middle; 11 | margin-left: 10px; 12 | } 13 | 14 | #atomify-track-details { 15 | display: inline-block; 16 | vertical-align: middle; 17 | // fixed width because length of music details 18 | // always changes slightly and minor div width 19 | // changes get distracting 20 | max-width: 300px; 21 | // hacky, but to support "Not Open" case 22 | min-width: 25px; 23 | margin-left: 1.5em; 24 | } 25 | 26 | .atomify-player-icon { 27 | margin-left: 1.5em; 28 | vertical-align: middle; 29 | } 30 | --------------------------------------------------------------------------------