1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-07-30 05:46:59 +00:00

webOS: Reworked page navigation, service subscription, and resolution handling

This commit is contained in:
Michael Hollister 2025-07-15 17:22:12 -05:00
parent 0da73f1f5b
commit a549296aca
16 changed files with 1425 additions and 374 deletions

View file

@ -43,6 +43,21 @@ export class Main {
private static windowVisible: boolean = false;
private static windowType: string = 'main';
private static serviceChannelEvents = [
'toast',
'connect',
'disconnect',
'play',
'pause',
'resume',
'stop',
'seek',
'setvolume',
'setspeed',
'setplaylistitem',
'event_subscribed_keys_update'
];
private static serviceChannelEventTimestamps: Map<string, number> = new Map();
private static async play(message: PlayMessage) {
Main.listeners.forEach(l => l.send(Opcode.PlayUpdate, new PlayUpdateMessage(Date.now(), message)));
@ -62,6 +77,9 @@ export class Main {
logger.info(`Launch response: ${JSON.stringify(response)}`);
logger.info(`Relaunching FCast Receiver with args: ${messageInfo.rendererEvent} ${JSON.stringify(messageInfo.rendererMessage)}`);
});
Main.windowVisible = true;
Main.windowType = 'player';
}
}
@ -82,37 +100,124 @@ export class Main {
Main.tcpListenerService = new TcpListenerService();
Main.webSocketListenerService = new WebSocketListenerService();
Main.emitter = new EventEmitter();
const voidCb = (message: any) => { message.respond({ returnValue: true, value: {} }); };
const objectCb = (message: any, value: any) => { message.respond({ returnValue: true, value: value }); };
service.register('service_channel', (message: any) => {
if (message.isSubscription) {
Main.serviceChannelEvents.forEach((event) => {
Main.emitter.on(event, (value) => {
const timestamp = Date.now();
const lastTimestamp = Main.serviceChannelEventTimestamps.get(event) ? Main.serviceChannelEventTimestamps.get(event) : -1;
registerService(service, 'toast', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'connect', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'disconnect', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'play', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'pause', (message: any) => { return voidCb.bind(this, message) });
registerService(service, 'resume', (message: any) => { return voidCb.bind(this, message) });
registerService(service, 'stop', (message: any) => { return voidCb.bind(this, message) });
registerService(service, 'seek', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'setvolume', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'setspeed', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'setplaylistitem', (message: any) => { return objectCb.bind(this, message) });
registerService(service, 'event_subscribed_keys_update', (message: any) => { return objectCb.bind(this, message) });
if (lastTimestamp < timestamp) {
Main.serviceChannelEventTimestamps.set(event, timestamp);
message.respond({ returnValue: true, subscriptionId: message.payload.subscriptionId, timestamp: timestamp, event: event, value: value });
}
});
});
}
message.respond({ returnValue: true, subscriptionId: message.payload.subscriptionId, timestamp: Date.now(), event: 'register', value: { subscribed: true }});
},
(message: any) => {
logger.info(`Canceled 'service_channel' service subscriber`);
Main.serviceChannelEvents.forEach((event) => {
Main.emitter.removeAllListeners(event);
});
message.respond({ returnValue: true, value: {} });
});
service.register('app_channel', (message: any) => {
switch (message.payload.event) {
case 'send_playback_error': {
const value: PlaybackErrorMessage = message.payload.value;
Main.listeners.forEach(l => l.send(Opcode.PlaybackError, value));
break;
}
case 'send_playback_update': {
const value: PlaybackUpdateMessage = message.payload.value;
Main.listeners.forEach(l => l.send(Opcode.PlaybackUpdate, value));
break;
}
case 'send_volume_update': {
const value: VolumeUpdateMessage = message.payload.value;
Main.cache.playerVolume = value.volume;
Main.listeners.forEach(l => l.send(Opcode.VolumeUpdate, value));
break;
}
case 'send_event': {
const value: EventMessage = message.payload.value;
Main.listeners.forEach(l => l.send(Opcode.Event, value));
break;
}
case 'play_request': {
const value: PlayMessage = message.payload.value.message;
const playlistIndex: number = message.payload.value.playlistIndex;
logger.debug(`Received play request for index ${playlistIndex}:`, value);
value.url = Main.mediaCache?.has(playlistIndex) ? Main.mediaCache?.getUrl(playlistIndex) : value.url;
Main.mediaCache?.cacheItems(playlistIndex);
Main.play(value);
break;
}
case 'get_sessions': {
// Having to mix and match session ids and ip addresses until querying websocket remote addresses is fixed
message.respond({
returnValue: true,
value: [].concat(Main.tcpListenerService.getSenders(), Main.webSocketListenerService.getSessions())
});
return;
}
case 'network_changed': {
logger.info('Network interfaces have changed', message);
Main.discoveryService.stop();
Main.discoveryService.start();
if (message.payload.value.fallback) {
message.respond({
returnValue: true,
value: getAllIPv4Addresses()
});
}
else {
message.respond({ returnValue: true, value: {} });
}
return;
}
case 'visibility_changed': {
logger.info('Window visibility has changed', message.payload.value);
Main.windowVisible = !message.payload.value.hidden;
Main.windowType = message.payload.value.window;
break;
}
default:
break;
}
message.respond({ returnValue: true, value: { success: true } });
});
Main.listeners = [Main.tcpListenerService, Main.webSocketListenerService];
Main.listeners.forEach(l => {
l.emitter.on("play", (message: PlayMessage) => Main.play(message));
l.emitter.on("pause", () => Main.emitter.emit('pause'));
l.emitter.on("resume", () => Main.emitter.emit('resume'));
l.emitter.on("stop", () => Main.emitter.emit('stop'));
l.emitter.on("seek", (message: SeekMessage) => Main.emitter.emit('seek', message));
l.emitter.on("setvolume", (message: SetVolumeMessage) => {
l.emitter.on('play', (message: PlayMessage) => Main.play(message));
l.emitter.on('pause', () => Main.emitter.emit('pause'));
l.emitter.on('resume', () => Main.emitter.emit('resume'));
l.emitter.on('stop', () => Main.emitter.emit('stop'));
l.emitter.on('seek', (message: SeekMessage) => Main.emitter.emit('seek', message));
l.emitter.on('setvolume', (message: SetVolumeMessage) => {
Main.cache.playerVolume = message.volume;
Main.emitter.emit('setvolume', message);
});
l.emitter.on("setspeed", (message: SetSpeedMessage) => Main.emitter.emit('setspeed', message));
l.emitter.on('setspeed', (message: SetSpeedMessage) => Main.emitter.emit('setspeed', message));
l.emitter.on('connect', (message) => {
ConnectionMonitor.onConnect(l, message, l instanceof WebSocketListenerService, () => {
@ -133,7 +238,7 @@ export class Main {
l.emitter.on('initial', (message) => {
logger.info(`Received 'Initial' message from sender: ${message}`);
});
l.emitter.on("setplaylistitem", (message: SetPlaylistItemMessage) => Main.emitter.emit('setplaylistitem', message));
l.emitter.on('setplaylistitem', (message: SetPlaylistItemMessage) => Main.emitter.emit('setplaylistitem', message));
l.emitter.on('subscribeevent', (message) => {
const subscribeData = l.subscribeEvent(message.sessionId, message.body.event);
@ -150,75 +255,6 @@ export class Main {
});
l.start();
});
service.register("send_playback_error", (message: any) => {
const value: PlaybackErrorMessage = message.payload.error;
Main.listeners.forEach(l => l.send(Opcode.PlaybackError, value));
message.respond({ returnValue: true, value: { success: true } });
});
service.register("send_playback_update", (message: any) => {
// logger.info("In send_playback_update callback");
const value: PlaybackUpdateMessage = message.payload.update;
Main.listeners.forEach(l => l.send(Opcode.PlaybackUpdate, value));
message.respond({ returnValue: true, value: { success: true } });
});
service.register("send_volume_update", (message: any) => {
const value: VolumeUpdateMessage = message.payload.update;
Main.cache.playerVolume = value.volume;
Main.listeners.forEach(l => l.send(Opcode.VolumeUpdate, value));
message.respond({ returnValue: true, value: { success: true } });
});
service.register("send_event", (message: any) => {
const value: EventMessage = message.payload.event;
Main.listeners.forEach(l => l.send(Opcode.Event, value));
message.respond({ returnValue: true, value: { success: true } });
});
service.register("play_request", (message: any) => {
const value: PlayMessage = message.payload.message;
const playlistIndex: number = message.payload.playlistIndex;
logger.debug(`Received play request for index ${playlistIndex}:`, value);
value.url = Main.mediaCache?.has(playlistIndex) ? Main.mediaCache?.getUrl(playlistIndex) : value.url;
Main.mediaCache?.cacheItems(playlistIndex);
Main.play(value);
message.respond({ returnValue: true, value: { success: true } });
});
// Having to mix and match session ids and ip addresses until querying websocket remote addresses is fixed
service.register("get_sessions", (message: any) => {
message.respond({
returnValue: true,
value: [].concat(Main.tcpListenerService.getSenders(), Main.webSocketListenerService.getSessions())
});
});
service.register("network_changed", (message: any) => {
logger.info('Network interfaces have changed', message);
Main.discoveryService.stop();
Main.discoveryService.start();
if (message.payload.fallback) {
message.respond({
returnValue: true,
value: getAllIPv4Addresses()
});
}
else {
message.respond({ returnValue: true, value: {} });
}
});
service.register("visibility_changed", (message: any) => {
logger.info('Window visibility has changed', message.payload);
Main.windowVisible = !message.payload.hidden;
Main.windowType = message.payload.window;
message.respond({ returnValue: true, value: {} });
});
}
catch (err) {
logger.error("Error initializing service:", err);
@ -251,23 +287,6 @@ export async function errorHandler(error: Error) {
Main.emitter.emit('toast', { message: error, icon: ToastIcon.ERROR });
}
function registerService(service: Service, method: string, callback: (message: any) => any) {
let callbackRef = null;
service.register(method, (message: any) => {
if (message.isSubscription) {
callbackRef = callback(message);
Main.emitter.on(method, callbackRef);
}
message.respond({ returnValue: true, value: { subscribed: true }});
},
(message: any) => {
logger.info(`Canceled ${method} service subscriber`);
Main.emitter.removeAllListeners(method);
message.respond({ returnValue: true, value: message.payload });
});
}
// Fallback for simulator or TV devices that don't work with the luna://com.palm.connectionmanager/getStatus method
function getAllIPv4Addresses() {
const interfaces = os.networkInterfaces();

View file

@ -3,7 +3,7 @@
"version": "2.0.0",
"vendor": "FUTO",
"type": "web",
"main": "main_window/index.html",
"main": "index.html",
"title": "FCast Receiver",
"appDescription": "FCast Receiver",
"icon": "assets/icons/icon.png",

View file

@ -1,4 +1,9 @@
const logger = window.targetAPI.logger;
import { v4 as uuidv4 } from 'modules/uuid';
import { Logger, LoggerType } from 'common/Logger';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
const logger = new Logger('Common', LoggerType.FRONTEND);
const serviceId = 'com.futo.fcast.receiver.service';
export enum RemoteKeyCode {
@ -10,59 +15,104 @@ export enum RemoteKeyCode {
Back = 461,
}
export function requestService(method: string, successCb: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void): any {
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: {},
onSuccess: (message: any) => {
if (message.value?.subscribed === true) {
logger.info(`requestService: Registered ${method} handler with service`);
}
else {
successCb(message);
}
},
onFailure: (message: any) => {
logger.error(`requestService: ${method} ${JSON.stringify(message)}`);
export class ServiceManager {
private static serviceChannelSuccessCbHandler?: (message: any) => void;
private static serviceChannelFailureCbHandler?: (message: any) => void;
private static serviceChannelCompleteCbHandler?: (message: any) => void;
if (failureCb) {
failureCb(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
}
},
subscribe: true,
resubscribe: true
});
}
export function callService(method: string, parameters?: any, successCb?: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: parameters,
constructor() {
// @ts-ignore
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'service_channel',
parameters: { subscriptionId: uuidv4() },
onSuccess: (message: any) => {
if (successCb) {
successCb(message);
if (message.value?.subscribed === true) {
logger.info(`requestService: Registered 'service_channel' handler with service`);
}
else if (ServiceManager.serviceChannelSuccessCbHandler) {
ServiceManager.serviceChannelSuccessCbHandler(message);
}
},
onFailure: (message: any) => {
logger.error(`callService: ${method} ${JSON.stringify(message)}`);
logger.error('Error subscribing to the service_channel:', message);
if (failureCb) {
failureCb(message);
if (ServiceManager.serviceChannelFailureCbHandler) {
ServiceManager.serviceChannelFailureCbHandler(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
if (ServiceManager.serviceChannelCompleteCbHandler) {
ServiceManager.serviceChannelCompleteCbHandler(message);
}
},
subscribe: false,
resubscribe: false
});
subscribe: true,
resubscribe: true
});
}
public subscribeToServiceChannel(successCb: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
ServiceManager.serviceChannelSuccessCbHandler = successCb;
ServiceManager.serviceChannelFailureCbHandler = failureCb;
ServiceManager.serviceChannelCompleteCbHandler = onCompleteCb;
}
public call(method: string, parameters?: any, successCb?: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
// @ts-ignore
const service = window.webOS.service.request(`luna://${serviceId}/`, {
method: 'app_channel',
parameters: { event: method, value: parameters },
onSuccess: (message: any) => {
if (successCb) {
successCb(message);
}
},
onFailure: (message: any) => {
logger.error(`callService: ${method} ${JSON.stringify(message)}`);
if (failureCb) {
failureCb(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
}
},
subscribe: false,
resubscribe: false
});
return service;
}
}
// CSS media queries do not work on older webOS versions...
export function initializeWindowSizeStylesheet() {
const resolution = sessionStorage.getItem('resolution');
if (resolution) {
window.onload = () => {
if (resolution == '1920x1080') {
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
}
else {
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
}
}
}
else {
window.onresize = () => {
if (window.innerWidth >= 1920 && window.innerHeight >= 1080) {
sessionStorage.setItem('resolution', '1920x1080');
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
}
else {
sessionStorage.setItem('resolution', '1280x720');
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
}
};
}
}
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {

View file

@ -0,0 +1,32 @@
import { Logger, LoggerType } from 'common/Logger';
import { ServiceManager } from 'lib/common';
declare global {
interface Window {
webOSApp: any;
}
}
const logger = new Logger('Main', LoggerType.FRONTEND);
const webPage: HTMLIFrameElement = document.getElementById('page') as HTMLIFrameElement;
let launchHandlerCallback = () => { logger.warn('No (re)launch handler set'); };
function loadPage(path: string) {
// @ts-ignore
webPage.src = path;
}
// We are embedding iframe element and using that for page navigation. This preserves a global JS context
// so bugs related to oversubscribing/canceling services are worked around by only subscribing once to
// required services
logger.info('Starting webOS application')
window.webOSApp = {
serviceManager: new ServiceManager(),
setLaunchHandler: (callback: () => void) => launchHandlerCallback = callback,
loadPage: loadPage
};
document.addEventListener('webOSLaunch', launchHandlerCallback);
document.addEventListener('webOSRelaunch', launchHandlerCallback);
loadPage('./main_window/index.html');

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<style>
iframe, body, html {
width: 100%;
height: 100%;
margin: 0;
border: none;
}
</style>
</head>
<body>
<iframe id="page", src="./main_window/index.html"></iframe>
<script src="./main.js"></script>
</body>
</html>

View file

@ -0,0 +1,101 @@
/* @media only screen and ((max-width: 1279px) or (max-height: 719px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
/* } */

View file

@ -0,0 +1,204 @@
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 20px;
margin: 5px;
margin-bottom: 10px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 32px;
height: 32px;
}
#overlay {
gap: 12.5vw;
font-size: 20px;
}
#title-text {
font-size: 100px;
}
#title-icon {
width: 84px;
height: 84px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 192px;
height: 192px;
margin: 15px auto;
padding: 12px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 4px;
margin-bottom: 4px;
}
#window-can-be-closed {
margin-bottom: 15px;
font-size: 18px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#connection-check {
width: 84px;
height: 84px;
margin: 24px;
}
#toast-notification {
padding: 8px;
top: -140px;
}
#toast-icon {
width: 60px;
height: 60px;
margin-right: 15px;
}
#toast-text {
font-size: 20px;
}
/* } */
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
}

View file

@ -3,23 +3,56 @@
import { preloadData } from 'common/main/Preload';
import { ToastIcon } from 'common/components/Toast';
import { EventMessage } from 'common/Packets';
import { callService, requestService } from 'lib/common';
import { ServiceManager, initializeWindowSizeStylesheet } from 'lib/common';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
declare global {
interface Window {
targetAPI: any;
webOSApp: any;
}
}
const logger = window.targetAPI.logger;
try {
const serviceId = 'com.futo.fcast.receiver.service';
let getSessionsService = null;
let networkChangedService = null;
let visibilityChangedService = null;
initializeWindowSizeStylesheet();
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
serviceManager.subscribeToServiceChannel((message: any) => {
switch (message.event) {
case 'toast':
preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration);
break;
case 'event_subscribed_keys_update':
preloadData.onEventSubscribedKeysUpdate(message.value);
break;
case 'connect':
preloadData.onConnectCb(null, message.value);
break;
case 'disconnect':
preloadData.onDisconnectCb(null, message.value);
break;
case 'play':
logger.info(`Main: Playing ${JSON.stringify(message)}`);
play(message.value);
break;
default:
break;
}
});
const toastService = requestService('toast', (message: any) => { preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration); });
const getDeviceInfoService = window.webOSDev.connection.getStatus({
onSuccess: (message: any) => {
logger.info('Network info status message', message);
const deviceName = 'FCast-LGwebOSTV';
const connections = [];
const connections: any[] = [];
let fallback = true;
if (message.wired.state !== 'disconnected') {
@ -35,7 +68,10 @@ try {
}
if (fallback) {
networkChangedService = callService('network_changed', { fallback: fallback }, (message: any) => {
const ipsIfaceName = document.getElementById('ips-iface-name');
ipsIfaceName.style.display = 'none';
serviceManager.call('network_changed', { fallback: fallback }, (message: any) => {
logger.info('Fallback network interfaces', message);
for (const ipAddr of message.value) {
connections.push({ type: 'wired', name: 'Ethernet', address: ipAddr });
@ -46,14 +82,10 @@ try {
}, (message: any) => {
logger.error('Main: preload - error fetching network interfaces', message);
preloadData.onToastCb('Error detecting network interfaces', ToastIcon.ERROR);
}, () => {
networkChangedService = null;
});
}
else {
networkChangedService = callService('network_changed', { fallback: fallback }, null, null, () => {
networkChangedService = null;
});
serviceManager.call('network_changed', { fallback: fallback });
preloadData.deviceInfo = { name: deviceName, interfaces: connections };
preloadData.onDeviceInfoCb();
}
@ -66,74 +98,39 @@ try {
resubscribe: true
});
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
window.targetAPI.getSessions(() => {
return new Promise((resolve, reject) => {
getSessionsService = callService('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
serviceManager.call('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
});
});
const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); });
const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
preloadData.sendEventCb = (event: EventMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_event',
parameters: { event },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); },
});
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
};
const playService = requestService('play', (message: any) => {
logger.info(`Main: Playing ${JSON.stringify(message)}`);
play(message.value);
});
const launchHandler = () => {
const params = window.webOSDev.launchParams();
logger.info(`Main: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
// WebOS 6.0 and earlier: Timestamp tracking seems to be necessary as launch event is raised regardless if app is in foreground or not
const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
const lastTimestamp = Number(sessionStorage.getItem('lastTimestamp'));
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('lastTimestamp', params.timestamp);
play(params.messageInfo);
}
};
document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => {
visibilityChangedService = callService('visibility_changed', { hidden: document.hidden, window: 'main' }, null, null, () => {
visibilityChangedService = null;
})
});
// Cannot go back to a state where user was previously casting a video, so exit.
// window.onpopstate = () => {
// window.webOS.platformBack();
// };
window.parent.webOSApp.setLaunchHandler(launchHandler);
document.addEventListener('visibilitychange', () => { serviceManager.call('visibility_changed', { hidden: document.hidden, window: 'main' }); });
const play = (messageInfo: any) => {
sessionStorage.setItem('playInfo', JSON.stringify(messageInfo));
getDeviceInfoService?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
getSessionsService?.cancel();
toastService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
playService?.cancel();
networkChangedService?.cancel();
visibilityChangedService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${messageInfo.contentViewer}/index.html`, '_self');
window.parent.webOSApp.loadPage(`${messageInfo.contentViewer}/index.html`);
};
}
catch (err) {
logger.error(`Main: preload ${JSON.stringify(err)}`);
logger.error(`Main: preload`, err);
preloadData.onToastCb(`Error starting the application: ${JSON.stringify(err)}`, ToastIcon.ERROR);
}

View file

@ -1,3 +1,9 @@
/* WebOS custom player styles */
html {
overflow: hidden;
}
.card-title {
font-family: InterBold;
}
@ -27,6 +33,14 @@
font-family: InterBold;
}
#ips {
gap: unset;
}
#ips-iface-icon {
margin-right: 15px;
}
#window-can-be-closed {
font-family: InterRegular;
}

View file

@ -0,0 +1,101 @@
/* @media only screen and ((max-width: 1279px) or (max-height: 719px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
/* } */

View file

@ -0,0 +1,204 @@
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 20px;
margin: 5px;
margin-bottom: 10px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 32px;
height: 32px;
}
#overlay {
gap: 12.5vw;
font-size: 20px;
}
#title-text {
font-size: 100px;
}
#title-icon {
width: 84px;
height: 84px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 192px;
height: 192px;
margin: 15px auto;
padding: 12px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 4px;
margin-bottom: 4px;
}
#window-can-be-closed {
margin-bottom: 15px;
font-size: 18px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#connection-check {
width: 84px;
height: 84px;
margin: 24px;
}
#toast-notification {
padding: 8px;
top: -140px;
}
#toast-icon {
width: 60px;
height: 60px;
margin-right: 15px;
}
#toast-text {
font-size: 20px;
}
/* } */
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
}

View file

@ -2,190 +2,149 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { preloadData } from 'common/player/Preload';
import { EventMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, VolumeUpdateMessage } from 'common/Packets';
import { callService, requestService } from 'lib/common';
import { ServiceManager, initializeWindowSizeStylesheet } from 'lib/common';
import { toast, ToastIcon } from 'common/components/Toast';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
declare global {
interface Window {
targetAPI: any;
webOSAPI: any;
webOSApp: any;
}
}
const logger = window.targetAPI.logger;
const serviceId = 'com.futo.fcast.receiver.service';
try {
let getSessions = null;
initializeWindowSizeStylesheet();
window.webOSAPI = {
pendingPlay: JSON.parse(sessionStorage.getItem('playInfo'))
};
const contentViewer = window.webOSAPI.pendingPlay?.contentViewer;
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
serviceManager.subscribeToServiceChannel((message: any) => {
switch (message.event) {
case 'toast':
preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration);
break;
case 'play': {
if (contentViewer !== message.value.contentViewer) {
window.parent.webOSApp.loadPage(`${message.value.contentViewer}/index.html`);
}
else {
if (message.value.rendererEvent === 'play-playlist') {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
}
}
else {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayCb(null, message.value.rendererMessage);
}
}
}
break;
}
case 'pause':
preloadData.onPauseCb();
break;
case 'resume':
preloadData.onResumeCb();
break;
case 'stop':
window.parent.webOSApp.loadPage('main_window/index.html');
break;
case 'seek':
preloadData.onSeekCb(null, message.value);
break;
case 'setvolume':
preloadData.onSetVolumeCb(null, message.value);
break;
case 'setspeed':
preloadData.onSetSpeedCb(null, message.value);
break;
case 'setplaylistitem':
preloadData.onSetPlaylistItemCb(null, message.value);
break;
case 'event_subscribed_keys_update':
preloadData.onEventSubscribedKeysUpdate(message.value);
break;
case 'connect':
preloadData.onConnectCb(null, message.value);
break;
case 'disconnect':
preloadData.onDisconnectCb(null, message.value);
break;
// 'play-playlist' is handled in the 'play' message for webOS
default:
break;
}
});
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_playback_error',
parameters: { error },
onSuccess: () => {},
onFailure: (message: any) => {
logger.error(`Player: send_playback_error ${JSON.stringify(message)}`);
},
});
serviceManager.call('send_playback_error', error, null, (message: any) => { logger.error(`Player: send_playback_error ${JSON.stringify(message)}`); });
};
preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_playback_update',
parameters: { update },
// onSuccess: (message: any) => {
// logger.info(`Player: send_playback_update ${JSON.stringify(message)}`);
// },
onSuccess: () => {},
onFailure: (message: any) => {
logger.error(`Player: send_playback_update ${JSON.stringify(message)}`);
},
});
serviceManager.call('send_playback_update', update, null, (message: any) => { logger.error(`Player: send_playback_update ${JSON.stringify(message)}`); });
};
preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_volume_update',
parameters: { update },
onSuccess: () => {},
onFailure: (message: any) => {
logger.error(`Player: send_volume_update ${JSON.stringify(message)}`);
},
});
serviceManager.call('send_volume_update', update, null, (message: any) => { logger.error(`Player: send_volume_update ${JSON.stringify(message)}`); });
};
preloadData.sendEventCb = (event: EventMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_event',
parameters: { event },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); },
});
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
};
const playService = requestService('play', (message: any) => {
if (contentViewer !== message.value.contentViewer) {
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${message.value.contentViewer}/index.html`, '_self');
}
else {
if (message.value.rendererEvent === 'play-playlist') {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
}
}
else {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayCb(null, message.value.rendererMessage);
}
}
}
}, (message: any) => {
logger.error(`Player: play ${JSON.stringify(message)}`);
});
const pauseService = requestService('pause', () => { preloadData.onPauseCb(); });
const resumeService = requestService('resume', () => { preloadData.onResumeCb(); });
const stopService = requestService('stop', () => {
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.back();
window.open('../main_window/index.html', '_self');
});
const seekService = requestService('seek', (message: any) => { preloadData.onSeekCb(null, message.value); });
const setVolumeService = requestService('setvolume', (message: any) => { preloadData.onSetVolumeCb(null, message.value); });
const setSpeedService = requestService('setspeed', (message: any) => { preloadData.onSetSpeedCb(null, message.value); });
const onSetPlaylistItemService = requestService('setplaylistitem', (message: any) => { preloadData.onSetPlaylistItemCb(null, message.value); });
preloadData.sendPlayRequestCb = (message: PlayMessage, playlistIndex: number) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'play_request',
parameters: { message: message, playlistIndex: playlistIndex },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: play_request ${playlistIndex} ${JSON.stringify(message)}`); },
});
serviceManager.call('play_request', { message: message, playlistIndex: playlistIndex }, null, (message: any) => { logger.error(`Player: play_request ${playlistIndex} ${JSON.stringify(message)}`); });
};
window.targetAPI.getSessions(() => {
return new Promise((resolve, reject) => {
getSessions = callService('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
serviceManager.call('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
});
});
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); });
const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
const onPlayPlaylistService = requestService('play-playlist', (message: any) => { preloadData.onPlayPlaylistCb(null, message.value); });
const launchHandler = () => {
// args don't seem to be passed in via event despite what documentation says...
const params = window.webOSDev.launchParams();
logger.info(`Player: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
// WebOS 6.0 and earlier: Timestamp tracking seems to be necessary as launch event is raised regardless if app is in foreground or not
const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
const lastTimestamp = Number(sessionStorage.getItem('lastTimestamp'));
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('playInfo', JSON.stringify(params.messageInfo));
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${params.messageInfo.contentViewer}/index.html`, '_self');
window.parent.webOSApp.loadPage(`${params.messageInfo.contentViewer}/index.html`);
}
};
document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => callService('visibility_changed', { hidden: document.hidden, window: contentViewer }));
window.parent.webOSApp.setLaunchHandler(launchHandler);
document.addEventListener('visibilitychange', () => serviceManager.call('visibility_changed', { hidden: document.hidden, window: contentViewer }));
}
catch (err) {
logger.error(`Player: preload ${JSON.stringify(err)}`);
logger.error(`Player: preload`, err);
toast(`Error starting the video player (preload): ${JSON.stringify(err)}`, ToastIcon.ERROR);
}

View file

@ -0,0 +1,101 @@
/* @media only screen and ((max-width: 1279px) or (max-height: 719px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
/* } */

View file

@ -0,0 +1,204 @@
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 20px;
margin: 5px;
margin-bottom: 10px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 32px;
height: 32px;
}
#overlay {
gap: 12.5vw;
font-size: 20px;
}
#title-text {
font-size: 100px;
}
#title-icon {
width: 84px;
height: 84px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 192px;
height: 192px;
margin: 15px auto;
padding: 12px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 4px;
margin-bottom: 4px;
}
#window-can-be-closed {
margin-bottom: 15px;
font-size: 18px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#connection-check {
width: 84px;
height: 84px;
margin: 24px;
}
#toast-notification {
padding: 8px;
top: -140px;
}
#toast-icon {
width: 60px;
height: 60px;
margin-right: 15px;
}
#toast-text {
font-size: 20px;
}
/* } */
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
}

View file

@ -1 +1,5 @@
/* Stub for future use */
/* WebOS custom player styles */
html {
overflow: hidden;
}

View file

@ -12,6 +12,71 @@ const TARGET = 'webOS';
// const TARGET = 'tizenOS';
module.exports = [
{
mode: buildMode,
entry: {
main: './src/Main.ts',
},
target: ['web', 'es5'],
module: {
rules: [
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
},
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
}
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'lib': path.resolve(__dirname, 'lib'),
'modules': path.resolve(__dirname, 'node_modules'),
'common': path.resolve(__dirname, '../../common/web'),
},
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
// Common assets
{
from: '../common/assets/**',
to: '[path][name][ext]',
context: path.resolve(__dirname, '..', '..', 'common'),
globOptions: { ignore: ['**/*.txt'] }
},
// Target assets
{ from: 'appinfo.json', to: '[name][ext]' },
{
from: '**',
to: 'assets/[path][name][ext]',
context: path.resolve(__dirname, 'assets'),
globOptions: { ignore: ['**/*.svg'] }
},
{
from: '**',
to: 'lib/[name][ext]',
context: path.resolve(__dirname, 'lib'),
globOptions: { ignore: ['**/*.txt'] }
},
{ from: './src/index.html', to: '[name][ext]' }
],
}),
new webpack.DefinePlugin({
TARGET: JSON.stringify(TARGET)
})
]
},
{
mode: buildMode,
entry: {
@ -50,31 +115,10 @@ module.exports = [
plugins: [
new CopyWebpackPlugin({
patterns: [
// Common assets
{
from: '../common/assets/**',
to: '../[path][name][ext]',
context: path.resolve(__dirname, '..', '..', 'common'),
globOptions: { ignore: ['**/*.txt'] }
},
{
from: '../../common/web/main/common.css',
to: '[name][ext]',
},
// Target assets
{ from: 'appinfo.json', to: '../[name][ext]' },
{
from: '**',
to: '../assets/[path][name][ext]',
context: path.resolve(__dirname, 'assets'),
globOptions: { ignore: ['**/*.svg'] }
},
{
from: '**',
to: '../lib/[name][ext]',
context: path.resolve(__dirname, 'lib'),
globOptions: { ignore: ['**/*.txt'] }
},
{
from: './src/main/*',
to: '[name][ext]',