diff --git a/runtime/js/runtime/.npmignore b/runtime/js/runtime/.npmignore index d0a1d1fad..945ce43a9 100644 --- a/runtime/js/runtime/.npmignore +++ b/runtime/js/runtime/.npmignore @@ -1 +1 @@ -bridge.js \ No newline at end of file +index.js \ No newline at end of file diff --git a/v2/internal/bridge/client_darwin.go b/v2/internal/bridge/client.go similarity index 95% rename from v2/internal/bridge/client_darwin.go rename to v2/internal/bridge/client.go index 181878cbc..106de6d63 100644 --- a/v2/internal/bridge/client_darwin.go +++ b/v2/internal/bridge/client.go @@ -140,11 +140,11 @@ func (b BridgeClient) SetApplicationMenu(menuJSON string) { } func (b BridgeClient) SetTrayMenu(trayMenuJSON string) { - b.session.log.Info("SetTrayMenu unsupported in Bridge mode") + b.session.sendMessage("TS" + trayMenuJSON) } -func (b BridgeClient) UpdateTrayMenuLabel(JSON string) { - b.session.log.Info("UpdateTrayMenuLabel unsupported in Bridge mode") +func (b BridgeClient) UpdateTrayMenuLabel(trayMenuJSON string) { + b.session.sendMessage("TS" + trayMenuJSON) } func (b BridgeClient) UpdateContextMenu(contextMenuJSON string) { diff --git a/v2/internal/runtime/js/runtime/.npmignore b/v2/internal/runtime/js/runtime/.npmignore new file mode 100644 index 000000000..e8310385c --- /dev/null +++ b/v2/internal/runtime/js/runtime/.npmignore @@ -0,0 +1 @@ +src \ No newline at end of file diff --git a/v2/internal/runtime/js/runtime/bridge.js b/v2/internal/runtime/js/runtime/bridge.js deleted file mode 100644 index 50d3070b7..000000000 --- a/v2/internal/runtime/js/runtime/bridge.js +++ /dev/null @@ -1,239 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The lightweight framework for web-like apps -(c) Lea Anthony 2019-present -*/ -/* jshint esversion: 6 */ - -function init() { - - // Bridge object - window.wailsbridge = { - reconnectOverlay: null, - reconnectTimer: 300, - wsURL: 'ws://' + window.location.hostname + ':34115/bridge', - connectionState: null, - config: {}, - websocket: null, - callback: null, - overlayHTML: - '
', - overlayCSS: - '.wails-reconnect-overlay{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter: blur(20px) saturate(160%) contrast(45%) brightness(140%);display:none;z-index:999999}.wails-reconnect-overlay-content{position:relative;top:50%;transform:translateY(-50%);margin: 0;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAuCAMAAACPpbA7AAAAflBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAAAAABAQEEBAQAAAAAAAAEBAQAAAADAwMAAAABAQEAAAAAAAAAAAAAAAAAAAACAgICAgIBAQEAAAAAAAAAAAAAAAAAAAACAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQWCC3waAAAAKXRSTlMALgUMIBk0+xEqJs70Xhb3lu3EjX2EZTlv5eHXvbarQj3cdmpXSqOeUDwaqNAAAAKCSURBVEjHjZTntqsgEIUPVVCwtxg1vfD+L3hHRe8K6snZf+KKn8OewvzsSSeXLruLnz+KHs0gr6DkT3xsRkU6VVn4Ha/UxLe1Z4y64i847sykPBh/AvQ7ry3eFN70oKrfcBJYvm/tQ1qxP4T3emXPeXAkvodPUvtdjbhk+Ft4c0hslTiXVOzxOJ15NWUblQhRsdu3E1AfCjj3Gdm18zSOsiH8Lk4TB480ksy62fiqNo4OpyU8O21l6+hyRtS6z8r1pHlmle5sR1/WXS6Mq2Nl+YeKt3vr+vdH/q4O68tzXuwkiZmngYb4R8Co1jh0+Ww2UTyWxBvtyxLO7QVjO3YOD/lWZpbXDGellFG2Mws58mMnjVZSn7p+XvZ6IF4nn02OJZV0aTO22arp/DgLPtrgpVoi6TPbZm4XQBjY159w02uO0BDdYsfrOEi0M2ulRXlCIPAOuN1NOVhi+riBR3dgwQplYsZRZJLXq23Mlo5njkbY0rZFu3oiNIYG2kqsbVz67OlNuZZIOlfxHDl0UpyRX86z/OYC/3qf1A1xTrMp/PWWM4ePzf8DDp1nesQRpcFk7BlwdzN08ZIALJpCaciQXO0f6k4dnuT/Ewg4l7qSTNzm2SykdHn6GJ12mWc6aCNj/g1cTXpB8YFfr0uVc96aFkkqiIiX4nO+salKwGtIkvfB+Ja8DxMeD3hIXP5mTOYPB4eVT0+32I5ykvPZjesnkGgIREgYnmLrPb0PdV3hoLup2TjcGBPM4mgsfF5BrawZR4/GpzYQzQfrUZCf0TCWYo2DqhdhTJBQ6j4xqmmLN5LjdRIY8LWExiFUsSrza/nmFBqw3I9tEZB9h0lIQSO9if8DkISDAj8CDawAAAAASUVORK5CYII=);background-repeat:no-repeat;background-position:center}.wails-reconnect-overlay-loadingspinner{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#f00 #eee0 #f00 #eee0;border-radius:50%;animation:loadingspin 1s linear infinite;margin:auto;padding:2.5em}@keyframes loadingspin{100%{transform:rotate(360deg); opacity}}', - log: function (message) { - // eslint-disable-next-line - console.log( - '%c wails bridge %c ' + message + ' ', - 'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem', - 'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem' - ); - } - }; - - window.onbeforeunload = function() { - if( window.wails.websocket ) { - window.wails.websocket.onclose = function () { }; - window.wails.websocket.close(); - window.wails.websocket = null; - } - } - -} - -function setupIPCBridge() { - // darwin - window.webkit = { - messageHandlers: { - external: { - postMessage: (message) => { - window.wailsbridge.websocket.send(message); - } - } - } - }; -} - -// Adapted from webview - thanks zserge! -function injectCSS(css) { - const elem = document.createElement('style'); - elem.setAttribute('type', 'text/css'); - elem.appendChild(document.createTextNode(css)); - const head = document.head || document.getElementsByTagName('head')[0]; - head.appendChild(elem); -} - -// Creates a node in the Dom -/** - * @param {HTMLElement} parent - * @param {string} elementType - * @param {string} id - * @param {string|null} className - * @param {string|null} content - */ -function createNode(parent, elementType, id, className, content) { - const d = document.createElement(elementType); - if (id) { - d.id = id; - } - if (className) { - d.className = className; - } - if (content) { - d.innerHTML = content; - } - parent.appendChild(d); - return d; -} - -// Sets up the overlay -function setupOverlay() { - const body = document.body; - const wailsBridgeNode = createNode(body, 'div', 'wails-bridge', null, null); - wailsBridgeNode.innerHTML = window.wailsbridge.overlayHTML; - - // Inject the overlay CSS - injectCSS(window.wailsbridge.overlayCSS); -} - -// Start the Wails Bridge -function startBridge() { - // Setup the overlay - setupOverlay(); - - window.wailsbridge.websocket = null; - window.wailsbridge.connectTimer = null; - window.wailsbridge.reconnectOverlay = document.querySelector( - '.wails-reconnect-overlay' - ); - window.wailsbridge.connectionState = 'disconnected'; - - // Shows the overlay - function showReconnectOverlay() { - window.wailsbridge.reconnectOverlay.style.display = 'block'; - } - - // Hides the overlay - const hideReconnectOverlay = function () { - window.wailsbridge.reconnectOverlay.style.display = 'none'; - } - window.wailsbridge.hideReconnectOverlay = hideReconnectOverlay; - - // Adds a script to the Dom. - // Removes it if second parameter is true. - function addScript(script, remove) { - const s = document.createElement('script'); - s.setAttribute('type', 'text/javascript'); - s.textContent = script; - document.head.appendChild(s); - - // Remove internal messages from the DOM - if (remove) { - s.parentNode.removeChild(s); - } - } - - // Handles incoming websocket connections - function handleConnect() { - window.wailsbridge.log('Connected to backend'); - setupIPCBridge(); - hideReconnectOverlay(); - clearInterval(window.wailsbridge.connectTimer); - window.wailsbridge.websocket.onclose = handleDisconnect; - window.wailsbridge.websocket.onmessage = handleMessage; - window.wailsbridge.connectionState = 'connected'; - } - - // Handles websocket disconnects - function handleDisconnect() { - window.wailsbridge.log('Disconnected from backend'); - window.wailsbridge.websocket = null; - window.wailsbridge.connectionState = 'disconnected'; - showReconnectOverlay(); - connect(); - } - - // Try to connect to the backend every 1s (default value). - // Change this value in the main wailsbridge object. - function connect() { - window.wailsbridge.connectTimer = setInterval(function () { - if (window.wailsbridge.websocket == null) { - window.wailsbridge.websocket = new WebSocket(window.wailsbridge.wsURL); - window.wailsbridge.websocket.onopen = handleConnect; - window.wailsbridge.websocket.onerror = function (e) { - e.stopImmediatePropagation(); - e.stopPropagation(); - e.preventDefault(); - window.wailsbridge.websocket = null; - return false; - }; - } - }, window.wailsbridge.reconnectTimer); - } - - function handleMessage(message) { - // As a bridge we ignore js and css injections - switch (message.data[0]) { - // Wails library - inject! - case 'b': - message = message.data.slice(1) - addScript(message); - window.wailsbridge.log('Loaded Wails Runtime'); - - // We need to now send a message to the backend telling it - // we have loaded (System Start) - window.webkit.messageHandlers.external.postMessage("SS"); - - // Now wails runtime is loaded, wails for the ready event - // and callback to the main app - // window.wails.Events.On('wails:loaded', function () { - if (window.wailsbridge.callback) { - window.wailsbridge.log('Notifying application'); - window.wailsbridge.callback(window.wails); - } - // }); - break; - // // Notifications - // case 'n': - // addScript(message.data.slice(1), true); - // break; - // // Binding - // case 'b': - // const binding = message.data.slice(1); - // //log("Binding: " + binding) - // window.wails._.NewBinding(binding); - // break; - // // Call back - case 'c': - const callbackData = message.data.slice(1); - window.wails._.Callback(callbackData); - break; - default: - window.wailsbridge.log('Unknown message: ' + message.data); - } - } - - // Start by showing the overlay... - showReconnectOverlay(); - - // ...and attempt to connect - connect(); -} - -function InitBridge(callback) { - // Set up the bridge - init(); - - // Save the callback - window.wailsbridge.callback = callback; - - // Start Bridge - startBridge(); -} - -module.exports = { - InitBridge: InitBridge, -} diff --git a/v2/internal/runtime/js/runtime/init.js b/v2/internal/runtime/js/runtime/init.js index 67471579f..70a65c608 100644 --- a/v2/internal/runtime/js/runtime/init.js +++ b/v2/internal/runtime/js/runtime/init.js @@ -9,7 +9,7 @@ The lightweight framework for web-like apps */ /* jshint esversion: 6 */ -import { InitBridge } from './bridge'; +import bridge from './bridge'; /** * ready will execute the callback when Wails has loaded @@ -25,7 +25,7 @@ function ready(callback) { } // If not we need to setup the bridge - InitBridge(callback); + bridge.InitBridge(callback); } module.exports = { diff --git a/v2/internal/runtime/js/runtime/src/Menu.svelte b/v2/internal/runtime/js/runtime/src/Menu.svelte new file mode 100644 index 000000000..a949b4e82 --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/Menu.svelte @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/v2/internal/runtime/js/runtime/src/Menubar.svelte b/v2/internal/runtime/js/runtime/src/Menubar.svelte new file mode 100644 index 000000000..156f50792 --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/Menubar.svelte @@ -0,0 +1,37 @@ + + +{#if $menuVisible } +
+ + {#each $trays as tray} + + {/each} + +
+{/if} + + \ No newline at end of file diff --git a/v2/internal/runtime/js/runtime/src/Overlay.svelte b/v2/internal/runtime/js/runtime/src/Overlay.svelte new file mode 100644 index 000000000..aa8266e04 --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/Overlay.svelte @@ -0,0 +1,55 @@ + + +{#if $overlayVisible } +
+
+
+
+
+{/if} + + \ No newline at end of file diff --git a/v2/internal/runtime/js/runtime/src/TrayMenu.svelte b/v2/internal/runtime/js/runtime/src/TrayMenu.svelte new file mode 100644 index 000000000..648cdfe83 --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/TrayMenu.svelte @@ -0,0 +1,21 @@ + + + + {tray.Label} + + + + \ No newline at end of file diff --git a/v2/internal/runtime/js/runtime/src/main.js b/v2/internal/runtime/js/runtime/src/main.js new file mode 100644 index 000000000..e28d832a3 --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/main.js @@ -0,0 +1,45 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The lightweight framework for web-like apps +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +import Overlay from './Overlay.svelte'; +import MenuBar from './Menubar.svelte'; +import {showOverlay} from "./store"; +import {StartWebsocket} from "./websocket"; + +let components = {}; + +function setupMenuBar() { + components.menubar = new MenuBar({ + target: document.body, + }); +} + +// Sets up the overlay +function setupOverlay() { + components.overlay = new Overlay({ + target: document.body, + anchor: document.querySelector('#wails-bridge'), + }); +} + +export function InitBridge(callback) { + + setupMenuBar() + + // Setup the overlay + setupOverlay(); + + // Start by showing the overlay... + showOverlay(); + + // ...and attempt to connect + StartWebsocket(callback); +} diff --git a/v2/internal/runtime/js/runtime/src/rollup.config.js b/v2/internal/runtime/js/runtime/src/rollup.config.js new file mode 100644 index 000000000..0c0dbf032 --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/rollup.config.js @@ -0,0 +1,35 @@ +import resolve from '@rollup/plugin-node-resolve'; +// import commonjs from '@rollup/plugin-commonjs'; +import svelte from 'rollup-plugin-svelte'; + +export default [ + // browser-friendly UMD build + { + input: 'main.js', + output: { + name: 'bridge', + file: '../bridge.js', + format: 'umd', + exports: "named" + }, + plugins: [ + svelte({ + // Optionally, preprocess components with svelte.preprocess: + // https://svelte.dev/docs#svelte_preprocess + // preprocess: { + // style: ({content}) => { + // return transformStyles(content); + // } + // }, + + // Emit CSS as "files" for other plugins to process. default is true + emitCss: false, + + }), + resolve({browser: true}), // so Rollup can find `ms` + // commonjs() // so Rollup can convert `ms` to an ES module + ] + }, + + +]; \ No newline at end of file diff --git a/v2/internal/runtime/js/runtime/src/store.js b/v2/internal/runtime/js/runtime/src/store.js new file mode 100644 index 000000000..52328255d --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/store.js @@ -0,0 +1,39 @@ + +import { writable } from 'svelte/store'; + +/** Overlay */ +export const overlayVisible = writable(false); + +export function showOverlay() { + overlayVisible.set(true); +} +export function hideOverlay() { + overlayVisible.set(false); +} + +/** Menubar **/ +export const menuVisible = writable(true); + +export function showMenuBar() { + menuVisible.set(true); +} +export function hideMenuBar() { + menuVisible.set(false); +} + +/** Trays **/ + +export const trays = writable([]); +export function setTray(tray) { + trays.update((current) => { + // Remove existing if it exists, else add + const index = current.findIndex(item => item.ID === tray.ID); + if ( index === -1 ) { + current.push(tray); + } else { + current[index] = tray; + } + return current; + }) +} + diff --git a/v2/internal/runtime/js/runtime/src/websocket.js b/v2/internal/runtime/js/runtime/src/websocket.js new file mode 100644 index 000000000..fffb54adf --- /dev/null +++ b/v2/internal/runtime/js/runtime/src/websocket.js @@ -0,0 +1,162 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The lightweight framework for web-like apps +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + + +import {setTray, hideOverlay, showOverlay} from "./store"; + +let websocket = null; +let callback = null; +let connectTimer; + +function log(message) { + // eslint-disable-next-line + console.log( + '%c wails bridge %c ' + message + ' ', + 'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem', + 'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem' + ); +} + +export function StartWebsocket(userCallback) { + + callback = userCallback; + + window.onbeforeunload = function() { + if( websocket ) { + websocket.onclose = function () { }; + websocket.close(); + websocket = null; + } + } + + // ...and attempt to connect + connect(); + +} + +function setupIPCBridge() { + // darwin + window.webkit = { + messageHandlers: { + external: { + postMessage: (message) => { + websocket.send(message); + } + } + } + }; +} + +// Handles incoming websocket connections +function handleConnect() { + log('Connected to backend'); + setupIPCBridge(); + hideOverlay(); + clearInterval(connectTimer); + websocket.onclose = handleDisconnect; + websocket.onmessage = handleMessage; +} + +// Handles websocket disconnects +function handleDisconnect() { + log('Disconnected from backend'); + websocket = null; + showOverlay(); + connect(); +} + +// Try to connect to the backend every 1s (default value). +function connect() { + connectTimer = setInterval(function () { + if (websocket == null) { + websocket = new WebSocket('ws://' + window.location.hostname + ':34115/bridge'); + websocket.onopen = handleConnect; + websocket.onerror = function (e) { + e.stopImmediatePropagation(); + e.stopPropagation(); + e.preventDefault(); + websocket = null; + return false; + }; + } + }, 1000); +} + +// Adds a script to the Dom. +// Removes it if second parameter is true. +function addScript(script, remove) { + const s = document.createElement('script'); + s.setAttribute('type', 'text/javascript'); + s.textContent = script; + document.head.appendChild(s); + + // Remove internal messages from the DOM + if (remove) { + s.parentNode.removeChild(s); + } +} + +function handleMessage(message) { + // As a bridge we ignore js and css injections + switch (message.data[0]) { + // Wails library - inject! + case 'b': + message = message.data.slice(1) + addScript(message); + log('Loaded Wails Runtime'); + + // We need to now send a message to the backend telling it + // we have loaded (System Start) + window.webkit.messageHandlers.external.postMessage("SS"); + + // Now wails runtime is loaded, wails for the ready event + // and callback to the main app + // window.wails.Events.On('wails:loaded', function () { + if (callback) { + log('Notifying application'); + callback(window.wails); + } + // }); + break; + // // Notifications + // case 'n': + // addScript(message.data.slice(1), true); + // break; + // // Binding + // case 'b': + // const binding = message.data.slice(1); + // //log("Binding: " + binding) + // window.wails._.NewBinding(binding); + // break; + // // Call back + case 'c': + const callbackData = message.data.slice(1); + window.wails._.Callback(callbackData); + break; + // Tray + case 'T': + const trayMessage = message.data.slice(1); + switch (trayMessage[0]) { + case 'S': + // Set tray + const trayJSON = trayMessage.slice(1); + let tray = JSON.parse(trayJSON) + setTray(tray) + break + default: + log('Unknown tray message: ' + message.data); + } + break; + + default: + log('Unknown message: ' + message.data); + } +}