Browse Source

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

tags/v4.0.0
Samuel Attard 3 years ago
parent
commit
0ec46152e8
No account linked to committer's email address
49 changed files with 1233 additions and 773 deletions
  1. 1
    2
      .eslintrc
  2. 0
    8
      src/assets/less/dialog_window.less
  3. 25
    31
      src/assets/less/main_window.less
  4. 0
    4
      src/index.js
  5. 5
    0
      src/main/utils/Settings.js
  6. 6
    10
      src/public_html/desktop_settings.html
  7. 2
    176
      src/public_html/index.html
  8. 3
    0
      src/renderer/GPMWebView/interface/customNavigation/goToURL.js
  9. 1
    0
      src/renderer/GPMWebView/interface/customNavigation/index.js
  10. 0
    8
      src/renderer/generic/core/APICode.js
  11. 0
    27
      src/renderer/generic/core/appDetails.js
  12. 0
    40
      src/renderer/generic/core/control-bar.js
  13. 0
    19
      src/renderer/generic/core/devtools.js
  14. 0
    47
      src/renderer/generic/core/goToURL.js
  15. 29
    14
      src/renderer/generic/core/index.js
  16. 0
    84
      src/renderer/generic/core/lyrics.js
  17. 0
    13
      src/renderer/generic/core/onlineStatus.js
  18. 0
    8
      src/renderer/generic/core/openPort.js
  19. 0
    8
      src/renderer/generic/core/uninstall.js
  20. 0
    16
      src/renderer/generic/core/updateAvailable.js
  21. 0
    49
      src/renderer/generic/core/webviewLoader.js
  22. 0
    46
      src/renderer/generic/customWindowThemeHandler.js
  23. 0
    2
      src/renderer/generic/index.js
  24. 43
    53
      src/renderer/generic/windowThemeHandler.js
  25. 0
    57
      src/renderer/settings/index.js
  26. 168
    0
      src/renderer/ui/components/generic/LyricsViewer.js
  27. 35
    0
      src/renderer/ui/components/generic/OfflineWarning.js
  28. 6
    6
      src/renderer/ui/components/generic/SettingsProvider.js
  29. 83
    0
      src/renderer/ui/components/generic/WebView.js
  30. 129
    0
      src/renderer/ui/components/generic/WindowContainer.js
  31. 63
    0
      src/renderer/ui/components/modals/APICodeModal.js
  32. 65
    0
      src/renderer/ui/components/modals/AboutModal.js
  33. 58
    0
      src/renderer/ui/components/modals/ConfirmTrayModal.js
  34. 101
    0
      src/renderer/ui/components/modals/GoToModal.js
  35. 65
    0
      src/renderer/ui/components/modals/OpenPortModal.js
  36. 33
    0
      src/renderer/ui/components/modals/ThemedDialog.js
  37. 55
    0
      src/renderer/ui/components/modals/UninstallV2Modal.js
  38. 69
    0
      src/renderer/ui/components/modals/UpdateModal.js
  39. 47
    0
      src/renderer/ui/components/modals/WelcomeNewVersionModal.js
  40. 2
    2
      src/renderer/ui/components/settings/tabs/GeneralTab.js
  41. 109
    0
      src/renderer/ui/pages/PlayerPage.js
  42. 3
    41
      src/renderer/ui/pages/SettingsPage.js
  43. 6
    2
      src/renderer/ui/utils/theme.js
  44. 11
    0
      src/renderer/windows/main.js
  45. 0
    0
      src/renderer/windows/settings/audioEQ.js
  46. 0
    0
      src/renderer/windows/settings/audioSelection.js
  47. 0
    0
      src/renderer/windows/settings/customStyle.js
  48. 10
    0
      src/renderer/windows/settings/index.js
  49. 0
    0
      src/renderer/windows/settings/lastFM.js

+ 1
- 2
.eslintrc View File

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

+ 0
- 8
src/assets/less/dialog_window.less View File

@@ -107,11 +107,3 @@
}
}
}

.windows-only {
display: none;

.win32 & {
display: block;
}
}

+ 25
- 31
src/assets/less/main_window.less View File

@@ -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;
}
}


+ 0
- 4
src/index.js View File

@@ -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
- 0
src/main/utils/Settings.js View File

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


+ 6
- 10
src/public_html/desktop_settings.html View File

@@ -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>

+ 2
- 176
src/public_html/index.html View File

@@ -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
- 0
src/renderer/GPMWebView/interface/customNavigation/goToURL.js View File

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

+ 1
- 0
src/renderer/GPMWebView/interface/customNavigation/index.js View File

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

+ 0
- 8
src/renderer/generic/core/APICode.js View File

@@ -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();
});

+ 0
- 27
src/renderer/generic/core/appDetails.js View File

@@ -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,
});
});
}
});

+ 0
- 40
src/renderer/generic/core/control-bar.js View File

@@ -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);
}
});
}

+ 0
- 19
src/renderer/generic/core/devtools.js View File

@@ -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();
};
});
}

+ 0
- 47
src/renderer/generic/core/goToURL.js View File

@@ -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());
});
});

+ 29
- 14
src/renderer/generic/core/index.js View File

@@ -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();
};
});
}

+ 0
- 84
src/renderer/generic/core/lyrics.js View File

@@ -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'));

+ 0
- 13
src/renderer/generic/core/onlineStatus.js View File

@@ -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();
});

+ 0
- 8
src/renderer/generic/core/openPort.js View File

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

+ 0
- 8
src/renderer/generic/core/uninstall.js View File

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

+ 0
- 16
src/renderer/generic/core/updateAvailable.js View File

@@ -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');
});
});

+ 0
- 49
src/renderer/generic/core/webviewLoader.js View File

@@ -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);
});
}

+ 0
- 46
src/renderer/generic/customWindowThemeHandler.js View File

@@ -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');

+ 0
- 2
src/renderer/generic/index.js View File

@@ -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);

+ 43
- 53
src/renderer/generic/windowThemeHandler.js View File

@@ -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');

+ 0
- 57
src/renderer/settings/index.js View File

@@ -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
- 0
src/renderer/ui/components/generic/LyricsViewer.js View File

@@ -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
- 0
src/renderer/ui/components/generic/OfflineWarning.js View File

@@ -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>
);
}
}

+ 6
- 6
src/renderer/ui/components/generic/SettingsProvider.js View File

@@ -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
- 0
src/renderer/ui/components/generic/WebView.js View File

@@ -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
- 0
src/renderer/ui/components/generic/WindowContainer.js View File

@@ -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
- 0
src/renderer/ui/components/modals/APICodeModal.js View File

@@ -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}
/>,
];
return (
<Dialog
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
>
<span id="APICodeContainer">
{this.state.code}
</span>
</Dialog>
);
}
}

+ 65
- 0
src/renderer/ui/components/modals/AboutModal.js View File

@@ -0,0 +1,65 @@
import { remote } from 'electron';
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';

const appVersion = remote.app.getVersion();
const appName = remote.app.getName();
const appInDevMode = remote.getGlobal('DEV_MODE') ? 'Running in Development Mode' : '';

export default class AboutModal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: false,
};
}

componentDidMount() {
Emitter.on('about', this.show);
}

componentWillUnmount() {
Emitter.off('about', this.show);
}

handleClose = () => {
this.setState({
open: false,
});
}

show = () => {
this.setState({
open: true,
});
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-text-close')}
onTouchTap={this.handleClose}
/>,
];
return (
<Dialog
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
modal={false}
>
<h4>
{TranslationProvider.query('label-about')} {appName}
</h4>
<p>
{TranslationProvider.query('label-version')}: {appVersion}
</p>
<p>
{appInDevMode}
</p>
</Dialog>
);
}
}

+ 58
- 0
src/renderer/ui/components/modals/ConfirmTrayModal.js View File

@@ -0,0 +1,58 @@
import { remote } from 'electron';
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';

export default class ConfirmTrayModal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: false,
};
}

handleClose = () => {
this.setState({
open: false,
});
Emitter.fire('window:close', remote.getCurrentWindow().id);
}

handleCloseAndNeverAgain = () => {
Settings.set('warnMinToTray', false);
this.handleClose();
}

show = () => {
this.setState({
open: true,
});
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-test-ok')}
primary
onTouchTap={this.handleClose}
/>,
<FlatButton
label={TranslationProvider.query('button-text-dont-tell-me-again')}
primary
keyboardFocused
onTouchTap={this.handleCloseAndNeverAgain}
/>,
];
return (
<Dialog
title={TranslationProvider.query('modal-confirmTray-title')}
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
>
{TranslationProvider.query('modal-confirmTray-content')}
</Dialog>
);
}
}

+ 101
- 0
src/renderer/ui/components/modals/GoToModal.js View File

@@ -0,0 +1,101 @@
import { remote } from 'electron';
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';
import TextField from 'material-ui/TextField';

export default class GoToModal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: false,
};
}

componentDidMount() {
Emitter.on('gotourl', this.show);
}

componentWillUnmount() {
Emitter.off('gotourl', this.show);
}

handleClose = () => {
this.setState({
open: false,
});
}

show = () => {
this.setState({
open: true,
});
findDOMNode(this.refs.input).querySelector('input').focus();
}

go = () => {
if (this.value) {
this.parseURL(this.value);
}
}

parseURL = (url) => {
if (url === 'DEV_MODE') {
const ok = confirm('You have instructed GPMDP to restart in Dev Mode.' + // eslint-disable-line
'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;

remote.app.relaunch({
args: remote.process.argv.slice(1).concat(['--dev']),
});
remote.app.quit();
} else if (url === 'DEBUG_INFO') {
Emitter.fire('generateDebugInfo');
}
if (!/https:\/\/play\.google\.com\/music\/listen/g.test(url)) return;
Emitter.fireAtGoogle('navigate:gotourl', url);
this.handleClose();
}

_onChange = (event, newValue) => {
this.value = newValue;
}

_onKeyUp = (event) => {
if (event.which === 13) {
this.go();
}
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-text-lets-go')}
primary
keyboardFocused
onTouchTap={this.go}
/>,
];
return (
<Dialog
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
modal={false}
>
<TextField
ref="input"
hintText={"E.g. https://play.google.com/music/listen"}
floatingLabelText={TranslationProvider.query('modal-goToURL-title')}
onKeyUp={this._onKeyUp}
onChange={this._onChange}
floatingLabelFixed
fullWidth
/>
</Dialog>
);
}
}

+ 65
- 0
src/renderer/ui/components/modals/OpenPortModal.js View File

@@ -0,0 +1,65 @@
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';

export default class OpenPortModal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: false,
};
}

componentDidMount() {
Emitter.on('openport:request', this.show);
}

componentWillUnmount() {
Emitter.off('openport:request', this.show);
}

handleClose = () => {
this.setState({
open: false,
});
}

show = () => {
this.setState({
open: true,
});
}

openNow = () => {
Emitter.fire('openport:confirm');
this.handleClose();
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-text-not-now')}
labelStyle={{ fontSize: 12 }}
style={{ height: 26, lineHeight: '26px', opacity: 0.7 }}
onTouchTap={this.handleClose}
/>,
<FlatButton
label={TranslationProvider.query('button-text-lets-do-this')}
primary
keyboardFocused
onTouchTap={this.openNow}
/>,
];
return (
<Dialog
title={TranslationProvider.query('modal-confirmTray-title')}
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
>
<div dangerouslySetInnerHTML={{ __html: TranslationProvider.query('modal-confirmOpenPort-content') }}></div>
</Dialog>
);
}
}

+ 33
- 0
src/renderer/ui/components/modals/ThemedDialog.js View File

@@ -0,0 +1,33 @@
import React, { Component, PropTypes } from 'react';
import Dialog from 'material-ui/Dialog';

export default class ThemedDialog extends Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string,
]).isRequired,
};

static contextTypes = {
muiTheme: PropTypes.object.isRequired,
};

render() {
return (
<Dialog
modal
{...this.props}
autoScrollBodyContent
actionsContainerStyle={{ backgroundColor: this.context.muiTheme.dialog.backgroundColor, borderTop: 0 }}
bodyStyle={{ backgroundColor: this.context.muiTheme.dialog.backgroundColor }}
titleStyle={{ backgroundColor: this.context.muiTheme.dialog.backgroundColor }}
>
{
this.props.children
}
</Dialog>
);
}
}

+ 55
- 0
src/renderer/ui/components/modals/UninstallV2Modal.js View File

@@ -0,0 +1,55 @@
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';

export default class UninstallV2Modal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: false,
};
}

componentDidMount() {
Emitter.on('uninstall:request', this.show);
}

componentWillUnmount() {
Emitter.off('uninstall:request', this.show);
}

handleClose = () => {
this.setState({
open: false,
});
Emitter.fire('uninstall:confirm');
}

show = () => {
this.setState({
open: true,
});
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-test-ok')}
primary
keyboardFocused
onTouchTap={this.handleClose}
/>,
];
return (
<Dialog
title={TranslationProvider.query('modal-confirmUninstall-title')}
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
>
<div dangerouslySetInnerHTML={{ __html: TranslationProvider.query('modal-confirmUninstall-content') }}></div>
</Dialog>
);
}
}

+ 69
- 0
src/renderer/ui/components/modals/UpdateModal.js View File

@@ -0,0 +1,69 @@
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';

export default class UpdateModal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: false,
};
}

componentDidMount() {
Emitter.on('update:available', this.show);
}

componentWillUnmount() {
Emitter.off('update:available', this.show);
}

handleClose = () => {
this.setState({
open: false,
});
}

show = () => {
this.setState({
open: true,
});
}

updateNow = () => {
Emitter.fire('update:trigger');
}

updateLater = () => {
Emitter.fire('update:wait');
this.handleClose();
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-text-not-now')}
labelStyle={{ fontSize: 12 }}
style={{ height: 26, lineHeight: '26px', opacity: 0.7 }}
onTouchTap={this.updateLater}
/>,
<FlatButton
label={TranslationProvider.query('button-text-lets-do-this')}
primary
keyboardFocused
onTouchTap={this.updateNow}
/>,
];
return (
<Dialog
title={TranslationProvider.query('modal-confirmUpdate-title')}
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
>
<div dangerouslySetInnerHTML={{ __html: TranslationProvider.query('modal-confirmUpdate-content') }}></div>
</Dialog>
);
}
}

+ 47
- 0
src/renderer/ui/components/modals/WelcomeNewVersionModal.js View File

@@ -0,0 +1,47 @@
import { remote } from 'electron';
import React, { Component } from 'react';
import Dialog from './ThemedDialog';
import FlatButton from 'material-ui/FlatButton';
import fs from 'fs';
import path from 'path';

const appVersion = remote.app.getVersion();
const changeLog = fs.readFileSync(path.resolve(`${__dirname}/../../../../../MR_CHANGELOG.html`), 'utf8');

export default class WelcomeNewVersionModal extends Component {
constructor(...args) {
super(...args);

this.state = {
open: Settings.get('welcomed') !== appVersion,
};
}

handleClose = () => {
this.setState({
open: false,
});
Settings.set('welcomed', appVersion);
}

render() {
const actions = [
<FlatButton
label={TranslationProvider.query('button-text-lets-go')}
primary
keyboardFocused
onTouchTap={this.handleClose}
/>,
];
return (
<Dialog
title={`${TranslationProvider.query('modal-welcome-title')} ${appVersion}`}
actions={actions}
open={this.state.open}
onRequestClose={this.handleClose}
>
<div dangerouslySetInnerHTML={{ __html: changeLog }}></div>
</Dialog>
);
}
}

+ 2
- 2
src/renderer/ui/components/settings/tabs/GeneralTab.js View File

@@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { requireSettings } from '../SettingsProvider';
import { requireSettings } from '../../generic/SettingsProvider';

import PlatformSpecific from '../PlatformSpecific';
import PlatformSpecific from '../../generic/PlatformSpecific';
import SettingsTabWrapper from './SettingsTabWrapper';
import ThemeOptions from '../ThemeOptions';
import ToggleableOption from '../ToggleableOption';

+ 109
- 0
src/renderer/ui/pages/PlayerPage.js View File

@@ -0,0 +1,109 @@
import { remote } from 'electron';
import React, { Component } from 'react';

import LyricsViewer from '../components/generic/LyricsViewer';
import OfflineWarning from '../components/generic/OfflineWarning';
import WebView from '../components/generic/WebView';
import WindowContainer from '../components/generic/WindowContainer';

// Modals
import AboutModal from '../components/modals/AboutModal';
import APICodeModal from '../components/modals/APICodeModal';
import ConfirmTrayModal from '../components/modals/ConfirmTrayModal';
import GoToModal from '../components/modals/GoToModal';
import OpenPortModal from '../components/modals/OpenPortModal';
import UpdateModal from '../components/modals/UpdateModal';
import UninstallV2Modal from '../components/modals/UninstallV2Modal';
import WelcomeNewVersionModal from '../components/modals/WelcomeNewVersionModal';

export default class PlayerPage extends Component {
constructor(...args) {
super(...args);

this.once = true;
this.targetPage = Settings.get('savePage', true) ?
Settings.get('lastPage', 'https://play.google.com/music/listen')
: 'https://play.google.com/music/listen';
this.ready = false;
this.state = {
webviewTarget: 'https://play.google.com/music/listen',
};
}

_confirmCloseWindow = () => {
this.refs.trayModal.show();
}

_didStopLoading = () => {
if (this.once) {
this.once = false;
this.refs.view.executeJavaScript(`window.location = "${this.targetPage}"`);
setTimeout(() => {
document.body.removeAttribute('loading');
}, 300);
}
}

_domReady = () => {
setTimeout(() => {
this.refs.view.focus();
this.ready = true;

const focusWebview = () => {
document.querySelector('webview::shadow object').focus();
};
window.addEventListener('beforeunload', () => {
remote.getCurrentWindow().removeListener('focus', focusWebview);
});
remote.getCurrentWindow().on('focus', focusWebview);
}, 700);
};

_didNavigate = (...args) => {
if (this.ready) this._savePage(...args);
}

_didNavigateInPage = (...args) => {
if (this.ready) this._savePage(...args);
}

_savePage = (param) => {
const url = param.url || param;
if (!/https?:\/\/play\.google\.com\/music/g.test(url)) return;
Settings.set('lastPage', url);
}

render() {
return (
<WindowContainer isMainWindow title="" confirmClose={this._confirmCloseWindow}>
<div className="drag-handle-large"></div>
<div className="loader">
<svg className="circular" viewBox="25 25 50 50">
<circle className="path" cx="50" cy="50" r="20" fill="none" strokeWidth="2" strokeMiterlimit="10" />
</svg>
</div>
<WebView
ref="view"
src={this.state.webviewTarget}
className="embedded-player"
preload="../renderer/GPMWebView"
didStopLoading={this._didStopLoading}
domReady={this._domReady}
didNavigate={this._didNavigate}
didNavigateInPage={this._didNavigateInPage}
/>