Skip to content

Commit 5b78151

Browse files
committed
Back to Basics - Build with electron-builder
- removed Webpack (and related deps) - changed to NPM - very simple file structure (+ logical naming) - only 3 run/build scripts - easy - window remembers position on reuse - Fixed: "Hide Others" - Fix: All "View" menu
1 parent 33ec59a commit 5b78151

14 files changed

+3314
-8277
lines changed

Diff for: README.md

+5-9
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,22 @@ Mixcloud Play is the missing desktop experience for [Mixcloud.com](https://www.m
1515
## Getting started
1616
## Building
1717
```sh
18-
yarn dist
18+
npm install
19+
npm build
1920
```
2021

2122
### Development
22-
Download "Electron.app" into the project root.
2323

2424
```sh
25-
yarn
26-
yarn build:local
27-
Electron.app/Contents/MacOS/Electron .
25+
npm start
2826
```
2927

30-
Use the compile macOS .app with Dev Tools and some extra debugging enabled.
28+
Use the compile macOS .app with Dev Tools and some extra debugging enabled:
3129

3230
```sh
33-
yarn
34-
yarn dist:debug
31+
npm build:debug
3532
```
3633

37-
3834
### Docker Compose
3935
```sh
4036
docker-compose build

Diff for: browser.css

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
::selection {
2+
background-color: transparent;
3+
}
4+
.ad-header-wrapper,
5+
.site-wrapper div:first-child {
6+
-webkit-app-region: drag;
7+
}

Diff for: browser.js

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
const { ipcRenderer, webFrame, shell } = require('electron');
2+
3+
const webview = document;
4+
const BASE_URL = 'https://www.mixcloud.com';
5+
const DEBUG = process.env.ELECTRON_DEBUG || false;
6+
7+
if (DEBUG)
8+
console.info(JSON.stringify(process.env, null, 4));
9+
10+
function concatEndpoints(endpoints) {
11+
for (const i in endpoints) {
12+
endpoints[i] = BASE_URL + endpoints[i];
13+
}
14+
15+
return endpoints;
16+
}
17+
18+
const Endpoints = concatEndpoints({
19+
DASHBOARD: '/dashboard/',
20+
NEWSHOWS: '/dashboard/new-uploads/'
21+
});
22+
23+
// webview.getSettings().setMediaPlaybackRequiresUserGesture(false); // to allow autoplay set by website
24+
25+
webview.addEventListener('permissionrequest', ({ e }) => {
26+
console.log('permission requested');
27+
28+
if (e.permission === 'media') {
29+
e.request.allow();
30+
}
31+
});
32+
33+
function didFinishLoad() {
34+
console.log('didFinishLoad');
35+
webview.removeEventListener('did-finish-load', didFinishLoad);
36+
webview.send('init');
37+
}
38+
webview.addEventListener('did-finish-load', didFinishLoad);
39+
40+
// Set Window Title
41+
webview.addEventListener('page-title-updated', ({ title }) => {
42+
webview.title = `${title} | Mixcloud Play`;
43+
});
44+
45+
ipcRenderer.on('goToDashboard', () => {
46+
console.log('ipcRenderer: goToDashboard');
47+
webview.location = Endpoints.DASHBOARD;
48+
});
49+
50+
ipcRenderer.on('goToNewShows', () => {
51+
console.log('ipcRenderer: goToNewShows');
52+
webview.location = Endpoints.NEWSHOWS;
53+
});
54+
55+
ipcRenderer.on('notificationClicked', (_, notificationIndex) => {
56+
webview.send('notificationClicked', notificationIndex);
57+
});
58+
59+
// MIXCLOUD
60+
ipcRenderer.on('playPause', () => {
61+
console.log('ipcRenderer: playPause');
62+
webview.send('playPause');
63+
});
64+
ipcRenderer.on('next', () => {
65+
console.log('ipcRenderer: next');
66+
webview.send('next');
67+
});
68+
69+
// Open all links in external browser
70+
webview.addEventListener('click', function(event) {
71+
if (event.target.href) {
72+
console.log(event.target.href);
73+
}
74+
if (event.target.tagName === 'A' && event.target.href.startsWith('http') && !event.target.href.includes('https://www.mixcloud.com/')) {
75+
event.preventDefault();
76+
shell.openExternal(event.target.href);
77+
}
78+
});
79+
80+
if (DEBUG) {
81+
webview.addEventListener('dom-ready', () => {
82+
webview.openDevTools();
83+
});
84+
// webview.addEventListener('console-message', (e) => {
85+
// console.log('Guest page logged a message:', e.message)
86+
// });
87+
}
88+
89+
// #region Notification
90+
const notifications = [];
91+
const NotificationOriginal = Notification;
92+
function NotificationDecorated(title) {
93+
const notification = {
94+
_handleClick: [],
95+
close() {},
96+
addEventListener(type, callback) {
97+
if (type !== 'click') return;
98+
99+
this._handleClick.push(callback);
100+
},
101+
click() {
102+
for (const callback of this._handleClick) {
103+
callback.call(this);
104+
}
105+
}
106+
}
107+
108+
ipcRenderer.send('notification', notifications.push(notification) - 1, title);
109+
return notification;
110+
}
111+
112+
Object.defineProperties(NotificationDecorated, {
113+
permission: {
114+
get() { return NotificationOriginal.permission }
115+
},
116+
maxActions: {
117+
get() { return NotificationOriginal.maxActions }
118+
},
119+
requestPermission: {
120+
get() { return NotificationOriginal.requestPermission }
121+
}
122+
});
123+
124+
window.Notification = NotificationDecorated;
125+
// #endregion
126+
127+
// #region Custom notification sound
128+
webFrame.registerURLSchemeAsBypassingCSP('file');
129+
130+
const AudioOriginal = Audio;
131+
const beaconNotificationRegex = /beacon-notification\.(?:.*)$/;
132+
133+
function createObserverCallback(tagName, callback) {
134+
return function(records) {
135+
for (const record of records) {
136+
for (const node of record.addedNodes) {
137+
if (node.tagName === tagName) {
138+
callback();
139+
}
140+
}
141+
}
142+
}
143+
}
144+
145+
ipcRenderer.on('notificationClicked', (_, notificationIndex) => {
146+
const originalOpen = window.open;
147+
window.open = (url) => {
148+
window.location = url;
149+
}
150+
notifications[notificationIndex].click();
151+
window.open = originalOpen;
152+
});
153+
154+
ipcRenderer.on('playPause', () => {
155+
console.log('playPause');
156+
const playPause = document.querySelector('[class*=PlayButton__PlayerControl]');
157+
if (playPause)
158+
playPause.click();
159+
});
160+
161+
ipcRenderer.on('next', () => {
162+
// TODO
163+
console.log('next');
164+
const row = Array.from(document.getElementsByClassName('cloudcast-upnext-row'));
165+
const nowPlaying = row.findIndex(song => song.classList.contains('now-playing'));
166+
const children = Array.from(row[nowPlaying + 1].childNodes);
167+
const image = children.find(child => child.classList.contains('cloudcast-row-image'));
168+
if (image)
169+
image.click();
170+
});
171+
172+
// ipcRenderer.on('init', () => {
173+
webview.addEventListener('DOMContentLoaded', () => {
174+
let currentArtist = '';
175+
let currentTrack = '';
176+
177+
setInterval(() => {
178+
console.log('currentTrack', currentTrack, 'currentArtist', currentArtist);
179+
180+
const titleElement = webview.querySelector('[class*=RebrandPlayerSliderComponent__Artist]');
181+
if (!titleElement) return;
182+
183+
const title = titleElement.innerText;
184+
if (title !== currentArtist) {
185+
currentArtist = String(title);
186+
console.log('New Artist', currentArtist);
187+
ipcRenderer.send('handlePlay', currentArtist);
188+
}
189+
190+
let trackElement = webview.querySelector('[class*=RebrandPlayerSliderComponent__Track-]');
191+
if (!trackElement) return;
192+
193+
let track = webview.querySelector('[class*=RebrandPlayerSliderComponent__Track-]').innerText;
194+
if (track !== currentTrack) {
195+
let trackTruncated = track.replace(/[\u2014\u002d]\sbuy$/gi, '');
196+
trackTruncated = trackTruncated.replace('by ', '');
197+
198+
currentTrack = trackTruncated;
199+
console.log('New Track', currentTrack);
200+
201+
let notificationSubtitle = 'Mixcloud Play';
202+
203+
ipcRenderer.send('nowPlaying', currentTrack, title, notificationSubtitle);
204+
}
205+
}, 2000);
206+
});
207+
208+
//Elements
209+
webview.addEventListener('click', function(event) {
210+
const playPause = webview.querySelector('[class*=PlayButton__PlayerControl]');
211+
const eventPath = event.path || (event.composedPath && event.composedPath()) || [];
212+
const playPauseClicked = eventPath.find(path => path === playPause);
213+
if (playPauseClicked) {
214+
console.log(playPauseClicked);
215+
const paused = playPauseClicked.classList.contains('dvjoTG');
216+
console.log(paused);
217+
let trackElement = webview.querySelector('[class*=RebrandPlayerControls__ShowTitle]');
218+
if (!trackElement) return;
219+
220+
let track = trackElement.innerText;
221+
console.log(track);
222+
ipcRenderer.send(paused ? 'handlePause' : 'handlePlay', track);
223+
}
224+
console.log(playPauseClicked);
225+
});

Diff for: docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ services:
1111
ELECTRON_CACHE: "/root/.cache/electron"
1212
ELECTRON_BUILDER_CACHE: "/root/.cache/electron-builder"
1313
working_dir: /project
14-
command: /bin/bash -c "yarn && yarn dist"
14+
command: /bin/bash -c "npm install && npm build:debug" # build on a macOS host for dist

0 commit comments

Comments
 (0)