Add initial files

This commit is contained in:
jeffvli
2022-12-08 00:38:12 -08:00
commit 4d64a96f75
239 changed files with 45979 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
import './player';
import './media-keys';
import './settings';
@@ -0,0 +1,27 @@
import type { BrowserWindow } from 'electron';
import { globalShortcut } from 'electron';
export const enableMediaKeys = (window: BrowserWindow | null) => {
globalShortcut.register('MediaStop', () => {
window?.webContents.send('renderer-player-stop');
});
globalShortcut.register('MediaPlayPause', () => {
window?.webContents.send('renderer-player-play-pause');
});
globalShortcut.register('MediaNextTrack', () => {
window?.webContents.send('renderer-player-next');
});
globalShortcut.register('MediaPreviousTrack', () => {
window?.webContents.send('renderer-player-previous');
});
};
export const disableMediaKeys = () => {
globalShortcut.unregister('MediaStop');
globalShortcut.unregister('MediaPlayPause');
globalShortcut.unregister('MediaNextTrack');
globalShortcut.unregister('MediaPreviousTrack');
};
@@ -0,0 +1,167 @@
import { ipcMain } from 'electron';
import MpvAPI from 'node-mpv';
import { store } from '../settings';
import uniq from 'lodash/uniq';
import type { PlayerData } from '../../../../../renderer/src/store/player.store';
import { getBrowserWindow } from '/@/mainWindow';
declare module 'node-mpv';
const BINARY_PATH = store.get('mpv_path') as string | undefined;
const MPV_PARAMETERS = store.get('mpv_parameters') as Array<string> | undefined;
const DEFAULT_MPV_PARAMETERS = () => {
const parameters = [];
if (
!MPV_PARAMETERS?.includes('--gapless-audio=weak') ||
!MPV_PARAMETERS?.includes('--gapless-audio=no') ||
!MPV_PARAMETERS?.includes('--gapless-audio=yes') ||
!MPV_PARAMETERS?.includes('--gapless-audio')
) {
parameters.push('--gapless-audio=yes');
}
if (
!MPV_PARAMETERS?.includes('--prefetch-playlist=no') ||
!MPV_PARAMETERS?.includes('--prefetch-playlist=yes') ||
!MPV_PARAMETERS?.includes('--prefetch-playlist')
) {
parameters.push('--prefetch-playlist=yes');
}
return parameters;
};
const mpv = new MpvAPI(
{
audio_only: true,
auto_restart: true,
binary: BINARY_PATH || '',
time_update: 1,
},
MPV_PARAMETERS
? uniq([...DEFAULT_MPV_PARAMETERS(), ...MPV_PARAMETERS])
: DEFAULT_MPV_PARAMETERS(),
);
mpv
.start()
.then(async () => {
// await mpv.load('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', 'replace');
// await mpv.play();
})
.catch((error) => {
console.log('error', error);
});
mpv.on('status', (status) => {
if (status.property === 'playlist-pos') {
if (status.value !== 0) {
getBrowserWindow()?.webContents.send('renderer-player-auto-next');
}
}
});
// Automatically updates the play button when the player is playing
mpv.on('resumed', () => {
getBrowserWindow()?.webContents.send('renderer-player-play');
});
// Automatically updates the play button when the player is stopped
mpv.on('stopped', () => {
getBrowserWindow()?.webContents.send('renderer-player-stop');
});
// Automatically updates the play button when the player is paused
mpv.on('paused', () => {
getBrowserWindow()?.webContents.send('renderer-player-pause');
});
mpv.on('quit', () => {
console.log('mpv quit');
});
// Event output every interval set by time_update, used to update the current time
mpv.on('timeposition', (time: number) => {
getBrowserWindow()?.webContents.send('renderer-player-current-time', time);
});
// Starts the player
ipcMain.on('player-play', async () => {
await mpv.play();
});
// Pauses the player
ipcMain.on('player-pause', async () => {
await mpv.pause();
});
// Stops the player
ipcMain.on('player-stop', async () => {
await mpv.stop();
});
// Goes to the next track in the playlist
ipcMain.on('player-next', async () => {
await mpv.next();
});
// Goes to the previous track in the playlist
ipcMain.on('player-previous', async () => {
await mpv.prev();
});
// Seeks forward or backward by the given amount of seconds
ipcMain.on('player-seek', async (_event, time: number) => {
await mpv.seek(time);
});
// Seeks to the given time in seconds
ipcMain.on('player-seek-to', async (_event, time: number) => {
await mpv.goToPosition(time);
});
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
ipcMain.on('player-set-queue', async (_event, data: PlayerData) => {
if (data.queue.current) {
await mpv.load(data.queue.current.streamUrl, 'replace');
}
if (data.queue.next) {
await mpv.load(data.queue.next.streamUrl, 'append');
}
});
// Replaces the queue in position 1 to the given data
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
const size = await mpv.getPlaylistSize();
if (size > 1) {
await mpv.playlistRemove(1);
}
if (data.queue.next) {
await mpv.load(data.queue.next.streamUrl, 'append');
}
});
// Sets the next song in the queue when reaching the end of the queue
ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
// Always keep the current song as position 0 in the mpv queue
// This allows us to easily set update the next song in the queue without
// disturbing the currently playing song
await mpv.playlistRemove(0);
if (data.queue.next) {
await mpv.load(data.queue.next.streamUrl, 'append');
}
});
// Sets the volume to the given value (0-100)
ipcMain.on('player-volume', async (_event, value: number) => {
mpv.volume(value);
});
// Toggles the mute status
ipcMain.on('player-mute', async () => {
mpv.mute();
});
@@ -0,0 +1,3 @@
import Store from 'electron-store';
export const store = new Store();
+3
View File
@@ -0,0 +1,3 @@
import './core';
// require(`./${process.platform}`);
+71
View File
@@ -0,0 +1,71 @@
import { app, ipcMain } from 'electron';
import './security-restrictions';
import { restoreOrCreateWindow } from '/@/mainWindow';
ipcMain.on('app-restart', () => {
app.relaunch();
app.exit(0);
});
/**
* Prevent electron from running multiple instances.
*/
const isSingleInstance = app.requestSingleInstanceLock();
if (!isSingleInstance) {
app.quit();
process.exit(0);
}
app.on('second-instance', restoreOrCreateWindow);
/**
* Disable Hardware Acceleration to save more system resources.
*/
app.disableHardwareAcceleration();
/**
* Shout down background process if all windows was closed
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
/**
* @see https://www.electronjs.org/docs/latest/api/app#event-activate-macos Event: 'activate'.
*/
app.on('activate', restoreOrCreateWindow);
/**
* Create the application window when the background process is ready.
*/
app
.whenReady()
.then(restoreOrCreateWindow)
.catch((e) => console.error('Failed create window:', e));
/**
* Install Vue.js or any other extension in development mode only.
* Note: You must install `electron-devtools-installer` manually
*/
// if (import.meta.env.DEV) {
// app.whenReady()
// .then(() => import('electron-devtools-installer'))
// .then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
// loadExtensionOptions: {
// allowFileAccess: true,
// },
// }))
// .catch(e => console.error('Failed install extension:', e));
// }
/**
* Check for new version of the application - production mode only.
*/
if (import.meta.env.PROD) {
app
.whenReady()
.then(() => import('electron-updater'))
.then(({ autoUpdater }) => autoUpdater.checkForUpdatesAndNotify())
.catch((e) => console.error('Failed check updates:', e));
}
+137
View File
@@ -0,0 +1,137 @@
import { app, BrowserWindow, ipcMain } from 'electron';
import { join } from 'path';
import { URL } from 'url';
import { disableMediaKeys, enableMediaKeys } from './features/core/media-keys';
import { store } from './features/core/settings';
import electronLocalShortcut from 'electron-localshortcut';
import './features';
const isDevelopment = process.env.NODE_ENV === 'development';
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
return installer
.default(
extensions.map((name) => installer[name]),
forceDownload,
)
.catch(console.log);
};
let browserWindow: BrowserWindow | null = null;
async function createWindow() {
if (isDevelopment) {
await installExtensions();
}
browserWindow = new BrowserWindow({
show: false, // Use the 'ready-to-show' event to show the instantiated BrowserWindow.
frame: false,
minWidth: 640,
minHeight: 600,
height: 900,
width: 1440,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false, // Sandbox disabled because the demo of preload script depend on the Node.js api
webviewTag: false, // The webview tag is not recommended. Consider alternatives like an iframe or Electron's BrowserView. @see https://www.electronjs.org/docs/latest/api/webview-tag#warning
preload: join(app.getAppPath(), 'packages/preload/dist/index.cjs'),
backgroundThrottling: false,
},
});
/**
* If the 'show' property of the BrowserWindow's constructor is omitted from the initialization options,
* it then defaults to 'true'. This can cause flickering as the window loads the html content,
* and it also has show problematic behaviour with the closing of the window.
* Use `show: false` and listen to the `ready-to-show` event to show the window.
*
* @see https://github.com/electron/electron/issues/25012 for the afford mentioned issue.
*/
browserWindow.on('ready-to-show', () => {
browserWindow?.show();
if (import.meta.env.DEV) {
browserWindow?.webContents.openDevTools();
}
});
ipcMain.on('window-maximize', () => {
browserWindow?.maximize();
});
ipcMain.on('window-unmaximize', () => {
browserWindow?.unmaximize();
});
ipcMain.on('window-minimize', () => {
browserWindow?.minimize();
});
ipcMain.on('window-close', () => {
browserWindow?.close();
});
electronLocalShortcut.register(browserWindow, 'Ctrl+Shift+I', () => {
browserWindow?.webContents.openDevTools();
});
const globalMediaKeysEnabled = store.get('global_media_hotkeys') as boolean;
if (globalMediaKeysEnabled) {
enableMediaKeys(browserWindow);
}
ipcMain.on('global-media-keys-enable', () => {
enableMediaKeys(browserWindow);
});
ipcMain.on('global-media-keys-disable', () => {
disableMediaKeys();
});
/**
* URL for main window.
* Vite dev server for development.
* `file://../renderer/index.html` for production and test.
*/
const pageUrl =
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined
? import.meta.env.VITE_DEV_SERVER_URL
: new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
await browserWindow.loadURL(pageUrl);
return browserWindow;
}
/**
* Restore an existing BrowserWindow or Create a new BrowserWindow.
*/
export async function restoreOrCreateWindow() {
let window = BrowserWindow.getAllWindows().find((w) => !w.isDestroyed());
if (window === undefined) {
window = await createWindow();
}
if (window.isMinimized()) {
window.restore();
}
window.focus();
}
ipcMain.on('app-restart', () => {
app.relaunch();
app.exit(0);
});
export const getBrowserWindow = () => {
return browserWindow;
};
+135
View File
@@ -0,0 +1,135 @@
import {app, shell} from 'electron';
import {URL} from 'url';
type Permissions =
| 'clipboard-read'
| 'media'
| 'display-capture'
| 'mediaKeySystem'
| 'geolocation'
| 'notifications'
| 'midi'
| 'midiSysex'
| 'pointerLock'
| 'fullscreen'
| 'openExternal'
| 'unknown';
/**
* A list of origins that you allow open INSIDE the application and permissions for them.
*
* In development mode you need allow open `VITE_DEV_SERVER_URL`.
*/
const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map<string, Set<Permissions>>(
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL
? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set()]]
: [],
);
/**
* A list of origins that you allow open IN BROWSER.
* Navigation to the origins below is only possible if the link opens in a new window.
*
* @example
* <a
* target="_blank"
* href="https://github.com/"
* >
*/
const ALLOWED_EXTERNAL_ORIGINS = new Set<`https://${string}`>(['https://github.com']);
app.on('web-contents-created', (_, contents) => {
/**
* Block navigation to origins not on the allowlist.
*
* Navigation exploits are quite common. If an attacker can convince the app to navigate away from its current page,
* they can possibly force the app to open arbitrary web resources/websites on the web.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
*/
contents.on('will-navigate', (event, url) => {
const {origin} = new URL(url);
if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
return;
}
// Prevent navigation
event.preventDefault();
if (import.meta.env.DEV) {
console.warn(`Blocked navigating to disallowed origin: ${origin}`);
}
});
/**
* Block requests for disallowed permissions.
* By default, Electron will automatically approve all permission requests.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
*/
contents.session.setPermissionRequestHandler((webContents, permission, callback) => {
const {origin} = new URL(webContents.getURL());
const permissionGranted = !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission);
callback(permissionGranted);
if (!permissionGranted && import.meta.env.DEV) {
console.warn(`${origin} requested permission for '${permission}', but was rejected.`);
}
});
/**
* Hyperlinks leading to allowed sites are opened in the default browser.
*
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
* You should deny any unexpected window creation.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
*/
contents.setWindowOpenHandler(({url}) => {
const {origin} = new URL(url);
// @ts-expect-error Type checking is performed in runtime.
if (ALLOWED_EXTERNAL_ORIGINS.has(origin)) {
// Open url in default browser.
shell.openExternal(url).catch(console.error);
} else if (import.meta.env.DEV) {
console.warn(`Blocked the opening of a disallowed origin: ${origin}`);
}
// Prevent creating a new window.
return {action: 'deny'};
});
/**
* Verify webview options before creation.
*
* Strip away preload scripts, disable Node.js integration, and ensure origins are on the allowlist.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
*/
contents.on('will-attach-webview', (event, webPreferences, params) => {
const {origin} = new URL(params.src);
if (!ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
if (import.meta.env.DEV) {
console.warn(`A webview tried to attach ${params.src}, but was blocked.`);
}
event.preventDefault();
return;
}
// Strip away preload scripts if unused or verify their location is legitimate.
delete webPreferences.preload;
// @ts-expect-error `preloadURL` exists. - @see https://www.electronjs.org/docs/latest/api/web-contents#event-will-attach-webview
delete webPreferences.preloadURL;
// Disable Node.js integration
webPreferences.nodeIntegration = false;
// Enable contextIsolation
webPreferences.contextIsolation = true;
});
});