Fenster eingebaut
This commit is contained in:
parent
f7a24eb969
commit
830411664c
|
@ -12,7 +12,7 @@ TODO::
|
|||
* ~~Logout~~
|
||||
* Konfis auf der Frontseite mit Websockets updaten
|
||||
* ~~Mode hinzufügen: in der Config soll man zwischen Gemeinden und Konfis umschalten können, so dass entweder die Gemeinden oder Konfis gegeneinander spielen~~
|
||||
* Alles in Fenster packen
|
||||
* ~~Alles in Fenster packen~~
|
||||
* Alles an schriften/Bildern/Etc Herunterladen, damit alles auch offline funktioniert
|
||||
* ~~Random Port (wenn man irgendwie den laufenden port finden kann)~~
|
||||
* ~~Front-Tabelle schöner~~
|
||||
|
@ -20,3 +20,8 @@ TODO::
|
|||
* Mini-Doku
|
||||
* Inbetriebnahme
|
||||
* Bedienung
|
||||
|
||||
Release: 3 Zips. Enthalten jeweils die kompilierten binaries, Templates, JS/CSS/Fonts/etc. Sozusagen Ready-to-Run.
|
||||
* Win
|
||||
* macOS
|
||||
* Linux
|
|
@ -1,6 +1,11 @@
|
|||
$(document).ready(function () {
|
||||
$('#loginform').submit(function (ev) {
|
||||
ev.preventDefault();
|
||||
login();
|
||||
});
|
||||
});
|
||||
|
||||
function login() {
|
||||
console.log('sumbit');
|
||||
var pass = $('#password').val();
|
||||
if(pass == ""){
|
||||
|
@ -21,6 +26,4 @@ $(document).ready(function () {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,4 @@
|
|||
.idea/
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Quentin RENARD
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,277 @@
|
|||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
const {app, BrowserWindow, ipcMain, Menu, MenuItem, Tray} = electron
|
||||
const consts = require('./src/consts.js')
|
||||
const client = require('./src/client.js').init()
|
||||
const rl = require('readline').createInterface({input: client.socket})
|
||||
|
||||
let elements = {}
|
||||
let menus = {}
|
||||
|
||||
// App is ready
|
||||
app.on('ready',() => {
|
||||
// Init
|
||||
const screen = electron.screen
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
// Listen to screen events
|
||||
screen.on('display-added', function() {
|
||||
client.write(consts.mainTargetID, consts.eventNames.displayEventAdded, {displays: {all: screen.getAllDisplays(), primary: screen.getPrimaryDisplay()}})
|
||||
})
|
||||
screen.on('display-metrics-changed', function() {
|
||||
client.write(consts.mainTargetID, consts.eventNames.displayEventMetricsChanged, {displays: {all: screen.getAllDisplays(), primary: screen.getPrimaryDisplay()}})
|
||||
})
|
||||
screen.on('display-removed', function() {
|
||||
client.write(consts.mainTargetID, consts.eventNames.displayEventRemoved, {displays: {all: screen.getAllDisplays(), primary: screen.getPrimaryDisplay()}})
|
||||
})
|
||||
|
||||
// Listen on main ipcMain
|
||||
ipcMain.on(consts.eventNames.ipcWindowMessage, (event, arg) => {
|
||||
client.write(arg.targetID, consts.eventNames.windowEventMessage, {message: arg.message})
|
||||
})
|
||||
|
||||
// Read from client
|
||||
rl.on('line', function(line){
|
||||
// Parse the JSON
|
||||
var json = JSON.parse(line)
|
||||
|
||||
// Switch on event name
|
||||
switch (json.name) {
|
||||
// Menu
|
||||
case consts.eventNames.menuCmdCreate:
|
||||
menuCreate(json.menu)
|
||||
menus[json.menu.rootId] = json.targetID
|
||||
setMenu(json.menu.rootId)
|
||||
client.write(json.targetID, consts.eventNames.menuEventCreated)
|
||||
break;
|
||||
case consts.eventNames.menuCmdDestroy:
|
||||
elements[json.targetID] = null
|
||||
if (menus[json.menu.rootId] == json.targetID) {
|
||||
menus[json.menu.rootId] = null
|
||||
setMenu(json.menu.rootId)
|
||||
}
|
||||
client.write(json.targetID, consts.eventNames.menuEventDestroyed)
|
||||
break;
|
||||
|
||||
// Menu item
|
||||
case consts.eventNames.menuItemCmdSetChecked:
|
||||
elements[json.targetID].checked = json.menuItemOptions.checked
|
||||
client.write(json.targetID, consts.eventNames.menuItemEventCheckedSet)
|
||||
break;
|
||||
case consts.eventNames.menuItemCmdSetEnabled:
|
||||
elements[json.targetID].enabled = json.menuItemOptions.enabled
|
||||
client.write(json.targetID, consts.eventNames.menuItemEventEnabledSet)
|
||||
break;
|
||||
case consts.eventNames.menuItemCmdSetLabel:
|
||||
elements[json.targetID].label = json.menuItemOptions.label
|
||||
client.write(json.targetID, consts.eventNames.menuItemEventLabelSet)
|
||||
break;
|
||||
case consts.eventNames.menuItemCmdSetVisible:
|
||||
elements[json.targetID].visible = json.menuItemOptions.visible
|
||||
client.write(json.targetID, consts.eventNames.menuItemEventVisibleSet)
|
||||
break;
|
||||
|
||||
// Sub menu
|
||||
case consts.eventNames.subMenuCmdAppend:
|
||||
elements[json.targetID].append(menuItemCreate(json.menuItem))
|
||||
setMenu(json.menuItem.rootId)
|
||||
client.write(json.targetID, consts.eventNames.subMenuEventAppended)
|
||||
break;
|
||||
case consts.eventNames.subMenuCmdClosePopup:
|
||||
var window = null
|
||||
if (typeof json.windowId !== "undefined") {
|
||||
window = elements[json.windowId]
|
||||
}
|
||||
elements[json.targetID].closePopup(window)
|
||||
client.write(json.targetID, consts.eventNames.subMenuEventClosedPopup)
|
||||
break;
|
||||
case consts.eventNames.subMenuCmdInsert:
|
||||
elements[json.targetID].insert(json.menuItemPosition, menuItemCreate(json.menuItem))
|
||||
setMenu(json.menuItem.rootId)
|
||||
client.write(json.targetID, consts.eventNames.subMenuEventInserted)
|
||||
break;
|
||||
case consts.eventNames.subMenuCmdPopup:
|
||||
var window = null
|
||||
if (typeof json.windowId !== "undefined") {
|
||||
window = elements[json.windowId]
|
||||
}
|
||||
json.menuPopupOptions.async = true
|
||||
elements[json.targetID].popup(window, json.menuPopupOptions)
|
||||
client.write(json.targetID, consts.eventNames.subMenuEventPoppedUp)
|
||||
break;
|
||||
|
||||
// Tray
|
||||
case consts.eventNames.trayCmdCreate:
|
||||
elements[json.targetID] = new Tray(json.trayOptions.image)
|
||||
if (typeof json.menuId !== "undefined") {
|
||||
elements[json.targetID].setContextMenu(elements[json.menuId]);
|
||||
}
|
||||
if (typeof json.trayOptions.tooltip !== "undefined") {
|
||||
elements[json.targetID].setToolTip(json.trayOptions.tooltip);
|
||||
}
|
||||
client.write(json.targetID, consts.eventNames.trayEventCreated)
|
||||
break;
|
||||
case consts.eventNames.trayCmdDestroy:
|
||||
elements[json.targetID].destroy()
|
||||
elements[json.targetID] = null
|
||||
client.write(json.targetID, consts.eventNames.trayEventDestroyed)
|
||||
break;
|
||||
|
||||
// Window
|
||||
case consts.eventNames.windowCmdBlur:
|
||||
elements[json.targetID].blur()
|
||||
break;
|
||||
case consts.eventNames.windowCmdCenter:
|
||||
elements[json.targetID].center()
|
||||
break;
|
||||
case consts.eventNames.windowCmdClose:
|
||||
elements[json.targetID].close()
|
||||
break;
|
||||
case consts.eventNames.windowCmdCreate:
|
||||
windowCreate(json)
|
||||
break;
|
||||
case consts.eventNames.windowCmdDestroy:
|
||||
elements[json.targetID].destroy()
|
||||
elements[json.targetID] = null
|
||||
break;
|
||||
case consts.eventNames.windowCmdFocus:
|
||||
elements[json.targetID].focus()
|
||||
break;
|
||||
case consts.eventNames.windowCmdHide:
|
||||
elements[json.targetID].hide()
|
||||
break;
|
||||
case consts.eventNames.windowCmdMaximize:
|
||||
elements[json.targetID].maximize()
|
||||
break;
|
||||
case consts.eventNames.windowCmdMessage:
|
||||
elements[json.targetID].webContents.send(consts.eventNames.ipcWindowMessage, json.message)
|
||||
break;
|
||||
case consts.eventNames.windowCmdMinimize:
|
||||
elements[json.targetID].minimize()
|
||||
break;
|
||||
case consts.eventNames.windowCmdMove:
|
||||
elements[json.targetID].setPosition(json.windowOptions.x, json.windowOptions.y, true)
|
||||
break;
|
||||
case consts.eventNames.windowCmdResize:
|
||||
elements[json.targetID].setSize(json.windowOptions.width, json.windowOptions.height, true)
|
||||
break;
|
||||
case consts.eventNames.windowCmdRestore:
|
||||
elements[json.targetID].restore()
|
||||
break;
|
||||
case consts.eventNames.windowCmdShow:
|
||||
elements[json.targetID].show()
|
||||
break;
|
||||
case consts.eventNames.windowCmdWebContentsCloseDevTools:
|
||||
elements[json.targetID].webContents.closeDevTools()
|
||||
break;
|
||||
case consts.eventNames.windowCmdWebContentsOpenDevTools:
|
||||
elements[json.targetID].webContents.openDevTools()
|
||||
break;
|
||||
case consts.eventNames.windowCmdUnmaximize:
|
||||
elements[json.targetID].unmaximize()
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
// Send electron.ready event
|
||||
client.write(consts.mainTargetID, consts.eventNames.appEventReady, {displays: {all: screen.getAllDisplays(), primary: screen.getPrimaryDisplay()}})
|
||||
})
|
||||
|
||||
// menuCreate creates a new menu
|
||||
function menuCreate(menu) {
|
||||
if (typeof menu !== "undefined") {
|
||||
elements[menu.id] = new Menu()
|
||||
for(var i = 0; i < menu.items.length; i++) {
|
||||
elements[menu.id].append(menuItemCreate(menu.items[i]))
|
||||
}
|
||||
return elements[menu.id]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// menuItemCreate creates a menu item
|
||||
function menuItemCreate(menuItem) {
|
||||
const itemId = menuItem.id
|
||||
menuItem.options.click = function(menuItem) {
|
||||
client.write(itemId, consts.eventNames.menuItemEventClicked, {menuItemOptions: menuItemToJSON(menuItem)})
|
||||
}
|
||||
if (typeof menuItem.submenu !== "undefined") {
|
||||
menuItem.options.type = 'submenu'
|
||||
menuItem.options.submenu = menuCreate(menuItem.submenu)
|
||||
}
|
||||
elements[itemId] = new MenuItem(menuItem.options)
|
||||
return elements[itemId]
|
||||
}
|
||||
|
||||
// menuItemToJSON returns the proper fields not to raise an exception
|
||||
function menuItemToJSON(menuItem) {
|
||||
return {
|
||||
checked: menuItem.checked,
|
||||
enabled: menuItem.enabled,
|
||||
label: menuItem.label,
|
||||
visible: menuItem.visible,
|
||||
}
|
||||
}
|
||||
|
||||
// setMenu sets a menu
|
||||
function setMenu(rootId) {
|
||||
var menu = null
|
||||
if (typeof menus[rootId] !== "undefined" && typeof elements[menus[rootId]] !== "undefined") {
|
||||
menu = elements[menus[rootId]]
|
||||
}
|
||||
rootId == consts.mainTargetID ? Menu.setApplicationMenu(menu) : elements[rootId].setMenu(menu)
|
||||
}
|
||||
|
||||
// windowCreate creates a new window
|
||||
function windowCreate(json) {
|
||||
elements[json.targetID] = new BrowserWindow(json.windowOptions)
|
||||
elements[json.targetID].setMenu(null)
|
||||
elements[json.targetID].loadURL(json.url);
|
||||
elements[json.targetID].on('blur', () => { client.write(json.targetID, consts.eventNames.windowEventBlur) })
|
||||
elements[json.targetID].on('closed', () => {
|
||||
client.write(json.targetID, consts.eventNames.windowEventClosed)
|
||||
delete elements[json.targetID]
|
||||
})
|
||||
elements[json.targetID].on('focus', () => { client.write(json.targetID, consts.eventNames.windowEventFocus) })
|
||||
elements[json.targetID].on('hide', () => { client.write(json.targetID, consts.eventNames.windowEventHide) })
|
||||
elements[json.targetID].on('maximize', () => { client.write(json.targetID, consts.eventNames.windowEventMaximize) })
|
||||
elements[json.targetID].on('minimize', () => { client.write(json.targetID, consts.eventNames.windowEventMinimize) })
|
||||
elements[json.targetID].on('move', () => { client.write(json.targetID, consts.eventNames.windowEventMove) })
|
||||
elements[json.targetID].on('ready-to-show', () => { client.write(json.targetID, consts.eventNames.windowEventReadyToShow) })
|
||||
elements[json.targetID].on('resize', () => { client.write(json.targetID, consts.eventNames.windowEventResize) })
|
||||
elements[json.targetID].on('restore', () => { client.write(json.targetID, consts.eventNames.windowEventRestore) })
|
||||
elements[json.targetID].on('show', () => { client.write(json.targetID, consts.eventNames.windowEventShow) })
|
||||
elements[json.targetID].on('unmaximize', () => { client.write(json.targetID, consts.eventNames.windowEventUnmaximize) })
|
||||
elements[json.targetID].on('unresponsive', () => { client.write(json.targetID, consts.eventNames.windowEventUnresponsive) })
|
||||
elements[json.targetID].webContents.on('did-finish-load', () => {
|
||||
elements[json.targetID].webContents.executeJavaScript(
|
||||
`const {ipcRenderer} = require('electron')
|
||||
const {dialog} = require('electron').remote
|
||||
var astilectron = {
|
||||
listen: function(callback) {
|
||||
ipcRenderer.on('`+ consts.eventNames.ipcWindowMessage +`', function(event, message) {
|
||||
callback(message)
|
||||
})
|
||||
},
|
||||
send: function(message) {
|
||||
ipcRenderer.send('`+ consts.eventNames.ipcWindowMessage +`', {message: message, targetID: '`+ json.targetID +`'})
|
||||
},
|
||||
showErrorBox: function(title, content) {
|
||||
dialog.showErrorBox(title, content)
|
||||
},
|
||||
showMessageBox: function(options, callback) {
|
||||
dialog.showMessageBox(null, options, callback)
|
||||
},
|
||||
showOpenDialog: function(options, callback) {
|
||||
dialog.showOpenDialog(null, options, callback)
|
||||
},
|
||||
showSaveDialog: function(options, callback) {
|
||||
dialog.showSaveDialog(null, options, callback)
|
||||
}
|
||||
}
|
||||
document.dispatchEvent(new Event('astilectron-ready'))`
|
||||
)
|
||||
client.write(json.targetID, consts.eventNames.windowEventDidFinishLoad)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Astilectron",
|
||||
"description": "Electron-based cross-language application framework",
|
||||
"devDependencies": {
|
||||
"electron": "1.6.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
`astilectron` is an Electron app that provides an API over a TCP socket that allows executing Electron's method as well as capturing Electron's events.
|
||||
|
||||
# Architecture
|
||||
|
||||
+-----------------------+ TCP +-------------+ IPC +---------------------+
|
||||
+ Client App (any Lang) |<--------->+ Astilectron +<-------->+ win1: (HTML/JS/CSS) +
|
||||
+-----------------------+ +-------------+ | +---------------------++
|
||||
| | +---->+ win2: (HTML/JS/CSS) +
|
||||
| +----------+ | | +---------------------++
|
||||
+---------+ Electron +--------+ +-->+ win3: (HTML/JS/CSS) +
|
||||
+----------+ +---------------------+
|
||||
|
||||
# Language bindings
|
||||
|
||||
Language bindings play a major role with `astilectron` as they allow communicating with its TCP socket and therefore interacting with its API in any language.
|
||||
|
||||
## I want to develop language bindings for a new language
|
||||
|
||||
Great! :)
|
||||
|
||||
Here's a few things you need to know:
|
||||
|
||||
- it's the responsibility of the language bindings to provision `astilectron` which usually consists of downloading and unzipping `astilectron` as well as `electron`
|
||||
- the TCP addr is sent to `astilectron` through a command line argument therefore your command line when calling `astilectron` should look like `<path to electron executable> <path to astilectron directory>/main.js <tcp addr>`
|
||||
|
||||
## Language bindings for GO
|
||||
|
||||
Check out [go-astilectron](https://github.com/asticode/go-astilectron) for `astilectron` GO language bindings
|
||||
|
||||
# Features and roadmap
|
||||
|
||||
- [x] window basic methods (create, show, close, resize, minimize, maximize, ...)
|
||||
- [x] window basic events (close, blur, focus, unresponsive, crashed, ...)
|
||||
- [x] remote messaging (messages between GO and the JS in the webserver)
|
||||
- [x] multi screens/displays
|
||||
- [x] menu methods and events (create, insert, append, popup, clicked, ...)
|
||||
- [x] dialogs (open or save file, alerts, ...)
|
||||
- [ ] accelerators (shortcuts)
|
||||
- [ ] file methods (drag & drop, ...)
|
||||
- [ ] clipboard methods
|
||||
- [ ] power monitor events (suspend, resume, ...)
|
||||
- [ ] notifications (macosx)
|
||||
- [ ] desktop capturer (audio and video)
|
||||
- [ ] session methods
|
||||
- [ ] session events
|
||||
- [ ] window advanced options (add missing ones)
|
||||
- [ ] window advanced methods (add missing ones)
|
||||
- [ ] window advanced events (add missing ones)
|
||||
- [ ] child windows
|
||||
|
||||
# Contribute
|
||||
|
||||
For now only GO has its official bindings with `astilectron`, but the more language has its bindings the better! Therefore if you feel like implementing bindings with `astilectron` in some other language feel free to reach out to me to get some more info and finally to get your repo listed here.
|
||||
|
||||
Also I'm far from being an expert in Node.JS therefore if you see anything that seems wrong in `astilectron` feel free to create an issue or even better contribute through a PR!
|
||||
|
||||
You know you want to! :D
|
||||
|
||||
# Cheers to
|
||||
|
||||
[thrust](https://github.com/breach/thrust) which is awesome but unfortunately not maintained anymore. It inspired this project.
|
|
@ -0,0 +1,27 @@
|
|||
'use strict'
|
||||
|
||||
const net = require('net');
|
||||
const url = require('url');
|
||||
|
||||
// Client can read/write messages from a TCP server
|
||||
class Client {
|
||||
// init initializes the Client
|
||||
init() {
|
||||
var u = url.parse("tcp://" + process.argv[2], false, false)
|
||||
this.socket = new net.Socket()
|
||||
this.socket.connect(u.port, u.hostname, function() {});
|
||||
this.socket.on('close', function() {
|
||||
process.exit()
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
// write writes an event to the server
|
||||
write(targetID, eventName, payload) {
|
||||
var data = {name: eventName, targetID: targetID}
|
||||
if (typeof payload != "undefined") Object.assign(data, payload)
|
||||
this.socket.write(JSON.stringify(data) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Client()
|
|
@ -0,0 +1,69 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
eventNames: {
|
||||
appEventReady: "app.event.ready",
|
||||
displayEventAdded: "display.event.added",
|
||||
displayEventMetricsChanged: "display.event.metrics.changed",
|
||||
displayEventRemoved: "display.event.removed",
|
||||
ipcWindowMessage: "ipc.window.message",
|
||||
menuCmdCreate: "menu.cmd.create",
|
||||
menuCmdDestroy: "menu.cmd.destroy",
|
||||
menuEventCreated: "menu.event.created",
|
||||
menuEventDestroyed: "menu.event.destroyed",
|
||||
menuItemCmdSetChecked: "menu.item.cmd.set.checked",
|
||||
menuItemCmdSetEnabled: "menu.item.cmd.set.enabled",
|
||||
menuItemCmdSetLabel: "menu.item.cmd.set.label",
|
||||
menuItemCmdSetVisible: "menu.item.cmd.set.visible",
|
||||
menuItemEventCheckedSet: "menu.item.event.checked.set",
|
||||
menuItemEventClicked: "menu.item.event.clicked",
|
||||
menuItemEventEnabledSet: "menu.item.event.enabled.set",
|
||||
menuItemEventLabelSet: "menu.item.event.label.set",
|
||||
menuItemEventVisibleSet: "menu.item.event.visible.set",
|
||||
subMenuCmdAppend: "sub.menu.cmd.append",
|
||||
subMenuCmdClosePopup: "sub.menu.cmd.close.popup",
|
||||
subMenuCmdInsert: "sub.menu.cmd.insert",
|
||||
subMenuCmdPopup: "sub.menu.cmd.popup",
|
||||
subMenuEventAppended: "sub.menu.event.appended",
|
||||
subMenuEventClosedPopup: "sub.menu.event.closed.popup",
|
||||
subMenuEventInserted: "sub.menu.event.inserted",
|
||||
subMenuEventPoppedUp: "sub.menu.event.popped.up",
|
||||
trayCmdCreate: "tray.cmd.create",
|
||||
trayCmdDestroy: "tray.cmd.destroy",
|
||||
trayEventCreated: "tray.event.created",
|
||||
trayEventDestroyed: "tray.event.destroyed",
|
||||
windowCmdBlur: "window.cmd.blur",
|
||||
windowCmdCenter: "window.cmd.center",
|
||||
windowCmdClose: "window.cmd.close",
|
||||
windowCmdCreate: "window.cmd.create",
|
||||
windowCmdDestroy: "window.cmd.destroy",
|
||||
windowCmdFocus: "window.cmd.focus",
|
||||
windowCmdHide: "window.cmd.hide",
|
||||
windowCmdMaximize: "window.cmd.maximize",
|
||||
windowCmdMessage: "window.cmd.message",
|
||||
windowCmdMinimize: "window.cmd.minimize",
|
||||
windowCmdMove: "window.cmd.move",
|
||||
windowCmdResize: "window.cmd.resize",
|
||||
windowCmdRestore: "window.cmd.restore",
|
||||
windowCmdShow: "window.cmd.show",
|
||||
windowCmdUnmaximize: "window.cmd.unmaximize",
|
||||
windowCmdWebContentsCloseDevTools: "window.cmd.web.contents.close.dev.tools",
|
||||
windowCmdWebContentsOpenDevTools: "window.cmd.web.contents.open.dev.tools",
|
||||
windowEventBlur: "window.event.blur",
|
||||
windowEventClosed: "window.event.closed",
|
||||
windowEventDidFinishLoad: "window.event.did.finish.load",
|
||||
windowEventFocus: "window.event.focus",
|
||||
windowEventHide: "window.event.hide",
|
||||
windowEventMaximize: "window.event.maximize",
|
||||
windowEventMessage: "window.event.message",
|
||||
windowEventMinimize: "window.event.minimize",
|
||||
windowEventMove: "window.event.move",
|
||||
windowEventReadyToShow: "window.event.ready.to.show",
|
||||
windowEventResize: "window.event.resize",
|
||||
windowEventRestore: "window.event.restore",
|
||||
windowEventShow: "window.event.show",
|
||||
windowEventUnmaximize: "window.event.unmaximize",
|
||||
windowEventUnresponsive: "window.event.unresponsive",
|
||||
},
|
||||
mainTargetID: 'main'
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2013-2017 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
v1.6.5
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
{"astilectron":{"version":"0.6.0"},"electron":{"version":"1.6.5"}}
|
|
@ -6,7 +6,7 @@ AdminPassword = geheim
|
|||
Mode = 1
|
||||
|
||||
; Serverkram
|
||||
; Das Interface inkl. Port, auf dem der Webserver läuft
|
||||
; Das Interface inkl. Port, auf dem der Webserver läuft. Muss komplett mit IP-Adresse vorhanden sein.
|
||||
Interface = 127.0.0.1:8080
|
||||
; Hier wird die Datenbank gespeichert
|
||||
DBFile = ./data.db
|
|
@ -26,6 +26,8 @@ import:
|
|||
- package: google.golang.org/api
|
||||
subpackages:
|
||||
- compute/v1
|
||||
- package: github.com/asticode/go-astilectron
|
||||
version: ~0.5.0
|
||||
testImport:
|
||||
- package: github.com/denisenkom/go-mssqldb
|
||||
- package: github.com/smartystreets/goconvey
|
||||
|
|
4
login.go
4
login.go
|
@ -19,6 +19,10 @@ func login(c echo.Context) error {
|
|||
//Wenn das password stimmt, einloggen
|
||||
if SiteConf.AdminPassword == pass {
|
||||
sess.Set("login", true)
|
||||
direct := c.QueryParam("direct")
|
||||
if direct == "true" {
|
||||
return c.Redirect(http.StatusSeeOther, "/admin")
|
||||
}
|
||||
return c.JSON(http.StatusOK, Message{"success"})
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, Message{"fail"})
|
||||
|
|
106
main.go
106
main.go
|
@ -6,9 +6,14 @@ import (
|
|||
"github.com/labstack/gommon/log"
|
||||
"html/template"
|
||||
|
||||
|
||||
"fmt"
|
||||
"github.com/astaxie/session"
|
||||
_ "github.com/astaxie/session/providers/memory"
|
||||
"github.com/asticode/go-astilectron"
|
||||
"github.com/asticode/go-astilog"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Initialize Session
|
||||
|
@ -85,5 +90,104 @@ func main() {
|
|||
|
||||
//Start the server
|
||||
e.Logger.SetLevel(log.ERROR)
|
||||
e.Start(SiteConf.Interface)
|
||||
go e.Start(SiteConf.Interface)
|
||||
|
||||
//Windows
|
||||
// Create astilectron
|
||||
var a *astilectron.Astilectron
|
||||
var err error
|
||||
if a, err = astilectron.New(astilectron.Options{
|
||||
AppName: "Astilectron",
|
||||
AppIconDefaultPath: "/gopher.png",
|
||||
AppIconDarwinPath: "/gopher.icns",
|
||||
BaseDirectoryPath: "astilectron-deps",
|
||||
}); err != nil {
|
||||
fmt.Println(err)
|
||||
astilog.Fatal(errors.Wrap(err, "creating new astilectron failed"))
|
||||
}
|
||||
defer a.Close()
|
||||
a.HandleSignals()
|
||||
|
||||
// Start
|
||||
if err = a.Start(); err != nil {
|
||||
fmt.Println(err)
|
||||
astilog.Fatal(errors.Wrap(err, "starting failed"))
|
||||
}
|
||||
|
||||
// Create Admin window
|
||||
var wAdmin *astilectron.Window
|
||||
if wAdmin, err = a.NewWindow("http://" + SiteConf.Interface + "/admin", &astilectron.WindowOptions{
|
||||
Center: astilectron.PtrBool(true),
|
||||
Height: astilectron.PtrInt(800),
|
||||
Width: astilectron.PtrInt(1200),
|
||||
}); err != nil {
|
||||
fmt.Println(err)
|
||||
astilog.Fatal(errors.Wrap(err, "new window failed"))
|
||||
}
|
||||
if err = wAdmin.Create(); err != nil {
|
||||
fmt.Println(err)
|
||||
astilog.Fatal(errors.Wrap(err, "creating window failed"))
|
||||
}
|
||||
|
||||
// Create Frontend window
|
||||
var wFrontend *astilectron.Window
|
||||
if wFrontend, err = a.NewWindow("http://" + SiteConf.Interface, &astilectron.WindowOptions{
|
||||
Center: astilectron.PtrBool(true),
|
||||
Height: astilectron.PtrInt(800),
|
||||
Width: astilectron.PtrInt(1200),
|
||||
}); err != nil {
|
||||
fmt.Println(err)
|
||||
astilog.Fatal(errors.Wrap(err, "new window failed"))
|
||||
}
|
||||
if err = wFrontend.Create(); err != nil {
|
||||
fmt.Println(err)
|
||||
astilog.Fatal(errors.Wrap(err, "creating window failed"))
|
||||
}
|
||||
|
||||
// If several displays, move the window to the second display
|
||||
var displays = a.Displays()
|
||||
if len(displays) > 1 {
|
||||
time.Sleep(time.Second)
|
||||
wFrontend.MoveInDisplay(displays[1], 50, 50)
|
||||
wFrontend.Maximize()
|
||||
}
|
||||
|
||||
// Blocking pattern
|
||||
a.Wait()
|
||||
|
||||
|
||||
// Initialize astilectron
|
||||
/*var a, _ = astilectron.New(astilectron.Options{
|
||||
AppName: "Konfi@Castle Kasino Kasse",
|
||||
AppIconDefaultPath: "<your .png icon>",
|
||||
AppIconDarwinPath: "<your .icns icon>",
|
||||
BaseDirectoryPath: "<where you want the provisioner to install the dependencies>",
|
||||
})
|
||||
defer a.Close()
|
||||
|
||||
// Start astilectron
|
||||
a.Start()
|
||||
|
||||
// Create a new window
|
||||
var wAdmin, _ = a.NewWindow("http://" + SiteConf.Interface + "/admin", &astilectron.WindowOptions{
|
||||
Center: astilectron.PtrBool(true),
|
||||
Height: astilectron.PtrInt(800),
|
||||
Width: astilectron.PtrInt(1300),
|
||||
})
|
||||
wAdmin.Create()
|
||||
|
||||
// Create a new window
|
||||
var wFrontend, _ = a.NewWindow("http://" + SiteConf.Interface, &astilectron.WindowOptions{
|
||||
Center: astilectron.PtrBool(true),
|
||||
Height: astilectron.PtrInt(600),
|
||||
Width: astilectron.PtrInt(600),
|
||||
})
|
||||
wFrontend.Create()
|
||||
|
||||
// If several displays, move the window to the second display
|
||||
var displays = a.Displays()
|
||||
if len(displays) > 1 {
|
||||
wFrontend.MoveInDisplay(displays[1], 50, 50)
|
||||
wFrontend.Maximize()
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Kasino Admin</title>
|
||||
<link rel="stylesheet" type="text/css" href="/assets/semantic/semantic.min.css">
|
||||
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
<script src="/assets/js/jquery-3.1.1.min.js"></script>
|
||||
<script>if (window.module) module = window.module;</script>
|
||||
<script src="/assets/semantic/semantic.min.js"></script>
|
||||
</head>
|
||||
{{if .Loggedin}}
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Kasino Admin</title>
|
||||
<link rel="stylesheet" type="text/css" href="/assets/semantic/semantic.min.css">
|
||||
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
<script src="/assets/js/jquery-3.1.1.min.js"></script>
|
||||
<script>if (window.module) module = window.module;</script>
|
||||
<script src="/assets/semantic/semantic.min.js"></script>
|
||||
</head>
|
||||
{{if .Loggedin}}
|
||||
|
|
|
@ -31,7 +31,9 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
<script src="/assets/js/jquery-3.1.1.min.js"></script>
|
||||
<script>if (window.module) module = window.module;</script>
|
||||
<script src="/assets/js/load.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Kasino Admin</title>
|
||||
<link rel="stylesheet" type="text/css" href="/assets/semantic/semantic.min.css">
|
||||
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
<script src="/assets/js/jquery-3.1.1.min.js"></script>
|
||||
<script>if (window.module) module = window.module;</script>
|
||||
<script src="/assets/semantic/semantic.min.js"></script>
|
||||
</head>
|
||||
<body style="background: url(/assets/bg.jpg) no-repeat center fixed">
|
||||
|
@ -14,7 +17,7 @@
|
|||
<h2 class="ui header">
|
||||
Kasino Admin
|
||||
</h2>
|
||||
<form class="ui large form" id="loginform" method="post">
|
||||
<form class="ui large form" id="loginform" method="post" onsubmit="login();return false;" action="/login?direct=true">
|
||||
<div class="ui segment">
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
cover.*
|
||||
examples/5.single_binary_distribution/vendor.go
|
||||
examples/test
|
||||
examples/vendor
|
||||
testdata/tmp/*
|
|
@ -0,0 +1,9 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.8
|
||||
- tip
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
script:
|
||||
go test -v
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Quentin RENARD
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,646 @@
|
|||
[![GoReportCard](http://goreportcard.com/badge/github.com/asticode/go-astilectron)](http://goreportcard.com/report/github.com/asticode/go-astilectron)
|
||||
[![GoDoc](https://godoc.org/github.com/asticode/go-astilectron?status.svg)](https://godoc.org/github.com/asticode/go-astilectron)
|
||||
[![GoCoverage](https://cover.run/go/github.com/asticode/go-astilectron.svg)](https://cover.run/go/github.com/asticode/go-astilectron)
|
||||
[![Travis](https://travis-ci.org/asticode/go-astilectron.svg?branch=master)](https://travis-ci.org/asticode/go-astilectron#)
|
||||
|
||||
Thanks to `go-astilectron` build cross platform GUI apps with GO and HTML/JS/CSS. It is the official GO bindings of [astilectron](https://github.com/asticode/astilectron) and is powered by [Electron](https://github.com/electron/electron).
|
||||
|
||||
# Real-life examples
|
||||
|
||||
Here's a list of awesome projects using `go-astilectron` (if you're using `go-astilectron` and want your project to be listed here please submit a PR):
|
||||
|
||||
- [go-astivid](https://github.com/asticode/go-astivid) Video tools written in GO
|
||||
- [GroupMatcher](https://github.com/veecue/GroupMatcher) Program to allocate persons to groups while trying to fulfill all the given wishes as good as possible
|
||||
|
||||
# Quick start
|
||||
|
||||
WARNING: the code below doesn't handle errors for readibility purposes. However you SHOULD!
|
||||
|
||||
### Import `go-astilectron`
|
||||
|
||||
To import `go-astilectron` run:
|
||||
|
||||
$ go get -u github.com/asticode/go-astilectron
|
||||
|
||||
### Start `go-astilectron`
|
||||
|
||||
```go
|
||||
// Initialize astilectron
|
||||
var a, _ = astilectron.New(astilectron.Options{
|
||||
AppName: "<your app name>",
|
||||
AppIconDefaultPath: "<your .png icon>",
|
||||
AppIconDarwinPath: "<your .icns icon>",
|
||||
BaseDirectoryPath: "<where you want the provisioner to install the dependencies>",
|
||||
})
|
||||
defer a.Close()
|
||||
|
||||
// Start astilectron
|
||||
a.Start()
|
||||
```
|
||||
|
||||
For everything to work properly we need to fetch 2 dependencies : [astilectron](https://github.com/asticode/astilectron) and [Electron](https://github.com/electron/electron). `.Start()` takes care of it by downloading the sources and setting them up properly.
|
||||
|
||||
In case you want to embed the sources in the binary to keep a unique binary you can use the **NewDisembedderProvisioner** function to get the proper **Provisioner** and attach it to `go-astilectron` with `.SetProvisioner(p Provisioner)`. Check out the [example](https://github.com/asticode/go-astilectron/tree/master/examples/5.single_binary_distribution/main.go) to see how to use it with [go-bindata](https://github.com/jteeuwen/go-bindata).
|
||||
|
||||
Beware when trying to add your own app icon as you'll need 2 icons : one compatible with MacOSX (.icns) and one compatible with the rest (.png for instance).
|
||||
|
||||
If no BaseDirectoryPath is provided, it defaults to the executable's directory path.
|
||||
|
||||
The majority of methods are synchrone which means that when executing them `go-astilectron` will block until it receives a specific Electron event or until the overall context is cancelled. This is the case of `.Start()` which will block until it receives the `app.event.ready` `astilectron` event or until the overall context is cancelled.
|
||||
|
||||
### Create a window
|
||||
|
||||
```go
|
||||
// Create a new window
|
||||
var w, _ = a.NewWindow("http://127.0.0.1:4000", &astilectron.WindowOptions{
|
||||
Center: astilectron.PtrBool(true),
|
||||
Height: astilectron.PtrInt(600),
|
||||
Width: astilectron.PtrInt(600),
|
||||
})
|
||||
w.Create()
|
||||
```
|
||||
|
||||
When creating a window you need to indicate a URL as well as options such as position, size, etc.
|
||||
|
||||
This is pretty straightforward except the `astilectron.Ptr*` methods so let me explain: GO doesn't do optional fields when json encoding unless you use pointers whereas Electron does handle optional fields. Therefore I added helper methods to convert int, bool and string into pointers and used pointers in structs sent to Electron.
|
||||
|
||||
### Add listeners
|
||||
|
||||
```go
|
||||
// Add a listener on Astilectron
|
||||
a.On(astilectron.EventNameAppCrash, func(e astilectron.Event) (deleteListener bool) {
|
||||
astilog.Error("App has crashed")
|
||||
return
|
||||
})
|
||||
|
||||
// Add a listener on the window
|
||||
w.On(astilectron.EventNameWindowEventResize, func(e astilectron.Event) (deleteListener bool) {
|
||||
astilog.Info("Window resized")
|
||||
return
|
||||
})
|
||||
```
|
||||
|
||||
Nothing much to say here either except that you can add listeners to Astilectron as well.
|
||||
|
||||
### Play with the window
|
||||
|
||||
```go
|
||||
// Play with the window
|
||||
w.Resize(200, 200)
|
||||
time.Sleep(time.Second)
|
||||
w.Maximize()
|
||||
```
|
||||
|
||||
Check out the [Window doc](https://godoc.org/github.com/asticode/go-astilectron#Window) for a list of all exported methods
|
||||
|
||||
### Send messages between GO and your webserver
|
||||
|
||||
In your webserver add the following javascript to any of the pages you want to interact with:
|
||||
|
||||
```html
|
||||
<script>
|
||||
// This will wait for the astilectron namespace to be ready
|
||||
document.addEventListener('astilectron-ready', function() {
|
||||
|
||||
// This will listen to messages sent by GO
|
||||
astilectron.listen(function(message) {
|
||||
|
||||
// This will send a message back to GO
|
||||
astilectron.send("I'm good bro")
|
||||
});
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
In your GO app add the following:
|
||||
|
||||
```go
|
||||
// Listen to messages sent by webserver
|
||||
w.On(astilectron.EventNameWindowEventMessage, func(e astilectron.Event) (deleteListener bool) {
|
||||
var m string
|
||||
e.Message.Unmarshal(&m)
|
||||
astilog.Infof("Received message %s", m)
|
||||
return
|
||||
})
|
||||
|
||||
// Send message to webserver
|
||||
w.Send("What's up?")
|
||||
```
|
||||
|
||||
And that's it!
|
||||
|
||||
NOTE: needless to say that the message can be something other than a string. A custom struct for instance!
|
||||
|
||||
### Handle several screens/displays
|
||||
|
||||
```go
|
||||
// If several displays, move the window to the second display
|
||||
var displays = a.Displays()
|
||||
if len(displays) > 1 {
|
||||
time.Sleep(time.Second)
|
||||
w.MoveInDisplay(displays[1], 50, 50)
|
||||
}
|
||||
```
|
||||
|
||||
### Menus
|
||||
|
||||
```go
|
||||
// Init a new app menu
|
||||
// You can do the same thing with a window
|
||||
var m = a.NewMenu([]*astilectron.MenuItemOptions{
|
||||
{
|
||||
Label: astilectron.PtrStr("Separator"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Normal 1")},
|
||||
{
|
||||
Label: astilectron.PtrStr("Normal 2"),
|
||||
OnClick: func(e astilectron.Event) (deleteListener bool) {
|
||||
astilog.Info("Normal 2 item has been clicked")
|
||||
return
|
||||
},
|
||||
},
|
||||
{Type: astilectron.MenuItemTypeSeparator},
|
||||
{Label: astilectron.PtrStr("Normal 3")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Checkbox"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Checked: astilectron.PtrBool(true), Label: astilectron.PtrStr("Checkbox 1"), Type: astilectron.MenuItemTypeCheckbox},
|
||||
{Label: astilectron.PtrStr("Checkbox 2"), Type: astilectron.MenuItemTypeCheckbox},
|
||||
{Label: astilectron.PtrStr("Checkbox 3"), Type: astilectron.MenuItemTypeCheckbox},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Radio"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Checked: astilectron.PtrBool(true), Label: astilectron.PtrStr("Radio 1"), Type: astilectron.MenuItemTypeRadio},
|
||||
{Label: astilectron.PtrStr("Radio 2"), Type: astilectron.MenuItemTypeRadio},
|
||||
{Label: astilectron.PtrStr("Radio 3"), Type: astilectron.MenuItemTypeRadio},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Roles"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Minimize"), Role: astilectron.MenuItemRoleMinimize},
|
||||
{Label: astilectron.PtrStr("Close"), Role: astilectron.MenuItemRoleClose},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Retrieve a menu item
|
||||
// This will retrieve the "Checkbox 1" item
|
||||
mi, _ := m.Item(1, 0)
|
||||
|
||||
// Add listener manually
|
||||
// An OnClick listener has already been added in the options directly for another menu item
|
||||
mi.On(astilectron.EventNameMenuItemEventClicked, func(e astilectron.Event) bool {
|
||||
astilog.Infof("Menu item has been clicked. 'Checked' status is now %t", *e.MenuItemOptions.Checked)
|
||||
return false
|
||||
})
|
||||
|
||||
// Create the menu
|
||||
m.Create()
|
||||
|
||||
// Manipulate a menu item
|
||||
mi.SetChecked(true)
|
||||
|
||||
// Init a new menu item
|
||||
var ni = m.NewItem(&astilectron.MenuItemOptions{
|
||||
Label: astilectron.PtrStr("Inserted"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Inserted 1")},
|
||||
{Label: astilectron.PtrStr("Inserted 2")},
|
||||
},
|
||||
})
|
||||
|
||||
// Insert the menu item at position "1"
|
||||
m.Insert(1, ni)
|
||||
|
||||
// Fetch a sub menu
|
||||
s, _ := m.SubMenu(0)
|
||||
|
||||
// Init a new menu item
|
||||
ni = s.NewItem(&astilectron.MenuItemOptions{
|
||||
Label: astilectron.PtrStr("Appended"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Appended 1")},
|
||||
{Label: astilectron.PtrStr("Appended 2")},
|
||||
},
|
||||
})
|
||||
|
||||
// Append menu item dynamically
|
||||
s.Append(ni)
|
||||
|
||||
// Pop up sub menu as a context menu
|
||||
s.Popup(&astilectron.MenuPopupOptions{PositionOptions: astilectron.PositionOptions{X: astilectron.PtrInt(50), Y: astilectron.PtrInt(50)}})
|
||||
|
||||
// Close popup
|
||||
s.ClosePopup()
|
||||
|
||||
// Destroy the menu
|
||||
m.Destroy()
|
||||
```
|
||||
|
||||
A few things to know:
|
||||
|
||||
* when assigning a role to a menu item, `go-astilectron` won't be able to capture its click event
|
||||
* on MacOS there's no such thing as a window menu, only app menus therefore my advice is to stick to one global app menu instead of creating separate window menus
|
||||
|
||||
### Tray
|
||||
|
||||
```go
|
||||
// New tray
|
||||
var t = a.NewTray(&astilectron.TrayOptions{
|
||||
Image: astilectron.PtrStr("/path/to/image.png"),
|
||||
Tooltip: astilectron.PtrStr("Tray's tooltip"),
|
||||
})
|
||||
|
||||
// New tray menu
|
||||
var m = t.NewMenu([]*astilectron.MenuItemOptions{
|
||||
{
|
||||
Label: astilectron.PtrStr("Root 1"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Item 1")},
|
||||
{Label: astilectron.PtrStr("Item 2")},
|
||||
{Type: astilectron.MenuItemTypeSeparator},
|
||||
{Label: astilectron.PtrStr("Item 3")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Root 2"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Item 1")},
|
||||
{Label: astilectron.PtrStr("Item 2")},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Create the menu
|
||||
m.Create()
|
||||
|
||||
// Create tray
|
||||
t.Create()
|
||||
```
|
||||
|
||||
### Dialogs
|
||||
|
||||
In your webserver add one of the following javascript to achieve any kind of dialog.
|
||||
|
||||
#### Error box
|
||||
|
||||
```html
|
||||
<script>
|
||||
// This will wait for the astilectron namespace to be ready
|
||||
document.addEventListener('astilectron-ready', function() {
|
||||
// This will open the dialog
|
||||
astilectron.showErrorBox("My Title", "My content")
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Message box
|
||||
|
||||
```html
|
||||
<script>
|
||||
// This will wait for the astilectron namespace to be ready
|
||||
document.addEventListener('astilectron-ready', function() {
|
||||
// This will open the dialog
|
||||
astilectron.showMessageBox({message: "My message", title: "My Title"})
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Open dialog
|
||||
|
||||
```html
|
||||
<script>
|
||||
// This will wait for the astilectron namespace to be ready
|
||||
document.addEventListener('astilectron-ready', function() {
|
||||
// This will open the dialog
|
||||
astilectron.showOpenDialog({properties: ['openFile', 'multiSelections'], title: "My Title"}, function(paths) {
|
||||
console.log("chosen paths are ", paths)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Save dialog
|
||||
|
||||
```html
|
||||
<script>
|
||||
// This will wait for the astilectron namespace to be ready
|
||||
document.addEventListener('astilectron-ready', function() {
|
||||
// This will open the dialog
|
||||
astilectron.showSaveDialog({title: "My title"}, function(filename) {
|
||||
console.log("chosen filename is ", filename)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Final code
|
||||
|
||||
```go
|
||||
// Set up the logger
|
||||
var l <your logger type>
|
||||
astilog.SetLogger(l)
|
||||
|
||||
// Start an http server
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello world</title>
|
||||
</head>
|
||||
<body>
|
||||
<span id="message">Hello world</span>
|
||||
<script>
|
||||
// This will wait for the astilectron namespace to be ready
|
||||
document.addEventListener('astilectron-ready', function() {
|
||||
// This will listen to messages sent by GO
|
||||
astilectron.listen(function(message) {
|
||||
document.getElementById('message').innerHTML = message
|
||||
// This will send a message back to GO
|
||||
astilectron.send("I'm good bro")
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`))
|
||||
})
|
||||
go http.ListenAndServe("127.0.0.1:4000", nil)
|
||||
|
||||
// Initialize astilectron
|
||||
var a, _ = astilectron.New(astilectron.Options{
|
||||
AppName: "<your app name>",
|
||||
AppIconDefaultPath: "<your .png icon>",
|
||||
AppIconDarwinPath: "<your .icns icon>",
|
||||
BaseDirectoryPath: "<where you want the provisioner to install the dependencies>",
|
||||
})
|
||||
defer a.Close()
|
||||
|
||||
// Handle quit
|
||||
a.HandleSignals()
|
||||
a.On(astilectron.EventNameAppCrash, func(e astilectron.Event) (deleteListener bool) {
|
||||
astilog.Error("App has crashed")
|
||||
return
|
||||
})
|
||||
|
||||
// Start astilectron: this will download and set up the dependencies, and start the Electron app
|
||||
a.Start()
|
||||
|
||||
// Init a new app menu
|
||||
// You can do the same thing with a window
|
||||
var m = a.NewMenu([]*astilectron.MenuItemOptions{
|
||||
{
|
||||
Label: astilectron.PtrStr("Separator"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Normal 1")},
|
||||
{
|
||||
Label: astilectron.PtrStr("Normal 2"),
|
||||
OnClick: func(e astilectron.Event) (deleteListener bool) {
|
||||
astilog.Info("Normal 2 item has been clicked")
|
||||
return
|
||||
},
|
||||
},
|
||||
{Type: astilectron.MenuItemTypeSeparator},
|
||||
{Label: astilectron.PtrStr("Normal 3")},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Checkbox"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Checked: astilectron.PtrBool(true), Label: astilectron.PtrStr("Checkbox 1"), Type: astilectron.MenuItemTypeCheckbox},
|
||||
{Label: astilectron.PtrStr("Checkbox 2"), Type: astilectron.MenuItemTypeCheckbox},
|
||||
{Label: astilectron.PtrStr("Checkbox 3"), Type: astilectron.MenuItemTypeCheckbox},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Radio"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Checked: astilectron.PtrBool(true), Label: astilectron.PtrStr("Radio 1"), Type: astilectron.MenuItemTypeRadio},
|
||||
{Label: astilectron.PtrStr("Radio 2"), Type: astilectron.MenuItemTypeRadio},
|
||||
{Label: astilectron.PtrStr("Radio 3"), Type: astilectron.MenuItemTypeRadio},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: astilectron.PtrStr("Roles"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Minimize"), Role: astilectron.MenuItemRoleMinimize},
|
||||
{Label: astilectron.PtrStr("Close"), Role: astilectron.MenuItemRoleClose},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Retrieve a menu item
|
||||
// This will retrieve the "Checkbox 1" item
|
||||
mi, _ := m.Item(1, 0)
|
||||
|
||||
// Add listener manually
|
||||
// An OnClick listener has already been added in the options directly for another menu item
|
||||
mi.On(astilectron.EventNameMenuItemEventClicked, func(e astilectron.Event) bool {
|
||||
astilog.Infof("Menu item has been clicked. 'Checked' status is now %t", *e.MenuItemOptions.Checked)
|
||||
return false
|
||||
})
|
||||
|
||||
// Create the menu
|
||||
m.Create()
|
||||
|
||||
// Create a new window with a listener on resize
|
||||
var w, _ = a.NewWindow("http://127.0.0.1:4000", &astilectron.WindowOptions{
|
||||
Center: astilectron.PtrBool(true),
|
||||
Height: astilectron.PtrInt(600),
|
||||
Icon: astilectron.PtrStr(<your icon path>),
|
||||
Width: astilectron.PtrInt(600),
|
||||
})
|
||||
w.On(astilectron.EventNameWindowEventResize, func(e astilectron.Event) (deleteListener bool) {
|
||||
astilog.Info("Window resized")
|
||||
return
|
||||
})
|
||||
w.On(astilectron.EventNameWindowEventMessage, func(e astilectron.Event) (deleteListener bool) {
|
||||
var m string
|
||||
e.Message.Unmarshal(&m)
|
||||
astilog.Infof("Received message %s", m)
|
||||
return
|
||||
})
|
||||
w.Create()
|
||||
|
||||
// Play with the window
|
||||
w.Resize(200, 200)
|
||||
time.Sleep(time.Second)
|
||||
w.Maximize()
|
||||
|
||||
// If several displays, move the window to the second display
|
||||
var displays = a.Displays()
|
||||
if len(displays) > 1 {
|
||||
time.Sleep(time.Second)
|
||||
w.MoveInDisplay(displays[1], 50, 50)
|
||||
}
|
||||
|
||||
// Send a message to the server
|
||||
time.Sleep(time.Second)
|
||||
w.Send("What's up?")
|
||||
|
||||
// Manipulate a menu item
|
||||
time.Sleep(time.Second)
|
||||
mi.SetChecked(true)
|
||||
|
||||
// Init a new menu item
|
||||
var ni = m.NewItem(&astilectron.MenuItemOptions{
|
||||
Label: astilectron.PtrStr("Inserted"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Inserted 1")},
|
||||
{Label: astilectron.PtrStr("Inserted 2")},
|
||||
},
|
||||
})
|
||||
|
||||
// Insert the menu item at position "1"
|
||||
time.Sleep(time.Second)
|
||||
m.Insert(1, ni)
|
||||
|
||||
// Fetch a sub menu
|
||||
s, _ := m.SubMenu(0)
|
||||
|
||||
// Init a new menu item
|
||||
ni = s.NewItem(&astilectron.MenuItemOptions{
|
||||
Label: astilectron.PtrStr("Appended"),
|
||||
SubMenu: []*astilectron.MenuItemOptions{
|
||||
{Label: astilectron.PtrStr("Appended 1")},
|
||||
{Label: astilectron.PtrStr("Appended 2")},
|
||||
},
|
||||
})
|
||||
|
||||
// Append menu item dynamically
|
||||
time.Sleep(time.Second)
|
||||
s.Append(ni)
|
||||
|
||||
// Pop up sub menu as a context menu
|
||||
time.Sleep(time.Second)
|
||||
s.Popup(&astilectron.MenuPopupOptions{PositionOptions: astilectron.PositionOptions{X: astilectron.PtrInt(50), Y: astilectron.PtrInt(50)}})
|
||||
|
||||
// Close popup
|
||||
time.Sleep(time.Second)
|
||||
s.ClosePopup()
|
||||
|
||||
// Destroy the menu
|
||||
time.Sleep(time.Second)
|
||||
m.Destroy()
|
||||
|
||||
// Blocking pattern
|
||||
a.Wait()
|
||||
```
|
||||
|
||||
# Bootstrap
|
||||
|
||||
For convenience purposes I've added a **bootstrap** to help first timers and avoid code duplications.
|
||||
|
||||
NOTE: you DON'T have to use the bootstrap, it's entirely up to you whether to use it or not.
|
||||
|
||||
The bootstrap allows you to quickly create a one-window application.
|
||||
|
||||
## Using static files and remote messaging (the best way)
|
||||
|
||||
In order to use the **bootstrap** with static files and remote messaging you must:
|
||||
|
||||
- follow the following project organization:
|
||||
|
||||
|--+ resources
|
||||
|
|
||||
|--+ app (contains your static files such as .html, .css, .js, .png, etc.)
|
||||
|--+ main.go
|
||||
|
||||
- use the `MessageHandler` **bootstrap** option in order to handle remote messaging
|
||||
- use `remote messaging` in your static files
|
||||
|
||||
## Using a web server
|
||||
|
||||
In order to use the **bootstrap** with a web server you must:
|
||||
|
||||
- follow the following project organization:
|
||||
|
||||
|--+ resources
|
||||
|
|
||||
|--+ static (contains your static files such as .css, .js, .png, etc.)
|
||||
|
|
||||
|--+ templates (contains your templates .html files)
|
||||
|--+ main.go
|
||||
- use the `AdaptRouter` and `TemplateData` **bootstrap** options in order to handle the server routes
|
||||
|
||||
## Common
|
||||
|
||||
- if you're using the `RestoreAssets` **bootstrap** option, add the following comment on top of your `main()` method:
|
||||
|
||||
//go:generate go-bindata -pkg $GOPACKAGE -o resources.go resources/...
|
||||
|
||||
and run the following command before building your binary:
|
||||
|
||||
$ go generate main.go
|
||||
|
||||
- use the `bootstrap.Run()` method
|
||||
|
||||
Check out the [example](https://github.com/asticode/go-astilectron/tree/master/examples/9.bootstrap) for a detailed working example (see the **Examples** section below for the specific commands to run).
|
||||
|
||||
# I want to see it in actions!
|
||||
|
||||
To make things clearer I've tried to split features in different [examples](https://github.com/asticode/go-astilectron/tree/master/examples).
|
||||
|
||||
To run any of the examples, run the following commands:
|
||||
|
||||
$ go run examples/<name of the example>/main.go -v
|
||||
|
||||
Here's a list of the examples:
|
||||
|
||||
- [1.basic_window](https://github.com/asticode/go-astilectron/tree/master/examples/1.basic_window/main.go) creates a basic window that displays a static .html file
|
||||
- [2.basic_window_events](https://github.com/asticode/go-astilectron/tree/master/examples/2.basic_window_events/main.go) plays with basic window methods and shows you how to set up your own listeners
|
||||
- [3.webserver_app](https://github.com/asticode/go-astilectron/tree/master/examples/3.webserver_app/main.go) sets up a basic webserver app
|
||||
- [4.remote_messaging](https://github.com/asticode/go-astilectron/tree/master/examples/4.remote_messaging/main.go) sends a message to the webserver and listens for any response
|
||||
- [5.single_binary_distribution](https://github.com/asticode/go-astilectron/tree/master/examples/5.single_binary_distribution/main.go) shows how to use `go-astilectron` in a unique binary. For this example you have to run one of the previous examples (so that the .zip files exist) and run the following commands:
|
||||
|
||||
```
|
||||
$ go generate examples/5.single_binary_distribution/main.go
|
||||
$ go run examples/5.single_binary_distribution/main.go examples/5.single_binary_distribution/vendor.go -v
|
||||
```
|
||||
|
||||
- [6.screens_and_displays](https://github.com/asticode/go-astilectron/tree/master/examples/6.screens_and_displays/main.go) plays around with screens and displays
|
||||
- [7.menus](https://github.com/asticode/go-astilectron/tree/master/examples/7.menus/main.go) creates and manipulates menus
|
||||
- [8.bootstrap](https://github.com/asticode/go-astilectron/tree/master/examples/8.bootstrap) shows how to use the **bootstrap**. For this example you have to run the following commands:
|
||||
|
||||
```
|
||||
$ go generate examples/8.bootstrap/main.go
|
||||
$ go run examples/8.bootstrap/main.go examples/8.bootstrap/resources.go -v
|
||||
```
|
||||
|
||||
- [9.tray](https://github.com/asticode/go-astilectron/tree/master/examples/9.tray/main.go) creates a tray
|
||||
|
||||
# Features and roadmap
|
||||
|
||||
- [x] custom branding (custom app name, app icon, etc.)
|
||||
- [x] window basic methods (create, show, close, resize, minimize, maximize, ...)
|
||||
- [x] window basic events (close, blur, focus, unresponsive, crashed, ...)
|
||||
- [x] remote messaging (messages between GO and the JS in the webserver)
|
||||
- [x] single binary distribution
|
||||
- [x] multi screens/displays
|
||||
- [x] menu methods and events (create, insert, append, popup, clicked, ...)
|
||||
- [x] bootstrap
|
||||
- [x] dialogs (open or save file, alerts, ...)
|
||||
- [x] tray
|
||||
- [ ] loader
|
||||
- [ ] bundle helper
|
||||
- [ ] accelerators (shortcuts)
|
||||
- [ ] file methods (drag & drop, ...)
|
||||
- [ ] clipboard methods
|
||||
- [ ] power monitor events (suspend, resume, ...)
|
||||
- [ ] notifications (macosx)
|
||||
- [ ] desktop capturer (audio and video)
|
||||
- [ ] session methods
|
||||
- [ ] session events
|
||||
- [ ] window advanced options (add missing ones)
|
||||
- [ ] window advanced methods (add missing ones)
|
||||
- [ ] window advanced events (add missing ones)
|
||||
- [ ] child windows
|
||||
|
||||
# Cheers to
|
||||
|
||||
[go-thrust](https://github.com/miketheprogrammer/go-thrust) which is awesome but unfortunately not maintained anymore. It inspired this project.
|
|
@ -0,0 +1,389 @@
|
|||
package astilectron
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astilog"
|
||||
"github.com/asticode/go-astitools/context"
|
||||
"github.com/asticode/go-astitools/exec"
|
||||
"github.com/asticode/go-astitools/slice"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Versions
|
||||
const (
|
||||
VersionAstilectron = "0.6.0"
|
||||
VersionElectron = "1.6.5"
|
||||
)
|
||||
|
||||
// Misc vars
|
||||
var (
|
||||
astilectronDirectoryPath = flag.String("astilectron-directory-path", "", "the astilectron directory path")
|
||||
validOSes = []string{"darwin", "linux", "windows"}
|
||||
)
|
||||
|
||||
// App event names
|
||||
const (
|
||||
EventNameAppClose = "app.close"
|
||||
EventNameAppCmdStop = "app.cmd.stop"
|
||||
EventNameAppCrash = "app.crash"
|
||||
EventNameAppErrorAccept = "app.error.accept"
|
||||
EventNameAppEventReady = "app.event.ready"
|
||||
EventNameAppNoAccept = "app.no.accept"
|
||||
EventNameAppTooManyAccept = "app.too.many.accept"
|
||||
)
|
||||
|
||||
// Astilectron represents an object capable of interacting with Astilectron
|
||||
// TODO Fix race conditions
|
||||
type Astilectron struct {
|
||||
canceller *asticontext.Canceller
|
||||
channelQuit chan bool
|
||||
dispatcher *Dispatcher
|
||||
displayPool *displayPool
|
||||
identifier *identifier
|
||||
listener net.Listener
|
||||
options Options
|
||||
paths *Paths
|
||||
provisioner Provisioner
|
||||
reader *reader
|
||||
stderrWriter *astiexec.StdWriter
|
||||
stdoutWriter *astiexec.StdWriter
|
||||
writer *writer
|
||||
}
|
||||
|
||||
// Options represents Astilectron options
|
||||
type Options struct {
|
||||
AppName string
|
||||
AppIconDarwinPath string // Darwin systems requires a specific .icns file
|
||||
AppIconDefaultPath string
|
||||
BaseDirectoryPath string
|
||||
}
|
||||
|
||||
// New creates a new Astilectron instance
|
||||
func New(o Options) (a *Astilectron, err error) {
|
||||
// Validate the OS
|
||||
if err = validateOS(runtime.GOOS); err != nil {
|
||||
err = errors.Wrap(err, "validating OS failed")
|
||||
return
|
||||
}
|
||||
a = &Astilectron{
|
||||
canceller: asticontext.NewCanceller(),
|
||||
channelQuit: make(chan bool),
|
||||
dispatcher: newDispatcher(),
|
||||
displayPool: newDisplayPool(),
|
||||
identifier: newIdentifier(),
|
||||
options: o,
|
||||
provisioner: DefaultProvisioner,
|
||||
}
|
||||
|
||||
// Set paths
|
||||
if a.paths, err = newPaths(runtime.GOOS, runtime.GOARCH, o); err != nil {
|
||||
err = errors.Wrap(err, "creating new paths failed")
|
||||
return
|
||||
}
|
||||
|
||||
// Add default listeners
|
||||
a.On(EventNameAppCmdStop, func(e Event) (deleteListener bool) {
|
||||
a.Stop()
|
||||
return
|
||||
})
|
||||
a.On(EventNameDisplayEventAdded, func(e Event) (deleteListener bool) {
|
||||
a.displayPool.update(e.Displays)
|
||||
return
|
||||
})
|
||||
a.On(EventNameDisplayEventMetricsChanged, func(e Event) (deleteListener bool) {
|
||||
a.displayPool.update(e.Displays)
|
||||
return
|
||||
})
|
||||
a.On(EventNameDisplayEventRemoved, func(e Event) (deleteListener bool) {
|
||||
a.displayPool.update(e.Displays)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// validateOS validates the OS
|
||||
func validateOS(os string) error {
|
||||
if !astislice.InStringSlice(os, validOSes) {
|
||||
return fmt.Errorf("OS %s is not supported", os)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidOSes returns a slice containing the names of all currently supported operating systems
|
||||
func ValidOSes() []string {
|
||||
return append(make([]string, 0, len(validOSes)), validOSes...)
|
||||
}
|
||||
|
||||
// SetProvisioner sets the provisioner
|
||||
func (a *Astilectron) SetProvisioner(p Provisioner) *Astilectron {
|
||||
a.provisioner = p
|
||||
return a
|
||||
}
|
||||
|
||||
// On implements the Listenable interface
|
||||
func (a *Astilectron) On(eventName string, l Listener) {
|
||||
a.dispatcher.addListener(mainTargetID, eventName, l)
|
||||
}
|
||||
|
||||
// Start starts Astilectron
|
||||
func (a *Astilectron) Start() (err error) {
|
||||
// Log
|
||||
astilog.Debug("Starting...")
|
||||
|
||||
// Start the dispatcher
|
||||
go a.dispatcher.start()
|
||||
|
||||
// Provision
|
||||
if err = a.provision(); err != nil {
|
||||
return errors.Wrap(err, "provisioning failed")
|
||||
}
|
||||
|
||||
// Unfortunately communicating with Electron through stdin/stdout doesn't work on Windows so all communications
|
||||
// will be done through TCP
|
||||
if err = a.listenTCP(); err != nil {
|
||||
return errors.Wrap(err, "listening failed")
|
||||
}
|
||||
|
||||
// Execute
|
||||
if err = a.execute(); err != nil {
|
||||
err = errors.Wrap(err, "executing failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// provision provisions Astilectron
|
||||
func (a *Astilectron) provision() error {
|
||||
astilog.Debug("Provisioning...")
|
||||
var ctx, _ = a.canceller.NewContext()
|
||||
return a.provisioner.Provision(ctx, *a.dispatcher, a.options.AppName, runtime.GOOS, *a.paths)
|
||||
}
|
||||
|
||||
// listenTCP listens to the first TCP connection coming its way (this should be Astilectron)
|
||||
func (a *Astilectron) listenTCP() (err error) {
|
||||
// Log
|
||||
astilog.Debug("Listening...")
|
||||
|
||||
// Listen
|
||||
if a.listener, err = net.Listen("tcp", "127.0.0.1:"); err != nil {
|
||||
return errors.Wrap(err, "tcp net.Listen failed")
|
||||
}
|
||||
|
||||
// Check a connection has been accepted quickly enough
|
||||
var chanAccepted = make(chan bool)
|
||||
go a.watchNoAccept(30*time.Second, chanAccepted)
|
||||
|
||||
// Accept connections
|
||||
go a.acceptTCP(chanAccepted)
|
||||
return
|
||||
}
|
||||
|
||||
// watchNoAccept checks whether a TCP connection is accepted quickly enough
|
||||
func (a *Astilectron) watchNoAccept(timeout time.Duration, chanAccepted chan bool) {
|
||||
var t = time.NewTimer(timeout)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-chanAccepted:
|
||||
return
|
||||
case <-t.C:
|
||||
astilog.Errorf("No TCP connection has been accepted in the past %s", timeout)
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppNoAccept, TargetID: mainTargetID})
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watchAcceptTCP accepts TCP connections
|
||||
func (a *Astilectron) acceptTCP(chanAccepted chan bool) {
|
||||
for i := 0; i <= 1; i++ {
|
||||
// Accept
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if conn, err = a.listener.Accept(); err != nil {
|
||||
astilog.Errorf("%s while TCP accepting", err)
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppErrorAccept, TargetID: mainTargetID})
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
|
||||
return
|
||||
}
|
||||
|
||||
// We only accept the first connection which should be Astilectron, close the next one and stop
|
||||
// the app
|
||||
if i > 0 {
|
||||
astilog.Errorf("Too many TCP connections")
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppTooManyAccept, TargetID: mainTargetID})
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Let the timer know a connection has been accepted
|
||||
chanAccepted <- true
|
||||
|
||||
// Create reader and writer
|
||||
a.writer = newWriter(conn)
|
||||
a.reader = newReader(a.dispatcher, conn)
|
||||
go a.reader.read()
|
||||
}
|
||||
}
|
||||
|
||||
// execute executes Astilectron in Electron
|
||||
func (a *Astilectron) execute() (err error) {
|
||||
// Log
|
||||
astilog.Debug("Executing...")
|
||||
|
||||
// Create command
|
||||
var ctx, _ = a.canceller.NewContext()
|
||||
var cmd = exec.CommandContext(ctx, a.paths.AppExecutable(), a.paths.AstilectronApplication(), a.listener.Addr().String())
|
||||
a.stderrWriter = astiexec.NewStdWriter(func(i []byte) { astilog.Debugf("Stderr says: %s", i) })
|
||||
a.stdoutWriter = astiexec.NewStdWriter(func(i []byte) { astilog.Debugf("Stdout says: %s", i) })
|
||||
cmd.Stderr = a.stderrWriter
|
||||
cmd.Stdout = a.stdoutWriter
|
||||
|
||||
// Execute command
|
||||
if err = a.executeCmd(cmd); err != nil {
|
||||
return errors.Wrap(err, "executing cmd failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// executeCmd executes the command
|
||||
func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
|
||||
var e = synchronousFunc(a.canceller, a, func() {
|
||||
// Start command
|
||||
astilog.Debugf("Starting cmd %s", strings.Join(cmd.Args, " "))
|
||||
if err = cmd.Start(); err != nil {
|
||||
err = errors.Wrapf(err, "starting cmd %s failed", strings.Join(cmd.Args, " "))
|
||||
return
|
||||
}
|
||||
|
||||
// Watch command
|
||||
go a.watchCmd(cmd)
|
||||
}, EventNameAppEventReady)
|
||||
|
||||
// Update display pool
|
||||
if e.Displays != nil {
|
||||
a.displayPool.update(e.Displays)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// watchCmd watches the cmd execution
|
||||
func (a *Astilectron) watchCmd(cmd *exec.Cmd) {
|
||||
// Wait
|
||||
cmd.Wait()
|
||||
|
||||
// Check the canceller to check whether it was a crash
|
||||
if !a.canceller.Cancelled() {
|
||||
astilog.Debug("App has crashed")
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppCrash, TargetID: mainTargetID})
|
||||
} else {
|
||||
astilog.Debug("App has closed")
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppClose, TargetID: mainTargetID})
|
||||
}
|
||||
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
|
||||
}
|
||||
|
||||
// Close closes Astilectron properly
|
||||
func (a *Astilectron) Close() {
|
||||
astilog.Debug("Closing...")
|
||||
a.canceller.Cancel()
|
||||
a.dispatcher.close()
|
||||
if a.listener != nil {
|
||||
a.listener.Close()
|
||||
}
|
||||
if a.reader != nil {
|
||||
a.reader.close()
|
||||
}
|
||||
if a.stderrWriter != nil {
|
||||
a.stderrWriter.Close()
|
||||
}
|
||||
if a.stdoutWriter != nil {
|
||||
a.stdoutWriter.Close()
|
||||
}
|
||||
if a.writer != nil {
|
||||
a.writer.close()
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSignals handles signals
|
||||
func (a *Astilectron) HandleSignals() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGABRT, syscall.SIGKILL, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
|
||||
go func() {
|
||||
for sig := range ch {
|
||||
astilog.Debugf("Received signal %s", sig)
|
||||
a.Stop()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop orders Astilectron to stop
|
||||
func (a *Astilectron) Stop() {
|
||||
astilog.Debug("Stopping...")
|
||||
a.canceller.Cancel()
|
||||
if a.channelQuit != nil {
|
||||
close(a.channelQuit)
|
||||
a.channelQuit = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Wait is a blocking pattern
|
||||
func (a *Astilectron) Wait() {
|
||||
if a.channelQuit == nil {
|
||||
return
|
||||
}
|
||||
<-a.channelQuit
|
||||
}
|
||||
|
||||
// Displays returns the displays
|
||||
func (a *Astilectron) Displays() []*Display {
|
||||
return a.displayPool.all()
|
||||
}
|
||||
|
||||
// PrimaryDisplay returns the primary display
|
||||
func (a *Astilectron) PrimaryDisplay() *Display {
|
||||
return a.displayPool.primary()
|
||||
}
|
||||
|
||||
// NewMenu creates a new app menu
|
||||
func (a *Astilectron) NewMenu(i []*MenuItemOptions) *Menu {
|
||||
return newMenu(nil, a, i, a.canceller, a.dispatcher, a.identifier, a.writer)
|
||||
}
|
||||
|
||||
// NewWindow creates a new window
|
||||
func (a *Astilectron) NewWindow(url string, o *WindowOptions) (*Window, error) {
|
||||
return newWindow(a.options, url, o, a.canceller, a.dispatcher, a.identifier, a.writer)
|
||||
}
|
||||
|
||||
// NewWindowInDisplay creates a new window in a specific display
|
||||
// This overrides the center attribute
|
||||
func (a *Astilectron) NewWindowInDisplay(d *Display, url string, o *WindowOptions) (*Window, error) {
|
||||
if o.X != nil {
|
||||
*o.X += d.Bounds().X
|
||||
} else {
|
||||
o.X = PtrInt(d.Bounds().X)
|
||||
}
|
||||
if o.Y != nil {
|
||||
*o.Y += d.Bounds().Y
|
||||
} else {
|
||||
o.Y = PtrInt(d.Bounds().Y)
|
||||
}
|
||||
return newWindow(a.options, url, o, a.canceller, a.dispatcher, a.identifier, a.writer)
|
||||
}
|
||||
|
||||
// NewTray creates a new tray
|
||||
func (a *Astilectron) NewTray(o *TrayOptions) *Tray {
|
||||
return newTray(o, a.canceller, a.dispatcher, a.identifier, a.writer)
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package astilectron
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAstilectron_Provision(t *testing.T) {
|
||||
// Init
|
||||
var o = Options{BaseDirectoryPath: mockedTempPath()}
|
||||
defer os.RemoveAll(o.BaseDirectoryPath)
|
||||
a, err := New(o)
|
||||
assert.NoError(t, err)
|
||||
defer a.dispatcher.close()
|
||||
go a.dispatcher.start()
|
||||
a.SetProvisioner(NewDisembedderProvisioner(mockedDisembedder, "astilectron", "electron/linux"))
|
||||
var hasStarted, hasStopped bool
|
||||
a.On(EventNameProvisionAstilectronMoved, func(e Event) bool {
|
||||
hasStarted = true
|
||||
return false
|
||||
})
|
||||
var wg = &sync.WaitGroup{}
|
||||
a.On(EventNameProvisionElectronFinished, func(e Event) bool {
|
||||
hasStopped = true
|
||||
wg.Done()
|
||||
return false
|
||||
})
|
||||
wg.Add(1)
|
||||
|
||||
// Test provision is successful and sends the correct events
|
||||
err = a.provision()
|
||||
assert.NoError(t, err)
|
||||
wg.Wait()
|
||||
assert.True(t, hasStarted)
|
||||
assert.True(t, hasStopped)
|
||||
}
|
||||
|
||||
func TestAstilectron_WatchNoAccept(t *testing.T) {
|
||||
// Init
|
||||
a, err := New(Options{})
|
||||
assert.NoError(t, err)
|
||||
defer a.dispatcher.close()
|
||||
go a.dispatcher.start()
|
||||
var isStopped bool
|
||||
var wg = &sync.WaitGroup{}
|
||||
a.On(EventNameAppCmdStop, func(e Event) bool {
|
||||
isStopped = true
|
||||
wg.Done()
|
||||
return false
|
||||
})
|
||||
c := make(chan bool)
|
||||
|
||||
// Test success
|
||||
go func() {
|
||||
time.Sleep(50 * time.Microsecond)
|
||||
c <- true
|
||||
}()
|
||||
a.watchNoAccept(time.Second, c)
|
||||
assert.False(t, isStopped)
|
||||
|
||||
// Test failure
|
||||
wg.Add(1)
|
||||
a.watchNoAccept(time.Nanosecond, c)
|
||||
wg.Wait()
|
||||
assert.True(t, isStopped)
|
||||
}
|
||||
|
||||
// mockedListener implements the net.Listener interface
|
||||
type mockedListener struct {
|
||||
c chan bool
|
||||
e chan bool
|
||||
}
|
||||
|
||||
func (l mockedListener) Accept() (net.Conn, error) {
|
||||
for {
|
||||
select {
|
||||
case <-l.c:
|
||||
return mockedConn{}, nil
|
||||
case <-l.e:
|
||||
return nil, errors.New("invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
func (l mockedListener) Close() error { return nil }
|
||||
func (l mockedListener) Addr() net.Addr { return nil }
|
||||
|
||||
// mockedConn implements the net.Conn interface
|
||||
type mockedConn struct{}
|
||||
|
||||
func (c mockedConn) Read(b []byte) (n int, err error) { return }
|
||||
func (c mockedConn) Write(b []byte) (n int, err error) { return }
|
||||
func (c mockedConn) Close() error { return nil }
|
||||
func (c mockedConn) LocalAddr() net.Addr { return nil }
|
||||
func (c mockedConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c mockedConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c mockedConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c mockedConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
// mockedAddr implements the net.Addr interface
|
||||
type mockedAddr struct{}
|
||||
|
||||
func (a mockedAddr) Network() string { return "" }
|
||||
func (a mockedAddr) String() string { return "" }
|
||||
|
||||
func TestAstilectron_AcceptTCP(t *testing.T) {
|
||||
// Init
|
||||
a, err := New(Options{})
|
||||
assert.NoError(t, err)
|
||||
defer a.Close()
|
||||
go a.dispatcher.start()
|
||||
var l = &mockedListener{c: make(chan bool), e: make(chan bool)}
|
||||
a.listener = l
|
||||
var isStopped bool
|
||||
var wg = &sync.WaitGroup{}
|
||||
a.On(EventNameAppCmdStop, func(e Event) bool {
|
||||
isStopped = true
|
||||
wg.Done()
|
||||
return false
|
||||
})
|
||||
c := make(chan bool)
|
||||
var isAccepted bool
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c:
|
||||
isAccepted = true
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go a.acceptTCP(c)
|
||||
|
||||
// Test accepted
|
||||
wg.Add(1)
|
||||
l.c <- true
|
||||
wg.Wait()
|
||||
assert.True(t, isAccepted)
|
||||
assert.False(t, isStopped)
|
||||
|
||||
// Test refused
|
||||
isAccepted = false
|
||||
wg.Add(1)
|
||||
l.c <- true
|
||||
wg.Wait()
|
||||
assert.False(t, isAccepted)
|
||||
assert.True(t, isStopped)
|
||||
|
||||
// Test error accept
|
||||
go a.acceptTCP(c)
|
||||
isStopped = false
|
||||
wg.Add(1)
|
||||
l.e <- true
|
||||
wg.Wait()
|
||||
assert.False(t, isAccepted)
|
||||
assert.True(t, isStopped)
|
||||
}
|
||||
|
||||
func TestIsValidOS(t *testing.T) {
|
||||
assert.NoError(t, validateOS("darwin"))
|
||||
assert.NoError(t, validateOS("linux"))
|
||||
assert.NoError(t, validateOS("windows"))
|
||||
assert.Error(t, validateOS("invalid"))
|
||||
}
|
||||
|
||||
func TestAstilectron_Wait(t *testing.T) {
|
||||
a, err := New(Options{})
|
||||
assert.NoError(t, err)
|
||||
a.HandleSignals()
|
||||
go func() {
|
||||
time.Sleep(20 * time.Microsecond)
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
assert.NoError(t, err)
|
||||
p.Signal(os.Interrupt)
|
||||
}()
|
||||
a.Wait()
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asticode/go-astilectron"
|
||||
"github.com/asticode/go-astilog"
|
||||
)
|
||||
|
||||
// MessageOut represents a message going out
|
||||
type MessageOut struct {
|
||||
Name string `json:"name"`
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
// MessageIn represents a message going in
|
||||
type MessageIn struct {
|
||||
Name string `json:"name"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
// handleMessages handles messages
|
||||
func handleMessages(w *astilectron.Window, messageHandler MessageHandler) astilectron.Listener {
|
||||
return func(e astilectron.Event) (deleteListener bool) {
|
||||
// Unmarshal message
|
||||
var m MessageIn
|
||||
var err error
|
||||
if err = e.Message.Unmarshal(&m); err != nil {
|
||||
astilog.Errorf("Unmarshaling message %+v failed", *e.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle message
|
||||
messageHandler(w, m)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asticode/go-astilectron"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// Options represents options
|
||||
type Options struct {
|
||||
AdaptAstilectron AdaptAstilectron
|
||||
AdaptRouter AdaptRouter
|
||||
AdaptWindow AdaptWindow
|
||||
AstilectronOptions astilectron.Options
|
||||
BaseDirectoryPath string
|
||||
CustomProvision CustomProvision
|
||||
Debug bool
|
||||
Homepage string
|
||||
MessageHandler MessageHandler
|
||||
RestoreAssets RestoreAssets
|
||||
TemplateData TemplateData
|
||||
WindowOptions *astilectron.WindowOptions
|
||||
}
|
||||
|
||||
// AdaptAstilectron is a function that adapts astilectron
|
||||
type AdaptAstilectron func(a *astilectron.Astilectron)
|
||||
|
||||
// AdaptRouter is a function that adapts the router
|
||||
type AdaptRouter func(r *httprouter.Router)
|
||||
|
||||
// AdaptWindow is a function that adapts the window
|
||||
type AdaptWindow func(w *astilectron.Window)
|
||||
|
||||
// CustomProvision is a function that executes custom provisioning
|
||||
type CustomProvision func(baseDirectoryPath string) error
|
||||
|
||||
// MessageHandler is a functions that handles messages
|
||||
type MessageHandler func(w *astilectron.Window, m MessageIn)
|
||||
|
||||
// RestoreAssets is a function that restores assets namely the go-bindata's RestoreAssets method
|
||||
type RestoreAssets func(dir, name string) error
|
||||
|
||||
// TemplateData is a function that retrieves a template's data
|
||||
type TemplateData func(name string, r *http.Request, p httprouter.Params) (d interface{}, err error)
|
38
vendor/github.com/asticode/go-astilectron/bootstrap/provisioner.go
generated
vendored
Normal file
38
vendor/github.com/asticode/go-astilectron/bootstrap/provisioner.go
generated
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/asticode/go-astilog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// provision provisions the resources as well as the custom provision
|
||||
func provision(baseDirectoryPath string, fnA RestoreAssets, fnP CustomProvision) (err error) {
|
||||
// Provision resources
|
||||
// TODO Handle upgrades and therefore removing the resources folder accordingly
|
||||
var pr = filepath.Join(baseDirectoryPath, "resources")
|
||||
if _, err = os.Stat(pr); os.IsNotExist(err) {
|
||||
// Restore assets
|
||||
astilog.Debugf("Restoring assets in %s", baseDirectoryPath)
|
||||
if err = fnA(baseDirectoryPath, "resources"); err != nil {
|
||||
err = errors.Wrapf(err, "restoring assets in %s failed", baseDirectoryPath)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
err = errors.Wrapf(err, "stating %s failed", pr)
|
||||
return
|
||||
} else {
|
||||
astilog.Debugf("%s already exists, skipping restoring assets...", pr)
|
||||
}
|
||||
|
||||
// Custom provision
|
||||
if fnP != nil {
|
||||
if err = fnP(baseDirectoryPath); err != nil {
|
||||
err = errors.Wrap(err, "custom provisioning failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue