mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-09 10:42:50 +00:00
Implemented main window with QR code for desktop receiver.
This commit is contained in:
parent
85530ca218
commit
cefe19e9ff
18 changed files with 501 additions and 35 deletions
|
@ -1,12 +1,15 @@
|
|||
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog } from 'electron';
|
||||
import path = require('path');
|
||||
import { TcpListenerService } from './TcpListenerService';
|
||||
import { PlaybackUpdateMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { PlaybackUpdateMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { DiscoveryService } from './DiscoveryService';
|
||||
import { Updater } from './Updater';
|
||||
import { WebSocketListenerService } from './WebSocketListenerService';
|
||||
import * as os from 'os';
|
||||
|
||||
export default class Main {
|
||||
static shouldOpenMainWindow = true;
|
||||
static playerWindow: Electron.BrowserWindow;
|
||||
static mainWindow: Electron.BrowserWindow;
|
||||
static application: Electron.App;
|
||||
static tcpListenerService: TcpListenerService;
|
||||
|
@ -19,6 +22,10 @@ export default class Main {
|
|||
const trayicon = nativeImage.createFromPath(icon)
|
||||
const tray = new Tray(trayicon.resize({ width: 16 }));
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Open window',
|
||||
click: () => Main.openMainWindow()
|
||||
},
|
||||
{
|
||||
label: 'Check for updates',
|
||||
click: async () => {
|
||||
|
@ -83,10 +90,6 @@ export default class Main {
|
|||
tray.setContextMenu(contextMenu);
|
||||
this.tray = tray;
|
||||
}
|
||||
|
||||
private static onClose() {
|
||||
Main.mainWindow = null;
|
||||
}
|
||||
|
||||
private static onReady() {
|
||||
Main.createTray();
|
||||
|
@ -100,38 +103,40 @@ export default class Main {
|
|||
|
||||
listeners.forEach(l => {
|
||||
l.emitter.on("play", (message) => {
|
||||
if (Main.mainWindow == null) {
|
||||
Main.mainWindow = new BrowserWindow({
|
||||
if (Main.playerWindow == null) {
|
||||
Main.playerWindow = new BrowserWindow({
|
||||
fullscreen: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
preload: path.join(__dirname, 'player/preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
Main.mainWindow.setAlwaysOnTop(false, 'pop-up-menu');
|
||||
Main.mainWindow.show();
|
||||
Main.playerWindow.setAlwaysOnTop(false, 'pop-up-menu');
|
||||
Main.playerWindow.show();
|
||||
|
||||
Main.mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
Main.mainWindow.on('ready-to-show', () => {
|
||||
Main.mainWindow?.webContents?.send("play", message);
|
||||
Main.playerWindow.loadFile(path.join(__dirname, 'player/index.html'));
|
||||
Main.playerWindow.on('ready-to-show', () => {
|
||||
Main.playerWindow?.webContents?.send("play", message);
|
||||
});
|
||||
Main.playerWindow.on('closed', () => {
|
||||
Main.playerWindow = null;
|
||||
});
|
||||
Main.mainWindow.on('closed', Main.onClose);
|
||||
} else {
|
||||
Main.mainWindow?.webContents?.send("play", message);
|
||||
Main.playerWindow?.webContents?.send("play", message);
|
||||
}
|
||||
});
|
||||
|
||||
l.emitter.on("pause", () => Main.mainWindow?.webContents?.send("pause"));
|
||||
l.emitter.on("resume", () => Main.mainWindow?.webContents?.send("resume"));
|
||||
l.emitter.on("pause", () => Main.playerWindow?.webContents?.send("pause"));
|
||||
l.emitter.on("resume", () => Main.playerWindow?.webContents?.send("resume"));
|
||||
|
||||
l.emitter.on("stop", () => {
|
||||
Main.mainWindow.close();
|
||||
Main.mainWindow = null;
|
||||
Main.playerWindow.close();
|
||||
Main.playerWindow = null;
|
||||
});
|
||||
|
||||
l.emitter.on("seek", (message) => Main.mainWindow?.webContents?.send("seek", message));
|
||||
l.emitter.on("setvolume", (message) => Main.mainWindow?.webContents?.send("setvolume", message));
|
||||
l.emitter.on("seek", (message) => Main.playerWindow?.webContents?.send("seek", message));
|
||||
l.emitter.on("setvolume", (message) => Main.playerWindow?.webContents?.send("setvolume", message));
|
||||
l.start();
|
||||
|
||||
ipcMain.on('send-playback-update', (event: IpcMainEvent, value: PlaybackUpdateMessage) => {
|
||||
|
@ -144,7 +149,7 @@ export default class Main {
|
|||
});
|
||||
|
||||
ipcMain.on('toggle-full-screen', () => {
|
||||
const window = Main.mainWindow;
|
||||
const window = Main.playerWindow;
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
@ -153,17 +158,70 @@ export default class Main {
|
|||
});
|
||||
|
||||
ipcMain.on('exit-full-screen', () => {
|
||||
const window = Main.mainWindow;
|
||||
const window = Main.playerWindow;
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.setFullScreen(false);
|
||||
});
|
||||
|
||||
if (Main.shouldOpenMainWindow) {
|
||||
Main.openMainWindow();
|
||||
}
|
||||
}
|
||||
|
||||
static getAllIPv4Addresses() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const ipv4Addresses: string[] = [];
|
||||
|
||||
for (const interfaceName in interfaces) {
|
||||
const addresses = interfaces[interfaceName];
|
||||
if (!addresses) continue;
|
||||
|
||||
for (const addressInfo of addresses) {
|
||||
if (addressInfo.family === 'IPv4' && !addressInfo.internal) {
|
||||
ipv4Addresses.push(addressInfo.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ipv4Addresses;
|
||||
}
|
||||
|
||||
static openMainWindow() {
|
||||
if (Main.mainWindow) {
|
||||
Main.mainWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
Main.mainWindow = new BrowserWindow({
|
||||
fullscreen: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'main/preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
Main.mainWindow.loadFile(path.join(__dirname, 'main/index.html'));
|
||||
Main.mainWindow.on('closed', () => {
|
||||
Main.mainWindow = null;
|
||||
});
|
||||
|
||||
Main.mainWindow.show();
|
||||
|
||||
Main.mainWindow.on('ready-to-show', () => {
|
||||
Main.mainWindow.webContents.send("device-info", {name: os.hostname(), addresses: Main.getAllIPv4Addresses()});
|
||||
});
|
||||
}
|
||||
|
||||
static main(app: Electron.App) {
|
||||
Main.application = app;
|
||||
const argv = process.argv;
|
||||
if (argv.includes('--no-main-window')) {
|
||||
Main.shouldOpenMainWindow = false;
|
||||
}
|
||||
|
||||
Main.application.on('ready', Main.onReady);
|
||||
Main.application.on('window-all-closed', () => { });
|
||||
}
|
||||
|
|
BIN
receivers/electron/src/main/c.mp4
Normal file
BIN
receivers/electron/src/main/c.mp4
Normal file
Binary file not shown.
38
receivers/electron/src/main/index.html
Normal file
38
receivers/electron/src/main/index.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="./video-js.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<title>FCast Receiver</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<video id="video-player" class="video-js" controls preload="auto" data-setup='{}' style="object-fit: cover;">
|
||||
<p class="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
||||
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
|
||||
</p>
|
||||
</video>
|
||||
<div id="overlay">
|
||||
<div id="title">FCast</div>
|
||||
<div id="connection-info">
|
||||
<div id="waiting-for-connection">Waiting for a connection</div>
|
||||
<div id="spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div id="manual-connection-info">Manual connection information</div>
|
||||
<div>
|
||||
<div id="ips">IPs</div><br />
|
||||
<div>Port<br>46899 (TCP), 46898 (WS)</div>
|
||||
</div>
|
||||
<div id="automatic-discovery">Automatic discovery is available via mDNS</div>
|
||||
<div id="qr-code"></div>
|
||||
<div id="scan-to-connect">Scan to connect</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
|
||||
<script src="./video.min.js"></script>
|
||||
<script src="./qrcode.min.js"></script>
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
5
receivers/electron/src/main/preload.js
Normal file
5
receivers/electron/src/main/preload.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onDeviceInfo: (callback) => ipcRenderer.on("device-info", callback)
|
||||
});
|
1
receivers/electron/src/main/qrcode.min.js
vendored
Normal file
1
receivers/electron/src/main/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
45
receivers/electron/src/main/renderer.js
Normal file
45
receivers/electron/src/main/renderer.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
const options = {
|
||||
textTrackSettings: false,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
controls: false
|
||||
};
|
||||
|
||||
const player = videojs("video-player", options, function onPlayerReady() {
|
||||
player.src({ type: "video/mp4", src: "./c.mp4" });
|
||||
});
|
||||
|
||||
|
||||
window.electronAPI.onDeviceInfo((_event, value) => {
|
||||
console.log("onDeviceInfo", value);
|
||||
|
||||
const ipsElement = document.getElementById('ips');
|
||||
if (ipsElement) {
|
||||
ipsElement.innerHTML = `IPs<br>${value.addresses.join('<br>')}`;
|
||||
}
|
||||
|
||||
const fcastConfig = {
|
||||
name: value.name,
|
||||
addresses: value.addresses,
|
||||
services: [
|
||||
{ port: 46899, type: 0 },
|
||||
{ port: 46898, type: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
const json = JSON.stringify(fcastConfig);
|
||||
let base64 = btoa(json);
|
||||
base64 = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
const url = `fcast://r/${base64}`;
|
||||
console.log("qr", {json, url, base64});
|
||||
|
||||
const qrCodeElement = document.getElementById('qr-code');
|
||||
new QRCode(qrCodeElement, {
|
||||
text: url,
|
||||
width: 256,
|
||||
height: 256,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
correctLevel : QRCode.CorrectLevel.H
|
||||
});
|
||||
});
|
106
receivers/electron/src/main/style.css
Normal file
106
receivers/electron/src/main/style.css
Normal file
|
@ -0,0 +1,106 @@
|
|||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-js {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#connection-info {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#qr-code {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#update-dialog, #waiting-for-connection, #manual-connection-info, #ips, #automatic-discovery, #scan-to-connect {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#update-button {
|
||||
background: blue;
|
||||
padding: 10px 28px;
|
||||
margin-top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#progress-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border: 8px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<link href="./video-js.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<title>FCast Receiver</title>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video-player" class="video-js" controls preload="auto" data-setup='{}'>
|
1
receivers/electron/src/player/video-js.min.css
vendored
Normal file
1
receivers/electron/src/player/video-js.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
26
receivers/electron/src/player/video.min.js
vendored
Normal file
26
receivers/electron/src/player/video.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue