├── .gitignore ├── LICENSE ├── README.md ├── app ├── assets │ ├── css │ │ ├── base.css │ │ ├── config.css │ │ ├── main.css │ │ └── unsemantic.css │ ├── files │ │ └── Hind-Regular.ttf │ ├── img │ │ ├── config.svg │ │ ├── equalizer.svg │ │ ├── folder.svg │ │ ├── icon.png │ │ ├── icon@1.25x.png │ │ ├── icon@1.33x.png │ │ ├── icon@1.4x.png │ │ ├── icon@1.8x.png │ │ ├── lang.svg │ │ ├── legal.svg │ │ ├── next.svg │ │ ├── play.png │ │ ├── play.svg │ │ ├── prev.svg │ │ ├── search.svg │ │ ├── soube-icon.svg │ │ ├── thumb-next.png │ │ ├── thumb-pause.png │ │ ├── thumb-play.png │ │ └── thumb-prev.png │ └── js │ │ ├── config │ │ ├── index.js │ │ └── lang.json │ │ ├── configMain.js │ │ ├── dom │ │ └── index.js │ │ ├── factory │ │ └── index.js │ │ ├── main.js │ │ ├── player │ │ ├── addSongFolder │ │ │ └── index.js │ │ ├── controls │ │ │ └── index.js │ │ ├── createView │ │ │ └── index.js │ │ ├── equalizer │ │ │ └── index.js │ │ └── index.js │ │ ├── thumbar │ │ └── index.js │ │ └── version │ │ └── index.js ├── index.js ├── package.json └── views │ ├── config-panel │ └── config.html │ └── main │ └── index.html ├── build ├── icon.icns ├── icon.ico └── icons │ ├── 144x144.png │ ├── 192x192.png │ ├── 192x292.png │ ├── 48x48.png │ ├── 72x72.png │ └── 96x96.png ├── jsconfig.json ├── package.json └── soube.desktop /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Diego Molina Vera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Soube](app/assets/img/icon@1.8x.png) 2 | 3 | # Soube 4 | 5 | ### This project is not longer maintained 6 | 7 | Thanks to all the people who downloaded the application and gave me feedback to improve it. Also, thanks to those who forked the project and those who gave me an start. 8 | 9 | Soube was a nice project from where I learned a lot from my mistakes, but for others reasons I stopped working on it for years. I wanted to improve the code, have a better folder structure, following JS best practices, and coding in a clean way (I think you can see that intention on the development branch). But this is not the end. The past years that I didn't work on Soube I was growing up as a developer, specially with JavaScript and some other importants Knowledge like Data Structure. Now I have a little bit of more knowledge that it will help me to bring Soube again but powerful and not only focused on local music file. This new Soube version won't be public until I decide it is stable and good enough to be share it with you all. I won't close this repository, so you are free to use it. Thanks you all! 10 | 11 | Soube is a simple and minimalist music player based on Electronjs. 12 | 13 | #### Install it on Windows, Mac & Linux 14 | 15 | [Soube website](http://soube.diegomolina.cl) 16 | 17 | ## Features 18 | * Notifications showing what song is played. 19 | * Auto detection of new songs. 20 | * Idiom. You can change the idiom of the config panel (Do this before anything). 21 | * Equalizer. 22 | * Responsive design. 23 | * Shorcuts to set play/pause,next, prev and disalbe/enable shuffle. 24 | * Searching by song. 25 | 26 | ## Linux users 27 | * For Distributions that don't use **rpm** or **deb** extensions, you have to follow the steps below. 28 | 29 | #### Step 1 30 | 31 | ``` 32 | Extract the files and you will see the next folder: 33 | * soube-linux-ia32 34 | or 35 | * soube-linux-x64 36 | ``` 37 | 38 | #### Step 2 39 | * Move the folder using the next command line. 40 | 41 | ``` 42 | sudo mv [Place_where_is_soube_folder]/[soube-linux-ia32 or soube-linux-x64] /opt/soube 43 | ``` 44 | 45 | #### step 3 46 | * Download the [soube.desktop](https://github.com/DracotMolver/Soube/blob/master/soube.desktop) file for an icon launcher and move it to this location (or your prefer one): 47 | 48 | ``` 49 | sudo [Place_where_is_the_file]/soube.desktop /usr/share/applications 50 | 51 | ``` 52 | 53 | * Done!. You should be ready to use Soube. 54 | 55 | ## Shortcuts 56 | 57 | * Ctrl + F // Display the searching option 58 | * Ctrl + Up // Set Play/Pause the song 59 | * Ctrl + Left // Prev song 60 | * Ctrl + Right // Next song 61 | * Ctrl + Down // Switch shuffle 62 | -------------------------------------------------------------------------------- /app/assets/css/base.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016- 2017 4 | */ 5 | /* ------------------------ GENERALES ------------------------ */ 6 | @font-face { 7 | font-family: 'Hind'; 8 | src: url(../files/Hind-Regular.ttf) 9 | } 10 | 11 | /** 12 | * Se realizan reset pero de los atributos más importantes 13 | * que vamos a usar en Chromium. 14 | * Al no ser una aplicación web "online", no debemos de preocuperanos, en parte, 15 | * por usar el comodín [*] para formatear todos los elementos HTML 16 | */ 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6, 23 | span, 24 | div, 25 | p { 26 | -webkit-text-size-adjust: 100%; 27 | -webkit-font-smooth: antialiased; 28 | font-variant-ligatures: common-ligatures no-discretionary-ligatures no-historical-ligatures; 29 | font-stretch: condensed; 30 | font-family: 'Hind', sans-serif; 31 | font-weight: normal; 32 | line-height: 1.35em; 33 | font-size: 100%; 34 | padding: 0; 35 | margin: 0; 36 | cursor: default; 37 | color: var(--blackColor) 38 | } 39 | 40 | input, 41 | button { 42 | border: none 43 | } 44 | 45 | input:focus, 46 | button:focus { 47 | outline-width: 0 48 | } 49 | 50 | ul { 51 | list-style: none 52 | } 53 | 54 | body, 55 | html { 56 | overflow: hidden; 57 | padding: 0; 58 | margin: 0; 59 | bottom: 0; 60 | height: 100%; 61 | width: 100%; 62 | right: 0; 63 | left: 0; 64 | top: 0 65 | } 66 | 67 | img, 68 | svg, 69 | a { 70 | cursor: pointer 71 | } 72 | 73 | .hide { 74 | display: none 75 | } 76 | 77 | /* ------------------------ VARIABLES ------------------------ */ 78 | :root { 79 | --lightGreyColor: #F4F6F6; 80 | --darkGreyColor: #979A9A; 81 | --darkPinkColor: #d81b60; 82 | --lightPinkColor: #f06292; 83 | --whiteColor: #FBFCFC; 84 | --blackColor: #424949; 85 | --pinkColor: #e91e63 86 | } 87 | 88 | /* ------------------------ GENERALES ------------------------ */ 89 | .border-bottom, 90 | .border-left, 91 | .border-right { 92 | border-width: 0; 93 | border-style: solid; 94 | box-sizing: border-box; 95 | } 96 | 97 | .border-bottom { 98 | border-bottom-width: .01em 99 | } 100 | 101 | .border-left { 102 | border-left-width: .01em 103 | } 104 | 105 | .border-right { 106 | border-right-width: .01em 107 | } 108 | 109 | .border-color-dark-pink { 110 | border-color: var(--darkPinkColor) 111 | } 112 | 113 | .border-color-light-grey { 114 | border-color: var(--lightGreyColor) 115 | } 116 | 117 | .border-color-dark-grey { 118 | border-color: var(--darkGreyColor) 119 | } 120 | 121 | .border-color-white { 122 | border-color: var(--whiteColor) 123 | } 124 | 125 | .border-color-black { 126 | border-color: var(--blackColor) 127 | } 128 | 129 | .border-color-pink { 130 | border-color: var(--pinkColor) 131 | } 132 | 133 | .border-color-light-pink { 134 | border-color: var(--lightPinkColor) 135 | } 136 | 137 | /* ------------------------ SCROLLBAR ------------------------ */ 138 | ::-webkit-scrollbar { 139 | background-color: var(--whiteColor); 140 | width: .8em 141 | } 142 | 143 | ::-webkit-scrollbar-thumb:window-inactive, 144 | ::-webkit-scrollbar-thumb { 145 | background: var(--blackColor) 146 | } -------------------------------------------------------------------------------- /app/assets/css/config.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016- 2017 4 | */ 5 | /* ------------------------ GENERALES ------------------------*/ 6 | h1 { 7 | font-size: 1.1em 8 | } 9 | 10 | h4 { 11 | font-size: .9em 12 | } 13 | 14 | h5 { 15 | font-size: .5625em 16 | } 17 | 18 | #nav { 19 | background: var(--pinkColor) 20 | } 21 | 22 | #nav > h1 { 23 | font-weight: 400; 24 | padding: .2em .34em; 25 | float: left; 26 | color: var(--whiteColor) 27 | } 28 | 29 | #_titleconfig, 30 | .range-circle:hover, 31 | .config-icons > img:hover, 32 | #legal > h5, 33 | .lang-option:hover, 34 | a, 35 | #_neweq { 36 | cursor: pointer 37 | } 38 | 39 | .config-icons { 40 | position: relative; 41 | margin: 1.6em auto auto 0; 42 | height: 4.6em 43 | } 44 | 45 | .config-option p { 46 | margin-bottom: .5em; 47 | font-weight: 500; 48 | margin-top: .5em; 49 | text-align: center; 50 | font-size: .95em 51 | } 52 | 53 | .config-option h4 { 54 | font-weight: 300; 55 | text-align: center; 56 | color: var(--darkGreyColor) 57 | } 58 | 59 | .config-opt-anim { 60 | animation: confSlide 1s ease-in-out forwards 61 | } 62 | 63 | .range-circle::before, 64 | #hertz::before, 65 | #k-hertz::before{ 66 | position: absolute; 67 | content: '' 68 | } 69 | 70 | /* ----------------------------- LEGAL ---------------------------- */ 71 | #legal { 72 | background: var(--whiteColor); 73 | padding: 1em; 74 | border: .01em solid var(--lightGreyColor); 75 | margin: 2em auto; 76 | height: auto; 77 | width: 60em; 78 | } 79 | 80 | #legal > h5 { 81 | color: var(--darkGreyColor) 82 | } 83 | 84 | /* ------------------------ CONFIGURACIÓN CARPETA DE MÚSICA ------------------------*/ 85 | #loading { 86 | background: rgba(245, 245, 245, .3); 87 | position: fixed; 88 | z-index: 3; 89 | height: 100%; 90 | width: 100%; 91 | cursor: wait; 92 | top: 0 93 | } 94 | 95 | #loading-info { 96 | margin-top: 18%; 97 | text-align: center; 98 | width: 100%; 99 | color: var(--blackColor); 100 | float: left 101 | } 102 | 103 | /* ------------------------ CONFIGURACIÓN DEL IDIOMA ------------------------*/ 104 | #lang { 105 | box-sizing: border-box 106 | } 107 | 108 | .lang-option:hover { 109 | border-bottom: .1em solid #0277bd 110 | } 111 | 112 | .lang-option { 113 | margin-top: .6em; 114 | text-align: center; 115 | font-size: 2em 116 | } 117 | 118 | /* ----------------------------- EQUALIZADOR ---------------------------- */ 119 | .range-container { 120 | border-radius: 5px; 121 | background: var(--darkGreyColor); 122 | padding: 0; 123 | margin: .5em 1.225em 1.4em; 124 | opacity: .5; 125 | height: 18em; 126 | width: .6em; 127 | float: left 128 | } 129 | 130 | .range-container:hover{ 131 | opacity: 1 132 | } 133 | 134 | .range-circle { 135 | background: rgba(233,30,99, .14); 136 | border-radius: 50%; 137 | position: relative; 138 | border: .1em solid var(--lightPinkColor); 139 | height: 1.5em; 140 | width: 1.5em; 141 | left: -9px; 142 | top: 130px 143 | } 144 | 145 | .range-circle::before { 146 | border-radius: 50%; 147 | box-shadow: 0 1px 2px 1px rgba(0,0,0,.14); 148 | background: var(--darkPinkColor); 149 | border: .1em solid var(--pinkColor); 150 | height: .7em; 151 | width: .7em; 152 | left: 5px; 153 | top: 5px 154 | } 155 | 156 | #equalizer-container { 157 | margin-top: 1em; 158 | height: 100%; 159 | width: 100%; 160 | float: left 161 | } 162 | 163 | #equalizer-container ul { 164 | padding: 0; 165 | margin: .6em .2em; 166 | width: 100%; 167 | float: left 168 | } 169 | 170 | #equalizer-container ul > li { 171 | text-align: center; 172 | display: inline-block; 173 | width: 2.8em 174 | } 175 | 176 | #hertz { 177 | width: 35em 178 | } 179 | 180 | #k-hertz { 181 | width: 32.2em 182 | } 183 | 184 | #hertz, 185 | #k-hertz { 186 | padding-right: .5em; 187 | padding-left: .5em; 188 | text-align: center; 189 | float: left 190 | } 191 | 192 | #hertz > div, 193 | #k-hertz > div{ 194 | background: white; 195 | margin: 0 auto; 196 | width: 3em; 197 | } 198 | 199 | #hertz::before, 200 | #k-hertz::before { 201 | background: var(--lightGreyColor); 202 | z-index: -1; 203 | height: 2px; 204 | top: 112px 205 | } 206 | 207 | #hertz::before { 208 | width: 35em; 209 | left: 13px 210 | } 211 | 212 | #k-hertz::before { 213 | width: 31em; 214 | left: 609px 215 | } 216 | 217 | #eq-buttons { 218 | background: var(--whiteColor) 219 | } 220 | 221 | #name-new-eq, 222 | #eq-buttons, 223 | #_neweq { 224 | border: 1px solid rgba(0,0,0,.14); 225 | border-radius: 4px; 226 | margin: 1em 0 .5em 1em 227 | } 228 | 229 | #name-new-eq { 230 | height: 28px; 231 | width: 58% 232 | } 233 | 234 | #_neweq { 235 | padding-right: .6em; 236 | padding-left: .6em; 237 | line-height: 1.9em; 238 | background: var(--darkGreyColor); 239 | color: var(--whiteColor) 240 | } 241 | 242 | #_neweq[disabled]{ 243 | background: var(--whiteColor); 244 | color: #d4d4d4 245 | } 246 | 247 | /*---------------------------------- ANIMACIONES ----------------------------------*/ 248 | @keyframes confSlide { 249 | 100% { 250 | transform: translateX(-100%); 251 | opacity: 0 252 | } 253 | } -------------------------------------------------------------------------------- /app/assets/css/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016- 2017 4 | */ 5 | /* ------------------------ GENERAL ------------------------*/ 6 | .btn-controls::after, 7 | .arrow::after, 8 | .arrow::before, 9 | .list-song-container::after, 10 | .tooltip::after { 11 | position: absolute; 12 | content: '' 13 | } 14 | 15 | /* ------- CONTROL PARA MOVER LA LISTA DE CANCIONES --------*/ 16 | #updown { 17 | border-radius: 5px; 18 | text-align: center; 19 | background: var(--whiteColor); 20 | box-shadow: 0 6px 10px 0 rgba(0, 0, 0, .3); 21 | position: absolute; 22 | z-index: 2; 23 | border: 1px solid var(--lightGreyColor); 24 | height: 30px; 25 | bottom: 10px; 26 | width: 66px; 27 | right: 22px 28 | } 29 | 30 | #updown:hover { 31 | cursor: pointer 32 | } 33 | 34 | #updown .arrow-updown:first-child { 35 | border-right: 1px solid var(--lightGreyColor) 36 | } 37 | 38 | #updown .arrow-updown{ 39 | line-height: 31px 40 | } 41 | 42 | /* ------------------------ TOP NAV ------------------------*/ 43 | #top-nav { 44 | background: var(--pinkColor) 45 | } 46 | 47 | #config { 48 | margin-top: 1.9em; 49 | padding-right: 35%; 50 | padding-left: 35%; 51 | height: 2em 52 | } 53 | 54 | /* ------------------------ CONTROLES DEL REPRODUCTOR ------------------------ */ 55 | .btn-controls { 56 | text-align: center; 57 | margin: .3em 0; 58 | height: 1.2em; 59 | float: left 60 | } 61 | 62 | .btn-controls::after { 63 | border-radius: 50%; 64 | background: var(--darkPinkColor); 65 | opacity: 0; 66 | height: 1.7em; 67 | width: 1.7em; 68 | left: 104px 69 | } 70 | 71 | #play-pause { 72 | text-align: center; 73 | margin: 1.25em .9em; 74 | height: 3em 75 | } 76 | 77 | #play-pause::after { 78 | height: 4em; 79 | width: 4em; 80 | left: 14px; 81 | top: 12px 82 | } 83 | 84 | #shuffle-icon { 85 | fill: var(--lightPinkColor) 86 | } 87 | 88 | #next::after { 89 | top: 0 90 | } 91 | 92 | #shuffle::after { 93 | top: 61px 94 | } 95 | 96 | #prev::after { 97 | top: 31px 98 | } 99 | 100 | .click-controls::after { 101 | animation: clickControlsEffect .3s ease-out forwards 102 | } 103 | 104 | #total-progress-bar, 105 | #progress-bar { 106 | height: .36em; 107 | cursor: e-resize 108 | } 109 | 110 | #total-progress-bar { 111 | background: var(--lightPinkColor) 112 | } 113 | 114 | #progress-bar { 115 | background: var(--whiteColor); 116 | box-shadow: 1px 0 3px 0 #fff; 117 | float: left; 118 | width: 0 119 | } 120 | 121 | /* ------------------------ DETALLES DE LA CANCIÓN ------------------------ */ 122 | #info-song { 123 | text-align: left; 124 | overflow: hidden; 125 | margin: .1em 1.2em; 126 | width: 85%; 127 | float: left 128 | } 129 | 130 | #info-song { 131 | -webkit-user-select: none; 132 | } 133 | 134 | #song-title > h1 { 135 | font-weight: 500; 136 | font-size: 1.6em 137 | } 138 | 139 | #artist > h2 { 140 | font-size: 1.3em 141 | } 142 | 143 | #album > h3 { 144 | font-weight: 300; 145 | font-size: 1em 146 | } 147 | 148 | #song-title > h1, 149 | #artist > h2, 150 | #album > h3, 151 | .search-results { 152 | text-overflow: ellipsis; 153 | overflow: hidden 154 | } 155 | 156 | #time { 157 | text-align: center; 158 | font-size: 1.16em; 159 | margin: .5em 0 160 | } 161 | 162 | #time-start, 163 | #time-end, 164 | #artist > h2 { 165 | font-weight: 400 166 | } 167 | 168 | #song-title > h1, 169 | #album > h3, 170 | #artist > h2, 171 | #time-end, 172 | #time-start 173 | /*#searchBy > option */ 174 | { 175 | color: var(--whiteColor); 176 | } 177 | 178 | /* ------------------------ LISTADO DE CANCIONES ------------------------ */ 179 | #list-songs { 180 | background: linear-gradient(var(--whiteColor) 30%, transparent), 181 | radial-gradient(at 50% 0, rgba(66,73,73, .3), transparent 80%); 182 | background-repeat: no-repeat; 183 | background-size: 100% 50px, 100% 15px; 184 | background-attachment: local, scroll; 185 | overflow: hidden; 186 | overflow-y: scroll; 187 | height: calc(100% - 95px); 188 | width: 100%; 189 | float: left 190 | } 191 | 192 | .list-song-container:nth-child(odd) { 193 | background: var(--lightGreyColor) 194 | } 195 | 196 | .list-song-container:nth-child(even) { 197 | background: var(--whiteColor) 198 | } 199 | 200 | .song-info { 201 | text-overflow: ellipsis; 202 | padding-left: .6em; 203 | font-weight: 300; 204 | line-height: 2em; 205 | font-size: .906em; 206 | overflow: hidden 207 | } 208 | 209 | .miscelaneo { 210 | margin-right: .6em; 211 | color: var(--darkGreyColor) 212 | } 213 | 214 | /* ------------------------ BUSQUEDA DE CANCIONES ------------------------ */ 215 | .search-results { 216 | margin: 1em 217 | } 218 | 219 | #search-container { 220 | background: rgba(245, 245, 245, .3); 221 | position: fixed; 222 | z-index: 10; 223 | height: 100%; 224 | width: 100%; 225 | top: 0 226 | } 227 | 228 | #search-wrapper { 229 | position: relative; 230 | padding: 6em 0 3.5em 0; 231 | margin: auto; 232 | height: auto; 233 | width: 11% 234 | } 235 | 236 | #search { 237 | background-image: url(../../assets/img/search.svg); 238 | background-repeat: no-repeat; 239 | background-color: transparent; 240 | background-size: 1.6em; 241 | border-bottom: .1em solid var(--lightPinkColor); 242 | padding-left: 1.6em; 243 | line-height: 0; 244 | transform: scale(0); 245 | margin: 0; 246 | cursor: text; 247 | width: 94%; 248 | color: var(--blackColor) 249 | } 250 | 251 | #search, 252 | #search-result { 253 | font-weight: 300; 254 | font-size: 1.6em 255 | } 256 | 257 | #search-result { 258 | padding-left: 1.6em; 259 | position: absolute; 260 | z-index: -1; 261 | color: rgba(182, 182, 182, .4); 262 | top: 3.9em 263 | } 264 | 265 | /*#searchBy { 266 | border-radius: 4px; 267 | padding-left: 7px; 268 | background: var(--darkGreyColor); 269 | height: 42px; 270 | border: none; 271 | width: 75%; 272 | color: var(--whiteColor); 273 | }*/ 274 | 275 | .search-anim { 276 | animation: showInputUp .5s ease-in forwards 277 | } 278 | 279 | .search-wrapper-anim { 280 | animation: displayInputSearch .5s .6s ease-in-out forwards 281 | } 282 | 283 | #container-results { 284 | overflow: hidden; 285 | height: 370px 286 | } 287 | 288 | .results, 289 | #wrapper-results{ 290 | height: 100%; 291 | float: left 292 | } 293 | 294 | .no-searching-found { 295 | text-align: center; 296 | font-size: 2.6em 297 | } 298 | 299 | /* ------------------------ PAGINACIÓN DEL INPUT SEARCH ------------------------ */ 300 | #pagination { 301 | position: relative; 302 | height: 30px; 303 | margin: 0 auto; 304 | width: 400px; 305 | top: 1em 306 | } 307 | 308 | .arrow::after, 309 | .arrow::before { 310 | border-radius: 25px; 311 | background: var(--blackColor); 312 | opacity: .2; 313 | height:.3em; 314 | width: 10% 315 | } 316 | 317 | .arrow-open-anim::after { 318 | animation: openArrowDown .6s ease-out forwards 319 | } 320 | 321 | .arrow-open-anim::before { 322 | animation: openArrowUp .6s ease-out forwards 323 | } 324 | 325 | #left-arrow, 326 | #right-arrow { 327 | cursor: pointer; 328 | } 329 | 330 | #left-arrow { 331 | float: left 332 | } 333 | 334 | #left-arrow::before, 335 | #left-arrow::after { 336 | transform-origin: left 337 | } 338 | 339 | #right-arrow::before, 340 | #right-arrow::after { 341 | transform-origin: right 342 | } 343 | 344 | /* -------------------------------------- */ 345 | #right-arrow::after, 346 | #left-arrow::before { /* Para obtener una mejor simetría en los ejes que se unen */ 347 | top: 2px 348 | } 349 | 350 | #right-arrow::before, 351 | #left-arrow::after { /* Para obtener una mejor simetría en los ejes que se unen */ 352 | top: 0 353 | } 354 | 355 | /* -------------------------------------- */ 356 | #right-arrow { 357 | float: right 358 | } 359 | 360 | /* ------------------------ MENSAJE DE BIENVENIDA ------------------------ */ 361 | #init-message { 362 | background: white !important; 363 | margin-top: 4em; 364 | text-align: center; 365 | font-size: 1.6em; 366 | color: var(--darkGreyColor) 367 | } 368 | 369 | /* ------------------------ MENSAJE DE CANCIONES NUEVAS ------------------------ */ 370 | #pop-up-container { 371 | position: absolute; 372 | height: auto; 373 | margin: 0 2%; 374 | width: auto; 375 | right: 0; 376 | left: 0; 377 | top: 6.4em 378 | } 379 | 380 | #pop-up, 381 | .search-results { 382 | border-radius: 2px; 383 | border-bottom: 2px solid var(--pinkColor); 384 | background: var(--darkGreyColor); 385 | text-align: center; 386 | font-size: .94em; 387 | padding: .6em; 388 | color: var(--whiteColor) 389 | } 390 | 391 | #pop-up { 392 | transform: scale(0) 393 | } 394 | 395 | #pop-up > a { 396 | text-decoration: none; 397 | color: currentColor 398 | } 399 | 400 | .pop-up-anim { 401 | animation: showPopUp .3s ease-in-out forwards 402 | } 403 | 404 | /* ------------------------ TOOLTIPS ------------------------ */ 405 | .tooltip { 406 | transform-origin: center; 407 | letter-spacing: .14em; 408 | border-radius: 10px; 409 | line-height: 2em; 410 | text-align: center; 411 | background: hsla(180, 5%, 27%, .8); 412 | font-size: .8em; 413 | transform: scale(1, 0); 414 | position: absolute; 415 | opacity: 0; 416 | padding: 0 1em; 417 | width: auto; 418 | color: var(--whiteColor); 419 | } 420 | 421 | .tooltip::after { 422 | border-style: solid; 423 | border-width: 0 8px 11px 8px; 424 | border-color: transparent transparent hsla(180, 5%, 27%, .8) transparent; 425 | height: 0; 426 | margin: 0 auto; 427 | width: 0; 428 | right: 0; 429 | left: 0; 430 | top: -11px 431 | } 432 | 433 | .text-container:hover > .tooltip { 434 | animation: showToolTip .3s ease-in forwards 435 | } 436 | 437 | /* ------------------------ CANCIÓN SELECCIONADA ------------------------ */ 438 | .list-song-container { 439 | width: 100%; 440 | float: left; 441 | cursor: pointer 442 | } 443 | 444 | .list-song-container::after { 445 | border-radius: 25px; 446 | background: rgba(66, 73, 73, 0); 447 | height: 28px; 448 | margin: 0 auto; 449 | width: 0; 450 | right: 0; 451 | left: 0; 452 | top: 188px 453 | } 454 | 455 | /* ------------------------ ANIMACIONES ------------------------ */ 456 | @keyframes showToolTip { 457 | 100% { 458 | transform: scale(1); 459 | opacity: 1 460 | } 461 | } 462 | 463 | @keyframes showPopUp { 464 | 100% { 465 | transform: scale(1) 466 | } 467 | } 468 | 469 | @keyframes displayInputSearch { 470 | 100% { 471 | width: 75% 472 | } 473 | } 474 | 475 | @keyframes showInputUp { 476 | 100% { 477 | transform: scale(1) 478 | } 479 | } 480 | 481 | @keyframes clickControlsEffect { 482 | 0% { 483 | transform: scale3d(.3, .3, 1) 484 | } 485 | 25%, 50% { 486 | opacity: .8 487 | } 488 | 100% { 489 | transform: scale3d(1.2, 1.2, 1); 490 | opacity: 0 491 | } 492 | } 493 | 494 | @keyframes openArrowDown { 495 | 0% { 496 | transform: rotate(0) 497 | } 498 | 100% { 499 | transform: rotate(40deg); 500 | opacity: 1 501 | } 502 | } 503 | 504 | @keyframes openArrowUp { 505 | 0% { 506 | transform: rotate(0) 507 | } 508 | 100% { 509 | transform: rotate(-40deg); 510 | opacity: 1 511 | } 512 | } 513 | 514 | /* ------------------------ MEDIA QUERIES ------------------------ */ 515 | @media only screen and (max-width: 750px) { 516 | /* ------------------------ BUSQUEDA DE CANCIONES ------------------------ */ 517 | .no-searching-found { 518 | font-size: 2em 519 | } 520 | 521 | /* ------------------------ CONTROLES DEL REPRODUCTOR ------------------------ */ 522 | #total-progress-bar{ 523 | margin: .1em 1.3em .5em 524 | } 525 | 526 | .prev-next::after { 527 | left: 14.7% 528 | } 529 | 530 | /* ------------------------ TOP NAV ------------------------*/ 531 | #config-container { 532 | border-radius: 50%; 533 | background: var(--pinkColor); 534 | position: absolute; 535 | padding: .3em; 536 | bottom: 7px; 537 | z-index: 4; 538 | height: 2.6em; 539 | width: 2.6em; 540 | right: 16px 541 | } 542 | 543 | #config { 544 | padding: 0; 545 | margin: 0; 546 | height: 100%; 547 | width: 100%; 548 | float: left 549 | } 550 | 551 | #updown { 552 | right: 70px 553 | } 554 | /* ------------------------ DETALLES DE LA CANCIÓN ------------------------ */ 555 | #info-song { 556 | text-align: left; 557 | margin: .3em 1.2em; 558 | width: 93% 559 | } 560 | 561 | #info-song > div { 562 | overflow: hidden 563 | } 564 | 565 | #song-title > h1 { 566 | font-size: 1.16em 567 | } 568 | 569 | #artist > h2, 570 | #album > h3 { 571 | font-size: 1em 572 | } 573 | 574 | #time { 575 | font-size: 1em; 576 | margin: 0 577 | } 578 | 579 | /* ------------------------ LISTADO DE CANCIONES ------------------------ */ 580 | .list-song-container::after { 581 | top: 193px 582 | } 583 | } -------------------------------------------------------------------------------- /app/assets/css/unsemantic.css: -------------------------------------------------------------------------------- 1 | .clear{clear:both;display:block;overflow:hidden;visibility:hidden;width:0;height:0}.grid-container:before,.mobile-grid-5:before,.mobile-grid-10:before,.mobile-grid-15:before,.mobile-grid-20:before,.mobile-grid-25:before,.mobile-grid-30:before,.mobile-grid-35:before,.mobile-grid-40:before,.mobile-grid-45:before,.mobile-grid-50:before,.mobile-grid-55:before,.mobile-grid-60:before,.mobile-grid-65:before,.mobile-grid-70:before,.mobile-grid-75:before,.mobile-grid-80:before,.mobile-grid-85:before,.mobile-grid-90:before,.mobile-grid-95:before,.mobile-grid-100:before,.mobile-grid-33:before,.mobile-grid-66:before,.grid-5:before,.grid-10:before,.grid-15:before,.grid-20:before,.grid-25:before,.grid-30:before,.grid-35:before,.grid-40:before,.grid-45:before,.grid-50:before,.grid-55:before,.grid-60:before,.grid-65:before,.grid-70:before,.grid-75:before,.grid-80:before,.grid-85:before,.grid-90:before,.grid-95:before,.grid-100:before,.grid-33:before,.grid-66:before,.grid-offset:before,.clearfix:before,.grid-container:after,.mobile-grid-5:after,.mobile-grid-10:after,.mobile-grid-15:after,.mobile-grid-20:after,.mobile-grid-25:after,.mobile-grid-30:after,.mobile-grid-35:after,.mobile-grid-40:after,.mobile-grid-45:after,.mobile-grid-50:after,.mobile-grid-55:after,.mobile-grid-60:after,.mobile-grid-65:after,.mobile-grid-70:after,.mobile-grid-75:after,.mobile-grid-80:after,.mobile-grid-85:after,.mobile-grid-90:after,.mobile-grid-95:after,.mobile-grid-100:after,.mobile-grid-33:after,.mobile-grid-66:after,.grid-5:after,.grid-10:after,.grid-15:after,.grid-20:after,.grid-25:after,.grid-30:after,.grid-35:after,.grid-40:after,.grid-45:after,.grid-50:after,.grid-55:after,.grid-60:after,.grid-65:after,.grid-70:after,.grid-75:after,.grid-80:after,.grid-85:after,.grid-90:after,.grid-95:after,.grid-100:after,.grid-33:after,.grid-66:after,.grid-offset:after,.clearfix:after{content:".";display:block;overflow:hidden;visibility:hidden;font-size:0;line-height:0;width:0;height:0}.grid-container:after,.mobile-grid-5:after,.mobile-grid-10:after,.mobile-grid-15:after,.mobile-grid-20:after,.mobile-grid-25:after,.mobile-grid-30:after,.mobile-grid-35:after,.mobile-grid-40:after,.mobile-grid-45:after,.mobile-grid-50:after,.mobile-grid-55:after,.mobile-grid-60:after,.mobile-grid-65:after,.mobile-grid-70:after,.mobile-grid-75:after,.mobile-grid-80:after,.mobile-grid-85:after,.mobile-grid-90:after,.mobile-grid-95:after,.mobile-grid-100:after,.mobile-grid-33:after,.mobile-grid-66:after,.grid-5:after,.grid-10:after,.grid-15:after,.grid-20:after,.grid-25:after,.grid-30:after,.grid-35:after,.grid-40:after,.grid-45:after,.grid-50:after,.grid-55:after,.grid-60:after,.grid-65:after,.grid-70:after,.grid-75:after,.grid-80:after,.grid-85:after,.grid-90:after,.grid-95:after,.grid-100:after,.grid-33:after,.grid-66:after,.grid-offset:after,.clearfix:after{clear:both}.grid-container{margin-left:auto;margin-right:auto;max-width:100%;height:100%}.mobile-grid-5,.mobile-grid-10,.mobile-grid-15,.mobile-grid-20,.mobile-grid-25,.mobile-grid-30,.mobile-grid-35,.mobile-grid-40,.mobile-grid-45,.mobile-grid-50,.mobile-grid-55,.mobile-grid-60,.mobile-grid-65,.mobile-grid-70,.mobile-grid-75,.mobile-grid-80,.mobile-grid-85,.mobile-grid-90,.mobile-grid-95,.mobile-grid-100,.mobile-grid-33,.mobile-grid-66,.grid-5,.grid-10,.grid-15,.grid-20,.grid-25,.grid-30,.grid-35,.grid-40,.grid-45,.grid-50,.grid-55,.grid-60,.grid-65,.grid-70,.grid-75,.grid-80,.grid-85,.grid-90,.grid-95,.grid-100,.grid-33,.grid-66{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.grid-parent{padding-left:0;padding-right:0}.grid-offset{clear:both;margin-left:-10px;margin-right:-10px}@media (max-width: 767px){.hide-on-mobile{display:none!important}.mobile-grid-5{float:left;width:5%}.mobile-grid-10{float:left;width:10%}.mobile-grid-15{float:left;width:15%}.mobile-grid-20{float:left;width:20%}.mobile-grid-25{float:left;width:25%}.mobile-grid-30{float:left;width:30%}.mobile-grid-35{float:left;width:35%}.mobile-grid-40{float:left;width:40%}.mobile-grid-45{float:left;width:45%}.mobile-grid-50{float:left;width:50%}.mobile-grid-55{float:left;width:55%}.mobile-grid-60{float:left;width:60%}.mobile-grid-65{float:left;width:65%}.mobile-grid-70{float:left;width:70%}.mobile-grid-75{float:left;width:75%}.mobile-grid-80{float:left;width:80%}.mobile-grid-85{float:left;width:85%}.mobile-grid-90{float:left;width:90%}.mobile-grid-95{float:left;width:95%}.mobile-grid-33{float:left;width:33.33333%}.mobile-grid-66{float:left;width:66.66667%}.mobile-grid-100{clear:both;width:100%}}@media (min-width: 768px){.hide-on-desktop{display:none!important}.grid-5{float:left;width:5%}.grid-10{float:left;width:10%}.grid-15{float:left;width:15%}.grid-20{float:left;width:20%}.grid-25{float:left;width:25%}.grid-30{float:left;width:30%}.grid-35{float:left;width:35%}.grid-40{float:left;width:40%}.grid-45{float:left;width:45%}.grid-50{float:left;width:50%}.grid-55{float:left;width:55%}.grid-60{float:left;width:60%}.grid-65{float:left;width:65%}.grid-70{float:left;width:70%}.grid-75{float:left;width:75%}.grid-80{float:left;width:80%}.grid-85{float:left;width:85%}.grid-90{float:left;width:90%}.grid-95{float:left;width:95%}.grid-33{float:left;width:33.33333%}.grid-66{float:left;width:66.66667%}.grid-100{clear:both;width:100%}} -------------------------------------------------------------------------------- /app/assets/files/Hind-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/files/Hind-Regular.ttf -------------------------------------------------------------------------------- /app/assets/img/config.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/img/equalizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/assets/img/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/icon.png -------------------------------------------------------------------------------- /app/assets/img/icon@1.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/icon@1.25x.png -------------------------------------------------------------------------------- /app/assets/img/icon@1.33x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/icon@1.33x.png -------------------------------------------------------------------------------- /app/assets/img/icon@1.4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/icon@1.4x.png -------------------------------------------------------------------------------- /app/assets/img/icon@1.8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/icon@1.8x.png -------------------------------------------------------------------------------- /app/assets/img/lang.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | A 31 | 32 | -------------------------------------------------------------------------------- /app/assets/img/legal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/assets/img/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/assets/img/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/play.png -------------------------------------------------------------------------------- /app/assets/img/play.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /app/assets/img/prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 13 | 14 | 21 | 24 | 25 | 32 | 35 | 36 | 43 | 46 | 47 | 50 | 55 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/assets/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/img/soube-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 28 | 33 | 39 | 44 | 49 | 55 | 56 | 60 | 65 | 71 | 76 | 81 | 87 | 88 | 89 | 107 | 109 | 110 | 112 | image/svg+xml 113 | 115 | 116 | 117 | 118 | 119 | 124 | 133 | s 146 | 150 | 156 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /app/assets/img/thumb-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/thumb-next.png -------------------------------------------------------------------------------- /app/assets/img/thumb-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/thumb-pause.png -------------------------------------------------------------------------------- /app/assets/img/thumb-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/thumb-play.png -------------------------------------------------------------------------------- /app/assets/img/thumb-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/app/assets/img/thumb-prev.png -------------------------------------------------------------------------------- /app/assets/js/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /* --------------------------------- Modules --------------------------------- */ 6 | //---- nodejs ---- 7 | const fs = require('fs'); 8 | 9 | //---- electron ---- 10 | const { 11 | remote, 12 | net 13 | } = require('electron'); 14 | 15 | //---- own ---- 16 | const version = require('./../version'); 17 | 18 | /* --------------------------------- Functions --------------------------------- */ 19 | // Will create all the files needed by the music player. 20 | // Some old files (old soubes versions) will be overwritens. 21 | // This function will checks for two files: 22 | // - config.json 23 | // - listSong.json 24 | function createFiles(app) { 25 | /* --------------------------------- Configuration --------------------------------- */ 26 | //---- constants ---- 27 | const PATH = app.getPath('userData'); 28 | const CONFIG_PATH = `${PATH}/config.json`; 29 | const LIST_SONG_PATH = `${PATH}/listSong.json`; 30 | 31 | if (!fs.existsSync(CONFIG_PATH)) { 32 | // Values by default 33 | const CONFIG = { 34 | lang: 'us', 35 | shuffle: true, 36 | musicFolder: '', 37 | equalizer: { 38 | reset: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 39 | rock: [80, 103, 105, 121, 145, 128, 125, 123, 122, 143, 163, 134, 135, 129, 139, 146, 144, 153, 152, 149, 124, 102, 103], 40 | electro: [99, 133, 102, 122, 100, 139, 125, 151, 158, 152, 124, 116, 116, 117, 147, 100, 139, 173, 112, 135, 165, 85, 121], 41 | acustic: [104, 124, 141, 0, 0, 104, 0, 104, 117, 0, 0, 0, 107, 104, 109, 123, 92, 107, 0, 154, 113, 84, 90] 42 | }, 43 | equalizerConfig: 'reset' 44 | }; 45 | 46 | fs.openSync(CONFIG_PATH, 'w'); 47 | fs.writeFileSync(CONFIG_PATH, JSON.stringify(CONFIG, null), { flag: 'w' }); 48 | } 49 | // else { 50 | // // ONLY TO UPDATE THE CONFIG FILE 51 | // var actualVersion = app.getVersion().toString(); 52 | // version(net, actualVersion, response => { 53 | // if (response === 'major' && actualVersion === '1.3.2') { 54 | // let config = JSON.parse(fs.readFileSync(CONFIG_PATH).toString()); 55 | // config.equalizer = { 56 | // reset: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 57 | // rock: [80, 103, 105, 121, 145, 128, 125, 123, 122, 143, 163, 134, 135, 129, 139, 146, 144, 153, 152, 149, 124, 102, 103], 58 | // electro: [99, 133, 102, 122, 100, 139, 125, 151, 158, 152, 124, 116, 116, 117, 147, 100, 139, 173, 112, 135, 165, 85, 121], 59 | // acustic: [104, 124, 141, 0, 0, 104, 0, 104, 117, 0, 0, 0, 107, 104, 109, 123, 92, 107, 0, 154, 113, 84, 90] 60 | // }; 61 | // config.equalizerConfig = 'reset'; 62 | // fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null), { flag: 'w' }); 63 | // } 64 | // }); 65 | // } 66 | 67 | /* --------------------------------- File of songs --------------------------------- */ 68 | if (!fs.existsSync(LIST_SONG_PATH)) { 69 | fs.openSync(LIST_SONG_PATH, 'w'); 70 | fs.writeFileSync(LIST_SONG_PATH, JSON.stringify({}, null), { flag: 'w' }); 71 | } 72 | } 73 | 74 | // Will save the files config.json and listSong.json if needed. 75 | function editFile(fileName, data) { 76 | //---- constants ---- 77 | fs.writeFile(`${remote.app.getPath('userData')}/${fileName}.json`, JSON.stringify(data, null), err => { }); 78 | } 79 | 80 | // Will get all the config files. 81 | // config.json [.confg] path 82 | // lang.json [local project] path 83 | // listSong.json [.config] path 84 | function init() { 85 | return { 86 | editFile, 87 | configFile: require(`${remote.app.getPath('userData')}/config.json`), 88 | listSongs: require(`${remote.app.getPath('userData')}/listSong.json`), 89 | langFile: require('./lang.json') 90 | } 91 | } 92 | 93 | module.exports = Object.freeze({ 94 | createFiles, 95 | init 96 | }); -------------------------------------------------------------------------------- /app/assets/js/config/lang.json: -------------------------------------------------------------------------------- 1 | { 2 | "es": { 3 | "title": "Desconocida", 4 | "artist": "Desconocido", 5 | "album": "Desconocido", 6 | "config": { 7 | "addSongFolder": "Carpeta música", 8 | "changeLanguage": "Cambiar idioma", 9 | "statusLanguage": "Español", 10 | "titleConfig": "Configuración", 11 | "addAccounts": "Cuentas", 12 | "equalizerSetting": "Ecualizador", 13 | "statusSongFolder": "No hay carpeta de música", 14 | "loadingSongFolder": "Cargando canciones: %d / %d
Paciencia, se están extrayendo los metadatos :)", 15 | "legal": "Términos legales", 16 | "newEQ": "Nuevo" 17 | }, 18 | "alerts": { 19 | "newVersion": "Tenemos una nueva versión!. Click para ir descargarlo.", 20 | "newSongsFound": "Agregando nuevas canciones", 21 | "welcome": "No hay una carpeta de música agregada.
Por favor, agrega tu carpeta de música dando click en esta ventana blanca o en el icono de configuración", 22 | "error_001": "Falta un archivo necesario para que funcione el reproductor. Ir a: https:\/\/github.com\/DracotMolver\/Soube y reportar el error. Gracias", 23 | "error_002": "No hay archivos para reproducir. Por favor, ir al icono de configuración y agregar tu carpeta de música", 24 | "error_003": "Se ha producido un error al intentar leer la carpeta: " 25 | }, 26 | "searchBy": [ 27 | "Canción", 28 | "Artista", 29 | "Álbum" 30 | ] 31 | }, 32 | "us": { 33 | "title": "Unknown", 34 | "artist": "Unknown", 35 | "album": "Unknown", 36 | "config": { 37 | "addSongFolder": "Music folder", 38 | "changeLanguage": "Change language", 39 | "statusLanguage": "English", 40 | "titleConfig": "Configuration", 41 | "addAccounts": "Accounts", 42 | "equalizerSetting": "Equalizer", 43 | "statusSongFolder": "No music folder", 44 | "loadingSongFolder": "Loading songs: %d1 / %d2
Patience, extracting metadata", 45 | "legal": "Legal terms", 46 | "newEQ": "New" 47 | }, 48 | "alerts": { 49 | "newVersion": "We've got a new version!. Click to download it.", 50 | "newSongsFound": "Adding new songs ", 51 | "welcome": "There is no music folder added.
Please, add your music folder doing click on this white frame or in the config icon", 52 | "error_001": "Missed a file needed to use the music player. Go to: https:\/\/github.com\/DracotMolver\/Soube and report the error. Thanks.", 53 | "error_002": "There are no files to play. Please, click on the config icon to add your music folder.", 54 | "error_003": "An error occured trying to read the folder: " 55 | }, 56 | "searchBy": [ 57 | "Song", 58 | "Artist", 59 | "Album" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/assets/js/configMain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /* --------------------------------- Modules --------------------------------- */ 6 | //---- electron ---- 7 | const { 8 | shell, 9 | ipcRenderer, 10 | remote 11 | } = require('electron'); 12 | 13 | //---- own ---- 14 | const factory = require('./factory'); 15 | const player = factory('player'); 16 | const EQ = factory('equilizer'); 17 | const { 18 | configFile, 19 | langFile, 20 | editFile 21 | } = require('./config').init(); 22 | require('./dom'); 23 | 24 | /* --------------------------------- Variables --------------------------------- */ 25 | let lang = langFile[configFile.lang]; 26 | let eqHrz = []; 27 | let newEQHrz = []; 28 | let actualPanel = null; 29 | 30 | /* --------------------------------- Functions --------------------------------- */ 31 | // Change the text in the config window 32 | (function updateTextContet() { 33 | $('#_addsongfolder').text(lang.config.addSongFolder); 34 | $('#_statussongfolder').text(configFile.musicFolder === '' ? lang.config.statusSongFolder : configFile.musicFolder); 35 | $('#_changelanguage').text(lang.config.changeLanguage); 36 | $('#_statuslanguage').text(lang.config.statusLanguage); 37 | $('#_titleconfig').text(lang.config.titleConfig); 38 | $('#_equalizersetting').text(lang.config.equalizerSetting); 39 | $('#_legal').text(lang.config.legal); 40 | $('#_neweq').text(lang.config.newEQ); 41 | })(); 42 | 43 | // Animation of the panel when select an option 44 | function animConfigPanel(e, text) { 45 | actualPanel = $(`#${$(e).data('action')}`).removeClass('hide'); 46 | 47 | $('#config-container-options') 48 | .addClass('config-opt-anim') 49 | .on({ 50 | 'animationend': function () { 51 | $('#config-container-values').removeClass('hide'); 52 | $(this).addClass('hide'); 53 | } 54 | }); 55 | 56 | $('#_titlesubconfig').text(` > ${text}`); 57 | } 58 | 59 | // Change the lang of the music player 60 | function onClickChangeLang() { 61 | animConfigPanel(this, lang.config.changeLanguage); 62 | 63 | $('.lang-option').on({ 64 | 'click': function () { 65 | configFile.lang = $(this).data('lang'); 66 | editFile('config', configFile); 67 | remote.getCurrentWindow().reload(); 68 | } 69 | }); 70 | } 71 | 72 | // Get the path song 73 | function saveSongList(parentFolder = '') { 74 | configFile.musicFolder = parentFolder; 75 | editFile('listSong', {}); 76 | editFile('config', configFile); 77 | 78 | $('#folder-status').child(0).text(parentFolder); 79 | 80 | // Show a loading 81 | // Read the content of the parent folder 82 | player.addSongFolder(parentFolder, () => { 83 | $('#loading').removeClass('hide'); 84 | $($('.grid-container').get(0)) 85 | .css('-webkit-filter:blur(2px)'); 86 | }, (i, maxLength) => { // Iterator function 87 | $('#_loading-info').text(`${lang.config.loadingSongFolder.replace('%d1', i).replace('%d2', maxLength)}`); 88 | 89 | if (i === maxLength) { 90 | // Ocultar loading 91 | $('#loading').addClass('hide'); 92 | $($('.grid-container').get(0)).rmAttr('style'); 93 | remote.BrowserWindow.getAllWindows()[0].reload() 94 | // ipcRenderer.send('display-list'); 95 | // remote.webContents.getAllWebContents 96 | } 97 | }); 98 | } 99 | 100 | // Animation over the buttons in the EQ panel 101 | function onEqualizerPanel(e) { 102 | animConfigPanel(this, lang.config.equalizerSetting); 103 | 104 | EQ.onDragMove(data => { 105 | ipcRenderer.send('equalizer-filter', data); 106 | }); 107 | 108 | EQ.onDragEnd((pos, db) => { 109 | newEQHrz[pos] = db; 110 | }); 111 | 112 | // Set the EQ config choosen 113 | newEQHrz = eqHrz = configFile.equalizer[configFile.equalizerConfig]; 114 | $('.range-circle').each((v, i) => { 115 | $(v).css(`top:${eqHrz[i] === 0 ? 130 : eqHrz[i]}px`); 116 | }).on({ 117 | mousedown: function () { 118 | EQ.onDragStart(this); 119 | } 120 | }); 121 | } 122 | 123 | // Options to config the EQ 124 | function setEQ () { 125 | configFile.equalizerConfig = this.value; 126 | editFile('config', configFile); 127 | 128 | eqHrz = configFile.equalizer[configFile.equalizerConfig]; 129 | $('.range-circle').each((v, i) => { 130 | $(v).css(`top:${eqHrz[i] === 0 ? 130 : eqHrz[i]}px`); 131 | 132 | ipcRenderer.send('equalizer-filter', [i, 133 | eqHrz[i] !== 0 ? parseFloat((eqHrz[i] < 130 ? 121 - eqHrz[i] : -eqHrz[i] + 140) / 10) : 0 134 | ]); 135 | }); 136 | } 137 | 138 | /** --------------------------------------- Events --------------------------------------- **/ 139 | // Refresh the window 140 | $('#_titleconfig').on({ 141 | click: () => { 142 | if (actualPanel !== null) { 143 | actualPanel.addClass('hide'); 144 | 145 | $('#config-container-options') 146 | .removeClass('config-opt-anim') 147 | .removeClass('hide'); 148 | 149 | $('#config-container-values').addClass('hide'); 150 | $('#_titlesubconfig').text(''); 151 | } 152 | } 153 | }); 154 | 155 | // Change the language 156 | $('#change-lang').on({ click: onClickChangeLang }); 157 | 158 | // Action to add the songs 159 | $('#add-songs').on({ 160 | click: () => { 161 | remote.dialog.showOpenDialog({ 162 | title: 'Add music folder', 163 | properties: ['openDirectory'] 164 | }, parentFolder => { 165 | if (parentFolder !== undefined) saveSongList(parentFolder[0]); 166 | }); 167 | } 168 | }); 169 | 170 | // Show the EQ panel 171 | $('#equalizer-panel').on({ click: onEqualizerPanel }); 172 | 173 | // EQ settings options 174 | Object.keys(configFile.equalizer).forEach(v => { 175 | $('#eq-buttons').insert( 176 | $('option').clone(true).val(v).text(v) 177 | .attr(configFile.equalizerConfig === v.toLowerCase() ? { selected:'selected' } : '') 178 | ); 179 | }); 180 | $('#eq-buttons').on({ change: setEQ }); 181 | 182 | // Check if the person want to add a new EQ setting 183 | $('#name-new-eq').on({ 184 | keyup: function() { 185 | if (this.value.trim().length > 3) $('#_neweq').rmAttr('disabled'); 186 | else $('#_neweq').attr({ disabled: 'disabled' }); 187 | } 188 | }); 189 | 190 | // Action to add a new EQ setting 191 | $('#_neweq').on({ 192 | click: function(e) { 193 | e.preventDefault(); 194 | const eqStylesNames = Object.keys(configFile.equalizer); 195 | 196 | if(eqStylesNames.indexOf($('#name-new-eq').val()) === -1) { 197 | configFile.equalizer[$('#name-new-eq').val()] = newEQHrz; 198 | editFile('config', configFile); 199 | } else { 200 | console.log('El nombres ya exíste o no es un nombre válido'); 201 | } 202 | } 203 | }); 204 | 205 | // Open the default browser of the OS 206 | $(':a').on({ 207 | click: function (e) { 208 | e.preventDefault(); 209 | shell.openExternal(this.href); 210 | } 211 | }); 212 | 213 | // Show legal terms 214 | $('#terms').on({ 215 | click: function() { 216 | animConfigPanel(this, lang.config.legal); 217 | } 218 | }); -------------------------------------------------------------------------------- /app/assets/js/dom/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | module.exports = (_ => { 6 | /* --------------------------------- Variables --------------------------------- */ 7 | //---- normals ---- 8 | var createdElements = {}; 9 | var poolOfElements = {}; 10 | var event = {}; 11 | 12 | /* --------------------------------- Events --------------------------------- */ 13 | const EVENT = { 14 | element: null, 15 | addClass: function(str) { 16 | const CLASS_NAME = this.element.className; 17 | 18 | if (CLASS_NAME.indexOf(str) === -1) 19 | this.element.className += CLASS_NAME === '' ? `${str}` : ` ${str}`; 20 | 21 | return this; 22 | }, 23 | empty: function() { 24 | let el = this.element; 25 | while(el.firstChild) el.removeChild(el.firstChild); 26 | 27 | return this; 28 | }, 29 | text: function(str = null) { 30 | if (str === null) return this.element.textContent; 31 | 32 | return this.element.innerHTML = `${str}`, this; 33 | }, 34 | removeClass: function(_class) { 35 | return this.element.className = this.element.className.replace(_class, ''), this; 36 | }, 37 | child: function(pos = -1) { 38 | return this.element = pos !== -1 ? this.element.children[pos] : Array.from(this.element.children), 39 | this; 40 | }, 41 | on: function(fn) { 42 | // Select element is like an array because of the options elements inside 43 | if (this.element.length !== undefined && this.element.nodeName !== 'SELECT') { 44 | this.element.forEach(e => onFunction(e, fn)); 45 | } else { 46 | onFunction(this.element, fn); 47 | } 48 | 49 | return this; 50 | }, 51 | data: function(data = null) { 52 | if (typeof data === 'string') { 53 | let d = this.element.dataset[data]; 54 | if (/^\d+$/.test(d)) return parseInt(d); 55 | else if (/^\d+(\.+)\d+$/.test(d)) return parseFloat(d); 56 | else if (/^(\w|\s)+$/.test(d)) return d.toString(); 57 | } else { 58 | Object.keys(data).forEach(v => { 59 | this.element.dataset[v] = data[v]; 60 | }); 61 | } 62 | 63 | return this; 64 | }, 65 | each: function(fn) { 66 | this.element.forEach((v, i) => { 67 | fn.length === 1 ? fn(v) : fn(v, i); 68 | }); 69 | 70 | return this; 71 | }, 72 | css: function(str) { 73 | let el = this.element; 74 | 75 | if (el.length !== undefined) { 76 | el.forEach(e => { 77 | if (e.style.cssText.indexOf(str)) 78 | e.style.cssText += e.style.cssText === '' ? `${str};` : ` ${str};`; 79 | }); 80 | } else { 81 | if (el.style.cssText.indexOf(str)) 82 | el.style.cssText += el.style.cssText === '' ? `${str};` : ` ${str};`; 83 | } 84 | 85 | return this; 86 | }, 87 | clone: function(isCloned) { 88 | if (typeof this.element === 'string') { 89 | return this.element = getCreatedElement(this.element).cloneNode(isCloned), this; 90 | } else { 91 | return event = Object.assign({}, EVENT), 92 | event.element = this.element.cloneNode(isCloned), event; 93 | } 94 | }, 95 | get: function(pos = -1) { 96 | return pos === -1 ? this.element : this.element[pos]; 97 | }, 98 | rmAttr: function(attr) { 99 | return this.element.removeAttribute(attr), this; 100 | }, 101 | attr: function (attr) { 102 | if (typeof attr === 'object') 103 | Object.keys(attr).forEach(v => { this.element.setAttribute(v, attr[v]); }); 104 | else if (attr !== '') 105 | return this.element.getAttribute(attr); 106 | 107 | return this; 108 | }, 109 | insert: function (...a) { 110 | a.forEach(v => { this.element.appendChild('element' in v ? v.element : v); }); 111 | return this; 112 | }, 113 | val: function (v = null) { 114 | return v === null ? this.element.value : this.element.value = v, this; 115 | }, 116 | has: function (s) { 117 | return this.element.className.indexOf(s) !== -1; 118 | } 119 | }; 120 | 121 | /* --------------------------------- Functions --------------------------------- */ 122 | function onFunction (el, fn) { 123 | Object.keys(fn).forEach(v => { 124 | /animation/.test(v) ? 125 | el.addEventListener(v.toLowerCase(), fn[v]) : 126 | el[`on${v.toLowerCase()}`] = fn[v]; 127 | }) 128 | } 129 | 130 | function saveCreatedElement(name) { 131 | if (!createdElements[name]) createdElements[name] = document.createElement(name); 132 | } 133 | 134 | function getCreatedElement(name) { 135 | return createdElements[name]; 136 | } 137 | 138 | function saveElementInPool(name, element) { 139 | poolOfElements[name] = element; 140 | } 141 | 142 | function inPool(name) { 143 | return poolOfElements[name] === EVENT.element; 144 | } 145 | 146 | function getElementInPool(name) { 147 | return poolOfElements[name]; 148 | } 149 | 150 | /* --------------------------------- Main Function --------------------------------- */ 151 | // Get an string to search for an element into the DOM and its return an Object 152 | // with all the needed functions 153 | const DOM = e => { 154 | event = Object.assign({}, EVENT); 155 | 156 | if (inPool(e)) { 157 | event.element = getElementInPool(e); 158 | } else { 159 | if ((r = /^(\.|#|:)/.exec(e))) { 160 | switch(r[0]) { 161 | case '.': e = Array.from(document.getElementsByClassName(e.slice(1, e.length))); break; 162 | case '#': e = document.getElementById(e.slice(1, e.length)); break; 163 | case ':': e = Array.from(document.getElementsByTagName(e.slice(1, e.length))); break; 164 | } 165 | 166 | saveElementInPool(e); 167 | } else if (typeof e === 'string') { 168 | saveCreatedElement(e); 169 | } 170 | } 171 | 172 | return event.element = e, event; 173 | } 174 | 175 | _.$ = DOM; 176 | })(global); -------------------------------------------------------------------------------- /app/assets/js/factory/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | function factory(nameClass) { 6 | switch (nameClass.toLowerCase()) { 7 | case 'equilizer': return require('./../player/equalizer'); 8 | case 'player': return require('./../player'); 9 | } 10 | } 11 | 12 | module.exports = factory; -------------------------------------------------------------------------------- /app/assets/js/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /** --------------------------------------- Modules --------------------------------------- **/ 6 | //---- Electron ---- 7 | const { 8 | ipcRenderer, 9 | remote 10 | } = require('electron'); 11 | 12 | //---- own ---- 13 | const PLAYER = require('./factory')('player'); 14 | const version = require('./version'); 15 | const config = require('./config'); 16 | const { 17 | configFile, 18 | langFile, 19 | listSongs 20 | } = config.init(); 21 | require('./dom'); 22 | 23 | /** --------------------------------------- Variables --------------------------------------- **/ 24 | //---- constants ---- 25 | const TIME_SCROLLING = 3.2; // Pixels per frame 26 | const LAPSE_POPUP = 4500; // Duration of info popups 27 | const LAPSE_SCROLLING = 60; // Lapse before do scrolling 28 | const MAX_ELEMENTS = 20; // Max of elementos to display when is filtering a song [searching bar] 29 | const BTN_FILTER_SONGS = [ // Elements to use as a items into the slide 30 | $('div').clone(false).addClass('grid-25 mobile-grid-25'), 31 | $('div').clone(false).addClass('search-results'), 32 | $('div').clone(false).addClass('results') 33 | ]; 34 | 35 | //---- normals ---- 36 | let lang = langFile[configFile.lang]; 37 | let clickedElement = null; // When you do click on the name of the song 38 | let positionElement = null; // Where is the song that you clicked on. 39 | let isSearchDisplayed = false; // Checks if it was launched the searching bar 40 | let totalResults = 0; // Amount of songs filtered 41 | let searchValue = ''; // The input text to search for 42 | let tempSlide = 0; // To create the pagination 43 | let countSlide = 0; 44 | let searchBy = 'title'; 45 | let fragmentSlide = null; // DocumentFragment() slide container 46 | let fragmentItems = null; // DocumentFragment() button container 47 | let slide = 0; // Amount of slides to make 48 | let regex = null; // The name of the song to searching for as a regular expression 49 | let list = []; // Filtered songs. 50 | let interval = 0; 51 | 52 | /** --------------------------------------- Functions --------------------------------------- **/ 53 | // Check if there's a new version to download 54 | function getActualVersion() { 55 | version(remote.net, remote.app.getVersion(), response => { 56 | if (response === 'major') { 57 | $('#pop-up-container') 58 | .removeClass('hide') 59 | .child(0) 60 | .addClass('pop-up-anim') 61 | .text( 62 | `${lang.alerts.newVersion}` 63 | ); 64 | 65 | $(':a').on({ 66 | click: function (e) { 67 | e.preventDefault(); 68 | shell.openExternal(this.href); 69 | } 70 | }); 71 | 72 | let tout = setTimeout(() => { 73 | $('#pop-up-container') 74 | .addClass('hide') 75 | .child(0) 76 | .removeClass('pop-up-anim'); 77 | 78 | clearTimeout(tout); 79 | }, LAPSE_POPUP); 80 | } 81 | }); 82 | } 83 | 84 | // Main function!! 85 | function loadSongs() { 86 | // Enable shuffle 87 | if (configFile.shuffle) $('#shuffle-icon').css('fill:#FBFCFC'); 88 | 89 | getActualVersion(); 90 | if (Object.keys(listSongs).length === 0) { 91 | $('#list-songs').text( 92 | `
${lang.alerts.welcome}
` 93 | ).on({ 94 | click: () => { 95 | ipcRenderer.send('config-panel'); 96 | } 97 | }); 98 | } else { 99 | // Render the list of songs 100 | PLAYER.createView(PLAYER); 101 | checkNewSongs(); 102 | } 103 | } 104 | loadSongs(); 105 | 106 | function hideSearchInputData() { 107 | $('#search-result').empty(); 108 | $('#search-container').addClass('hide'); 109 | $('#search-wrapper').removeClass('search-wrapper-anim'); 110 | $($('.grid-container').get(0)).rmAttr('style'); 111 | $('#search').removeClass('input-search-anim'); 112 | isSearchDisplayed = false; 113 | } 114 | 115 | // This function is use into the function itemSlide 116 | function selectedSong (position) { 117 | PLAYER.controls.playSongAtPosition(position); 118 | hideSearchInputData(); 119 | } 120 | 121 | // will be executed every time the user hit down a keyword 122 | // So, I carefully tried to do a clean, cheaper and faster code :). 123 | function searchInputData(e) { 124 | // Clean if there's no coincidence 125 | $('#wrapper-results').removeClass('no-searching-found').empty(); 126 | $('#pagination').addClass('hide'); 127 | 128 | searchValue = this.value.trim(); 129 | 130 | if (searchValue !== '') { 131 | countSlide = 0; 132 | // Complete the text 133 | if (e.key === 'ArrowRight' && searchValue.length > 1) 134 | this.value = $('#search-result').text(); 135 | 136 | regex = new RegExp(`${searchValue.replace(/\s+/g, ' ')}`, 'ig'); 137 | list = listSongs.filter(v => regex.test(v[searchBy])); 138 | 139 | if (e.key === 'Enter') selectedSong(list[list.length - 1].position); 140 | 141 | // Show possibles results 142 | totalResults = list.length - 1; 143 | tempSlide = slide = totalResults > MAX_ELEMENTS ? Math.floor(totalResults / MAX_ELEMENTS) : 1; 144 | fragmentSlide = fragmentItems = document.createDocumentFragment(); 145 | 146 | // Make an slide with all the filtered coincidences 147 | const FILTERED_SONGS = totalResults < MAX_ELEMENTS ? totalResults + 1 : MAX_ELEMENTS; 148 | if (list.length > 0) { 149 | while (slide--) { 150 | for (var i = 0; i < FILTERED_SONGS; i++ , totalResults--) { 151 | fragmentItems.appendChild( 152 | BTN_FILTER_SONGS[0].clone(true) 153 | .insert( 154 | BTN_FILTER_SONGS[1].clone(true) 155 | .text(list[totalResults][searchBy]) 156 | ) 157 | .data({ position: list[totalResults].position }) 158 | .on({ 159 | click: function() { 160 | selectedSong($(this).data('position')); 161 | } 162 | }).get() 163 | ); 164 | } 165 | 166 | // All the buttons into the slides 167 | fragmentSlide.appendChild( 168 | BTN_FILTER_SONGS[2].clone(true) 169 | .insert(fragmentItems) 170 | .css(`width:${document.body.clientWidth}px`).get() 171 | ); 172 | fragmentItems = document.createDocumentFragment(); 173 | } 174 | 175 | // Add the pagination if there's more than one slide 176 | tempSlide > 1 ? 177 | $('#pagination').removeClass('hide').child(1).addClass('arrow-open-anim') : 178 | $('#pagination').addClass('hide'); 179 | 180 | // Display all the filtered songs 181 | $('#wrapper-results').empty() 182 | .insert(fragmentSlide) 183 | .css(`width:${tempSlide * document.body.clientWidth}px`); 184 | 185 | } else { 186 | // Clean if there's no coincidence 187 | $('#wrapper-results') 188 | .text(':( It seems that you don\'t have what you\'re searching for.') 189 | .addClass('no-searching-found'); 190 | $('#pagination').addClass('hide'); 191 | } 192 | } 193 | 194 | // Show the first coincidence to show as a "ghost text". 195 | $('#search-result').text(list.length > 0 ? list[list.length - 1][searchBy] : ''); 196 | } 197 | 198 | // Check if there are new songs to be added 199 | function checkNewSongs() { 200 | PLAYER.addSongFolder(configFile.musicFolder, () => { 201 | // show pop-up 202 | $('#pop-up-container').removeClass('hide').child(0).addClass('pop-up-anim'); 203 | }, (i, maxlength) => { 204 | $('#pop-up').text(`${langFile[configFile.lang].alerts.newSongsFound}${i} / ${maxlength}`); 205 | 206 | if (i === maxlength) { 207 | // hide pop-up 208 | $('#pop-up-container').addClass('hide').child(0).removeClass('pop-up-anim'); 209 | remote.getCurrentWindow().reload(); 210 | } 211 | }); 212 | } 213 | 214 | function btnActions(action) { 215 | switch (action) { 216 | case 'play-pause': 217 | if (PLAYER.controls.playSong() === 'resume') { 218 | if (process.platform) ipcRenderer.send('thumb-bar-update', 'pauseMomment'); 219 | } else { 220 | if (process.platform) ipcRenderer.send('thumb-bar-update', 'playMomment'); 221 | } 222 | break; 223 | case 'next': PLAYER.controls.nextSong(); break; 224 | case 'prev': PLAYER.controls.prevSong(); break; 225 | case 'shuffle': PLAYER.controls.shuffle() ;break; 226 | } 227 | } 228 | 229 | function clickBtnControls() { 230 | $(this).addClass('click-controls') 231 | .on({ 232 | animationend: function () { 233 | $(this).removeClass('click-controls'); 234 | } 235 | }); 236 | 237 | if (listSongs.length !== 0) { 238 | btnActions(this.id); 239 | } else { 240 | dialog.showMessageBox({ 241 | type: 'info', 242 | buttons: ['Ok'], 243 | message: lang.alerts.error_002 244 | }); 245 | } 246 | } 247 | 248 | function scrollAnimation(direction) { 249 | const ANIMATION = () => { 250 | if (direction === 'up') 251 | $('#list-songs').get().scrollTop -= TIME_SCROLLING; 252 | else 253 | $('#list-songs').get().scrollTop += TIME_SCROLLING; 254 | 255 | interval = requestAnimationFrame(ANIMATION); 256 | }; 257 | interval = requestAnimationFrame(ANIMATION); 258 | } 259 | 260 | /** --------------------------------------- Events --------------------------------------- **/ 261 | // Scrolling the list of songs 262 | $('#song-title').on({ 263 | click: function () { 264 | if (this.children[0].textContent.trim() !== '') { 265 | clickedElement = $('#list-songs').get(); 266 | positionElement = $(`#${$(this).data('position')}`).get(); 267 | const ELEMENT = clickedElement.scrollTop; 268 | const TOP = positionElement.offsetTop; 269 | const TOPNAV = Math.round($('#top-nav').get().offsetHeight); 270 | const DISTANCE = TOP - (TOPNAV + 100); 271 | clickedElement.scrollTop += ELEMENT !== DISTANCE ? DISTANCE - ELEMENT : - (DISTANCE - ELEMENT); 272 | } 273 | } 274 | }); 275 | 276 | // Scrolling the list of songs like it was the barscroll 277 | // of the browser 278 | $('.arrow-updown').on({ 279 | mousedown: function () { 280 | scrollAnimation($(this).data('direction')); 281 | }, 282 | mouseup: () => { 283 | cancelAnimationFrame(interval); 284 | } 285 | }); 286 | 287 | // Choose an option to search by: (new feature) 288 | // - Song 289 | // - Artist 290 | // - Album 291 | // $('#searchBy').on({ change: function () { searchBy = this.value; } }); 292 | 293 | // Open the window configuration 294 | $('#config').on({ click: () => { ipcRenderer.send('show-config'); } }); 295 | 296 | // Action when do click on over the buttons play, next, prev and shuffle 297 | $('.btn-controls').on({ click: clickBtnControls }); 298 | 299 | // step forward or step back the song using the progress bar 300 | $('#total-progress-bar').on({ click: function (e) { PLAYER.controls.moveForward(e, this); } }); 301 | 302 | // Action over the pagination 303 | $('.arrow').on({ 304 | click: function () { 305 | if (this.id === 'right-arrow' && $(this).has('arrow-open-anim')) { 306 | if (countSlide < tempSlide) ++countSlide; 307 | } else if (this.id === 'left-arrow' && $(this).has('arrow-open-anim')) { 308 | if (countSlide < tempSlide && countSlide > 0) --countSlide; 309 | } 310 | 311 | if (countSlide === tempSlide - 1) $('#right-arrow').removeClass('arrow-open-anim'); 312 | if (countSlide === 1) $('#left-arrow').addClass('arrow-open-anim'); 313 | if (countSlide === 0) $('#left-arrow').removeClass('arrow-open-anim'); 314 | if (countSlide === tempSlide - 2) $('#right-arrow').addClass('arrow-open-anim'); 315 | if (countSlide < tempSlide && countSlide !== -1) { 316 | $('#wrapper-results').child() 317 | .css(`transform:translateX(${-1 * (countSlide * document.body.clientWidth)}px)`); 318 | } 319 | } 320 | }); 321 | 322 | /** --------------------------------------- Ipc Renderers --------------------------------------- **/ 323 | // Close the searching bar 324 | ipcRenderer.on('close-search-song', () => { if (isSearchDisplayed) hideSearchInputData(); }); 325 | 326 | // Display the searching bar [ctrl + F] 327 | ipcRenderer.on('search-song', () => { 328 | if (!isSearchDisplayed) { 329 | $('#search-container').removeClass('hide'); 330 | $('#search-wrapper').addClass('search-wrapper-anim'); 331 | $($('.grid-container').get(0)).css('-webkit-filter:blur(1px)'); 332 | $('#wrapper-results').empty(); 333 | $('#search').addClass('search-anim') 334 | .on({ keyup: searchInputData }).val('').get().focus(); 335 | isSearchDisplayed = true; 336 | countSlide = 0; 337 | 338 | let resizeTimes; 339 | window.onresize = function() { 340 | clearTimeout(resizeTimes); 341 | resizeTimes = setTimeout(() => { 342 | $('#wrapper-results') 343 | .css(`width:${tempSlide * document.body.clientWidth}px`); 344 | $('.results').css(`width:${document.body.clientWidth}px`); 345 | }, 160); 346 | }; 347 | } 348 | }); 349 | 350 | // Send the values from the equalizer to the AudioContext [player/controls/index.js] 351 | ipcRenderer.on('get-equalizer-filter', (e, a) => { 352 | PLAYER.controls.setFilterVal(...a); 353 | }); 354 | 355 | // Play or pause song [Ctrl + Up] 356 | ipcRenderer.on('play-and-pause-song', PLAYER.controls.playSong); 357 | 358 | // Next song [Ctrl + Right] 359 | ipcRenderer.on('next-song', PLAYER.controls.nextSong); 360 | 361 | // Prev song [Ctrl + Left] 362 | ipcRenderer.on('prev-song', PLAYER.controls.prevSong); 363 | 364 | // Shuffle [Ctrl + Down] 365 | ipcRenderer.on('shuffle', PLAYER.controls.shuffle); 366 | 367 | // ThumbarButtons [Windows] 368 | ipcRenderer.on('thumbar-controls', (e, a) => { btnActions(a); }); 369 | 370 | // Because the requestAnimationFrame is single thread in the window 371 | // We must save the actual time lapse when we minimized the Window 372 | // and then recalculate the time when we unminimized the window. 373 | ipcRenderer.on('save-current-time', PLAYER.controls.saveCurrentTime); 374 | ipcRenderer.on('update-current-time', PLAYER.controls.updateCurrentTime); -------------------------------------------------------------------------------- /app/assets/js/player/addSongFolder/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /* --------------------------------- Modules --------------------------------- */ 6 | //---- nodejs ---- 7 | const fs = require('fs'); 8 | const exec = require('child_process').exec; 9 | const path = require('path'); 10 | 11 | //---- electron ---- 12 | const remote = require('electron').remote; 13 | 14 | //---- other ---- 15 | const musicmetadata = require('musicmetadata'); 16 | 17 | //---- own ---- 18 | let { 19 | configFile, 20 | langFile, 21 | listSongs, 22 | editFile 23 | } = require('./../../config').init(); 24 | 25 | /* --------------------------------- Variables --------------------------------- */ 26 | //---- normals ---- 27 | let lang = langFile[configFile.lang]; 28 | let songs = []; 29 | let files = []; 30 | 31 | /* --------------------------------- Functions --------------------------------- */ 32 | // List of files and sub-files 33 | // In this way, we avoid to use recursion 34 | function findFiles(dir) { 35 | let allFiles = []; 36 | let tmpFolders = []; 37 | let foldersSize = 0; 38 | let folders = []; 39 | let baseFolder = ''; 40 | const RGX_EXT = /(\.mp3|\.wmv|\.wav|\.ogg)$/ig; 41 | 42 | fs.readdirSync(dir).forEach(files => { 43 | // Based folders 44 | baseFolder = path.join(dir, files); 45 | if (fs.lstatSync(baseFolder).isDirectory()) { 46 | folders.push(baseFolder); 47 | } else if (fs.lstatSync(baseFolder).isFile() && RGX_EXT.test(files)) { 48 | allFiles.push(baseFolder); 49 | } 50 | }); 51 | 52 | foldersSize = folders.length - 1; 53 | while (foldersSize > -1) { 54 | fs.readdirSync(folders[foldersSize]).forEach(files => { 55 | baseFolder = path.join(folders[foldersSize], files); 56 | if (fs.lstatSync(baseFolder).isDirectory()) { 57 | tmpFolders.push(baseFolder); 58 | } else if (fs.lstatSync(baseFolder).isFile() && RGX_EXT.test(files)) { 59 | allFiles.push(baseFolder); 60 | } 61 | }); 62 | 63 | folders.pop(); 64 | folders = folders.concat(tmpFolders); 65 | foldersSize = folders.length - 1; 66 | } 67 | 68 | return allFiles; 69 | } 70 | 71 | // Will get all this songs files. 72 | // It will compare if there's more or few songs 73 | function addSongFolder(folder, fnStart, fnIter) { 74 | // Get the object from listsong.json - only if was already created it 75 | songs = Object.keys(listSongs).length === 0 ? [] : listSongs; 76 | const readAllFiles = readFiles => { 77 | if (songs.length < readFiles.length) { // Add songs 78 | files = readFiles.filter(f => { 79 | if (songs.find(v => v.filename === f) === undefined) return path.normalize(f); 80 | }); 81 | 82 | fnStart(); 83 | extractMetadata(fnIter); 84 | } else if(songs.length > readFiles.length) { // Remove songs 85 | songs = songs.filter(f => { 86 | if (readFiles.find(v => v === f.filename)) return f; 87 | }).map((v, i) => (v.position = i, v)); 88 | 89 | editFile('listSong', songs); 90 | remote.getCurrentWindow().reload(); 91 | } 92 | }; 93 | 94 | // command line [Linux | Mac] 95 | if (process.platform === 'darwin' || process.platform === 'linux') { 96 | const command = `find ${path.normalize(folder.replace(/\b\s{1}/g, '\\ '))} -type f | grep -E \"\.(mp3|wmv|wav|ogg)$\"`; 97 | exec(command, (error, stdout, stderr) => { 98 | if (error) { 99 | remote.dialog.showErrorBox('Error [003]', `${lang.alerts.error_003} ${folder}\n${stderr}`); 100 | return; 101 | } 102 | 103 | readAllFiles(stdout.trim().split('\n')); 104 | }); 105 | } else if (process.platform === 'win32') { 106 | // Only for windows 107 | readAllFiles(findFiles(folder)); 108 | } 109 | } 110 | 111 | // Will get all the needed metadata from a song file 112 | function extractMetadata(fnIter) { 113 | (function(f) { 114 | let asyncForEach = { 115 | init: 0, 116 | end: 0, 117 | loop: () => { 118 | if (asyncForEach.init < asyncForEach.end) { 119 | musicmetadata(fs.createReadStream(f[asyncForEach.init]), (error, data) => { 120 | // In case of error, it will save data using what is inside the lang.json file 121 | songs.push( 122 | error ? 123 | { 124 | artist: lang.artist.trim().replace(/\s/g, ' '), 125 | album: lang.album.trim().replace(/\s/g, ' '), 126 | title: lang.title.trim().replace(/\s/g, ' '), 127 | filename: f[asyncForEach.init] 128 | } : 129 | { 130 | artist: (data.artist.length !== 0 ? data.artist[0] : lang.artist).trim().replace(/\s/g, ' '), 131 | album: (data.album.trim().length !== 0 ? data.album : lang.album).trim().replace(/\s/g, ' '), 132 | title: (data.title.trim().length !== 0 ? data.title : lang.title).trim().replace(/\s/g, ' '), 133 | filename: f[asyncForEach.init] 134 | } 135 | ); 136 | 137 | fnIter(asyncForEach.init + 1, asyncForEach.end); 138 | if (asyncForEach.init + 1 === asyncForEach.end) { 139 | editFile('listSong', 140 | songs.sort((a, b) => 141 | // Works fine with English and Spanish words. Don't know if it's fine for others languages :( 142 | a.artist.toLowerCase().normalize('NFC') < b.artist.toLowerCase().normalize('NFC') ? - 1 : 143 | a.artist.toLowerCase().normalize('NFC') > b.artist.toLowerCase().normalize('NFC') 144 | ).map((v, i) => (v.position = i, v)) 145 | ); 146 | } else { 147 | asyncForEach.init++; 148 | asyncForEach.loop(); 149 | } 150 | }); 151 | } 152 | }, 153 | steps: (init, end) => { 154 | asyncForEach.end = end; 155 | asyncForEach.init = init; 156 | } 157 | }; 158 | 159 | asyncForEach.steps(0, f.length); 160 | asyncForEach.loop(); 161 | })(files); 162 | } 163 | 164 | module.exports = addSongFolder; -------------------------------------------------------------------------------- /app/assets/js/player/controls/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 12016 - 2017 4 | */ 5 | /* --------------------------------------- Modules ------------------------------------------- */ 6 | //---- nodejs ---- 7 | const path = require('path'); 8 | 9 | //---- electron ---- 10 | const dialog = require('electron').remote.dialog; 11 | 12 | //---- own ---- 13 | const { 14 | configFile, 15 | langFile, 16 | listSongs, 17 | editFile 18 | } = require('./../../config').init(); 19 | require('./../../dom'); 20 | 21 | /* --------------------------------------- Variables ------------------------------------------- */ 22 | //---- normals ---- 23 | let poolOfSongs = {}; // Will keep all the AudioBuffers 24 | let lang = langFile[configFile.lang]; 25 | let isMovingForward = false; // if is using the progress bar of the song 26 | let isNextAble = false; // if the next song can be played (needed because AudioNode.stop() works with Promise) 27 | let isSongPlaying = false; // It's the AudioNode playing 28 | let isplayedAtPosition = false // If the song is clicked on from the list 29 | let position = Math.floor(Math.random() * listSongs.length); // Position of the song to play for the very first time. 30 | let prevSongsToPlay = []; // Will keep all the filename of old songs 31 | let file = ''; // Will keep the data song info 32 | let oldFile = ''; // Will keep the data of the song played 33 | let filter = []; // Array for createBiquadFilter to use the Frequencies 34 | let duration = 0; // max duration of the song 35 | let source = null; // AudioNode object 36 | 37 | // Elapsed time 38 | let lapse = 0; 39 | let percent = 0; 40 | let millisecond = 0; 41 | let time = 0; 42 | let minute = 0; 43 | let second = 0; 44 | let interval = null; 45 | let lastCurrentTime = 0; 46 | 47 | // Notification 48 | let notification = null; 49 | let notifi = { 50 | lang: 'US', 51 | tag: 'song', 52 | silent: false, 53 | icon: path.join(__dirname, '../../../', 'img', 'play.png') 54 | }; 55 | 56 | //---- constants ---- 57 | const SECONDS_U = 60; 58 | const audioContext = new window.AudioContext(); // Object AudioContext 59 | const xhtr = new XMLHttpRequest(); // Object XMLHttpRequest 60 | const hrz = [ // Frequencies 61 | 40, 80, 90, 100, 120, 150, 200, 62 | 300, 400, 500, 600, 800, 1000, 63 | 1600, 2000, 3000, 4000, 5000, 6000, 64 | 7000, 8000, 10000, 16000 65 | ]; 66 | 67 | // Cords to generate the animation 68 | // Google Chrome is throwing the next warning message: 69 | // ** SVG's SMIL animations (, , etc.) are deprecated and 70 | // will be removed. Please use CSS animations or Web animations instead.**- 71 | // For now all works fine. :P 72 | const anim = { 73 | from: [ 74 | 'M 5.827315,6.7672041 62.280287,48.845328 62.257126,128.62684 5.8743095,170.58995 Z', 75 | 'm 61.189203,48.025 56.296987,40.520916 0,0.0028 -56.261916,40.850634 z' 76 | ], 77 | to: [ 78 | 'M 5.827315,6.7672041 39.949651,6.9753863 39.92649,170.36386 5.8743095,170.58995 Z', 79 | 'm 83.814203,6.9000001 34.109487,0.037583 -0.0839,163.399307 -33.899661,0.16304 z' 80 | ] 81 | }; 82 | 83 | /** --------------------------------------- Functions --------------------------------------- **/ 84 | // Enable shuffle 85 | function shuffle() { 86 | file = ''; 87 | configFile.shuffle = !configFile.shuffle; 88 | $('#shuffle-icon').css(configFile.shuffle ? 'fill:#FBFCFC' : 'fill:#f06292'); 89 | editFile('config', configFile); 90 | } 91 | 92 | // Animation of the play/pause buttons 93 | function animPlayAndPause(animName) { 94 | const animAttr = i => { 95 | return animName === 'play' ? 96 | { from: anim.from[i], to: anim.to[i] } : 97 | { from: anim.to[i], to: anim.from[i] }; 98 | }; 99 | 100 | $('.anim-play').each((v, i) => { 101 | $(v).attr(animAttr(i)).get().beginElement(); 102 | }); 103 | } 104 | 105 | function playSongAtPosition(pos = -1) { 106 | if (source !== null) { 107 | source.stop(0); 108 | source = null; 109 | } 110 | 111 | if (oldFile !== '') prevSongsToPlay.push(oldFile); 112 | 113 | file = ''; 114 | isplayedAtPosition = true; 115 | position = pos; 116 | initSong(); 117 | } 118 | 119 | function playSong() { 120 | if (!isSongPlaying && audioContext.state === 'running') { // Reproducción única 121 | initSong(); 122 | 123 | return 'resume'; 124 | } else if (isSongPlaying && audioContext.state === 'running') { // Ya reproduciendo 125 | audioContext.suspend().then(() => { 126 | isSongPlaying = false; 127 | }); 128 | cancelAnimationFrame(interval); 129 | animPlayAndPause('pause'); 130 | 131 | return 'paused'; 132 | } else if (!isSongPlaying && audioContext.state === 'suspended') { // Pausado 133 | isSongPlaying = true; 134 | startTimer(); 135 | audioContext.resume(); 136 | animPlayAndPause('play'); 137 | 138 | return 'resume'; 139 | } 140 | } 141 | 142 | // Lapse of time 143 | function startTimer() { 144 | const UPDATE = () => { 145 | if (++millisecond > 59) { 146 | millisecond = 0; 147 | if (++second > 59) { 148 | ++minute; 149 | second = 0; 150 | } 151 | 152 | $('#time-start').text(`${formatDecimals(minute)}:${formatDecimals(second)}`); 153 | $('#progress-bar').css(`width:${percent += lapse}%`); 154 | } 155 | interval = requestAnimationFrame(UPDATE); 156 | }; 157 | interval = requestAnimationFrame(UPDATE); 158 | } 159 | 160 | // Clean the everything when the ended function is executed 161 | function stopTimer() { 162 | if (!isMovingForward) { 163 | $(`#${oldFile.position}`).child().each(v => { $(v).css('color:#424949'); }); 164 | isSongPlaying = false; 165 | cancelAnimationFrame(interval); 166 | millisecond = second = minute = percent = lapse = 0; 167 | isNextAble = true; 168 | if (isNextAble && !isMovingForward && !isplayedAtPosition) initSong(); 169 | } else if (isMovingForward) { 170 | 171 | // It must be created a new AudioNode, because the stop function delete the node. 172 | setAudioBuffer(poolOfSongs[oldFile.filename]); 173 | 174 | isMovingForward = false; 175 | isSongPlaying = true; 176 | } 177 | } 178 | 179 | // Show the data of the selected song 180 | function dataSong(file) { 181 | $('#time-start').text('00:00'); 182 | $('#progress-bar').css('width:0'); 183 | $('#song-title').data({position: file.position}).child().each(v => { $(v).text(file.title); }); 184 | $('#artist').child().each(v => { $(v).text(file.artist); }); 185 | $('#album').child().each(v => { $(v).text(file.album); }); 186 | 187 | if (notification !== null) { 188 | notification.close(); 189 | notification = null 190 | } 191 | 192 | notifi.body = `${file.artist.replace(/\ /g, ' ')} from ${file.album.replace(/\ /g, ' ')}`; 193 | notification = new Notification(file.title.replace(/\ /g, ' '), notifi); 194 | } 195 | 196 | function setBufferInPool(filePath, buffer) { 197 | if (!poolOfSongs[filePath]) poolOfSongs[filePath] = buffer; 198 | } 199 | 200 | function getFile() { 201 | return listSongs[ 202 | isplayedAtPosition ? position : 203 | (configFile.shuffle ? Math.floor(Math.random() * listSongs.length) : ++position 204 | )]; 205 | } 206 | 207 | function formatDecimals(decimal) { 208 | return decimal > 9 ? `${decimal}` : `0${decimal}`; 209 | } 210 | 211 | function setAudioBuffer(buffer) { 212 | source = audioContext.createBufferSource(); 213 | source.onended = stopTimer; 214 | source.buffer = buffer; 215 | 216 | // connect all the nodes 217 | source.connect(filter[0]); 218 | filter.reduce((p, c) => p.connect(c)).connect(audioContext.destination); 219 | startTimer(); 220 | isMovingForward ? source.start(0, forward) : source.start(0); 221 | lastCurrentTime = audioContext.currentTime; 222 | } 223 | 224 | function initSong() { 225 | animPlayAndPause('play'); 226 | 227 | // Get the buffer of the song 228 | const getBuffer = (filePath, fnc) => { 229 | // Read the file 230 | xhtr.open('GET', `file://${filePath}`, true); 231 | xhtr.responseType = 'arraybuffer'; 232 | xhtr.onload = () => { 233 | audioContext.decodeAudioData(xhtr.response).then(buffer => { 234 | fnc(buffer); 235 | }, reason => { 236 | dialog.showErrorBox('Error [002]', `${lang.alerts.playSong}\n${reason}`); 237 | fnc(false); 238 | }); 239 | } 240 | xhtr.send(null); 241 | }; 242 | 243 | const setSong = (buffer) => { 244 | // The buffer gives us the song's duration. 245 | // The duration is in seconds, therefore we need to convert it to minutes 246 | time = ((duration = buffer.duration) / SECONDS_U).toString(); 247 | lapse = 100 / duration; 248 | 249 | $('#time-end').text(` 250 | ${formatDecimals(parseInt(time.slice(0, time.lastIndexOf('.'))))}:${formatDecimals(Math.floor(time.slice(time.lastIndexOf('.')) * SECONDS_U))} 251 | `); 252 | 253 | setAudioBuffer(buffer); 254 | 255 | // Change the color the actual song 256 | $(`#${file.position}`).child().each(v => { $(v).css('color:#e91e63'); }); 257 | 258 | isSongPlaying = true; 259 | } 260 | 261 | const nextPossibleSong = () => { 262 | isplayedAtPosition = false; 263 | position = oldFile.position; 264 | 265 | // Next (possible) song to play 266 | // if it is not saved into the buffer, we have to get it and save it 267 | file = getFile(); 268 | if (!poolOfSongs[file.filename]) { 269 | getBuffer(file.filename, data => { 270 | if (!data) throw data; 271 | 272 | isNextAble = true; 273 | setBufferInPool(file.filename, data); 274 | }); 275 | } 276 | }; 277 | 278 | // Get the buffer of song if it is in the poolOfSongs 279 | // Note: The oldFile is an important variable, because is saved into 280 | // the prevSongsToPlay array, which has all the played songs. 281 | if (poolOfSongs[file.filename]) { 282 | // play the song and save it as an old song (oldFile) 283 | dataSong((oldFile = file)); 284 | setSong(poolOfSongs[file.filename]); // Set the buffer 285 | nextPossibleSong(); 286 | } else { 287 | // Get the song to play 288 | file = getFile(); 289 | getBuffer(file.filename, data => { 290 | if (!data) throw data; 291 | 292 | // Save the buffer 293 | setBufferInPool(file.filename, data); 294 | 295 | // Play the song and save it as old (oldFile) 296 | dataSong((oldFile = file)); 297 | setSong(data); 298 | nextPossibleSong(); 299 | }); 300 | } 301 | } 302 | 303 | function nextSong() { 304 | if (isNextAble) { 305 | // oldFile saved 306 | prevSongsToPlay.push(oldFile); 307 | 308 | if (!isSongPlaying && audioContext.state === 'suspended') audioContext.resume(); 309 | 310 | if(source !== null) { 311 | source.stop(0); 312 | source = null; 313 | } 314 | 315 | isNextAble = false; 316 | } 317 | } 318 | 319 | function prevSong() { 320 | if (prevSongsToPlay.length > 0 && isNextAble) { 321 | file = prevSongsToPlay.pop(); 322 | position = file.position; 323 | 324 | if (!isSongPlaying && audioContext.state === 'suspended') audioContext.resume(); 325 | 326 | if(source !== null) { 327 | source.stop(0); 328 | source = null; 329 | } 330 | 331 | isNextAble = false; 332 | } 333 | } 334 | 335 | function setFilterVal(a, b) { 336 | filter[a].gain.setValueAtTime(b, audioContext.currentTime); 337 | } 338 | 339 | function filters() { 340 | let f = null; 341 | let db = configFile.equalizer[configFile.equalizerConfig].map(v => 342 | v !== 0 ? parseFloat((v < 130 ? 121 - v : -v + 140) / 10) : 0 343 | ); 344 | 345 | filter = hrz.map((v, i) => 346 | (f = audioContext.createBiquadFilter(), 347 | f.type = 'peaking', 348 | f.frequency.value = v, 349 | f.Q.value = 1, 350 | f.gain.value = db[i], f) 351 | ); 352 | } 353 | filters(); 354 | 355 | function moveForward(event, element) { 356 | isMovingForward = true; 357 | cancelAnimationFrame(interval); 358 | 359 | forward = duration * event.offsetX / element.clientWidth; 360 | time = (forward / SECONDS_U).toString(); 361 | 362 | // Calculate the new time 363 | minute = parseInt(time.slice(0, time.lastIndexOf('.'))); 364 | second = Math.floor(time.slice(time.lastIndexOf('.')) * SECONDS_U); 365 | millisecond = forward * 100; 366 | 367 | // Calculate the percent of the progress bar 368 | percent = forward * (100 / duration); 369 | source.stop(0); 370 | } 371 | 372 | function saveCurrentTime() { 373 | lastCurrentTime = audioContext.currentTime; 374 | } 375 | 376 | function updateCurrentTime() { 377 | let totalTime = Math.floor(audioContext.currentTime - lastCurrentTime); 378 | if (totalTime > 60){ 379 | totalTime = (totalTime / 60).toString(); 380 | minute += parseInt(totalTime.slice(0, totalTime.lastIndexOf('.'))); 381 | second += Math.floor(totalTime.slice(totalTime.lastIndexOf('.')) * SECONDS_U); 382 | percent += lapse * Math.floor(audioContext.currentTime - lastCurrentTime); 383 | } 384 | } 385 | 386 | module.exports = { 387 | nextSong, 388 | prevSong, 389 | playSong, 390 | shuffle, 391 | setFilterVal, 392 | moveForward, 393 | playSongAtPosition, 394 | saveCurrentTime, 395 | updateCurrentTime 396 | } -------------------------------------------------------------------------------- /app/assets/js/player/createView/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /** --------------------------------------- Modules --------------------------------------- **/ 6 | // Own modules 7 | const { 8 | listSongs 9 | } = require('./../../config').init(); 10 | require('./../../dom'); 11 | 12 | /** --------------------------------------- Functions --------------------------------------- **/ 13 | function createView(player) { 14 | const child = $('div').clone(false).addClass('grid-33 mobile-grid-33 song-info'); 15 | const parentContainer = $('div').clone(false).addClass('list-song-container'); 16 | const f = document.createDocumentFragment(); 17 | let title = null; 18 | let album = null; 19 | let artist = null; 20 | 21 | listSongs.forEach((v, i) => { 22 | title = child.clone(true).text(v.title); 23 | artist = child.clone(true).text(`by${v.artist}`); 24 | album = child.clone(true).text(`from${v.album}`); 25 | 26 | f.appendChild( 27 | parentContainer.clone(true) 28 | .attr({id: i}) 29 | .data({ 30 | position: i, 31 | artist: v.artist, 32 | title: v.title, 33 | album: v.album, 34 | url: v.filename 35 | }) 36 | .insert(title, artist, album) 37 | .on({ 38 | click: function() { 39 | player.controls.playSongAtPosition($(this).data('position')); 40 | } 41 | }).get() 42 | ); 43 | }); 44 | 45 | $('#list-songs').insert(f); 46 | } 47 | 48 | module.exports = createView; -------------------------------------------------------------------------------- /app/assets/js/player/equalizer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /* --------------------------------- Modules --------------------------------- */ 6 | require('./../../dom'); 7 | //---- normals ---- 8 | let range = null; 9 | let y = 0; 10 | let db = 0; 11 | let pos = 0; 12 | let _db = 0; 13 | let _pos = 0; 14 | 15 | function onDragMove(fn) { 16 | $(document).on({ 17 | mousemove: e => { 18 | if (range !== null) { 19 | y = parseInt(window.getComputedStyle(range).getPropertyValue('top')); 20 | db = (e.clientY - range.offsetTop) + y; 21 | 22 | if (db > 0 && db < 261) $(range).css(`top:${db}px`); 23 | 24 | return fn([ 25 | $(range).data('position'), 26 | db !== 0 ? parseFloat((db < 130 ? 121 - db : -db + 140) / 10) : 0 27 | ]); 28 | } 29 | } 30 | }); 31 | }; 32 | 33 | function onDragEnd(fn) { 34 | $(document).on({ 35 | mouseup: () => { 36 | _pos = pos 37 | _db = db; 38 | range = null; 39 | y = db = pos = 0; 40 | return fn(_pos, _db); 41 | } 42 | }); 43 | } 44 | 45 | function onDragStart(el) { 46 | pos = $((range = el)).data('position'); 47 | }; 48 | 49 | module.exports = { 50 | onDragMove, 51 | onDragStart, 52 | onDragEnd 53 | }; -------------------------------------------------------------------------------- /app/assets/js/player/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera. 3 | * @copyright 2016 - 2017 4 | */ 5 | 6 | const addSongFolder = require('./addSongFolder'); 7 | const createView = require('./createView'); 8 | const controls = require('./controls'); 9 | 10 | module.exports = Object.freeze({ 11 | addSongFolder, 12 | createView, 13 | controls 14 | }); -------------------------------------------------------------------------------- /app/assets/js/thumbar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /* --------------------------------- Functions --------------------------------- */ 6 | function makeThumBar(win, imgs = {}) { 7 | const play = { 8 | icon: imgs.play, 9 | tooltip: 'Play', 10 | click: () => { 11 | win.setThumbarButtons(pauseMomment); 12 | win.webContents.send('thumbar-controls', 'play-pause'); 13 | } 14 | }; 15 | 16 | const prev = { 17 | icon: imgs.prev, 18 | tooltip: 'Prev', 19 | click: () => win.webContents.send('thumbar-controls', 'prev') 20 | }; 21 | 22 | const next = { 23 | icon: imgs.next, 24 | tooltip: 'Next', 25 | click: () => win.webContents.send('thumbar-controls', 'next') 26 | }; 27 | 28 | const pause = { 29 | icon: imgs.pause, 30 | tooltip: 'Pause', 31 | click: () => { 32 | win.setThumbarButtons(playMomment); 33 | win.webContents.send('thumbar-controls', 'play-pause'); 34 | } 35 | } 36 | 37 | const playMomment = [prev, play, next]; 38 | const pauseMomment = [prev, pause, next]; 39 | 40 | return { 41 | playMomment, 42 | pauseMomment 43 | }; 44 | } 45 | 46 | module.exports = Object.freeze({ 47 | makeThumBar 48 | }); -------------------------------------------------------------------------------- /app/assets/js/version/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | /* --------------------------------- Functions --------------------------------- */ 6 | function getActualVersion(net, version, fn) { 7 | var data = ''; 8 | const request = new net.ClientRequest({ 9 | method: 'GET', 10 | protocol: 'https:', 11 | hostname: 'api.github.com', 12 | path: '/repos/dracotmolver/soube/releases/latest', 13 | headers: { 14 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0' 15 | } 16 | }); 17 | 18 | request.chunkedEncoding = true; 19 | request.on('response', response => { 20 | response.on('data', chunk => { 21 | data += chunk; 22 | }); 23 | response.on('end', () => { 24 | const actualVersion = version.split('.'); 25 | const newVersion = JSON.parse(data).tag_name.split('.'); 26 | 27 | const diff = (b, c) => { 28 | let val = false; 29 | for (var i = 0, s = c.length; i < s; i++) { 30 | if (parseInt(c[i]) > parseInt(b[i])) { 31 | val = true; 32 | break; 33 | } 34 | } 35 | 36 | return val; 37 | }; 38 | fn(diff(actualVersion, newVersion) ? 'major' : 'same'); 39 | }); 40 | }); 41 | 42 | request.on('error', error => { 43 | console.error(error); 44 | }); 45 | 46 | request.end(); 47 | } 48 | 49 | module.exports = getActualVersion; -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Diego Alberto Molina Vera 3 | * @copyright 2016 - 2017 4 | */ 5 | process.env.NODE_ENV = 'production'; 6 | 7 | /* --------------------------------- Modules --------------------------------- */ 8 | //---- Electron ---- 9 | const { 10 | globalShortcut, 11 | BrowserWindow, 12 | nativeImage, 13 | ipcMain, 14 | Tray, 15 | app 16 | } = require('electron'); 17 | 18 | //---- Node ---- 19 | const path = require('path'); 20 | 21 | //---- Own ---- 22 | const config = require('./../app/assets/js/config'); 23 | const thumbar = require('./../app/assets/js/thumbar'); // [Windows] 24 | 25 | /* --------------------------------- Variables --------------------------------- */ 26 | let configWindow = null; 27 | let mainWindow = null; 28 | const shortKeys = { 29 | 'CommandOrControl+F': 'search-song', // Input search 30 | 'Esc': 'close-search-song', // Cerrar input search 31 | 'CommandOrControl+Up': 'play-and-pause-song', // Play & Pause 32 | 'CommandOrControl+Right': 'next-song', // Next 33 | 'CommandOrControl+Left': 'prev-song', // Prev 34 | 'CommandOrControl+Down': 'shuffle' // Shuffle 35 | }; 36 | let thumbarButtons = {}; 37 | 38 | /* --------------------------------- Funciones --------------------------------- */ 39 | function closeRegisteredKeys() { 40 | Object.keys(shortKeys).forEach(v => { globalShortcut.unregister(v); }); 41 | } 42 | 43 | function registreKeys() { 44 | Object.keys(shortKeys).forEach(v => { 45 | globalShortcut.register(v, () => { 46 | mainWindow.webContents.send(shortKeys[v]); 47 | }); 48 | }); 49 | } 50 | 51 | // Make the icon of the app 52 | function makeIcon(name) { 53 | return nativeImage.createFromPath(path.join(__dirname, 'assets', 'img', name)); 54 | } 55 | 56 | function ready() { 57 | // Make all the config files 58 | config.createFiles(app); 59 | 60 | // Will shows the icon of the notificaion 61 | const appIcon = new Tray(path.join(__dirname, 'assets', 'img', 'icon.png')); 62 | 63 | // Player 64 | mainWindow = new BrowserWindow({ 65 | autoHideMenuBar: true, 66 | defaultEncoding: 'utf-8', 67 | useContentSize: true, 68 | minHeight: 640, 69 | minWidth: 600, 70 | height: 600, 71 | center: true, 72 | width: 1200, 73 | show: false, 74 | icon: makeIcon('icon.png') 75 | }); 76 | 77 | mainWindow.setMenu(null); 78 | // mainWindow.webContents.openDevTools(); 79 | mainWindow.loadURL(path.join('file://', __dirname, 'views', 'main', 'index.html')); 80 | mainWindow.on('closed', () => { 81 | closeRegisteredKeys(); 82 | appIcon.destroy(); 83 | BrowserWindow.getAllWindows().forEach(v => { v.close(); }); 84 | mainWindow = configWindow = null; 85 | }) 86 | .on('focus', registreKeys) 87 | .on('blur', closeRegisteredKeys) 88 | .on('minimize', () => { 89 | mainWindow.webContents.send('save-current-time'); 90 | }) 91 | .on('restore', () => { 92 | // This happens also when you reload the website (refresh) 93 | mainWindow.webContents.send('update-current-time'); 94 | }); 95 | 96 | mainWindow.once('ready-to-show', () => { 97 | mainWindow.show(); 98 | // Thumbar-button [Windows] 99 | if (process.platform === 'win32') { 100 | thumbarButtons = thumbar.makeThumBar(mainWindow, { 101 | next: makeIcon('thumb-next.png'), 102 | pause: makeIcon('thumb-pause.png'), 103 | prev: makeIcon('thumb-prev.png'), 104 | play: makeIcon('thumb-play.png') 105 | }); 106 | 107 | // Thumbar Buttons 108 | mainWindow.setThumbarButtons(thumbarButtons.playMomment); 109 | } 110 | }); 111 | } 112 | 113 | function showConfigPanel() { 114 | if (configWindow === null) { 115 | configWindow = new BrowserWindow({ 116 | autoHideMenuBar: true, 117 | defaultEncoding: 'utf-8', 118 | useContentSize: true, 119 | resizable: false, 120 | height: 500, 121 | center: true, 122 | width: 1125, 123 | icon: makeIcon('icon.png') 124 | }); 125 | 126 | configWindow.setMenu(null); 127 | // configWindow.webContents.openDevTools(); 128 | configWindow.loadURL(path.join('file://', __dirname, 'views', 'config-panel', 'config.html')); 129 | configWindow.on('closed', () => { 130 | configWindow = null; 131 | }); 132 | } 133 | } 134 | 135 | /* --------------------------------- Electronjs O_o --------------------------------- */ 136 | app.on('window-all-closed', () => { app.quit(); }) 137 | app.setName('Soube'); 138 | app.on('ready', ready); 139 | 140 | /* --------------------------------- Ipc Main --------------------------------- */ 141 | // Config panel 142 | ipcMain.on('show-config', showConfigPanel); 143 | 144 | // Displays the list of song after update or overwrite the song folder 145 | // ipcMain.on('display-list', () => { 146 | // mainWindow.webContents.send('order-display-list'); 147 | // }); 148 | 149 | // Sending data from the EQ to the AudioContext 150 | ipcMain.on('equalizer-filter', (e, a) => { 151 | mainWindow.webContents.send('get-equalizer-filter', a); 152 | }); 153 | 154 | // Updating of the thumbar buttons 155 | ipcMain.on('thumb-bar-update', (e, a) => { 156 | mainWindow.setThumbarButtons(thumbarButtons[a]); 157 | }); 158 | 159 | // Launch the config panel from the Player 160 | ipcMain.on('config-panel', showConfigPanel); -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soube", 3 | "version": "1.4.2", 4 | "description": "Simple music player", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "email": "dracotm25@gmail.com", 9 | "name": "Diego Molina Vera" 10 | }, 11 | "homepage": "http://soube.diegomolina.cl", 12 | "dependencies": { 13 | "musicmetadata": "^2.0.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/views/config-panel/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |

23 |
24 |
25 |

26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |

35 |
36 |
37 |

38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 |

47 |
48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 | 63 |
64 | 65 |
66 |
67 |

Español

68 |
69 |
70 |

English

71 |
72 |
73 | 74 |
75 |
76 |
77 | 79 |
80 |
81 |
82 | 83 |
84 |
85 | 86 |
87 |
88 |
89 |
90 |
91 |
Hz
92 |
KHz
93 |
94 |
95 |
    96 |
  • 40

  • 97 |
  • 80

  • 98 |
  • 90

  • 99 |
  • 100

  • 100 |
  • 120

  • 101 |
  • 150

  • 102 |
  • 200

  • 103 |
  • 300

  • 104 |
  • 400

  • 105 |
  • 500

  • 106 |
  • 600

  • 107 |
  • 800

  • 108 |
  • 1

  • 109 |
  • 1.6

  • 110 |
  • 2

  • 111 |
  • 3

  • 112 |
  • 4

  • 113 |
  • 5

  • 114 |
  • 6

  • 115 |
  • 7

  • 116 |
  • 8

  • 117 |
  • 10

  • 118 |
  • 16

  • 119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | 195 | 206 |
207 |
208 | 209 |
210 |

211 |
212 |
213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /app/views/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 | ▲ 14 |
15 |
16 | ▼ 17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 |
40 | 43 |
44 | 45 |
46 | 49 |
50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 |

 

95 |
96 |
97 |
98 |

 

99 |
100 |
101 |
102 |

 

103 |
104 |
105 |
106 |
107 |
108 |
109 | 00:00 - 00:00 110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 |
121 | 122 |
123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 |
131 |
132 | 139 |
140 | 141 |
142 |
143 |
144 |
145 |
146 |
147 | 151 |
152 | 153 |
154 |
155 |
156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- 1 | icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icon.ico -------------------------------------------------------------------------------- /build/icons/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icons/144x144.png -------------------------------------------------------------------------------- /build/icons/192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icons/192x192.png -------------------------------------------------------------------------------- /build/icons/192x292.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icons/192x292.png -------------------------------------------------------------------------------- /build/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icons/48x48.png -------------------------------------------------------------------------------- /build/icons/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icons/72x72.png -------------------------------------------------------------------------------- /build/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracotMolver/soube/8e801be2b8318aab4078790e67aaccb3a402c336/build/icons/96x96.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "es6", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components", 12 | "jspm_packages", 13 | "tmp", 14 | "temp" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soube", 3 | "version": "1.4.2", 4 | "description": "Simple music player", 5 | "author": { 6 | "email": "dracotm25@gmail.com", 7 | "name": "Diego Molina Vera" 8 | }, 9 | "homepage": "http://soube.diegomolina.cl", 10 | "devDependencies": { 11 | "electron": "^1.6.4", 12 | "electron-builder": "^10.17.3", 13 | "electron-packager": "^8.6.0" 14 | }, 15 | "build": { 16 | "copyright": "Copyright © 2016 - 2017. Diego Molina Vera.", 17 | "asar": false, 18 | "linux": { 19 | "category": "Audio", 20 | "packageCategory": "sound", 21 | "target": [ 22 | "rpm", 23 | "deb" 24 | ], 25 | "synopsis": "Soube — Simple music player", 26 | "desktop": { 27 | "Type": "Application", 28 | "Encoding": "UTF-8", 29 | "Name": "Soube", 30 | "Exec": "soube", 31 | "Terminal": false, 32 | "Version": "1.4.2", 33 | "Categories": "Audio;Music;Player;AudioVideo" 34 | } 35 | }, 36 | "win": { 37 | "target": [ 38 | "nsis", 39 | "squirrel" 40 | ] 41 | }, 42 | "mac": { 43 | "category": "public.app-category.music", 44 | "target": "dmg" 45 | } 46 | }, 47 | "scripts": { 48 | "pack": "build --dir", 49 | "dist": "build", 50 | "build": "electron-packager ./app soube --out=dist --platform=linux --ignore='^/dist$' --arch=all --prune --icon='./app/assets/img/icon.png'" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /soube.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.4.2 3 | Name=Soube 4 | Exec=/opt/soube/soube 5 | Terminal=false 6 | Icon=/opt/soube/resources/app/assets/img/icon.png 7 | Type=Application 8 | Categories=Application;MusicPlayer;Player --------------------------------------------------------------------------------