From a549296acaebc9a72eed03b017e9748ffb5b74e5 Mon Sep 17 00:00:00 2001 From: Michael Hollister Date: Tue, 15 Jul 2025 17:22:12 -0500 Subject: [PATCH] webOS: Reworked page navigation, service subscription, and resolution handling --- .../webos/fcast-receiver-service/src/Main.ts | 237 +++++++++-------- receivers/webos/fcast-receiver/appinfo.json | 2 +- receivers/webos/fcast-receiver/lib/common.ts | 136 +++++++--- receivers/webos/fcast-receiver/src/Main.ts | 32 +++ receivers/webos/fcast-receiver/src/index.html | 17 ++ .../fcast-receiver/src/main/1280x720.css | 101 ++++++++ .../fcast-receiver/src/main/1920x1080.css | 204 +++++++++++++++ .../webos/fcast-receiver/src/main/Preload.ts | 109 ++++---- .../webos/fcast-receiver/src/main/style.css | 14 + .../fcast-receiver/src/player/1280x720.css | 101 ++++++++ .../fcast-receiver/src/player/1920x1080.css | 204 +++++++++++++++ .../fcast-receiver/src/player/Preload.ts | 245 ++++++++---------- .../fcast-receiver/src/viewer/1280x720.css | 101 ++++++++ .../fcast-receiver/src/viewer/1920x1080.css | 204 +++++++++++++++ .../webos/fcast-receiver/src/viewer/style.css | 6 +- .../webos/fcast-receiver/webpack.config.js | 86 ++++-- 16 files changed, 1425 insertions(+), 374 deletions(-) create mode 100644 receivers/webos/fcast-receiver/src/Main.ts create mode 100644 receivers/webos/fcast-receiver/src/index.html create mode 100644 receivers/webos/fcast-receiver/src/main/1280x720.css create mode 100644 receivers/webos/fcast-receiver/src/main/1920x1080.css create mode 100644 receivers/webos/fcast-receiver/src/player/1280x720.css create mode 100644 receivers/webos/fcast-receiver/src/player/1920x1080.css create mode 100644 receivers/webos/fcast-receiver/src/viewer/1280x720.css create mode 100644 receivers/webos/fcast-receiver/src/viewer/1920x1080.css diff --git a/receivers/webos/fcast-receiver-service/src/Main.ts b/receivers/webos/fcast-receiver-service/src/Main.ts index 7e9a285..7081700 100644 --- a/receivers/webos/fcast-receiver-service/src/Main.ts +++ b/receivers/webos/fcast-receiver-service/src/Main.ts @@ -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 = 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(); diff --git a/receivers/webos/fcast-receiver/appinfo.json b/receivers/webos/fcast-receiver/appinfo.json index 6db255e..b18bcd1 100644 --- a/receivers/webos/fcast-receiver/appinfo.json +++ b/receivers/webos/fcast-receiver/appinfo.json @@ -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", diff --git a/receivers/webos/fcast-receiver/lib/common.ts b/receivers/webos/fcast-receiver/lib/common.ts index 5c9e425..96b2aa4 100644 --- a/receivers/webos/fcast-receiver/lib/common.ts +++ b/receivers/webos/fcast-receiver/lib/common.ts @@ -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', ''); + } + else { + document.head.insertAdjacentHTML('beforeend', ''); + } + } + } + else { + window.onresize = () => { + if (window.innerWidth >= 1920 && window.innerHeight >= 1080) { + sessionStorage.setItem('resolution', '1920x1080'); + document.head.insertAdjacentHTML('beforeend', ''); + } + else { + sessionStorage.setItem('resolution', '1280x720'); + document.head.insertAdjacentHTML('beforeend', ''); + } + }; + } } export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } { diff --git a/receivers/webos/fcast-receiver/src/Main.ts b/receivers/webos/fcast-receiver/src/Main.ts new file mode 100644 index 0000000..a93624b --- /dev/null +++ b/receivers/webos/fcast-receiver/src/Main.ts @@ -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'); diff --git a/receivers/webos/fcast-receiver/src/index.html b/receivers/webos/fcast-receiver/src/index.html new file mode 100644 index 0000000..abeb5b0 --- /dev/null +++ b/receivers/webos/fcast-receiver/src/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/receivers/webos/fcast-receiver/src/main/1280x720.css b/receivers/webos/fcast-receiver/src/main/1280x720.css new file mode 100644 index 0000000..8b38b4e --- /dev/null +++ b/receivers/webos/fcast-receiver/src/main/1280x720.css @@ -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; + } +/* } */ diff --git a/receivers/webos/fcast-receiver/src/main/1920x1080.css b/receivers/webos/fcast-receiver/src/main/1920x1080.css new file mode 100644 index 0000000..720ab67 --- /dev/null +++ b/receivers/webos/fcast-receiver/src/main/1920x1080.css @@ -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; + } +} diff --git a/receivers/webos/fcast-receiver/src/main/Preload.ts b/receivers/webos/fcast-receiver/src/main/Preload.ts index 695534c..6069294 100644 --- a/receivers/webos/fcast-receiver/src/main/Preload.ts +++ b/receivers/webos/fcast-receiver/src/main/Preload.ts @@ -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); } diff --git a/receivers/webos/fcast-receiver/src/main/style.css b/receivers/webos/fcast-receiver/src/main/style.css index 1c92bb3..bc51980 100644 --- a/receivers/webos/fcast-receiver/src/main/style.css +++ b/receivers/webos/fcast-receiver/src/main/style.css @@ -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; } diff --git a/receivers/webos/fcast-receiver/src/player/1280x720.css b/receivers/webos/fcast-receiver/src/player/1280x720.css new file mode 100644 index 0000000..8b38b4e --- /dev/null +++ b/receivers/webos/fcast-receiver/src/player/1280x720.css @@ -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; + } +/* } */ diff --git a/receivers/webos/fcast-receiver/src/player/1920x1080.css b/receivers/webos/fcast-receiver/src/player/1920x1080.css new file mode 100644 index 0000000..720ab67 --- /dev/null +++ b/receivers/webos/fcast-receiver/src/player/1920x1080.css @@ -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; + } +} diff --git a/receivers/webos/fcast-receiver/src/player/Preload.ts b/receivers/webos/fcast-receiver/src/player/Preload.ts index 1e5e174..a0a726b 100644 --- a/receivers/webos/fcast-receiver/src/player/Preload.ts +++ b/receivers/webos/fcast-receiver/src/player/Preload.ts @@ -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); } diff --git a/receivers/webos/fcast-receiver/src/viewer/1280x720.css b/receivers/webos/fcast-receiver/src/viewer/1280x720.css new file mode 100644 index 0000000..8b38b4e --- /dev/null +++ b/receivers/webos/fcast-receiver/src/viewer/1280x720.css @@ -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; + } +/* } */ diff --git a/receivers/webos/fcast-receiver/src/viewer/1920x1080.css b/receivers/webos/fcast-receiver/src/viewer/1920x1080.css new file mode 100644 index 0000000..720ab67 --- /dev/null +++ b/receivers/webos/fcast-receiver/src/viewer/1920x1080.css @@ -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; + } +} diff --git a/receivers/webos/fcast-receiver/src/viewer/style.css b/receivers/webos/fcast-receiver/src/viewer/style.css index f9f22a9..b2c5fbf 100644 --- a/receivers/webos/fcast-receiver/src/viewer/style.css +++ b/receivers/webos/fcast-receiver/src/viewer/style.css @@ -1 +1,5 @@ -/* Stub for future use */ +/* WebOS custom player styles */ + +html { + overflow: hidden; +} diff --git a/receivers/webos/fcast-receiver/webpack.config.js b/receivers/webos/fcast-receiver/webpack.config.js index 9ce297c..e5aa042 100644 --- a/receivers/webos/fcast-receiver/webpack.config.js +++ b/receivers/webos/fcast-receiver/webpack.config.js @@ -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]',