├── .babelrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .travis.yml ├── .tx └── config ├── LICENSE ├── README.md ├── bin ├── before-build ├── build ├── build-linux ├── build-mac ├── build-windows ├── compile ├── generate-config └── generate-env ├── bootup.js ├── build ├── background.png ├── icon.icns └── icon.ico ├── config ├── api_config.development.json ├── api_config.sample.json └── ga_config.json ├── data └── thanks.json ├── index.html ├── logo.png ├── misc └── linux │ └── kaku.desktop ├── package-lock.json ├── package.json ├── src ├── locales │ ├── ar.ini │ ├── br.ini │ ├── cs.ini │ ├── da.ini │ ├── de.ini │ ├── el.ini │ ├── en.ini │ ├── es.ini │ ├── fa_IR.ini │ ├── fr.ini │ ├── id_ID.ini │ ├── it.ini │ ├── ja.ini │ ├── ko.ini │ ├── metadata.json │ ├── nl_BE.ini │ ├── nl_NL.ini │ ├── pl.ini │ ├── pt.ini │ ├── pt_BR.ini │ ├── ru.ini │ ├── sk_SK.ini │ ├── sl_SI.ini │ ├── sv.ini │ ├── tr.ini │ └── zh-TW.ini ├── main.js ├── modules │ ├── AppCore.js │ ├── AutoUpdater.js │ ├── Constants.js │ ├── Database.js │ ├── DownloadManager.js │ ├── ErrorMonitor.js │ ├── HistoryManager.js │ ├── L10nManager.js │ ├── NewsFetcher.js │ ├── PlaylistManager.js │ ├── PreferenceManager.js │ ├── README.md │ ├── Searcher.js │ ├── Tracker.js │ ├── backuper │ │ ├── DropboxBackuper.js │ │ └── LocalBackuper.js │ ├── importer │ │ └── YoutubeImporter.js │ └── wrapper │ │ ├── Firebase.js │ │ └── Youtube.js ├── public │ ├── fonts │ │ └── Arimo │ │ │ ├── Arimo-Bold.ttf │ │ │ ├── Arimo-BoldItalic.ttf │ │ │ ├── Arimo-Italic.ttf │ │ │ ├── Arimo-Regular.ttf │ │ │ └── LICENSE.txt │ ├── images │ │ ├── icons │ │ │ ├── kaku.icns │ │ │ ├── kaku.ico │ │ │ ├── kaku.png │ │ │ ├── touchbar │ │ │ │ ├── kaku.png │ │ │ │ └── kaku@2x.png │ │ │ └── tray │ │ │ │ ├── default.png │ │ │ │ ├── default@2x.png │ │ │ │ └── windows.ico │ │ ├── others │ │ │ └── cast.png │ │ └── track-placeholder.png │ └── less │ │ ├── components │ │ ├── about.less │ │ ├── chatroom.less │ │ ├── connection_check.less │ │ ├── fonts.less │ │ ├── global.less │ │ ├── online_dj.less │ │ ├── settings.less │ │ ├── sidebar.less │ │ ├── tab_content.less │ │ ├── toolbar.less │ │ ├── topranking.less │ │ ├── track.less │ │ └── videojs.less │ │ ├── includes │ │ ├── mixins.less │ │ └── variables.less │ │ └── index.less └── views │ ├── components │ ├── about │ │ └── index.js │ ├── alltracks │ │ └── index.js │ ├── chatroom │ │ ├── comment │ │ │ ├── comment-form.js │ │ │ ├── comment-list.js │ │ │ ├── comment-no-data.js │ │ │ └── comment.js │ │ └── index.js │ ├── connection-check │ │ └── index.js │ ├── history │ │ └── index.js │ ├── menus │ │ ├── index.js │ │ └── playlist.js │ ├── news │ │ ├── index.js │ │ └── news-tag.js │ ├── online-dj │ │ ├── choose-role-page.js │ │ ├── dashboard-page.js │ │ └── index.js │ ├── play-queue │ │ └── index.js │ ├── player │ │ ├── control-buttons.js │ │ ├── index.js │ │ └── track.js │ ├── playlist │ │ └── index.js │ ├── searchbar │ │ └── index.js │ ├── settings │ │ └── index.js │ ├── shared │ │ ├── action-button.js │ │ ├── add-to-play-queue-button.js │ │ ├── l10n-span.js │ │ ├── track-mode-button.js │ │ ├── track │ │ │ ├── no-track.js │ │ │ ├── track-list.js │ │ │ ├── track-square.js │ │ │ └── track.js │ │ └── tracks.js │ ├── toolbar │ │ └── index.js │ └── topranking │ │ └── index.js │ └── modules │ ├── AppMenus.js │ ├── AppTouchBar.js │ ├── AppTray.js │ ├── CastingManager.js │ ├── Dialog.js │ ├── EasterEggs.js │ ├── KonamiCodeManager.js │ ├── Notifier.js │ ├── Player.js │ ├── RemotePlayer.js │ └── TabManager.js ├── tests ├── ui │ ├── SearchBar.test.js │ ├── Tooltip.test.js │ └── setup.js └── unit │ ├── AppCore.test.js │ ├── HistoryManager.test.js │ ├── L10nManager.test.js │ ├── PlaylistManager.test.js │ ├── PreferenceManager.test.js │ ├── mocks │ ├── Database.js │ ├── Electron │ │ ├── Remote.js │ │ └── index.js │ ├── KakuCore.js │ └── Tracker.js │ └── setup.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [EragonJ] 4 | patreon: EragonJ 5 | open_collective: Kaku 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Bug 2 | 3 | Please describe the bug you found here! 4 | 5 | #### Steps to reproduce the bug 6 | 7 | If you do find a solid way to reproduce this, please write them down here because it can be super useful for debugging! 8 | 9 | #### What is your Kaku version? 10 | 11 | 1.8.0 (for example) 12 | 13 | #### What is your OS (with version number)? 14 | 15 | Mac 10.11 (for example) 16 | 17 | *Note for Linux Users: Please provide here the result of `uname -srmv`* 18 | 19 | #### Which platform are you using to listen to music? 20 | 21 | YouTube (for example) 22 | 23 | #### Which track format are you using? 24 | 25 | best video (or best audio) 26 | 27 | #### Is there any error coming from Inspector? 28 | 29 | You can use: 30 | * `ctrl + alt + i` on **Windows** 31 | * `ctrl + alt + i` on **Linux** 32 | * `cmd + option + i` on **Mac** 33 | 34 | To turn on `inspector`. 35 | 36 | Then switch to the `console` tab and copy the error(s) message(s) you found! 37 | If possible, any screenshot would be better! 38 | 39 | #### Others 40 | 41 | Feel free to point out any missing points here :) thanks! 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixed bug: #XXX 2 | 3 | What got changed here : 4 | * A 5 | * B 6 | * C 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | dist 4 | src/public/css 5 | config/*.production.json 6 | env.json 7 | node_modules 8 | .module_cache 9 | kaku/ 10 | kaku.bundled.js 11 | kaku.bundled.js.map 12 | npm-debug.log 13 | .lvimrc 14 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.0.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode9.4 2 | sudo: false 3 | language: node_js 4 | node_js: "10.0.0" 5 | os: 6 | - osx 7 | 8 | script: 9 | - "npm test" 10 | - "npm run generate:config" 11 | - "npm run release" 12 | 13 | before_install: 14 | - "npm i -g npm@6.4.1" 15 | 16 | cache: 17 | directories: 18 | - "node_modules" 19 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [desktop-app.enini] 5 | file_filter = src/locales/.ini 6 | lang_map = zh: zh-TW, de_DE: de 7 | source_file = src/locales/en.ini 8 | source_lang = en 9 | type = PHP_INI 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 EragonJ (E.J.) 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 | -------------------------------------------------------------------------------- /bin/before-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shell = require('shelljs'); 4 | const env = process.env.NODE_ENV || 'development'; 5 | 6 | // we'll keep everything in this folder 7 | shell.exec('mkdir -p dist/release'); 8 | shell.exec('mkdir -p dist/release/mac'); 9 | shell.exec('mkdir -p dist/release/linux'); 10 | shell.exec('mkdir -p dist/release/windows'); 11 | shell.exec('mkdir -p dist/release/windows32'); 12 | shell.exec('mkdir -p release'); 13 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shell = require('shelljs'); 4 | const env = process.env.NODE_ENV || 'development'; 5 | 6 | // compile all base stuffs first 7 | shell.exec('NODE_ENV=' + env + ' npm run compile'); 8 | 9 | // start to create builds 10 | shell.exec('rm -rf dist'); 11 | shell.exec('npm run beforebuild'); 12 | shell.exec('npm run build:mac'); 13 | shell.exec('npm run build:linux'); 14 | shell.exec('npm run build:windows'); 15 | -------------------------------------------------------------------------------- /bin/build-linux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shell = require('shelljs'); 4 | 5 | shell.exec('./node_modules/.bin/build --linux --x64 --ia32 -p onTagOrDraft'); 6 | -------------------------------------------------------------------------------- /bin/build-mac: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shell = require('shelljs'); 4 | const colors = require('colors/safe'); 5 | 6 | const env = process.env.NODE_ENV || 'development'; 7 | const CSC_NAME = process.env.CSC_NAME; 8 | let cmd = './node_modules/.bin/build --mac -p onTagOrDraft'; 9 | 10 | if (CSC_NAME) { 11 | cmd = `CSC_NAME='${CSC_NAME}' ` + cmd; 12 | } 13 | 14 | if (env === 'production' && !CSC_NAME) { 15 | console.log( 16 | colors.yellow(`[Note] CSC_NAME is missing when creating production build, so the build will not be code-signed`) 17 | ); 18 | } 19 | 20 | shell.exec(cmd); 21 | -------------------------------------------------------------------------------- /bin/build-windows: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shell = require('shelljs'); 4 | 5 | // 32 bit 6 | shell.exec('./node_modules/.bin/build --windows --ia32 --x64 -p onTagOrDraft'); 7 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('shelljs'); 4 | var env = process.env.NODE_ENV || 'development'; 5 | 6 | // compile everything 7 | shell.exec('npm run compile:less'); 8 | shell.exec('npm run compile:js'); 9 | shell.exec('NODE_ENV=' + env + ' npm run generate:env'); 10 | -------------------------------------------------------------------------------- /bin/generate-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var shell = require('shelljs'); 5 | 6 | if (!process.env.CI) { 7 | return; 8 | } 9 | 10 | var apiKeyNames = [ 11 | 'YOUTUBE_API_KEY', 12 | 'VIMEO_API_CLIENT_ID', 13 | 'VIMEO_API_CLIENT_SECRET', 14 | 'SOUND_CLOUD_API_CLIENT_ID', 15 | 'SOUND_CLOUD_API_CLIENT_SECRET', 16 | 'DROPBOX_API_APP_KEY', 17 | 'DROPBOX_API_APP_SECRET', 18 | 'ROLLBAR_API_KEY', 19 | 'FIREBASE_URL', 20 | 'KAKU_SERVER_URL' 21 | ]; 22 | 23 | var apiKeys = {}; 24 | apiKeyNames.forEach(function(keyName) { 25 | // prefix with K_ , so that we can easily recognize them from travis's panel 26 | apiKeys[keyName] = process.env['K_' + keyName]; 27 | }); 28 | 29 | fs.writeFileSync('config/api_config.production.json', JSON.stringify(apiKeys)); 30 | -------------------------------------------------------------------------------- /bin/generate-env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var shell = require('shelljs'); 5 | var command = shell.exec('git log --format="%H" -n 1', { 6 | silent: true 7 | }); 8 | var env = process.env.NODE_ENV || 'development'; 9 | var commitId = 'unknown'; 10 | 11 | if (command.code === 0) { 12 | // take off ending new line 13 | commitId = command.output.replace(/\r|\n/, ''); 14 | } 15 | 16 | var envInfo = { 17 | env: env, 18 | commitId: commitId 19 | }; 20 | 21 | fs.writeFileSync('env.json', JSON.stringify(envInfo)); 22 | -------------------------------------------------------------------------------- /bootup.js: -------------------------------------------------------------------------------- 1 | require('electron-reload')(__dirname); 2 | 3 | const autoUpdater = require('electron-updater').autoUpdater; 4 | const path = require('path'); 5 | const ShortcutManager = require('electron-localshortcut'); 6 | 7 | const { 8 | app, 9 | BrowserWindow, 10 | globalShortcut, 11 | ipcMain, 12 | Menu 13 | } = require('electron'); 14 | 15 | const iconsFolder = path.join(__dirname, 'src', 'public', 'images', 'icons'); 16 | const kakuIcon = path.join(iconsFolder, 'kaku.png'); 17 | 18 | // Keep a global reference of the window object, if you don't, the window will 19 | // be closed automatically when the javascript object is GCed. 20 | let mainWindow = null; 21 | 22 | class Bootup { 23 | constructor(mainWindow) { 24 | this._mainWindow = mainWindow; 25 | this._setupBrowserWindow(); 26 | } 27 | 28 | _resetMenus() { 29 | // initialize empty menus, all logics will be handled in AppMenus 30 | const menu = Menu.buildFromTemplate([]); 31 | Menu.setApplicationMenu(menu); 32 | } 33 | 34 | _setupBrowserWindow() { 35 | // This may help the black screen / option issue 36 | app.disableHardwareAcceleration(); 37 | 38 | // Quit when all windows are closed. 39 | app.on('window-all-closed', () => { 40 | if (process.platform !== 'darwin') { 41 | app.quit(); 42 | } 43 | }); 44 | 45 | app.on('activate', () => { 46 | if (!this._mainWindow) { 47 | this._spawnWindow(); 48 | } 49 | }); 50 | 51 | // This method will be called when Electron has done everything 52 | // initialization and ready for creating browser windows. 53 | app.on('ready', () => { 54 | this._resetMenus(); 55 | this._spawnWindow(); 56 | this._bindShortcuts(); 57 | this._bindAutoUpdate(); 58 | }); 59 | } 60 | 61 | _spawnWindow() { 62 | // Create the browser window. 63 | this._mainWindow = new BrowserWindow({ 64 | 'icon': kakuIcon, 65 | 'width': 1060, 66 | 'height': 600, 67 | 'minWidth': 1060, 68 | 'minHeight': 600, 69 | 'frame': false 70 | }); 71 | 72 | // and load the index.html of the app. 73 | this._mainWindow.loadURL('file://' + __dirname + '/index.html'); 74 | 75 | // Emitted when the window is closed. 76 | this._mainWindow.on('closed', () => { 77 | // Dereference the window object, usually you would store windows 78 | // in an array if your app supports multi windows, this is the time 79 | // when you should delete the corresponding element. 80 | this._mainWindow = null; 81 | }); 82 | } 83 | 84 | _bindShortcuts() { 85 | const localKeys = [ 86 | 'Escape' 87 | ]; 88 | 89 | const globalKeys = [ 90 | 'MediaNextTrack', 91 | 'MediaPreviousTrack', 92 | 'MediaPlayPause' 93 | ]; 94 | 95 | globalKeys.forEach(key => { 96 | globalShortcut.register(key, () => { 97 | this._emitShortcutEvent(this._mainWindow, key); 98 | }); 99 | }); 100 | 101 | localKeys.forEach(key => { 102 | ShortcutManager.register(key, () => { 103 | this._emitShortcutEvent(this._mainWindow, key); 104 | }); 105 | }); 106 | } 107 | 108 | _bindAutoUpdate() { 109 | autoUpdater.on('checking-for-update', (e) => { 110 | this._mainWindow.webContents.send('au-checking-for-update', e); 111 | }); 112 | 113 | autoUpdater.on('update-available', (e) => { 114 | this._mainWindow.webContents.send('au-update-available', e); 115 | }); 116 | 117 | autoUpdater.on('update-not-available', (e) => { 118 | this._mainWindow.webContents.send('au-update-not-available', e); 119 | }); 120 | 121 | autoUpdater.on('error', (e, error) => { 122 | this._mainWindow.webContents.send('au-error', e, error); 123 | }); 124 | 125 | autoUpdater.on('update-downloaded', (e, info) => { 126 | this._mainWindow.webContents.send('au-update-downloaded', e, info); 127 | }); 128 | 129 | ipcMain.on('au-check-for-update', (e, isDev) => { 130 | if (!isDev) { 131 | autoUpdater.checkForUpdates(); 132 | } 133 | }); 134 | 135 | ipcMain.on('au-quit-and-install', () => { 136 | autoUpdater.quitAndInstall(); 137 | }); 138 | } 139 | 140 | _emitShortcutEvent(win, shortcut) { 141 | if (win) { 142 | win.webContents.send('key-' + shortcut); 143 | } 144 | } 145 | } 146 | 147 | new Bootup(mainWindow); 148 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/build/background.png -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/build/icon.ico -------------------------------------------------------------------------------- /config/api_config.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "YOUTUBE_API_KEY": "AIzaSyB66TNmwSOD_hNn3AXodHmiicQShRwgLd0", 3 | "VIMEO_API_CLIENT_ID": "329bc865fa7f73e144dc7348c701e7ace07a33f9", 4 | "VIMEO_API_CLIENT_SECRET": "ZOrH9vHMo0/q+vS8X/5YFkVYFjPlfra14kYqzGhM6L1tYR6ba21+vcQ3CgpSOb+mDvpzgETRFhY2hQdXG23l2GXHQM8EWFVp97FpGK/58iGNhJpS6bQXj6qgSIHIkMoK", 5 | "SOUND_CLOUD_API_CLIENT_ID": "a0cd15b5890dd822ac212a8847aa4d72", 6 | "SOUND_CLOUD_API_CLIENT_SECRET": "c0584366dfc4b6ddbfa7817e5cf7de8a", 7 | "DROPBOX_API_APP_KEY": "gkz21dn23wuohgc", 8 | "DROPBOX_API_APP_SECRET": "ikah3z67vfjxkaq", 9 | "ROLLBAR_API_KEY": "4f8054c006fa4052b8299a8132be597a", 10 | "FIREBASE_URL": "https://kaku-development.firebaseio.com", 11 | "KAKU_SERVER_URL": "http://localhost:3000" 12 | } 13 | -------------------------------------------------------------------------------- /config/api_config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "YOUTUBE_API_KEY": "YOUR_API_KEY_HERE", 3 | "VIMEO_API_CLIENT_ID": "YOUR_VIMEO_API_CLIENT_ID_HERE", 4 | "VIMEO_API_CLIENT_SECRET": "YOUR_VIMEO_API_CLIENT_SECRET_HERE", 5 | "SOUND_CLOUD_API_CLIENT_ID": "YOUR_SOUND_CLOUD_API_CLIENT_ID_HERE", 6 | "SOUND_CLOUD_API_CLIENT_SECRET": "YOUR_SOUND_CLOUD_API_CLIENT_SECRET_HERE", 7 | "DROPBOX_API_APP_KEY": "YOUR_DROPBOX_API_APP_KEY_HERE", 8 | "DROPBOX_API_APP_SECRET": "YOUR_DROPBOX_API_APP_SECRET_HERE", 9 | "ROLLBAR_API_KEY": "YOUR_ROLLBAR_API_KEY_HERE", 10 | "FIREBASE_URL": "YOUR_FIEBASE_URL_HERE", 11 | "KAKU_SERVER_URL": "YOUR_KAKU_SERVER_URL" 12 | } 13 | -------------------------------------------------------------------------------- /config/ga_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "RESOURCE_KEY": "UA-64370244-2" 3 | } 4 | -------------------------------------------------------------------------------- /data/thanks.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Chia-Lung Chen (EragonJ)", 3 | 4 | "contributors": [ 5 | "neighborhood999", 6 | "rickychien", 7 | "ovr", 8 | "mimi89999", 9 | "aksakalli", 10 | "iursevla", 11 | "Rickgg", 12 | "distriker", 13 | "mpizza", 14 | "olgierd-on-fire", 15 | "kmindi", 16 | "nishaya" 17 | ], 18 | 19 | "translators": [ 20 | "1wise", 21 | "alvianx", 22 | "Ananaso", 23 | "Anne017", 24 | "asyard", 25 | "Azahast", 26 | "barvirm", 27 | "cakecatz", 28 | "daltonmenezes", 29 | "dimdimbe", 30 | "emyrandt", 31 | "enzo_md", 32 | "eragonj", 33 | "ericsala", 34 | "fernandosavio", 35 | "FlyingFeather", 36 | "fri", 37 | "FromBabylon", 38 | "fthuin", 39 | "fourgray", 40 | "funilrys", 41 | "GentleMime", 42 | "GilbertoCharles", 43 | "grzesjam", 44 | "Gyges", 45 | "hamzaalalach", 46 | "Hergeirs", 47 | "hgkti", 48 | "HLFH", 49 | "Hougo", 50 | "hugutux", 51 | "IvanNIeto", 52 | "Jebiel", 53 | "jigageek", 54 | "JulienMartin", 55 | "katabamia", 56 | "Kingbalou", 57 | "kmartin", 58 | "kmindi", 59 | "krystianoxpl", 60 | "leventpehlivan", 61 | "mafallahi", 62 | "maichanchinhls", 63 | "mbnoimi", 64 | "mciverza", 65 | "mentos1386", 66 | "MrDJthib", 67 | "naphthalene", 68 | "neighborhood999", 69 | "nuffmurda", 70 | "olgierd", 71 | "omaranwar", 72 | "Oussama001", 73 | "Pablohn", 74 | "pauloesteban", 75 | "seppi91", 76 | "sicay", 77 | "SirWindfield", 78 | "snevow", 79 | "SolidSnake", 80 | "sthefanicristin_gtc_ZGQ3YW", 81 | "tarbib", 82 | "taniadaniela", 83 | "thiagoserra", 84 | "tiagohm", 85 | "tomsik68", 86 | "Troppax", 87 | "Trudko", 88 | "txelu", 89 | "ultraconformist", 90 | "vherckb", 91 | "vstoykov", 92 | "wiiliam", 93 | "willov", 94 | "yunusem", 95 | "yurayko" 96 | ], 97 | 98 | "others": [ 99 | "李枘鋒", 100 | "郭俊仁", 101 | "鄭煒霖", 102 | "香菇", 103 | "BClai Alexia", 104 | "王鼎元", 105 | "邱冠嘉", 106 | "茹憶", 107 | "陳羿瀚", 108 | "Zhang Jin-Yen", 109 | "程柏涵", 110 | "Amy Cheng", 111 | "黃娜娜", 112 | "Jim Yeh", 113 | "馬邦軒", 114 | "Shu-Hsuan Chen" 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kaku 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

Loading...

17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/logo.png -------------------------------------------------------------------------------- /misc/linux/kaku.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.8.5 4 | Name=Kaku 5 | Comment=The next generation music client http://kaku.rocks 6 | Exec=/usr/bin/kaku 7 | Icon=/usr/share/kaku/logo.png 8 | Terminal=false 9 | Type=Application 10 | Categories=Application;Audio;AudioVideo; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kaku", 3 | "version": "2.0.2", 4 | "description": "The next generation music client", 5 | "main": "bootup.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/EragonJ/Kaku" 9 | }, 10 | "keywords": [ 11 | "Kaku", 12 | "music", 13 | "player", 14 | "youtube" 15 | ], 16 | "author": { 17 | "name": "Chia-Lung, Chen (EragonJ)", 18 | "email": "eragonj@eragonj.me", 19 | "url": "https://eragonj.me" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/EragonJ/Kaku/issues" 24 | }, 25 | "homepage": "https://github.com/EragonJ/Kaku", 26 | "scripts": { 27 | "dev": "./node_modules/.bin/webpack --watch", 28 | "start": "./node_modules/.bin/electron .", 29 | "compile:less": "./node_modules/.bin/lessc src/public/less/index.less src/public/css/index.css", 30 | "compile:js": "./node_modules/.bin/webpack", 31 | "compile": "./bin/compile", 32 | "generate:env": "./bin/generate-env", 33 | "generate:config": "./bin/generate-config", 34 | "build:mac": "./bin/build-mac", 35 | "build:linux": "./bin/build-linux", 36 | "build:windows": "./bin/build-windows", 37 | "build": "NODE_ENV=production ./bin/build", 38 | "build:development": "NODE_ENV=development ./bin/build", 39 | "beforebuild": "./bin/before-build", 40 | "release": "npm run build", 41 | "test": "./node_modules/.bin/mocha -u tdd -t 5000 --reporter dot --compilers js:babel-core/register --require ./tests/unit/setup.js 'tests/unit/*.test.js'", 42 | "uitest-all": "npm run-script prepare-build && npm run-script uitest", 43 | "uitest": "./node_modules/.bin/mocha -u tdd -t 15000 --reporter dot tests/ui/*.test.js" 44 | }, 45 | "build": { 46 | "appId": "com.kaku.kaku-desktop", 47 | "publish": [ 48 | { 49 | "provider": "github", 50 | "vPrefixedTagName": false 51 | } 52 | ], 53 | "files": [ 54 | "**/*", 55 | "!dist/**/*", 56 | "!tests/**/*", 57 | "!kaku/**/*", 58 | "!bin/**/*" 59 | ], 60 | "mac": { 61 | "category": "public.app-category.music" 62 | }, 63 | "dmg": { 64 | "contents": [ 65 | { 66 | "x": 410, 67 | "y": 190, 68 | "type": "link", 69 | "path": "/Applications" 70 | }, 71 | { 72 | "x": 130, 73 | "y": 190 74 | } 75 | ] 76 | }, 77 | "win": { 78 | "icon": "build/icon.ico" 79 | }, 80 | "linux": { 81 | "target": [ 82 | "tar.gz", 83 | "AppImage", 84 | "deb" 85 | ], 86 | "extraFiles": [ 87 | "misc/${os}/kaku.desktop", 88 | "logo.png" 89 | ] 90 | } 91 | }, 92 | "devDependencies": { 93 | "babel-core": "^6.25.0", 94 | "babel-loader": "^6.4.1", 95 | "babel-preset-es2015": "^6.18.0", 96 | "babel-preset-react": "^6.24.1", 97 | "chai": "^4.0.2", 98 | "chai-as-promised": "^7.1.1", 99 | "chromedriver": "^2.21.2", 100 | "colors": "^1.1.2", 101 | "electron": "^1.8.4", 102 | "electron-builder": "^20.26.0", 103 | "electron-reload": "^1.4.0", 104 | "jsdom": "^11.1.0", 105 | "json-loader": "^0.5.4", 106 | "less": "^2.7.1", 107 | "mocha": "^3.4.2", 108 | "pouchdb-adapter-memory": "^6.2.0", 109 | "proxyquire": "^1.7.4", 110 | "shelljs": "^0.6.0", 111 | "sinon": "^1.17.3", 112 | "webdriverio": "^4.0.1", 113 | "webpack": "^1.12.12" 114 | }, 115 | "dependencies": { 116 | "animate.css": "^3.5.1", 117 | "bootbox": "^4.4.0", 118 | "bootstrap": "^3.3.6", 119 | "bootstrap-notify": "^3.1.3", 120 | "castv2-client": "^1.1.1", 121 | "classnames": "2.2.5", 122 | "dropbox": "EragonJ/dropbox-sdk-js", 123 | "electron-cookies": "^1.1.0", 124 | "electron-localshortcut": "^0.6.0", 125 | "electron-updater": "^4.0.0", 126 | "emoji-mart": "^0.3.5", 127 | "firebase": "^2.4.0", 128 | "font-awesome": "^4.4.0", 129 | "jquery": "^3.3.1", 130 | "kaku-core": "^0.0.15", 131 | "mdns-js": "^0.5.0", 132 | "node-itunes-rss-data": "^1.1.1", 133 | "node-soundcloud": "0.0.5", 134 | "pouchdb-browser": "^6.2.0", 135 | "prop-types": "^15.5.10", 136 | "react": "^15.3.1", 137 | "react-dom": "^15.3.1", 138 | "react-emoji": "^0.4.4", 139 | "react-tooltip": "^3.2.2", 140 | "reactfire": "^0.7.0", 141 | "request": "^2.69.0", 142 | "request-progress": "^2.0.1", 143 | "rollbar-browser": "^1.9.4", 144 | "universal-analytics": "^0.3.9", 145 | "uuid": "^3.1.0", 146 | "validator": "^5.2.0", 147 | "video.js": "^4.12.12", 148 | "vimeo": "^1.1.4", 149 | "youtube-node": "EragonJ/youtube-node" 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/locales/ar.ini: -------------------------------------------------------------------------------- 1 | # global app stuff 2 | app_title_normal=Kaku 3 | app_title_playing=Kaku - يقرأ {{name}} 4 | app_menu_kaku=Kaku 5 | app_menu_about_kaku=حول Kaku 6 | app_menu_sevices=خدمات 7 | app_menu_hide_kaku=إخفاء Kaku 8 | app_menu_hide_others=إخفاء الكل 9 | app_menu_show_all=عرض الكل 10 | app_menu_quit=خروج 11 | app_menu_edit=تحرير 12 | app_menu_undo=تراجع 13 | app_menu_redo=استعادة 14 | app_menu_cut=قص 15 | app_menu_copy=نسخ 16 | app_menu_paste=لصق 17 | app_menu_select_all=تحديد الكل 18 | app_menu_view=عرض 19 | app_menu_reload=إعادة تحميل 20 | app_menu_search_track=بحث عن مسار 21 | app_menu_toggle_devtools=نمط أدوات التطوير 22 | app_menu_control=تحكم 23 | app_menu_play_previous_track=قراءة المسار السابق 24 | app_menu_play_or_pause_track=قراءة أو إيقاف مؤقت للمسار 25 | app_menu_play_next_track=قراءة المسار التالي 26 | app_menu_increase_volume=زيادة حجم الصوت 27 | app_menu_decrease_volume=إنقاص حجم الصوت 28 | app_menu_download_track=تحميل المسار 29 | app_menu_window=نافذة 30 | app_menu_minimize=تصغير 31 | app_menu_close=إغلاق 32 | app_menu_bring_all_to_front=جلب الكل للأمام 33 | 34 | # sidebar 35 | sidebar_home=الرئيسية 36 | sidebar_search_results=عرض النتائج 37 | sidebar_news=آخر الأخبار 38 | sidebar_history=محفوظات 39 | sidebar_settings=إعدادات 40 | sidebar_add_playlist=إضافة لقائمة القراءة 41 | sidebar_online_dj=دي جيه 42 | sidebar_about=حول Kaku 43 | 44 | # player 45 | player_play_previous_track=قراءة المسار السابق 46 | player_play_or_pause_track=قراءة أو إيقاف مؤقت للمسار 47 | player_play_next_track=قراءة المسار التالي 48 | player_repeat_no=بدون تكرار 49 | player_repeat_one=تكرار المسار الحالي 50 | player_repeat_all=تكرار كل المسارات 51 | player_open_in_browser=فتح بالمتصفح 52 | player_show_lyrics=عرض الكلمات 53 | player_toggle_tv_mode=نمط التلفاز 54 | player_cast_to_device=بث لجهاز 55 | player_to_cast_prompt=عثرت على {{name}}، هل تود البث له؟ 56 | player_no_device_found_alert=عذراً، لم أعثر على أي جهاز للبث له! 57 | 58 | # top-ranking 59 | topranking_header=أعلى ترتيب 60 | 61 | # search 62 | search_header=نتائج البحث 63 | 64 | # news 65 | news_header=آخر الأخبار 66 | 67 | # history 68 | history_header=محفوظات 69 | history_clean_all=تنظيف الكل 70 | 71 | # settings 72 | settings_header=إعدادات 73 | settings_option_desktop_notificaion_enabled=تمكين تنبيهات سطح المكتب 74 | settings_option_always_on_top_enabled=تمكين بالأعلى دائماً 75 | settings_option_default_language=اللغة الافتراضية 76 | settings_option_default_searcher=الباحث الافتراضية 77 | settings_option_default_track_format=صيغة المسار الافتراضية 78 | settings_option_default_top_ranking_country=أعلى ترتيب الافتراضي 79 | settings_option_reset_database=تصفير قاعدة البيانات 80 | settings_option_update_player=تحديث القارئ 81 | settings_option_backup=نسخ احتياطي 82 | settings_option_choose_backup_method=اختيار طريقة النسخ الاحتياطي 83 | settings_option_backup_to_local=نسخ احتياطي محلي 84 | settings_option_sync_data_from_local=مزامنة من جهازك 85 | settings_option_backup_to_dropbox=نسخ احتياطي لـDropbox 86 | settings_option_sync_data_from_dropbox=مزامنة من Dropbox 87 | settings_option_sync_data_confirm=كل قوائم القراءة (بما فيها المسارات) سيتم استبدالها بالبيانات الجديدة، هل أنت متأكد من مزامنة البيانات؟ 88 | settings_option_reset_database_confirm=هل أنت متأكد من تصفير قاعدة البيانات؟ 89 | settings_option_best_video_format=أفضل فيديو 90 | settings_option_best_audio_format=أفضل صوت 91 | settings_option_enter_playlist_url_prompt=يرجى إدخال مسار قائمة القراءة 92 | settings_option_import_playlist=استيراد قائمة القراءة 93 | settings_option_choose_import_playlist_method=اختر طريقة استيراد قائمة القراءة 94 | settings_option_import_youtube_playlist=استيراد قائمة قراءة Youtube 95 | settings_option_searcher_youtube=Youtube 96 | settings_option_searcher_vimeo=Vimeo 97 | settings_option_searcher_soundcloud=SoundCloud 98 | settings_option_searcher_audius=Audius 99 | settings_option_searcher_all=الكل (بطيء) 100 | 101 | # online-dj 102 | online_dj_header=دي جيه 103 | online_dj_be_a_dj=كن دي جيه 104 | online_dj_be_a_guest=كن ضيف 105 | online_dj_dj_intro=إن كنت دي جيه، أي ضيف دخل غرفتك سيستمع للمسارات التي تقوم بقراءتها كذلك أي مشارك بنفس الغرفة سيكون قادراً على الدردشة معك. 106 | online_dj_guest_intro=إن كنت دي جيه، أي ضيف دخل غرفتك سيستمع للمسارات التي تقوم بقراءتها كذلك أي مشارك بنفس الغرفة سيكون قادراً على الدردشة. 107 | online_dj_role_is_dj_your_name=لقبك 108 | online_dj_role_is_dj_room_name=أعط غرفتك اسماً 109 | online_dj_role_is_dj_create_room=إنشاء غرفة 110 | online_dj_role_is_guest_your_name=لقبك 111 | online_dj_role_is_guest_room_key=مفتاح الغرفة 112 | online_dj_role_is_guest_join_room=انضم لغرفة 113 | online_dj_dashboard_your_name=لقبك 114 | online_dj_dashboard_room_key=مفتاح الغرفة 115 | online_dj_dashboard_click_to_copy_key=تم نسخ مفتاحك :) 116 | 117 | # about 118 | about_header=حول Kaku 119 | ; about_intro=Hi all, we are glad that you guys love Kaku and use it in your daily life ! As you may know, Kaku is an open source project focuses on providing an simple interface to users to listen musics from existed online platform like YouTube, Vimeo ... etc. If you do enjoy this Kaku experience, please don't be shy to help us click below buttons and spread some words to the world to make more people notice this applcation ! Thanks :) 120 | about_option_support_intro=ادعمنا بواسطة Bitcoin 121 | about_option_support_button_wording=تبرع لنا 122 | about_option_support_wallet_address=عنوان محفظة Bitcoin 123 | about_option_support_copy_wallet_address=نسخ 124 | about_option_support_click_to_copy_wallet_address_alert=شكراً لدعم Kaku :) 125 | about_option_facebook_intro=امنحنا إعجاباً في Facebook 126 | about_option_facebook_button_wording=صفحة Facebook 127 | about_option_twitter_intro=تويتر ساعدنا 128 | about_option_twitter_button_wording=تويتر لـ Kaku 129 | about_option_github_intro=هل أنت مبرمج؟ 130 | about_option_github_button_wording=ساعدنا بحل ثغرة 131 | about_option_bug_intro=هل عثرت على ثغرة؟ 132 | about_option_bug_button_wording=إبلاغ عن ثغرات 133 | about_option_comment_intro=دعنا نتحدث 134 | about_option_comment_button_wording=انضم لقناتنا على Gitter 135 | about_option_facebook_dm_intro=اترك رسالة لنا 136 | about_option_facebook_dm_button_wording=رسالة Facebook 137 | about_option_special_thanks_intro=شكر خاص 138 | about_option_special_thanks_button_wording=يا شباب أنتم رائعون 139 | about_option_qa_intro=تلميحات لـ Kaku 140 | about_option_qa_button_wording=أسئلة متكرّرة 141 | 142 | # notifier 143 | notifier_playlist=قائمة قراءة 144 | notifier_input_playlist_name=يرجى منح قائمة القراءة هذه اسماً 145 | 146 | # chatroom 147 | chatroom_header=غرفة الدردشة 148 | chatroom_comment_no_data=لا توجد رسائل 149 | chartoom_comment_form_submit=تقديم 150 | 151 | # component 152 | component_play_all=قراءة الكل 153 | component_no_track=لا يوجد مسار 154 | component_connection_lost=لا يوجد اتصال، يرجى فحص حالة الشبكة لديك. 155 | -------------------------------------------------------------------------------- /src/locales/ko.ini: -------------------------------------------------------------------------------- 1 | # global app stuff 2 | app_title_normal=Kaku 3 | app_title_playing=Kaku - {{name}} 4 | app_menu_kaku=Kaku 5 | app_menu_edit=편집 6 | app_menu_view=보기 7 | app_menu_search_track=음악 검색 8 | app_menu_control=제어 9 | app_menu_play_previous_track=이전 음악 재생 10 | app_menu_play_or_pause_track=재생 또는 일시중지 11 | app_menu_play_next_track=다음 음악 재생 12 | app_menu_increase_volume=소리 높이기 13 | app_menu_decrease_volume=소리 낮추기 14 | app_menu_download_track=음악 다운로드 15 | app_menu_window=윈도우 16 | app_menu_where_to_download_trak=다운로드 위치를 지정하세요. 17 | app_menu_start_download_track=다운로드 18 | app_menu_start_download_track_success=다운로드 완료! 19 | app_menu_start_download_track_error=다시 시도하세요 20 | 21 | app_tray_show_or_minimize=보여주기 또는 최소화 22 | app_tray_play_previous_track=이전 곡 재생 23 | app_tray_play_next_track=다음 곡 재생 24 | app_tray_play_or_pause_track=음악 재생 또는 일시중지 25 | app_tray_help=도움말 26 | app_tray_docs=문서 27 | app_tray_issue=이슈 28 | app_tray_quit=종료 29 | 30 | # sidebar 31 | sidebar_home=메인 32 | sidebar_search_results=검색 결과 33 | sidebar_play_queue= 재생 대기열 34 | sidebar_news=최근 소식 35 | sidebar_history=재생 기록 36 | sidebar_settings=설정 37 | sidebar_add_playlist=재생목록 추가 38 | sidebar_online_dj=온라인 DJ 39 | sidebar_about=Kaku에 관하여 40 | 41 | # player 42 | player_play_previous_track=이전 음악 재생 43 | player_play_or_pause_track=재생 또는 일시중지 44 | player_play_next_track=다음 음악 재생 45 | player_repeat_no=반복하지 않음 46 | player_repeat_one= 현재 음악 반복 47 | player_repeat_all=모든 음악 반복 48 | player_repeat_random=무작위 곡 재생 49 | player_open_in_browser=브라우저에서 보기 50 | player_show_lyrics=가사 보기 51 | player_toggle_tv_mode=TV 모드 토글 52 | player_cast_to_device=장치로 캐스트하기 53 | player_to_cast_prompt={{name}}을 찾았습니다. 캐스트합니까? 54 | player_no_device_found_alert=캐스트 장치를 찾지 못했습니다! 55 | 56 | # top-ranking 57 | topranking_header=인기 음악 58 | 59 | # search 60 | search_header=검색 결과 61 | 62 | # play-queue 63 | play_queue_header=재생 대기열 64 | 65 | # news 66 | news_header=최근 소식 67 | 68 | # history 69 | history_header=기록 70 | history_clean_all=모두 지우기 71 | 72 | # settings 73 | settings_header=설정 74 | settings_option_desktop_notificaion_enabled=바탕화면 알림 활성화 75 | settings_option_always_on_top_enabled=항상 맨 위에 76 | settings_option_chatroom_enabled=채팅방 활성화 77 | settings_option_default_language=언어 78 | settings_option_default_searcher=검색 플랫폼 79 | settings_option_default_track_format=음악 포맷 80 | settings_option_default_top_ranking_country=인기 음악 국가 81 | settings_option_reset_database=데이터베이스 초기화 82 | settings_option_update_player=플레이어 업데이트 83 | settings_option_backup=백업 84 | settings_option_choose_backup_method=백업 방법 선택하기 85 | settings_option_backup_to_local=로컬에 백업 86 | settings_option_sync_data_from_local=로컬 동기화 87 | settings_option_backup_to_dropbox=드롭박스에 백업 88 | settings_option_sync_data_from_dropbox=드롭박스 동기화 89 | settings_option_sync_data_confirm=모든 재생목록(음악 포함)을 덮어씁니다. 그래도 동기화합니까? 90 | settings_option_reset_database_confirm=데이터베이스를 초기화합니까? 91 | settings_option_best_video_format=추천 비디오 92 | settings_option_best_audio_format=추천 오디오 93 | settings_option_enter_playlist_url_prompt=재생목록 url 주소를 입력하세요 94 | settings_option_import_playlist=재생목록 가져오기 95 | settings_option_choose_import_playlist_method=재생목록을 가져올 방법 선택하기 96 | settings_option_import_youtube_playlist=Youtube 재생목록 가져오기 97 | settings_option_searcher_youtube=YouTube 98 | settings_option_searcher_vimeo=Vimeo 99 | settings_option_searcher_soundcloud=SoundCloud 100 | settings_option_searcher_mixcloud=MixCloud 101 | settings_option_searcher_audius=Audius 102 | settings_option_searcher_all=전체 (느림) 103 | 104 | # online-dj 105 | online_dj_header=온라인 DJ 106 | online_dj_be_a_dj=DJ 시작하기 107 | online_dj_be_a_guest=게스트로 참여하기 108 | online_dj_dj_intro=DJ로 시작하면 여러분이 재생 중인 음악을 다른 사람들도 들을 수 있으며 함께 채팅도 가능합니다. 109 | online_dj_guest_intro=게스트로 참여하면 DJ가 재생 중인 음악들을 들을 수 있으며 함께 채팅도 가능합니다. 110 | online_dj_role_is_dj_your_name=이름 111 | online_dj_role_is_dj_room_name=방 이름 112 | online_dj_role_is_dj_create_room=방 만들기 113 | online_dj_role_is_guest_your_name=이름 114 | online_dj_role_is_guest_room_key=방 비밀번호 115 | online_dj_role_is_guest_join_room=방 입장하기 116 | online_dj_dashboard_your_name=이름 117 | online_dj_dashboard_room_key=방 비밀번호 118 | online_dj_dashboard_click_to_copy_key=비밀번호가 복사됐습니다 :) 119 | 120 | # about 121 | about_header=Kaku에 관하여 122 | about_intro=여러분, 안녕하세요. Kaku를 사랑하고 사용해 주셔서 감사합니다. 아시다시피, Kaku는 Youtube, Vimeo 등의 온라인 플랫폼에서 음악을 듣기 위해 사용하기 쉬운 인터페이스를 제공하는 오픈소스 프로젝트입니다. Kaku를 자주 사용한다면 부끄러워 말고 아래 버튼을 눌러 더 많은 사람들이 사용할 수 있도록 지원해 주세요! 감사합니다 :) 123 | about_option_support_intro=지원하기 124 | about_option_support_button_wording=비트코인으로 지원하기 125 | about_option_support_button_patreon_wording=Patreon 126 | about_option_support_wallet_address=비트코인 지갑 주소 127 | about_option_support_copy_wallet_address=복사하기 128 | about_option_support_click_to_copy_wallet_address_alert=Kaku를 지원해 주셔서 감사합니다 :) 129 | about_option_facebook_intro=좋아요 130 | about_option_facebook_button_wording=페이스북 페이지 131 | about_option_twitter_intro=트윗으로 도와주세요 132 | about_option_twitter_button_wording=Kaku 트윗하기 133 | about_option_github_intro=해커인가요? 134 | about_option_github_button_wording=패치를 만들어 주세요 135 | about_option_bug_intro=버그가 있나요? 136 | about_option_bug_button_wording=알려주세요 137 | about_option_comment_intro=대화에 참여하세요 138 | about_option_comment_button_wording=Gitter 채널에 입장하기 139 | about_option_facebook_dm_intro=메시지를 남겨주세요 140 | about_option_facebook_dm_button_wording=페이스북 메시지 141 | about_option_special_thanks_intro=감사의 말 142 | about_option_special_thanks_button_wording=여러분이 최고입니다 143 | about_option_qa_intro=Kaku 팁 144 | about_option_qa_button_wording=질문답변 확인하기 145 | 146 | # main 147 | main_autoupdate_ytdl=코어 모듈 업데이트 중(처음 실행 시 5~30초 소요) 148 | main_autoupdate_ytdl_success=업데이트 완료! 이제 즐기세요 :) 149 | main_autoupdate_ytdl_error=문제가 발생했습니다. 설정 메뉴에서 직접 업데이트하세요 150 | 151 | # notifier 152 | notifier_playlist=재생목록 153 | notifier_input_playlist_name=재생목록 이름 154 | 155 | # chatroom 156 | chatroom_header=채팅방 157 | chatroom_comment_no_data=메시지가 없습니다 158 | chartoom_comment_form_submit=보내기 159 | 160 | # component 161 | component_play_all=모두 재생하기 162 | component_no_track=비었습니다 163 | component_add_to_play_queue=재생 대기열에 추가 164 | component_connection_lost=접속이 끊겼습니다. 네트워크 상태를 확인하세요. 165 | 166 | # autoupdater 167 | autoupdater_found_update_title=새로운 업데이트가 있습니다 168 | autoupdater_found_update_message=새로운 버전 - {{version}} 버전이 있습니다. 종료하면 자동으로 설치됩니다. 169 | autoupdater_yes_button_wording=예 170 | autoupdater_no_button_wording=아니오 171 | 172 | # touchbar 173 | ; touchbar_play_pause=Play / Pause 174 | ; touchbar_play_previous_track=Previous Track 175 | ; touchbar_play_next_track=Next Track 176 | -------------------------------------------------------------------------------- /src/locales/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": [ 3 | { "label": "English", "lang": "en" }, 4 | { "label": "Spanish", "lang": "es" }, 5 | { "label": "Français", "lang": "fr" }, 6 | { "label": "Русский", "lang": "ru" }, 7 | { "label": "繁體中文", "lang": "zh-TW" }, 8 | { "label": "日本語", "lang": "ja" }, 9 | { "label": "한국어", "lang": "ko" }, 10 | { "label": "Portuguese", "lang": "pt" }, 11 | { "label": "Portuguese (Brazil)", "lang": "pt_BR" }, 12 | { "label": "Čeština", "lang": "cs" }, 13 | { "label": "Türkçe", "lang": "tr" }, 14 | { "label": "Deutsch", "lang": "de" }, 15 | { "label": "Slovenčina", "lang": "sk_SK" }, 16 | { "label": "Polski", "lang": "pl" }, 17 | { "label": "Bahasa Indonesia", "lang": "id_ID" }, 18 | { "label": "Italiano", "lang": "it" }, 19 | { "label": "Nederlands (België)", "lang": "nl_BE" }, 20 | { "label": "‏العربية‏", "lang": "ar" }, 21 | { "label": "Slovenščina", "lang": "sl_SI" }, 22 | { "label": "Dansk", "lang": "da" }, 23 | { "label": "Svenska", "lang": "sv" }, 24 | { "label": "‏فارسی‏", "lang": "fa_IR" }, 25 | { "label": "Brezhoneg", "lang": "br" }, 26 | { "label": "Nederland", "lang": "nl_NL" }, 27 | { "label": "Ελληνικά", "lang": "el" } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ini: -------------------------------------------------------------------------------- 1 | # global app stuff 2 | app_title_normal=Kaku 3 | app_title_playing=Kaku - 正在播放 {{name}} 4 | app_menu_kaku=Kaku 5 | app_menu_edit=編輯 6 | app_menu_view=檢視 7 | app_menu_search_track=搜尋歌曲 8 | app_menu_control=控制 9 | app_menu_play_previous_track=播放前一首歌曲 10 | app_menu_play_or_pause_track=播放 / 暫停 11 | app_menu_play_next_track=播放下一首歌曲 12 | app_menu_increase_volume=增加音量 13 | app_menu_decrease_volume=降低音量 14 | app_menu_download_track=下載歌曲 15 | app_menu_window=視窗 16 | app_menu_where_to_download_trak=在哪裡下載你的歌曲? 17 | app_menu_start_download_track=開始下載你的歌曲 18 | app_menu_start_download_track_success=下載完成!去確認一下吧 :) 19 | app_menu_start_download_track_error=抱歉,有東西出錯了,請重試 20 | 21 | app_tray_show_or_minimize=顯示 / 縮小視窗 22 | app_tray_play_previous_track=播放前一首歌曲 23 | app_tray_play_next_track=播放下一首歌曲 24 | app_tray_play_or_pause_track=播放 / 暫停 25 | app_tray_help=幫助 26 | app_tray_docs=文件說明 27 | app_tray_issue=問題回報 28 | app_tray_quit=結束 29 | 30 | # sidebar 31 | sidebar_home=首頁 32 | sidebar_search_results=搜尋結果 33 | sidebar_play_queue=播放列表 34 | sidebar_news=最新消息 35 | sidebar_history=歷史記錄 36 | sidebar_settings=設定 37 | sidebar_add_playlist=新增播放清單 38 | sidebar_online_dj=線上 DJ 39 | sidebar_about=關於 Kaku 40 | 41 | # player 42 | player_play_previous_track=播放前一首歌曲 43 | player_play_or_pause_track=播放 / 暫停 44 | player_play_next_track=播放下一首歌曲 45 | player_repeat_no=不重覆播放 46 | player_repeat_one=重覆播放目前歌曲 47 | player_repeat_all=重覆播放所有歌曲 48 | player_repeat_random=隨機播放歌曲 49 | player_open_in_browser=在瀏覽器開啟 50 | player_show_lyrics=顯示歌詞 51 | player_toggle_tv_mode=切換電視模式 52 | player_cast_to_device=投影到裝置上 53 | player_to_cast_prompt=發現 {{name}},確定要投影嗎 ? 54 | player_no_device_found_alert=抱歉,找不到投影裝置! 55 | 56 | # top-ranking 57 | topranking_header=熱門排行 58 | 59 | # search 60 | search_header=搜尋結果 61 | 62 | # play-queue 63 | play_queue_header=播放列表 64 | 65 | # news 66 | news_header=最新消息 67 | 68 | # history 69 | history_header=歷史記錄 70 | history_clean_all=清除記錄 71 | 72 | # settings 73 | settings_header=設定 74 | settings_option_desktop_notificaion_enabled=啟用桌面通知 75 | settings_option_always_on_top_enabled=啟用最上層顯示 76 | settings_option_chatroom_enabled=啟用聊天室 77 | settings_option_default_language=預設語言 78 | settings_option_default_searcher=預設搜尋平台 79 | settings_option_default_track_format=預設音樂格式 80 | settings_option_default_top_ranking_country=預設熱門排門榜 81 | settings_option_reset_database=重置資料庫 82 | settings_option_update_player=更新播放器 83 | settings_option_backup=備份 84 | settings_option_choose_backup_method=選擇備份方法 85 | settings_option_backup_to_local=備份資料到本機 86 | settings_option_sync_data_from_local=從本機同步資料回來 87 | settings_option_backup_to_dropbox=備份資料到 Dropbox 88 | settings_option_sync_data_from_dropbox=從 Dropbox 同步資料回來 89 | settings_option_sync_data_confirm=所有的播放清單(包含歌曲資料)將會被新的資料取代,確定要同步資料嗎? 90 | settings_option_reset_database_confirm=確定要重置資料庫嗎? 91 | settings_option_best_video_format=高畫質影片 92 | settings_option_best_audio_format=高音質音軌 93 | settings_option_enter_playlist_url_prompt=請輸入播放清單的網址 94 | settings_option_import_playlist=匯入播放清單 95 | settings_option_choose_import_playlist_method=選擇匯入方法 96 | settings_option_import_youtube_playlist=匯入 YouTube 播放清單 97 | settings_option_searcher_youtube=Youtube 98 | settings_option_searcher_vimeo=Vimeo 99 | settings_option_searcher_soundcloud=SoundCloud 100 | settings_option_searcher_mixcloud=MixCloud 101 | settings_option_searcher_audius=Audius 102 | settings_option_searcher_all=全部(較慢) 103 | 104 | # online-dj 105 | online_dj_header=線上 DJ 106 | online_dj_be_a_dj=成為 DJ 107 | online_dj_be_a_guest=成為聽眾 108 | online_dj_dj_intro=如果你是 DJ 的話,所有加入你房間的聽眾將會聽到你正在播放的歌曲。除此之外,在同一個房間的所有人可以透過聊天室一起聊天哦! 109 | online_dj_guest_intro=如果你是聽眾的話,你將可以即時聽到 DJ 正在播放的歌曲。除此之外,在同一個房間的所有人可以透過聊天室一起聊天哦! 110 | online_dj_role_is_dj_your_name=你的暱稱 111 | online_dj_role_is_dj_room_name=幫你的房間取個名字 112 | online_dj_role_is_dj_create_room=開房去 113 | online_dj_role_is_guest_your_name=你的暱稱 114 | online_dj_role_is_guest_room_key=房間鑰匙 115 | online_dj_role_is_guest_join_room=加入房間 116 | online_dj_dashboard_your_name=你的暱稱 117 | online_dj_dashboard_room_key=房間鑰匙 118 | online_dj_dashboard_click_to_copy_key=已經複製你的房間鑰匙了 :) 119 | 120 | # about 121 | about_header=關於 Kaku 122 | about_intro=嗨大家好,希望你們很喜歡 Kaku 也使用得很開心!如你們所知,Kaku 是一個開放原始碼的軟體,我們當初設計他的理念就是希望能夠提供一個簡單的介面讓使用者可以很輕鬆的聆聽各個線上平台的音樂如 YouTube 、 Vimeo … 等。如果你很享受這個特別的 Kaku 體驗,希望能幫我們按一下下面的按鈕宣傳一下,讓更多人知道這個軟體,感謝 :) 123 | about_option_support_intro=支持我們 124 | about_option_support_button_wording=用 Bitcoin 支持我們 125 | about_option_support_button_patreon_wording=用 Patreon 支持我們 126 | about_option_support_wallet_address=Bitcoin 錢包位置 127 | about_option_support_copy_wallet_address=複製 128 | about_option_support_click_to_copy_wallet_address_alert=感謝你對 Kaku 的支持 :) 129 | about_option_facebook_intro=幫我們給個 Like 130 | about_option_facebook_button_wording=我們的 Facebook Page 131 | about_option_twitter_intro=幫我們 Tweet 一下 132 | about_option_twitter_button_wording=給 Kaku 一個推 133 | about_option_github_intro=是程式設計師嗎? 134 | about_option_github_button_wording=上個 Patch 吧 135 | about_option_bug_intro=發現 Bug ? 136 | about_option_bug_button_wording=幫忙回報一下 137 | about_option_comment_intro=來聊天吧 138 | about_option_comment_button_wording=加入我們的 Gitter 頻道 139 | about_option_facebook_dm_intro=留言給我們 140 | about_option_facebook_dm_button_wording=Facebook 訊息 141 | about_option_special_thanks_intro=特別感謝 142 | about_option_special_thanks_button_wording=你們超棒 143 | about_option_qa_intro=使用 Kaku 的小提示 144 | about_option_qa_button_wording=查看 Q&A 145 | 146 | # main 147 | main_autoupdate_ytdl=正在自動更新核心模組,請稍等!(第一次會花大概 5~30 秒) 148 | main_autoupdate_ytdl_success=完成!你現在可以播音樂了 :) 149 | main_autoupdate_ytdl_error=出錯了,不過還是可以到設定裡手動更新 150 | 151 | # notifier 152 | notifier_playlist=播放清單 153 | notifier_input_playlist_name=請替此播放清單取個名字 154 | 155 | # chatroom 156 | chatroom_header=聊天室 157 | chatroom_comment_no_data=沒有訊息 158 | chartoom_comment_form_submit=送出 159 | 160 | # component 161 | component_play_all=播放全部 162 | component_no_track=目前沒有任何歌曲 163 | component_add_to_play_queue=新增到播放列表 164 | component_connection_lost=連線異常,請檢查網路狀況 165 | 166 | # autoupdater 167 | autoupdater_found_update_title=偵測到新版本 168 | autoupdater_found_update_message=偵測到新版本 - {{version}} ,將自動安裝並離開程式 169 | autoupdater_yes_button_wording=確定更新 170 | autoupdater_no_button_wording=否 171 | 172 | # touchbar 173 | ; touchbar_play_pause=Play / Pause 174 | ; touchbar_play_previous_track=Previous Track 175 | ; touchbar_play_next_track=Next Track 176 | -------------------------------------------------------------------------------- /src/modules/AppCore.js: -------------------------------------------------------------------------------- 1 | import Fs from 'fs'; 2 | import Path from 'path'; 3 | import { EventEmitter } from 'events'; 4 | const Remote = require('electron').remote; 5 | const App = Remote.app; 6 | 7 | class AppCore extends EventEmitter { 8 | constructor() { 9 | super(); 10 | this._envInfo = null; 11 | this._title = ''; 12 | 13 | Object.defineProperty(this, 'title', { 14 | enumerable: true, 15 | configurable: true, 16 | set(title) { 17 | this._title = title; 18 | this.emit('titleUpdated', title); 19 | }, 20 | get() { 21 | return this._title; 22 | } 23 | }); 24 | } 25 | 26 | isDev() { 27 | if (!this._envInfo) { 28 | this._envInfo = this.getEnvInfo(); 29 | } 30 | return this._envInfo.env === 'development'; 31 | } 32 | 33 | isProduction() { 34 | if (!this._envInfo) { 35 | this._envInfo = this.getEnvInfo(); 36 | } 37 | return this._envInfo.env === 'production'; 38 | } 39 | 40 | getEnvInfo() { 41 | const envFilePath = Path.join(App.getAppPath(), 'env.json'); 42 | const envInfo = Fs.readFileSync(envFilePath, 'utf8'); 43 | return JSON.parse(envInfo); 44 | } 45 | 46 | getPackageInfo() { 47 | const packageFilePath = Path.join(App.getAppPath(), 'package.json'); 48 | const packageInfo = Fs.readFileSync(packageFilePath, 'utf8'); 49 | return JSON.parse(packageInfo); 50 | } 51 | 52 | getInfoFromDataFolder(filename) { 53 | const filePath = Path.join(App.getAppPath(), 'data', filename); 54 | const fileInfo = Fs.readFileSync(filePath, 'utf8'); 55 | return JSON.parse(fileInfo); 56 | } 57 | } 58 | 59 | module.exports = new AppCore(); 60 | -------------------------------------------------------------------------------- /src/modules/AutoUpdater.js: -------------------------------------------------------------------------------- 1 | import Electron from 'electron'; 2 | const IpcRenderer = Electron.ipcRenderer; 3 | const Remote = Electron.remote; 4 | const App = Remote.app; 5 | const Dialog = Remote.dialog; 6 | 7 | import os from 'os'; 8 | import Path from 'path'; 9 | import AppCore from './AppCore'; 10 | import Constants from './Constants'; 11 | import L10nManager from './L10nManager'; 12 | import { Downloader } from 'kaku-core/modules/YoutubeDL'; 13 | 14 | const _ = L10nManager.get.bind(L10nManager); 15 | const ytdlDownloader = new Downloader(); 16 | ytdlDownloader.setPath(App.getPath('userData')) 17 | 18 | class AutoUpdater { 19 | constructor() { 20 | if (AppCore.isDev()) { 21 | return; 22 | } 23 | 24 | if (os.platform() !== 'darwin' && os.platform() !== 'win32') { 25 | return; 26 | } 27 | 28 | IpcRenderer.on('au-checking-for-update', (e) => { 29 | console.log('found a new update'); 30 | }); 31 | 32 | IpcRenderer.on('au-update-available', (e) => { 33 | console.log('update is available'); 34 | }); 35 | 36 | IpcRenderer.on('au-update-not-available', (e) => { 37 | console.log('update not available'); 38 | }); 39 | 40 | IpcRenderer.on('au-error', (e, error) => { 41 | console.log(error); 42 | }); 43 | 44 | IpcRenderer.on('au-update-downloaded', (e, info) => { 45 | let {releaseName} = info; 46 | 47 | console.log('update downloaded'); 48 | 49 | let title = _('autoupdater_found_update_title'); 50 | let message = _('autoupdater_found_update_message', { 51 | version: releaseName 52 | }); 53 | 54 | Dialog.showMessageBox({ 55 | type: 'question', 56 | title: title, 57 | message: message, 58 | buttons: [ 59 | _('autoupdater_yes_button_wording'), 60 | _('autoupdater_no_button_wording') 61 | ], 62 | cancelId: -1 63 | }, (response) => { 64 | if (response === 0) { 65 | IpcRenderer.send('au-quit-and-install'); 66 | } 67 | }); 68 | }); 69 | } 70 | 71 | updateApp() { 72 | IpcRenderer.send('au-check-for-update', AppCore.isDev()); 73 | } 74 | 75 | updateYoutubeDl(force=false) { 76 | return ytdlDownloader.save(os.platform(), force); 77 | } 78 | } 79 | 80 | module.exports = new AutoUpdater(); 81 | -------------------------------------------------------------------------------- /src/modules/Constants.js: -------------------------------------------------------------------------------- 1 | let Constants = {}; 2 | 3 | try { 4 | Constants.API = require('../../config/api_config.production.json'); 5 | Constants.GA = require('../../config/ga_config.json'); 6 | } 7 | catch(e) { 8 | Constants.API = {}; 9 | Constants.GA = {}; 10 | } 11 | 12 | Constants.KEY_MAP = { 13 | 8: 'DELETE', 14 | 13: 'ENTER', 15 | 32: 'SPACE', 16 | 27: 'ESC', 17 | 37: 'ARROW_LEFT', 18 | 38: 'ARROW_UP', 19 | 39: 'ARROW_RIGHT', 20 | 40: 'ARROW_DOWN' 21 | }; 22 | 23 | // If users forget to change from *.sample.json to *.production.json, 24 | // require will throw out error here, because if we don't have these 25 | // configs here, our program will be broken somehow. In this way, we should 26 | // make sure it does exist before running Kaku. 27 | module.exports = Constants; 28 | -------------------------------------------------------------------------------- /src/modules/Database.js: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb-browser'; 2 | let opt = {}; 3 | 4 | if (global && global.IS_TEST === true) { 5 | PouchDB.plugin(require('pouchdb-adapter-memory')); 6 | opt.adapter = 'memory'; 7 | } 8 | else { 9 | opt.adapter = 'idb'; 10 | } 11 | 12 | const KakuDB = new PouchDB('kaku', opt); 13 | 14 | // Note: 15 | // Because we add something new in the prototype chain and this is not safe, 16 | // we added some prefix to make it unique 17 | PouchDB.prototype.resetDatabase = function() { 18 | return this.destroy().catch((error) => { 19 | console.log('Something goes wrong when dropping database'); 20 | console.log(error); 21 | }); 22 | }; 23 | 24 | module.exports = KakuDB; 25 | -------------------------------------------------------------------------------- /src/modules/DownloadManager.js: -------------------------------------------------------------------------------- 1 | // TODO 2 | // 1. handle multiple downloads 3 | // 2. support pause / canel downloads 4 | // ... etc 5 | import Fs from 'fs'; 6 | import Request from 'request'; 7 | import RequestProgress from 'request-progress'; 8 | import { EventEmitter } from 'events'; 9 | 10 | class DownloadManager extends EventEmitter { 11 | constructor() { 12 | super(); 13 | 14 | this._cssAnimationTime = 1000; 15 | this._downloads = []; 16 | } 17 | 18 | download(src, path) { 19 | const req = RequestProgress(Request.get(src), { 20 | delay: 1000 // start to emit after 1000ms delay 21 | }); 22 | 23 | req.on('progress', (state) => { 24 | this.emit('download-progress', Math.floor(state.percentage * 100)); 25 | }) 26 | .on('error', (error) => { 27 | console.log('error when requesting file'); 28 | console.log(error); 29 | this.emit('download-error'); 30 | }) 31 | .pipe(Fs.createWriteStream(path)) 32 | .on('error', (error) => { 33 | console.log('error when saving file to path' + path); 34 | console.log(error); 35 | this.emit('download-error'); 36 | }) 37 | .on('close', () => { 38 | const index = this._downloads.indexOf(req); 39 | if (index >= 0) { 40 | this._downloads.splice(index, 1); 41 | } 42 | 43 | this.emit('download-progress', 100); 44 | 45 | setTimeout(() => { 46 | this.emit('download-finish'); 47 | }, this._cssAnimationTime); 48 | }); 49 | 50 | this._downloads.push(req); 51 | return req; 52 | } 53 | } 54 | 55 | module.exports = new DownloadManager(); 56 | -------------------------------------------------------------------------------- /src/modules/ErrorMonitor.js: -------------------------------------------------------------------------------- 1 | import Constants from './Constants'; 2 | import AppCore from './AppCore'; 3 | import Rollbar from 'rollbar-browser'; 4 | 5 | const env = AppCore.isDev() ? 'development' : 'production'; 6 | const rollbarConfig = { 7 | accessToken: Constants.API.ROLLBAR_API_KEY, 8 | captureUncaught: true, 9 | captureUnhandledRejections: true, 10 | payload: { 11 | environment: env, 12 | } 13 | }; 14 | 15 | module.exports = { 16 | init: function() { 17 | if (env === 'production') { 18 | return Rollbar.init(rollbarConfig); 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/modules/HistoryManager.js: -------------------------------------------------------------------------------- 1 | import DB from './Database'; 2 | import { EventEmitter } from 'events'; 3 | import BaseTrack from 'kaku-core/models/track/BaseTrack'; 4 | 5 | class HistoryManager extends EventEmitter { 6 | constructor() { 7 | super(); 8 | this._tracks = []; 9 | this._initializedPromise = null; 10 | 11 | Object.defineProperty(HistoryManager.prototype, 'tracks', { 12 | enumerable: true, 13 | configurable: false, 14 | get() { 15 | return this._tracks; 16 | } 17 | }); 18 | } 19 | 20 | ready() { 21 | return this.init(); 22 | } 23 | 24 | init() { 25 | if (this._initializedPromise) { 26 | return this._initializedPromise; 27 | } else { 28 | this._initializedPromise = DB.get('history').catch((error) => { 29 | if (error.status === 404) { 30 | return DB.put({ 31 | _id: 'history', 32 | tracks: [] 33 | }); 34 | } else { 35 | throw error; 36 | } 37 | }) 38 | .then((doc) => { 39 | let tracks = doc.tracks || []; 40 | this._tracks = tracks.map((rawTrack) => { 41 | return BaseTrack.fromJSON(rawTrack); 42 | }); 43 | }) 44 | .then(() => { 45 | // bind needed events for these playlists 46 | this.on('history-updated', () => { 47 | this._storeTracksToDB(); 48 | }); 49 | }) 50 | .catch((error) => { 51 | console.log(error); 52 | }); 53 | 54 | return this._initializedPromise; 55 | } 56 | } 57 | 58 | add(track) { 59 | if (!this._hasTrack(track)) { 60 | this._tracks.unshift(track); 61 | this.emit('history-updated', this._tracks); 62 | } 63 | } 64 | 65 | remove(track) { 66 | if (this._hasTrack(track)) { 67 | const index = this._getTrackIndex(track); 68 | this._tracks.splice(index, 1); 69 | this.emit('history-updated', this._tracks); 70 | } 71 | } 72 | 73 | clean() { 74 | this._tracks = []; 75 | this.emit('history-updated', this._tracks); 76 | } 77 | 78 | _hasTrack(track) { 79 | const index = this._getTrackIndex(track); 80 | return index !== -1; 81 | } 82 | 83 | _getTrackIndex(track) { 84 | return this._tracks.indexOf(track); 85 | } 86 | 87 | _storeTracksToDB() { 88 | return DB.get('history').then((doc) => { 89 | return DB.put({ 90 | _id: 'history', 91 | _rev: doc._rev, 92 | tracks: this._tracks.map((track) => { 93 | return track.toJSON(); 94 | }) 95 | }); 96 | }) 97 | .catch((error) => { 98 | console.log(error); 99 | }); 100 | } 101 | } 102 | 103 | module.exports = new HistoryManager(); 104 | -------------------------------------------------------------------------------- /src/modules/L10nManager.js: -------------------------------------------------------------------------------- 1 | import Fs from 'fs'; 2 | import Path from 'path'; 3 | import { EventEmitter } from 'events'; 4 | import IniParser from 'kaku-core/modules/IniParser'; 5 | const L10nMetadata = require('../locales/metadata').languages; 6 | const Remote = require('electron').remote; 7 | const App = Remote.app; 8 | 9 | class L10nManager extends EventEmitter { 10 | constructor() { 11 | super(); 12 | this._cachedStrings = {}; 13 | this._currentLanguage = 'en'; 14 | this._reParam = /\{\{\s*(\w+)\s*\}\}/g; 15 | 16 | // Because might have a lot of L10nSpan, so we should de-limit the number 17 | // of listeners here 18 | this.setMaxListeners(0); 19 | 20 | // prepare all needed strings 21 | L10nMetadata.forEach((language) => { 22 | let fileName = language.lang + '.ini'; 23 | let languageFilePath = Path.join( 24 | App.getAppPath(), 'src', 'locales', fileName 25 | ); 26 | let rawIniData = Fs.readFileSync(languageFilePath, 'utf-8'); 27 | this._cachedStrings[language.lang] = IniParser.parse(rawIniData); 28 | }, this); 29 | 30 | this.emit('language-initialized'); 31 | } 32 | 33 | changeLanguage(newLanguage) { 34 | if (this._currentLanguage === newLanguage) { 35 | return; 36 | } 37 | else { 38 | let oldLanguage = this._currentLanguage; 39 | this._currentLanguage = newLanguage; 40 | this.emit('language-changed', newLanguage, oldLanguage); 41 | } 42 | } 43 | 44 | get(id, params, fallbackToEn) { 45 | let currentLanguage = this._currentLanguage; 46 | 47 | if (fallbackToEn) { 48 | currentLanguage = 'en'; 49 | } 50 | 51 | const rawString = this._cachedStrings[currentLanguage][id]; 52 | const replacedString = this._getReplacedString(rawString, params); 53 | 54 | if (!replacedString) { 55 | console.error( 56 | 'You are accessing a non-exist l10nId : ', id, 57 | ' in lang: ', currentLanguage); 58 | 59 | // If we still find nothing in `en`, we should exit directly. 60 | if (fallbackToEn) { 61 | return ''; 62 | } 63 | else { 64 | return this.get(id, params, true); 65 | } 66 | } 67 | else { 68 | return replacedString; 69 | } 70 | } 71 | 72 | getSupportedLanguages() { 73 | return L10nMetadata; 74 | } 75 | 76 | _getReplacedString(rawString, params) { 77 | let replacedString = rawString; 78 | let matched; 79 | let replacedParam; 80 | let foundError = false; 81 | 82 | do { 83 | matched = this._reParam.exec(replacedString); 84 | if (matched) { 85 | let matchedBracketSubject = matched[0]; 86 | let matchedParamKey = matched[1]; 87 | 88 | if (matchedBracketSubject && matchedParamKey) { 89 | replacedParam = params[matchedParamKey]; 90 | 91 | // we find a {{ xxx }} block, 92 | // but there is no related key to replace it 93 | if (!replacedParam) { 94 | // in order not to get stucked in infinite loop, let's make sure 95 | // we would jump out. 96 | foundError = true; 97 | console.log('we can\'t find related param - ', matchedParamKey, 98 | ' to replace it, please check your passing params again'); 99 | } 100 | else { 101 | replacedString = replacedString.replace( 102 | matchedBracketSubject, replacedParam); 103 | } 104 | } 105 | } 106 | } while(matched && !foundError); 107 | 108 | return replacedString; 109 | } 110 | } 111 | 112 | module.exports = new L10nManager(); 113 | -------------------------------------------------------------------------------- /src/modules/NewsFetcher.js: -------------------------------------------------------------------------------- 1 | import Request from 'request'; 2 | 3 | class NewsFetcher { 4 | constructor() { 5 | this._newsLink = 'https://kaku.rocks/news.json'; 6 | } 7 | 8 | get() { 9 | const promise = new Promise((resolve, reject) => { 10 | Request.get(this._newsLink, (error, response, body) => { 11 | if (error) { 12 | reject(error); 13 | console.log(error); 14 | } 15 | else { 16 | var result = JSON.parse(body); 17 | resolve(result.news); 18 | } 19 | }); 20 | }); 21 | return promise; 22 | } 23 | } 24 | 25 | module.exports = new NewsFetcher(); 26 | -------------------------------------------------------------------------------- /src/modules/PreferenceManager.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | class PreferenceManager extends EventEmitter { 4 | constructor() { 5 | super(); 6 | this._preferenceStorage = window.localStorage; 7 | } 8 | 9 | setPreference(key, newPreference) { 10 | const oldPreference = this.getPreference(key); 11 | if (oldPreference !== newPreference) { 12 | this._preferenceStorage[key] = newPreference; 13 | this.emit('preference-updated', key, newPreference, oldPreference); 14 | } 15 | } 16 | 17 | getPreference(key) { 18 | const preference = this._preferenceStorage[key]; 19 | if (preference === 'true') { 20 | return true; 21 | } 22 | else if (preference === 'false') { 23 | return false; 24 | } 25 | else if (typeof preference === 'undefined') { 26 | return undefined; 27 | } 28 | else { 29 | return preference; 30 | } 31 | } 32 | } 33 | 34 | module.exports = new PreferenceManager(); 35 | -------------------------------------------------------------------------------- /src/modules/README.md: -------------------------------------------------------------------------------- 1 | # src/modules 2 | 3 | In Kaku, `src/modules` would keep UI-free codes here. For each module, they should 4 | mainly focus on **data** and should not interact with any UI. 5 | 6 | ## src/modules/backuper/* 7 | 8 | Backuper is a module that can help users **sync data back** and **backup data out**. 9 | For each module, we should at least implement `.backup()` and `.syncDataBack()` 10 | two methods and they would be triggered by `BackuperManager` in UI. 11 | 12 | ** TODO:** add BackuperManager 13 | 14 | ## src/modules/searcher/* 15 | 16 | Because Kaku is built on top of `youtube-dl`, so we should be able to search / download any 17 | tracks from any online service it supports. Right now we do support `SoundCloud`, 18 | `Vimeo` and `YouTube`. 19 | 20 | If you want to add any additional searcher here, just follow the other file and make sure you 21 | do implement `.search()` and it will be triggered by `Searcher.js`. 22 | 23 | Reference: [Supported extractor in youtube-dl](https://github.com/rg3/youtube-dl/tree/master/youtube_dl/extractor) 24 | 25 | ## src/modules/AutoUpdater.js 26 | 27 | We implemented a small auto updater which can help us fetch latest release (from Github) and compare its version with current version. If latest release has newer version, we will find out needed zip file for user's platfom (Windows, Linux ...). 28 | 29 | ## src/modules/BaseModule.js 30 | 31 | **Should be deprecated.** 32 | 33 | ## src/modules/Constants.js 34 | 35 | We will keep all neede constants here and let other modules use. 36 | 37 | ## src/modules/Database.js 38 | 39 | Because we use pouchdb as our DB, this is just a wrapper with some customized methods. 40 | 41 | ## src/modules/Defer.js 42 | 43 | This is a wrapped Promise to make it work like jQuery defer. 44 | 45 | ## src/modules/DownloadManager.js 46 | 47 | In Kaku, we can let users download tracks, so we need this DownloadManager to control each download state and reflect its state to UI. In the future, we need to implement parallel downloads and cancel feature. 48 | 49 | ## src/modules/Dropbox.js 50 | 51 | This is a wrapper for `node-dropbox`. 52 | 53 | ## src/modules/HistoryManager.js 54 | 55 | We use this HistoryManager to store played tracks. But right now, it is not connected with database, so the history will be removed each time. We should make it stored in database in the future. 56 | 57 | ## src/modules/IniParser.js 58 | 59 | Because our locale files are written in Ini format, we write our own parser to parse Ini file and return that as JavaScript object. 60 | 61 | ## src/modules/KakuCore.js 62 | 63 | For global stuffs, we will keep them in KakuCore, but right now only use it to change our title. 64 | 65 | ## src/modules/L10nManager.js 66 | 67 | L10nManager will read all locale files from `locales/` and use `IniParser` to parse these strings. After parsing, we would keep these locales (in Object) in its cache for later use. 68 | 69 | ## src/modules/NewsFetcher.js 70 | 71 | This helps us fetch latest news from [our website](https://kaku.rocks/news.json) and let users know latest news. 72 | 73 | ## src/modules/PackageInfo.js 74 | 75 | **Should be deprecated.** 76 | 77 | ## src/modules/PlaylistManger.js 78 | 79 | PlaylistManager helps us handle CRUD actions for playlist. 80 | 81 | ## src/modules/PreferenceManager.js 82 | 83 | This is a manager which builds on top of **localstorage** to keep users' preferences like language, searcher ... etc. 84 | 85 | ## src/modules/Searcher.js (Should be renamed to SearcherManager.js) 86 | 87 | This is a manager which controls each searcher and search tracks for users. For more information, please check `Modules/Searcher/` 88 | 89 | ## src/modules/TopRanking.js 90 | 91 | After doing some survey, we noticed that iTunes would provide latest TopRankings for different countries. In additin to this, these data are updated often. Please check [iTunes' data](https://rss.itunes.apple.com) for more information. 92 | 93 | ## src/modules/Tracker.js 94 | 95 | This will leverage Google Analytics to help us track some useful data like which track is the most popular ... etc. With these data, we can make Kaku better in the future. 96 | 97 | ## src/modules/TrackInfoFetcher.js 98 | 99 | This is where we use `youtube-dl` to help us get real information (like resource link) from online service (YouTube, Vimeo ... etc). 100 | -------------------------------------------------------------------------------- /src/modules/Searcher.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import Constants from './Constants'; 3 | 4 | // searchers 5 | import YoutubeSearcher from 'kaku-core/modules/searcher/YoutubeSearcher'; 6 | import VimeoSearcher from 'kaku-core/modules/searcher/VimeoSearcher'; 7 | import SoundCloudSearcher from 'kaku-core/modules/searcher/SoundCloudSearcher'; 8 | import MixCloudSearcher from 'kaku-core/modules/searcher/MixCloudSearcher'; 9 | import AudiusSearcher from 'kaku-core/modules/searcher/AudiusSearcher' 10 | 11 | class Searcher extends EventEmitter { 12 | constructor() { 13 | super(); 14 | 15 | let self = this; 16 | 17 | // default searcher 18 | this._selectedSearcherName = 'youtube'; 19 | 20 | // supported searchers 21 | this._searchers = { 22 | 'youtube': new YoutubeSearcher({ 23 | apiKey: Constants.API.YOUTUBE_API_KEY 24 | }), 25 | 'vimeo': new VimeoSearcher({ 26 | clientId: Constants.API.VIMEO_API_CLIENT_ID, 27 | clientSecret: Constants.API.VIMEO_API_CLIENT_SECRET 28 | }), 29 | 'soundcloud': new SoundCloudSearcher({ 30 | clientId: Constants.API.SOUND_CLOUD_API_CLIENT_ID, 31 | clientSecret: Constants.API.SOUND_CLOUD_API_CLIENT_SECRET 32 | }), 33 | 'mixcloud': new MixCloudSearcher(), 34 | 'audius': new AudiusSearcher(), 35 | 'all': { 36 | search: function(keyword, limit) { 37 | let promises = [ 38 | self._searchers.youtube.search(keyword, limit), 39 | self._searchers.vimeo.search(keyword, limit), 40 | self._searchers.soundcloud.search(keyword, limit), 41 | self._searchers.mixcloud.search(keyword, limit), 42 | self._searchers.audius.search(keyword) 43 | ]; 44 | return Promise.all(promises); 45 | } 46 | } 47 | }; 48 | 49 | this._searchResults = []; 50 | } 51 | 52 | get selectedSearcher() { 53 | return this._searchers[this._selectedSearcherName]; 54 | } 55 | 56 | get searchResults() { 57 | return this._searchResults; 58 | } 59 | 60 | getSupportedSearchers() { 61 | let promise = new Promise((resolve) => { 62 | resolve(Object.keys(this._searchers)); 63 | }); 64 | return promise; 65 | } 66 | 67 | search(keyword, limit, toSave = false) { 68 | if (!keyword) { 69 | return Promise.resolve([]); 70 | } 71 | else { 72 | return this.selectedSearcher.search(keyword, limit).then((results) => { 73 | // merge arrays into one array 74 | if (this._selectedSearcherName === 'all') { 75 | results = [].concat.apply([], results); 76 | } 77 | 78 | if (toSave) { 79 | this._searchResults = results; 80 | this.emit('search-results-updated', results); 81 | } 82 | return results; 83 | }); 84 | } 85 | } 86 | 87 | changeSearcher(searcherName) { 88 | let searcher = this._searchers[searcherName]; 89 | if (searcher) { 90 | this._selectedSearcherName = searcherName; 91 | this.emit('searcher-changed', searcherName); 92 | } 93 | } 94 | } 95 | 96 | module.exports = new Searcher(); 97 | -------------------------------------------------------------------------------- /src/modules/Tracker.js: -------------------------------------------------------------------------------- 1 | import Constants from './Constants'; 2 | import Ua from 'universal-analytics'; 3 | const Visitor = Ua(Constants.GA.RESOURCE_KEY); 4 | 5 | module.exports = Visitor; 6 | -------------------------------------------------------------------------------- /src/modules/backuper/LocalBackuper.js: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import Fs from 'fs'; 3 | 4 | class LocalBackuper { 5 | backup(datas, options) { 6 | if (!options || !datas) { 7 | return Promise.reject(); 8 | } 9 | 10 | const basePath = options.basePath; 11 | const folderName = options.folderName; 12 | const folderPath = Path.join(basePath, folderName); 13 | 14 | return this._createFolder(folderPath).then(() => { 15 | return this._writeFiles(folderPath, datas); 16 | }); 17 | } 18 | 19 | syncDataBack(options) { 20 | if (!options) { 21 | return Promise.reject(); 22 | } 23 | 24 | const folderPath = options.folderPath; 25 | return this._readFiles(folderPath); 26 | } 27 | 28 | _createFolder(folderPath) { 29 | const promise = new Promise((resolve, reject) => { 30 | // check folder first 31 | Fs.lstat(folderPath, (error, stats) => { 32 | // if no such folder, then create 33 | if (error) { 34 | Fs.mkdir(folderPath, (error) => { 35 | // Maybe I/O is blocked, we can't do following works anymore. 36 | if (error) { 37 | reject(error); 38 | } 39 | else { 40 | resolve(); 41 | } 42 | }); 43 | } 44 | else { 45 | resolve(); 46 | } 47 | }); 48 | }); 49 | return promise; 50 | } 51 | 52 | _writeFiles(folderPath, datas) { 53 | let promises = []; 54 | datas.forEach((data) => { 55 | let promise = new Promise((resolve, reject) => { 56 | let content = JSON.stringify(data); 57 | let fileName = data.id + '.txt'; 58 | let filePath = Path.join(folderPath, fileName); 59 | Fs.writeFile(filePath, content, (error) => { 60 | // no matter success or not, we would still keep going. 61 | if (error) { 62 | console.log(error); 63 | } 64 | resolve(); 65 | }); 66 | }); 67 | promises.push(promise); 68 | }); 69 | return Promise.all(promises); 70 | } 71 | 72 | _readFiles(folderPath) { 73 | const promise = new Promise((resolve, reject) => { 74 | Fs.readdir(folderPath, (error, files) => { 75 | // TODO 76 | // need to know what the error is 77 | if (error) { 78 | reject(error); 79 | } 80 | else { 81 | const allowedFiles = files.filter((fileName) => { 82 | return fileName.match(/.txt$/); 83 | }); 84 | 85 | let promises = []; 86 | allowedFiles.forEach((fileName) => { 87 | let promise = new Promise((resolve, reject) => { 88 | let filePath = Path.join(folderPath, fileName); 89 | Fs.readFile(filePath, (error, content) => { 90 | if (error) { 91 | reject(error); 92 | } 93 | else { 94 | resolve(JSON.parse(content)); 95 | } 96 | }); 97 | }); 98 | promises.push(promise); 99 | }); 100 | 101 | Promise.all(promises).then((contents) => { 102 | resolve(contents); 103 | }).catch((error) => { 104 | reject(error); 105 | }); 106 | } 107 | }); 108 | }); 109 | 110 | return promise; 111 | } 112 | } 113 | 114 | module.exports = new LocalBackuper(); 115 | -------------------------------------------------------------------------------- /src/modules/importer/YoutubeImporter.js: -------------------------------------------------------------------------------- 1 | import UniqueId from 'kaku-core/modules/UniqueId'; 2 | import Youtube from '../wrapper/Youtube'; 3 | import PlaylistManager from '../../modules/PlaylistManager'; 4 | import YoutubeTrack from 'kaku-core/models/track/YoutubeTrack'; 5 | 6 | class YoutubeImporter { 7 | constructor() { 8 | this._regex = /[&?]list=([a-z0-9_-]+)/i; 9 | } 10 | 11 | _getPlaylistTitle(id) { 12 | let promise = new Promise((resolve, reject) => { 13 | Youtube.getPlayListsById(id, (error, result) => { 14 | if (error) { 15 | reject(error); 16 | } 17 | else { 18 | let title = 19 | result.items && 20 | result.items[0] && 21 | result.items[0].snippet && 22 | result.items[0].snippet.title || 23 | 'playlist - ' + UniqueId(6); 24 | resolve(title); 25 | } 26 | }); 27 | }); 28 | return promise; 29 | } 30 | 31 | import(url) { 32 | let id = this._parsePlaylistId(url); 33 | if (!id) { 34 | return Promise.reject(); 35 | } 36 | 37 | return this._getPlaylistTitle(id).then((title) => { 38 | return PlaylistManager.addYoutubePlaylist(title, id); 39 | }).then((playlist) => { 40 | let promise = new Promise((resolve, reject) => { 41 | // TODO 42 | // support paging to make sure we can fetch all items back 43 | Youtube.getPlayListsItemsById(id, 50, (error, result) => { 44 | if (error) { 45 | reject(error); 46 | } 47 | else { 48 | let rawTracks = result.items || []; 49 | // change from rawData into YoutubeTrack 50 | let tracks = rawTracks.map((rawTrack) => { 51 | let youtubeTrack = new YoutubeTrack(); 52 | youtubeTrack.initYoutubeResult(rawTrack); 53 | return youtubeTrack; 54 | }); 55 | // add all tracks in one operation 56 | playlist.addTracks(tracks).then(() => { 57 | resolve(playlist); 58 | }); 59 | } 60 | }); 61 | }); 62 | return promise; 63 | }); 64 | } 65 | 66 | _parsePlaylistId(url) { 67 | url = url || ''; 68 | let result; 69 | let matches = url.match(this._regex); 70 | if (matches && matches.length > 0) { 71 | return matches[1]; 72 | } 73 | } 74 | } 75 | 76 | module.exports = new YoutubeImporter(); 77 | -------------------------------------------------------------------------------- /src/modules/wrapper/Firebase.js: -------------------------------------------------------------------------------- 1 | let Firebase = require('firebase'); 2 | let EventEmitter = require('events').EventEmitter; 3 | let Constants = require('../Constants'); 4 | 5 | // We will keep all events in our internal variable 6 | let events = new EventEmitter(); 7 | 8 | const ROOMS = { 9 | METADATA: 'metadata', 10 | ONLINE_USERS: 'onlineUsers', 11 | PLAYED_TRACKS: 'playedTracks', 12 | COMMENTS: 'comments', 13 | COMMAND: 'command', 14 | SPECIAL_CONNECTED: '.info/connected' 15 | }; 16 | 17 | Firebase.roomKey = ''; 18 | Firebase.metadata = {}; 19 | Firebase.userInfo = {}; 20 | Firebase.rooms = {}; 21 | 22 | // Firebase.setup('40e566e7-7767-4601-a1e1-2b12e620afcf'); 23 | Firebase.setup = function(roomKey, userInfo) { 24 | Firebase.roomKey = roomKey; 25 | Firebase.userInfo = userInfo; 26 | events.emit('setup', userInfo); 27 | }; 28 | 29 | Firebase.setMetadata = function(metadata) { 30 | Firebase.metadata = metadata || {}; 31 | events.emit('meatadata-updated', Firebase.metadata); 32 | }; 33 | 34 | // How to use : 35 | // let ref = Firebase.join('comments'); 36 | Firebase.join = function(roomName) { 37 | let roomKey = Firebase.roomKey; 38 | if (roomKey) { 39 | let url = Constants.API.FIREBASE_URL + 'rooms'; 40 | let ref = new Firebase(url + '/' + roomKey + '/' + roomName); 41 | events.emit('room-joined', roomName, ref); 42 | 43 | // keep the reference for later use 44 | Firebase.rooms[roomName] = ref; 45 | return ref; 46 | } 47 | }; 48 | 49 | // This one is special !!! 50 | // https://www.firebase.com/docs/web/guide/offline-capabilities.html 51 | Firebase.joinConnectedRoom = function() { 52 | let url = Constants.API.FIREBASE_URL; 53 | let ref = new Firebase(url + '/' + ROOMS.SPECIAL_CONNECTED); 54 | events.emit('room-joined', 'connected', ref); 55 | 56 | // keep the reference for later use 57 | Firebase.rooms.connected = ref; 58 | return ref; 59 | }; 60 | 61 | Firebase.joinOnlineUsersRoom = function() { 62 | let ref = Firebase.join(ROOMS.ONLINE_USERS); 63 | return ref; 64 | }; 65 | 66 | Firebase.joinCommentsRoom = function() { 67 | let ref = Firebase.join(ROOMS.COMMENTS); 68 | return ref; 69 | }; 70 | 71 | Firebase.joinMetadataRoom = function() { 72 | let ref = Firebase.join(ROOMS.METADATA); 73 | return ref; 74 | }; 75 | 76 | Firebase.joinCommandRoom = function() { 77 | let ref = Firebase.join(ROOMS.COMMAND); 78 | return ref; 79 | }; 80 | 81 | Firebase.joinPlayedTracksRoom = function() { 82 | let ref = Firebase.join(ROOMS.PLAYED_TRACKS); 83 | return ref; 84 | }; 85 | 86 | Firebase.leaveAll = function() { 87 | Object.keys(Firebase.rooms).forEach((roomName) => { 88 | Firebase.leave(roomName); 89 | }); 90 | 91 | // cleaned up cached data 92 | Firebase.roomKey = ''; 93 | Firebase.metadata = {}; 94 | Firebase.userInfo = {}; 95 | events.emit('room-left-all'); 96 | }; 97 | 98 | Firebase.leave = function(roomName) { 99 | let options = Firebase.get(roomName); 100 | if (options) { 101 | events.emit('room-left', roomName, options); 102 | delete Firebase[roomName]; 103 | } 104 | }; 105 | 106 | Firebase.get = function(roomName) { 107 | let obj = Firebase.rooms[roomName]; 108 | return obj; 109 | }; 110 | 111 | Firebase.on = function(eventName, callback) { 112 | events.on(eventName, callback); 113 | }; 114 | 115 | module.exports = Firebase; 116 | -------------------------------------------------------------------------------- /src/modules/wrapper/Youtube.js: -------------------------------------------------------------------------------- 1 | import Constants from '../Constants'; 2 | import Youtube from 'youtube-node'; 3 | 4 | const youtube = new Youtube(); 5 | youtube.setKey(Constants.API.YOUTUBE_API_KEY); 6 | 7 | module.exports = youtube; 8 | -------------------------------------------------------------------------------- /src/public/fonts/Arimo/Arimo-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/fonts/Arimo/Arimo-Bold.ttf -------------------------------------------------------------------------------- /src/public/fonts/Arimo/Arimo-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/fonts/Arimo/Arimo-BoldItalic.ttf -------------------------------------------------------------------------------- /src/public/fonts/Arimo/Arimo-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/fonts/Arimo/Arimo-Italic.ttf -------------------------------------------------------------------------------- /src/public/fonts/Arimo/Arimo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/fonts/Arimo/Arimo-Regular.ttf -------------------------------------------------------------------------------- /src/public/images/icons/kaku.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/kaku.icns -------------------------------------------------------------------------------- /src/public/images/icons/kaku.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/kaku.ico -------------------------------------------------------------------------------- /src/public/images/icons/kaku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/kaku.png -------------------------------------------------------------------------------- /src/public/images/icons/touchbar/kaku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/touchbar/kaku.png -------------------------------------------------------------------------------- /src/public/images/icons/touchbar/kaku@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/touchbar/kaku@2x.png -------------------------------------------------------------------------------- /src/public/images/icons/tray/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/tray/default.png -------------------------------------------------------------------------------- /src/public/images/icons/tray/default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/tray/default@2x.png -------------------------------------------------------------------------------- /src/public/images/icons/tray/windows.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/icons/tray/windows.ico -------------------------------------------------------------------------------- /src/public/images/others/cast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/others/cast.png -------------------------------------------------------------------------------- /src/public/images/track-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EragonJ/Kaku/c28b9a0c61444009f422e3078377968aed16409f/src/public/images/track-placeholder.png -------------------------------------------------------------------------------- /src/public/less/components/about.less: -------------------------------------------------------------------------------- 1 | .about-component { 2 | button i { 3 | margin-right: 5px; 4 | } 5 | 6 | button { 7 | margin-right: 5px; 8 | } 9 | 10 | p.well { 11 | font-size: 18px; 12 | 13 | &:first-letter { 14 | font-size: 30px; 15 | line-height: initial; 16 | } 17 | } 18 | } 19 | 20 | .special-thanks-modal { 21 | .modal-header { 22 | text-align: center; 23 | } 24 | 25 | .modal-body { 26 | h1 { 27 | border: 1px solid #000; 28 | border-width: 1px 0; 29 | text-align: center; 30 | padding: 5px 0; 31 | margin: 5px 0; 32 | } 33 | 34 | ul { 35 | margin: 20px 0; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/public/less/components/chatroom.less: -------------------------------------------------------------------------------- 1 | .chatroom { 2 | position: absolute; 3 | bottom: 0; 4 | right: 100px; 5 | background-color: white; 6 | border-radius: 5px 5px 0 0; 7 | z-index: 5; 8 | max-width: 300px; 9 | border: 1px solid #cccccc; 10 | border-bottom: 0; 11 | 12 | &.disabled { 13 | display: none; 14 | } 15 | 16 | &.shown { 17 | .comment-component { 18 | display: inline-block; 19 | } 20 | } 21 | 22 | &.online { 23 | .header:after { 24 | background-color: #5cb85c; 25 | } 26 | } 27 | 28 | &.offline { 29 | .header:after { 30 | background-color: #777777; 31 | } 32 | } 33 | 34 | &.error { 35 | .header:after { 36 | background-color: #d9534f; 37 | } 38 | } 39 | 40 | .unread-count { 41 | position: absolute; 42 | z-index: 1; 43 | right: 0; 44 | top: 0; 45 | transform: translate(50%, -50%); 46 | } 47 | 48 | .header { 49 | position: relative; 50 | font-size: 16px; 51 | margin: 0; 52 | padding: 5px 10px 5px 25px; 53 | background-color: #efefef; 54 | cursor: pointer; 55 | white-space: nowrap; 56 | text-overflow: ellipsis; 57 | overflow: hidden; 58 | border-bottom: 1px solid #cccccc; 59 | border-radius: 5px 5px 0 0; 60 | 61 | i { 62 | margin-right: 2px; 63 | } 64 | 65 | &:after { 66 | content: ' '; 67 | display: inline-block; 68 | position: absolute; 69 | top: 50%; 70 | left: 10px; 71 | width: 8px; 72 | height: 8px; 73 | margin-top: -4px; 74 | border-radius: 8px; 75 | } 76 | } 77 | 78 | .comment-component { 79 | // hide by default 80 | display: none; 81 | 82 | .comment-author { 83 | font-size: 16px; 84 | margin-right: 5px; 85 | font-weight: bold; 86 | } 87 | 88 | .comment-no-data { 89 | margin: 30px 0; 90 | text-align: center; 91 | } 92 | 93 | .comment-text { 94 | position: relative; 95 | word-break: break-word; 96 | background-color: #efefef; 97 | border-radius: 5px; 98 | border: 1px solid #aaaaaa; 99 | padding: 2px 5px; 100 | margin: 5px 0; 101 | max-width: 70%; 102 | 103 | &:before { 104 | content: ' '; 105 | display: inline-block; 106 | position: absolute; 107 | left: 0; 108 | top: 50%; 109 | margin-top: -5px; 110 | margin-left: -5px; 111 | width: 0; 112 | height: 0; 113 | border-right: 5px solid #aaaaaa; 114 | border-top: 5px solid transparent; 115 | border-bottom: 5px solid transparent; 116 | } 117 | 118 | &:after { 119 | content: ' '; 120 | display: inline-block; 121 | position: absolute; 122 | left: 0; 123 | top: 50%; 124 | margin-top: -4px; 125 | margin-left: -4px; 126 | width: 0; 127 | height: 0; 128 | border-right: 4px solid #efefef; 129 | border-top: 4px solid transparent; 130 | border-bottom: 4px solid transparent; 131 | } 132 | } 133 | 134 | .comment { 135 | margin: 10px; 136 | 137 | &:last-child { 138 | border-bottom: 0; 139 | } 140 | } 141 | 142 | .comment-list { 143 | max-height: 150px; 144 | overflow: auto; 145 | } 146 | 147 | .comment-form { 148 | padding: 10px; 149 | border-top: 1px solid #cccccc; 150 | 151 | button[type="submit"] { 152 | color: #0498f5; 153 | margin-left: 10px; 154 | } 155 | } 156 | } 157 | 158 | .emoji-mart { 159 | position: absolute; 160 | bottom: 48px; 161 | right: -77px; 162 | z-index: 2; 163 | 164 | &:before { 165 | content: ' '; 166 | width: 0; 167 | height: 0; 168 | border: solid transparent; 169 | position: absolute; 170 | left: 50%; 171 | border-top-color: white; 172 | border-width: 9px; 173 | margin-left: -9px; 174 | bottom: -18px; 175 | z-index: 1; 176 | } 177 | 178 | &:after { 179 | content: " "; 180 | width: 0; 181 | height: 0; 182 | border: solid transparent; 183 | position: absolute; 184 | left: 50%; 185 | border-top-color: #d9d9d9; 186 | border-width: 10px; 187 | margin-left: -10px; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/public/less/components/connection_check.less: -------------------------------------------------------------------------------- 1 | .connection-check-component { 2 | i { 3 | margin-right: 5px; 4 | } 5 | 6 | // for others, check .global-overlay in _global.scss 7 | &.is-online { 8 | display: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/public/less/components/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Arimo'; 3 | src: url(../fonts/Arimo/Arimo-Regular.ttf) format('woff2'); 4 | } 5 | -------------------------------------------------------------------------------- /src/public/less/components/global.less: -------------------------------------------------------------------------------- 1 | @import '../includes/variables.less'; 2 | @import '../includes/mixins.less'; 3 | 4 | * { 5 | -webkit-user-select: none; 6 | } 7 | 8 | input { 9 | -webkit-user-select: text; 10 | } 11 | 12 | body, 13 | html, 14 | .root, 15 | .content-page { 16 | font-family: 'Arimo', sans-serif; 17 | height: 100%; 18 | } 19 | 20 | button { 21 | &:hover { 22 | cursor: pointer; 23 | } 24 | 25 | &[disabled]:hover { 26 | cursor: default; 27 | } 28 | } 29 | 30 | .global-overlay { 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | right: 0; 35 | bottom: 0; 36 | background: rgba(0, 0, 0, 0.5); 37 | z-index: @highestZindex; 38 | 39 | h1 { 40 | position: absolute; 41 | font-size: 20px; 42 | right: 10px; 43 | bottom: 0; 44 | color: white; 45 | } 46 | } 47 | 48 | a { 49 | color: initial; 50 | 51 | &:hover, 52 | &:focus { 53 | color: initial; 54 | text-decoration: initial; 55 | } 56 | } 57 | 58 | .row-no-padding { 59 | margin-left: 0; 60 | margin-right: 0; 61 | 62 | & > [class*="col-"] { 63 | padding-left: 0 !important; 64 | padding-right: 0 !important; 65 | } 66 | } 67 | 68 | .top-row { 69 | height: 40px; 70 | } 71 | 72 | .bottom-row { 73 | .clearfix(); 74 | height: calc(~"100% - 40px"); 75 | 76 | .left, 77 | .right { 78 | height: 100%; 79 | float: left; 80 | } 81 | 82 | .left { 83 | width: 260px; 84 | } 85 | 86 | .right { 87 | width: calc(~"100% - 260px"); 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/public/less/components/online_dj.less: -------------------------------------------------------------------------------- 1 | .online-dj-slot { 2 | .choose-role-page, 3 | .dashboard-page { 4 | display: none; 5 | } 6 | 7 | &[data-page="0"] { 8 | .choose-role-page { 9 | display: block; 10 | } 11 | } 12 | 13 | &[data-page="1"] { 14 | .dashboard-page { 15 | display: block; 16 | } 17 | } 18 | 19 | .tab-content { 20 | margin: 10px 0; 21 | } 22 | 23 | .dashboard-page { 24 | .form-inline { 25 | text-align: right; 26 | margin: 0 0 15px 0; 27 | 28 | label { 29 | margin: 0 5px 0 0; 30 | } 31 | 32 | .form-group, 33 | button { 34 | margin: 0 0 0 5px; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/public/less/components/settings.less: -------------------------------------------------------------------------------- 1 | .settings-component { 2 | .dropdown-menu i { 3 | margin-right: 5px; 4 | } 5 | 6 | .dropdown-toggle .caret { 7 | margin-left: 5px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/public/less/components/sidebar.less: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | @videoHeight: 140px; 3 | @controlsHeight: 25px; 4 | @toolbarHeight: 40px; 5 | 6 | height: 100%; 7 | border-right: 1px solid #cccccc; 8 | 9 | .menus { 10 | // 155px is the height of player 11 | height: ~"calc(100% - @{videoHeight} - @{controlsHeight})"; 12 | overflow: auto; 13 | 14 | ul li { 15 | font-size: 16px; 16 | border-left: 5px solid #ffffff; 17 | 18 | &.seperator { 19 | border-bottom: 1px solid #cccccc; 20 | margin: 5px 0; 21 | } 22 | 23 | &.add-playlist { 24 | a { 25 | color: #2195f7; 26 | } 27 | } 28 | 29 | &.active { 30 | border-left-color: #93c6f2; 31 | background-color: #d9f8fa; 32 | transition: background-color 0.2s ease-in-out; 33 | } 34 | 35 | &.active .icon { 36 | color: #2195f7; 37 | } 38 | 39 | a { 40 | display: block; 41 | padding: 5px; 42 | white-space: nowrap; 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | } 46 | 47 | .icon { 48 | margin-right: 5px; 49 | } 50 | } 51 | } 52 | 53 | .player { 54 | 55 | // so many hacks here ... 56 | &.tv-mode { 57 | #player { 58 | position: absolute; 59 | top: @toolbarHeight; 60 | bottom: 0; 61 | left: 0; 62 | right: 0; 63 | height: auto; 64 | z-index: 5; 65 | } 66 | 67 | .control-buttons { 68 | @offset: 5px; 69 | 70 | position: absolute; 71 | top: @toolbarHeight + @offset; 72 | right: @offset; 73 | 74 | .btn { 75 | display: none; 76 | } 77 | 78 | .tv-button { 79 | display: inline; 80 | position: relative; 81 | background: transparent; 82 | z-index: 6; 83 | } 84 | } 85 | } 86 | 87 | .track { 88 | width: 200px; 89 | height: 200px; 90 | background-image: url(../images/track-placeholder.png); 91 | background-color: #efefef; 92 | background-size: 100%; 93 | } 94 | 95 | .control-buttons { 96 | .btn { 97 | outline: none; 98 | border: 0; 99 | width: calc(100% / 7); // we have 7 buttons 100 | color: #ccc; // sync with videojs's style 101 | background-color: #000; 102 | // there are some unconsistent styles on Windows, 103 | // so we have to override it directly. 104 | padding: 2px 6px 3px; 105 | border-radius: 0; 106 | } 107 | 108 | .btn[disabled] { 109 | opacity: 1; 110 | } 111 | 112 | .btn:active { 113 | background-color: #444; 114 | } 115 | 116 | .btn:hover { 117 | color: lighten(#ccc, 10%); 118 | } 119 | 120 | // We need to additionally align the mode word 121 | .btn.repeat-button { 122 | position: relative; 123 | 124 | .mode { 125 | position: absolute; 126 | bottom: 0; 127 | right: 8px; 128 | font-size: 10px; 129 | background-color: rgba(0, 0, 0, 0.7); 130 | } 131 | } 132 | 133 | .btn.external-button { 134 | i { 135 | vertical-align: middle; 136 | } 137 | } 138 | 139 | .btn.cast-button { 140 | img { 141 | width: 15px; 142 | height: 12px; 143 | margin-top: -2px; 144 | object-fit: none; 145 | object-position: 0 0; 146 | } 147 | 148 | &:hover img { 149 | object-position: -15px 0; 150 | } 151 | 152 | &.is-connecting img { 153 | object-position: 0 -12px; 154 | } 155 | 156 | &.is-connecting:hover img { 157 | object-position: -15px -12px; 158 | } 159 | 160 | &.is-casting img { 161 | object-position: 0 -24px; 162 | } 163 | 164 | &.is-casting:hover img { 165 | object-position: -15px -24px; 166 | } 167 | } 168 | } 169 | 170 | #player { 171 | // to make spinner at the right place 172 | position: relative; 173 | // We have to override its height directly 174 | height: @videoHeight; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/public/less/components/tab_content.less: -------------------------------------------------------------------------------- 1 | // Basically this is the style only for first level tab-content 2 | .right > .tab-content { 3 | height: 100%; 4 | overflow: auto; 5 | 6 | .header { 7 | position: relative; 8 | display: flex; 9 | border-bottom: 1px solid #cccccc; 10 | padding-bottom: 10px; 11 | margin-bottom: 10px; 12 | } 13 | 14 | h1 { 15 | margin: 0; 16 | white-space: nowrap; 17 | text-overflow: ellipsis; 18 | overflow: hidden; 19 | font-size: 30px; 20 | 21 | i { 22 | margin-right: 10px; 23 | } 24 | } 25 | 26 | .control-buttons { 27 | margin-left: auto; 28 | 29 | i { 30 | pointer-events: none; 31 | } 32 | 33 | button { 34 | margin-right: 5px; 35 | } 36 | 37 | button:last-child { 38 | margin-right: 0; 39 | } 40 | 41 | .btn-group { 42 | button { 43 | margin-right: 0; 44 | } 45 | 46 | button:last-child { 47 | margin-right: 5px; 48 | } 49 | } 50 | } 51 | 52 | .tab-pane { 53 | padding: 10px; 54 | overflow-x: hidden; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/public/less/components/toolbar.less: -------------------------------------------------------------------------------- 1 | .toolbar-component { 2 | -webkit-app-region: drag; 3 | 4 | * { 5 | -webkit-app-region: no-drag; 6 | } 7 | 8 | position: relative; 9 | text-align: right; 10 | background: #efefef; 11 | height: 40px; 12 | border-bottom: 1px solid #cccccc; 13 | 14 | .toolbar-buttons { 15 | position: absolute; 16 | float: left; 17 | top: 50%; 18 | left: 13px; 19 | margin-top: -12px; 20 | 21 | .button { 22 | display: inline-block; 23 | font-size: 8px; 24 | border-radius: 10px; 25 | text-align: center; 26 | margin-right: 8px; 27 | width: 12px; 28 | height: 12px; 29 | line-height: 14px; 30 | cursor: pointer; 31 | color: #333; 32 | 33 | // mimic OSX's native behavior 34 | i { 35 | visibility: hidden; 36 | } 37 | } 38 | 39 | .button:hover { 40 | i { 41 | visibility: visible; 42 | } 43 | } 44 | } 45 | 46 | .shrink-button { 47 | background-color: #fdbc40; 48 | } 49 | 50 | .enlarge-button { 51 | background-color: #34c94a; 52 | } 53 | 54 | .close-button { 55 | background-color: #fc625d; 56 | } 57 | 58 | .devtools-button { 59 | background-color: #b3cdf7; 60 | } 61 | 62 | .toolbar-song-information { 63 | -webkit-user-select: initial; 64 | position: absolute; 65 | line-height: 40px; 66 | color: #666666; 67 | left: 50%; 68 | transform: translateX(-50%); 69 | } 70 | 71 | .toolbar-progressbar { 72 | width: 0; // default is zero, and it will be updated repeatedly 73 | height: 2px; 74 | background-color: #f27c7c; 75 | position: absolute; 76 | top: 0; 77 | transition: width 1s ease-in-out; 78 | box-shadow: 1px 1px 3px #cccccc; 79 | } 80 | 81 | .searchbar-component { 82 | position: absolute; 83 | right: 10px; 84 | top: 50%; 85 | transform: translateY(-50%); 86 | 87 | // to make autocomplete ul/li can cover stuffs under it 88 | z-index: 10; 89 | 90 | .loader { 91 | display: none; 92 | position: absolute; 93 | right: 10px; 94 | top: 50%; 95 | transform: translateY(-50%); 96 | } 97 | 98 | .loader.show { 99 | display: inline; 100 | } 101 | 102 | .searchbar-user-input { 103 | height: 26px; 104 | width: 300px; 105 | border-radius: 0; 106 | // leave some spaces for the loader 107 | padding-right: 30px; 108 | } 109 | 110 | .autocomplete-list { 111 | position: absolute; 112 | width: 100%; 113 | border: 1px solid #ccc; 114 | text-align: left; 115 | border-radius: 0 0 5px 5px; 116 | border-top: 0; 117 | max-height: 160px; 118 | overflow-x: hidden; 119 | overflow-y: scroll; 120 | 121 | li { 122 | border-bottom: 1px solid #ccc; 123 | background-color: white; 124 | padding: 2px 12px; 125 | 126 | // TODO 127 | // extract this into mixin 128 | text-overflow: ellipsis; 129 | overflow: hidden; 130 | white-space: nowrap; 131 | } 132 | 133 | li:hover { 134 | cursor: pointer; 135 | } 136 | 137 | li.selected { 138 | background: #ccc; 139 | } 140 | 141 | li:last-child { 142 | border: 0; 143 | } 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/public/less/components/topranking.less: -------------------------------------------------------------------------------- 1 | .topranking-slot { 2 | // -10px to make it align to the search form 3 | // width: calc(100% - 10px); 4 | // overflow-x: scroll; 5 | // border: 1px solid #cccccc; 6 | // margin: 10px 0; 7 | } 8 | 9 | .topranking-component { 10 | // $trackSize: $defaultTrackImageSize + ($defaultTrackPadding + $defaultTrackMargin + $defaultTrackBorder) * 2; 11 | // width: $trackSize * 100; 12 | // height: $trackSize; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/public/less/components/track.less: -------------------------------------------------------------------------------- 1 | @import '../includes/variables.less'; 2 | @import '../includes/mixins.less'; 3 | 4 | .track { 5 | position: relative; 6 | border-radius: 5px; 7 | overflow: hidden; 8 | cursor: pointer; 9 | background: #efefef; 10 | } 11 | 12 | .track-square { 13 | display: inline-block; 14 | padding: 5px; 15 | margin: 0 10px 5px 0; 16 | border: 1px solid #000; 17 | 18 | &.active { 19 | border-color: darken(#d9f8fa, 10%); 20 | background-color: darken(#d9f8fa, 10%); 21 | } 22 | 23 | &:hover { 24 | .info { 25 | background-color: rgba(0, 0, 0, 0.5); 26 | } 27 | } 28 | 29 | img { 30 | width: 170px; 31 | height: 170px; 32 | border-radius: 5px; 33 | object-fit: contain; 34 | } 35 | 36 | .info { 37 | position: absolute; 38 | bottom: 0; 39 | left: 0; 40 | right: 0; 41 | background-color: rgba(0, 0, 0, 0.3); 42 | color: white; 43 | padding: 0 8px 5px; 44 | } 45 | 46 | .ribbon { 47 | position: absolute; 48 | top: 2px; 49 | right: -17px; 50 | background: rgba(255, 255, 255, 0.8); 51 | width: 60px; 52 | transform: rotate(45deg); 53 | text-align: center; 54 | color: black; 55 | 56 | // we need horizontal icon 57 | i { 58 | transform: rotate(-45deg); 59 | } 60 | } 61 | 62 | .track-name { 63 | font-size: 16px; 64 | } 65 | 66 | .track-artist { 67 | font-size: 14px; 68 | } 69 | 70 | .track-name, 71 | .track-artist { 72 | .kaku_add_ellipsis(); 73 | } 74 | } 75 | 76 | .track-list { 77 | display: block; 78 | height: 30px; 79 | margin: 10px 0; 80 | color: #000; 81 | 82 | &.active { 83 | background-color: #d9f8fa; 84 | } 85 | 86 | i { 87 | margin: 0 10px; 88 | vertical-align: top; 89 | line-height: 30px; 90 | } 91 | 92 | img { 93 | width: 30px; 94 | height: 100%; 95 | vertical-align: top; 96 | object-fit: contain; 97 | } 98 | 99 | .icon-wrapper { 100 | display: inline-block; 101 | width: 32px; 102 | } 103 | 104 | .track-name { 105 | width: calc(~"100% - 25% - 32px - 30px"); 106 | text-align: left; 107 | } 108 | 109 | .track-artist { 110 | width: 25%; 111 | text-align: right; 112 | } 113 | 114 | .track-name, 115 | .track-artist { 116 | display: inline-block; 117 | line-height: 30px; 118 | padding: 0 10px; 119 | .kaku_add_ellipsis(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/public/less/components/videojs.less: -------------------------------------------------------------------------------- 1 | .vjs-default-skin { 2 | background-color: #000000; 3 | } 4 | 5 | .vjs-default-skin .vjs-volume-control { 6 | right: 1em; 7 | } 8 | -------------------------------------------------------------------------------- /src/public/less/includes/mixins.less: -------------------------------------------------------------------------------- 1 | .kaku_add_ellipsis() { 2 | white-space: nowrap; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | } 6 | 7 | .clearfix() { 8 | &:before, 9 | &:after { 10 | content: " "; 11 | display: table; 12 | } 13 | &:after { 14 | clear: both; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/public/less/includes/variables.less: -------------------------------------------------------------------------------- 1 | // others 2 | @highestZindex: 9999; 3 | -------------------------------------------------------------------------------- /src/public/less/index.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | @import 'components/fonts'; 3 | 4 | // Global & Shared components 5 | @import 'components/global'; 6 | @import 'components/track'; 7 | 8 | // Individual components 9 | @import 'components/toolbar'; 10 | @import 'components/topranking'; 11 | @import 'components/sidebar'; 12 | @import 'components/tab_content'; 13 | @import 'components/connection_check'; 14 | @import 'components/settings'; 15 | @import 'components/about'; 16 | @import 'components/online_dj'; 17 | @import 'components/chatroom'; 18 | 19 | // 3rd party customizations 20 | @import 'components/videojs'; 21 | -------------------------------------------------------------------------------- /src/views/components/alltracks/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactTooltip from 'react-tooltip'; 3 | import Searcher from '../../../modules/Searcher'; 4 | import TracksComponent from '../shared/tracks'; 5 | import Player from '../../modules/Player'; 6 | 7 | class AllTracksComponent extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | tracks: [] 13 | }; 14 | 15 | this._clickToPlayAll = this._clickToPlayAll.bind(this); 16 | } 17 | 18 | componentDidMount() { 19 | Searcher.on('search-results-updated', (results) => { 20 | this.setState({ 21 | tracks: results 22 | }); 23 | }); 24 | } 25 | 26 | componentDidUpdate() { 27 | ReactTooltip.rebuild(); 28 | } 29 | 30 | _clickToPlayAll() { 31 | let noUpdate = true; 32 | Player.cleanupTracks(noUpdate); 33 | Player.addTracks(this.state.tracks); 34 | Player.playNextTrack(0); 35 | } 36 | 37 | render() { 38 | let tracks = this.state.tracks; 39 | let controls = { 40 | trackModeButton: true, 41 | playAllButton: true, 42 | deleteAllButton: false, 43 | addToPlayQueueButton: true 44 | }; 45 | 46 | return ( 47 | 54 | ); 55 | } 56 | } 57 | 58 | module.exports = AllTracksComponent; 59 | -------------------------------------------------------------------------------- /src/views/components/chatroom/comment/comment-form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ActionButton from '../../shared/action-button'; 4 | import { Picker } from 'emoji-mart'; 5 | 6 | class CommentFormComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | text: '', 11 | isPickerShown: false 12 | }; 13 | 14 | this._onSubmit = this._onSubmit.bind(this); 15 | this._toggleEmojiPicker = this._toggleEmojiPicker.bind(this); 16 | this._selectEmoji = this._selectEmoji.bind(this); 17 | this._handleInputChange = this._handleInputChange.bind(this); 18 | } 19 | 20 | _onSubmit(e) { 21 | e.preventDefault(); 22 | 23 | let text = this.refs.text.value.trim(); 24 | if (text.length === 0) { 25 | return; 26 | } 27 | 28 | this.setState({ 29 | text: '' 30 | }); 31 | 32 | // let its parent know which comment is sent 33 | this.props.onSubmit(text); 34 | } 35 | 36 | _handleInputChange(e) { 37 | let text = e.target.value; 38 | this.setState({ 39 | text: text 40 | }); 41 | } 42 | 43 | _selectEmoji(emoji, e) { 44 | let inputText = this.state.text; 45 | let emojiText = emoji.id; 46 | let result = `${inputText} :${emojiText}:`; 47 | result = result.trim(); 48 | 49 | this.setState({ 50 | text: result, 51 | isPickerShown: false 52 | }); 53 | } 54 | 55 | _toggleEmojiPicker() { 56 | let isPickerShown = this.state.isPickerShown; 57 | this.setState({ 58 | isPickerShown: !isPickerShown 59 | }); 60 | } 61 | 62 | componentDidUpdate() { 63 | let isShown = this.props.shown; 64 | 65 | if (isShown) { 66 | this.refs.text.focus(); 67 | } 68 | else { 69 | this.refs.text.blur(); 70 | } 71 | } 72 | 73 | render() { 74 | let isRoomConnected = this.props.connected; 75 | let isPickerShown = this.state.isPickerShown; 76 | 77 | return ( 78 |
79 |
80 |
81 | 88 | 89 | 95 | 96 |
97 |
98 | 104 | {isPickerShown && 105 | 111 | } 112 | 113 | ); 114 | } 115 | } 116 | 117 | CommentFormComponent.propTypes = { 118 | onSubmit: PropTypes.func.isRequired, 119 | connected: PropTypes.bool, 120 | shown: PropTypes.bool 121 | }; 122 | 123 | CommentFormComponent.defaultProps = { 124 | onSubmit: function() { }, 125 | connected: false, 126 | shown: false 127 | }; 128 | 129 | export default CommentFormComponent; 130 | -------------------------------------------------------------------------------- /src/views/components/chatroom/comment/comment-list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Comment from './comment'; 4 | import CommentNoData from './comment-no-data'; 5 | 6 | class CommentListComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidUpdate() { 12 | let commentListNode = this.refs.commentList; 13 | let scrollHeight = commentListNode.scrollHeight; 14 | $(commentListNode).scrollTop(scrollHeight); 15 | } 16 | 17 | render() { 18 | let renderedElement; 19 | let commentNodes = this.props.comments.map((comment, index) => { 20 | return ; 21 | }); 22 | 23 | if (commentNodes.length !== 0) { 24 | renderedElement = commentNodes; 25 | } 26 | else { 27 | renderedElement = ; 28 | } 29 | 30 | return ( 31 |
{renderedElement}
32 | ); 33 | } 34 | } 35 | 36 | CommentListComponent.propTypes = { 37 | comments: PropTypes.array.isRequired 38 | }; 39 | 40 | CommentListComponent.defaultProps = { 41 | comments: [] 42 | }; 43 | 44 | export default CommentListComponent; 45 | -------------------------------------------------------------------------------- /src/views/components/chatroom/comment/comment-no-data.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import L10nSpan from '../../shared/l10n-span'; 3 | 4 | class CommentNoDataComponent extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | } 19 | 20 | export default CommentNoDataComponent; 21 | -------------------------------------------------------------------------------- /src/views/components/chatroom/comment/comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactEmoji from 'react-emoji'; 4 | 5 | class CommentComponent extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | let data = this.props.data; 12 | 13 | // TODO 14 | // drop ReactEmoji since we do have emoji-mart now. 15 | // need to figure out the way to parse strings to 16 | let emojifiedComment = ReactEmoji.emojify(data.comment); 17 | 18 | return ( 19 |
20 | {data.userName} 21 |
22 | {emojifiedComment} 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | CommentComponent.propTypes = { 30 | data: PropTypes.object.isRequired 31 | }; 32 | 33 | CommentComponent.defaultProps = { 34 | data: {} 35 | }; 36 | 37 | export default CommentComponent; 38 | -------------------------------------------------------------------------------- /src/views/components/chatroom/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ClassNames from 'classnames'; 3 | import ReactFireMixin from 'reactfire'; 4 | import L10nSpan from '../shared/l10n-span'; 5 | import CommentForm from './comment/comment-form'; 6 | import CommentList from './comment/comment-list'; 7 | import Constants from '../../../modules/Constants'; 8 | import Firebase from '../../../modules/wrapper/Firebase'; 9 | import PreferenceManager from '../../../modules/PreferenceManager'; 10 | 11 | const PREFERENCE_KEY = 'default.chatroom.enabled'; 12 | 13 | let ChatroomComponent = React.createClass({ 14 | mixins: [ReactFireMixin], 15 | 16 | getInitialState: function() { 17 | return { 18 | enabled: PreferenceManager.getPreference(PREFERENCE_KEY), 19 | shown: false, 20 | unreadMessageCount: 0, 21 | isRoomConnected: false, 22 | roomName: '', 23 | onlineUsers: [], 24 | userInfo: {}, 25 | comments: [] 26 | }; 27 | }, 28 | 29 | componentWillMount: function() { 30 | PreferenceManager.on('preference-updated', (key, enabled) => { 31 | if (key === PREFERENCE_KEY) { 32 | this.setState({ 33 | enabled: enabled 34 | }); 35 | } 36 | }); 37 | 38 | Firebase.on('meatadata-updated', (metadata) => { 39 | this.setState({ 40 | roomName: metadata.roomName 41 | }); 42 | }); 43 | 44 | // Great, if the room is opened 45 | Firebase.on('setup', (userInfo) => { 46 | // we can join to specifc room 47 | let commentsRef = Firebase.joinCommentsRoom(); 48 | this.bindAsArray(commentsRef, 'comments'); 49 | 50 | // if there is any comment coming when the chatroom is hidden, 51 | // we should show some UI for users about this 52 | commentsRef.on('value', (snapshot) => { 53 | // it will be triggered when initialized, so we need to check 54 | // there is indeed any value coming before keep the count. 55 | if (snapshot.val() && !this.state.shown) { 56 | this.setState({ 57 | unreadMessageCount: this.state.unreadMessageCount + 1 58 | }); 59 | } 60 | }); 61 | 62 | let onlineUsersRef = Firebase.joinOnlineUsersRoom(); 63 | this.bindAsArray(onlineUsersRef, 'onlineUsers'); 64 | 65 | let currentUserRef = onlineUsersRef.push(); 66 | // http://stackoverflow.com/questions/15982215/firebase-count-online-users 67 | // 68 | // This is special, we need to join to this special room to make sure 69 | // we can reflect the count of online users 70 | let connectedRef = Firebase.joinConnectedRoom(); 71 | connectedRef.on('value', (snapshot) => { 72 | if (snapshot.val() === true) { 73 | // Keep current user's information to `onlineUsersRef` 74 | currentUserRef.set(userInfo); 75 | currentUserRef.onDisconnect().remove(); 76 | } 77 | }); 78 | 79 | // keep current users information 80 | this.setState({ 81 | isRoomConnected: true, 82 | userInfo: userInfo 83 | }); 84 | }); 85 | 86 | Firebase.on('room-left', (roomName, ref) => { 87 | if ('comments' === roomName) { 88 | this.unbind('comments'); 89 | } 90 | else if ('onlineUsers' === roomName) { 91 | this.unbind('onlineUsers'); 92 | } 93 | }); 94 | 95 | Firebase.on('room-left-all', () => { 96 | // do a final cleanup 97 | this.setState({ 98 | userInfo: {}, 99 | roomName: '', 100 | isRoomConnected: false 101 | }); 102 | }); 103 | }, 104 | 105 | _onHeaderClick: function() { 106 | let isShown = this.state.shown; 107 | let option = {}; 108 | option.shown = !isShown; 109 | 110 | // before clicking, it is hidden, so it means user is going to 111 | // open the chatroom, then we have to reset the unreadMessageCount 112 | if (!isShown) { 113 | option.unreadMessageCount = 0; 114 | } 115 | 116 | this.setState(option); 117 | }, 118 | 119 | _onCommentSubmit: function(comment) { 120 | let userName = this.state.userInfo.userName; 121 | this.firebaseRefs.comments.push({ 122 | userName: userName, 123 | comment: comment 124 | }); 125 | }, 126 | 127 | _getOnlineUsersCount: function() { 128 | let onlineUsers = this.state.onlineUsers; 129 | // unbind() will cause the value to `undefined` ... 130 | let onlineUsersCount = onlineUsers && onlineUsers.length; 131 | if (this.state.isRoomConnected) { 132 | return onlineUsersCount; 133 | } 134 | else { 135 | return 0; 136 | } 137 | }, 138 | 139 | render: function() { 140 | let headerSpan; 141 | let unreadCountSpan; 142 | let enabled = this.state.enabled; 143 | let roomName = this.state.roomName; 144 | let comments = this.state.comments; 145 | let shown = this.state.shown; 146 | let isRoomConnected = this.state.isRoomConnected; 147 | let unreadMessageCount = this.state.unreadMessageCount; 148 | let onlineUsersCount = this._getOnlineUsersCount(); 149 | 150 | if (roomName) { 151 | headerSpan = {roomName}; 152 | } 153 | else { 154 | headerSpan = ; 155 | } 156 | 157 | if (unreadMessageCount > 0) { 158 | unreadCountSpan = {unreadMessageCount}; 159 | } 160 | 161 | let chatroomClass = ClassNames({ 162 | 'disabled': !enabled, 163 | 'chatroom': true, 164 | 'shown': shown, 165 | 'online': isRoomConnected, 166 | 'offline': !isRoomConnected, 167 | 'error': false 168 | }); 169 | 170 | return ( 171 |
172 | {unreadCountSpan} 173 |

174 | {headerSpan} - ({onlineUsersCount}) 175 |

176 |
177 | 178 | 182 |
183 |
184 | ); 185 | } 186 | }); 187 | 188 | module.exports = ChatroomComponent; 189 | -------------------------------------------------------------------------------- /src/views/components/connection-check/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ClassNames from 'classnames'; 3 | import L10nSpan from '../shared/l10n-span'; 4 | 5 | class ConnectionCheckComponent extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | isOnline: navigator.onLine 11 | }; 12 | 13 | this._checkConnection = this._checkConnection.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | window.addEventListener('online', this._checkConnection); 18 | window.addEventListener('offline', this._checkConnection); 19 | } 20 | 21 | _checkConnection() { 22 | this.setState({ 23 | isOnline: navigator.onLine 24 | }); 25 | } 26 | 27 | render() { 28 | const className = ClassNames({ 29 | 'connection-check-component': true, 30 | 'global-overlay': true, 31 | 'is-online': this.state.isOnline 32 | }); 33 | 34 | return ( 35 |
36 |

37 | 38 | 39 |

40 |
41 | ); 42 | } 43 | } 44 | 45 | module.exports = ConnectionCheckComponent; 46 | -------------------------------------------------------------------------------- /src/views/components/history/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Player from '../../modules/Player'; 3 | import HistoryManager from '../../../modules/HistoryManager'; 4 | import TracksComponent from '../shared/tracks'; 5 | 6 | class HistoryComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | tracks: [] 12 | }; 13 | 14 | this._clickToDeleteAll = this._clickToDeleteAll.bind(this); 15 | } 16 | 17 | componentWillMount() { 18 | HistoryManager.ready().then(() => { 19 | this.setState({ 20 | tracks: HistoryManager.tracks 21 | }); 22 | }); 23 | } 24 | 25 | componentDidMount() { 26 | HistoryManager.on('history-updated', (tracks) => { 27 | this.setState({ 28 | tracks: tracks 29 | }); 30 | }); 31 | } 32 | 33 | _clickToDeleteAll() { 34 | HistoryManager.clean(); 35 | } 36 | 37 | render() { 38 | let tracks = this.state.tracks; 39 | let controls = { 40 | trackModeButton: true, 41 | playAllButton: false, 42 | deleteAllButton: true, 43 | addToPlayQueueButton: false 44 | }; 45 | 46 | return ( 47 | 54 | ); 55 | } 56 | } 57 | 58 | module.exports = HistoryComponent; 59 | -------------------------------------------------------------------------------- /src/views/components/menus/playlist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Electron from 'electron'; 4 | 5 | import TabManager from '../../modules/TabManager'; 6 | import PlaylistManager from '../../../modules/PlaylistManager'; 7 | import Notifier from '../../modules/Notifier'; 8 | import Dialog from '../../modules/Dialog'; 9 | 10 | const Remote = Electron.remote; 11 | const Menu = Remote.Menu; 12 | const MenuItem = Remote.MenuItem; 13 | 14 | class PlaylistUI extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | _createContextMenuForPlaylist(playlist) { 20 | let menu = new Menu(); 21 | 22 | let removeMenuItem = new MenuItem({ 23 | label: 'Remove this playlist', 24 | click: () => { 25 | PlaylistManager 26 | .removePlaylistById(playlist.id) 27 | .catch((error) => { 28 | Notifier.alert(error); 29 | }); 30 | } 31 | }); 32 | 33 | let renameMenuItem = new MenuItem({ 34 | label: 'Rename this playlist', 35 | click: () => { 36 | Dialog.prompt({ 37 | title: 'Please input your playlist name', 38 | value: playlist.name, 39 | callback: (rawPlaylistName) => { 40 | rawPlaylistName = rawPlaylistName || ''; 41 | var sanitizedPlaylistName = rawPlaylistName.trim(); 42 | if (!sanitizedPlaylistName) { 43 | // do nothing 44 | } 45 | else { 46 | PlaylistManager 47 | .renamePlaylistById(playlist.id, sanitizedPlaylistName) 48 | .catch((error) => { 49 | Notifier.alert(error); 50 | }); 51 | } 52 | } 53 | }); 54 | } 55 | }); 56 | 57 | menu.append(renameMenuItem); 58 | menu.append(removeMenuItem); 59 | return menu; 60 | } 61 | 62 | _clickToShowContextMenu(playlist, event) { 63 | event.preventDefault(); 64 | let menu = this._createContextMenuForPlaylist(playlist); 65 | menu.popup(Remote.getCurrentWindow(), { 66 | async: true 67 | }); 68 | } 69 | 70 | _clickToSetTab(id) { 71 | TabManager.setTab('playlist', id); 72 | } 73 | 74 | render() { 75 | let index = this.props.index; 76 | let playlist = this.props.playlist; 77 | 78 | let clickToShowContextMenu = 79 | this._clickToShowContextMenu.bind(this, playlist); 80 | 81 | let clickToSetTab = 82 | this._clickToSetTab.bind(this, playlist.id); 83 | 84 | return ( 85 |
  • 86 | 93 | 94 | {playlist.name} 95 | 96 |
  • 97 | ); 98 | } 99 | }; 100 | 101 | PlaylistUI.propTypes = { 102 | index: PropTypes.number, 103 | playlist: PropTypes.object 104 | }; 105 | 106 | PlaylistUI.defaultProps = { 107 | index: 0, 108 | playlist: {} 109 | }; 110 | 111 | export default PlaylistUI; 112 | -------------------------------------------------------------------------------- /src/views/components/news/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import L10nSpan from '../shared/l10n-span'; 3 | import NewsTag from '../news/news-tag'; 4 | import NewsFetcher from '../../../modules/NewsFetcher'; 5 | 6 | class NewsComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | news: [] 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | NewsFetcher.get().then((news) => { 17 | this.setState({ 18 | news: news 19 | }); 20 | }); 21 | } 22 | 23 | render() { 24 | let news = this.state.news; 25 | 26 | return ( 27 |
    28 |
    29 |

    30 | 31 | 32 |

    33 |
    34 |
    35 | {news.map((eachNews, index) => { 36 | return 37 | })} 38 |
    39 |
    40 | ); 41 | } 42 | } 43 | 44 | module.exports = NewsComponent; 45 | -------------------------------------------------------------------------------- /src/views/components/news/news-tag.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ClassNames from 'classnames'; 4 | 5 | class NewsTag extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | let data = this.props.data; 12 | let title = `${data.date} - ${data.title}`; 13 | let content = data.content; 14 | let label = `panel-${data.label}` || 'panel-default'; 15 | 16 | let classObject = {}; 17 | classObject.panel = true; 18 | classObject[label] = true; 19 | const className = ClassNames(classObject); 20 | 21 | return ( 22 |
    23 |
    {title}
    24 |
    30 |
    31 | ); 32 | } 33 | } 34 | 35 | NewsTag.propTypes = { 36 | data: PropTypes.object.isRequired 37 | }; 38 | 39 | NewsTag.defaultProps = { 40 | date: '', 41 | title: '', 42 | content: '' 43 | }; 44 | 45 | module.exports = NewsTag; 46 | -------------------------------------------------------------------------------- /src/views/components/online-dj/choose-role-page.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import UuidV4 from 'uuid/v4'; 5 | import Validator from 'validator'; 6 | import ActionButton from '../shared/action-button'; 7 | import L10nSpan from '../shared/l10n-span'; 8 | 9 | class ChooseRolePage extends Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | _onSubmit(role, e) { 15 | e.preventDefault(); 16 | // TODO 17 | // move to formsy-react later 18 | let errors = []; 19 | let $form = $(e.target); 20 | let userInfo = { 21 | role: role 22 | }; 23 | 24 | if (role === 'dj') { 25 | userInfo.userName = $form.find('.user-name').val(); 26 | userInfo.roomName = $form.find('.room-name').val(); 27 | userInfo.roomKey = UuidV4(); 28 | 29 | if (!userInfo.roomName) { 30 | errors.push(role + '-room-name'); 31 | } 32 | } 33 | else { 34 | userInfo.userName = $form.find('.user-name').val(); 35 | userInfo.roomKey = $form.find('.room-key').val(); 36 | } 37 | 38 | if (!userInfo.userName.length) { 39 | errors.push(role + '-user-name'); 40 | } 41 | 42 | if (!Validator.isUUID(userInfo.roomKey, 4)) { 43 | errors.push(role + '-room-key'); 44 | } 45 | 46 | if (errors.length === 0) { 47 | this.props.onRoleChoose(userInfo); 48 | } 49 | else { 50 | Object.keys(this.refs).forEach((refName) => { 51 | let node = this.refs[refName]; 52 | if (errors.indexOf(refName) >= 0) { 53 | node.classList.add('has-error'); 54 | } 55 | else { 56 | node.classList.remove('has-error'); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | render() { 63 | let onDJSubmit = this._onSubmit.bind(this, 'dj'); 64 | let onGuestSubmit = this._onSubmit.bind(this, 'guest'); 65 | 66 | return ( 67 |
    68 | 80 |
    81 |
    82 |
    83 | 84 |
    85 |
    86 |
    87 | 90 |
    91 | 94 |
    95 |
    96 |
    97 | 100 |
    101 | 104 |
    105 |
    106 |
    107 |
    108 | 113 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 | 120 |
    121 |
    122 |
    123 | 126 |
    127 | 130 |
    131 |
    132 |
    133 | 136 |
    137 | 140 |
    141 |
    142 |
    143 |
    144 | 149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 | ); 156 | } 157 | } 158 | 159 | ChooseRolePage.propTypes = { 160 | onRoleChoose: PropTypes.func.isRequired 161 | }; 162 | 163 | ChooseRolePage.defaultProps = { 164 | onRoleChoose: function() { } 165 | }; 166 | 167 | export default ChooseRolePage; 168 | -------------------------------------------------------------------------------- /src/views/components/online-dj/dashboard-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Firebase from 'firebase'; 3 | import { 4 | clipboard as Clipboard 5 | } from 'electron'; 6 | import PropTypes from 'prop-types'; 7 | import ReactFireMixin from 'reactfire'; 8 | import Track from '../shared/track/track'; 9 | import L10nSpan from '../shared/l10n-span'; 10 | import Notifier from '../../modules/Notifier'; 11 | import ActionButton from '../shared/action-button'; 12 | import Constants from '../../../modules/Constants'; 13 | import L10nManager from '../../../modules/L10nManager'; 14 | import BaseTrack from 'kaku-core/models/track/BaseTrack'; 15 | 16 | let DashboardPage = React.createClass({ 17 | mixins: [ReactFireMixin], 18 | 19 | propTypes: { 20 | userInfo: PropTypes.object.isRequired, 21 | onLeft: PropTypes.func.isRequired 22 | }, 23 | 24 | getDefaultProps: function() { 25 | return { 26 | userInfo: {}, 27 | onLeft: function() {} 28 | }; 29 | }, 30 | 31 | getInitialState: function() { 32 | return { 33 | playedTracks: [] 34 | }; 35 | }, 36 | 37 | componentWillMount: function() { 38 | Firebase.on('setup', () => { 39 | let playedTracksRef = Firebase.joinPlayedTracksRoom(); 40 | this.bindAsArray(playedTracksRef, 'playedTracks'); 41 | }); 42 | 43 | Firebase.on('room-left', (roomName) => { 44 | if ('playedTracks' === roomName) { 45 | this.unbind('playedTracks'); 46 | } 47 | }); 48 | }, 49 | 50 | _onClickToCopy: function() { 51 | let node = this.refs['room-key']; 52 | let key = node.value; 53 | Clipboard.writeText(key); 54 | Notifier.alert(L10nManager.get('online_dj_dashboard_click_to_copy_key')); 55 | }, 56 | 57 | render: function() { 58 | let userInfo = this.props.userInfo; 59 | let role = userInfo.role; 60 | let roomKey = userInfo.roomKey; 61 | let playedTracks = this.state.playedTracks || []; 62 | playedTracks = playedTracks.map((rawTrackInfo) => { 63 | return BaseTrack.fromJSON(rawTrackInfo); 64 | }); 65 | 66 | // TODO 67 | // we can't use [].reverse() here, weird ! This may be the bug in 68 | // ReactFire 69 | let finalTracks = []; 70 | for (let i = 0, len = playedTracks.length; i < len; i++) { 71 | finalTracks[i] = playedTracks[len - 1 - i]; 72 | } 73 | 74 | /* jshint ignore:start */ 75 | return ( 76 |
    77 |
    78 |
    79 | 83 | 88 |
    89 |
    90 | 94 | 100 |
    101 | 105 | 110 |
    111 |
    112 | {finalTracks.map(function(track, index) { 113 | return ; 114 | })} 115 |
    116 |
    117 | ); 118 | /* jshint ignore:end */ 119 | } 120 | }); 121 | 122 | module.exports = DashboardPage; 123 | -------------------------------------------------------------------------------- /src/views/components/online-dj/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import L10nSpan from '../shared/l10n-span'; 3 | import ChooseRolePage from './choose-role-page'; 4 | import DashboardPage from './dashboard-page'; 5 | import Firebase from '../../../modules/wrapper/Firebase'; 6 | 7 | const MAX_PAGE_COUNT = 2; 8 | 9 | class OnlineDJComponent extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | page: 0, 15 | userInfo: {} 16 | }; 17 | 18 | this._onRoleChoose = this._onRoleChoose.bind(this); 19 | this._onLeft = this._onLeft.bind(this); 20 | this._changeToPage = this._changeToPage.bind(this); 21 | } 22 | 23 | _onRoleChoose(userInfo) { 24 | this.setState({ 25 | userInfo: userInfo 26 | }); 27 | 28 | // TODO 29 | // we need to change userInfo to make it cleaner later. 30 | // 31 | // { 32 | // role: 'xxx', 33 | // userName: 'xxx', 34 | // roomName: 'yyy', 35 | // roomKey: 'zzz' 36 | // } 37 | Firebase.setup(userInfo.roomKey, userInfo); 38 | 39 | let ref = Firebase.joinMetadataRoom(); 40 | if (userInfo.role === 'dj') { 41 | let metadata = { 42 | roomName: userInfo.roomName, 43 | roomKey: userInfo.roomKey 44 | }; 45 | Firebase.setMetadata(metadata); 46 | ref.set(metadata); 47 | } 48 | else { 49 | ref.on('value', (snapshot) => { 50 | Firebase.setMetadata(snapshot.val()); 51 | }); 52 | } 53 | this._changeToPage(1); 54 | } 55 | 56 | _onLeft() { 57 | Firebase.leaveAll(); 58 | this._changeToPage(0); 59 | } 60 | 61 | _changeToPage(page) { 62 | page = Math.max(0, Math.min(page, MAX_PAGE_COUNT - 1)); 63 | 64 | this.setState({ 65 | page: page 66 | }); 67 | } 68 | 69 | render() { 70 | let page = this.state.page; 71 | let userInfo = this.state.userInfo; 72 | let renderedElement; 73 | 74 | return ( 75 |
    76 |
    77 |

    78 | 79 | 80 |

    81 |
    82 |
    83 | 84 | 85 |
    86 |
    87 | ); 88 | } 89 | } 90 | 91 | module.exports = OnlineDJComponent; 92 | -------------------------------------------------------------------------------- /src/views/components/play-queue/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Player from '../../modules/Player'; 3 | import TracksComponent from '../shared/tracks'; 4 | 5 | class PlayQueueComponent extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | tracks: [] 11 | }; 12 | 13 | this._clickToPlayAll = this._clickToPlayAll.bind(this) 14 | this._clickToDeleteAll = this._clickToDeleteAll.bind(this) 15 | } 16 | 17 | componentDidMount() { 18 | Player.on('tracksUpdated', (tracks) => { 19 | this.setState({ 20 | tracks: tracks 21 | }); 22 | }); 23 | } 24 | 25 | _clickToDeleteAll() { 26 | Player.cleanupTracks(); 27 | } 28 | 29 | _clickToPlayAll() { 30 | Player.playNextTrack(0); 31 | } 32 | 33 | render() { 34 | let tracks = this.state.tracks; 35 | let controls = { 36 | trackModeButton: true, 37 | playAllButton: true, 38 | deleteAllButton: true, 39 | addToPlayQueueButton: false 40 | }; 41 | 42 | return ( 43 | 51 | ); 52 | } 53 | } 54 | 55 | module.exports = PlayQueueComponent; 56 | -------------------------------------------------------------------------------- /src/views/components/player/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Electron from 'electron'; 3 | import ClassNames from 'classnames'; 4 | import PlayerTrack from '../player/track'; 5 | import Player from '../../modules/Player'; 6 | import PlayerControlButtons from '../player/control-buttons'; 7 | 8 | const Remote = Electron.remote; 9 | const IpcRender = Electron.ipcRenderer; 10 | 11 | class PlayerComponent extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | tvMode: false 17 | }; 18 | 19 | this._onClickToToggleTVMode = this._onClickToToggleTVMode.bind(this); 20 | } 21 | 22 | _onClickToToggleTVMode() { 23 | // Note : this is not the same with fullscreen ! 24 | this.setState({ 25 | tvMode: !this.state.tvMode 26 | }); 27 | } 28 | 29 | componentDidMount() { 30 | IpcRender.on('key-Escape', () => { 31 | if (this.state.tvMode) { 32 | this.setState({ 33 | tvMode: false 34 | }); 35 | } 36 | }); 37 | 38 | // This is important because we use some CSS hack in TV mode and this 39 | // will influence the control bar in fullscreen, so we need to change 40 | // it back to normal mode to make sure the UI looks good ! 41 | Player.on('fullscreenchange', () => { 42 | this.setState({ 43 | tvMode: false 44 | }); 45 | }); 46 | } 47 | 48 | render() { 49 | const playerClass = ClassNames({ 50 | 'player': true, 51 | 'tv-mode': this.state.tvMode 52 | }); 53 | 54 | return ( 55 |
    56 | 57 | 58 |
    59 | ); 60 | } 61 | } 62 | 63 | module.exports = PlayerComponent; 64 | -------------------------------------------------------------------------------- /src/views/components/player/track.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Player from '../../modules/Player'; 3 | 4 | class PlayerTrack extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this._onPlayerPlay = this._onPlayerPlay.bind(this); 9 | this._onPlayerPause = this._onPlayerPause.bind(this); 10 | this._onPlayerProgress = this._onPlayerProgress.bind(this); 11 | this._setupPlayer = this._setupPlayer.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | this._setupPlayer(); 16 | } 17 | 18 | _onPlayerPlay() { 19 | 20 | } 21 | 22 | _onPlayerPause() { 23 | 24 | } 25 | 26 | _onPlayerProgress() { 27 | 28 | } 29 | 30 | _setupPlayer() { 31 | let playerDOM = document.createElement('video'); 32 | playerDOM.id = 'player'; 33 | playerDOM.classList.add('video-js'); 34 | playerDOM.classList.add('vjs-default-skin'); 35 | this.refs.playerComponent.appendChild(playerDOM); 36 | 37 | Player.setPlayer(playerDOM); 38 | Player.on('play', this._onPlayerPlay); 39 | Player.on('pause', this._onPlayerPause); 40 | Player.on('progress', this._onPlayerProgress); 41 | } 42 | 43 | render() { 44 | return ( 45 |
    48 |
    49 | ); 50 | } 51 | } 52 | 53 | module.exports = PlayerTrack; 54 | -------------------------------------------------------------------------------- /src/views/components/playlist/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TabManager from '../../modules/TabManager'; 3 | import Player from '../../modules/Player'; 4 | import PlaylistManager from '../../../modules/PlaylistManager'; 5 | import TracksComponent from '../shared/tracks'; 6 | 7 | class PlaylistComponent extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | playlist: {}, 13 | tracks: [] 14 | }; 15 | 16 | this._clickToPlayAll = this._clickToPlayAll.bind(this); 17 | } 18 | 19 | componentDidMount() { 20 | TabManager.on('changed', (tabName, tabOptions) => { 21 | if (tabName === 'playlist') { 22 | var playlistId = tabOptions; 23 | var playlist = PlaylistManager.findPlaylistById(playlistId); 24 | PlaylistManager.showPlaylistById(playlist.id); 25 | } 26 | else { 27 | // we should clean the internal state of activePlaylist 28 | PlaylistManager.hidePlaylist(); 29 | } 30 | }); 31 | 32 | PlaylistManager.on('shown', (playlist) => { 33 | this._boundUpdateInternalPlaylist = 34 | this._updateInternalPlaylist.bind(this, playlist); 35 | 36 | this.setState({ 37 | playlist: playlist, 38 | tracks: playlist.tracks 39 | }); 40 | 41 | playlist.removeListener('tracksUpdated', 42 | this._boundUpdateInternalPlaylist); 43 | playlist.on('tracksUpdated', this._boundUpdateInternalPlaylist); 44 | }); 45 | 46 | PlaylistManager.on('renamed', (playlist) => { 47 | if (playlist.id === this.state.playlist.id) { 48 | this._updateInternalPlaylist(playlist); 49 | } 50 | }); 51 | } 52 | 53 | _updateInternalPlaylist(playlist) { 54 | this.setState({ 55 | playlist: playlist 56 | }); 57 | } 58 | 59 | _clickToPlayAll() { 60 | let noUpdate = true; 61 | 62 | Player.cleanupTracks(noUpdate); 63 | Player.addTracks(this.state.tracks); 64 | Player.playNextTrack(0); 65 | } 66 | 67 | render() { 68 | let playlistName = this.state.playlist.name || ''; 69 | let tracks = this.state.tracks; 70 | let controls = { 71 | trackModeButton: true, 72 | playAllButton: true, 73 | deleteAllButton: false, 74 | addToPlayQueueButton: true 75 | }; 76 | 77 | return ( 78 | 85 | ); 86 | } 87 | } 88 | 89 | module.exports = PlaylistComponent; 90 | -------------------------------------------------------------------------------- /src/views/components/shared/action-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ClassNames from 'classnames'; 4 | import L10nSpan from './l10n-span'; 5 | 6 | class ActionButton extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | let buttonSpan; 13 | 14 | if (this.props.l10nId) { 15 | buttonSpan = ; 16 | } 17 | else { 18 | buttonSpan = {this.props.wording}; 19 | } 20 | 21 | return ( 22 | 30 | ); 31 | } 32 | } 33 | 34 | ActionButton.propTypes = { 35 | type: PropTypes.string, 36 | wording: PropTypes.string, 37 | l10nId: PropTypes.string, 38 | buttonClass: PropTypes.string, 39 | iconClass: PropTypes.string, 40 | isDisabled: PropTypes.bool, 41 | onClick: PropTypes.func 42 | }; 43 | 44 | ActionButton.defaultProps = { 45 | type: 'button', 46 | wording: '', 47 | l10nId: '', 48 | buttonClass: '', 49 | iconClass: '', 50 | isDisabled: false, 51 | onClick: function() {} 52 | }; 53 | 54 | export default ActionButton; 55 | -------------------------------------------------------------------------------- /src/views/components/shared/add-to-play-queue-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import L10nSpan from './l10n-span'; 4 | import Player from '../../modules/Player'; 5 | 6 | class AddToPlayQueueButton extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this._clickToAddToPlayQueue = this._clickToAddToPlayQueue.bind(this); 11 | } 12 | 13 | _clickToAddToPlayQueue() { 14 | Player.addTracks(this.props.data); 15 | } 16 | 17 | render() { 18 | let isDiabled = (this.props.data.length === 0); 19 | 20 | return ( 21 | 28 | ); 29 | } 30 | } 31 | 32 | AddToPlayQueueButton.propTypes = { 33 | data: PropTypes.array.isRequired 34 | }; 35 | 36 | AddToPlayQueueButton.defaultProps = { 37 | data: [] 38 | }; 39 | 40 | module.exports = AddToPlayQueueButton; 41 | -------------------------------------------------------------------------------- /src/views/components/shared/l10n-span.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import L10nManager from '../../../modules/L10nManager'; 4 | 5 | const _ = L10nManager.get.bind(L10nManager); 6 | 7 | class L10nSpan extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | translation: '' 13 | }; 14 | 15 | this._updateL10nTranslation = this._updateL10nTranslation.bind(this); 16 | } 17 | 18 | componentDidMount() { 19 | this._updateL10nTranslation(); 20 | 21 | L10nManager.on('language-initialized', () => { 22 | this._updateL10nTranslation(); 23 | }); 24 | 25 | L10nManager.on('language-changed', () => { 26 | this._updateL10nTranslation(); 27 | }); 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | // NOTE 32 | // we need to add this to make manually changing l10nId work. 33 | this._updateL10nTranslation(nextProps); 34 | } 35 | 36 | _updateL10nTranslation(nextProps) { 37 | let props = nextProps || this.props; 38 | let translation = _(props.l10nId, props.l10nParams); 39 | this.setState({ 40 | translation: translation 41 | }); 42 | } 43 | 44 | render() { 45 | let translation = this.state.translation; 46 | let where = this.props.where || 'default'; 47 | let children = null; 48 | let props = {}; 49 | 50 | // clone props at first 51 | for (let key in this.props) { 52 | props[key] = this.props[key]; 53 | } 54 | 55 | if (this.props.children) { 56 | children = this.props.children; 57 | } 58 | 59 | if (where === 'default') { 60 | if (!children) { 61 | children = translation; 62 | } 63 | else { 64 | console.error('Can\'t assign l10nId in l10nSpan with children inside'); 65 | console.error('We will directly ignore the translation here'); 66 | } 67 | } 68 | else { 69 | // TODO 70 | // dangerous, we should add more protect 71 | props[where] = translation; 72 | } 73 | 74 | // [Note] - can be refactored later 75 | // 76 | // React.js will check unknown props for us, it's a quick hack to remove 77 | // them because of the effort to change the interface. 78 | 79 | props['data-l10nId'] = props.l10nId; 80 | props['data-l10nParams'] = props.l10nParams; 81 | props['data-where'] = props.where; 82 | 83 | delete props.l10nId; 84 | delete props.l10nParams; 85 | delete props.where; 86 | 87 | return React.createElement('span', props, children); 88 | } 89 | } 90 | 91 | L10nSpan.propTypes = { 92 | l10nId: PropTypes.string.isRequired, 93 | l10nParams: PropTypes.object, 94 | where: PropTypes.string 95 | }; 96 | 97 | L10nSpan.defaultProps = { 98 | l10nId: '', 99 | l10nParams: {} 100 | }; 101 | 102 | module.exports = L10nSpan; 103 | -------------------------------------------------------------------------------- /src/views/components/shared/track-mode-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ClassNames from 'classnames'; 4 | import PreferenceManager from '../../../modules/PreferenceManager'; 5 | 6 | const PREFERENCE_KEY = 'default.track.mode'; 7 | 8 | class TrackModeButton extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | mode: 'square' 14 | }; 15 | 16 | this._onClick = this._onClick.bind(this); 17 | } 18 | 19 | componentDidMount() { 20 | let mode = PreferenceManager.getPreference(PREFERENCE_KEY); 21 | if (mode) { 22 | this.setState({ 23 | mode: mode 24 | }); 25 | 26 | this.props.onTrackModeChange(mode); 27 | } 28 | 29 | PreferenceManager.on('preference-updated', (key, newMode) => { 30 | if (key === PREFERENCE_KEY) { 31 | this.setState({ 32 | mode: newMode 33 | }); 34 | this.props.onTrackModeChange(newMode); 35 | } 36 | }); 37 | } 38 | 39 | _onClick(event) { 40 | let target = event.target; 41 | let newMode = target.dataset.mode; 42 | if (newMode !== this.state.mode) { 43 | PreferenceManager.setPreference(PREFERENCE_KEY, newMode); 44 | } 45 | } 46 | 47 | render() { 48 | let mode = this.state.mode; 49 | 50 | let listButtonClass = ClassNames({ 51 | 'btn': true, 52 | 'btn-default': true, 53 | 'track-list-mode': true, 54 | 'active': (mode === 'list') 55 | }); 56 | 57 | let squareButtonClass = ClassNames({ 58 | 'btn': true, 59 | 'btn-default': true, 60 | 'track-square-mode': true, 61 | 'active': (mode === 'square') 62 | }); 63 | 64 | return ( 65 |
    66 | 73 | 80 |
    81 | ); 82 | } 83 | } 84 | 85 | TrackModeButton.propTypes = { 86 | onTrackModeChange: PropTypes.func 87 | }; 88 | 89 | TrackModeButton.defaultProps = { 90 | onTrackModeChange: function() {} 91 | }; 92 | 93 | export default TrackModeButton; 94 | -------------------------------------------------------------------------------- /src/views/components/shared/track/no-track.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import L10nSpan from '../l10n-span'; 3 | 4 | class NoTrack extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 |
    12 | 13 |
    14 | ); 15 | } 16 | } 17 | 18 | module.exports = NoTrack; 19 | -------------------------------------------------------------------------------- /src/views/components/shared/track/track-list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class TrackList extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | return ( 10 |
    14 | 15 | 16 |
    {this.props.track.artist}
    17 |
    {this.props.track.title}
    18 |
    19 | ); 20 | } 21 | } 22 | 23 | module.exports = TrackList; 24 | -------------------------------------------------------------------------------- /src/views/components/shared/track/track-square.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class TrackSquare extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | return ( 10 |
    15 | 16 |
    17 | 18 |
    19 |
    20 |
    {this.props.track.title}
    21 |
    {this.props.track.artist}
    22 |
    23 |
    24 | ); 25 | } 26 | } 27 | 28 | module.exports = TrackSquare; 29 | -------------------------------------------------------------------------------- /src/views/components/shared/track/track.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Electron from 'electron'; 4 | import ClassNames from 'classnames'; 5 | 6 | import PlaylistManager from '../../../../modules/PlaylistManager'; 7 | import Notifier from '../../../modules/Notifier'; 8 | import Player from '../../../modules/Player'; 9 | 10 | import TrackList from './track-list'; 11 | import TrackSquare from './track-square'; 12 | import TabManager from '../../../modules/TabManager'; 13 | 14 | const Remote = Electron.remote; 15 | const Menu = Remote.Menu; 16 | const MenuItem = Remote.MenuItem; 17 | 18 | class Track extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | playingTrack: {} 24 | }; 25 | 26 | this._setPlayingTrack = this._setPlayingTrack.bind(this); 27 | this._createContextMenu = this._createContextMenu.bind(this); 28 | } 29 | 30 | componentDidMount() { 31 | Player.on('play', this._setPlayingTrack); 32 | } 33 | 34 | componentWillUnmount() { 35 | Player.off('play', this._setPlayingTrack); 36 | } 37 | 38 | _setPlayingTrack() { 39 | this.setState({ 40 | playingTrack: Player.playingTrack 41 | }); 42 | } 43 | 44 | _clickToPlay(track) { 45 | if (TabManager.tabName === 'play-queue') { 46 | let index = this.props.index; 47 | Player.playNextTrack(index); 48 | } 49 | else { 50 | let noUpdate = true; 51 | Player.cleanupTracks(noUpdate); 52 | Player.addTracks([track]); 53 | Player.playNextTrack(0); 54 | } 55 | } 56 | 57 | _clickToShowContextMenu(track, event) { 58 | // TODO 59 | // if we are under playlist section already, 60 | // we should not shown this context menu 61 | event.preventDefault(); 62 | let menu = this._createContextMenu(track); 63 | menu.popup(Remote.getCurrentWindow(), { 64 | async: true 65 | }); 66 | } 67 | 68 | _createContextMenu(track) { 69 | let menu = new Menu(); 70 | let playlists = PlaylistManager.playlists; 71 | 72 | playlists.forEach((playlist) => { 73 | let clickToAddTrack = ((playlist) => { 74 | return () => { 75 | playlist 76 | .addTrack(track) 77 | .catch((error) => { 78 | Notifier.alert(error); 79 | }); 80 | }; 81 | })(playlist); 82 | 83 | let clickToRemoveTrack = ((playlist) => { 84 | return () => { 85 | playlist 86 | .removeTrack(track) 87 | .catch((error) => { 88 | Notifier.alert(error); 89 | }); 90 | }; 91 | })(playlist); 92 | 93 | // TODO 94 | // add l10n support here 95 | let menuItemToAddTrack = new MenuItem({ 96 | label: `Add to ${playlist.name}`, 97 | click: clickToAddTrack 98 | }); 99 | 100 | let menuItemToRemoveTrack = new MenuItem({ 101 | label: `Remove from ${playlist.name}`, 102 | click: clickToRemoveTrack 103 | }); 104 | 105 | if (PlaylistManager.isDisplaying) { 106 | if (PlaylistManager.activePlaylist.isSameWith(playlist)) { 107 | menu.append(menuItemToRemoveTrack); 108 | } 109 | else { 110 | menu.insert(0, menuItemToAddTrack); 111 | } 112 | } 113 | else { 114 | // TODO 115 | // we have to check if this track does exist in this playlist, 116 | // but no matter how, right now we have internal protect in 117 | // playlist.addTrack() to make sure we won't add the same track 118 | // to the same playlist. 119 | menu.insert(0, menuItemToAddTrack); 120 | } 121 | }); 122 | return menu; 123 | } 124 | 125 | render() { 126 | let mode = this.props.mode; 127 | let track = this.props.data; 128 | let trackClassName = ClassNames({ 129 | track: true, 130 | 'track-square': (mode === 'square'), 131 | 'track-list': (mode === 'list'), 132 | active: track.isSameTrackWith(this.state.playingTrack) 133 | }); 134 | 135 | let iconObject = {}; 136 | iconObject.fa = true; 137 | 138 | switch (track.trackType) { 139 | case 'YoutubeTrack': 140 | iconObject['fa-youtube'] = true; 141 | break; 142 | 143 | case 'VimeoTrack': 144 | iconObject['fa-vimeo'] = true; 145 | break; 146 | 147 | case 'SoundCloudTrack': 148 | iconObject['fa-soundcloud'] = true; 149 | break; 150 | 151 | case 'MixCloudTrack': 152 | iconObject['fa-mixcloud'] = true; 153 | break; 154 | 155 | default: 156 | iconObject['fa-music'] = true; 157 | break; 158 | } 159 | 160 | let iconClassName = ClassNames(iconObject); 161 | let trackUI; 162 | let trackProps = { 163 | track: track, 164 | onClick: this._clickToPlay.bind(this, track), 165 | onContextMenu: this._clickToShowContextMenu.bind(this, track), 166 | iconClassName: iconClassName, 167 | trackClassName: trackClassName 168 | }; 169 | 170 | // We will dispatch do different views here based on incoming mode 171 | if (mode === 'square') { 172 | trackUI = ; 173 | } 174 | else if (mode === 'list') { 175 | trackUI = ; 176 | } 177 | 178 | return trackUI; 179 | } 180 | } 181 | 182 | Track.propTypes = { 183 | data: PropTypes.object.isRequired, 184 | mode: PropTypes.string, 185 | index: PropTypes.number 186 | }; 187 | 188 | Track.defaultProps = { 189 | data: {}, 190 | mode: 'square', 191 | index: -1 192 | }; 193 | 194 | module.exports = Track; 195 | -------------------------------------------------------------------------------- /src/views/components/shared/tracks.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactTooltip from 'react-tooltip'; 4 | import Track from './track/track'; 5 | import NoTrack from './track/no-track'; 6 | import L10nSpan from './l10n-span'; 7 | import ActionButton from './action-button'; 8 | import TrackModeButton from './track-mode-button'; 9 | import AddToPlayQueueButton from './add-to-play-queue-button'; 10 | 11 | class TracksComponent extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | trackMode: 'square' 17 | }; 18 | 19 | this._onTrackModeChange = this._onTrackModeChange.bind(this); 20 | } 21 | 22 | _onTrackModeChange(mode) { 23 | this.setState({ 24 | trackMode: mode 25 | }); 26 | } 27 | 28 | componentDidUpdate() { 29 | // Only rebuild the tooltip when we are under sqaure more 30 | // this can avoid manipulate DOM tree too much 31 | if (this.state.trackMode === 'square') { 32 | ReactTooltip.rebuild(); 33 | } 34 | } 35 | 36 | render() { 37 | let { 38 | tracks, 39 | headerL10nId, 40 | headerWording, 41 | headerIconClass, 42 | controls, 43 | onDeleteAllClick, 44 | onPlayAllClick 45 | } = this.props; 46 | 47 | let trackMode = this.state.trackMode; 48 | let noTracks = (tracks.length === 0); 49 | 50 | // TODO 51 | // right now L10nSpan is just a span, we can extend it so that we can 52 | // also pass plain strings. 53 | let headerSpan; 54 | if (headerL10nId) { 55 | headerSpan = ; 56 | } 57 | else { 58 | headerSpan = {headerWording}; 59 | } 60 | 61 | let deleteAllButton; 62 | if (controls.deleteAllButton) { 63 | deleteAllButton = 64 | 70 | } 71 | 72 | let trackModeButton; 73 | if (controls.trackModeButton) { 74 | trackModeButton = 75 | 77 | } 78 | 79 | let playAllButton; 80 | if (controls.playAllButton) { 81 | playAllButton = 82 | 88 | } 89 | 90 | let addToPlayQueueButton; 91 | if (controls.addToPlayQueueButton) { 92 | addToPlayQueueButton = 93 | 94 | } 95 | 96 | let noTracksDiv; 97 | if (noTracks) { 98 | noTracksDiv = ; 99 | } 100 | 101 | return ( 102 |
    103 |
    104 |

    105 | 106 | {headerSpan} 107 |

    108 |
    109 | {trackModeButton} 110 | {addToPlayQueueButton} 111 | {deleteAllButton} 112 | {playAllButton} 113 |
    114 |
    115 |
    116 | {noTracksDiv} 117 | {tracks.map(function(track, index) { 118 | return ; 119 | })} 120 |
    121 |
    122 | ); 123 | } 124 | } 125 | 126 | TracksComponent.propTypes = { 127 | headerL10nId: PropTypes.string, 128 | headerWording: PropTypes.string, 129 | headerIconClass: PropTypes.string.isRequired, 130 | controls: PropTypes.object, 131 | tracks: PropTypes.array.isRequired, 132 | onDeleteAllClick: PropTypes.func, 133 | onPlayAllClick: PropTypes.func 134 | }; 135 | 136 | TracksComponent.defaultProps = { 137 | headerL10nId: '', 138 | headerWording: '', 139 | headerIconClass: '', 140 | controls: { 141 | trackModeButton: true, 142 | playAllButton: true, 143 | addToPlayQueueButton: true, 144 | deleteAllButton: false 145 | }, 146 | onDeleteAllClick: function() {}, 147 | onPlayAllClick: function() {}, 148 | tracks: [] 149 | }; 150 | 151 | module.exports = TracksComponent; 152 | -------------------------------------------------------------------------------- /src/views/components/toolbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Electron from 'electron'; 3 | import SearchbarComponent from '../searchbar'; 4 | import DownloadManager from '../../../modules/DownloadManager'; 5 | import AppCore from '../../../modules/AppCore'; 6 | 7 | const Remote = Electron.remote; 8 | const App = Remote.app; 9 | 10 | class ToolbarComponent extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | downloadPercent: 0, 16 | title: '' 17 | }; 18 | 19 | this._onCloseButtonClick = this._onCloseButtonClick.bind(this); 20 | this._onShrinkButtonClick = this._onShrinkButtonClick.bind(this); 21 | this._onEnlargeButtonClick = this._onEnlargeButtonClick.bind(this); 22 | this._processDownloadProgress = this._processDownloadProgress.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | this.setState({ 27 | title: AppCore.title 28 | }); 29 | 30 | AppCore.on('titleUpdated', (title) => { 31 | this.setState({ 32 | title: title 33 | }); 34 | }); 35 | 36 | DownloadManager.on('download-progress', (percent) => { 37 | this._processDownloadProgress(percent); 38 | }); 39 | 40 | DownloadManager.on('download-finish', () => { 41 | this._processDownloadProgress(0); 42 | }); 43 | 44 | DownloadManager.on('download-error', () => { 45 | this._processDownloadProgress(0); 46 | }); 47 | } 48 | 49 | _onCloseButtonClick() { 50 | Remote.getCurrentWindow().close(); 51 | } 52 | 53 | _onShrinkButtonClick() { 54 | Remote.getCurrentWindow().minimize(); 55 | } 56 | 57 | _onEnlargeButtonClick() { 58 | let win = Remote.getCurrentWindow(); 59 | if (win.isMaximized()) { 60 | win.unmaximize(); 61 | } 62 | else { 63 | win.maximize(); 64 | } 65 | } 66 | 67 | _processDownloadProgress(percent) { 68 | this.setState({ 69 | downloadPercent: percent 70 | }); 71 | } 72 | 73 | render() { 74 | let title = this.state.title; 75 | let downloadPercent = this.state.downloadPercent; 76 | let progressStyle = { 77 | width: `${downloadPercent}%` 78 | }; 79 | 80 | return ( 81 |
    82 |
    83 | 86 | 87 | 88 | 91 | 92 | 93 | 96 | 97 | 98 |
    99 |
    100 | {title} 101 |
    102 |
    103 | 104 |
    105 |
    106 |
    107 | ); 108 | } 109 | } 110 | 111 | module.exports = ToolbarComponent; 112 | -------------------------------------------------------------------------------- /src/views/components/topranking/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TopRanking from 'kaku-core/modules/TopRanking'; 3 | import TracksComponent from '../shared/tracks'; 4 | import Player from '../../modules/Player'; 5 | 6 | class TopRankingComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | tracks: [] 12 | }; 13 | 14 | this._clickToPlayAll = this._clickToPlayAll.bind(this); 15 | } 16 | 17 | componentDidMount() { 18 | TopRanking.on('topRanking-changed', () => { 19 | TopRanking.get().then((tracks) => { 20 | this.setState({ 21 | tracks: tracks 22 | }); 23 | }); 24 | }); 25 | 26 | TopRanking.get().then((tracks) => { 27 | this.setState({ 28 | tracks: tracks 29 | }); 30 | }); 31 | } 32 | 33 | _clickToPlayAll() { 34 | let noUpdate = true; 35 | Player.cleanupTracks(noUpdate); 36 | Player.addTracks(this.state.tracks); 37 | Player.playNextTrack(0); 38 | } 39 | 40 | render() { 41 | let tracks = this.state.tracks; 42 | let controls = { 43 | trackModeButton: true, 44 | playAllButton: true, 45 | deleteAllButton: false, 46 | addToPlayQueueButton: true 47 | }; 48 | 49 | return ( 50 | 57 | ); 58 | } 59 | } 60 | 61 | module.exports = TopRankingComponent; 62 | -------------------------------------------------------------------------------- /src/views/modules/AppMenus.js: -------------------------------------------------------------------------------- 1 | import Electron from 'electron'; 2 | import Notifier from './Notifier'; 3 | import Player from './Player'; 4 | import L10nManager from '../../modules/L10nManager'; 5 | import DownloadManager from '../../modules/DownloadManager'; 6 | 7 | const Remote = Electron.remote; 8 | const App = Remote.app; 9 | const Menu = Remote.Menu; 10 | const Dialog = Remote.dialog; 11 | const MenuItem = Remote.MenuItem; 12 | const BrowserWindow = Remote.BrowserWindow; 13 | 14 | const _ = L10nManager.get.bind(L10nManager); 15 | 16 | class AppMenus { 17 | constructor() { 18 | L10nManager.on('language-changed', () => { 19 | this.build(); 20 | }); 21 | } 22 | 23 | build() { 24 | let templates = this._getMenusTemplateForCurrentPlatform(); 25 | let menus = Menu.buildFromTemplate(templates); 26 | Menu.setApplicationMenu(menus); 27 | } 28 | 29 | _getMenuTemplateForAbout() { 30 | return { 31 | label: _('app_menu_kaku'), 32 | submenu: [ 33 | { role: 'about', }, 34 | { type: 'separator' }, 35 | { role: 'services', submenu: [] }, 36 | { type: 'separator' }, 37 | { role: 'hide' }, 38 | { role: 'hideothers' }, 39 | { role: 'unhide' }, 40 | { type: 'separator' }, 41 | { role: 'quit' } 42 | ] 43 | }; 44 | } 45 | 46 | _getMenuTemplateForEdit() { 47 | return { 48 | label: _('app_menu_edit'), 49 | submenu: [ 50 | { role: 'undo' }, 51 | { role: 'redo' }, 52 | { type: 'separator' }, 53 | { role: 'cut' }, 54 | { role: 'copy' }, 55 | { role: 'paste' }, 56 | { role: 'pasteandmatchstyle' }, 57 | { role: 'delete' }, 58 | { role: 'selectall' } 59 | ] 60 | }; 61 | } 62 | 63 | _getMenuTemplateForView() { 64 | return { 65 | label: _('app_menu_view'), 66 | submenu: [ 67 | { 68 | label: _('app_menu_search_track'), 69 | accelerator: 'CmdOrCtrl+F', 70 | click() { 71 | // TODO 72 | // need to move this out to its own module 73 | document.querySelector('.searchbar-user-input').focus(); 74 | } 75 | }, 76 | { type: 'separator' }, 77 | { role: 'reload' }, 78 | { role: 'forcereload' }, 79 | { role: 'toggledevtools' }, 80 | { type: 'separator' }, 81 | { role: 'resetzoom' }, 82 | { role: 'zoomin' }, 83 | { role: 'zoomout' }, 84 | { type: 'separator' }, 85 | { role: 'togglefullscreen' } 86 | ] 87 | }; 88 | } 89 | 90 | _getMenuTemplateForControl() { 91 | return { 92 | label: _('app_menu_control'), 93 | submenu: [ 94 | { 95 | label: _('app_menu_play_previous_track'), 96 | accelerator: 'CmdOrCtrl+Left', 97 | click() { 98 | Player.playPreviousTrack(); 99 | } 100 | }, 101 | { 102 | label: _('app_menu_play_or_pause_track'), 103 | accelerator: 'CmdOrCtrl+P', 104 | click() { 105 | Player.playOrPause(); 106 | } 107 | }, 108 | { 109 | label: _('app_menu_play_next_track'), 110 | accelerator: 'CmdOrCtrl+Right', 111 | click() { 112 | Player.playNextTrack(); 113 | } 114 | }, 115 | { 116 | type: 'separator' 117 | }, 118 | { 119 | label: _('app_menu_increase_volume'), 120 | accelerator: 'CmdOrCtrl+Up', 121 | click() { 122 | Player.setVolume('up'); 123 | } 124 | }, 125 | { 126 | label: _('app_menu_decrease_volume'), 127 | accelerator: 'CmdOrCtrl+Down', 128 | click() { 129 | Player.setVolume('down'); 130 | } 131 | }, 132 | { 133 | type: 'separator' 134 | }, 135 | { 136 | label: _('app_menu_download_track'), 137 | accelerator: 'CmdOrCtrl+D', 138 | click() { 139 | let track = Player.playingTrack; 140 | if (track) { 141 | let filename = track.title + '.' + track.ext; 142 | let src = track.platformTrackRealUrl; 143 | Dialog.showSaveDialog({ 144 | title: _('app_menu_where_to_download_trak'), 145 | defaultPath: filename 146 | }, (path) => { 147 | if (path) { 148 | Notifier.alert(_('app_menu_start_download_track')); 149 | let req = DownloadManager.download(src, path); 150 | req.on('error', () => { 151 | Notifier.alert(_('app_menu_start_download_track_error')); 152 | }).on('close', () => { 153 | Notifier.alert(_('app_menu_start_download_track_success')); 154 | }); 155 | } 156 | }); 157 | } 158 | } 159 | } 160 | ] 161 | }; 162 | } 163 | 164 | _getMenuTemplateForWindow() { 165 | return { 166 | label: _('app_menu_window'), 167 | submenu: [ 168 | { role: 'close' }, 169 | { role: 'minimize' }, 170 | { role: 'zoom' }, 171 | { type: 'separator' }, 172 | { role: 'front' } 173 | ] 174 | }; 175 | } 176 | 177 | _getMenusTemplateForCurrentPlatform() { 178 | let templates = []; 179 | let platform = process.platform; 180 | 181 | switch (platform) { 182 | case 'darwin': 183 | templates.push(this._getMenuTemplateForAbout()); 184 | templates.push(this._getMenuTemplateForEdit()); 185 | templates.push(this._getMenuTemplateForView()); 186 | templates.push(this._getMenuTemplateForControl()); 187 | templates.push(this._getMenuTemplateForWindow()); 188 | break; 189 | 190 | default: 191 | templates.push(this._getMenuTemplateForView()); 192 | templates.push(this._getMenuTemplateForControl()); 193 | break; 194 | } 195 | 196 | return templates; 197 | } 198 | } 199 | 200 | module.exports = new AppMenus(); 201 | -------------------------------------------------------------------------------- /src/views/modules/AppTouchBar.js: -------------------------------------------------------------------------------- 1 | import Electron from 'electron'; 2 | import L10nManager from '../../modules/L10nManager'; 3 | 4 | const _ = L10nManager.get.bind(L10nManager); 5 | const Remote = Electron.remote; 6 | const TouchBar = Remote.TouchBar; 7 | const App = Remote.app; 8 | const BrowserWindow = Remote.BrowserWindow; 9 | 10 | const { 11 | TouchBarButton, 12 | TouchBarSpacer 13 | } = TouchBar; 14 | 15 | class AppTouchBar { 16 | constructor() { 17 | L10nManager.on('language-changed', () => { 18 | this.destroy(); 19 | this.build(); 20 | }); 21 | } 22 | 23 | build() { 24 | if (process.platform !== 'darwin') { 25 | return; 26 | } 27 | 28 | const mainWindow = Remote.getCurrentWindow(); 29 | 30 | const playOrPauseButton = new TouchBarButton({ 31 | label: _('touchbar_play_pause'), 32 | click: () => { 33 | mainWindow.webContents.send('key-MediaPlayPause'); 34 | } 35 | }); 36 | 37 | const playPreviousButton = new TouchBarButton({ 38 | label: `◀ ${_('touchbar_play_previous_track')}`, 39 | click: () => { 40 | mainWindow.webContents.send('key-MediaPreviousTrack'); 41 | } 42 | }); 43 | 44 | const playNextButton = new TouchBarButton({ 45 | label: `${_('touchbar_play_next_track')} ▶`, 46 | click: () => { 47 | mainWindow.webContents.send('key-MediaNextTrack'); 48 | } 49 | }); 50 | 51 | const touchbar = new TouchBar([ 52 | new TouchBarSpacer({size: 'flexible'}), 53 | playPreviousButton, 54 | playOrPauseButton, 55 | playNextButton 56 | ]); 57 | 58 | mainWindow.setTouchBar(touchbar); 59 | } 60 | 61 | destroy() { 62 | if (process.platform !== 'darwin') { 63 | return; 64 | } 65 | 66 | const mainWindow = Remote.getCurrentWindow(); 67 | mainWindow.setTouchBar(); 68 | } 69 | } 70 | 71 | module.exports = new AppTouchBar(); 72 | -------------------------------------------------------------------------------- /src/views/modules/AppTray.js: -------------------------------------------------------------------------------- 1 | import Electron from 'electron'; 2 | import path from 'path'; 3 | import L10nManager from '../../modules/L10nManager'; 4 | 5 | const Shell = Electron.shell; 6 | const Remote = Electron.remote; 7 | const Dialog = Remote.dialog; 8 | const Menu = Remote.Menu; 9 | const Tray = Remote.Tray; 10 | const App = Remote.app; 11 | 12 | const _ = L10nManager.get.bind(L10nManager); 13 | const iconsFolder = 14 | path.join(App.getAppPath(), 'src', 'public', 'images', 'icons'); 15 | const trayDefaultIcon = path.join(iconsFolder, 'tray', 'default.png'); 16 | const trayWindowsIcon = path.join(iconsFolder, 'tray', 'windows.ico'); 17 | 18 | class AppTray { 19 | constructor() { 20 | this.tray = null; 21 | 22 | L10nManager.on('language-changed', () => { 23 | this.destroy(); 24 | this.build(); 25 | }); 26 | } 27 | 28 | build() { 29 | let mainWindow = Remote.getCurrentWindow(); 30 | let icon = trayDefaultIcon; 31 | 32 | if (process.platform === 'win32') { 33 | icon = trayWindowsIcon; 34 | } 35 | 36 | this.tray = new Tray(icon); 37 | 38 | const trayMenu = [ 39 | { 40 | label: _('app_tray_show_or_minimize'), 41 | click() { 42 | !mainWindow.isMinimized() ? mainWindow.minimize() : mainWindow.show(); 43 | } 44 | }, 45 | { 46 | type: 'separator' 47 | }, 48 | { 49 | label: _('app_tray_play_previous_track'), 50 | click() { 51 | mainWindow.webContents.send('tray-MediaPreviousTrack'); 52 | } 53 | }, 54 | { 55 | label: _('app_tray_play_next_track'), 56 | click() { 57 | mainWindow.webContents.send('tray-MediaNextTrack'); 58 | } 59 | }, 60 | { 61 | label: _('app_tray_play_or_pause_track'), 62 | click() { 63 | mainWindow.webContents.send('tray-MediaPlayPause'); 64 | } 65 | }, 66 | { 67 | type: 'separator' 68 | }, 69 | { 70 | label: _('app_tray_help'), 71 | submenu: [{ 72 | label: _('app_tray_docs'), 73 | click() { 74 | Shell.openExternal('https://kaku.rocks/docs/index.html'); 75 | } 76 | }, { 77 | label: _('app_tray_issue'), 78 | click() { 79 | Shell.openExternal('https://github.com/EragonJ/Kaku/issues'); 80 | } 81 | } 82 | ] 83 | }, 84 | { 85 | label: _('app_tray_quit'), 86 | click() { 87 | App.quit(); 88 | } 89 | } 90 | ]; 91 | 92 | this.tray.setToolTip('Kaku'); 93 | this.tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); 94 | } 95 | 96 | destroy() { 97 | if (this.tray) { 98 | this.tray.destroy(); 99 | } 100 | } 101 | } 102 | 103 | module.exports = new AppTray(); 104 | -------------------------------------------------------------------------------- /src/views/modules/CastingManager.js: -------------------------------------------------------------------------------- 1 | import Mdns from 'mdns-js'; 2 | import Player from './Player'; 3 | import { EventEmitter } from 'events'; 4 | import { Client, DefaultMediaReceiver } from 'castv2-client'; 5 | const Browser = Mdns.createBrowser(Mdns.tcp('googlecast')); 6 | 7 | class CastingManager extends EventEmitter { 8 | constructor() { 9 | super(); 10 | 11 | this.connected = false; 12 | this.services = new Map(); 13 | this._initialized = false; 14 | this._client = null; 15 | this._castingPlayer = null; 16 | this._castingTrack = null; 17 | } 18 | 19 | init() { 20 | if (this._initialized) { 21 | return; 22 | } 23 | this._initialized = true; 24 | 25 | Browser.on('ready', () => { 26 | Browser.discover(); 27 | }); 28 | 29 | Browser.on('update', (service) => { 30 | let address = service.addresses[0]; 31 | this.services.set(address, { 32 | name: service.fullname || 'Unknown', 33 | address: address, 34 | port: service.port 35 | }); 36 | }); 37 | 38 | Player.on('seeked', () => { 39 | this.seek(Player.playingTrackTime); 40 | }); 41 | 42 | Player.on('play', () => { 43 | this.play(Player.playingTrack); 44 | }); 45 | 46 | Player.on('pause', () => { 47 | this.pause(); 48 | }); 49 | 50 | Player.on('stop', () => { 51 | this.stop(); 52 | }); 53 | } 54 | 55 | close() { 56 | this.connected = false; 57 | this._castingPlayer = null; 58 | if (this._client) { 59 | this._client = null; 60 | } 61 | } 62 | 63 | connect(address) { 64 | if (!address) { 65 | return; 66 | } 67 | 68 | this.emit('connecting'); 69 | 70 | this._client = new Client(); 71 | this._client.connect(address, () => { 72 | this._client.launch(DefaultMediaReceiver, (err, player) => { 73 | if (err) { 74 | console.log(err); 75 | } 76 | else { 77 | this._castingPlayer = player; 78 | this.connected = true; 79 | this.emit('connected'); 80 | } 81 | }); 82 | }); 83 | 84 | this._client.on('close', () => { 85 | this.close(); 86 | }); 87 | 88 | this._client.on('error', (err) => { 89 | console.log('Error: %s', err.message); 90 | this.emit('error', err); 91 | this.close(); 92 | }); 93 | } 94 | 95 | _ready() { 96 | return !!this._castingPlayer; 97 | } 98 | 99 | play(track) { 100 | if (!this._ready()) { 101 | return; 102 | } 103 | 104 | if (this._castingTrack && track.isSameTrackWith(this._castingTrack)) { 105 | this.resume(); 106 | } 107 | else { 108 | let media = {}; 109 | media.contentId = track.platformTrackRealUrl; 110 | media.contentType = 'video/mp4'; 111 | media.streamType = 'BUFFERED'; 112 | media.metadata = {}; 113 | media.metadata.title = track.title; 114 | media.metadata.images = [ 115 | { url: track.covers.default } 116 | ]; 117 | 118 | this._castingPlayer.on('status', (status) => { 119 | console.log('status broadcast playerState=%s', status.playerState); 120 | }); 121 | 122 | this._castingPlayer.load(media, { 123 | autoplay: true 124 | }, (err, status) => { 125 | console.log('media loaded playerState=%s', status.playerState); 126 | }); 127 | 128 | this._castingTrack = track; 129 | } 130 | } 131 | 132 | pause() { 133 | if (!this._ready()) { 134 | return; 135 | } 136 | this._castingPlayer.pause(); 137 | } 138 | 139 | resume() { 140 | if (!this._ready()) { 141 | return; 142 | } 143 | this._castingPlayer.play(); 144 | } 145 | 146 | stop() { 147 | if (!this._ready()) { 148 | return; 149 | } 150 | this._castingPlayer.stop(); 151 | } 152 | 153 | seek(time) { 154 | if (!this._ready()) { 155 | return; 156 | } 157 | this._castingPlayer.seek(time); 158 | } 159 | } 160 | 161 | module.exports = new CastingManager(); 162 | -------------------------------------------------------------------------------- /src/views/modules/Dialog.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Bootstrap from 'bootstrap'; 3 | import Bootbox from 'bootbox'; 4 | import L10nManager from '../../modules/L10nManager'; 5 | 6 | function Dialog() {} 7 | 8 | [ 'alert', 9 | 'confirm', 10 | 'prompt', 11 | 'setLocale' 12 | ].forEach((name) => { 13 | Dialog.prototype[name] = function() { 14 | Bootbox[name].apply(Bootbox, arguments); 15 | }; 16 | }); 17 | 18 | let dialog = new Dialog(); 19 | 20 | L10nManager.on('language-changed', (newLanguage) => { 21 | let transformedLanguage = newLanguage; 22 | 23 | // TODO 24 | // we have to make sure our language naming is the same with 25 | // bootbox's language. 26 | switch (newLanguage) { 27 | case 'zh-TW': 28 | transformedLanguage = 'zh_TW'; 29 | break; 30 | 31 | default: 32 | transformedLanguage = 'en'; 33 | break; 34 | } 35 | 36 | dialog.setLocale(transformedLanguage); 37 | }); 38 | 39 | module.exports = dialog; 40 | -------------------------------------------------------------------------------- /src/views/modules/EasterEggs.js: -------------------------------------------------------------------------------- 1 | import Notifier from './Notifier'; 2 | import Player from './Player'; 3 | 4 | class EasterEggs { 5 | constructor() { 6 | this._timerIds = []; 7 | 8 | /* jshint ignore:start */ 9 | this._sentences = [ 10 | '%E3%81%8A%E6%97%A9%E3%81%86%E3%82%AB%E3%82%AF%E3%81%A1%E3%82%83%E3%82%93', 11 | '%E5%A6%82%E6%9E%9C%E4%BD%A0%E8%83%BD%E5%A4%A0%E7%9C%8B%E5%88%B0%E9%80%99%E5%80%8B%E8%A8%8A%E6%81%AF%E7%9A%84%E8%A9%B1', 12 | '%E5%B0%B1%E4%BB%A3%E8%A1%A8%E4%BD%A0%E8%A7%A3%E9%96%8B%20Konami%20Code%20%E4%BA%86%EF%BC%81', 13 | '%E6%B2%92%E6%9C%89%E6%84%8F%E5%A4%96%E7%9A%84%E8%A9%B1', 14 | '%E4%BB%8A%E5%A4%A9%E6%87%89%E8%A9%B2%E6%98%AF%207/19%20-%20%E5%A6%B3%E7%9A%84%E7%94%9F%E6%97%A5', 15 | '%E7%82%BA%E4%BA%86%E6%BA%96%E5%82%99%E5%A6%B3%E7%9A%84%E7%94%9F%E6%97%A5%E7%A6%AE%E7%89%A9', 16 | '%E6%88%91%E6%83%B3%E4%BA%86%E8%B6%85%E4%B9%85%E9%83%BD%E6%B2%92%E4%BB%80%E9%BA%BC%E6%83%B3%E6%B3%95', 17 | '%E5%8F%AA%E5%9B%A0%E7%82%BA%E6%88%91%E6%83%B3%E8%A6%81%E9%80%81%E5%A6%B3%E4%B8%80%E5%80%8B%E7%8D%A8%E4%B8%80%E7%84%A1%E4%BA%8C%E7%9A%84%E7%A6%AE%E7%89%A9%EF%BC%81', 18 | '%E4%BD%86%E6%9C%89%E4%B8%80%E5%A4%A9%EF%BC%8C%E6%88%91%E7%AA%81%E7%84%B6%E9%96%93%E6%83%B3%E5%88%B0%E4%B8%80%E4%BB%B6%E4%BA%8B', 19 | '%E8%BA%AB%E7%82%BA%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88%E5%B8%AB%E7%9A%84%E6%88%91%E6%9C%80%E6%93%85%E9%95%B7%E7%9A%84%E5%B0%B1%E6%98%AF%E5%AF%AB%E7%A8%8B%E5%BC%8F%E4%BA%86', 20 | '%E5%A6%82%E6%9E%9C%E5%8F%AF%E4%BB%A5%E8%87%AA%E5%B7%B1%E5%BE%9E%E7%84%A1%E5%88%B0%E6%9C%89%E5%AF%AB%E5%87%BA%E5%92%8C%E5%A6%B3%E6%9C%89%E9%97%9C%E7%9A%84%E7%A8%8B%E5%BC%8F', 21 | '%E6%87%89%E8%A9%B2%E6%9C%83%E8%AE%93%E5%A6%B3%E5%BE%88%E6%84%9F%E5%8B%95%E5%90%A7%EF%BC%9F%EF%BC%81', 22 | '%E6%89%80%E4%BB%A5%E5%A6%B3%E7%9F%A5%E9%81%93%E7%82%BA%E4%BB%80%E9%BA%BC%E5%AE%83%E5%8F%AB%E5%81%9A%20Kaku%20%E4%BA%86%E5%97%8E%20:D%20?', 23 | '%E5%8D%8A%E5%B9%B4%E5%89%8D%EF%BC%8C%E6%88%91%E5%B0%B1%E9%96%8B%E5%A7%8B%E5%AF%AB%E7%AC%AC%E4%B8%80%E8%A1%8C%E7%A8%8B%E5%BC%8F%E7%A2%BC', 24 | '%E6%AF%8F%E5%A4%A9%E4%B8%8D%E8%AB%96%E6%88%91%E6%9C%89%E5%A4%9A%E5%BF%99', 25 | '%E6%88%91%E9%83%BD%E6%9C%83%E7%9B%A1%E9%87%8F%E6%92%AD%E5%87%BA%E8%87%B3%E5%B0%91%2030%20%E5%88%86%E9%90%98%E5%AF%AB%E4%B8%80%E9%BB%9E%E9%BB%9E', 26 | '%E6%B2%92%E6%83%B3%E5%88%B0%E9%81%8E%E4%BA%86%E5%8D%8A%E5%B9%B4%E5%A4%9A%E4%B9%9F%E5%AF%AB%E5%87%BA%E4%B8%80%E5%80%8B%E5%8E%9F%E5%9E%8B%E4%BA%86%EF%BC%81', 27 | '%E5%AE%83%E9%9B%96%E7%84%B6%E9%82%84%E4%B8%8D%E5%A4%A0%E5%AE%8C%E6%95%B4%EF%BC%8C%E4%B9%9F%E9%82%84%E7%AE%97%E5%8B%98%E7%94%A8%E4%BA%86%E5%90%A7%EF%BC%81', 28 | '%E4%B8%8D%E8%AB%96%E5%A6%82%E4%BD%95%EF%BC%8C%E5%B8%8C%E6%9C%9B%E5%A6%B3%E6%9C%83%E5%96%9C%E6%AD%A1%E5%AE%83', 29 | '%E5%BE%88%E9%AB%98%E8%88%88%E5%9C%A8%E6%88%91%E4%BA%BA%E7%94%9F%E6%9C%80%E4%BD%8E%E6%BD%AE%E7%9A%84%E6%99%82%E5%80%99%E9%81%87%E5%88%B0%E4%BA%86%E5%A6%B3', 30 | '%E5%9B%A0%E7%82%BA%E5%A6%B3%E5%B0%B1%E6%98%AF%E6%88%91%E6%89%BE%E5%B0%8B%2025%20%E5%B9%B4%E7%9A%84%E9%82%A3%E9%A1%86%E9%81%BA%E8%90%BD%E4%B9%8B%E6%98%9F', 31 | '%E6%9C%80%E5%BE%8C%EF%BC%8C%E9%80%81%E7%B5%A6%E5%A6%B3%E9%80%99%E9%A6%96%E5%B1%AC%E6%96%BC%E6%88%91%E5%80%91%E5%85%A9%E5%80%8B%E4%BA%BA%E7%9A%84%E6%AD%8C', 32 | 'Adam%20Levine%20-%20Lost%20Stars', 33 | '%E7%94%9F%E6%97%A5%E5%BF%AB%E6%A8%82%20:)' 34 | ]; 35 | /* jshint ignore:end */ 36 | 37 | // You are my lost star :) 38 | this._lostTrackForKaku = { 39 | artist: 'Adam Levine', 40 | title: 'Lost Stars' 41 | }; 42 | 43 | this._ALERT_TIMEOUT = 5800; 44 | this._ALERT_EXTRA_WAITING = 300; 45 | } 46 | 47 | show() { 48 | Player.play(this._lostTrackForKaku); 49 | // We may show EasterEggs for multiple times, so let's drop old timerIds 50 | // and make sure everything is new. 51 | this._cleanup(); 52 | this._sentences.forEach((eachSentence, index) => { 53 | let timeout = this._ALERT_TIMEOUT * index + this._ALERT_EXTRA_WAITING; 54 | let id = setTimeout(((eachSentence) => { 55 | return () => { 56 | Notifier.alert(decodeURI(eachSentence)); 57 | }; 58 | })(eachSentence), timeout); 59 | this._timerIds.push(id); 60 | }); 61 | } 62 | 63 | _cleanup() { 64 | this._timerIds.forEach((id) => { 65 | clearTimeout(id); 66 | }); 67 | } 68 | } 69 | 70 | module.exports = new EasterEggs(); 71 | -------------------------------------------------------------------------------- /src/views/modules/KonamiCodeManager.js: -------------------------------------------------------------------------------- 1 | class KonamiCodeManager { 2 | constructor() { 3 | this._pattern = "38384040373937396665"; 4 | this._keyCodeCache = ''; 5 | this._callback = () => {}; 6 | this._boundCheckKeyCodePattern = this._checkKeyCodePattern.bind(this); 7 | } 8 | 9 | attach(root, callback) { 10 | if (root instanceof Element) { 11 | root.removeEventListener('keydown', this._boundCheckKeyCodePattern); 12 | root.addEventListener('keydown', this._boundCheckKeyCodePattern); 13 | this._callback = callback; 14 | } 15 | } 16 | 17 | _checkKeyCodePattern(e) { 18 | if (e) { 19 | this._keyCodeCache += e.keyCode; 20 | if (this._keyCodeCache.length === this._pattern.length) { 21 | if (this._keyCodeCache === this._pattern) { 22 | console.log('KonamiCode passed, let\'s show some easter eggs :)'); 23 | this._callback(); 24 | } 25 | this._keyCodeCache = ''; 26 | } 27 | else if (!this._pattern.match(this._keyCodeCache)) { 28 | this._keyCodeCache = ''; 29 | } 30 | } 31 | } 32 | } 33 | 34 | module.exports = new KonamiCodeManager(); 35 | -------------------------------------------------------------------------------- /src/views/modules/Notifier.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Bootstrap from 'bootstrap'; 3 | import BootstrapNotify from 'bootstrap-notify'; 4 | 5 | import { EventEmitter } from 'events'; 6 | import PreferenceManager from '../../modules/PreferenceManager'; 7 | 8 | class Notifier extends EventEmitter { 9 | constructor() { 10 | super(); 11 | } 12 | 13 | _getDefaultNotifyOptions() { 14 | return { 15 | placement: { 16 | from: 'bottom', 17 | align: 'right' 18 | } 19 | }; 20 | } 21 | 22 | _getProcessedArguments() { 23 | const args = arguments; 24 | let firstOption = {}; 25 | const secondOption = this._getDefaultNotifyOptions(); 26 | 27 | if (args.length <= 0 || args.length > 2) { 28 | throw new Error('you are passing wrong parameters into Notifier'); 29 | } 30 | 31 | // We assume String type means `message` 32 | if (typeof args[0] === 'string') { 33 | firstOption.message = args[0]; 34 | } 35 | 36 | // If it is Object type, good, just override it 37 | if (typeof args[0] === 'object') { 38 | firstOption = args[0]; 39 | } 40 | 41 | // If we are passing two parameters, because we already handled 42 | // args[0], so what we have to do here is handle args[1] 43 | if (args.length === 2) { 44 | Object.keys(args[1]).each((key) => { 45 | secondOption[key] = args[1][key]; 46 | }); 47 | } 48 | 49 | return [firstOption, secondOption]; 50 | } 51 | 52 | alert() { 53 | const args = this._getProcessedArguments(...arguments); 54 | $.notify(args[0], args[1]); 55 | } 56 | 57 | sendDesktopNotification(options) { 58 | const isDesktopNotificationEnabled = 59 | PreferenceManager.getPreference('desktop.notification.enabled'); 60 | // TODO 61 | // 1. add more checks here 62 | // 2. change default kaku icon 63 | if (isDesktopNotificationEnabled && options.body) { 64 | const title = options.title || 'Love from Kaku'; 65 | const notification = new window.Notification(title, options); 66 | } 67 | } 68 | } 69 | 70 | module.exports = new Notifier(); 71 | -------------------------------------------------------------------------------- /src/views/modules/RemotePlayer.js: -------------------------------------------------------------------------------- 1 | import Player from './Player'; 2 | import BaseTrack from 'kaku-core/models/track/BaseTrack'; 3 | import Firebase from '../../modules/wrapper/Firebase'; 4 | 5 | class RemotePlayer { 6 | constructor() { 7 | this._initialized = false; 8 | 9 | this._onPlayerPlay = this._onPlayerPlay.bind(this); 10 | this._onPlayerPause = this._onPlayerPause.bind(this); 11 | this._onPlayerStop = this._onPlayerStop.bind(this); 12 | } 13 | 14 | init() { 15 | if (this._initialized) { 16 | return; 17 | } 18 | this._initialized = true; 19 | 20 | Firebase.on('setup', (userInfo) => { 21 | // keep this in internal variable 22 | this.userInfo = userInfo; 23 | if (userInfo.role !== 'dj') { 24 | // disable non-dj users' players 25 | Player.disable(true); 26 | } 27 | 28 | this._playedTracksRef = Firebase.joinPlayedTracksRoom(); 29 | this._commandRef = Firebase.joinCommandRoom(); 30 | 31 | // TODO 32 | // there is something wrong with the playingTime, so need to fix it later 33 | if (userInfo.role === 'guest') { 34 | this._commandRef.on('value', (snapshot) => { 35 | let action = snapshot.val(); 36 | // for the first time, there is no action at all, 37 | // so we will wait until everything is there 38 | if (action) { 39 | let command = action.command; 40 | let data = action.data; 41 | if (command === 'play') { 42 | let track = BaseTrack.fromJSON(data.track); 43 | let time = data.time; 44 | Player.play(track, time, true); 45 | } 46 | else if (command === 'stop') { 47 | Player.stop(true); 48 | } 49 | else if (command === 'pause') { 50 | Player.pause(true); 51 | } 52 | } 53 | }); 54 | } 55 | else if (userInfo.role === 'dj') { 56 | Player.on('play', this._onPlayerPlay); 57 | Player.on('pause', this._onPlayerPause); 58 | Player.on('stop', this._onPlayerStop); 59 | } 60 | }); 61 | 62 | Firebase.on('room-left', (roomName) => { 63 | if ('command' === roomName) { 64 | if (this.userInfo.role === 'dj') { 65 | Player.off('play', this._onPlayerPlay); 66 | Player.off('pause', this._onPlayerPause); 67 | Player.off('stop', this._onPlayerStop); 68 | } 69 | this._playedTracksRef = null; 70 | this._commandRef = null; 71 | Player.disable(false); 72 | } 73 | }); 74 | } 75 | 76 | _onPlayerPlay() { 77 | let playingTrack = Player.playingTrack.toJSON(); 78 | let time = Player.playingTrackTime; 79 | 80 | this._playedTracksRef.push(playingTrack); 81 | this._commandRef.set({ 82 | command: 'play', 83 | data: { 84 | track: playingTrack, 85 | time: time 86 | } 87 | }); 88 | } 89 | 90 | _onPlayerPause() { 91 | this._commandRef.set({ 92 | command: 'pause' 93 | }); 94 | } 95 | 96 | _onPlayerStop() { 97 | this._commandRef.set({ 98 | commnad: 'stop' 99 | }); 100 | } 101 | } 102 | 103 | module.exports = new RemotePlayer(); 104 | -------------------------------------------------------------------------------- /src/views/modules/TabManager.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | class TabManager extends EventEmitter { 4 | constructor() { 5 | super(); 6 | 7 | // default tab is home 8 | this.tabName = 'home'; 9 | this.tabOptions = undefined; 10 | } 11 | 12 | setTab(tabName, tabOptions) { 13 | if (tabName === this.tabName && tabOptions === this.tabOptions) { 14 | return; 15 | } 16 | else { 17 | this.tabName = tabName; 18 | this.tabOptions = tabOptions; 19 | this.emit('changed', tabName, tabOptions); 20 | } 21 | } 22 | } 23 | 24 | module.exports = new TabManager(); 25 | -------------------------------------------------------------------------------- /tests/ui/SearchBar.test.js: -------------------------------------------------------------------------------- 1 | require('./setup'); 2 | 3 | suite('Searchbar', () => { 4 | 'use strict'; 5 | 6 | var searchbarSelector = '.searchbar-user-input'; 7 | test('it does find tracks when typing', () => { 8 | return Kaku.init().setValue(searchbarSelector, 'test') 9 | .waitForVisible('.autocomplete-list') 10 | .then((visible) => { 11 | assert.ok(visible); 12 | }) 13 | .end(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/ui/Tooltip.test.js: -------------------------------------------------------------------------------- 1 | require('./setup'); 2 | 3 | suite('Tooltip', () => { 4 | 'use strict'; 5 | 6 | var trackComponentSelector = '.topranking-component'; 7 | var firstTrackSelector = trackComponentSelector + ' > .track:first-child'; 8 | var tooltipSelector = '.__react_component_tooltip'; 9 | 10 | test('it should show correct tooltip when users hover on it', () => { 11 | var tooltipText; 12 | var trackText; 13 | 14 | return Kaku.init() 15 | .waitForVisible(firstTrackSelector, 5000) 16 | .moveToObject(firstTrackSelector, 10, 10) 17 | .waitForText(firstTrackSelector) 18 | .then((text) => { 19 | trackText = text; 20 | }) 21 | .waitForText(tooltipSelector) 22 | .then((text) => { 23 | tooltipText = text; 24 | assert.equal(trackText, tooltipText); 25 | }) 26 | .end(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/ui/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chromeDriverProcess; 4 | 5 | var chaiAsPromised = require('chai-as-promised'); 6 | var childProcess = require('child_process'); 7 | var chromedriver = require('chromedriver'); 8 | var webdriverio = require('webdriverio'); 9 | 10 | var kakuPath; 11 | 12 | // Note 13 | // we only support Mac & linux platform for uitest for now. 14 | switch (process.platform) { 15 | case 'darwin': 16 | kakuPath = './build/Kaku-darwin-x64/Kaku.app/Contents/MacOS/Electron'; 17 | break; 18 | 19 | default: 20 | kakuPath = './build/app/Kaku'; 21 | break; 22 | } 23 | 24 | var client = webdriverio.remote({ 25 | host: 'localhost', 26 | port: 9515, 27 | desiredCapabilities: { 28 | browserName: 'chrome', 29 | chromeOptions: { 30 | binary: kakuPath 31 | } 32 | } 33 | }); 34 | 35 | var assert = require('assert'); 36 | var chai = require('chai'); 37 | 38 | // Note 39 | // we will create a child process for chromeDriver when running tests 40 | beforeEach((done) => { 41 | chromeDriverProcess = childProcess.execFile(chromedriver.path, [ 42 | '--url-base=/wd/hub', 43 | '--port=9515' 44 | ]); 45 | 46 | setTimeout(() => { 47 | done(); 48 | }, 1000); 49 | }); 50 | 51 | afterEach(() => { 52 | if (chromeDriverProcess !== null) { 53 | chromeDriverProcess.kill(); 54 | } 55 | }); 56 | 57 | chai.use(chaiAsPromised); 58 | global.assert = chai.assert; 59 | global.Kaku = client; 60 | -------------------------------------------------------------------------------- /tests/unit/AppCore.test.js: -------------------------------------------------------------------------------- 1 | suite('AppCore', () => { 2 | 'use strict'; 3 | 4 | var AppCore; 5 | var sandbox; 6 | 7 | setup(() => { 8 | AppCore = proxyquire('../../src/modules/AppCore', { 9 | electron: require('./mocks/Electron') 10 | }); 11 | sandbox = sinon.sandbox.create(); 12 | sandbox.stub(AppCore, 'emit'); 13 | }); 14 | 15 | teardown(() => { 16 | sandbox.restore(); 17 | }); 18 | 19 | suite('.title >', () => { 20 | setup(() => { 21 | AppCore.title = 'test'; 22 | }); 23 | 24 | test('title will be updated and also emit needed event', () => { 25 | assert.equal(AppCore._title, 'test'); 26 | assert.isTrue(AppCore.emit.calledWith('titleUpdated', 'test')); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/unit/HistoryManager.test.js: -------------------------------------------------------------------------------- 1 | suite('HistoryManager', () => { 2 | 'use strict'; 3 | 4 | var historyManager; 5 | var fakeTrack = {}; 6 | var sandbox; 7 | 8 | setup(() => { 9 | sandbox = sinon.sandbox.create(); 10 | fakeTrack = {}; 11 | historyManager = require('../../src/modules/HistoryManager'); 12 | // beacuse this is singleton, we have to cleanup by ourselves 13 | historyManager.clean(); 14 | sandbox.stub(historyManager, 'emit'); 15 | }); 16 | 17 | teardown(() => { 18 | sandbox.restore(); 19 | }); 20 | 21 | suite('add() >', () => { 22 | test('if without the same track, then add it', () => { 23 | historyManager.add(fakeTrack); 24 | assert.ok(historyManager._hasTrack(fakeTrack)); 25 | assert.ok(historyManager.emit.calledWith('history-updated')); 26 | }); 27 | 28 | test('if with the same track, do nothing', () => { 29 | historyManager._tracks.push(fakeTrack); 30 | historyManager.add(fakeTrack); 31 | assert.equal(historyManager.tracks.length, 1); 32 | assert.isFalse(historyManager.emit.calledWith('history-updated')); 33 | }); 34 | }); 35 | 36 | suite('remove() >', () => { 37 | test('if with the same track, then remove it', () => { 38 | historyManager.add(fakeTrack); 39 | historyManager.remove(fakeTrack); 40 | assert.equal(historyManager.tracks.length, 0); 41 | assert.ok(historyManager.emit.calledWith('history-updated')); 42 | }); 43 | 44 | test('if without the same track, do nothing', () => { 45 | historyManager.remove(fakeTrack); 46 | assert.isFalse(historyManager.emit.calledWith('history-updated')); 47 | }); 48 | }); 49 | 50 | suite('clean() >', () => { 51 | test('do cleanup all internal tracks', () => { 52 | historyManager._tracks.push(fakeTrack); 53 | historyManager._tracks.push(fakeTrack); 54 | 55 | historyManager.clean(); 56 | assert.equal(historyManager.tracks.length, 0); 57 | assert.ok(historyManager.emit.calledWith('history-updated', 58 | historyManager.tracks)); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/L10nManager.test.js: -------------------------------------------------------------------------------- 1 | suite('L10nManager', () => { 2 | 'use strict'; 3 | 4 | var sandbox; 5 | var l10nManager; 6 | 7 | setup(() => { 8 | l10nManager = proxyquire('../../src/modules/L10nManager', { 9 | electron: require('./mocks/Electron') 10 | }); 11 | l10nManager._cachedStrings = { 12 | 'en': { 13 | 'fake_1': 'fake 1', 14 | 'fake_2': 'fake 2' 15 | }, 16 | 'zh-TW': { 17 | 'fake_1': '假的 1' 18 | }, 19 | }; 20 | sandbox = sinon.sandbox.create(); 21 | sandbox.stub(console, 'error'); 22 | sandbox.stub(l10nManager, 'emit'); 23 | }); 24 | 25 | teardown(() => { 26 | sandbox.restore(); 27 | }); 28 | 29 | suite('changeLanguage()', () => { 30 | setup(() => { 31 | l10nManager._currentLanguage = 'zh-TW'; 32 | }); 33 | 34 | test('if we want to change to the same language, do nothing', () => { 35 | l10nManager.changeLanguage('zh-TW'); 36 | assert.isFalse(l10nManager.emit.called); 37 | }); 38 | 39 | test('if we want to change to different language, do change', () => { 40 | l10nManager.changeLanguage('en'); 41 | assert.isTrue( 42 | l10nManager.emit.calledWith('language-changed', 'en', 'zh-TW')); 43 | }); 44 | }); 45 | 46 | suite('get()', () => { 47 | setup(() => { 48 | l10nManager._currentLanguage = 'zh-TW'; 49 | }); 50 | 51 | test('if language is matched, we will directly get result', () => { 52 | var t = l10nManager.get('fake_1', {}); 53 | assert.equal(t, '假的 1'); 54 | }); 55 | 56 | test('if no language is matched, we will fall back to en', () => { 57 | var t = l10nManager.get('fake_2', {}); 58 | assert.isTrue(console.error.called); 59 | assert.equal(t, 'fake 2'); 60 | }); 61 | 62 | test('even en has no this string, we will return empty string', () => { 63 | var t = l10nManager.get('no_this_key', {}); 64 | assert.isTrue(console.error.called); 65 | assert.equal(t, ''); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/unit/PlaylistManager.test.js: -------------------------------------------------------------------------------- 1 | suite('PlaylistManager', () => { 2 | 'use strict'; 3 | 4 | var playlistManager; 5 | var sandbox; 6 | 7 | setup((done) => { 8 | playlistManager = require('../../src/modules/PlaylistManager'); 9 | 10 | // Let's stub all db related operations 11 | sandbox = sinon.sandbox.create(); 12 | sandbox.spy(playlistManager, 'emit'); 13 | sandbox.stub(playlistManager, '_storePlaylistsToDB', () => { 14 | return Promise.resolve(); 15 | }); 16 | 17 | playlistManager.ready().then(() => { 18 | playlistManager._activePlaylist = null; 19 | playlistManager._isDisplaying = false; 20 | // insert two fakeData 21 | playlistManager._playlists = [ 22 | { id: 'id1', name: 'playlist1' }, 23 | { id: 'id2', nam1: 'playlist2' } 24 | ]; 25 | done(); 26 | }); 27 | }); 28 | 29 | teardown(() => { 30 | sandbox.restore(); 31 | }); 32 | 33 | suite('findPlaylistById() >', () => { 34 | test('if without playlist, return undefined', () => { 35 | var playlist = playlistManager.findPlaylistById('id0'); 36 | assert.equal(playlist, undefined); 37 | }); 38 | 39 | test('if with playlist, return it', () => { 40 | var playlist = playlistManager.findPlaylistById('id1'); 41 | assert.equal(playlist.id, 'id1'); 42 | }); 43 | }); 44 | 45 | suite('findPlaylistByName() >', () => { 46 | test('if without playlist, return undefined', () => { 47 | var playlist = playlistManager.findPlaylistByName('playlist0'); 48 | assert.equal(playlist, undefined); 49 | }); 50 | 51 | test('if with playlist, return it', () => { 52 | var playlist = playlistManager.findPlaylistByName('playlist1'); 53 | assert.equal(playlist.name, 'playlist1'); 54 | }); 55 | }); 56 | 57 | suite('findPlaylistIndexById() >', () => { 58 | test('if without playlist, return -1', () => { 59 | var index = playlistManager.findPlaylistIndexById('id0'); 60 | assert.equal(index, -1); 61 | }); 62 | 63 | test('if with playlist, return its index', () => { 64 | var index = playlistManager.findPlaylistIndexById('id1'); 65 | assert.equal(index, 0); 66 | }); 67 | }); 68 | 69 | suite('findPlaylistIndexByName() >', () => { 70 | test('if without playlist, return -1', () => { 71 | var index = playlistManager.findPlaylistIndexByName('playlist0'); 72 | assert.equal(index, -1); 73 | }); 74 | 75 | test('if with playlist, return its index', () => { 76 | var index = playlistManager.findPlaylistIndexByName('playlist1'); 77 | assert.equal(index, 0); 78 | }); 79 | }); 80 | 81 | suite('renamePlaylistById() >', () => { 82 | test('if without playlist, then reject', (done) => { 83 | playlistManager.renamePlaylistById('id0', 'newPlaylist0').catch(() => { 84 | assert.ok(true); 85 | }).then(done, done); 86 | }); 87 | 88 | test('if with playlist, then rename it', (done) => { 89 | playlistManager.renamePlaylistById('id1', 'newPlaylist1').then(() => { 90 | var playlist = playlistManager.findPlaylistByName('newPlaylist1'); 91 | assert.equal(playlist.id, 'id1'); 92 | assert.ok(playlistManager.emit.calledWith('renamed', playlist)); 93 | }).then(done, done); 94 | }); 95 | }); 96 | 97 | suite('removePlaylistById() >', () => { 98 | test('if without playlist, then reject', (done) => { 99 | playlistManager.removePlaylistById('id0').catch(() => { 100 | assert.ok(true); 101 | }).then(done, done); 102 | }); 103 | 104 | test('if with playlist, then remove it', (done) => { 105 | playlistManager.removePlaylistById('id1').then((removedPlaylist) => { 106 | assert.equal(playlistManager.playlists.length, 1); 107 | assert.ok(playlistManager.emit.calledWith('removed', removedPlaylist)); 108 | }).then(done, done); 109 | }); 110 | }); 111 | 112 | suite('showPlaylistById() >', () => { 113 | test('if without playlist, do nothing', () => { 114 | playlistManager.showPlaylistById('id0'); 115 | assert.isFalse(playlistManager.emit.calledWith('shown')); 116 | }); 117 | 118 | test('if with playlist, do shown', () => { 119 | playlistManager.showPlaylistById('id1'); 120 | 121 | var playlist = playlistManager.findPlaylistById('id1'); 122 | assert.equal(playlistManager._activePlaylist, playlist); 123 | assert.ok(playlistManager._isDisplaying); 124 | assert.ok(playlistManager.emit.calledWith('shown', playlist)); 125 | }); 126 | }); 127 | 128 | suite('hidePlaylist() >', () => { 129 | test('do hide playlist', () => { 130 | playlistManager.hidePlaylist(); 131 | assert.equal(playlistManager._activePlaylist, null); 132 | assert.isFalse(playlistManager._isDisplaying); 133 | assert.ok(playlistManager.emit.calledWith('hidden')); 134 | }); 135 | }); 136 | 137 | suite('_addPlaylist() >', () => { 138 | test('if there is any playlist with the same name, then reject', (done) => { 139 | playlistManager._addPlaylist({ 140 | id: 'id1', 141 | name: 'playlist1' 142 | }).catch(() => { 143 | assert.ok(true); 144 | }).then(done, done); 145 | }); 146 | 147 | test('if there is no same-named playlist, then add it', (done) => { 148 | var rawPlaylist = { 149 | id: 'id3', 150 | name: 'playlist3' 151 | }; 152 | 153 | playlistManager._addPlaylist(rawPlaylist).then((playlist) => { 154 | assert.equal(rawPlaylist.name, playlist.name); 155 | assert.ok(playlistManager.emit.calledWith('added', playlist)); 156 | }).then(done, done); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /tests/unit/PreferenceManager.test.js: -------------------------------------------------------------------------------- 1 | suite('PreferenceManager', () => { 2 | 'use strict'; 3 | 4 | var preferenceManager; 5 | 6 | setup(() => { 7 | preferenceManager = require('../../src/modules/PreferenceManager'); 8 | preferenceManager._preferenceStorage = {}; 9 | }); 10 | 11 | suite('setPreference() >', () => { 12 | var testKey = 'testKey'; 13 | var sandbox = sinon.sandbox.create(); 14 | 15 | setup(() => { 16 | sandbox.stub(preferenceManager, 'emit'); 17 | }); 18 | 19 | teardown(() => { 20 | sandbox.restore(); 21 | }); 22 | 23 | test('if preference is still the same, do nothing', () => { 24 | preferenceManager._preferenceStorage[testKey] = 'true'; 25 | preferenceManager.setPreference(testKey, true); 26 | assert.isFalse(preferenceManager.emit.called); 27 | }); 28 | 29 | test('if preference is different, do emit', () => { 30 | preferenceManager._preferenceStorage[testKey] = 'true'; 31 | preferenceManager.setPreference(testKey, false); 32 | assert.ok(preferenceManager.emit.calledWith('preference-updated')); 33 | }); 34 | }); 35 | 36 | suite('getPreference() > ', () => { 37 | var testKey = 'testKey'; 38 | 39 | test('\'true\' would be true', () => { 40 | preferenceManager._preferenceStorage[testKey] = 'true'; 41 | assert.equal(preferenceManager.getPreference(testKey), true); 42 | }); 43 | 44 | test('\'false\' would be false', () => { 45 | preferenceManager._preferenceStorage[testKey] = 'false'; 46 | assert.equal(preferenceManager.getPreference(testKey), false); 47 | }); 48 | 49 | test('undefined would be undefined', () => { 50 | preferenceManager._preferenceStorage[testKey] = undefined; 51 | assert.equal(preferenceManager.getPreference(testKey), undefined); 52 | }); 53 | 54 | test('othwers would be its original value', () => { 55 | preferenceManager._preferenceStorage[testKey] = 'test'; 56 | assert.equal(preferenceManager.getPreference(testKey), 'test'); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/unit/mocks/Database.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get: function() { 3 | return Promise.resolve({}); 4 | }, 5 | put: function() { 6 | return Promise.resolve(); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tests/unit/mocks/Electron/Remote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'app': { 3 | getAppPath: function() { 4 | return './'; 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tests/unit/mocks/Electron/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | remote: require('./Remote') 3 | }; 4 | -------------------------------------------------------------------------------- /tests/unit/mocks/KakuCore.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getRootPath: function() { 3 | return ''; 4 | }, 5 | getAppRootPath: function() { 6 | return ''; 7 | }, 8 | getEnvInfo: function() { 9 | return {}; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /tests/unit/mocks/Tracker.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | event: function() { 3 | return { 4 | send: function() {} 5 | }; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tests/unit/setup.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const chai = require('chai'); 3 | const sinon = require('sinon'); 4 | const { JSDOM } = require('jsdom'); 5 | const proxyquire = require('proxyquire').noCallThru(); 6 | 7 | const { window } = new JSDOM(''); 8 | 9 | global.window = window; 10 | global.document = window.document; 11 | global.IS_TEST = true; 12 | global.assert = chai.assert; 13 | global.sinon = sinon; 14 | global.proxyquire = proxyquire; 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const webpack = require("webpack"); 6 | 7 | let nodeModules = {}; 8 | fs.readdirSync('node_modules') 9 | .filter((x) => { 10 | return [ 11 | '.bin', 12 | 'jquery', 13 | 'bootstrap', 14 | 'bootstrap-notify', 15 | 'bootbox' 16 | ].indexOf(x) === -1; 17 | }) 18 | .forEach((mod) => { 19 | nodeModules[mod] = 'commonjs ' + mod; 20 | }); 21 | 22 | let config = { 23 | entry: './src/main.js', 24 | output: { 25 | path: __dirname, 26 | filename: 'kaku.bundled.js' 27 | }, 28 | externals: nodeModules, 29 | target: 'atom', 30 | module: { 31 | loaders: [ 32 | { 33 | test: /\.js$/, 34 | exclude: /(node_modules|bower_components)/, 35 | loader: 'babel', 36 | query: { 37 | presets: ['es2015', 'react'] 38 | } 39 | }, 40 | { 41 | test: /\.json$/, 42 | loader: 'json-loader' 43 | } 44 | ] 45 | }, 46 | plugins: [ 47 | new webpack.ProvidePlugin({ 48 | '$': 'jquery', 49 | 'jQuery': 'jquery', 50 | 'window.jQuery': 'jquery', 51 | 'window.$': 'jquery' 52 | }) 53 | ] 54 | }; 55 | 56 | module.exports = config; 57 | --------------------------------------------------------------------------------