├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── Soundnode-app.png ├── app ├── index.html ├── public │ ├── img │ │ ├── logo-badge.png │ │ ├── logo-short.png │ │ ├── logo.150.png │ │ ├── logo.70.png │ │ ├── song-placeholder.png │ │ ├── temp-playing.png │ │ └── temp-user.jpg │ ├── js │ │ ├── about │ │ │ └── aboutCtrl.js │ │ ├── app.js │ │ ├── charts │ │ │ └── chartsCtrl.js │ │ ├── common │ │ │ ├── SC2apiService.js │ │ │ ├── SCapiService.js │ │ │ ├── appCtrl.js │ │ │ ├── configLocation.js │ │ │ ├── copyDirective.js │ │ │ ├── favoriteSongDirective.js │ │ │ ├── modalFactory.js │ │ │ ├── mprisService.js │ │ │ ├── notificationFactory.js │ │ │ ├── openExternalLinkDirective.js │ │ │ ├── osNotificationService.js │ │ │ ├── playerService.js │ │ │ ├── playlistDirective.js │ │ │ ├── queueCtrl.js │ │ │ ├── queueListDirective.js │ │ │ ├── queueService.js │ │ │ ├── repostedSongDirective.js │ │ │ ├── roundFilter.js │ │ │ ├── showMoreDirective.js │ │ │ ├── songDirective.js │ │ │ ├── tracksDirective.js │ │ │ └── utilsService.js │ │ ├── components │ │ │ ├── header │ │ │ │ ├── backForwardButtons.jsx │ │ │ │ ├── headerActions.jsx │ │ │ │ ├── settingsButton.jsx │ │ │ │ ├── settingsDropdown.jsx │ │ │ │ └── windowActions.jsx │ │ │ └── main.jsx │ │ ├── favorites │ │ │ └── favoritesCtrl.js │ │ ├── followers │ │ │ └── followersCtrl.js │ │ ├── following │ │ │ └── followingCtrl.js │ │ ├── news │ │ │ └── newsCtrl.js │ │ ├── player │ │ │ └── playerCtrl.js │ │ ├── playlists │ │ │ ├── collapsibleDirective.js │ │ │ ├── playlistDashboardCtrl.js │ │ │ └── playlistsCtrl.js │ │ ├── profile │ │ │ └── profileCtrl.js │ │ ├── search │ │ │ ├── searchCtrl.js │ │ │ └── searchInputCtrl.js │ │ ├── settings │ │ │ └── settingsCtrl.js │ │ ├── stream │ │ │ └── streamCtrl.js │ │ ├── system │ │ │ ├── guiConfig.js │ │ │ ├── settings.js │ │ │ └── startup.js │ │ ├── tag │ │ │ └── tagCtrl.js │ │ ├── track │ │ │ └── trackCtrl.js │ │ ├── tracks │ │ │ └── tracksCtrl.js │ │ ├── updater │ │ │ └── updaterCtrl.js │ │ └── user │ │ │ └── userCtrl.js │ └── stylesheets │ │ └── sass │ │ ├── _components │ │ ├── _appContainer.scss │ │ ├── _appState.scss │ │ ├── _aside.scss │ │ ├── _buttons.scss │ │ ├── _charts.scss │ │ ├── _config.scss │ │ ├── _default.scss │ │ ├── _dropdown.scss │ │ ├── _following.scss │ │ ├── _loader.scss │ │ ├── _player.scss │ │ ├── _playlist-songs.scss │ │ ├── _profile.scss │ │ ├── _queueList.scss │ │ ├── _search.scss │ │ ├── _settings.scss │ │ ├── _songlist.scss │ │ ├── _spinner.scss │ │ ├── _topFrame.scss │ │ ├── _track.scss │ │ ├── _user.scss │ │ └── _view-container.scss │ │ └── app.scss ├── soundnode.icns ├── soundnode.ico ├── soundnode.png └── views │ ├── about │ └── about.html │ ├── charts │ └── charts.html │ ├── common │ ├── filter-tracks.html │ ├── loading.html │ ├── modals │ │ ├── closeButton.html │ │ ├── confirm.html │ │ ├── default.html │ │ └── rate-limit.html │ ├── queueList.html │ └── tracks.html │ ├── favorites │ └── favorites.html │ ├── followers │ └── followers.html │ ├── following │ └── following.html │ ├── news │ └── news.html │ ├── playlists │ ├── playlistDashboard.html │ └── playlists.html │ ├── profile │ └── profile.html │ ├── search │ └── search.html │ ├── settings │ └── settings.html │ ├── stream │ └── stream.html │ ├── tag │ └── tag.html │ ├── track │ └── track.html │ └── tracks │ └── tracks.html ├── doc └── img │ └── dev_tools.png ├── main.js ├── package.json ├── renovate.json ├── webpack.dev.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | 'es2015', 4 | 'stage-0', 5 | 'react' 6 | ] 7 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | 16 | [COMMIT_EDITMSG] 17 | max_line_length = 0 18 | 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-scoped-var": 1, 4 | "brace-style": [ 5 | 1, 6 | "1tbs" 7 | ], 8 | "camelcase": [ 9 | 1, 10 | { 11 | "properties": "always" 12 | } 13 | ], 14 | "comma-dangle": 1, 15 | "comma-spacing": [ 16 | 1, 17 | { 18 | "before": false, 19 | "after": true 20 | } 21 | ], 22 | "comma-style": [ 23 | 1, 24 | "last" 25 | ], 26 | "eol-last": 0, 27 | "generator-star-spacing": [ 28 | 1, 29 | "after" 30 | ], 31 | "guard-for-in": 1, 32 | "indent": [ 33 | 1, 34 | 2 35 | ], 36 | "no-eq-null": 2, 37 | "no-extra-parens": 1, 38 | "no-lonely-if": 1, 39 | "no-mixed-spaces-and-tabs": [ 40 | 1, 41 | "smart-tabs" 42 | ], 43 | "no-spaced-func": 1, 44 | "no-throw-literal": 2, 45 | "no-trailing-spaces": 0, 46 | "no-underscore-dangle": 0, 47 | "no-undefined": 2, 48 | "no-unused-vars": 1, 49 | "no-var": 1, 50 | "quotes": [ 51 | 0, 52 | "double", 53 | "avoid-escape" 54 | ], 55 | "radix": 1, 56 | "semi-spacing": [ 57 | 1, 58 | { 59 | "before": false, 60 | "after": true 61 | } 62 | ], 63 | "strict": [ 64 | 1 65 | ], 66 | "vars-on-top": 1, 67 | "wrap-iife": 1, 68 | "react/display-name": 0, 69 | "react/jsx-boolean-value": 1, 70 | "jsx-quotes": 1, 71 | "react/jsx-no-undef": 1, 72 | "react/jsx-sort-props": 1, 73 | "react/jsx-sort-prop-types": 1, 74 | "react/jsx-uses-react": 1, 75 | "react/jsx-uses-vars": 1, 76 | "react/no-did-mount-set-state": 1, 77 | "react/no-did-update-set-state": 1, 78 | "react/no-multi-comp": 1, 79 | "react/no-unknown-property": 1, 80 | "react/prop-types": 1, 81 | "react/react-in-jsx-scope": 1, 82 | "react/self-closing-comp": 1, 83 | "react/sort-comp": 1, 84 | "react/wrap-multilines": 1 85 | }, 86 | "env": { 87 | "es6": true, 88 | "browser": true, 89 | "node": true, 90 | "mocha": true 91 | }, 92 | "extends": "eslint:recommended", 93 | "ecmaFeatures": { 94 | "experimentalObjectRestSpread": true, 95 | "modules": true, 96 | "arrowFunctions": true, 97 | "blockBindings": true, 98 | "classes": true, 99 | "defaultParams": true, 100 | "forOf": true, 101 | "generators": true, 102 | "superInFunctions": true, 103 | "templateStrings": true, 104 | "jsx": true 105 | }, 106 | "plugins": [ 107 | "react" 108 | ] 109 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | .sass-cache 5 | dist/ 6 | app/dist/ 7 | report/ 8 | npm-debug.log 9 | app/public/stylesheets/css/app.css 10 | app/public/stylesheets/css/app.css.map 11 | fpm/packages/*.deb 12 | userConfig.json 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | If you would like to contribute to the project please follow the guidelines set out below. Keep in mind that they are not here to make your contribution a painful experience, but to simplify our jobs looking through hundreds of issues and pull requests (making it a 30 minute task instead of a 4 hour job!) 4 | 5 | ## Pull Request 6 | 7 | Pull Request for new features, bugs or translations are often appreciated. However please follow the following guidelines to save as much time as possible for the maintainer. 8 | 9 | - __Make your commit message as descriptive as possible.__ Include as much information as you can. Explain anything that the file diffs themselves won’t make apparent. 10 | - __Document your pull request__. Explain your fix, link to the relevant issue. A pull request without any comment will get closed. 11 | - __Consolidate multiple commits into a single commit when you rebase.__ If you’ve got several commits in your local repository that all have to do with a single change, you can squash multiple commits into a single, clean, descriptive commit when using git-rebase. When you do, good karma is yours. 12 | - __Make sure the target of your pull request is the relevant dev branch__. Most of bugfix or new feature should go to the `master` branch. 13 | - __Include only commits fixing a specific issue__. If your pull request has unrelated commit, it will get closed. 14 | 15 | ### UI changes 16 | 17 | More to come on the official release 18 | 19 | ## Report a bug 20 | 21 | Before reporting any issues, please use the search tools to see if someone filed the same bug before. 22 | 23 | When creating a new issue make sure to include the following: 24 | - Version of Soundnode App used. Are you running from source? Which revision? Are you using a released build? Which release? 25 | - Your environment. What is your operating system? 32 or 64 bits? 26 | - Step to reproduce. Even if the step is only to open the app, __include it!__ Include the actual result and what you expected. 27 | - Messages you get when running from console with the `--debug` parameter. 28 | - A screenshot of any visual bug. 29 | 30 | Here is what a great bug report would look like: 31 | ``` 32 | Song not playing 33 | 34 | Version: Release 0.2.7 for Windows 35 | Downloaded from: soundnodeapp.com 36 | OS: Windows 7 64bits 37 | 38 | How to reproduce: 39 | - Open Soundnode App 40 | - Click on the `first Hip Hop` song in `stream` category 41 | - Click "play" 42 | - Wait for song to start 43 | Actual result: 44 | - the song doesn't start 45 | - not show the current song being play 46 | 47 | Console output: 48 | [6239:0317/031639:INFO:CONSOLE(0)] "event.returnValue is deprecated. Please use the standard event.preventDefault() instead.", source: (0) 49 | ... 50 | ``` 51 | 52 | ## Feature suggestions 53 | 54 | Create a issue with name [FEATURE SUGGESTION] 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://gitter.im/Soundnode/soundnode-app?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | Soundnode App 4 | ============ 5 | 6 | Soundnode App is an Open-Source project to support Soundcloud for desktop Mac, Windows, and Linux. 7 | It's built with Electron, Node.js, Angular.js, and uses the Soundcloud API. 8 | 9 | > Be aware that Soundnode relies on Soundcloud API which only allows third party apps to play 15 thousand tracks daily. When the rate limit is reached all users are blocked from playing/streaming tracks. The stream will be re-enable one day after (at the same time) streams were blocked. 10 | 11 | Follow us on twitter for updates [@Soundnodeapp](https://www.twitter.com/soundnodeapp). 12 | 13 | Featured on [Producthunt](https://www.producthunt.com/tech/soundnode-2), [TNW](http://thenextweb.com/apps/2016/01/25/soundnode-is-the-soundcloud-desktop-app-youve-been-waiting-for/#gref) 14 | and [Gizmodo](http://gizmodo.com/soundnode-turns-soundcloud-into-a-spotify-like-desktop-1754953529) 15 | 16 |  17 | 18 | ## Features 19 | 20 | - No need to install 21 | - Native media keyboard shortcuts 22 | - Search for new songs 23 | - Easy navigation 24 | - Listen to songs from your Stream, Likes, Tracks, Following or Playlists 25 | - Like songs and save to your liked playlist 26 | - Full playlist feature 27 | - Follow/Unfollow users 28 | 29 | And much more! 30 | 31 | ## Configuration 32 | 33 | Since soundcloud applies a rate limit to third party apps, you need to configure your own API key to make soundnode work. 34 | 35 | Unfortunately soundcloud suspended new application creation, so to retrieve your api key, you have to dig into the soundcloud [website](https://soundcloud.com/). 36 | 37 | * Login to soundcloud.com on favorite browser 38 | * Look for an api call and write down the client_id parameter 39 |  40 | * Edit your userConfig.json file (see here for location : https://github.com/eliecharra/soundnode-app/blob/master/app/public/js/common/configLocation.js#L34) and update clientId parameter with the previously retrieved one. 41 | 42 | ## How to contribute 43 | 44 | First, building, testing, and reporting bugs is highly appreciated. Please include the console's output and steps to reproduce the problem in your bug report, if possible. 45 | 46 | If you want to develop, you can look at the issues, especially the bugs, and then fix them. 47 | Here's a [list of issues](https://github.com/Soundnode/soundnode-app/issues?state=open). 48 | 49 | Please follow the [contribution guidelines](https://github.com/Soundnode/soundnode-app/blob/master/CONTRIBUTING.md). 50 | 51 | ## Development 52 | 53 | See the [Development page](https://github.com/Soundnode/soundnode-app/wiki/Development) for a complete guide on how to build 54 | the app locally on your computer. 55 | 56 | Check out [Electron documentation](https://electron.atom.io/docs/) 57 | 58 | ## Supported Platforms 59 | 60 | - Windows 61 | - Mac 62 | - Linux 63 | 64 | ## Author 65 | 66 | - [Michael Lancaster](https://github.com/weblancaster) 67 | 68 | ## Contributors 69 | 70 | Thanks to all [contributors](https://github.com/Soundnode/soundnode-app/graphs/contributors) that are helping or helped making Soundnode better. 71 | 72 | ## License 73 | 74 | GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 [license](https://github.com/Soundnode/soundnode-app/blob/master/LICENSE.md). 75 | -------------------------------------------------------------------------------- /Soundnode-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/Soundnode-app.png -------------------------------------------------------------------------------- /app/public/img/logo-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/logo-badge.png -------------------------------------------------------------------------------- /app/public/img/logo-short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/logo-short.png -------------------------------------------------------------------------------- /app/public/img/logo.150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/logo.150.png -------------------------------------------------------------------------------- /app/public/img/logo.70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/logo.70.png -------------------------------------------------------------------------------- /app/public/img/song-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/song-placeholder.png -------------------------------------------------------------------------------- /app/public/img/temp-playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/temp-playing.png -------------------------------------------------------------------------------- /app/public/img/temp-user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/public/img/temp-user.jpg -------------------------------------------------------------------------------- /app/public/js/about/aboutCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('AboutCtrl', function ( 4 | $scope, 5 | $http, 6 | $rootScope, 7 | ngDialog, 8 | $window 9 | ) { 10 | var urlAbout = 'https://api.github.com/repos/Soundnode/soundnode-about/contents/about.html'; 11 | var urlRelease = 'https://api.github.com/repos/Soundnode/soundnode-app/releases'; 12 | var config = { 13 | headers: { 14 | 'Accept': 'application/vnd.github.v3.raw+json' 15 | } 16 | }; 17 | 18 | $scope.appVersion = $window.settings.appVersion; 19 | $scope.appLatestVersion = ''; 20 | $scope.content = ''; 21 | $scope.isLatest = ($scope.appVersion >= $scope.appLatestVersion); 22 | 23 | $scope.openModal = function () { 24 | ngDialog.open({ 25 | showClose: false, 26 | template: 'views/about/about.html', 27 | scope: $scope 28 | }); 29 | }; 30 | 31 | /** 32 | * Get Soundnode about.html from Github 33 | */ 34 | $http({ 35 | method: 'GET', 36 | url: urlAbout, 37 | headers: config.headers 38 | }).then(function successCallback(response) { 39 | $scope.content = response.data 40 | }, function errorCallback(error) { 41 | console.log('Error retrieving about', error) 42 | }); 43 | 44 | /** 45 | * Get App version from latest release from Github 46 | */ 47 | $http({ 48 | method: 'GET', 49 | url: urlRelease, 50 | headers: config.headers 51 | }).then(function successCallback(response) { 52 | var release = response.data[0]; 53 | $scope.appLatestVersion = release.tag_name; 54 | }, function errorCallback(error) { 55 | console.log('Error retrieving latest release', error); 56 | }); 57 | 58 | }); -------------------------------------------------------------------------------- /app/public/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const guiConfig = require(`${__dirname}/public/js/system/guiConfig.js`).guiConfig; 4 | 5 | var app = angular.module('App', [ 6 | 'ui.router', 7 | 'ngSanitize', 8 | 'cfp.hotkeys', 9 | 'infinite-scroll', 10 | 'ngDialog' 11 | ]); 12 | 13 | app.config(function ( 14 | $stateProvider, 15 | $urlRouterProvider, 16 | hotkeysProvider 17 | ) { 18 | 19 | // Hotkeys config 20 | hotkeysProvider.includeCheatSheet = true; 21 | 22 | // unmatched url redirect to / 23 | $urlRouterProvider.otherwise('/'); 24 | 25 | $stateProvider 26 | .state('stream', { 27 | url: '/', 28 | templateUrl: 'views/stream/stream.html', 29 | controller: 'StreamCtrl' 30 | }) 31 | .state('charts', { 32 | url: '/charts/:genre', 33 | templateUrl: 'views/charts/charts.html', 34 | controller: 'ChartsCtrl' 35 | }) 36 | .state('favorites', { 37 | url: '/favorites', 38 | templateUrl: 'views/favorites/favorites.html', 39 | controller: 'FavoritesCtrl' 40 | }) 41 | .state('tracks', { 42 | url: '/tracks', 43 | templateUrl: 'views/tracks/tracks.html', 44 | controller: 'TracksCtrl' 45 | }) 46 | .state('track', { 47 | url: '/track/:id', 48 | templateUrl: 'views/track/track.html', 49 | controller: 'TrackCtrl' 50 | }) 51 | .state('playlists', { 52 | url: '/playlists', 53 | templateUrl: 'views/playlists/playlists.html', 54 | controller: 'PlaylistsCtrl' 55 | }) 56 | .state('search', { 57 | url: '/search?q', 58 | templateUrl: 'views/search/search.html', 59 | controller: 'searchCtrl' 60 | }) 61 | .state('tag', { 62 | url: '/tag/:name', 63 | templateUrl: 'views/tag/tag.html', 64 | controller: 'tagCtrl' 65 | }) 66 | .state('following', { 67 | url: '/following', 68 | templateUrl: 'views/following/following.html', 69 | controller: 'FollowingCtrl' 70 | }) 71 | .state('followers', { 72 | url: '/followers', 73 | templateUrl: 'views/followers/followers.html', 74 | controller: 'FollowersCtrl' 75 | }) 76 | .state('profile', { 77 | url: '/profile/:id', 78 | templateUrl: 'views/profile/profile.html', 79 | controller: 'ProfileCtrl' 80 | }) 81 | .state('settings', { 82 | url: '/settings', 83 | templateUrl: 'views/settings/settings.html', 84 | controller: 'SettingsCtrl' 85 | }) 86 | .state('news', { 87 | url: '/news', 88 | templateUrl: 'views/news/news.html', 89 | controller: 'NewsCtrl' 90 | }); 91 | }); 92 | 93 | app.run(function ( 94 | $rootScope, 95 | $log, 96 | $state, 97 | SCapiService, 98 | hotkeys, 99 | utilsService, 100 | notificationFactory 101 | ) { 102 | 103 | //start GA 104 | window.settings.visitor.pageview("/").send(); 105 | 106 | // toastr config override 107 | // toastr.options.positionClass = 'toast-top-right'; 108 | // toastr.options.timeOut = 4000; 109 | 110 | $rootScope.oldView = ""; 111 | $rootScope.currentView = ""; 112 | 113 | $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { 114 | $rootScope.oldView = fromState.name; 115 | $rootScope.currentView = toState.name; 116 | 117 | // set GA page/view 118 | if (toState.name === "") { 119 | window.settings.visitor.pageview("/").send(); 120 | } else { 121 | window.settings.visitor.pageview(toState.name).send(); 122 | } 123 | 124 | }); 125 | 126 | // disable cmd (ctrl) + click and middle mouse click to open a new tab/page 127 | document.addEventListener('click', function (e) { 128 | if (e.metaKey || e.ctrlKey || e.which === 2) { 129 | e.preventDefault(); 130 | } 131 | }, false); 132 | 133 | hotkeys.add({ 134 | combo: ['command+w'], 135 | description: 'Minimize window', 136 | callback: function () { 137 | guiConfig.minimize(); 138 | } 139 | }); 140 | 141 | hotkeys.add({ 142 | combo: ['mod+,'], 143 | description: 'Open Settings', 144 | callback: function () { 145 | $state.go('settings'); 146 | } 147 | }); 148 | 149 | function updateOnlineStatus() { 150 | if (!navigator.onLine) { 151 | notificationFactory.warn("Seems like internet connection is down."); 152 | } else { 153 | notificationFactory.success("Awesome! You're connected back to the internet."); 154 | } 155 | } 156 | 157 | window.addEventListener('online', updateOnlineStatus); 158 | window.addEventListener('offline', updateOnlineStatus); 159 | }); 160 | 161 | angular.module('infinite-scroll').value('THROTTLE_MILLISECONDS', 200); 162 | -------------------------------------------------------------------------------- /app/public/js/charts/chartsCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('ChartsCtrl', function ( 4 | $scope, 5 | $filter, 6 | $rootScope, 7 | SCapiService, 8 | SC2apiService, 9 | utilsService, 10 | $stateParams 11 | ) { 12 | var tracksIds = []; 13 | $scope.genres = [ 14 | { 15 | "link": 'all-music', 16 | "title": "All" 17 | }, 18 | { 19 | "link": 'alternativerock', 20 | "title": "Alternative Rock" 21 | }, 22 | { 23 | "link": 'ambient', 24 | "title": "Ambient" 25 | }, 26 | { 27 | "link": 'classical', 28 | "title": "Classical" 29 | }, 30 | { 31 | "link": 'country', 32 | "title": "Country" 33 | }, 34 | { 35 | "link": 'danceedm', 36 | "title": "Dance & EDM" 37 | }, 38 | { 39 | "link": 'dancehall', 40 | "title": "Dancehall" 41 | }, 42 | { 43 | "link": 'deephouse', 44 | "title": "Deep House" 45 | }, 46 | { 47 | "link": 'disco', 48 | "title": "Disco" 49 | }, 50 | { 51 | "link": 'drumbass', 52 | "title": "Drum & Bass" 53 | }, 54 | { 55 | "link": 'dubstep', 56 | "title": "Dubstep" 57 | }, 58 | { 59 | "link": 'electronic', 60 | "title": "Electronic" 61 | }, 62 | { 63 | "link": 'folksingersongwriter', 64 | "title": "Folk & Singer-Songwriter" 65 | }, 66 | { 67 | "link": 'hiphoprap', 68 | "title": "Hip-hop & Rap" 69 | }, 70 | { 71 | "link": 'house', 72 | "title": "House" 73 | }, 74 | { 75 | "link": 'indie', 76 | "title": "Indie" 77 | }, 78 | { 79 | "link": 'jazzblues', 80 | "title": "Jazz & Blues" 81 | }, 82 | { 83 | "link": 'latin', 84 | "title": "Latin" 85 | }, 86 | { 87 | "link": 'metal', 88 | "title": "Metal" 89 | }, 90 | { 91 | "link": 'piano', 92 | "title": "Piano" 93 | }, 94 | { 95 | "link": 'pop', 96 | "title": "Pop" 97 | }, 98 | { 99 | "link": 'rbsoul', 100 | "title": "R&B & Soul" 101 | }, 102 | { 103 | "link": 'reggae', 104 | "title": "Reggae" 105 | }, 106 | { 107 | "link": 'reggaeton', 108 | "title": "Reggaeton" 109 | }, 110 | { 111 | "link": 'rock', 112 | "title": "Rock" 113 | }, 114 | { 115 | "link": 'soundtrack', 116 | "title": "Soundtrack" 117 | }, 118 | { 119 | "link": 'techno', 120 | "title": "Techno" 121 | }, 122 | { 123 | "link": 'trance', 124 | "title": "Trance" 125 | }, 126 | { 127 | "link": 'trap', 128 | "title": "Trap" 129 | }, 130 | { 131 | "link": 'triphop', 132 | "title": "Triphop" 133 | }, 134 | { 135 | "link": 'world', 136 | "title": "World" 137 | } 138 | ]; 139 | 140 | var url_genre = $stateParams.genre; 141 | var genre = {}; 142 | if(!url_genre){ 143 | genre = { 144 | "link" : "all-music", 145 | "title" : "All Music" 146 | } 147 | }else{ 148 | genre = $filter('filter')($scope.genres, {"link":url_genre}, true)[0] 149 | } 150 | 151 | $scope.title = 'Top 50 - '+genre.title; 152 | $scope.data = ''; 153 | $scope.busy = false; 154 | 155 | 156 | SC2apiService.getCharts(genre.link) 157 | .then(filterCollection) 158 | .then(function (collection) { 159 | $scope.data = collection; 160 | loadTracksInfo(collection); 161 | }) 162 | .catch(function (error) { 163 | console.log('error', error); 164 | }) 165 | .finally(function () { 166 | utilsService.updateTracksLikes($scope.data); 167 | utilsService.updateTracksReposts($scope.data); 168 | $rootScope.isLoading = false; 169 | }); 170 | 171 | $scope.loadMore = function() { 172 | if ( $scope.busy ) { 173 | return; 174 | } 175 | $scope.busy = true; 176 | 177 | SC2apiService.getNextPage() 178 | .then(filterCollection) 179 | .then(function (collection) { 180 | $scope.data = $scope.data.concat(collection); 181 | utilsService.updateTracksLikes(collection, true); 182 | utilsService.updateTracksReposts(collection, true); 183 | loadTracksInfo(collection); 184 | }, function (error) { 185 | console.log('error', error); 186 | }).finally(function () { 187 | $scope.busy = false; 188 | $rootScope.isLoading = false; 189 | }); 190 | }; 191 | 192 | function filterCollection(data) { 193 | return data.collection.filter(function (item) { 194 | // Keep only tracks (remove playlists, etc) 195 | var isTrackType = item.type === 'track' || 196 | item.type === 'track-repost' || 197 | !!(item.track && item.track.streamable); 198 | if (!isTrackType) { 199 | return false; 200 | } 201 | 202 | // Filter reposts: display only first appearance of track in stream 203 | var exists = tracksIds.indexOf(item.track.id) > -1; 204 | if (exists) { 205 | return false; 206 | } 207 | 208 | // "stream_url" property is missing in V2 API 209 | item.track.stream_url = item.track.uri + '/stream'; 210 | 211 | tracksIds.push(item.track.id); 212 | return true; 213 | }); 214 | } 215 | 216 | // Load extra information, because SoundCloud v2 API does not return 217 | // number of track likes 218 | function loadTracksInfo(collection) { 219 | var ids = collection.map(function (item) { 220 | return item.track.id; 221 | }); 222 | 223 | SC2apiService.getTracksByIds(ids) 224 | .then(function (tracks) { 225 | // Both collections are unordered 226 | collection.forEach(function (item) { 227 | tracks.forEach(function (track) { 228 | if (item.track.id === track.id) { 229 | item.track.favoritings_count = track.likes_count; 230 | return false; 231 | } 232 | }); 233 | }); 234 | }) 235 | .catch(function (error) { 236 | console.log('error', error); 237 | }); 238 | } 239 | 240 | }); 241 | -------------------------------------------------------------------------------- /app/public/js/common/SC2apiService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Service to work with SoundCloud API v2 4 | // Note: v2 API is not officially supported by SoundCloud yet, 5 | // be careful using these methods, because they might stop working at any moment 6 | 7 | app.service('SC2apiService', function ( 8 | $rootScope, 9 | $window, 10 | $http, 11 | $q, 12 | modalFactory 13 | ) { 14 | 15 | /** 16 | * Soundcloud v2 API endpoint 17 | * @type {String} 18 | */ 19 | var SOUNDCLOUD_API_V2 = 'https://api-v2.soundcloud.com/'; 20 | 21 | /** 22 | * Store URL of the next page, when acessing resource supporting pagination 23 | * @type {String} 24 | */ 25 | var nextPageUrl = ''; 26 | 27 | 28 | // Public API 29 | 30 | /** 31 | * Get current user stream 32 | * @return {promise} 33 | */ 34 | this.getStream = function () { 35 | var params = { 36 | limit: 30 37 | }; 38 | return sendRequest('stream', { params: params }) 39 | .then(onResponseSuccess) 40 | .then(updateNextPageUrl) 41 | .catch(onResponseError); 42 | }; 43 | 44 | /** 45 | * Get charts (top 50) 46 | * @returns {Promise.} 47 | */ 48 | this.getCharts = function (genre) { 49 | // kind=top&genre=soundcloud%3Agenres%3Aall-music&limit=50 50 | 51 | var params = { 52 | kind: 'top', 53 | genre: 'soundcloud:genres:'+genre, 54 | limit: 50 55 | }; 56 | return sendRequest('charts', { params: params }) 57 | .then(onResponseSuccess) 58 | .then(updateNextPageUrl) 59 | .catch(onResponseError); 60 | }; 61 | 62 | /** 63 | * Get next page for last requested resource 64 | * @return {promise} 65 | */ 66 | this.getNextPage = function () { 67 | if (!nextPageUrl) { 68 | return $q.reject('No next page URL'); 69 | } 70 | return sendRequest(nextPageUrl) 71 | .then(onResponseSuccess) 72 | .then(updateNextPageUrl) 73 | .catch(onResponseError); 74 | }; 75 | 76 | /** 77 | * Get extended information about particular tracks 78 | * @param {array} ids - tracks ids 79 | * @return {promise} 80 | */ 81 | this.getTracksByIds = function (ids) { 82 | var params = { 83 | urns: ids.map(function (trackId) { 84 | return 'soundcloud:tracks:' + trackId.toString(); 85 | }).join(',') 86 | }; 87 | return sendRequest('tracks', { params: params }) 88 | .then(onResponseSuccess) 89 | .catch(onResponseError); 90 | }; 91 | 92 | // Private methods 93 | 94 | /** 95 | * Utility method to send http request 96 | * @param {resource} resource - url part with resource name 97 | * @param {object} config - options for $http 98 | * @param {object} options - custom options (show loading, etc) 99 | * @return {promise} 100 | */ 101 | function sendRequest(resource, config, options) { 102 | config = config || {}; 103 | // Check if passed absolute url 104 | if (resource.indexOf('http') === 0) { 105 | config.url = resource; 106 | } else { 107 | config.url = SOUNDCLOUD_API_V2 + resource; 108 | } 109 | config.params = config.params || {}; 110 | config.params.oauth_token = $window.scAccessToken; 111 | 112 | options = options || {}; 113 | if (options.loading !== false) { 114 | $rootScope.isLoading = true; 115 | } 116 | 117 | return $http(config); 118 | } 119 | 120 | /** 121 | * Response success handler 122 | * @param {object} response - $http response object 123 | * @return {object} - response data 124 | */ 125 | function onResponseSuccess(response) { 126 | if (response.status !== 200) { 127 | return $q.reject(response.data); 128 | } 129 | return response.data; 130 | } 131 | 132 | /** 133 | * Response error handler 134 | * @param {object} response - $http response object 135 | * @return {promise} 136 | */ 137 | function onResponseError(response) { 138 | if (response.status === 429) { 139 | modalFactory.rateLimitReached(); 140 | } 141 | return $q.reject(response.data); 142 | } 143 | 144 | /** 145 | * Update value of the next page for paginatable resources 146 | * @param {data} data - response data 147 | * @return {object} - pass through data to use function in promise chain 148 | */ 149 | function updateNextPageUrl(data) { 150 | nextPageUrl = data.next_href || ''; 151 | return data; 152 | } 153 | 154 | }); 155 | -------------------------------------------------------------------------------- /app/public/js/common/appCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('AppCtrl', function ($rootScope, $scope, $window, $log, ngDialog) { 4 | 5 | // check if track has Art work 6 | // otherwise replace to Soundnode App logo 7 | $scope.showBigArtwork = function (img) { 8 | var newArtwork; 9 | if ( ! (angular.isUndefined(img) || img === null) ) { 10 | newArtwork = img.replace('large', 't300x300'); 11 | return newArtwork; 12 | } else { 13 | newArtwork = 'public/img/song-placeholder.png'; 14 | return newArtwork; 15 | } 16 | }; 17 | 18 | // Format song duration on tracks 19 | // for human reading 20 | $scope.formatSongDuration = function (duration) { 21 | var minutes = Math.floor(duration / 60000) 22 | , seconds = ((duration % 60000) / 1000).toFixed(0); 23 | 24 | return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; 25 | }; 26 | 27 | /* 28 | * Navigation back and forward 29 | */ 30 | $scope.goBack = function() { 31 | $window.history.back(); 32 | }; 33 | 34 | $scope.goForward = function() { 35 | $window.history.forward(); 36 | }; 37 | 38 | // Close all open modals 39 | $scope.closeModal = function() { 40 | ngDialog.closeAll(); 41 | }; 42 | }); -------------------------------------------------------------------------------- /app/public/js/common/configLocation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const userHome = require('user-home'); 5 | const mkdirp = require('mkdirp'); 6 | 7 | const configuration = { 8 | 9 | createUserConfig(userConfigPath) { 10 | mkdirp(userConfigPath, err => { 11 | if (err) { 12 | console.error(err); 13 | } 14 | }); 15 | }, 16 | 17 | createIfNotExist(path) { 18 | try { 19 | const pathInfo = fs.statSync(path); 20 | if (!pathInfo.isDirectory()) { 21 | this.createUserConfig(path); 22 | } 23 | } 24 | catch(error) { 25 | this.createUserConfig(path); 26 | } 27 | }, 28 | 29 | /** 30 | * Get the configuration folder location 31 | * 32 | * @returns {string} The folder location of the config 33 | */ 34 | getUserConfig() { 35 | let userConfigPath = null; 36 | 37 | /** Windows platform */ 38 | if (process.platform === 'win32') { 39 | userConfigPath = `${userHome}/.config/Soundnode`; 40 | } 41 | 42 | /** Linux platforms - XDG Standard */ 43 | if (process.platform === 'linux') { 44 | userConfigPath = `${userHome}/.config/Soundnode`; 45 | } 46 | 47 | /** Mac os configuration location */ 48 | if (process.platform === 'darwin') { 49 | userConfigPath = `${userHome}/Library/Preferences/Soundnode`; 50 | } 51 | 52 | /** Unsupported platform */ 53 | if (userConfigPath === null) { 54 | throw `could not set config path for this OS ${process.platform}` 55 | } 56 | 57 | this.createIfNotExist(userConfigPath) 58 | 59 | return userConfigPath; 60 | }, 61 | 62 | /** 63 | * Get the configuration path 64 | * 65 | * @returns {string} The file location of the config 66 | */ 67 | getPath() { 68 | return `${this.getUserConfig()}/userConfig.json` 69 | }, 70 | 71 | /** 72 | * Get the config file 73 | * 74 | * @returns {Object} Parsed version of the saved file 75 | */ 76 | getConfigfile() { 77 | return JSON.parse(fs.readFileSync(`${this.getPath()}`, 'utf-8')); 78 | }, 79 | 80 | /** 81 | * Ensure the config exists 82 | * 83 | * @returns {Boolean} True if the file exists 84 | */ 85 | containsConfig() { 86 | return fs.existsSync(`${this.getPath()}`); 87 | } 88 | 89 | } 90 | 91 | module.exports = configuration; 92 | -------------------------------------------------------------------------------- /app/public/js/common/copyDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('copyDirective', function ( 4 | notificationFactory 5 | ) { 6 | return { 7 | restrict: 'A', 8 | 9 | link: function ($scope, elem, attrs) { 10 | 11 | elem.bind('click', function () { 12 | var info = elem.attr('data-copy'); 13 | var clipboard = gui.Clipboard.get(); 14 | clipboard.set(info, 'text'); 15 | notificationFactory.success("Song url copied to clipboard!"); 16 | }); 17 | } 18 | }; 19 | }); -------------------------------------------------------------------------------- /app/public/js/common/favoriteSongDirective.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | app.directive('favoriteSong', function ( 4 | $rootScope, 5 | SCapiService, 6 | notificationFactory 7 | ) { 8 | return { 9 | restrict: 'A', 10 | scope: { 11 | favorite: "=", 12 | count: "=" 13 | }, 14 | link: function ($scope, elem, attrs) { 15 | var userId, songId; 16 | 17 | elem.bind('click', function () { 18 | userId = $rootScope.userId; 19 | songId = attrs.songId; 20 | 21 | if (this.classList.contains('liked')) { 22 | 23 | SCapiService.deleteFavorite(userId, songId) 24 | .then(function (status) { 25 | if (typeof status == "object") { 26 | notificationFactory.warn("Song removed from likes!"); 27 | $scope.favorite = false; 28 | $scope.count -= 1; 29 | $rootScope.$broadcast("track::unfavorited", songId); 30 | } 31 | }, function () { 32 | notificationFactory.error("Something went wrong!"); 33 | }) 34 | } else { 35 | SCapiService.saveFavorite(userId, songId) 36 | .then(function (status) { 37 | if (typeof status == "object") { 38 | notificationFactory.success("Song added to likes!"); 39 | $scope.favorite = true; 40 | $scope.count += 1; 41 | $rootScope.$broadcast("track::favorited", songId); 42 | } 43 | }, function () { 44 | notificationFactory.error("Something went wrong!"); 45 | }); 46 | } 47 | 48 | }); 49 | } 50 | } 51 | }); -------------------------------------------------------------------------------- /app/public/js/common/modalFactory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const moment = require('moment'); 4 | 5 | app.factory('modalFactory', function ( 6 | $http, 7 | ngDialog 8 | ) { 9 | 10 | var modalFactory = { 11 | // Unified modal to ask for confirmation of some action 12 | confirm: confirm, 13 | // Displays modal with a warning that rate limit is reached, with a link 14 | // to SoundCloud docs attached. Call it when response returns 429 status 15 | rateLimitReached: rateLimitReached 16 | }; 17 | 18 | return modalFactory; 19 | 20 | // 21 | 22 | function confirm(message) { 23 | return ngDialog.openConfirm({ 24 | showClose: false, 25 | template: 'views/common/modals/confirm.html', 26 | controller: ['$scope', function ($scope) { 27 | 28 | $scope.content = message; 29 | 30 | $scope.closeModal = function () { 31 | ngDialog.closeAll(); 32 | }; 33 | }] 34 | }); 35 | } 36 | 37 | function rateLimitReached(timeToReset) { 38 | return ngDialog.open({ 39 | showClose: false, 40 | template: 'views/common/modals/rate-limit.html', 41 | controller: ['$scope', function ($scope) { 42 | const urlGH = 'https://api.github.com/repos/Soundnode/soundnode-about/contents/rate-limit-reached.html'; 43 | const config = { 44 | headers: { 45 | 'Accept': 'application/vnd.github.v3.raw+json' 46 | } 47 | }; 48 | 49 | $scope.content = ''; 50 | $scope.timeToReset = moment().to(timeToReset); 51 | 52 | $scope.closeModal = function () { 53 | ngDialog.closeAll(); 54 | }; 55 | 56 | $http({ 57 | method: 'GET', 58 | url: urlGH, 59 | headers: config.headers 60 | }).then(function (response) { 61 | $scope.content = response.data; 62 | }, function (error) { 63 | console.log('Error', error); 64 | }); 65 | }] 66 | }); 67 | } 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /app/public/js/common/mprisService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { 4 | ipcRenderer 5 | } = require('electron'); 6 | 7 | /** 8 | * mpris integration for linux systems using DBUS 9 | */ 10 | app.factory("mprisService", function ($rootScope, $log, $timeout, $window, $state) { 11 | // media keys are supported on osx/windows already anyway 12 | var supportedPlatforms = { 13 | "linux": false, 14 | "win32": false, 15 | "darwin": false 16 | }; 17 | 18 | // We check if the platform is supported 19 | if (!supportedPlatforms[process.platform]) return false; 20 | 21 | // Require mpris 22 | var Player = require("mpris-service"); 23 | 24 | // Initialize & configure mpris 25 | var mprisPlayer = Player({ 26 | canRaise: true, 27 | name: "soundnode", 28 | identity: "Soundnode", 29 | desktopEntry: "soundnode", 30 | supportedInterfaces: ["player"], 31 | supportedUriSchemes: ["http", "file"], 32 | supportedMimeTypes: ["audio/mpeg", "application/ogg"] 33 | }); 34 | 35 | // Configuration 36 | mprisPlayer.canControl = true; 37 | mprisPlayer.canSeek = false; 38 | 39 | // When the user asks dbus to show the player, we'll show it. 40 | mprisPlayer.on("raise", function () { 41 | gui.Window.get().show(); 42 | }); 43 | 44 | /** Functions **/ 45 | 46 | /** 47 | * This is called whenever you play/resume a track. 48 | * 49 | * @param trackid {number} (SC Track ID) 50 | * @param length {number} (Microseconds - Integer) 51 | * @param artwork {uri} 52 | * @param title {string} 53 | * @param artist {string} 54 | */ 55 | mprisPlayer.play = function (trackid, length, artwork, title, artist) { 56 | /** Overrides **/ 57 | length = 0; // UNSUPPORTED 58 | 59 | mprisPlayer.metadata = { 60 | "mpris:trackid": mprisPlayer.objectPath("track/" + trackid), 61 | "mpris:length": length, 62 | "mpris:artUrl": artwork, 63 | "xesam:title": title, 64 | "xesam:album": "", 65 | "xesam:artist": artist 66 | }; 67 | 68 | // Tell dbus/mpris that we're currently playing. 69 | mprisPlayer.playbackStatus = "Playing"; 70 | }; 71 | 72 | /** 73 | * Tells mpris that we're paused. 74 | */ 75 | mprisPlayer.pause = function () { 76 | mprisPlayer.playbackStatus = "Paused"; 77 | }; 78 | 79 | /** 80 | * This is called whenever you stop playback 81 | */ 82 | mprisPlayer.stop = function () { 83 | mprisPlayer.playbackStatus = "Stopped"; 84 | }; 85 | 86 | // Return the mprisPlayer object 87 | return mprisPlayer; 88 | }); 89 | -------------------------------------------------------------------------------- /app/public/js/common/notificationFactory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toastr = require(`../node_modules/toastr/build/toastr.min.js`); 4 | 5 | app.factory('notificationFactory', function () { 6 | return { 7 | success: function (message) { 8 | toastr.success(message, "Success"); 9 | }, 10 | warn: function (message) { 11 | toastr.warning(message, "Hey"); 12 | }, 13 | info: function (message) { 14 | toastr.info(message, "FYI"); 15 | }, 16 | error: function (message) { 17 | toastr.error(message, "Oh No"); 18 | } 19 | }; 20 | }); -------------------------------------------------------------------------------- /app/public/js/common/openExternalLinkDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | app.directive('openExternal', function () { 4 | return { 5 | restrict: 'A', 6 | link: function ($scope, elem, attrs) { 7 | var el; 8 | 9 | elem.bind('click', function (e) { 10 | e.preventDefault(); 11 | 12 | if ( this.hasAttribute('data-link') ) { 13 | el = attrs.href; 14 | } else { 15 | el = attrs.href + '?client_id=' + window.localStorage.scClientId; 16 | } 17 | gui.Shell.openExternal( el ); 18 | }); 19 | 20 | } 21 | } 22 | }) -------------------------------------------------------------------------------- /app/public/js/common/osNotificationService.js: -------------------------------------------------------------------------------- 1 | app.factory('osNotificationService', function ( 2 | $window 3 | ) { 4 | var songNotification; 5 | var osNotificationService = {}; 6 | 7 | /** 8 | * Check if track title needs to be shortned 9 | * and return 10 | * @param {string} trackTitle (track title) 11 | */ 12 | function shortTrackTitle(trackTitle) { 13 | return (trackTitle.length > 63 && process.platform == "win32") ? trackTitle.substr(0, 60) + "..." : trackTitle; 14 | } 15 | 16 | /** 17 | * Check if user config is enabled to display 18 | * OS native notification 19 | * 20 | * @param {any} trackObj (track object) 21 | */ 22 | osNotificationService.show = function(trackObj) { 23 | if ($window.localStorage.notificationToggle === "true") { 24 | songNotification = new Notification(shortTrackTitle(trackObj.songTitle), { 25 | body: trackObj.songUser, 26 | icon: trackObj.songThumbnail, 27 | silent: true 28 | }); 29 | songNotification.onclick = function () { 30 | ipcRenderer.send('showApp'); 31 | }; 32 | } 33 | } 34 | 35 | return osNotificationService; 36 | }); -------------------------------------------------------------------------------- /app/public/js/common/playlistDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('playlist', function ($rootScope, ngDialog, $log) { 4 | return { 5 | restrict: 'A', 6 | link: function ($scope, elem, attrs) { 7 | $scope.playlistSongId = ''; 8 | $scope.playlistSongName = ''; 9 | 10 | elem.bind('click', function () { 11 | $scope.playlistSongId = attrs.songId; 12 | $scope.playlistSongName = attrs.songName; 13 | 14 | ngDialog.open({ 15 | showClose: false, 16 | scope: $scope, 17 | controller: 'PlaylistDashboardCtrl', 18 | template: 'views/playlists/playlistDashboard.html' 19 | }); 20 | 21 | }); 22 | } 23 | } 24 | }); -------------------------------------------------------------------------------- /app/public/js/common/queueCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('QueueCtrl', function( 4 | $scope, 5 | $rootScope, 6 | queueService, 7 | $log, $timeout, 8 | playerService, 9 | SCapiService, 10 | ngDialog, 11 | notificationFactory, 12 | $location, 13 | $anchorScroll, 14 | utilsService 15 | ) { 16 | $scope.data = queueService.getAll(); 17 | 18 | $scope.$on('activateQueue', function(event, data) { 19 | $scope.activateTrackInQueue(); 20 | }); 21 | 22 | // Listen to DOM mutation and react 23 | function addDOMmutationListener() { 24 | var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 25 | var list = document.querySelector('.queueListView_list'); 26 | 27 | var observer = new MutationObserver(function(mutations) { 28 | $scope.activateTrackInQueue(); 29 | }); 30 | 31 | observer.observe(list, { 32 | childList: true 33 | }); 34 | } 35 | 36 | // quick hack to add DOM mutation listener 37 | // time to wait until page is fully loaded 38 | $timeout(function() { 39 | addDOMmutationListener(); 40 | }, 1000); 41 | 42 | $scope.activateTrackInQueue = function($event) { 43 | if ( $scope.data.length < 1 ) { 44 | return; 45 | } 46 | 47 | var trackId = queueService.getTrack().songId; 48 | var track = document.querySelector('.queueListView_list_item[data-song-id="' + trackId + '"]'); 49 | var oldActive = document.querySelector('.queueListView_list_item.active'); 50 | 51 | if ( oldActive ) { 52 | oldActive.classList.remove('active'); 53 | } 54 | 55 | if ( track ) { 56 | track.classList.add('active'); 57 | } 58 | }; 59 | 60 | /** 61 | * remove track from the queue 62 | * @param $event 63 | */ 64 | $scope.remove = function($event) { 65 | var trackData = $($event.target).closest('.queueListView_list_item').data(); 66 | var trackPosition = queueService.find(trackData.songId); 67 | 68 | queueService.remove(trackPosition); 69 | playerService.playNewSong(); 70 | }; 71 | 72 | /** 73 | * like track from the queue 74 | * @param $event 75 | */ 76 | $scope.like = function($event) { 77 | var trackData = $($event.target).closest('.queueListView_list_item').data(); 78 | var userId = $rootScope.userId; 79 | var songId = trackData.songId; 80 | 81 | SCapiService.saveFavorite(userId, songId) 82 | .then(function(status) { 83 | if ( typeof status == "object" ) { 84 | notificationFactory.success("Song added to likes!"); 85 | utilsService.markTrackAsFavorite(songId); 86 | $rootScope.$broadcast("track::favorited", songId); 87 | } 88 | }, function(status) { 89 | notificationFactory.error("Something went wrong!"); 90 | }); 91 | }; 92 | 93 | /** 94 | * repost track from queue 95 | * @param {object} $event - Angular event 96 | * @return {promise} 97 | */ 98 | $scope.repost = function ($event) { 99 | var trackData = $($event.target).closest('.queueListView_list_item').data(); 100 | var songId = trackData.songId; 101 | 102 | return SCapiService.createRepost(songId) 103 | .then(function (status) { 104 | if (angular.isObject(status)) { 105 | notificationFactory.success('Song added to reposts!'); 106 | utilsService.markTrackAsReposted(songId); 107 | } 108 | }) 109 | .catch(function (status) { 110 | notificationFactory.error('Something went wrong!'); 111 | }); 112 | }; 113 | 114 | /** 115 | * add track to playlist 116 | * @param $event 117 | */ 118 | $scope.addToPlaylist = function($event) { 119 | var trackData = $($event.target).closest('.queueListView_list_item').data(); 120 | 121 | $scope.playlistSongId = trackData.songId; 122 | $scope.playlistSongName = trackData.songTitle; 123 | 124 | ngDialog.open({ 125 | showClose: false, 126 | scope: $scope, 127 | controller: 'PlaylistDashboardCtrl', 128 | template: 'views/playlists/playlistDashboard.html' 129 | }); 130 | }; 131 | 132 | /** 133 | * scroll view to track 134 | * @param $event 135 | */ 136 | $scope.gotoTrack = function($event) { 137 | var trackData = $($event.target).closest('.queueListView_list_item').data(); 138 | var trackId = trackData.songId; 139 | 140 | $location.hash(trackId); 141 | $anchorScroll(); 142 | }; 143 | 144 | /** 145 | * Dynamically change position of menu not to overlap fixed footer and header 146 | * @param {object} $event - Angular event 147 | */ 148 | $scope.menuPosition = function ($event) { 149 | var $hover = $($event.target); 150 | var $menu = $hover.find('.queueListView_list_item_options_list'); 151 | var $arrow = $hover.find('.queueListView_list_item_options_arrow'); 152 | 153 | var menuHeight = $menu.height(); 154 | var arrowHeight = $arrow.outerHeight(); 155 | var headerHeight = $('.topFrame').outerHeight(); 156 | var footerHeight = $('.player_inner').outerHeight(); 157 | var hoverHeight = $hover.outerHeight(); 158 | 159 | // Calculate top and bottom edge position and compare it with current 160 | var borderTop = headerHeight; 161 | var borderBottom = window.innerHeight - footerHeight; 162 | var hoverTop = $hover.offset().top; 163 | 164 | // Arrow in the middle by default 165 | var newTop = -menuHeight / 2 + arrowHeight / 2; 166 | 167 | // If overlapping top border - move list to the bottom 168 | if (hoverTop - menuHeight / 2 < borderTop) { 169 | newTop = -arrowHeight; 170 | // If overlapping bottom border - move list to the top 171 | } else if (hoverTop + hoverHeight + menuHeight / 2 > borderBottom) { 172 | newTop = -menuHeight + arrowHeight; 173 | } 174 | 175 | $menu.css({ top: newTop }); 176 | }; 177 | 178 | }); -------------------------------------------------------------------------------- /app/public/js/common/queueListDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('queueList', function () { 4 | return { 5 | restrict: 'E', 6 | scope: true, 7 | controller: "QueueCtrl", 8 | templateUrl: function(elem, attr){ 9 | return "views/common/queueList.html"; 10 | } 11 | } 12 | }); -------------------------------------------------------------------------------- /app/public/js/common/queueService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.factory('queueService', function() { 4 | /** 5 | * @class Queue 6 | */ 7 | var Queue = {}; 8 | 9 | /** 10 | * Store all tracks from the current view 11 | * where the track is playing 12 | * item track format below 13 | * { 14 | * title: 'track title', 15 | * published_by: 'john doe', 16 | * track_thumb: 'path/to/thumb' 17 | * track_url: 'path/to/track' 18 | * } 19 | * 20 | * @type {Array} 21 | */ 22 | Queue.list = []; 23 | 24 | /** 25 | * Return queue size 26 | * @method size 27 | * @return {Number} [number of items in the list] 28 | */ 29 | Queue.size = function() { 30 | return this.list.length; 31 | }; 32 | 33 | /** 34 | * Clean Queue and reset currentPosition to 0 35 | * @method clear 36 | */ 37 | Queue.clear = function() { 38 | this.list.length = this.currentPosition = 0; 39 | }; 40 | 41 | /** 42 | * Store currentPosition in the list 43 | * @propriety currentPosition 44 | */ 45 | Queue.currentPosition = 0; 46 | 47 | /** 48 | * Push tracks from array to Queue list 49 | * { 50 | * title: 'track title', 51 | * published_by: 'john doe', 52 | * track_thumb: 'path/to/thumb' 53 | * track_url: 'path/to/track' 54 | * } 55 | * @method push 56 | */ 57 | Queue.push = function(tracks) { 58 | for ( var i = 0; i < tracks.length; i++ ) { 59 | this.list.push(tracks[i]); 60 | } 61 | }; 62 | 63 | /** 64 | * Insert single track to Queue list 65 | * { 66 | * title: 'track title', 67 | * published_by: 'john doe', 68 | * track_thumb: 'path/to/thumb' 69 | * track_url: 'path/to/track' 70 | * } 71 | * @param currentElData [track obj] 72 | * @method insert 73 | */ 74 | Queue.insert = function(currentElData) { 75 | if ( this.isEmpty() ) { 76 | this.list.splice(0, 0, currentElData); 77 | return; 78 | } 79 | 80 | var addAt = this.currentPosition + 1; 81 | this.list.splice(addAt, 0, currentElData); 82 | }; 83 | 84 | /** 85 | * Check if Queue list is empty 86 | * @method isEmpty 87 | * @return {Boolean} [false if list isn't empty and true for when list is empty] 88 | */ 89 | Queue.isEmpty = function() { 90 | return this.size() < 1; 91 | }; 92 | 93 | /** 94 | * Decrease (by 1) currentPosition in the list 95 | * if not empty 96 | * @method prev 97 | */ 98 | Queue.prev = function() { 99 | if ( this.currentPosition !== 0 ) { 100 | --this.currentPosition; 101 | } 102 | }; 103 | 104 | /** 105 | * Increase (by 1) currentPosition in the list 106 | * if currentPosition not equal to list size 107 | * @method next 108 | */ 109 | Queue.next = function() { 110 | if ( this.currentPosition !== this.size() ) { 111 | ++this.currentPosition; 112 | } 113 | }; 114 | 115 | /** 116 | * Get item in the list with current position 117 | * @method getTrack 118 | * @return {[type]} [return track at current position specified] 119 | */ 120 | Queue.getTrack = function() { 121 | if ( !this.isEmpty() ) { 122 | return this.list[this.currentPosition]; 123 | } 124 | }; 125 | 126 | /** 127 | * Get all items in Queue list 128 | * @method getAll 129 | * @returns {Array} [All items in list] 130 | */ 131 | Queue.getAll = function() { 132 | return this.list; 133 | }; 134 | 135 | /** 136 | * Get track id and find it 137 | * in the queue list 138 | * if track is in the list return track position 139 | * otherwise return false 140 | * @param id [track containing the track id] 141 | * @method find 142 | * @returns {id} or {false} 143 | */ 144 | Queue.find = function(id) { 145 | 146 | if ( this.isEmpty() ) { 147 | return false; 148 | } 149 | 150 | for ( var i = 0; i < this.list.length; i++ ) { 151 | if ( this.list[i].songId === id ) { 152 | return i; 153 | } 154 | } 155 | }; 156 | 157 | /** 158 | * Remove song from the Queue list by track id 159 | * @param position [track id] 160 | * @method remove 161 | * @returns {false} or {true} 162 | */ 163 | Queue.remove = function(position) { 164 | if ( this.isEmpty() ) { 165 | return false; 166 | } 167 | 168 | this.list.splice(position, 1); 169 | 170 | // keep current playing track in the same position 171 | // after a track removed from the list 172 | if ( this.currentPosition > position ) { 173 | this.currentPosition = --this.currentPosition; 174 | } else if ( this.currentPosition < position ) { 175 | this.currentPosition = this.currentPosition++; 176 | } 177 | }; 178 | 179 | // expose Queue for debugging ONLY 180 | window.Queue = Queue; 181 | 182 | // Make Queue obj accessible 183 | return Queue; 184 | }); -------------------------------------------------------------------------------- /app/public/js/common/repostedSongDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('repostedSong', function ( 4 | $rootScope, 5 | SCapiService, 6 | notificationFactory 7 | ) { 8 | return { 9 | restrict: 'A', 10 | scope: { 11 | reposted: '=' 12 | }, 13 | link: function ($scope, elem, attrs) { 14 | var songId; 15 | 16 | elem.bind('click', function () { 17 | songId = attrs.songId; 18 | 19 | if (this.classList.contains('reposted')) { 20 | 21 | SCapiService.deleteRepost(songId) 22 | .then(function (status) { 23 | if (angular.isObject(status)) { 24 | notificationFactory.warn('Song removed from reposts!'); 25 | $scope.reposted = false; 26 | } 27 | }) 28 | .catch(function () { 29 | notificationFactory.error('Something went wrong!'); 30 | }); 31 | 32 | } else { 33 | 34 | SCapiService.createRepost(songId) 35 | .then(function (status) { 36 | if (angular.isObject(status)) { 37 | notificationFactory.success('Song added to reposts!'); 38 | $scope.reposted = true; 39 | } 40 | }) 41 | .catch(function () { 42 | notificationFactory.error('Something went wrong!'); 43 | }); 44 | 45 | } 46 | 47 | }); 48 | 49 | } 50 | }; 51 | }); -------------------------------------------------------------------------------- /app/public/js/common/roundFilter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.filter('round', function() { 4 | return function(number) { 5 | 6 | if (number < 1000) { 7 | return number 8 | } else if (number < 1000000 ) { 9 | return Math.floor(number/1000) + 'K'; 10 | } else { 11 | return Math.floor(number/1000000) + 'M'; 12 | } 13 | 14 | } 15 | 16 | }); -------------------------------------------------------------------------------- /app/public/js/common/showMoreDirective.js: -------------------------------------------------------------------------------- 1 | app.directive('textCollapse', ['$compile', function ($compile) { 2 | 3 | return { 4 | restrict: 'A', 5 | link: function (scope, element, attrs) { 6 | 7 | var maxHeight = attrs.textMaxHeight; 8 | 9 | scope.collapsed = true; 10 | 11 | scope.toggle = function () { 12 | scope.collapsed = !scope.collapsed; 13 | element.css('height', (scope.collapsed) ? maxHeight : 'auto'); 14 | }; 15 | 16 | attrs.$observe('textToCollapse', function (text) { 17 | element.html(text); 18 | 19 | if (element.clientHeight > maxHeight) { 20 | var toggleButton = $compile('{{collapsed ? "Show more" : "Show less"}}')(scope); 21 | 22 | element.css({ 'height': maxHeight, 'overflow': 'hidden', 'text-overflow': 'elipsis' }); 23 | element.after(toggleButton); 24 | } 25 | }); 26 | } 27 | }; 28 | }]); -------------------------------------------------------------------------------- /app/public/js/common/songDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('song', function ($rootScope, $window, playerService) { 4 | return { 5 | restrict: 'A', 6 | link: function ($scope, elem, attrs ) { 7 | var currentEl; 8 | 9 | elem.bind('click', function () { 10 | currentEl = this; 11 | 12 | $scope.$apply(function() { 13 | playerService.songClicked(currentEl); 14 | }); 15 | }); 16 | 17 | // Updating favorites when they get sent from other scope like the queue, stream, and player 18 | $scope.$on('track::favorited', function(event, trackId) { 19 | if ($scope.data.id == parseInt(trackId)) { 20 | $scope.data.user_favorite = true; 21 | } 22 | }); 23 | $scope.$on('track::unfavorited', function(event, trackId) { 24 | if ($scope.data.id == parseInt(trackId)) { 25 | $scope.data.user_favorite = false; 26 | } 27 | }); 28 | 29 | } 30 | } 31 | }); -------------------------------------------------------------------------------- /app/public/js/common/tracksDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('tracks', function () { 4 | return { 5 | restrict: 'AE', 6 | scope: { 7 | data: '=', 8 | user: '=', 9 | type: '=' 10 | }, 11 | templateUrl: "views/common/tracks.html" 12 | }; 13 | }); -------------------------------------------------------------------------------- /app/public/js/common/utilsService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.factory('utilsService', function ( 4 | queueService, 5 | SCapiService, 6 | $q, 7 | $rootScope, 8 | $timeout, 9 | modalFactory 10 | ) { 11 | /** 12 | * API (helpers/utils) to interact with the UI 13 | * and the rest of the App 14 | * @type {{}} 15 | */ 16 | var Utils = { 17 | /** 18 | * Store cache of fetched likes ids 19 | * @type {Array} 20 | */ 21 | likesIds: [], 22 | /** 23 | * Store cache of fetched reposts ids 24 | * @type {Array} 25 | */ 26 | repostsIds: [] 27 | }; 28 | 29 | /** 30 | * Find track and mark as favorited 31 | * @param trackId (track id) 32 | * @method markTrackAsFavorite 33 | */ 34 | Utils.markTrackAsFavorite = function (trackId) { 35 | var track = document.querySelector('a[favorite-song][data-song-id="' + trackId + '"]'); 36 | track.classList.add('liked'); 37 | //track.setAttribute('favorite', true); 38 | }; 39 | 40 | /** 41 | * Find track and mark as reposted 42 | * @param trackId (track id) 43 | * @method markTrackAsReposted 44 | */ 45 | Utils.markTrackAsReposted = function (trackId) { 46 | var track = document.querySelector('a[reposted-song][data-song-id="' + trackId + '"]'); 47 | track.classList.add('reposted'); 48 | }; 49 | 50 | /** 51 | * Activate track in view based on trackId 52 | * @param trackId [contain track id] 53 | */ 54 | Utils.activateCurrentSong = function (trackId) { 55 | var el = document.querySelector('span[data-song-id="' + trackId + '"]'); 56 | 57 | if (el) { 58 | el.classList.add('currentSong'); 59 | } 60 | }; 61 | 62 | /** 63 | * Responsible to deactivate current song 64 | * (remove class "currentSong" from element) 65 | */ 66 | Utils.deactivateCurrentSong = function () { 67 | var currentSong = this.getCurrentSong(); 68 | 69 | if (currentSong) { 70 | currentSong.classList.remove('currentSong'); 71 | } 72 | }; 73 | 74 | /** 75 | * Responsible to get the current song 76 | * @return {object} [current song object] 77 | */ 78 | Utils.getCurrentSong = function () { 79 | return document.querySelector('.currentSong'); 80 | }; 81 | 82 | /** 83 | * Get a number between min index and max index 84 | * in the Queue array 85 | * @returns {number} [index in array] 86 | */ 87 | Utils.shuffle = function () { 88 | var max = queueService.size() - 1; 89 | var min = 0; 90 | 91 | queueService.currentPosition = Math.floor(Math.random() * (max - min) + min); 92 | }; 93 | 94 | /** 95 | * Get siblings of current song 96 | * @params clickedSong [track DOM element] 97 | * @returns array [sibling of ] 98 | */ 99 | Utils.getSongSiblingsData = function (clickedSong) { 100 | var elCurrentSongParent = $(clickedSong).closest('li'); 101 | var elCurrentSongSiblings = $(elCurrentSongParent).nextAll('li'); 102 | var elCurrentSongSiblingData; 103 | var list = []; 104 | 105 | for (var i = 0; i < elCurrentSongSiblings.length; i++) { 106 | elCurrentSongSiblingData = $(elCurrentSongSiblings[i]).find('.songList_item_song_button').data(); 107 | list.push(elCurrentSongSiblingData); 108 | } 109 | 110 | return list; 111 | }; 112 | 113 | /** 114 | * Fetch ids of liked tracks and apply them to existing collection 115 | * @param {array} collection - stream collection or tracks array 116 | * @param {boolean} fromCache - if should make request to API 117 | * @return {promise} - promise with original collection 118 | */ 119 | Utils.updateTracksLikes = function (collection, fromCache) { 120 | var fetchLikedIds; 121 | 122 | if ( fromCache ) { 123 | fetchLikedIds = $q(function(resolve) { 124 | resolve(Utils.likesIds) 125 | }); 126 | } else { 127 | fetchLikedIds = SCapiService.getFavoritesIds() 128 | } 129 | 130 | return fetchLikedIds.then(function (ids) { 131 | if (!fromCache) { 132 | Utils.likesIds = ids; 133 | } 134 | collection.forEach(function (item) { 135 | var track = item.track || item; 136 | var id = track.id || track.songId; 137 | // modify each track by reference 138 | track.user_favorite = Utils.likesIds.indexOf(id) > -1; 139 | }); 140 | return collection; 141 | }); 142 | }; 143 | 144 | /** 145 | * When manipulating likes after a page is loaded, it's necessary to 146 | * manipulate the likesIds cache when you modify user_favorite like 147 | * above. Used to sync the likes between player and everything else 148 | * @param {number or string} id - the song id to add to likes 149 | */ 150 | Utils.addCachedFavorite = function (id) { 151 | id = parseInt(id); 152 | var index = Utils.likesIds.indexOf(id); 153 | if (index == -1) { 154 | Utils.likesIds.push(id); 155 | } 156 | } 157 | 158 | /** 159 | * When manipulating likes after a page is loaded, it's necessary to 160 | * manipulate the likesIds cache when you modify user_favorite like 161 | * above. Used to sync the likes between player and everything else 162 | * @param {number or string} id - the song id to remove from likes 163 | */ 164 | Utils.removeCachedFavorite = function (id) { 165 | id = parseInt(id); 166 | var index = Utils.likesIds.indexOf(id); 167 | if (index > -1) { 168 | Utils.likesIds.splice(index, 1); 169 | } 170 | } 171 | 172 | /** 173 | * Fetch ids of reposted tracks and apply them to existing collection 174 | * @param {array} collection - stream collection or tracks array 175 | * @param {boolean} fromCache - if should make request to API 176 | * @return {promise} - promise with original collection 177 | */ 178 | Utils.updateTracksReposts = function (collection, fromCache) { 179 | var fetchRepostsIds = fromCache ? 180 | $q(function (resolve) { resolve(Utils.repostsIds) }) : 181 | SCapiService.getRepostsIds(); 182 | return fetchRepostsIds.then(function (ids) { 183 | if (!fromCache) { 184 | Utils.repostsIds = ids; 185 | } 186 | collection.forEach(function (item) { 187 | var track = item.track || item; 188 | // modify each track by reference 189 | track.user_reposted = Utils.repostsIds.indexOf(track.id) > -1; 190 | }); 191 | return collection; 192 | }); 193 | }; 194 | 195 | /** 196 | * Check if current track position is last 197 | * @returns {boolean} 198 | */ 199 | Utils.isLastTrackInQueue = function () { 200 | return queueService.currentPosition === queueService.size() - 1; 201 | }; 202 | 203 | /** 204 | * Scroll main view to bottom 205 | */ 206 | Utils.scrollToBottom = function () { 207 | var $mainView = document.querySelector('.mainView'); 208 | $mainView.scrollTop = $mainView.scrollHeight; 209 | }; 210 | 211 | /** 212 | * Watch for changes on "isLoading" state 213 | * and once that happened click the next tracks 214 | * to last track (not in Queue) to Queue 215 | */ 216 | Utils.addLoadedTracks = function () { 217 | var that = this; 218 | 219 | $rootScope.$watch('isLoading', function (newVal, oldVal) { 220 | if (oldVal) { 221 | $timeout(function () { 222 | var currentSong = that.getCurrentSong(); 223 | 224 | $(currentSong) 225 | .closest('li') 226 | .next() 227 | .find('.songList_item_song_button') 228 | .click(); 229 | }, 0) 230 | } 231 | }); 232 | }; 233 | 234 | /** 235 | * Find current track playing and set as "currentSong" 236 | */ 237 | Utils.setCurrent = function () { 238 | var $track = queueService.getTrack(); 239 | 240 | if ($track !== null && $track != undefined) { 241 | $timeout(function () { 242 | $('#' + $track.songId).addClass('currentSong'); 243 | }, 1500); 244 | } 245 | 246 | }; 247 | 248 | Utils.isPlayable = function (trackUrl) { 249 | return SCapiService.checkRateLimit(trackUrl); 250 | } 251 | 252 | return Utils; 253 | 254 | }); -------------------------------------------------------------------------------- /app/public/js/components/header/backForwardButtons.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class BackForwardActions extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | } 17 | 18 | export default BackForwardActions; -------------------------------------------------------------------------------- /app/public/js/components/header/headerActions.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import BackForwardActions from "./backForwardButtons"; 3 | import WindowActions from "./windowActions"; 4 | 5 | class HeaderActions extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | } 15 | 16 | export default HeaderActions; 17 | -------------------------------------------------------------------------------- /app/public/js/components/header/settingsButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SettingsDropdown from "./settingsDropdown" 3 | 4 | class SettingsButton extends Component { 5 | constructor (props) { 6 | super(props); 7 | 8 | this.state = { 9 | isVisible: false, 10 | isMouseIn: false 11 | }; 12 | 13 | //Close when we click on the UI 14 | window.addEventListener('click', this.closeOut.bind(this), false); 15 | } 16 | 17 | stopClose (e) { 18 | e.stopPropagation(); 19 | }; 20 | 21 | closeOut () { 22 | this.setState({ 23 | isVisible: false 24 | }); 25 | }; 26 | 27 | toggleSettings () { 28 | this.setState({ 29 | isVisible: !this.state.isVisible 30 | }); 31 | }; 32 | 33 | stopEventsAndToggleSettings (e) { 34 | this.stopClose(e); 35 | this.toggleSettings(); 36 | }; 37 | 38 | render () { 39 | return ( 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | export default SettingsButton; -------------------------------------------------------------------------------- /app/public/js/components/header/settingsDropdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class SettingsDropdown extends Component { 4 | render () { 5 | return ( 6 | 7 | 8 | About 9 | 10 | 11 | Settings 12 | 13 | 14 | News 15 | 16 | 17 | Shortcuts (shift + ?) 18 | 19 | 20 | ) 21 | } 22 | } 23 | 24 | export default SettingsDropdown; -------------------------------------------------------------------------------- /app/public/js/components/header/windowActions.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | const guiConfig = require('../../system/guiConfig').guiConfig; 4 | 5 | class WindowActions extends Component { 6 | closeApp() { 7 | guiConfig.close(); 8 | } 9 | 10 | minimizeApp() { 11 | guiConfig.minimize(); 12 | } 13 | 14 | maximizeApp() { 15 | guiConfig.maximize(); 16 | } 17 | 18 | render() { 19 | 20 | if (window.process.platform == 'linux32' 21 | || window.process.platform == 'linux64' 22 | || window.process.platform == 'linux' 23 | || window.process.platform == 'darwin') { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } else if (window.process.platform == 'win32') { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | } 53 | } 54 | 55 | export default WindowActions; -------------------------------------------------------------------------------- /app/public/js/components/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import HeaderActions from "./header/headerActions"; 5 | import SettingsButton from "./header/settingsButton"; 6 | 7 | // While migration is happening 8 | // for every component group 9 | // we will need to render them separately 10 | // once a group is done e.g every component in header 11 | // should combined into one component composing all header 12 | // components 13 | 14 | ReactDOM.render( 15 | , 16 | document.querySelector(".headerActionsApp") 17 | ); 18 | 19 | ReactDOM.render( 20 | , 21 | document.querySelector(".settingsApp") 22 | ); -------------------------------------------------------------------------------- /app/public/js/favorites/favoritesCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('FavoritesCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | SCapiService, 7 | utilsService 8 | ) { 9 | var endpoint = 'me/favorites' 10 | , params = 'linked_partitioning=1'; 11 | 12 | $scope.title = 'Likes'; 13 | $scope.data = ''; 14 | $scope.busy = false; 15 | 16 | SCapiService.get(endpoint, params) 17 | .then(function(data) { 18 | $scope.data = data.collection; 19 | }, function(error) { 20 | console.log('error', error); 21 | }).finally(function() { 22 | utilsService.updateTracksReposts($scope.data); 23 | $rootScope.isLoading = false; 24 | utilsService.setCurrent(); 25 | }); 26 | 27 | $scope.loadMore = function() { 28 | if ( $scope.busy ) { 29 | return; 30 | } 31 | $scope.busy = true; 32 | 33 | SCapiService.getNextPage() 34 | .then(function(data) { 35 | for ( var i = 0; i < data.collection.length; i++ ) { 36 | $scope.data.push( data.collection[i] ) 37 | } 38 | utilsService.updateTracksReposts(data.collection, true); 39 | }, function(error) { 40 | console.log('error', error); 41 | }).finally(function(){ 42 | $scope.busy = false; 43 | $rootScope.isLoading = false; 44 | utilsService.setCurrent(); 45 | }); 46 | }; 47 | 48 | }); -------------------------------------------------------------------------------- /app/public/js/followers/followersCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('FollowersCtrl', function ($scope, SCapiService, $rootScope, $log) { 4 | $scope.title = 'Followers'; 5 | $scope.data = ''; 6 | $scope.busy = false; 7 | 8 | SCapiService.getFollowing('followers') 9 | .then(function(data) { 10 | $scope.data = data.collection.sort( sortBy("username") ); 11 | }, function(error) { 12 | console.log('error', error); 13 | }).finally(function() { 14 | $rootScope.isLoading = false; 15 | }); 16 | 17 | $scope.loadMore = function() { 18 | if ( $scope.busy ) { 19 | return; 20 | } 21 | $scope.busy = true; 22 | 23 | SCapiService.getNextPage() 24 | .then(function(data) { 25 | for ( var i = 0; i < data.collection.length; i++ ) { 26 | $scope.data.push( data.collection[i] ) 27 | } 28 | }, function(error) { 29 | console.log('error', error); 30 | }).finally(function(){ 31 | $scope.busy = false; 32 | $rootScope.isLoading = false; 33 | }); 34 | }; 35 | 36 | function sortBy(prop){ 37 | return function(a,b){ 38 | if( a[prop] > b[prop]){ 39 | return 1; 40 | }else if( a[prop] < b[prop] ){ 41 | return -1; 42 | } 43 | return 0; 44 | } 45 | } 46 | 47 | 48 | }); -------------------------------------------------------------------------------- /app/public/js/following/followingCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('FollowingCtrl', function ($scope, SCapiService, $rootScope, $log) { 4 | $scope.title = 'Following'; 5 | $scope.data = ''; 6 | $scope.busy = false; 7 | 8 | SCapiService.getFollowing('followings') 9 | .then(function(data) { 10 | $scope.data = data.collection.sort( sortBy("username") ); 11 | }, function(error) { 12 | console.log('error', error); 13 | }).finally(function() { 14 | $rootScope.isLoading = false; 15 | }); 16 | 17 | $scope.loadMore = function() { 18 | if ( $scope.busy ) { 19 | return; 20 | } 21 | $scope.busy = true; 22 | 23 | SCapiService.getNextPage() 24 | .then(function(data) { 25 | for ( var i = 0; i < data.collection.length; i++ ) { 26 | $scope.data.push( data.collection[i] ) 27 | } 28 | }, function(error) { 29 | console.log('error', error); 30 | }).finally(function(){ 31 | $scope.busy = false; 32 | $rootScope.isLoading = false; 33 | }); 34 | }; 35 | 36 | function sortBy(prop){ 37 | return function(a,b){ 38 | if( a[prop] > b[prop]){ 39 | return 1; 40 | }else if( a[prop] < b[prop] ){ 41 | return -1; 42 | } 43 | return 0; 44 | } 45 | } 46 | 47 | 48 | }); -------------------------------------------------------------------------------- /app/public/js/news/newsCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('NewsCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | $http 7 | ) { 8 | var urlNews = 'https://api.github.com/repos/Soundnode/soundnode-about/contents/news.html'; 9 | var config = { 10 | headers: { 11 | 'Accept': 'application/vnd.github.v3.raw+json' 12 | } 13 | }; 14 | 15 | $scope.content = ''; 16 | 17 | // get news.html from Github 18 | $http({ 19 | method: 'GET', 20 | url: urlNews, 21 | headers: config.headers 22 | }).then(function (response) { 23 | $scope.content = response.data; 24 | }, function (error) { 25 | console.log('Error', error); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /app/public/js/player/playerCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('PlayerCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | $window, 7 | playerService, 8 | mprisService, 9 | queueService, 10 | hotkeys, 11 | $state, 12 | $log, 13 | $timeout, 14 | SCapiService, 15 | notificationFactory 16 | ) { 17 | $scope.imgPath = 'public/img/temp-playing.png'; 18 | 19 | if ($window.localStorage.volume) { 20 | $scope.volume = +$window.localStorage.volume; 21 | playerService.volume($scope.volume); 22 | } else { 23 | $scope.volume = 1.0; 24 | playerService.volume($scope.volume); 25 | $window.localStorage.volume = +$scope.volume; 26 | } 27 | 28 | /** 29 | * Show/Hide volume range 30 | * @type {exports} 31 | */ 32 | $scope.isVisible = false; 33 | $scope.toggleRange = function () { 34 | $scope.isVisible = !$scope.isVisible; 35 | }; 36 | 37 | /** 38 | * Responsible to send a new volume 39 | * value on range change 40 | * @param volume [value of the volume] 41 | * @method adjustVolume 42 | */ 43 | $scope.adjustVolume = function (volume) { 44 | playerService.volume(volume); 45 | }; 46 | 47 | $scope.playPause = function ($event) { 48 | togglePlayPause(); 49 | }; 50 | 51 | $scope.prevSong = function ($event) { 52 | if ($rootScope.isSongPlaying) { 53 | playerService.playPrevSong(); 54 | } 55 | }; 56 | 57 | $scope.nextSong = function ($event) { 58 | if ($rootScope.isSongPlaying) { 59 | playerService.playNextSong(); 60 | } 61 | }; 62 | 63 | $scope.lock = function ($event) { 64 | var elButton = document.querySelector('.player_lock'); 65 | elButton.classList.toggle('active'); 66 | $rootScope.lock = !$rootScope.lock; 67 | }; 68 | 69 | $scope.repeat = function ($event) { 70 | var elButton = document.querySelector('.player_repeat'); 71 | elButton.classList.toggle('active'); 72 | $rootScope.repeat = !$rootScope.repeat; 73 | }; 74 | 75 | $scope.shuffle = function ($event) { 76 | var elButton = document.querySelector('.player_shuffle'); 77 | elButton.classList.toggle('active'); 78 | $rootScope.shuffle = !$rootScope.shuffle; 79 | }; 80 | 81 | $scope.toggleQueue = function ($event) { 82 | var elButton = document.querySelector('.player_queueList'); 83 | elButton.classList.toggle('active'); 84 | document.querySelector('.queueList').classList.toggle('active'); 85 | }; 86 | 87 | $scope.goToSong = function ($event) { 88 | var trackObj = queueService.getTrack(); 89 | $state.go('track', { id: trackObj.songId }); 90 | }; 91 | 92 | $scope.goToUser = function ($event) { 93 | var trackObj = queueService.getTrack(); 94 | $state.go('profile', { id: trackObj.songUserId }); 95 | }; 96 | 97 | $scope.favorite = function ($event) { 98 | var userId = $rootScope.userId; 99 | var track = queueService.getTrack(); 100 | 101 | if ($event.currentTarget.classList.contains('active')) { 102 | 103 | SCapiService.deleteFavorite(userId, track.songId) 104 | .then(function (status) { 105 | if (typeof status == "object") { 106 | notificationFactory.warn("Song removed from likes!"); 107 | $event.currentTarget.classList.remove('active'); 108 | $rootScope.$broadcast("track::unfavorited", track.songId); 109 | } 110 | }, function () { 111 | notificationFactory.error("Something went wrong!"); 112 | }) 113 | } else { 114 | SCapiService.saveFavorite(userId, track.songId) 115 | .then(function (status) { 116 | if (typeof status == "object") { 117 | notificationFactory.success("Song added to likes!"); 118 | $event.currentTarget.classList.add('active'); 119 | $rootScope.$broadcast("track::favorited", track.songId); 120 | } 121 | }, function (status) { 122 | notificationFactory.error("Something went wrong!"); 123 | }); 124 | } 125 | }; 126 | 127 | // Listen for updates from other scopes about favorites and unfavorites 128 | $scope.$on('track::favorited', function (event, trackId) { 129 | var track = queueService.getTrack(); 130 | if (track && trackId == track.songId) { 131 | var elFavorite = document.querySelector('.player_favorite'); 132 | elFavorite.classList.add('active'); 133 | } 134 | }); 135 | $scope.$on('track::unfavorited', function (event, trackId) { 136 | var track = queueService.getTrack(); 137 | if (track && trackId == track.songId) { 138 | var elFavorite = document.querySelector('.player_favorite'); 139 | elFavorite.classList.remove('active'); 140 | } 141 | }); 142 | 143 | /** 144 | * Used between multiple functions, so we'll leave it here so it reduces 145 | * the amount of times we define it. 146 | */ 147 | var togglePlayPause = function () { 148 | var track = queueService.getTrack(); 149 | 150 | if ($rootScope.isSongPlaying || track == null) { 151 | playerService.pauseSong(); 152 | } else { 153 | playerService.playSong(); 154 | } 155 | }; 156 | 157 | 158 | /* 159 | * Add native media shortcuts 160 | */ 161 | ipcRenderer.on('MediaPlayPause', () => { 162 | $scope.$apply(function () { 163 | togglePlayPause(); 164 | }); 165 | }); 166 | 167 | ipcRenderer.on('MediaStop', () => { 168 | $scope.$apply(function () { 169 | if ($rootScope.isSongPlaying) { 170 | playerService.stopSong(); 171 | } 172 | }); 173 | }); 174 | 175 | ipcRenderer.on('MediaPreviousTrack', () => { 176 | $scope.$apply(function () { 177 | if ($rootScope.isSongPlaying) { 178 | playerService.playPrevSong(); 179 | } 180 | }); 181 | }); 182 | 183 | ipcRenderer.on('MediaNextTrack', () => { 184 | $scope.$apply(function () { 185 | if ($rootScope.isSongPlaying) { 186 | playerService.playNextSong(); 187 | } 188 | }); 189 | }); 190 | 191 | /** 192 | * Add native media shortcuts for linux based systems 193 | */ 194 | if (process.platform === "linux" && mprisService) { 195 | // Set a default state 196 | mprisService.playbackStatus = mprisService.playbackStatus || "Stopped"; 197 | 198 | // When the user toggles play/pause 199 | mprisService.on("playpause", function () { 200 | togglePlayPause(); 201 | }); 202 | 203 | // When the user stops the song 204 | mprisService.on("stop", function () { 205 | playerService.stopSong(); 206 | }); 207 | 208 | // When the user requests next song 209 | mprisService.on("next", function () { 210 | playerService.playNextSong(); 211 | }); 212 | 213 | // When the user requests previous song 214 | mprisService.on("previous", function () { 215 | playerService.playPrevSong(); 216 | }); 217 | 218 | // When the user toggles shuffle 219 | mprisService.on("shuffle", function () { 220 | playerService.shuffle(); 221 | }); 222 | } 223 | 224 | //TODO: replace all hotkeys to native electrom commands 225 | hotkeys.add({ 226 | combo: 'space', 227 | description: 'Play/Pause song', 228 | callback: function (event) { 229 | event.preventDefault(); 230 | togglePlayPause(); 231 | } 232 | }); 233 | 234 | hotkeys.add({ 235 | combo: ['command+return', 'ctrl+return'], 236 | description: 'Play/Pause song', 237 | callback: function () { 238 | togglePlayPause(); 239 | } 240 | }); 241 | 242 | hotkeys.add({ 243 | combo: 'ctrl+right', 244 | description: 'Next song', 245 | callback: function () { 246 | if ($rootScope.isSongPlaying) { 247 | playerService.playNextSong(); 248 | } 249 | } 250 | }); 251 | 252 | hotkeys.add({ 253 | combo: 'ctrl+left', 254 | description: 'Prev song', 255 | callback: function () { 256 | if ($rootScope.isSongPlaying) { 257 | playerService.playPrevSong(); 258 | } 259 | } 260 | }); 261 | 262 | hotkeys.add({ 263 | combo: ['command+up', 'ctrl+up'], 264 | description: 'Volume Up', 265 | callback: function (e) { 266 | e.preventDefault(); 267 | playerService.volume(playerService.volume() + 0.1); 268 | $scope.volume = playerService.volume(); 269 | } 270 | }); 271 | 272 | hotkeys.add({ 273 | combo: ['command+down', 'ctrl+down'], 274 | description: 'Volume Down', 275 | callback: function (e) { 276 | e.preventDefault(); 277 | playerService.volume(playerService.volume() - 0.1); 278 | $scope.volume = playerService.volume(); 279 | } 280 | }); 281 | 282 | hotkeys.add({ 283 | combo: ['command+l', 'ctrl+l'], 284 | description: 'Heart track', 285 | callback: function () { 286 | $timeout(function () { 287 | angular.element(".player_favorite").triggerHandler('click'); 288 | }, 0); 289 | } 290 | }); 291 | 292 | hotkeys.add({ 293 | combo: ['shift+q'], 294 | description: 'Toggle Queue', 295 | callback: function (e) { 296 | e.preventDefault(); 297 | $scope.toggleQueue() 298 | } 299 | }); 300 | 301 | hotkeys.add({ 302 | combo: ['shift+r'], 303 | description: 'Toggle repeat on/off', 304 | callback: function (e) { 305 | e.preventDefault(); 306 | $scope.repeat() 307 | } 308 | }); 309 | 310 | hotkeys.add({ 311 | combo: ['shift+s'], 312 | description: 'Toggle shuffle on/off', 313 | callback: function (e) { 314 | e.preventDefault(); 315 | $scope.shuffle() 316 | } 317 | }); 318 | 319 | hotkeys.add({ 320 | combo: ['shift+l'], 321 | description: 'Toggle lock on/off', 322 | callback: function (e) { 323 | e.preventDefault(); 324 | $scope.lock() 325 | } 326 | }); 327 | 328 | 329 | }); 330 | -------------------------------------------------------------------------------- /app/public/js/playlists/collapsibleDirective.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.directive('collapsible', function ($rootScope) { 4 | return { 5 | restrict: 'A', 6 | link: function ($scope, elem, attrs ) { 7 | 8 | elem.bind('click', function () { 9 | if ( elem.parent().attr('data-playlist-hidden') === 'true' ) { 10 | elem.parent().attr('data-playlist-hidden', 'false'); 11 | elem.children().text('hide'); 12 | } else { 13 | elem.parent().attr('data-playlist-hidden', 'true'); 14 | elem.children().text('show'); 15 | } 16 | }); 17 | 18 | } 19 | } 20 | }); -------------------------------------------------------------------------------- /app/public/js/playlists/playlistDashboardCtrl.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | app.controller('PlaylistDashboardCtrl', function($rootScope, $scope, SCapiService, $log, $window, $http, ngDialog, notificationFactory) { 4 | var endpoint = 'me/playlists' 5 | , params = ''; 6 | 7 | $scope.data = ''; 8 | 9 | SCapiService.get(endpoint, params) 10 | .then(function(data) { 11 | $scope.data = data; 12 | }, function(error) { 13 | $log.log('error', error); 14 | }).finally(function() { 15 | $rootScope.isLoading = false; 16 | }); 17 | 18 | /** 19 | * Responsible to add track to a particular playlist 20 | * @params playlistId [playlist id that contains the track] 21 | * @method saveToPlaylist 22 | */ 23 | $scope.saveToPlaylist = function(playlistId) { 24 | var endpoint = 'users/'+ $rootScope.userId + '/playlists/'+ playlistId 25 | , params = ''; 26 | 27 | SCapiService.get(endpoint, params) 28 | .then(function(response) { 29 | var track = { 30 | "id": Number.parseInt($scope.playlistSongId) 31 | } 32 | , uri = response.uri + '.json?&oauth_token=' + $window.scAccessToken 33 | , tracks = response.tracks; 34 | 35 | tracks.push(track); 36 | 37 | $http.put(uri, { "playlist": { 38 | "tracks": tracks 39 | } 40 | }).then(function(response, status) { 41 | notificationFactory.success("Song added to playlist!"); 42 | }, function(error) { 43 | notificationFactory.error("Something went wrong!"); 44 | $log.log(response); 45 | return $q.reject(response.data); 46 | }).finally(function() { 47 | ngDialog.closeAll(); 48 | }) 49 | 50 | }, function(error) { 51 | 52 | }); 53 | 54 | }; 55 | 56 | /** 57 | * Responsible to create a new playlist 58 | * and save the selected song 59 | * @method createPlaylistAndSaveSong 60 | */ 61 | $scope.createPlaylistAndSaveSong = function() { 62 | SCapiService.createPlaylist($scope.playlistName) 63 | .then(function(response, status) { 64 | $scope.saveToPlaylist(response.id); 65 | notificationFactory.success("New playlist created!"); 66 | }, function(response) { 67 | notificationFactory.error("Something went wrong!"); 68 | $log.log(response); 69 | }) 70 | .finally(function() { 71 | ngDialog.closeAll(); 72 | }); 73 | }; 74 | 75 | // Format song duration on tracks 76 | // for human reading 77 | $scope.formatSongDuration = function(duration) { 78 | var minutes = Math.floor(duration / 60000) 79 | , seconds = ((duration % 60000) / 1000).toFixed(0); 80 | 81 | return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; 82 | }; 83 | 84 | // Close all open modals 85 | $scope.closeModal = function() { 86 | ngDialog.closeAll(); 87 | }; 88 | 89 | /** 90 | * Responsible to check if there's a artwork 91 | * otherwise replace with default badge 92 | * @param thumb [ track artwork ] 93 | */ 94 | $scope.checkForPlaceholder = function (thumb) { 95 | var newSize; 96 | 97 | if ( thumb === null ) { 98 | return 'public/img/logo-badge.png'; 99 | } else { 100 | newSize = thumb.replace('large', 'badge'); 101 | return newSize; 102 | } 103 | } 104 | }); -------------------------------------------------------------------------------- /app/public/js/playlists/playlistsCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('PlaylistsCtrl', function ( 4 | $scope, 5 | SCapiService, 6 | $rootScope, 7 | $log, 8 | $window, 9 | $http, 10 | $state, 11 | $stateParams, 12 | notificationFactory, 13 | modalFactory, 14 | utilsService, 15 | queueService 16 | ) { 17 | var endpoint = 'me/playlists' 18 | , params = 'limit=125'; 19 | 20 | $scope.title = 'Playlists'; 21 | $scope.data = ''; 22 | 23 | SCapiService.get(endpoint, params) 24 | .then(function(data) { 25 | data.forEach(function(playlist, i) { 26 | var l = playlist.tracks.length; 27 | while(l--) { 28 | if(!playlist.tracks[l].streamable) { 29 | data[i].tracks.splice(l, 1); 30 | } 31 | } 32 | }); 33 | $scope.data = data; 34 | }, function(error) { 35 | console.log('error', error); 36 | }).finally(function(){ 37 | $rootScope.isLoading = false; 38 | utilsService.setCurrent(); 39 | }); 40 | 41 | /** 42 | * Responsible to remove track from a particular playlist 43 | * @params songId [track id to be removed from the playlist 44 | * @params playlistId [playlist id that contains the track] 45 | * @method removeFromPlaylist 46 | */ 47 | $scope.removeFromPlaylist = function(songId, playlistId) { 48 | var endpoint = 'users/'+ $rootScope.userId + '/playlists/'+ playlistId 49 | , params = ''; 50 | 51 | SCapiService.get(endpoint, params) 52 | .then(function(response) { 53 | var uri = response.uri + '.json?&oauth_token=' + $window.scAccessToken 54 | , tracks = response.tracks 55 | , songIndex 56 | , i = 0; 57 | 58 | // finding the track index 59 | for ( ; i < tracks.length ; i++ ) { 60 | if ( songId == tracks[i].id ) { 61 | songIndex = i; 62 | } 63 | } 64 | 65 | // Removing the track from the tracks list 66 | tracks.splice(songIndex, 1); 67 | 68 | $http.put(uri, { "playlist": { 69 | "tracks": tracks 70 | } 71 | }).then(function(response) { 72 | notificationFactory.success("Song removed from Playlist!"); 73 | }, function(response) { 74 | notificationFactory.error("Something went wrong!"); 75 | $log.log(response); 76 | return $q.reject(response.data); 77 | }).finally(function() { 78 | var inQueue = queueService.find(songId); 79 | 80 | $('#' + songId).remove(); 81 | 82 | if ( inQueue ) { 83 | queueService.remove(inQueue); 84 | } 85 | }) 86 | 87 | }, function(error) { 88 | console.log('error', error); 89 | }); 90 | 91 | }; 92 | 93 | /** 94 | * Responsible to delete entire playlist 95 | * @params playlistId [playlist id] 96 | * @method removePlaylist 97 | */ 98 | $scope.removePlaylist = function(playlistId) { 99 | modalFactory 100 | .confirm('Do you really want to delete the playlist?') 101 | .then(function () { 102 | SCapiService.removePlaylist(playlistId) 103 | .then(function(response) { 104 | if ( typeof response === 'object' ) { 105 | notificationFactory.success("Playlist removed!"); 106 | } 107 | }, function(error) { 108 | notificationFactory.error("Something went wrong!"); 109 | }) 110 | .finally(function() { 111 | $('#' + playlistId).remove(); 112 | }); 113 | }); 114 | }; 115 | 116 | /** 117 | * Responsible to check if there's a artwork 118 | * otherwise replace with default badge 119 | * @param thumb [ track artwork ] 120 | * @method checkForPlaceholder 121 | */ 122 | $scope.checkForPlaceholder = function (thumb) { 123 | var newSize; 124 | 125 | if ( thumb === null ) { 126 | return 'public/img/logo-badge.png'; 127 | } else { 128 | newSize = thumb.replace('large', 'badge'); 129 | return newSize; 130 | } 131 | }; 132 | }); -------------------------------------------------------------------------------- /app/public/js/profile/profileCtrl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Johannes Sjöberg on 11/11/2014. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | app.controller('ProfileCtrl', function ( 8 | $scope, 9 | $rootScope, 10 | $stateParams, 11 | SCapiService, 12 | utilsService 13 | ) { 14 | 15 | //ctrl variables 16 | var userId = $stateParams.id; 17 | $scope.isFollowing = false; 18 | 19 | //scope variables 20 | $scope.profile_data = ''; 21 | $scope.followers_count = ''; 22 | $scope.busy = false; 23 | //tracks 24 | $scope.data = ''; 25 | $scope.isLoggedInUser = userId == $rootScope.userId; 26 | $scope.follow_button_text = ''; 27 | 28 | 29 | SCapiService.getProfile(userId) 30 | .then(function (data) { 31 | $scope.profile_data = data; 32 | $scope.profile_data.description = (data.description) ? data.description.replace(/\n/g, '') : ''; 33 | $scope.followers_count = numberWithCommas(data.followers_count); 34 | }, function (error) { 35 | console.log('error', error); 36 | }).finally(function () { 37 | $rootScope.isLoading = false; 38 | }); 39 | 40 | SCapiService.getProfileTracks(userId) 41 | .then(function (data) { 42 | $scope.data = data.collection; 43 | }, function (error) { 44 | console.log('error', error); 45 | }).finally(function () { 46 | utilsService.updateTracksReposts($scope.data); 47 | $rootScope.isLoading = false; 48 | utilsService.setCurrent(); 49 | }); 50 | 51 | SCapiService.isFollowing(userId) 52 | .then(function (data) { 53 | $scope.isFollowing = data != 404; 54 | $scope.setFollowButtonText(); 55 | }, function (error) { 56 | console.log('error', error); 57 | }).finally(function () { 58 | $rootScope.isLoading = false; 59 | }); 60 | 61 | $scope.loadMore = function () { 62 | if ($scope.busy) { 63 | return; 64 | } 65 | $scope.busy = true; 66 | 67 | SCapiService.getNextPage() 68 | .then(function (data) { 69 | for (var i = 0; i < data.collection.length; i++) { 70 | $scope.data.push(data.collection[i]); 71 | } 72 | utilsService.updateTracksReposts(data.collection, true); 73 | }, function (error) { 74 | if (error != 'No next page URL') { // not a real error 75 | console.log('error', error); 76 | } 77 | }).finally(function () { 78 | $scope.busy = false; 79 | $rootScope.isLoading = false; 80 | utilsService.setCurrent(); 81 | }); 82 | }; 83 | 84 | $scope.setFollowButtonText = function () { 85 | if ($scope.isLoggedInUser) { 86 | $scope.follow_button_text = 'Logged In'; 87 | } else if ($scope.isFollowing) { 88 | $scope.follow_button_text = 'Following'; 89 | } else { 90 | $scope.follow_button_text = 'Follow'; 91 | } 92 | }; 93 | 94 | $scope.hoverIn = function () { 95 | if ($scope.isFollowing && !$scope.isLoggedInUser) { 96 | $scope.follow_button_text = 'Unfollow'; 97 | } 98 | }; 99 | 100 | $scope.changeFollowing = function () { 101 | if ($scope.isLoggedInUser) { 102 | return // nothing to do 103 | } 104 | if ($scope.isFollowing) { 105 | $scope.isFollowing = false; 106 | SCapiService.unfollowUser(userId) 107 | .then(function () { }, 108 | function (errorResponse) { 109 | $scope.isFollowing = true; 110 | handlerFollowOrUnfollowError(errorResponse); 111 | }).finally(function () { 112 | $scope.setFollowButtonText(); 113 | $rootScope.isLoading = false; 114 | }); 115 | } else { 116 | $scope.isFollowing = true; 117 | SCapiService.followUser(userId) 118 | .then(function () { }, 119 | function (errorResponse) { 120 | $scope.isFollowing = false; 121 | handlerFollowOrUnfollowError(errorResponse) 122 | }).finally(function () { 123 | $scope.setFollowButtonText(); 124 | $rootScope.isLoading = false; 125 | }); 126 | } 127 | $scope.setFollowButtonText(); 128 | }; 129 | 130 | function handlerFollowOrUnfollowError(errorResponse) { 131 | console.log('error', errorResponse); 132 | if (errorResponse.status == 429) { 133 | alertBlockedFollowingFunctionality(errorResponse); 134 | } 135 | } 136 | 137 | function alertBlockedFollowingFunctionality(errorResponse) { 138 | errorResponse.data.errors.forEach(function (error) { 139 | if (error.reason_phrase == "warn: too many followings") { 140 | alert(getErrorText(error.release_at)); 141 | } 142 | }); 143 | } 144 | 145 | function numberWithCommas(x) { 146 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 147 | } 148 | 149 | function getErrorText(releasedAt) { 150 | var date = new Date(releasedAt); 151 | return "Hello, \n\nyour follow/unfollow functionality have been temporarily blocked because your account has previously gotten this warning many times." + 152 | "\n\nAs mentioned in SoundCloud Terms of Use, a high volume of similar actions from an account" + 153 | " in a short period of time will be considered a violation of the anti-spam policies. " + 154 | "As these actions aim to unfairly boost popularity within the community, they are forbidden on the SoundCloud platform." + 155 | "\n\nIt will be possible to follow/unfollow again at " + date.toLocaleString() + "."; 156 | } 157 | 158 | 159 | }); -------------------------------------------------------------------------------- /app/public/js/search/searchCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('searchCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | $http, 7 | $stateParams, 8 | SCapiService, 9 | utilsService 10 | ) { 11 | 12 | $scope.title = 'Results for: ' + $stateParams.q; 13 | $scope.data = ''; 14 | var limit = 20; 15 | 16 | SCapiService.search('tracks', limit, $stateParams.q) 17 | .then(function(data) { 18 | $scope.data = data.collection; 19 | }, function(error) { 20 | console.log('error', error); 21 | }).finally(function(){ 22 | utilsService.updateTracksReposts($scope.data); 23 | $rootScope.isLoading = false; 24 | }); 25 | 26 | $scope.loadMore = function() { 27 | if ($scope.busy) { 28 | return; 29 | } 30 | $scope.busy = true; 31 | 32 | SCapiService.getNextPage() 33 | .then(function(data) { 34 | for ( var i = 0; i < data.collection.length; i++ ) { 35 | $scope.data.push( data.collection[i] ) 36 | } 37 | utilsService.updateTracksReposts(data.collection, true); 38 | }, function(error) { 39 | console.log('error', error); 40 | }).finally(function(){ 41 | $scope.busy = false; 42 | $rootScope.isLoading = false; 43 | }); 44 | }; 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /app/public/js/search/searchInputCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('SearchInputCtrl', function ($scope, $http, $state, $window, SCapiService) { 4 | $scope.title = 'Search results'; 5 | 6 | var isTypeaheadAborted = false; 7 | 8 | $scope.onSubmit = function(keyword) { 9 | if (keyword == void 0 || keyword.length == 0) return; 10 | $state.go('search', {q: keyword}, {reload: true}); 11 | $scope.blurTypeahead(); 12 | isTypeaheadAborted = true; 13 | }; 14 | 15 | $scope.typeahead = function(keyword) { 16 | if (keyword.length == 0) { 17 | $scope.blurTypeahead(); 18 | return 19 | } 20 | var dropdown = document.getElementById('searchDropDown'); 21 | dropdown.innerHTML = ''; 22 | 23 | isTypeaheadAborted = false; 24 | 25 | // search for artists 26 | SCapiService.search('users', 4, keyword) 27 | .then(function(data) { 28 | if (isTypeaheadAborted) { 29 | $scope.blurTypeahead(); 30 | return false; 31 | } 32 | if (data.collection.length < 1) { 33 | var error = document.createElement('div'); 34 | error.innerHTML = 'Unable to find any songs or Users named ' + keyword + ''; 35 | dropdown.appendChild(error); 36 | return false; 37 | } 38 | 39 | var artists = document.createElement('div'); 40 | artists.className = 'artist-container'; 41 | var title = document.createElement('h3'); 42 | title.className = 'dropdown-title'; 43 | title.innerHTML = 'Artists'; 44 | artists.appendChild(title); 45 | 46 | // May not actually be 4 tracks long 47 | for(var i = 0; i < data.collection.length; i++) { 48 | var child = document.createElement('div'); 49 | child.className = 'dropdown-item'; 50 | child.id = data.collection[i].id; 51 | 52 | child.addEventListener("mousedown", function(){ 53 | $state.go('profile', {id: this.id}); 54 | 55 | }); 56 | 57 | child.innerHTML = ' ' + data.collection[i].username +''; 58 | artists.appendChild(child); 59 | } 60 | 61 | dropdown.appendChild(artists); 62 | }) 63 | .catch(function(err) { 64 | console.error(err); 65 | }); 66 | 67 | // search for tracks 68 | SCapiService.search('tracks', 4, keyword) 69 | .then(function(data) { 70 | if (isTypeaheadAborted) { 71 | $scope.blurTypeahead(); 72 | return false; 73 | } 74 | if (data.collection.length < 1) { 75 | return false; 76 | } 77 | var tracks = document.createElement('div'); 78 | tracks.className = 'tracks-container'; 79 | var title = document.createElement('h3'); 80 | title.className = 'dropdown-title'; 81 | title.innerHTML = 'Tracks'; 82 | tracks.appendChild(title); 83 | 84 | // May not actually be 4 tracks long 85 | for(var i = 0; i < data.collection.length; i++) { 86 | var child = document.createElement('div'); 87 | var image = data.collection[i].artwork_url || 'public/img/song-placeholder.png'; 88 | child.className = 'dropdown-item'; 89 | child.id = data.collection[i].id; 90 | 91 | child.innerHTML = ' ' + data.collection[i].title +''; 92 | child.addEventListener("mousedown", function(){ 93 | $state.go('search', {q: keyword}, {reload: true}); 94 | }); 95 | tracks.appendChild(child); 96 | } 97 | 98 | var showAll = document.createElement('h3'); 99 | showAll.className = 'show-all'; 100 | showAll.innerHTML = 'Show All'; 101 | showAll.addEventListener("mousedown", function(){ 102 | $state.go('search', {q: keyword}, {reload: true}); 103 | }); 104 | 105 | dropdown.appendChild(tracks); 106 | dropdown.appendChild(showAll); 107 | }) 108 | .then(function(){ 109 | if (isTypeaheadAborted) { 110 | $scope.blurTypeahead(); 111 | return false; 112 | } 113 | dropdown.style.display = 'block'; 114 | }) 115 | .catch(function(err) { 116 | console.error(err); 117 | }); 118 | }; 119 | 120 | $scope.blurTypeahead = function() { 121 | var dropdown = window.document.getElementById('searchDropDown'); 122 | dropdown.style.display = 'none'; 123 | }; 124 | 125 | $scope.refocusTypehead = function(keyword) { 126 | if(keyword) { 127 | $scope.typeahead(keyword); 128 | } 129 | } 130 | }); 131 | -------------------------------------------------------------------------------- /app/public/js/settings/settingsCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('SettingsCtrl', function ($scope, notificationFactory) { 4 | $scope.title = "Settings"; 5 | $scope.client_id = window.localStorage.scClientId; 6 | 7 | /** 8 | * Enable or disable song notification 9 | */ 10 | if ( window.localStorage.notificationToggle ) { 11 | $scope.notification = JSON.parse(window.localStorage.notificationToggle); 12 | } else { 13 | window.localStorage.notificationToggle = $scope.notification = true; 14 | } 15 | 16 | $scope.notificationSettings = function() { 17 | window.localStorage.notificationToggle = $scope.notification; 18 | }; 19 | 20 | $scope.scClientId = function () { 21 | window.localStorage.scClientId = $scope.client_id; 22 | window.settings.updateUserConfig(); 23 | }; 24 | 25 | /** 26 | * Clea storage which remove everything stored in window.localStorage 27 | */ 28 | $scope.cleanStorage = function() { 29 | window.localStorage.clear(); 30 | guiConfig.logOut(); 31 | } 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /app/public/js/stream/streamCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('StreamCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | SCapiService, 7 | SC2apiService, 8 | utilsService 9 | ) { 10 | var tracksIds = []; 11 | 12 | $scope.title = 'Stream'; 13 | $scope.data = ''; 14 | $scope.busy = false; 15 | 16 | SC2apiService.getStream() 17 | .then(filterCollection) 18 | .then(function (collection) { 19 | $scope.data = collection; 20 | 21 | }) 22 | .catch(function (error) { 23 | console.log('error', error); 24 | }) 25 | .finally(function () { 26 | utilsService.updateTracksLikes($scope.data); 27 | utilsService.updateTracksReposts($scope.data); 28 | $rootScope.isLoading = false; 29 | }); 30 | 31 | $scope.loadMore = function() { 32 | if ( $scope.busy ) { 33 | return; 34 | } 35 | $scope.busy = true; 36 | 37 | SC2apiService.getNextPage() 38 | .then(filterCollection) 39 | .then(function (collection) { 40 | $scope.data = $scope.data.concat(collection); 41 | utilsService.updateTracksLikes(collection, true); 42 | utilsService.updateTracksReposts(collection, true); 43 | }, function (error) { 44 | console.log('error', error); 45 | }).finally(function () { 46 | $scope.busy = false; 47 | $rootScope.isLoading = false; 48 | }); 49 | }; 50 | 51 | function filterCollection(data) { 52 | return data.collection.filter(function (item) { 53 | // Keep only tracks (remove playlists, etc) 54 | var isTrackType = item.type === 'track' || 55 | item.type === 'track-repost' || 56 | !!(item.track && item.track.streamable); 57 | if (!isTrackType) { 58 | return false; 59 | } 60 | 61 | // Filter reposts: display only first appearance of track in stream 62 | var exists = tracksIds.indexOf(item.track.id) > -1; 63 | if (exists) { 64 | return false; 65 | } 66 | 67 | // "stream_url" property is missing in V2 API 68 | item.track.stream_url = item.track.uri + '/stream'; 69 | 70 | tracksIds.push(item.track.id); 71 | return true; 72 | }); 73 | } 74 | 75 | }); -------------------------------------------------------------------------------- /app/public/js/system/guiConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { 4 | ipcRenderer 5 | } = require('electron'); 6 | const fs = require('fs-extra'); 7 | const configuration = require('../common/configLocation'); 8 | 9 | let guiConfig = {}; 10 | 11 | // close the App 12 | guiConfig.close = function () { 13 | ipcRenderer.send('closeApp'); 14 | }; 15 | 16 | // quit hard 17 | guiConfig.destroy = function () { 18 | ipcRenderer.send('destroyApp'); 19 | }; 20 | 21 | // minimize the App 22 | guiConfig.minimize = function () { 23 | ipcRenderer.send('minimizeApp'); 24 | }; 25 | 26 | // maximize the App 27 | guiConfig.maximize = function () { 28 | ipcRenderer.send('maximizeApp'); 29 | }; 30 | 31 | guiConfig.logOut = function () { 32 | fs.removeSync(configuration.getPath()); 33 | guiConfig.destroy(); 34 | }; 35 | 36 | module.exports = { 37 | guiConfig: guiConfig 38 | } 39 | -------------------------------------------------------------------------------- /app/public/js/system/settings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ua = require('universal-analytics'); 4 | const configuration = require('../common/configLocation'); 5 | const userConfig = configuration.getConfigfile(); 6 | const fs = require('fs-extra'); 7 | 8 | // Set up some core settings 9 | window.settings = {}; 10 | 11 | // App version 12 | window.settings.appVersion = '7.0.0'; 13 | 14 | // GA >> DO NOT CHANGE OR USE THIS CODE << 15 | window.settings.visitor = ua('UA-67310953-1'); 16 | 17 | // set window access token 18 | window.scAccessToken = userConfig.accessToken; 19 | 20 | // set window clientId 21 | window.localStorage.setItem('scClientId', userConfig.clientId); 22 | 23 | window.settings.updateUserConfig = function () { 24 | fs.writeFileSync(configuration.getPath(), JSON.stringify({ 25 | accessToken: userConfig.accessToken, 26 | clientId: window.localStorage.scClientId 27 | }), 'utf-8'); 28 | } 29 | -------------------------------------------------------------------------------- /app/public/js/system/startup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function startApp() { 4 | setTimeout(function () { 5 | angular.bootstrap(document, ['App']); 6 | document.body.setAttribute('data-isVisible', 'true'); 7 | }, 2000); 8 | } 9 | 10 | startApp(); -------------------------------------------------------------------------------- /app/public/js/tag/tagCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('tagCtrl', function ( 4 | $scope, 5 | $rootScope, 6 | $http, 7 | $stateParams, 8 | SCapiService, 9 | utilsService 10 | ) { 11 | var tagUrl = encodeURIComponent($stateParams.name); 12 | 13 | $scope.tag = $stateParams.name; 14 | $scope.data = ''; 15 | 16 | SCapiService.get('search/sounds', 'limit=32&q=*&filter.genre_or_tag=' + tagUrl) 17 | .then(function (data) { 18 | $scope.data = data.collection; 19 | }, function (error) { 20 | console.log('error', error); 21 | }).finally(function () { 22 | utilsService.updateTracksReposts($scope.data); 23 | $rootScope.isLoading = false; 24 | }); 25 | 26 | $scope.loadMore = function () { 27 | if ( $scope.busy || !SCapiService.next_page) { 28 | return; 29 | } 30 | $scope.busy = true; 31 | 32 | SCapiService.getNextPage() 33 | .then(function (data) { 34 | for (var i = 0; i < data.collection.length; i++) { 35 | $scope.data.push(data.collection[i]); 36 | } 37 | utilsService.updateTracksReposts(data.collection, true); 38 | }, function (error) { 39 | console.log('error', error); 40 | }).finally(function () { 41 | $scope.busy = false; 42 | $rootScope.isLoading = false; 43 | }); 44 | }; 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /app/public/js/track/trackCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('TrackCtrl', function ( 4 | $scope, 5 | SCapiService, 6 | $rootScope, 7 | $stateParams, 8 | utilsService 9 | ) { 10 | var songId = $stateParams.id; 11 | $scope.hover = false; 12 | 13 | $scope.track = ''; 14 | $scope.busy = false; 15 | 16 | SCapiService.get('tracks/' + songId) 17 | .then(function(data) { 18 | data.description = data.description.replace(/\n/g, ''); 19 | 20 | data.created_at = data.created_at.replace(' +0000', ''); 21 | 22 | if (data.tag_list.length > 0) { 23 | data.tag_list = data.tag_list.match(/"[^"]*"|[^\s"]+/g); 24 | 25 | for (var i = 0; i < data.tag_list.length; ++i) { 26 | data.tag_list[i] = data.tag_list[i].replace(/"/g, ''); 27 | } 28 | } else { 29 | data.tag_list = []; 30 | } 31 | 32 | data.tag_list.unshift(data.genre); 33 | 34 | $scope.track = data; 35 | }, function(error) { 36 | console.log('error', error); 37 | }).finally(function() { 38 | $rootScope.isLoading = false; 39 | utilsService.setCurrent(); 40 | }); 41 | 42 | SCapiService.get('tracks/' + songId + '/comments', 'linked_partitioning=1&limit=30') 43 | .then(function(data) { 44 | $scope.comments = data.collection; 45 | }, function(error) { 46 | console.log('error', error); 47 | }).finally(function() { 48 | $rootScope.isLoading = false; 49 | }); 50 | 51 | $scope.loadMore = function() { 52 | if ( $scope.busy ) { 53 | return; 54 | } 55 | $scope.busy = true; 56 | 57 | SCapiService.getNextPage() 58 | .then(function(data) { 59 | for ( var i = 0; i < data.collection.length; i++ ) { 60 | $scope.comments.push( data.collection[i] ) 61 | } 62 | }, function(error) { 63 | console.log('error', error); 64 | }).finally(function(){ 65 | $scope.busy = false; 66 | $rootScope.isLoading = false; 67 | utilsService.setCurrent(); 68 | }); 69 | }; 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /app/public/js/tracks/tracksCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | app.controller('TracksCtrl', function ($scope, SCapiService, $rootScope) { 4 | var endpoint = 'me/tracks' 5 | , params = 'limit=33'; 6 | 7 | $scope.title = 'Tracks'; 8 | $scope.data = ''; 9 | $scope.busy = false; 10 | 11 | SCapiService.get(endpoint, params) 12 | .then(function(data) { 13 | $scope.data = data; 14 | }, function(error) { 15 | console.log('error', error); 16 | }).finally(function() { 17 | $rootScope.isLoading = false; 18 | }); 19 | 20 | $scope.loadMore = function() { 21 | if ( $scope.busy ) { 22 | return; 23 | } 24 | $scope.busy = true; 25 | 26 | SCapiService.getNextPage() 27 | .then(function(data) { 28 | for ( var i = 0; i < data.length; i++ ) { 29 | $scope.data.push( data[i] ) 30 | } 31 | }, function(error) { 32 | console.log('error', error); 33 | }).finally(function(){ 34 | $scope.busy = false; 35 | $rootScope.isLoading = false; 36 | }); 37 | }; 38 | 39 | }); -------------------------------------------------------------------------------- /app/public/js/updater/updaterCtrl.js: -------------------------------------------------------------------------------- 1 | app.controller('UpdaterCtrl', function ($scope, $http, $window) { 2 | var url = 'https://api.github.com/repos/Soundnode/soundnode-app/releases'; 3 | var config = { 4 | headers: { 5 | 'Accept': 'application/vnd.github.v3.raw+json' 6 | } 7 | }; 8 | 9 | $scope.updateAvailable = false; 10 | 11 | $http({ 12 | method: 'GET', 13 | url: url, 14 | headers: config.headers 15 | }).then(function successCallback(response) { 16 | var release = response.data[0]; 17 | var isMasterRelease = release.target_commitish === 'master'; 18 | 19 | if (isMasterRelease) { 20 | if ($window.settings.appVersion < release.tag_name) { 21 | $scope.updateAvailable = true; 22 | } 23 | } 24 | }, function errorCallback(error) { 25 | console.log('Error checking if is a new version available', error); 26 | }); 27 | }); -------------------------------------------------------------------------------- /app/public/js/user/userCtrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.controller('UserCtrl', function ( 4 | $rootScope, 5 | $scope, 6 | $window, 7 | $state, 8 | SCapiService 9 | ) { 10 | var endpoint = 'me'; 11 | var params = ''; 12 | 13 | $rootScope.userId = ''; 14 | 15 | SCapiService.get(endpoint, params) 16 | .then(function (data) { 17 | $rootScope.userId = data.id; 18 | $scope.data = data; 19 | }, function () { 20 | guiConfig.logOut(); 21 | }); 22 | 23 | $scope.logOut = function () { 24 | $window.localStorage.clear(); 25 | guiConfig.logOut(); 26 | } 27 | 28 | $scope.loadUserProfile = function () { 29 | $state.go('profile', { id: $rootScope.userId }); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_appContainer.scss: -------------------------------------------------------------------------------- 1 | .ui_app { 2 | padding-top: 50px; 3 | padding-bottom: 66px; 4 | background: $bodyBackground; 5 | overflow-x: hidden; 6 | position: relative; 7 | height: 100%; 8 | } 9 | 10 | .fa { 11 | color: #fff; 12 | } 13 | 14 | /* 15 | * Modal/Dialog 16 | */ 17 | .modalView { 18 | font-size: 15px; 19 | } 20 | .modalView_content { 21 | 22 | p, 23 | strong { 24 | margin: 10px 0; 25 | } 26 | ul { 27 | margin: 10px 0; 28 | } 29 | 30 | } 31 | .closeModal { 32 | text-align: right; 33 | display: block; 34 | font-size: 25px; 35 | margin: 0 0 20px 0; 36 | 37 | i { 38 | cursor: pointer; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_appState.scss: -------------------------------------------------------------------------------- 1 | body[data-isVisible="false"] { 2 | & .ui_app { 3 | display: none; 4 | } 5 | & .box-loader { 6 | display: block; 7 | } 8 | } 9 | 10 | body[data-isVisible="true"] { 11 | & .ui_app { 12 | display: block; 13 | } 14 | & .box-loader { 15 | display: none; 16 | } 17 | } 18 | 19 | /* playing/ not playing state */ 20 | .fa-pause { 21 | display: none; 22 | } 23 | 24 | .songPlaying { 25 | & .currentSong { 26 | & .fa:first-child { 27 | display: none; 28 | } 29 | 30 | & .fa:last-child { 31 | display: table-cell; 32 | vertical-align: middle; 33 | } 34 | } 35 | 36 | .player_controls { 37 | .player_play-pause { 38 | .fa-play { 39 | display: none; 40 | } 41 | .fa-pause { 42 | display: block; 43 | } 44 | } 45 | } 46 | } 47 | 48 | /* sub navigation states */ 49 | .subNav_nav[data-isVisible="false"] { 50 | display: none; 51 | } 52 | 53 | .subNav_nav[data-isVisible="true"] { 54 | display: block; 55 | } 56 | 57 | .subNav { 58 | position: relative; 59 | } 60 | 61 | .subNav .subNav_button { 62 | display: block; 63 | 64 | & i { 65 | display: block; 66 | color: $defaultColor; 67 | cursor: pointer; 68 | font-size: 20px; 69 | } 70 | &:hover i { 71 | color: #fff; 72 | } 73 | } 74 | 75 | .subNav_nav { 76 | background: $sectionBackground; 77 | border-right: 1px solid $separatorDarkColor; 78 | box-shadow: 0 0 7px 0px #000; 79 | padding: 5px 20px; 80 | border-radius: 5px; 81 | position: absolute; 82 | top: 25px; 83 | right: 0; 84 | } 85 | 86 | .subNav_nav_item { 87 | margin: 5px 0; 88 | text-transform: lowercase; 89 | } 90 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_aside.scss: -------------------------------------------------------------------------------- 1 | .aside { 2 | background: $sectionBackground; 3 | border-right: 1px solid $separatorDarkColor; 4 | width: 200px; 5 | position: fixed; 6 | top: 50px; 7 | bottom: 0; 8 | left: 0; 9 | } 10 | 11 | .header { 12 | padding: 15px 10px 10px; 13 | } 14 | 15 | /* main navigation */ 16 | .mainNav { 17 | margin: 10px 0 30px 0; 18 | } 19 | 20 | .ui_title { 21 | text-transform: uppercase; 22 | font-size: 13px; 23 | font-weight: normal; 24 | color: $linkColor; 25 | margin: 0 0 10px; 26 | padding-left: 15px; 27 | } 28 | 29 | .mainNav_item { 30 | 31 | &:last-child { 32 | margin: 0; 33 | } 34 | 35 | &.active a { 36 | border-color: rgba(255, 255, 255, 0.5); 37 | background: rgba(255, 255, 255, 0.05); 38 | color: #FFF; 39 | } 40 | } 41 | 42 | .mainNav_button { 43 | font-size: 13px; 44 | display: block; 45 | padding: 9px 0 9px 15px; 46 | border-left: 1px solid transparent; 47 | -webkit-user-drag: none; 48 | 49 | &:hover, &:hover span { 50 | cursor: pointer; 51 | } 52 | 53 | & .fa { 54 | color: inherit; 55 | display: inline-block; 56 | width: 25px; 57 | } 58 | } 59 | 60 | .mainNav_tit { 61 | display: inline-block; 62 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_buttons.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | float: right; 3 | text-align: center; 4 | color: #ededed; 5 | background: transparent; 6 | border: 1px solid; 7 | padding: .75em 0; 8 | border-radius: .25em; 9 | width: 100px; 10 | font-size: 13px; 11 | letter-spacing: 1px; 12 | transition: $transitionFastValue; 13 | margin-left: 1em; 14 | 15 | &.active, &.liked { 16 | color: $scColor; 17 | &:hover { 18 | color: #FFF; 19 | } 20 | } 21 | 22 | &:hover { 23 | color: $defaultColor; 24 | cursor: pointer; 25 | & span { 26 | cursor: pointer; 27 | } 28 | } 29 | 30 | &:focus { 31 | outline: none; 32 | } 33 | } 34 | 35 | .button.show_more { 36 | padding: .34em 0; 37 | font-size: 11px; 38 | line-height: 16px; 39 | } 40 | 41 | .button.inline { 42 | float: none; 43 | display: inline-block; 44 | margin: 0 5px 0 0; 45 | width: auto; 46 | padding: .4em .75em; 47 | font-size: 10px; 48 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_charts.scss: -------------------------------------------------------------------------------- 1 | .genre-selector { 2 | width: 100%; 3 | overflow: hidden; 4 | height: 75px; 5 | transition: height 0.4s; 6 | margin: 0 0 20px 0; 7 | 8 | &:hover{ 9 | height: 230px; 10 | } 11 | 12 | .title { 13 | color: #a9aaae; 14 | } 15 | } 16 | 17 | .genre-selector button.button{ 18 | margin-top: 5px; 19 | width: 19%; 20 | } 21 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_config.scss: -------------------------------------------------------------------------------- 1 | $bodyBackground: #121314; 2 | $sectionBackground: #222326; 3 | $linkColor: #a9aaae; 4 | $defaultColor: #949599; 5 | $separatorCleanColor: #3e3e40; 6 | $separatorDarkColor: #000; 7 | $scColor: #f50; 8 | $transitionValue: all 0.5s ease; 9 | $transitionFastValue: all 0.25s ease; 10 | $transitionInstantValue: all 0.1s ease-out; -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_default.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | cursor: default; 4 | } 5 | 6 | html { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | background: $bodyBackground; 12 | font: 62.5% 'Open Sans', helvetica, sans-serif, arial !important; 13 | color: $defaultColor; 14 | height: 100%; 15 | overflow: hidden; 16 | -webkit-user-select: none; 17 | letter-spacing: 1px; 18 | } 19 | 20 | a { 21 | color: $linkColor; 22 | text-decoration: none; 23 | transition: $transitionValue; 24 | } 25 | 26 | a:hover { 27 | color: #fff; 28 | } 29 | 30 | ul, 31 | ol, 32 | li { 33 | list-style: none; 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | h1, 39 | h2, 40 | h3, 41 | h4, 42 | h5, 43 | h6 { 44 | margin: 0; 45 | color: #fff; 46 | font-weight: 600; 47 | } 48 | 49 | input[type=text], 50 | input[type=search], 51 | input[type=email] { 52 | border: 1px solid #000; 53 | background: #FFFFFF; 54 | display: inline-block; 55 | height: 25px; 56 | padding: 0 0 0 5px; 57 | outline: none; 58 | color: #000000; 59 | font-size: 12px; 60 | transition: $transitionValue; 61 | border-radius: 5px; 62 | 63 | &:focus { 64 | box-shadow: 0 0 5px $scColor; 65 | } 66 | } 67 | 68 | input[type=range]{ 69 | -webkit-appearance: none; 70 | } 71 | 72 | input[type=range]::-webkit-slider-runnable-track { 73 | width: 300px; 74 | height: 5px; 75 | background: $bodyBackground; 76 | border: none; 77 | border-radius: 2px; 78 | } 79 | 80 | input[type=range]::-webkit-slider-thumb { 81 | -webkit-appearance: none; 82 | border: none; 83 | height: 13px; 84 | width: 13px; 85 | border-radius: 50%; 86 | background: $bodyBackground; 87 | margin-top: -4px; 88 | } 89 | 90 | input[type=range]:focus { 91 | outline: none; 92 | } 93 | 94 | input[type=range]:focus::-webkit-slider-runnable-track { 95 | background: $bodyBackground; 96 | } 97 | 98 | ::-webkit-scrollbar-track { 99 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 100 | background-color: #222326; 101 | } 102 | 103 | ::-webkit-scrollbar { 104 | width: 10px; 105 | background-color: #222326; 106 | 107 | } 108 | 109 | ::-webkit-scrollbar-thumb { 110 | background-color: #333; 111 | } 112 | 113 | .clearfix:after { 114 | content: ""; 115 | display: table; 116 | clear: both; 117 | } 118 | 119 | .fa{ 120 | cursor: pointer; 121 | } 122 | 123 | .ui-db { 124 | display: block; 125 | } 126 | 127 | .selectable-text { 128 | -webkit-user-select: initial; 129 | } 130 | 131 | .unselectable-text, 132 | .unselectable-text * { 133 | -webkit-user-select: none; 134 | } 135 | 136 | body { 137 | #toast-container{ 138 | & > div { 139 | padding: 10px 15px 10px 45px; 140 | width: 278px; 141 | -moz-border-radius: 0; 142 | -webkit-border-radius: 0; 143 | border-radius: 0; 144 | -moz-box-shadow: none; 145 | -webkit-box-shadow: none; 146 | box-shadow: none; 147 | color: #ecf0f1; 148 | } 149 | & > div:hover { 150 | -moz-box-shadow: none; 151 | -webkit-box-shadow: none; 152 | box-shadow: none; 153 | } 154 | } 155 | 156 | .toast-message a, 157 | .toast-message label { 158 | font-size: 0.900em; 159 | color: #ecf0f1; 160 | } 161 | 162 | .toast-message a:hover { 163 | color: #bdc3c7; 164 | } 165 | 166 | .toast { 167 | background-color: #333; 168 | } 169 | .toast-success { 170 | background-color: #2ecc71; 171 | } 172 | .toast-error { 173 | background-color: #c0392b; 174 | } 175 | .toast-info { 176 | background-color: #2980b9; 177 | } 178 | .toast-warning { 179 | background-color: #d35400; 180 | } 181 | .toast-progress{ 182 | background-color: #222; 183 | } 184 | .toast-top-right { 185 | top: 62px; 186 | right: 42px; 187 | } 188 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: absolute; 3 | top: 46px; 4 | z-index: 1000; 5 | display: none; 6 | min-width: 160px; 7 | padding: 0; 8 | margin: 2px 0 0; 9 | list-style: none; 10 | font-size: 13px; 11 | background-color: #111; 12 | border-radius: 0 0 7px 7px; 13 | box-shadow: 2px 2px 20px 2px #000000; 14 | width: 100%; 15 | overflow: hidden; 16 | } 17 | 18 | .artist-container, .tracks-container { 19 | margin-bottom: 10px; 20 | } 21 | 22 | .dropdown-item { 23 | width: 100%; 24 | padding: 10px 10px 10px 10px; 25 | text-align: left; 26 | 27 | &, & h4 { 28 | cursor: pointer; 29 | } 30 | 31 | & img { 32 | margin-right: 10px; 33 | width: 25px; 34 | height: 25px; 35 | } 36 | 37 | &:hover { 38 | background: rgba(0, 0, 0, 0.4); 39 | 40 | & h4 { 41 | font-weight: 700; 42 | color: #FFF; 43 | } 44 | } 45 | } 46 | 47 | .dropdown h3, .dropdown h4 { 48 | color: #ffffff; 49 | font-weight: 600; 50 | } 51 | 52 | .dropdown h3 { 53 | text-align: left; 54 | text-transform: uppercase; 55 | font-size: 13px; 56 | &.dropdown-title { 57 | color: $scColor; 58 | border-left: 5px solid $scColor; 59 | text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6); 60 | background: rgba(0, 0, 0, 0.3); 61 | padding: 10px; 62 | will-change: padding-left; 63 | transition: all 0.3s ease-in-out; 64 | } 65 | } 66 | 67 | .dropdown h4 { 68 | white-space: nowrap; 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | padding: 3px 0 0 0; 72 | } 73 | 74 | .dropdown h3.show-all { 75 | color: $scColor; 76 | border-left: 5px solid $scColor; 77 | text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6); 78 | background: rgba(0, 0, 0, 0.3); 79 | padding: 10px; 80 | will-change: padding-left; 81 | transition: all 0.3s ease-in-out; 82 | 83 | &:hover { 84 | color: #FFF; 85 | cursor: pointer; 86 | padding-left: 15px; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_following.scss: -------------------------------------------------------------------------------- 1 | .following { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: space-around; 6 | } 7 | 8 | .following_item { 9 | display: inline-block; 10 | position: relative; 11 | margin: 0 5px 60px 5px; 12 | width: 210px; 13 | 14 | & .songList_item_song_tit { 15 | height: auto; 16 | } 17 | 18 | & .songList_item_box:first-child { 19 | margin-bottom: 20px; 20 | } 21 | 22 | & .songList_item_box:last-child { 23 | margin: 10px 0 0 0; 24 | } 25 | 26 | & .fa:last-child { 27 | display: table-cell; 28 | } 29 | 30 | & .songList_item_container_artwork { 31 | height: 210px; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_loader.scss: -------------------------------------------------------------------------------- 1 | .box-loader { 2 | display: block; 3 | height: 35px; 4 | width: 300px; 5 | position: absolute; 6 | text-align: center; 7 | top: 50%; 8 | left: 50%; 9 | margin-left: -150px; 10 | margin-top: -17px; 11 | 12 | & > h4 { 13 | font-size: 12px; 14 | font-weight: normal; 15 | } 16 | } 17 | 18 | .loader:before { 19 | content: ""; 20 | position: absolute; 21 | top: 0px; 22 | left: -25px; 23 | height: 12px; 24 | width: 12px; 25 | border-radius: 12px; 26 | -webkit-animation: loaderB 3s ease-in-out infinite; 27 | animation: loaderB 3s ease-in-out infinite; 28 | } 29 | 30 | .loader { 31 | position: relative; 32 | width: 12px; 33 | height: 12px; 34 | top: 46%; 35 | left: 46%; 36 | border-radius: 12px; 37 | -webkit-animation: loaderM 3s ease-in-out infinite; 38 | animation: loaderM 3s ease-in-out infinite; 39 | } 40 | 41 | 42 | .loader:after { 43 | content: ""; 44 | position: absolute; 45 | top: 0px; 46 | left: 25px; 47 | height: 12px; 48 | width: 12px; 49 | border-radius: 10px; 50 | -webkit-animation: loaderA 3s ease-in-out infinite; 51 | animation: loaderA 3s ease-in-out infinite; 52 | } 53 | 54 | @-webkit-keyframes loaderB{ 55 | 0%{background-color: rgba(255, 85, 0, .2);} 56 | 25%{background-color: rgba(255, 85, 0, 1);} 57 | 50%{background-color: rgba(255, 85, 0, .2);} 58 | 75%{background-color: rgba(255, 85, 0, .2);} 59 | 100%{background-color: rgba(255, 85, 0, .2);} 60 | } 61 | @keyframes loaderB{ 62 | 0%{background-color: rgba(255, 85, 0, .2);} 63 | 25%{background-color: rgba(255, 85, 0, 1);} 64 | 50%{background-color: rgba(255, 85, 0, .2);} 65 | 75%{background-color: rgba(255, 85, 0, .2);} 66 | 100%{background-color: rgba(255, 85, 0, .2);} 67 | } 68 | 69 | @-webkit-keyframes loaderM{ 70 | 0%{background-color: rgba(255, 85, 0, .2);} 71 | 25%{background-color: rgba(255, 85, 0, .2);} 72 | 50%{background-color: rgba(255, 85, 0, 1);} 73 | 75%{background-color: rgba(255, 85, 0, .2);} 74 | 100%{background-color: rgba(255, 85, 0, .2);} 75 | } 76 | @keyframes loaderM{ 77 | 0%{background-color: rgba(255, 85, 0, .2);} 78 | 25%{background-color: rgba(255, 85, 0, .2);} 79 | 50%{background-color: rgba(255, 85, 0, 1);} 80 | 75%{background-color: rgba(255, 85, 0, .2);} 81 | 100%{background-color: rgba(255, 85, 0, .2);} 82 | } 83 | 84 | @-webkit-keyframes loaderA{ 85 | 0%{background-color: rgba(255, 85, 0, .2);} 86 | 25%{background-color: rgba(255, 85, 0, .2);} 87 | 50%{background-color: rgba(255, 85, 0, .2);} 88 | 75%{background-color: rgba(255, 85, 0, 1);} 89 | 100%{background-color: rgba(255, 85, 0, .2);} 90 | } 91 | @keyframes loaderA{ 92 | 0%{background-color: rgba(255, 85, 0, .2);} 93 | 25%{background-color: rgba(255, 85, 0, .2);} 94 | 50%{background-color: rgba(255, 85, 0, .2);} 95 | 75%{background-color: rgba(255, 85, 0, 1);} 96 | 100%{background-color: rgba(255, 85, 0, .2);} 97 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_player.scss: -------------------------------------------------------------------------------- 1 | .player_inner { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | width: 100%; 6 | background: $sectionBackground; 7 | border-top: 1px solid $separatorDarkColor; 8 | display: flex; 9 | flex-direction: row; 10 | z-index: 1; 11 | padding-top: 5px; 12 | } 13 | 14 | .player_thumb { 15 | height: 50px; 16 | width: 50px; 17 | margin: 5px 0; 18 | float: left; 19 | cursor: pointer; 20 | } 21 | 22 | .player_title { 23 | font-size: 14px; 24 | white-space: nowrap; 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | padding: 5px 10px 0; 28 | cursor: pointer; 29 | } 30 | 31 | .player_details { 32 | width: 300px; 33 | flex: 1; 34 | padding: 0 5px; 35 | } 36 | 37 | .player_user { 38 | display: inline-block; 39 | color: #FFF; 40 | font-size: 11px; 41 | padding: 0 10px; 42 | white-space: nowrap; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | font-weight: 300; 46 | cursor: pointer; 47 | } 48 | 49 | .player_currentSong { 50 | display: none; 51 | margin: 10px 0 0; 52 | } 53 | 54 | .player_controls { 55 | padding: 0 10px; 56 | overflow: hidden; 57 | width: 200px; 58 | display: flex; 59 | align-items: center; 60 | 61 | & .fa { 62 | font-size: 20px; 63 | color: #FFF; 64 | &.thin::before { 65 | -webkit-text-stroke: 1px #000; 66 | } 67 | } 68 | } 69 | 70 | .player_volume { 71 | margin: 20px 30px 0; 72 | position: relative; 73 | 74 | & .fa { 75 | font-size: 20px; 76 | } 77 | 78 | & .player_volume_range { 79 | width: 100px; 80 | display: inline-block; 81 | position: absolute; 82 | -webkit-transform: rotate(-90deg); 83 | left: -40px; 84 | margin-top: -90px; 85 | padding: 10px; 86 | background: $sectionBackground; 87 | border-radius: 5px; 88 | 89 | &::-webkit-slider-thumb { 90 | -webkit-appearance: none; 91 | height: 11px; 92 | width: 11px; 93 | border-radius: 6px; 94 | background: $scColor; 95 | cursor: pointer; 96 | margin-top: -4px; 97 | } 98 | 99 | &::-webkit-slider-runnable-track { 100 | width: 100%; 101 | height: 3px; 102 | cursor: pointer; 103 | background: $scColor; 104 | border-radius: 2.5px; 105 | } 106 | 107 | &:focus::-webkit-slider-runnable-track { 108 | background: $scColor; 109 | } 110 | 111 | &[data-isVisible="false"] { 112 | display: none; 113 | } 114 | 115 | &[data-isVisible="true"] { 116 | display: inline-block; 117 | } 118 | } 119 | 120 | } 121 | 122 | .player_time { 123 | display: inline-block; 124 | font-size: 11px; 125 | vertical-align: top; 126 | color: #ccc; 127 | } 128 | 129 | .player_timeLeft { 130 | margin-left: 25px; 131 | } 132 | 133 | .player_timeCurrent:not(:empty) { 134 | &::after { 135 | content: '/'; 136 | margin: 0 3px; 137 | } 138 | } 139 | 140 | .player_favorite { 141 | margin: 0 0 0 10px; 142 | 143 | .fa { 144 | color: #949599; 145 | font-size: 12px; 146 | } 147 | 148 | &.active { 149 | .fa { 150 | color: #f50; 151 | } 152 | } 153 | } 154 | .player_progress_wrapper { 155 | position: absolute; 156 | // 1px horizontal protection area for window mouseleave workaround 157 | padding: 0 1px; 158 | top: 0; 159 | height: 20px; 160 | left: 0; 161 | width: 100%; 162 | } 163 | .player_progress { 164 | position: relative; 165 | top: -20px; 166 | height: 4px; 167 | padding: 20px 0 10px; 168 | overflow: hidden; 169 | cursor: pointer; 170 | box-sizing: content-box; 171 | 172 | &:hover { 173 | & .player_progress_bar::before { 174 | background: rgba(255, 255, 255, 0.2); 175 | } 176 | 177 | & .player_progress_bar::after { 178 | transform: scale(1); 179 | } 180 | } 181 | 182 | & .player_progress_bar { 183 | display: block; 184 | width: 0; 185 | height: 4px; 186 | background: $scColor; 187 | 188 | &::before { 189 | content: ''; 190 | position: absolute; 191 | display: block; 192 | height: 4px; 193 | width: 100%; 194 | background: rgba(0, 0, 0, 0.15); 195 | z-index: -1; 196 | transition: $transitionInstantValue; 197 | } 198 | 199 | &::after { 200 | content: ''; 201 | position: relative; 202 | top: -4px; 203 | left: 6px; 204 | display: block; 205 | float: right; 206 | height: 12px; 207 | width: 12px; 208 | background: rgb(255, 122, 0); 209 | border-radius: 50%; 210 | box-shadow: 0 0 2px 0 #000; 211 | transform: scale(0); 212 | transition: $transitionInstantValue; 213 | } 214 | } 215 | 216 | &.mousetrap { 217 | // reset scrub if mouse leaves this area 218 | $topThreshold: 100px; 219 | $bottomThreshold: 50px; 220 | 221 | top: -$topThreshold; 222 | padding-top: $topThreshold; 223 | padding-bottom: $bottomThreshold; 224 | } 225 | } 226 | 227 | .player_controls span { 228 | display: block; 229 | overflow: hidden; 230 | text-align: center; 231 | flex-grow: 1; 232 | } 233 | 234 | .player_lock, 235 | .player_repeat, 236 | .player_shuffle, 237 | .player_queueList { 238 | & .fa { 239 | font-size: 14px; 240 | } 241 | 242 | &.active, 243 | &.active .fa { 244 | color: $scColor; 245 | } 246 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_playlist-songs.scss: -------------------------------------------------------------------------------- 1 | .songList_item.playlist[data-playlist-hidden="false"] { 2 | height: auto; 3 | } 4 | .songList_item.playlist[data-playlist-hidden="true"] { 5 | height: 15px; 6 | } 7 | 8 | .songList_item.playlist { 9 | display: block; 10 | width: 100%; 11 | margin: 0 0 15px 0; 12 | 13 | & .songList_item_song_button { 14 | height: 100%; 15 | padding-top: 0; 16 | font-size: 32px; 17 | } 18 | 19 | & .songList_item_song_user { 20 | display: block; 21 | margin: 0 0 10px 0; 22 | cursor: pointer; 23 | width: 100%; 24 | float: none; 25 | 26 | & span { 27 | cursor: pointer; 28 | } 29 | } 30 | 31 | & .songList_item_songs_list { 32 | margin: 20px 0 0 0; 33 | } 34 | 35 | & .songList_item_songs_list_item { 36 | cursor: pointer; 37 | overflow: hidden; 38 | margin: 0 0 5px 0; 39 | } 40 | 41 | & .songList_item_container_artwork { 42 | width: 47px; 43 | height: 47px; 44 | float: left; 45 | position: relative; 46 | } 47 | 48 | & .songList_item_container_info { 49 | float: left; 50 | margin: 0 0 0 10px; 51 | } 52 | 53 | & .songList_item_song_tit { 54 | height: auto; 55 | max-width: 800px; 56 | } 57 | 58 | & .songList_item_song_info { 59 | display: inline-block; 60 | margin: 0 5px; 61 | 62 | &:first-child { 63 | margin-left: 0; 64 | } 65 | } 66 | 67 | } 68 | 69 | /* 70 | * Playlist dashboard 71 | */ 72 | .playlistDashboard_field { 73 | width: 300px; 74 | } 75 | 76 | .playlistDashboard_list { 77 | margin: 20px 0; 78 | } 79 | .playlistDashboard_list_item { 80 | overflow: hidden; 81 | padding: 10px 0; 82 | 83 | &:hover { 84 | background: $sectionBackground; 85 | cursor: pointer; 86 | } 87 | } 88 | 89 | .playlistDashboard_selectedSong { 90 | color: #fff; 91 | } 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_profile.scss: -------------------------------------------------------------------------------- 1 | .profile { 2 | overflow: hidden; 3 | margin-bottom: 25px; 4 | display: flex; 5 | } 6 | 7 | .profile_box { 8 | display: inline-block; 9 | margin: 0 5px 0 5px; 10 | flex: 1 0 300px; 11 | } 12 | 13 | .profile_pic { 14 | margin: 0 5px 0 5px; 15 | flex: 0 1 300px; 16 | min-width: 150px; 17 | } 18 | 19 | .profile_description { 20 | font-size: 12px; 21 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_queueList.scss: -------------------------------------------------------------------------------- 1 | .queueList { 2 | background: $sectionBackground; 3 | position: fixed; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | width: 350px; 8 | padding: 70px 0 70px; 9 | display: block; 10 | overflow-y: scroll; 11 | transform: translateX(350px); 12 | transition: all .5s ease-in-out; 13 | 14 | &.active { 15 | transform: translateX(0); 16 | } 17 | } 18 | 19 | .queueListView_list { 20 | width: 100%; 21 | // Keep some offset from the bottom not to mix last queue item 22 | // with player progress bar on mouse over 23 | padding-bottom: 15px; 24 | 25 | & .queueListView_list_itemTitle { 26 | text-align: center; 27 | font-size: 14px; 28 | margin-bottom: 20px; 29 | } 30 | 31 | & .queueListView_list_item { 32 | font-size: 12px; 33 | color: #fff; 34 | border-bottom: 1px solid $separatorDarkColor; 35 | padding: 10px 20px; 36 | white-space: nowrap; 37 | cursor: pointer; 38 | 39 | &:hover, 40 | &.active { 41 | background: $separatorDarkColor; 42 | } 43 | 44 | &:last-child { 45 | border-bottom: none; 46 | } 47 | } 48 | 49 | & .queueListView_list_item_index, 50 | & .queueListView_list_item_title, 51 | & .queueListView_list_item_user, 52 | & .queueListView_list_item_options{ 53 | display: inline-block; 54 | font-weight: 100; 55 | } 56 | 57 | & .queueListView_list_item_index { 58 | position: relative; 59 | top: -4px; 60 | } 61 | 62 | & .queueListView_list_item_title { 63 | width: 143px; 64 | } 65 | 66 | & .queueListView_list_item_user { 67 | width: 124px; 68 | } 69 | 70 | & .queueListView_list_item_title, 71 | & .queueListView_list_item_user { 72 | white-space: nowrap; 73 | text-overflow: ellipsis; 74 | overflow: hidden; 75 | } 76 | } 77 | 78 | .queueListView_list_item_options { 79 | position: relative; 80 | width: 10px; 81 | text-align: center; 82 | height: 17px; 83 | 84 | &.active { 85 | .queueListView_list_item_options_list, 86 | .queueListView_list_item_options_arrow { 87 | display: block; 88 | } 89 | } 90 | } 91 | 92 | .queueListView_list_item_options_list { 93 | display: none; 94 | position: absolute; 95 | right: 19px; 96 | top: -11px; 97 | background: $separatorCleanColor; 98 | z-index: 10; 99 | border-radius: 2px; 100 | 101 | & li:last-child { 102 | border-bottom: none; 103 | } 104 | } 105 | 106 | .queueListView_list_item_options_list_button { 107 | padding: 10px; 108 | text-align: center; 109 | border-bottom: 1px solid $separatorDarkColor; 110 | 111 | &:hover { 112 | background: $separatorDarkColor; 113 | } 114 | } 115 | 116 | .queueListView_list_item_options_arrow { 117 | display: none; 118 | position: absolute; 119 | top: -3px; 120 | right: 8px; 121 | width: 0; 122 | height: 0; 123 | border-top: 12px solid transparent; 124 | border-bottom: 10px solid transparent; 125 | border-left: 12px solid #3E3E40; 126 | } 127 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_search.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | width: 100%; 3 | text-align: center; 4 | margin: 10px 0; 5 | } 6 | 7 | .search-form { 8 | position: relative; 9 | 10 | & .search-icon { 11 | position: absolute; 12 | top: 6px; 13 | right: 5px; 14 | color: $defaultColor; 15 | transition: $transitionFastValue; 16 | } 17 | } 18 | 19 | 20 | .search-form .search_field { 21 | box-sizing: border-box; 22 | border: none; 23 | border-bottom: 1px solid $separatorCleanColor; 24 | background: transparent; 25 | border-radius: 0; 26 | color: #ffffff; 27 | height: auto; 28 | padding: 5px 5px 5px; 29 | width: 100%; 30 | font-weight: 700; 31 | 32 | &:focus { 33 | box-shadow: none; 34 | border-bottom-color: #ffffff; 35 | 36 | & +.search-icon { 37 | color: #ffffff; 38 | } 39 | } 40 | 41 | &:hover { 42 | cursor: text; 43 | } 44 | 45 | ::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 46 | color: #ffffff; 47 | font-weight: 400; 48 | } 49 | } 50 | 51 | .searchButton { 52 | display: none; 53 | } 54 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_settings.scss: -------------------------------------------------------------------------------- 1 | .settings-container { 2 | width: 540px; 3 | } 4 | 5 | .settings-switch {} 6 | 7 | .full-flex { 8 | width: 100%; 9 | display: flex; 10 | flex-wrap: wrap; 11 | align-items: center; 12 | justify-content: center; 13 | margin: 10px 0 10px; 14 | } 15 | 16 | .flex-column { 17 | flex-direction: column; 18 | } 19 | 20 | // Switch for Settings 21 | .onoffswitch { 22 | position: relative; 23 | width: 50px; 24 | -webkit-user-select:none; 25 | } 26 | .onoffswitch-checkbox { 27 | display: none; 28 | } 29 | .onoffswitch-label { 30 | display: block; 31 | overflow: hidden; 32 | cursor: pointer; 33 | height: 15px; 34 | padding: 0; 35 | line-height: 15px; 36 | border: 1px solid transparent; 37 | border-radius: 25px; 38 | background-color: #222326; 39 | transition: all 0.2s ease-in; 40 | } 41 | .onoffswitch-label:before { 42 | content: ""; 43 | display: block; 44 | width: 24px; 45 | margin: -5px; 46 | background: #525252; 47 | position: absolute; top: 0; bottom: 0; 48 | right: 31px; 49 | border: 1px solid transparent; 50 | border-radius: 25px; 51 | transition: all 0.2s ease-in; 52 | } 53 | .onoffswitch-checkbox:checked + .onoffswitch-label { 54 | background-color: #FF5500; 55 | border: 1px solid transparent; 56 | border-radius: 25px; 57 | } 58 | .onoffswitch-checkbox:checked + .onoffswitch-label, .onoffswitch-checkbox:checked + .onoffswitch-label:before { 59 | border-color: #FF5500; 60 | border: 1px solid transparent; 61 | border-radius: 25px; 62 | } 63 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { 64 | margin-left: 0; 65 | } 66 | .onoffswitch-checkbox:checked + .onoffswitch-label:before { 67 | right: 0px; 68 | background-color: #FF5500; 69 | box-shadow: 3px 6px 18px 0px rgba(0, 0, 0, 0.2); 70 | border: 1px solid rgba(0, 0, 0, .3); 71 | } 72 | 73 | .settings-secondary-container { 74 | width: 590px; 75 | } 76 | 77 | input[type="text"].full-width { 78 | margin-top: 5px; 79 | margin-bottom: 10px; 80 | width: 100%; 81 | } 82 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_songlist.scss: -------------------------------------------------------------------------------- 1 | .songList { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: space-around; 6 | } 7 | 8 | .songList_item { 9 | position: relative; 10 | overflow: hidden; 11 | margin: 0 5px 30px 5px; 12 | width: 210px; 13 | 14 | & .songList_item_box:first-child { 15 | margin-bottom: 20px; 16 | } 17 | 18 | & .songList_item_box:last-child { 19 | margin: 10px 0 0 0; 20 | } 21 | } 22 | 23 | .songList_item_box { 24 | width: 300px; 25 | height: 300px; 26 | } 27 | 28 | .songList_item_container_artwork { 29 | position: relative; 30 | width: 100%; 31 | border: 1px solid $separatorDarkColor; 32 | } 33 | 34 | .songList_item_artwork { 35 | display: block; 36 | width: 100%; 37 | } 38 | 39 | .songList_item_song_button { 40 | @extend .pointer; 41 | display: flex; 42 | font-size: 50px; 43 | width: 100%; 44 | position: absolute; 45 | top: 0; 46 | height: 100%; 47 | background: rgba(0, 0, 0, 0.8); 48 | text-align: center; 49 | transition: all 150ms; 50 | opacity: 0; 51 | align-items: center; 52 | justify-content: center; 53 | 54 | & .fa:last-child { 55 | display: none; 56 | } 57 | } 58 | 59 | .pointer { 60 | cursor: pointer; 61 | } 62 | 63 | li.active { 64 | & .songList_item_song_button { 65 | opacity: 1; 66 | } 67 | } 68 | 69 | div.active { 70 | & .songList_item_song_button { 71 | -webkit-transform: scale(1); 72 | opacity: 1; 73 | } 74 | } 75 | 76 | .songPlaying { 77 | & .songList_item_song_button.currentSong { 78 | -webkit-transform: scale(1); 79 | opacity: 1; 80 | & ~ div { 81 | display: flex; 82 | } 83 | } 84 | } 85 | 86 | .songList_item_song_info { 87 | display: block; 88 | color: $defaultColor; 89 | font-size: 11px; 90 | text-transform: uppercase; 91 | 92 | } 93 | 94 | .songList_item_song_user { 95 | width: 165px; 96 | float: left; 97 | } 98 | 99 | .songList_item_repost { 100 | & > i { 101 | color: inherit; 102 | cursor: default; 103 | } 104 | & > a { 105 | font-size: 8px; 106 | } 107 | } 108 | 109 | .songList_item_song_length { 110 | float: right; 111 | width: 45px; 112 | text-align: right 113 | } 114 | 115 | .songList_item_song_tit { 116 | font-size: 17px; 117 | margin: 5px 0; 118 | display: -webkit-box; 119 | -webkit-line-clamp: 2; 120 | -webkit-box-orient: vertical; 121 | overflow: hidden; 122 | height: 46px; 123 | &:hover { 124 | @extend .pointer; 125 | } 126 | } 127 | 128 | .songList_item_song_social_details { 129 | position: absolute; 130 | bottom: 0; 131 | width: 100%; 132 | color: #FFF; 133 | font-size: 12px; 134 | padding: 5px 0; 135 | display: none; 136 | } 137 | 138 | .songList_item_song_social_details > span { 139 | flex-grow: 1; 140 | white-space: nowrap; 141 | padding: 0 10px; 142 | text-align: center; 143 | } 144 | 145 | .songList_item_song_social_details i { 146 | margin-right: 5px; 147 | } 148 | 149 | .songList_item_song_details { 150 | margin-top: 15px; 151 | font-size: 12px; 152 | 153 | & div { 154 | margin: 0 0 5px 0; 155 | } 156 | 157 | & a { 158 | display: inline-block; 159 | margin: 0 10px 0 0; 160 | 161 | &:last-child { 162 | margin-right: 0; 163 | } 164 | 165 | &.liked, 166 | &.reposted { 167 | & > .fa { 168 | color: $scColor; 169 | } 170 | } 171 | 172 | &:hover { 173 | @extend .pointer; 174 | color: #FFF; 175 | 176 | & > .fa { 177 | color: #FFF; 178 | @extend .pointer; 179 | } 180 | } 181 | 182 | & > .fa { 183 | color: $defaultColor; 184 | display: inline-block; 185 | transition: $transitionValue; 186 | } 187 | } 188 | 189 | & .downloadSong { 190 | position: relative; 191 | 192 | & input { 193 | display: block; 194 | position: absolute; 195 | top: 0; 196 | left: 0; 197 | width: 100%; 198 | height: 100%; 199 | visibility: hidden; 200 | } 201 | } 202 | } 203 | 204 | .songList_item_additional_details { 205 | & .songList_item_genre { 206 | position: absolute; 207 | top: 10px; 208 | right: 0; 209 | margin: 0; 210 | background: rgba(0, 0, 0, 0.9); 211 | border-radius: 2px 0 0 2px; 212 | padding: 3px 6px; 213 | color: #FFF; 214 | transition: $transitionFastValue; 215 | &:hover { 216 | @extend .pointer; 217 | background: #FFF; 218 | color: #000; 219 | } 220 | } 221 | 222 | & .songList_item_license { 223 | display: none; 224 | } 225 | } 226 | 227 | @-webkit-keyframes heartBeat { 228 | 0% { 229 | font-size: 12px; 230 | } 231 | 50% { 232 | font-size: 15px; 233 | } 234 | 100% { 235 | font-size: 12px; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | margin: 100px auto; 3 | width: 50px; 4 | height: 30px; 5 | text-align: center; 6 | font-size: 10px; 7 | } 8 | 9 | .spinner > div { 10 | background-color: $separatorCleanColor; 11 | height: 100%; 12 | width: 6px; 13 | display: inline-block; 14 | 15 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 16 | animation: stretchdelay 1.2s infinite ease-in-out; 17 | } 18 | 19 | .spinner { 20 | 21 | .rect2 { 22 | -webkit-animation-delay: -1.1s; 23 | animation-delay: -1.1s; 24 | } 25 | 26 | .rect3 { 27 | -webkit-animation-delay: -1.0s; 28 | animation-delay: -1.0s; 29 | } 30 | 31 | .rect4 { 32 | -webkit-animation-delay: -0.9s; 33 | animation-delay: -0.9s; 34 | } 35 | 36 | .rect5 { 37 | -webkit-animation-delay: -0.8s; 38 | animation-delay: -0.8s; 39 | } 40 | } 41 | 42 | @-webkit-keyframes stretchdelay { 43 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 44 | 20% { -webkit-transform: scaleY(1.0) } 45 | } 46 | 47 | @keyframes stretchdelay { 48 | 0%, 40%, 100% { 49 | transform: scaleY(0.4); 50 | -webkit-transform: scaleY(0.4); 51 | } 20% { 52 | transform: scaleY(1.0); 53 | -webkit-transform: scaleY(1.0); 54 | } 55 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_topFrame.scss: -------------------------------------------------------------------------------- 1 | .topFrame { 2 | background: $bodyBackground; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | height: 50px; 8 | border-top: 2px solid $scColor; 9 | border-bottom: 1px solid $separatorDarkColor; 10 | box-shadow: 0 0 7px 0px #000; 11 | z-index: 100; 12 | -webkit-app-region: drag; 13 | } 14 | 15 | .windowAction { 16 | overflow: hidden; 17 | float: left; 18 | margin: 7px 0 0 0; 19 | -webkit-app-region: no-drag; 20 | } 21 | .windowAction_item { 22 | float: left; 23 | margin: 13px 5px 0; 24 | font-size: 7px; 25 | cursor: pointer; 26 | border-radius: 15px; 27 | padding: 1px 3px; 28 | letter-spacing: 0; 29 | 30 | 31 | &:first-child { 32 | margin-left: 10px; 33 | } 34 | 35 | &:hover .fa { 36 | color: $defaultColor; 37 | } 38 | 39 | & .fa { 40 | cursor: pointer; 41 | color: #222326; 42 | } 43 | } 44 | 45 | .macActionButtons { 46 | & #closeApp { 47 | background: #FF4441; 48 | } 49 | 50 | & #minimizeApp { 51 | background: #FFBE00; 52 | } 53 | 54 | & #expandApp { 55 | background: #00D645; 56 | } 57 | 58 | & .fa { 59 | opacity: 0; 60 | } 61 | 62 | &:hover .fa { 63 | opacity: 1; 64 | color: #222326; 65 | } 66 | } 67 | 68 | .windowsActionButtons { 69 | float: right; 70 | -webkit-app-region: no-drag; 71 | margin: 0; 72 | 73 | & li { 74 | margin: 0; 75 | padding: 7px 14px 7px; 76 | border-radius: 0; 77 | &:hover { 78 | background: #2C2C2C; 79 | .fa { 80 | color: #FFF; 81 | } 82 | } 83 | } 84 | 85 | & #minimizeApp { 86 | padding: 10px 14px 4px; 87 | } 88 | 89 | & #closeApp { 90 | padding: 6px 20px 8px; 91 | background: #390000; 92 | &:hover { 93 | background: #530000; 94 | } 95 | } 96 | 97 | & .fa { 98 | color: #FFF; 99 | } 100 | } 101 | 102 | .navigationButton { 103 | font-size: 14px; 104 | margin-top: 8px; 105 | 106 | & .fa { 107 | color: #464647; 108 | } 109 | 110 | &.goBack { 111 | 112 | } 113 | 114 | &.goForward { 115 | 116 | } 117 | } 118 | 119 | .appInfo { 120 | float: right; 121 | margin: 15px 0 0 0; 122 | -webkit-app-region: no-drag; 123 | } 124 | .appInfo_item { 125 | margin: 0 10px; 126 | float: left; 127 | 128 | & a { 129 | font-size: 14px; 130 | color: $linkColor; 131 | cursor: pointer; 132 | 133 | &:hover { 134 | color: #fff; 135 | } 136 | } 137 | 138 | &:last-child a { 139 | display: none; 140 | color: $scColor; 141 | &.updateAvailable { 142 | display: block; 143 | } 144 | } 145 | } 146 | 147 | .topbarSearch { 148 | position: relative; 149 | float: right; 150 | margin-right: 10px; 151 | max-width: 500px; 152 | -webkit-app-region: no-drag; 153 | } 154 | 155 | @media (max-width: 940px) { 156 | .topbarSearch { 157 | width: 25%; 158 | } 159 | } 160 | 161 | @media (min-width: 940px) { 162 | .topbarSearch { 163 | width: 35%; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_track.scss: -------------------------------------------------------------------------------- 1 | .trackView { 2 | & .likes_count { 3 | &.liked { 4 | & > i { 5 | color: $scColor; 6 | } 7 | } 8 | } 9 | & .trackDetails { 10 | display: flex; 11 | 12 | & .additionalInfo { 13 | min-width: 150px; 14 | flex: 0 1 300px; 15 | } 16 | 17 | & .trackCover { 18 | position: relative; 19 | font-size: 0; 20 | 21 | & img { 22 | width: 100%; 23 | } 24 | 25 | & .details { 26 | display: flex; 27 | position: absolute; 28 | bottom: 0; 29 | width: 100%; 30 | text-align: center; 31 | font-size: 14px; 32 | color: #FFF; 33 | padding: 5px; 34 | background: rgba(0,0,0,0.6); 35 | 36 | & >span { 37 | flex-grow: 1; 38 | & .fa { 39 | display: inline-block !important; 40 | } 41 | } 42 | } 43 | } 44 | 45 | & .content { 46 | padding: 0 10px; 47 | flex: 1 0 300px; 48 | 49 | & .user_name { 50 | cursor: pointer; 51 | } 52 | 53 | & h1 { 54 | white-space: nowrap; 55 | overflow: hidden; 56 | text-overflow: ellipsis; 57 | } 58 | 59 | & .track_description { 60 | margin-bottom: 12px; 61 | min-height: 196px; 62 | } 63 | } 64 | 65 | } 66 | 67 | & .tagContainer { 68 | padding: 10px 0; 69 | & .tag { 70 | display: inline-block; 71 | padding: 3px 5px; 72 | background: #323232; 73 | border-radius: 4px; 74 | margin: 0 5px 5px 0; 75 | transition: $transitionFastValue; 76 | &:hover { 77 | color: #FFF; 78 | cursor: pointer; 79 | } 80 | } 81 | } 82 | 83 | & .trackActions { 84 | & a { 85 | color: $defaultColor; 86 | text-transform: uppercase; 87 | border-color: #323232; 88 | 89 | &:hover { 90 | color: #FFF; 91 | } 92 | 93 | & .fa { 94 | margin-right: 3px; 95 | color: inherit; 96 | } 97 | } 98 | } 99 | 100 | & .trackComments { 101 | & > h2 { 102 | margin-bottom: 15px; 103 | } 104 | 105 | & .comment { 106 | padding: 9px 5px; 107 | 108 | & .user_thumb { 109 | margin: 5px 0; 110 | border: 1px solid #2c2c2c; 111 | width: 35px; 112 | height: 35px; 113 | } 114 | 115 | & h3 { 116 | margin: 3px 0; 117 | padding-left: 50px; 118 | color: $linkColor; 119 | 120 | span { 121 | font-weight: 300; 122 | color: $defaultColor; 123 | font-size: 0.9em; 124 | } 125 | 126 | & .user_name { 127 | cursor: pointer; 128 | display: initial; 129 | font-size: inherit; 130 | font-weight: inherit; 131 | color: inherit; 132 | } 133 | } 134 | 135 | & p { 136 | margin: 0; 137 | font-size: 1.05em; 138 | padding-left: 50px; 139 | color: #FFF; 140 | } 141 | 142 | & .date { 143 | float: right; 144 | } 145 | } 146 | 147 | } 148 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_user.scss: -------------------------------------------------------------------------------- 1 | $user_image_size: 40px; 2 | %clickable-block { 3 | cursor: pointer; 4 | display: block; 5 | } 6 | 7 | .user { 8 | padding: 0 0 10px; 9 | overflow: hidden; 10 | } 11 | 12 | .user_thumb { 13 | @extend %clickable-block; 14 | width: $user_image_size; 15 | height: $user_image_size; 16 | border-radius: $user_image_size; 17 | float: left; 18 | -webkit-user-drag: none; 19 | } 20 | 21 | .user_inner { 22 | @extend %clickable-block; 23 | float: left; 24 | margin: 0 0 0 10px; 25 | cursor: pointer; 26 | } 27 | 28 | .user_name { 29 | @extend %clickable-block; 30 | font-size: 15px; 31 | color: #fff; 32 | white-space: nowrap; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | width: 128px; 36 | line-height: 22px; 37 | } 38 | 39 | .user_logOut { 40 | @extend %clickable-block; 41 | font-size: 10px; 42 | } 43 | 44 | .user_info { 45 | margin: 15px 0 0; 46 | font-size: 13px; 47 | } 48 | 49 | .user_info_wrap { 50 | display: inline-block; 51 | margin: 0 4px; 52 | 53 | &:hover { 54 | color: #fff; 55 | } 56 | 57 | & small, 58 | & strong { 59 | @extend %clickable-block; 60 | } 61 | } -------------------------------------------------------------------------------- /app/public/stylesheets/sass/_components/_view-container.scss: -------------------------------------------------------------------------------- 1 | body { 2 | .ngdialog-content { 3 | position: absolute; 4 | top: 50%; 5 | left: 50%; 6 | width: 800px; 7 | margin-top: -250px; 8 | margin-left: -400px; 9 | background: rgba(0, 0, 0, 0.9); 10 | padding: 20px; 11 | font-size: 20px; 12 | } 13 | } 14 | 15 | .mainView { 16 | // flex: 5 0 0; 17 | padding: 10px 20px; 18 | margin-left: 200px; 19 | overflow-y: scroll; 20 | overflow-x: hidden; 21 | height: 100%; 22 | 23 | & > div { 24 | min-width: 130px; 25 | max-width: 100%; 26 | margin: 0 auto; 27 | } 28 | 29 | & > div > h1 { 30 | margin: 10px 0 30px; 31 | font-weight: 300; 32 | font-size: 3em; 33 | } 34 | } 35 | 36 | /* 37 | * Modal UI components for adding song to playlist 38 | */ 39 | .mediaBox_wrapper { 40 | float: left; 41 | margin: 0 5px; 42 | 43 | &, & * { 44 | cursor: pointer; 45 | } 46 | 47 | &:first-child { 48 | margin-left: 0px; 49 | } 50 | } 51 | 52 | .mediaBox_item_info { 53 | font-size: 12px; 54 | display: inline-block; 55 | margin: 0 5px 0 0; 56 | 57 | &:last-child { 58 | margin-right: 0; 59 | } 60 | } 61 | 62 | /* 63 | * filter tracks component 64 | */ 65 | .filterTracks { 66 | h4 { 67 | font-size: 12px 68 | } 69 | 70 | ul { 71 | margin-bottom: 30px; 72 | } 73 | 74 | li { 75 | display: inline; 76 | margin: 10px; 77 | 78 | strong { 79 | font-size: 11px; 80 | } 81 | } 82 | li:first-child { 83 | margin-left: 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/public/stylesheets/sass/app.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /* ========================================================================== 4 | @Config 5 | ========================================================================== */ 6 | @import "_components/_config"; 7 | 8 | /* ========================================================================== 9 | @Default 10 | ========================================================================== */ 11 | @import "_components/_default.scss"; 12 | 13 | /* ========================================================================== 14 | @App state 15 | ========================================================================== */ 16 | @import "_components/_appState"; 17 | 18 | /* ========================================================================== 19 | @App header - top Frame 20 | ========================================================================== */ 21 | @import "_components/_topFrame"; 22 | 23 | /* ========================================================================== 24 | @App container 25 | ========================================================================== */ 26 | @import "_components/_appContainer"; 27 | 28 | /* ========================================================================== 29 | @User 30 | ========================================================================== */ 31 | @import "_components/_user"; 32 | 33 | /* ========================================================================== 34 | @Aside 35 | ========================================================================== */ 36 | @import "_components/_aside"; 37 | 38 | /* ========================================================================== 39 | @Search 40 | ========================================================================== */ 41 | @import "_components/_search"; 42 | 43 | /* ========================================================================== 44 | @NowPlaying 45 | ========================================================================== */ 46 | @import "_components/player"; 47 | 48 | /* ========================================================================== 49 | @MainView - wrapper 50 | ========================================================================== */ 51 | @import "_components/_view-container"; 52 | 53 | /* ========================================================================== 54 | @SongList 55 | ========================================================================== */ 56 | 57 | @import "_components/_songlist"; 58 | 59 | /* ========================================================================== 60 | @Charts 61 | ========================================================================== */ 62 | @import "_components/_charts"; 63 | 64 | /* ========================================================================== 65 | @Playlist songList 66 | ========================================================================== */ 67 | @import "_components/_playlist-songs"; 68 | 69 | /* ========================================================================== 70 | @Loader 71 | ========================================================================== */ 72 | @import "_components/_loader"; 73 | 74 | /* ========================================================================== 75 | @Spinner 76 | ========================================================================== */ 77 | @import "_components/_spinner"; 78 | 79 | /* ========================================================================== 80 | @Following 81 | ========================================================================== */ 82 | @import "_components/_following"; 83 | 84 | 85 | /* ========================================================================== 86 | @Profile 87 | ========================================================================== */ 88 | @import "_components/_profile"; 89 | 90 | /* ========================================================================== 91 | @Dropdown 92 | ========================================================================== */ 93 | @import "_components/_dropdown"; 94 | 95 | /* ========================================================================== 96 | @QueueList 97 | ========================================================================== */ 98 | @import "_components/_queueList"; 99 | 100 | /* ========================================================================== 101 | @Track 102 | ========================================================================== */ 103 | @import "_components/_track"; 104 | 105 | /* ========================================================================== 106 | @Buttons 107 | ========================================================================== */ 108 | @import "_components/_buttons"; 109 | 110 | /* ========================================================================== 111 | @Settings 112 | ========================================================================== */ 113 | @import "_components/_settings"; 114 | -------------------------------------------------------------------------------- /app/soundnode.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/soundnode.icns -------------------------------------------------------------------------------- /app/soundnode.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/soundnode.ico -------------------------------------------------------------------------------- /app/soundnode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/app/soundnode.png -------------------------------------------------------------------------------- /app/views/about/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You have the latest version {{ appVersion }}. 7 | Your current version ({{ appVersion }}) isn't the latest ({{ appLatestVersion }}), please download the latest version. 8 | 9 | -------------------------------------------------------------------------------- /app/views/charts/charts.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | Charts By Genre 7 | 8 | {{genre.title}} 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/views/common/filter-tracks.html: -------------------------------------------------------------------------------- 1 | Filter tracks by: 2 | 3 | 4 | 5 | 6 | > 30 minutes 7 | 8 | 9 | 10 | 11 | 12 | no reposts 13 | 14 | 15 | 16 | 17 | 18 | > 10k listens 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/views/common/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/views/common/modals/closeButton.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/views/common/modals/confirm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/views/common/modals/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/views/common/modals/rate-limit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Time to reset: {{timeToReset}} 6 | -------------------------------------------------------------------------------- /app/views/common/queueList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TRACKS 7 | | 8 | ARTIST 9 | 10 | 19 | 20 | {{ $index + 1 }} 21 | {{ item.songTitle }} 22 | by: {{ item.songUser }} 23 | 27 | 28 | 29 | 30 | Remove 31 | Like 32 | Repost 33 | Add to playlist 34 | Go to track 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/views/common/tracks.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | {{data.comment_count | round}} 13 | 14 | 15 | {{data.likes_count || data.favoritings_count | round}} 16 | 17 | 18 | {{data.reposts_count | round}} 19 | 20 | 21 | 22 | 23 | 24 | {{ data.title }} 25 | 26 | 27 | 28 | 29 | {{ data.user.username }} 30 | 31 | 32 | 33 | 34 | {{ user.username }} 35 | 36 | 37 | 38 | 39 | {{ formatSongDuration (data.duration) }} 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | #{{ data.genre }} 59 | {{ data.license }} 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/views/favorites/favorites.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/views/followers/followers.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{ data.username }} 29 | 30 | Tracks: {{ data.track_count }} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/views/following/following.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{ data.username }} 29 | 30 | Tracks: {{ data.track_count }} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/views/news/news.html: -------------------------------------------------------------------------------- 1 | 2 | News 3 | 4 | -------------------------------------------------------------------------------- /app/views/playlists/playlistDashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Where do you want to add: {{ playlistSongName }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{ data.title }} 20 | 21 | {{ data.track_count }} 22 | 23 | 24 | {{ formatSongDuration(data.duration) }} 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/views/playlists/playlists.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | 7 | 13 | 14 | {{ data.title }} playlist - show 15 | 16 | {{ data.track_count }} 17 | 18 | 19 | {{ formatSongDuration (data.duration) }} 20 | 21 | 22 | delete playlist 23 | 24 | 25 | 26 | 32 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {{ tracks.title }} 52 | 53 | by: {{ tracks.user.username }} 54 | {{ formatSongDuration (tracks.duration) }} 55 | 56 | remove 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/views/profile/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{follow_button_text}} 9 | {{profile_data.username}} 10 | Followers: {{followers_count}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | Tracks: 18 | 19 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/views/search/search.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} {{ keyword }} 3 | 4 | 5 | 6 | 7 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/views/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ ::title }} 4 | 5 | 6 | 7 | 8 | 9 | Notifications 10 | Enable/Disable song notifications 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | API Key 22 | Swap out API Key for your own. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Clean local storage 31 | Remove last user logged in, last window screen size, notification state. 32 | 33 | 34 | 35 | Clean All 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/views/stream/stream.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/views/tag/tag.html: -------------------------------------------------------------------------------- 1 | 2 | Most popular tracks for #{{tag}} 3 | 4 | 5 | 6 | 7 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/views/track/track.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{track.playback_count | number}} 26 | 27 | 30 | {{track.favoritings_count | round}} 31 | 32 | 33 | 34 | 35 | 36 | 39 | {{track.user_favorite ? 'Liked' : 'Like'}} 40 | {{track.user_favorite ? 'Unlike' : 'Like'}} 41 | 42 | {{track.title}} 43 | 44 | {{track.user.username}} 45 | Created: {{track.created_at | date: short}} 46 | 47 | 49 | 50 | 51 | {{ track.purchase_title }} 52 | Download 53 | Add to playlist 54 | Permalink 55 | Copy track link 56 | 57 | 58 | 59 | 60 | 61 | #{{tag}} 62 | 63 | 64 | 69 | Comments: ({{track.comment_count}}) 70 | 71 | 72 | 73 | 74 | 75 | 76 | {{comment.user.username}} 77 | at {{comment.timestamp | date : 'mm:ss'}} 78 | {{comment.created_at | limitTo: 19}} 79 | 80 | 81 | {{comment.body}} 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/views/tracks/tracks.html: -------------------------------------------------------------------------------- 1 | 2 | {{ ::title }} 3 | 4 | 5 | 6 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /doc/img/dev_tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soundnode/soundnode-app/04ddd01e1135738ff36ef2b8152966c375e46674/doc/img/dev_tools.png -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require('fs-extra'); 4 | const { 5 | app, 6 | BrowserWindow, 7 | ipcMain, 8 | globalShortcut, 9 | Menu 10 | } = require('electron'); 11 | const windowStateKeeper = require('electron-window-state'); 12 | const configuration = require('./app/public/js/common/configLocation'); 13 | 14 | // custom constants 15 | const clientId = '342b8a7af638944906dcdb46f9d56d98'; 16 | const redirectUri = 'http://sc-redirect.herokuapp.com/callback.html'; 17 | const SCconnect = `https://soundcloud.com/connect?&client_id=${clientId}&redirect_uri=${redirectUri}&response_type=token`; 18 | 19 | let mainWindow; 20 | let authenticationWindow; 21 | 22 | app.on('ready', () => { 23 | checkUserConfig(); 24 | }); 25 | 26 | function checkUserConfig() { 27 | const containsConfig = configuration.containsConfig(); 28 | 29 | if (containsConfig) { 30 | initMainWindow(); 31 | } else { 32 | authenticateUser(); 33 | } 34 | } 35 | 36 | /** 37 | * User config file doesn't exists 38 | * therefore open soundcloud authentication page 39 | */ 40 | function authenticateUser() { 41 | let contents; 42 | 43 | authenticationWindow = new BrowserWindow({ 44 | width: 600, 45 | height: 600, 46 | frame: false, 47 | webPreferences: { 48 | nodeIntegration: false, 49 | webSecurity: false 50 | } 51 | }); 52 | authenticationWindow.loadURL(SCconnect); 53 | authenticationWindow.show(); 54 | 55 | contents = authenticationWindow.webContents; 56 | 57 | contents.on('did-navigate', (_event, url, httpResponseCode) => { 58 | const access_tokenStr = 'access_token='; 59 | const expires_inStr = '&expires_in'; 60 | let accessToken; 61 | 62 | if (url.indexOf('access_token=') < 0) { 63 | return false; 64 | } 65 | 66 | accessToken = url.substring(url.indexOf(access_tokenStr) + 13, url.indexOf(expires_inStr)); 67 | 68 | accessToken = accessToken.split('&scope=')[0]; 69 | 70 | setUserData(accessToken); 71 | authenticationWindow.destroy(); 72 | }); 73 | } 74 | 75 | function setUserData(accessToken) { 76 | fs.writeFileSync(configuration.getPath(), JSON.stringify({ 77 | accessToken: accessToken, 78 | clientId: clientId 79 | }), 'utf-8'); 80 | 81 | initMainWindow(); 82 | } 83 | 84 | function initMainWindow() { 85 | let mainWindowState = windowStateKeeper({ 86 | defaultWidth: 1180, 87 | defaultHeight: 755 88 | }); 89 | 90 | mainWindow = new BrowserWindow({ 91 | x: mainWindowState.x, 92 | y: mainWindowState.y, 93 | width: mainWindowState.width, 94 | height: mainWindowState.height, 95 | minWidth: 800, 96 | minHeight: 640, 97 | center: true, 98 | frame: false, 99 | webPreferences: { 100 | nodeIntegration: true 101 | } 102 | }); 103 | 104 | mainWindow.loadURL(`file://${__dirname}/app/index.html`); 105 | 106 | if (process.env.NODE_ENV === 'development') { 107 | mainWindow.webContents.openDevTools(); 108 | } 109 | 110 | mainWindow.webContents.on('will-navigate', function (e, url) { 111 | if (url.indexOf('build/index.html#') < 0) { 112 | e.preventDefault(); 113 | } 114 | }); 115 | 116 | mainWindow.webContents.on('did-finish-load', function () { 117 | mainWindow.setTitle('Soundnode'); 118 | mainWindow.show(); 119 | mainWindow.focus(); 120 | }); 121 | 122 | mainWindowState.manage(mainWindow); 123 | initializeMediaShortcuts(); 124 | menuBar(); 125 | } 126 | 127 | app.on('will-quit', () => { 128 | // Unregister all shortcuts. 129 | globalShortcut.unregisterAll() 130 | }) 131 | 132 | app.on('activate', () => { 133 | showAndFocus(); 134 | }); 135 | 136 | /** 137 | * Receive maximize event and trigger command 138 | */ 139 | ipcMain.on('maximizeApp', () => { 140 | if (mainWindow.isMaximized()) { 141 | mainWindow.unmaximize(); 142 | } else { 143 | mainWindow.maximize(); 144 | } 145 | }); 146 | 147 | /** 148 | * Receive minimize event and trigger command 149 | */ 150 | ipcMain.on('minimizeApp', () => { 151 | mainWindow.minimize() 152 | }); 153 | 154 | /** 155 | * Receive hide event and trigger command 156 | */ 157 | ipcMain.on('hideApp', () => { 158 | mainWindow.hide(); 159 | }); 160 | 161 | ipcMain.on('showApp', () => { 162 | showAndFocus(); 163 | }); 164 | 165 | /** 166 | * Receive close event and trigger command 167 | */ 168 | ipcMain.on('closeApp', () => { 169 | if (process.platform !== "darwin") { 170 | mainWindow.destroy(); 171 | } else { 172 | mainWindow.hide(); 173 | } 174 | }); 175 | 176 | // 177 | ipcMain.on('destroyApp', () => { 178 | mainWindow.close(); 179 | }); 180 | 181 | function showAndFocus() { 182 | mainWindow.show(); 183 | mainWindow.focus(); 184 | } 185 | 186 | function initializeMediaShortcuts() { 187 | globalShortcut.register('MediaPlayPause', () => { 188 | mainWindow.webContents.send('MediaPlayPause'); 189 | }); 190 | 191 | globalShortcut.register('MediaStop', () => { 192 | mainWindow.webContents.send('MediaStop'); 193 | }); 194 | 195 | globalShortcut.register('MediaPreviousTrack', () => { 196 | mainWindow.webContents.send('MediaPreviousTrack'); 197 | }); 198 | 199 | globalShortcut.register('MediaNextTrack', () => { 200 | mainWindow.webContents.send('MediaNextTrack'); 201 | }); 202 | } 203 | 204 | function menuBar() { 205 | const template = [ 206 | { 207 | role: 'editMenu', 208 | label: 'Soundnode' 209 | }, 210 | { 211 | role: 'view', 212 | label: 'View', 213 | submenu: [ 214 | { 215 | role: 'togglefullscreen' 216 | }, 217 | { 218 | role: 'close' 219 | }, 220 | { 221 | type: 'separator' 222 | }, 223 | { 224 | label: 'Learn More', 225 | click() { 226 | require('electron').shell.openExternal('https://github.com/Soundnode/soundnode-app/wiki/Help') 227 | } 228 | }, 229 | { 230 | label: 'License', 231 | click() { 232 | require('electron').shell.openExternal('https://github.com/Soundnode/soundnode-app/blob/master/LICENSE.md') 233 | } 234 | } 235 | ] 236 | }, 237 | { 238 | role: 'windowMenu', 239 | submenu: [ 240 | { 241 | role: 'quit' 242 | }, 243 | { 244 | label: 'Reload', 245 | accelerator: 'CmdOrCtrl+R', 246 | click(item, focusedWindow) { 247 | if (focusedWindow) { 248 | focusedWindow.reload() 249 | } 250 | } 251 | }, 252 | { 253 | label: 'Toggle Developer Tools', 254 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', 255 | click(item, focusedWindow) { 256 | if (focusedWindow) { 257 | focusedWindow.webContents.toggleDevTools() 258 | } 259 | } 260 | }, 261 | { 262 | type: 'separator' 263 | }, 264 | { 265 | role: 'resetzoom' 266 | }, 267 | { 268 | role: 'zoomin' 269 | }, 270 | { 271 | role: 'zoomout' 272 | } 273 | ] 274 | } 275 | ]; 276 | 277 | const menu = Menu.buildFromTemplate(template); 278 | Menu.setApplicationMenu(menu) 279 | } 280 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Soundnode", 3 | "version": "7.0.0", 4 | "main": "main.js", 5 | "description": "Soundnode App is the Soundcloud for desktop", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/Soundnode/soundnode-app.git" 9 | }, 10 | "scripts": { 11 | "prestart": "npm run watch", 12 | "start": "./node_modules/.bin/electron .", 13 | "release": "npm run clean && npm run webpack:prod && npm run sass:prod && npm run package:all", 14 | "watch": "npm run webpack:dev & npm run sass:dev", 15 | "clean": "./node_modules/.bin/rimraf ./dist", 16 | "webpack:prod": "./node_modules/.bin/webpack -p --config ./webpack.prod.js", 17 | "webpack:dev": "./node_modules/.bin/webpack -d --watch --config ./webpack.dev.js", 18 | "sass:prod": "./node_modules/.bin/node-sass --include-path ./app/public/stylesheets/sass --output-style compressed ./app/public/stylesheets/sass/app.scss ./app/public/stylesheets/css/app.css", 19 | "sass:dev": "./node_modules/.bin/node-sass --recursive --include-path ./app/public/stylesheets/sass --output-style expanded ./app/public/stylesheets/sass/app.scss ./app/public/stylesheets/css/app.css", 20 | "package:osx": "electron-packager ./ Soundnode --platform=darwin --out ./dist/Soundnode --electron-version 8.0.1 --overwrite --icon ./app/soundnode.ico", 21 | "package:linux": "electron-packager ./ Soundnode --platform=linux --out ./dist/Soundnode --electron-version 8.0.1 --overwrite --icon ./app/soundnode.icns", 22 | "package:win32": "electron-packager ./ Soundnode --platform=win32 --out ./dist/Soundnode --electron-version 8.0.1 --overwrite --icon ./app/soundnode.icns", 23 | "package:all": "npm run package:osx && npm run package:linux && npm run package:win32" 24 | }, 25 | "author": "Michael Lancaster", 26 | "license": "GPL-3.0", 27 | "devDependencies": { 28 | "babel-core": "6.26.3", 29 | "babel-loader": "^7.0.0", 30 | "babel-preset-es2015": "^6.22.0", 31 | "babel-preset-react": "^6.23.0", 32 | "babel-preset-stage-0": "^6.22.0", 33 | "babel-register": "^6.23.0", 34 | "electron": "^8.0.1", 35 | "electron-packager": "^14.2.1", 36 | "eslint": "^4.0.0", 37 | "eslint-plugin-react": "^7.1.0", 38 | "install": "^0.10.1", 39 | "node-sass": "^4.5.0", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^3.3.0" 42 | }, 43 | "dependencies": { 44 | "angular": "^1.6.2", 45 | "angular-hotkeys": "^1.7.0", 46 | "angular-sanitize": "^1.6.2", 47 | "angular-toastr": "^2.1.1", 48 | "angular-ui-router": "^0.4.2", 49 | "electron-window-state": "^5.0.0", 50 | "font-awesome": "^4.7.0", 51 | "fs-extra": "^8.0.0", 52 | "jquery": "^3.1.1", 53 | "lodash": "^4.17.4", 54 | "mkdirp": "^0.5.1", 55 | "moment": "^2.17.1", 56 | "ng-dialog": "^1.0.0", 57 | "ng-infinite-scroll": "^1.3.0", 58 | "normalize.css": "^8.0.0", 59 | "react": "^16.0.0", 60 | "react-dom": "^16.0.0", 61 | "toastr": "^2.1.2", 62 | "universal-analytics": "^0.4.8", 63 | "user-home": "^2.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | target: 'node', 8 | devtool: 'eval', 9 | entry: path.join(__dirname, './app/public/js/components/main.jsx'), 10 | output: { 11 | path: path.join(__dirname, './app/dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | use: [ 20 | 'babel-loader' 21 | ] 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.jsx'] 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | 'process.env.NODE_ENV': '"development"' 31 | }), 32 | new webpack.NoEmitOnErrorsPlugin() 33 | ], 34 | externals: [ 35 | (function () { 36 | const IGNORES = [ 37 | 'electron' 38 | ]; 39 | return function (context, request, callback) { 40 | if (IGNORES.indexOf(request) >= 0) { 41 | return callback(null, "require('" + request + "')"); 42 | } 43 | return callback(); 44 | }; 45 | })() 46 | ] 47 | }; -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | target: 'node', 8 | devtool: 'source-map', 9 | entry: path.join(__dirname, './app/public/js/components/main.jsx'), 10 | output: { 11 | path: path.join(__dirname, './app/dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | use: [ 20 | 'babel-loader' 21 | ] 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.jsx'] 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | 'process.env.NODE_ENV': '"production"' 31 | }), 32 | new webpack.optimize.AggressiveMergingPlugin(), 33 | new webpack.NoEmitOnErrorsPlugin() 34 | ], 35 | externals: [ 36 | (function () { 37 | const IGNORES = [ 38 | 'electron' 39 | ]; 40 | return function (context, request, callback) { 41 | if (IGNORES.indexOf(request) >= 0) { 42 | return callback(null, "require('" + request + "')"); 43 | } 44 | return callback(); 45 | }; 46 | })() 47 | ] 48 | }; --------------------------------------------------------------------------------
Where do you want to add: {{ playlistSongName }}
49 |
{{comment.body}}
76 | {{comment.user.username}} 77 | at {{comment.timestamp | date : 'mm:ss'}} 78 | {{comment.created_at | limitTo: 19}} 79 |
80 | 81 |{{comment.body}}
82 |