Browse Source

Massive refactor... So big I didn't even check before committing :D

v4
Samuel Attard 5 years ago
parent
commit
0ec46152e8
No known key found for this signature in database GPG Key ID: 273DC1869D8F13EF
  1. 3
      .eslintrc
  2. 8
      src/assets/less/dialog_window.less
  3. 56
      src/assets/less/main_window.less
  4. 4
      src/index.js
  5. 5
      src/main/utils/Settings.js
  6. 16
      src/public_html/desktop_settings.html
  7. 178
      src/public_html/index.html
  8. 3
      src/renderer/GPMWebView/interface/customNavigation/goToURL.js
  9. 1
      src/renderer/GPMWebView/interface/customNavigation/index.js
  10. 8
      src/renderer/generic/core/APICode.js
  11. 27
      src/renderer/generic/core/appDetails.js
  12. 40
      src/renderer/generic/core/control-bar.js
  13. 19
      src/renderer/generic/core/devtools.js
  14. 47
      src/renderer/generic/core/goToURL.js
  15. 43
      src/renderer/generic/core/index.js
  16. 84
      src/renderer/generic/core/lyrics.js
  17. 13
      src/renderer/generic/core/onlineStatus.js
  18. 8
      src/renderer/generic/core/openPort.js
  19. 8
      src/renderer/generic/core/uninstall.js
  20. 16
      src/renderer/generic/core/updateAvailable.js
  21. 49
      src/renderer/generic/core/webviewLoader.js
  22. 46
      src/renderer/generic/customWindowThemeHandler.js
  23. 2
      src/renderer/generic/index.js
  24. 96
      src/renderer/generic/windowThemeHandler.js
  25. 57
      src/renderer/settings/index.js
  26. 168
      src/renderer/ui/components/generic/LyricsViewer.js
  27. 35
      src/renderer/ui/components/generic/OfflineWarning.js
  28. 12
      src/renderer/ui/components/generic/SettingsProvider.js
  29. 83
      src/renderer/ui/components/generic/WebView.js
  30. 129
      src/renderer/ui/components/generic/WindowContainer.js
  31. 63
      src/renderer/ui/components/modals/APICodeModal.js
  32. 65
      src/renderer/ui/components/modals/AboutModal.js
  33. 58
      src/renderer/ui/components/modals/ConfirmTrayModal.js
  34. 101
      src/renderer/ui/components/modals/GoToModal.js
  35. 65
      src/renderer/ui/components/modals/OpenPortModal.js
  36. 33
      src/renderer/ui/components/modals/ThemedDialog.js
  37. 55
      src/renderer/ui/components/modals/UninstallV2Modal.js
  38. 69
      src/renderer/ui/components/modals/UpdateModal.js
  39. 47
      src/renderer/ui/components/modals/WelcomeNewVersionModal.js
  40. 4
      src/renderer/ui/components/settings/tabs/GeneralTab.js
  41. 109
      src/renderer/ui/pages/PlayerPage.js
  42. 44
      src/renderer/ui/pages/SettingsPage.js
  43. 8
      src/renderer/ui/utils/theme.js
  44. 11
      src/renderer/windows/main.js
  45. 0
      src/renderer/windows/settings/audioEQ.js
  46. 0
      src/renderer/windows/settings/audioSelection.js
  47. 0
      src/renderer/windows/settings/customStyle.js
  48. 10
      src/renderer/windows/settings/index.js
  49. 0
      src/renderer/windows/settings/lastFM.js

3
.eslintrc

@ -8,8 +8,7 @@
"WindowManager": true,
"PlaybackAPI": true,
"TranslationProvider": true,
"GPM": true,
"$": true
"GPM": true
},
"env": {
"mocha": true

8
src/assets/less/dialog_window.less

@ -107,11 +107,3 @@
}
}
}
.windows-only {
display: none;
.win32 & {
display: block;
}
}

56
src/assets/less/main_window.less

@ -26,10 +26,6 @@ html, body {
top: 0;
}
.darwin-title-bar {
display: none;
}
// Hide custom frame when native frame is used
.native-frame {
.lean-overlay {
@ -39,35 +35,33 @@ html, body {
top: -100px;
}
.darwin& {
.darwin-title-bar {
height: @title-bar-height-darwin;
transition: margin-top 160ms;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: @title-bar-text-size;
// -webkit-user-select: none;
-webkit-app-region: drag;
padding: 0 70px;
// overflow: hidden;
.darwin-title-bar {
height: @title-bar-height-darwin;
transition: margin-top 160ms;
.title {
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: @grey;
}
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: @title-bar-text-size;
// -webkit-user-select: none;
-webkit-app-region: drag;
padding: 0 70px;
// overflow: hidden;
.title {
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: @grey;
}
background-color: @orange;
[loaded] & {
transition: background-color 0.3s ease;
}
background-color: @orange;
[loaded] & {
transition: background-color 0.3s ease;
}
}

4
src/index.js

@ -56,10 +56,6 @@ updateShortcuts();
}
global.DEV_MODE = process.env['TEST_SPEC'] || argv.development || argv.dev; // eslint-disable-line
if (Settings.get('START_IN_DEV_MODE', false)) {
global.DEV_MODE = true;
Settings.set('START_IN_DEV_MODE', false);
}
// Initialize the logger with some default logging levels.
const defaultFileLogLevel = 'info';

5
src/main/utils/Settings.js

@ -49,6 +49,11 @@ class Settings {
Emitter.sendToGooglePlayMusic(`settings:change:${key}`, value, key);
}
this._save();
} else {
Emitter.fire('settings:set', {
key,
value,
});
}
}

16
src/public_html/desktop_settings.html

@ -5,7 +5,8 @@
<link href="../assets/css/core.css" rel="stylesheet" type="text/css" />
</head>
<body>
<section class="window-border">
<div id="settings-window" style="height: 100%"></div>
<!-- <section class="window-border">
<header class="darwin-title-bar">
<div class="title">Desktop Settings</div>
</header>
@ -28,7 +29,7 @@
<span is="translation-key">title-settings</span>
</div>
<div class="window-main">
<div class="row" id="settings-panel">
<div class="row" id="settings-panel"> -->
<!-- <div class="col s12">
<ul class="tabs">
<li class="tab col s3"><a href="#general" class="theme-text orange-text"><span is="translation-key">title-settings-general</span></a></li>
@ -213,17 +214,12 @@
<a class="waves-effect waves-light btn style-refresh"><i class="material-icons left">refresh</i><span is="translation-key">settings-options-style-refresh</span></a>
</div>
</div> -->
</div>
<!-- </div>
</main>
</section>
<script>window._module = window.module; window.module = undefined;</script>
<script src="../assets/util/jquery.min.js"></script>
<script src="../assets/util/materialize.min.js"></script>
<script src="../assets/util/nouislider.min.js"></script>
</section> -->
<script>
window.module = window._module;
require('../renderer/generic');
require('../renderer/settings');
require('../renderer/windows/settings');
</script>
</body>
</html>

178
src/public_html/index.html

@ -5,183 +5,9 @@
<link href="../assets/css/core.css" rel="stylesheet" type="text/css" />
</head>
<body loading>
<section class="window-border">
<header class="darwin-title-bar">
<div class="title">Google Play Music Desktop Player</div>
</header>
<header class="title-bar">
<div class="drag-handle"></div>
<div class="controls">
<div id="min" class="control">
<img src="../assets/img/control_bar/min.png" />
</div>
<div id="max" class="control">
<img src="../assets/img/control_bar/max.png" />
</div>
<div id="close" class="control">
<img src="../assets/img/control_bar/close.png" />
</div>
</div>
</header>
<main class="embedded-player-container">
<div class="drag-handle-large"></div>
<div class="loader">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"/>
</svg>
</div>
<webview src="https://play.google.com/music/listen" class="embedded-player" preload="../renderer/GPMWebView" nodeIntegration ></webview>
<div class="offline-warning">
<i class="material-icons left">portable_wifi_off</i>
<h1>We can't connect to Google Play Music. Please check your internet connection.</h1>
</div>
</main>
</section>
<!-- Lyrics Container -->
<div id="lyrics_back">
<div id="lyrics_container">
<div id="lyrics">
<h1><span is="translation-key">lyrics-no-song-message</span></h1>
</div>
<div id="shadow"></div>
</div>
<div id="lyrics_progress">
<div id="lyrics_bar" class="lyrics-progress"></div>
</div>
</div>
<!-- End Lyrics Container -->
<!-- Confirm Minimize to Tray Modal -->
<div id="confirmTray" class="modal">
<div class="modal-content">
<h4 is="translation-key-h4">modal-confirmTray-title</h4>
<p is="translation-key-p">modal-confirmTray-content</p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" id="confirmTrayButton" class="modal-action modal-close waves-effect waves-green btn-flat">
<span is="translation-key">button-test-ok</span>
</a>
<a href="javascript:void(0)" id="confirmTrayNeverButton" class="modal-action modal-close waves-effect waves-orange btn-flat">
<span is="translation-key">button-text-dont-tell-me-again</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- Confirm uninstall of 2.X Modal -->
<div id="confirmUninstall" class="modal">
<div class="modal-content">
<h4 is="translation-key-h4">modal-confirmUninstall-title</h4>
<p is="translation-key-p">modal-confirmUninstall-content</p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" class="modal-action modal-close waves-effect waves-green btn-flat">
<span is="translation-key">button-test-ok</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- Code Pop Up -->
<div id="APICode" class="modal">
<div class="modal-content">
<span id="APICodeContainer"></span>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" class="modal-action modal-close waves-effect waves-green btn-flat">
<span is="translation-key">button-test-ok</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- Confirm open port spawn -->
<div id="confirmOpenPort" class="modal">
<div class="modal-content">
<h4 is="translation-key-h4">modal-confirmTray-title</h4>
<p is="translation-key-p">modal-confirmOpenPort-content</p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" id="confirmOpenPortButton" class="modal-action modal-close waves-effect waves-green btn" style="margin-right: 6px">
<span is="translation-key">button-text-lets-do-this</span>
</a>
<a href="javascript:void(0)" id="notNowOpenPortButton" class="modal-action modal-close waves-effect waves-orange btn-flat red-text">
<span is="translation-key">button-text-not-now</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- Confirm restart and update Modal -->
<div id="confirmUpdate" class="modal">
<div class="modal-content">
<h4 is="translation-key-h4">modal-confirmUpdate-title</h4>
<p is="translation-key-p">modal-confirmUpdate-content</p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" id="confirmUpdateButton" class="modal-action modal-close waves-effect waves-green btn" style="margin-right: 6px">
<span is="translation-key">button-text-lets-do-this</span>
</a>
<a href="javascript:void(0)" id="waitUpdateButton" class="modal-action modal-close waves-effect waves-orange btn-flat red-text">
<span is="translation-key">button-text-not-now</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- Welcome to a new version Modal -->
<div id="welcome" class="modal">
<div class="modal-content">
<h4><span is="translation-key">modal-welcome-title</span> <span data-app-version></span></h4>
<p data-app-changes></p>
<p is="translation-key-p">modal-welcome-content</p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" class="modal-action modal-close waves-effect waves-green btn" style="margin-right: 6px">
<span is="translation-key">button-text-lets-go</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- Go to GPM URL Modal -->
<div id="goToURL" class="modal">
<div class="modal-content">
<h4 is="translation-key-h4">modal-goToURL-title</h4>
<p>
<input type="text" placeholder="E.g. https://play.google.com/music/listen" />
</p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" class="modal-action modal-close waves-effect waves-green btn" style="margin-right: 6px">
<span is="translation-key">button-text-lets-go</span>
</a>
</div>
</div>
<!-- End Modal -->
<!-- About GPMDP Modal -->
<div id="about" class="modal">
<div class="modal-content">
<h4><span is="translation-key">label-about</span> <span data-app-name></span></h4>
<p><span is="translation-key">label-version</span>: <span data-app-version></span></p>
<p><span data-app-dev-mode></span></p>
</div>
<div class="modal-footer">
<a href="javascript:void(0)" class="modal-action modal-close waves-effect waves-green btn">
<span is="translation-key">button-text-close</span>
</a>
</div>
</div>
<!-- End Modal -->
<script>window._module = window.module; window.module = undefined;</script>
<script src="../assets/util/jquery.min.js"></script>
<script src="../assets/util/materialize.min.js"></script>
<div id="main-window" style="height: 100%"></div>
<script>
window.module = window._module;
document.body.classList.add(process.platform);
require('../renderer/windows/main');
</script>
</body>
</html>

3
src/renderer/GPMWebView/interface/customNavigation/goToURL.js

@ -0,0 +1,3 @@
Emitter.on('navigate:gotourl', (event, data) => {
window.location = data;
});

1
src/renderer/GPMWebView/interface/customNavigation/index.js

@ -1,2 +1,3 @@
import './goToURL';
import './hotkeyNavigation';
import './mouseButtonNavigation';

8
src/renderer/generic/core/APICode.js

@ -1,8 +0,0 @@
Emitter.on('show:code_controller', (event, data) => {
$('#APICode').find('#APICodeContainer').text(data.authCode);
$('#APICode').openModal();
});
Emitter.on('hide:code_controller', () => {
$('#APICode').closeModal();
});

27
src/renderer/generic/core/appDetails.js

@ -1,27 +0,0 @@
import { remote } from 'electron';
import fs from 'fs';
import path from 'path';
window.addEventListener('load', () => {
if (window.$ && Settings.get('welcomed') !== remote.app.getVersion() && $('#welcome').length) {
$('[data-app-changes]').html(fs.readFileSync(path.resolve(`${__dirname}/../../../../MR_CHANGELOG.html`), 'utf8')); // eslint-disable-line
$('#welcome').openModal({
dismissible: false,
complete: () => {
Emitter.fire('welcomed', remote.app.getVersion());
},
});
}
if (window.$) {
const modal = $('#about');
$('[data-app-version]').text(remote.app.getVersion());
$('[data-app-name]').text(remote.app.getName());
$('[data-app-dev-mode]').text(remote.getGlobal('DEV_MODE') ? 'Running in Development Mode' : '');
Emitter.on('about', () => {
modal.openModal({
dismissable: true,
});
});
}
});

40
src/renderer/generic/core/control-bar.js

@ -1,40 +0,0 @@
import { remote } from 'electron';
const minButton = document.querySelector('#min');
const maxButton = document.querySelector('#max');
const closeButton = document.querySelector('#close');
if (minButton) {
minButton.addEventListener('click',
Emitter.fire.bind(Emitter, 'window:minimize', remote.getCurrentWindow().id));
}
if (maxButton) {
maxButton.addEventListener('click',
Emitter.fire.bind(Emitter, 'window:maximize', remote.getCurrentWindow().id));
}
if (closeButton) {
closeButton.addEventListener('click', () => {
if (document.getElementById('confirmTray') && window.$ && Settings.get('warnMinToTray', true) &&
Settings.get('minToTray', true)) {
if ($('#confirmTray').data('open')) {
return;
}
$('#confirmTray').data('open', true);
$('#confirmTray').openModal();
$('#confirmTrayButton').one('click', () => {
Emitter.fire('window:close', remote.getCurrentWindow().id);
$('#confirmTray').data('open', false);
});
$('#confirmTrayNeverButton').one('click', () => {
Emitter.fire('window:close', remote.getCurrentWindow().id);
Emitter.fire('settings:set', {
key: 'warnMinToTray',
value: false,
});
$('#confirmTray').data('open', false);
});
} else {
Emitter.fire('window:close', remote.getCurrentWindow().id);
}
});
}

19
src/renderer/generic/core/devtools.js

@ -1,19 +0,0 @@
import { remote } from 'electron';
if (remote.getGlobal('DEV_MODE')) {
try {
require('devtron').install();
} catch (e) {
// Who cares
}
window.addEventListener('load', () => {
const webview = document.querySelector('webview');
if (!webview) {
return;
}
window.openGPMDevTools = () => {
webview.openDevTools();
};
});
}

47
src/renderer/generic/core/goToURL.js

@ -1,47 +0,0 @@
import { remote } from 'electron';
const parseURL = (url) => {
if (url === 'DEV_MODE') {
const ok = confirm('You have instructed GPMDP to start in Dev Mode the next ' + // eslint-disable-line
'time it launches. Please be careful and only continue if you know ' +
'what you are doing or have been told what to do by a project maintainer.');
if (!ok) return;
Emitter.fire('settings:set', {
key: 'START_IN_DEV_MODE',
value: true,
});
// Give Settings time to flush to the FS
setTimeout(() => {
remote.app.quit();
}, 500);
} else if (url === 'DEBUG_INFO') {
Emitter.fire('generateDebugInfo');
}
if (!/https:\/\/play\.google\.com\/music\/listen/g.test(url)) return;
document.querySelector('webview').executeJavaScript(`window.location = "${url}"`);
};
window.addEventListener('load', () => {
if (!window.$) return;
const modal = $('#goToURL');
const URLInput = modal.find('input');
const URLButton = modal.find('a');
Emitter.on('gotourl', () => {
modal.openModal({
dismissible: true,
});
URLInput.focus();
});
URLButton.click(() => {
parseURL(URLInput.val());
});
URLInput.on('keydown', (e) => {
if (e.which !== 13) return;
modal.closeModal();
parseURL(URLInput.val());
});
});

43
src/renderer/generic/core/index.js

@ -1,15 +1,30 @@
import './APICode';
import './devtools';
import './goToURL';
import './uninstall';
import './updateAvailable';
import './appDetails';
import './lyrics';
import './openPort';
import './onlineStatus';
import { remote } from 'electron';
// Anything that requires the DOM needs to go here as of Electron 0.36.6
document.addEventListener('DOMContentLoaded', () => {
require('./webviewLoader');
require('./control-bar');
});
if (remote.getGlobal('DEV_MODE')) {
// Attempt to install DevTron
try {
if (!remote.BrowserWindow.getDevToolsExtensions().hasOwnProperty('devtron')) {
require('devtron').install();
}
} catch (e) {
// Who cares
}
// Attempt to install React Developer Tools
try {
const devtoolsInstaller = require('electron-devtools-installer');
devtoolsInstaller.default(devtoolsInstaller.REACT_DEVELOPER_TOOLS);
} catch (err) {
// Whoe cares
}
window.addEventListener('load', () => {
const webview = document.querySelector('webview');
if (!webview) {
return;
}
window.openGPMDevTools = () => {
webview.openDevTools();
};
});
}

84
src/renderer/generic/core/lyrics.js

@ -1,84 +0,0 @@
window.addEventListener('load', () => {
if (!window.$) return;
if (!$('#lyrics').length) return;
let animate = false;
let animationTimer;
let noLyricsTimer;
let jumpDetect;
let isPlaying = false;
// Handle new lyrics strings
const lyricsHandler = (lyrics) => {
if (!lyrics) {
$('#lyrics').html('<h1><span is="translation-key">lyrics-loading-message</span></h1>');
$('#lyrics p').stop();
animate = false;
clearTimeout(noLyricsTimer);
noLyricsTimer = setTimeout(() => {
$('#lyrics').html('<h1><span is="translation-key">lyrics-failed-message</span></h1>');
}, 4000);
} else {
clearTimeout(noLyricsTimer);
const scroll = Settings.get('scrollLyrics', true);
const lyricsHTML = lyrics.replace(/\n/g, '<br />');
$('#lyrics').html(`<p ${scroll ? 'data-scroll' : ''}>${lyricsHTML}</p>`);
animate = scroll;
}
};
// Handle playing and pausing
const stateHandler = (remoteIsPlaying) => {
isPlaying = remoteIsPlaying;
if (!isPlaying) return $('#lyrics p').stop();
animate = Settings.get('scrollLyrics', true);
};
// Handle time progression of a song
const timeHandler = (timeObj) => {
$('#lyrics_bar').width(`${(timeObj.total === 0 ? 0 : timeObj.current / timeObj.total) * 100}%`);
let jumped = false;
if (Math.abs(timeObj.current - jumpDetect) > 1000 && $('#lyrics p').attr('data-scroll')) {
animate = true;
jumped = true;
}
jumpDetect = timeObj.current;
if (!isPlaying || !animate || !timeObj.total || !$('#lyrics p').get(0)) return;
const lyricsP = $('#lyrics p');
const maxHeight = parseInt(lyricsP.get(0).scrollHeight, 10);
const viewPortHeight = parseInt(lyricsP.innerHeight(), 10);
const waitTime = (viewPortHeight / maxHeight) * timeObj.total * 0.3;
const actualWaitTime = Math.max(0, waitTime - timeObj.current);
clearTimeout(animationTimer);
if (jumped) {
lyricsP.stop();
lyricsP.scrollTop(maxHeight * (Math.max(0, timeObj.current - actualWaitTime) / timeObj.total));
}
animationTimer = setTimeout(() => {
lyricsP.stop().animate({
scrollTop: maxHeight - viewPortHeight,
}, timeObj.total - timeObj.current - actualWaitTime - waitTime, 'linear');
}, actualWaitTime);
animate = false;
};
const scrollSettingsHandler = (state) => {
const lyricsP = $('#lyrics p');
animate = state;
if (state) {
lyricsP.attr('data-scroll', true);
} else {
lyricsP.removeAttr('data-scroll');
clearTimeout(animationTimer);
lyricsP.stop();
}
};
Emitter.on('PlaybackAPI:change:lyrics', (e, arg) => lyricsHandler(arg));
Emitter.on('PlaybackAPI:change:state', (e, arg) => stateHandler(arg));
Emitter.on('PlaybackAPI:change:time', (e, arg) => timeHandler(arg));
Emitter.on('settings:set:scrollLyrics', (e, arg) => scrollSettingsHandler(arg));
window.addEventListener('resize', () => { animate = true; });
$('#lyrics_back').click(() => $('#lyrics_back').removeClass('vis'));
});
Emitter.on('lyrics:show', () => $('#lyrics_back').addClass('vis'));

13
src/renderer/generic/core/onlineStatus.js

@ -1,13 +0,0 @@
window.addEventListener('load', () => {
if (!navigator.onLine) {
document.body.classList.add('offline');
}
});
window.addEventListener('online', () => {
document.body.classList.remove('offline');
// DEV: This is currently fired when resuming from sleep
// Disabling this line till we can figure it out
// document.querySelector('webview').reload();
});

8
src/renderer/generic/core/openPort.js

@ -1,8 +0,0 @@
Emitter.on('openport:request', () => {
$('#confirmOpenPort').openModal({
dismissible: false,
});
$('#confirmOpenPortButton').one('click', () => {
Emitter.fire('openport:confirm');
});
});

8
src/renderer/generic/core/uninstall.js

@ -1,8 +0,0 @@
Emitter.on('uninstall:request', () => {
$('#confirmUninstall').openModal({
dismissible: false,
complete: () => {
Emitter.fire('uninstall:confirm');
},
});
});

16
src/renderer/generic/core/updateAvailable.js

@ -1,16 +0,0 @@
Emitter.on('update:available', () => {
$('#confirmUpdate').openModal({
dismissible: false,
});
});
window.addEventListener('load', () => {
if (!window.$) return;
$('#confirmUpdateButton').click(() => {
Emitter.fire('update:trigger');
});
$('#waitUpdateButton').click(() => {
Emitter.fire('update:wait');
});
});

49
src/renderer/generic/core/webviewLoader.js

@ -1,49 +0,0 @@
import { remote } from 'electron';
const webview = document.querySelector('webview');
if (process.env.TEST_SPEC) {
webview.setAttribute('partition', '__TEST__');
}
if (webview) {
let once = true;
const targetPage = Settings.get('savePage', true) ?
Settings.get('lastPage', 'https://play.google.com/music/listen') :
'https://play.google.com/music/listen';
document.body.setAttribute('loading', 'loading');
webview.addEventListener('did-stop-loading', () => {
if (once) {
once = false;
document.querySelector('webview').executeJavaScript(`window.location = "${targetPage}"`);
setTimeout(() => {
document.body.removeAttribute('loading');
}, 300);
}
});
const savePage = (param) => {
const url = param.url || param;
if (!/https?:\/\/play\.google\.com\/music/g.test(url)) return;
Emitter.fire('settings:set', {
key: 'lastPage',
value: url,
});
};
webview.addEventListener('dom-ready', () => {
setTimeout(() => {
webview.focus();
webview.addEventListener('did-navigate', savePage);
webview.addEventListener('did-navigate-in-page', savePage);
const focusWebview = () => {
document.querySelector('webview::shadow object').focus();
};
window.addEventListener('beforeunload', () => {
remote.getCurrentWindow().removeListener('focus', focusWebview);
});
remote.getCurrentWindow().on('focus', focusWebview);
}, 700);
});
}

46
src/renderer/generic/customWindowThemeHandler.js

@ -1,46 +0,0 @@
import GMusicTheme from 'gmusic-theme.js';
// Hack the crap out of GMusicTheme
Object.assign(GMusicTheme.prototype, {
_drawLogo: () => {},
_refreshStyleSheet: () => {},
disable: () => {},
enable: () => {},
redrawTheme: () => {},
});
let customColor = Settings.get('themeColor');
let themeType = Settings.get('themeType', 'FULL');
let styles = '';
const hackedGPMTheme = new GMusicTheme();
const customStyle = document.createElement('style');
document.body.appendChild(customStyle);
const redrawCustomStyles = () => {
hackedGPMTheme.updateTheme({
type: themeType,
backHighlight: '#1a1b1d',
foreSecondary: customColor,
});
customStyle.innerHTML = hackedGPMTheme.substituteColors(styles);
};
Emitter.on('settings:change:themeColor', (event, newCustomColor) => {
customColor = newCustomColor;
redrawCustomStyles();
});
Emitter.on('settings:change:themeType', (event, newThemeType) => {
themeType = newThemeType;
redrawCustomStyles();
});
Emitter.on('LoadMainAppCustomStyles', (event, newStyles) => {
styles = newStyles;
redrawCustomStyles();
});
redrawCustomStyles();
Emitter.fire('FetchMainAppCustomStyles');

2
src/renderer/generic/index.js

@ -16,8 +16,6 @@ require('./translations');
document.addEventListener('DOMContentLoaded', () => {
require('./windowThemeHandler');
setTimeout(() => require('electron').remote.getCurrentWindow().show(), 100);
const nativeFrameAtLaunch = Settings.get('nativeFrame');
document.body.classList.toggle('native-frame', nativeFrameAtLaunch);

96
src/renderer/generic/windowThemeHandler.js

@ -1,56 +1,46 @@
if (window.$ && window.$.ajax) {
require('./customWindowThemeHandler');
Emitter.on('settings:change:theme', (event, state) => {
if (!state) {
document.body.removeAttribute('theme');
} else {
document.body.setAttribute('theme', 'on');
}
});
Emitter.on('settings:change:themeType', (event, type) => {
if (type === 'FULL') {
document.body.setAttribute('full', 'full');
document.body.removeAttribute('light');
} else {
document.body.removeAttribute('full');
document.body.setAttribute('light', 'light');
}
});
import GMusicTheme from 'gmusic-theme.js';
Emitter.on('window:updateTitle', (event, newTitle) => {
const titleBar = document.querySelector('.darwin-title-bar .title');
if (titleBar) {
titleBar.innerHTML = newTitle;
}
});
// Hack the crap out of GMusicTheme
Object.assign(GMusicTheme.prototype, {
_drawLogo: () => {},
_refreshStyleSheet: () => {},
disable: () => {},
enable: () => {},
redrawTheme: () => {},
});
let customColor = Settings.get('themeColor');
let themeType = Settings.get('themeType', 'FULL');
let styles = '';
const hackedGPMTheme = new GMusicTheme();
if (Settings.get('theme')) {
document.body.setAttribute('theme', 'on');
}
if (Settings.get('themeType', 'FULL') === 'FULL') {
document.body.setAttribute('full', 'full');
} else {
document.body.setAttribute('light', 'light');
}
const style = $('<style></style>');
$('body').append(style);
const redrawTheme = (customColor) => {
const color = customColor || Settings.get('themeColor');
const border = `[theme][light] .window-border{border-color:${color}}`;
const titleBar = `[theme][light] .title-bar{background:${color}}`;
const darwinTitleBar = `[theme][light] .darwin-title-bar{background-color:${color}}`;
const header = `[theme][light] .dialog .window-title{background:${color}}`;
const lyricsProgress = `
[theme][full] #lyrics_bar{background:${color} !important}
[theme][light] #lyrics_progress{background:${color} !important}
[theme][full] #lyrics_progress{background:#222326 !important}`; // @darkprimary
style.html(border + titleBar + darwinTitleBar + header + lyricsProgress);
document.body.setAttribute('loaded', 'loaded');
};
redrawTheme();
Emitter.on('settings:change:themeColor', (event, customColor) => {
redrawTheme(customColor);
const customStyle = document.createElement('style');
document.body.appendChild(customStyle);
const redrawCustomStyles = () => {
hackedGPMTheme.updateTheme({
type: themeType,
backHighlight: '#1a1b1d',
foreSecondary: customColor,
});
}
customStyle.innerHTML = hackedGPMTheme.substituteColors(styles);
};
Emitter.on('settings:change:themeColor', (event, newCustomColor) => {
customColor = newCustomColor;
redrawCustomStyles();
});
Emitter.on('settings:change:themeType', (event, newThemeType) => {
themeType = newThemeType;
redrawCustomStyles();
});
Emitter.on('LoadMainAppCustomStyles', (event, newStyles) => {
styles = newStyles;
redrawCustomStyles();
});
redrawCustomStyles();
Emitter.fire('FetchMainAppCustomStyles');

57
src/renderer/settings/index.js

@ -1,57 +0,0 @@
// import '../generic/translations';
// import './themeSettings';
// import './audioSelection';
// // import './audioEQ';
// import './hotkeys';
// import './tray';
// import './lastFM';
// import './mini';
// import './general';
// import './customStyle';
import React from 'react';
import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin';
import SettingsPage from '../ui/pages/SettingsPage';
try {
const devtoolsInstaller = require('electron-devtools-installer');
devtoolsInstaller.default(devtoolsInstaller.REACT_DEVELOPER_TOOLS);
} catch (err) {
// Whoe cares
}
document.body.classList.add(process.platform);
injectTapEventPlugin();
if (window.$) {
$(() => {
ReactDOM.render(<SettingsPage />, document.querySelector('#settings-panel'));
// $('ul.tabs').tabs();
// $('.indicator').addClass('theme-back').addClass('orange');
//
// const style = $('<style></style>');
// $('body').append(style);
// const redrawTheme = (customColor) => {
// const color = customColor || Settings.get('themeColor');
// const text = `[theme] .theme-text{color:${color} !important;}`;
// const back = `[theme] .theme-back{background:${color} !important;}`;
// const checkbox = `[theme] input[type=checkbox]:checked + label::after{background:${color}` +
// `!important;border-color:${color} !important;}`;
// const slider = `[theme] .range-label{background:${color};border:none}
// [theme] .noUi-horizontal .noUi-handle{background:transparent}`;
// const input = `[theme] .input-field input[type=text]:focus + label {color:${color};}
// [theme] .input-field input[type=text]:focus {border-bottom-color:${color};
// box-shadow: 0 1px 0 0 ${color};}`;
// const button = `[theme] .btn{background:${color}}`;
// const switch_ = '.switch label input[type=checkbox]:checked+.lever{background:#aaa}';
// const toggle_ = `.switch label input[type=checkbox]:checked+.lever:after{background:${color}}`; // eslint-disable-line
// style.html(text + back + checkbox + slider + input + button + switch_ + toggle_);
// };
//
// redrawTheme();
// Emitter.on('settings:change:themeColor', (event, customColor) => {
// redrawTheme(customColor);
// });
});
}

168
src/renderer/ui/components/generic/LyricsViewer.js

@ -0,0 +1,168 @@
import $ from 'jquery';
import React, { Component, PropTypes } from 'react';
import { requireSettings } from './SettingsProvider';
class LyricsViewer extends Component {
static propTypes = {
theme: PropTypes.bool.isRequired,
themeColor: PropTypes.string.isRequired,
themeType: PropTypes.string.isRequired,
};
constructor(...args) {
super(...args);
this.state = {
visible: false,
};
}
// I hate this so much
// But I also don't know how to do this with React so meh
// TODO: Clean this rubbish up
componentDidMount() {
let animate = false;
let animationTimer;
let noLyricsTimer;
let jumpDetect;
let isPlaying = false;
// Handle new lyrics strings
this.lyricsHandler = (e, lyrics) => {
if (!lyrics) {
$('#lyrics').html('<h1><span is="translation-key">lyrics-loading-message</span></h1>');
$('#lyrics p').stop();
animate = false;
clearTimeout(noLyricsTimer);
noLyricsTimer = setTimeout(() => {
$('#lyrics').html('<h1><span is="translation-key">lyrics-failed-message</span></h1>');
}, 4000);
} else {
clearTimeout(noLyricsTimer);
const scroll = Settings.get('scrollLyrics', true);
const lyricsHTML = lyrics.replace(/\n/g, '<br />');
$('#lyrics').html(`<p ${scroll ? 'data-scroll' : ''}>${lyricsHTML}</p>`);
animate = scroll;
}
};
// Handle playing and pausing
this.stateHandler = (e, remoteIsPlaying) => {
isPlaying = remoteIsPlaying;
if (!isPlaying) return $('#lyrics p').stop();
animate = Settings.get('scrollLyrics', true);
};
// Handle time progression of a song
this.timeHandler = (e, timeObj) => {
$('#lyrics_bar').width(`${(timeObj.total === 0 ? 0 : timeObj.current / timeObj.total) * 100}%`);
let jumped = false;
if (Math.abs(timeObj.current - jumpDetect) > 1000 && $('#lyrics p').attr('data-scroll')) {
animate = true;
jumped = true;
}
jumpDetect = timeObj.current;
if (!isPlaying || !animate || !timeObj.total || !$('#lyrics p').get(0)) return;
const lyricsP = $('#lyrics p');
const maxHeight = parseInt(lyricsP.get(0).scrollHeight, 10);
const viewPortHeight = parseInt(lyricsP.innerHeight(), 10);
const waitTime = (viewPortHeight / maxHeight) * timeObj.total * 0.3;
const actualWaitTime = Math.max(0, waitTime - timeObj.current);
clearTimeout(animationTimer);
if (jumped) {
lyricsP.stop();
lyricsP.scrollTop(maxHeight * (Math.max(0, timeObj.current - actualWaitTime) / timeObj.total));
}
animationTimer = setTimeout(() => {
lyricsP.stop().animate({
scrollTop: maxHeight - viewPortHeight,
}, timeObj.total - timeObj.current - actualWaitTime - waitTime, 'linear');
}, actualWaitTime);
animate = false;
};
this.scrollSettingsHandler = (e, state) => {
const lyricsP = $('#lyrics p');
animate = state;
if (state) {
lyricsP.attr('data-scroll', true);
} else {
lyricsP.removeAttr('data-scroll');
clearTimeout(animationTimer);
lyricsP.stop();
}
};
this.startAnimating = () => {
animate = true;
};
this._hook();
}
componentWillUnmount() {
this._unhook();
}
_hook() {
Emitter.on('lyrics:show', this.show);
Emitter.on('PlaybackAPI:change:lyrics', this.lyricsHandler);
Emitter.on('PlaybackAPI:change:state', this.stateHandler);
Emitter.on('PlaybackAPI:change:time', this.timeHandler);
Emitter.on('settings:set:scrollLyrics', this.scrollSettingsHandler);
window.addEventListener('resize', this.startAnimating);
}
_unhook() {
Emitter.off('lyrics:show', this.show);
Emitter.off('PlaybackAPI:change:lyrics', this.lyricsHandler);
Emitter.off('PlaybackAPI:change:state', this.stateHandler);
Emitter.off('PlaybackAPI:change:time', this.timeHandler);
Emitter.off('settings:set:scrollLyrics', this.scrollSettingsHandler);
window.removeEventListener('resize', this.startAnimating);
}
hide = () => {
this.setState({
visible: false,
});
}
show = () => {
this.setState({
visible: true,
});
}
render() {
const barStyle = {};
const progressStyle = {};
if (this.props.theme) {
if (this.props.themeType === 'FULL') {
progressStyle.backgroundColor = this.props.themeColor;
} else {
barStyle.backgroundColor = this.props.themeColor;
progressStyle.backgroundColor = '#222326';
}
}
return (
<div id="lyrics_back" className={this.state.visible ? 'vis' : ''} onClick={this.hide}>
<div id="lyrics_container">
<div id="lyrics">
<h1>
{TranslationProvider.query('lyrics-no-song-message')}
</h1>
</div>
<div id="shadow"></div>
</div>
<div id="lyrics_progress" style={progressStyle}>
<div id="lyrics_bar" className="lyrics-progress" style={barStyle}></div>
</div>
</div>
);
}
}
export default requireSettings(LyricsViewer, ['theme', 'themeColor', 'themeType']);

35
src/renderer/ui/components/generic/OfflineWarning.js

@ -0,0 +1,35 @@
import React, { Component } from 'react';
export default class OfflineWarning extends Component {
constructor(...args) {
super(...args);
this.state = {
online: navigator.onLine,
};
}
componentDidMount() {
window.addEventListener('online', this._handleOnlineChange);
}
componentWillUnmount() {
window.removeEventListener('online', this._handleOnlineChange);
}
_handleOnlineChange = () => {
this.setState({
online: navigator.onLine,
});
}
render() {
if (this.state.online) return null;
return (
<div className="offline-warning">
<i className="material-icons left">portable_wifi_off</i>
<h1>We can't connect to Google Play Music. Please check your internet connection.</h1>
</div>
);
}
}

12
src/renderer/ui/components/generic/SettingsProvider.js

@ -35,10 +35,7 @@ export default class SettingsProvider extends Component {
}
setSetting(key, value) {
Emitter.fire('settings:set', {
key,
value,
});
Settings.set(key, value);
}
handleKeyChange = (event, keyValue, keyName) => {
@ -55,7 +52,10 @@ export default class SettingsProvider extends Component {
}
}
export const requireSettings = (component, settingsArray, settingsDefaults = {}) =>
(props) => (
export const requireSettings = (component, settingsArray, settingsDefaults = {}) => {
const WrappedComponent = (props) => (
<SettingsProvider component={component} componentProps={props} keys={settingsArray} defaults={settingsDefaults} />
);
WrappedComponent.displayName = `Wrapped${component.name ? component.name : 'Component'}`;
return WrappedComponent;
};

83
src/renderer/ui/components/generic/WebView.js

@ -0,0 +1,83 @@
import _ from 'lodash';
import React, { Component, PropTypes } from 'react';
import { findDOMNode } from 'react-dom';
const EVENTS = [
'load-commit',
'did-finish-load',
'did-fail-load',
'did-frame-finish-load',
'did-start-loading',
'did-stop-loading',
'did-get-response-details',
'did-get-redirect-request',
'dom-ready',
'page-title-set',
'page-favicon-updated',
'enter-html-full-screen',
'leave-html-full-screen',
'console-message',
'new-window',
'close',
'ipc-message',
'crashed',
'gpu-crashed',
'plugin-crashed',
'destroyed',
];
const METHODS = [
'focus',
'executeJavaScript',
];
export default class WebView extends Component {
static propTypes = {
src: PropTypes.string.isRequired,
className: PropTypes.string.isRequired,
preload: PropTypes.string.isRequired,
}
componentDidMount() {
const view = findDOMNode(this.refs.view);
EVENTS.forEach((eventKey) => {
view.addEventListener(eventKey, (...args) => {
// console.info(eventKey, args);
if (this.props[_.camelCase(eventKey)]) {
this.props[_.camelCase(eventKey)](...args);
}
});
});
METHODS.forEach(method => {
this[method] = (...args) => {
if (!view[method]) return;
view[method](...args);
};
});
view.addEventListener('dom-ready', () => {
view.addEventListener('did-navigate', (...args) => {
if (this.props.didNavigate) this.props.didNavigate(...args); // eslint-disable-line
});
view.addEventListener('did-navigate-in-page', (...args) => {
if (this.props.didNavigateInPage) this.props.didNavigateInPage(...args); // eslint-disable-line
});
});
}
render() {
return (
<webview
ref="view"
src={this.props.src}
className={this.props.className}
preload={this.props.preload}
/>
);
}
}
EVENTS.forEach((propTypes, event) => {
WebView.propTypes[_.camelCase(event)] = React.PropTypes.func;
});

129
src/renderer/ui/components/generic/WindowContainer.js

@ -0,0 +1,129 @@
import { remote } from 'electron';
import React, { Component, PropTypes } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import PlatformSpecific from './PlatformSpecific';
import { requireSettings } from './SettingsProvider';
import generateTheme from '../../utils/theme';
class WindowContainer extends Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
]).isRequired,
confirmClose: PropTypes.func,
isMainWindow: PropTypes.bool,
title: PropTypes.string.isRequired,
};
constructor(...args) {
super(...args);
this.state = {
nativeFrame: Settings.get('nativeFrame'),
theme: Settings.get('theme'),
themeColor: Settings.get('themeColor'),
themeType: Settings.get('themeType', 'FULL'),
};
}
componentDidMount() {
Emitter.on('settings:change:theme', this._themeUpdate);
Emitter.on('settings:change:themeColor', this._themeColorUpdate);
Emitter.on('settings:change:themeType', this._themeTypeUpdate);
}
componentWillUnmount() {
Emitter.off('settings:change:theme', this._themeUpdate);
Emitter.off('settings:change:themeColor', this._themeColorUpdate);
Emitter.off('settings:change:themeType', this._themeTypeUpdate);
}
_themeUpdate = (event, theme) => {
this.setState({ theme });
}
_themeColorUpdate = (event, themeColor) => {
this.setState({ themeColor });
}
_themeTypeUpdate = (event, themeType) => {
this.setState({ themeType });
}
minWindow = () => {
Emitter.fire('window:minimize', remote.getCurrentWindow().id);
}
maxWindow = () => {
Emitter.fire('window:maximize', remote.getCurrentWindow().id);
}
closeWindow = () => {
if (this.props.isMainWindow && this.props.confirmClose && Settings.get('warnMinToTray', true) && Settings.get('minToTray', true)) {
this.props.confirmClose();
} else {
Emitter.fire('window:close', remote.getCurrentWindow().id);
}
}
render() {
const muiTheme = generateTheme(this.state.theme, this.state.themeColor, this.state.themeType);
const fadedBackground = {};
if (this.state.theme && this.state.themeType === 'FULL') {
fadedBackground.backgroundColor = '#121212';
fadedBackground.color = '#FAFAFA';
}
return (
<MuiThemeProvider muiTheme={muiTheme}>
<section className="window-border" style={{ borderColor: muiTheme.tabs.backgroundColor }}>
<PlatformSpecific platform="darwin">
<header className="darwin-title-bar" style={{ backgroundColor: muiTheme.tabs.backgroundColor }}>
<div className="title">{this.props.title}</div>
</header>
</PlatformSpecific>
<header className="title-bar" style={{ backgroundColor: muiTheme.tabs.backgroundColor }}>
<div className="drag-handle"></div>
<div className="controls">
{
['min', 'max', 'close'].map((action) => (
<div key={action} className="control" onClick={this[`${action}Window`]}>
<img src={`../assets/img/control_bar/${action}.png`} alt={action} />
</div>
))
}
</div>
</header>
{
this.props.isMainWindow ?
(
<main className="embedded-player-container" style={fadedBackground}>
{
this.props.children
}
</main>
) :
(
<main className="dialog">
<div className="window-title" style={{ backgroundColor: muiTheme.tabs.backgroundColor }}>
{this.props.title}
</div>
<div className="window-main" style={fadedBackground}>
{
this.props.children
}
</div>
</main>
)
}
</section>
</MuiThemeProvider>
);
}
}
export default requireSettings(WindowContainer, ['theme', 'themeColor', 'themeType']);

63
src/renderer/ui/components/modals/APICodeModal.js

@ -0,0 +1,63 @@
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';
export default class APICodeModal extends Component {
constructor(...args) {
super(...args);
this.state = {
code: '1234',
open: false,
};
}
componentDidMount() {
Emitter.on('show:code_controller', this._show);
Emitter.on('hide:code_controller', this._hide);
}
componentWillUnmount() {
Emitter.off('show:code_controller', this._show);
Emitter.off('hide:code_controller', this._hide);
}
handleClose = () => {
this.setState({
open: false,
});
}
_hide = () => {
this.handleClose();
}
_show = (event, data) => {
this.setState({
code: data.authCode,
open: true,
});
}
render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-test-ok')}
primary
keyboardFocused
onTouchTap={this.handleClose}