Fenster eingebaut

This commit is contained in:
konrad 2017-08-31 22:49:55 +02:00
parent f7a24eb969
commit 830411664c
495 changed files with 182201 additions and 64944 deletions

View File

@ -12,11 +12,16 @@ 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~~
* ~~Version einbauen, soll in der Konsole und im Adminbereich angezeigt werden (dezent)~~
* Mini-Doku
* Inbetriebnahme
* Bedienung
* Bedienung
Release: 3 Zips. Enthalten jeweils die kompilierten binaries, Templates, JS/CSS/Fonts/etc. Sozusagen Ready-to-Run.
* Win
* macOS
* Linux

View File

@ -1,26 +1,29 @@
$(document).ready(function () {
$('#loginform').submit(function (ev) {
ev.preventDefault();
console.log('sumbit');
var pass = $('#password').val();
if(pass == ""){
$('#msg').html('<div class="ui error message" style="display: block;">Bitte gib ein Passwort ein.</div>');
} else {
$.ajax({
url: '/login',
method: 'POST',
data: 'password=' + pass,
success: function (msg) {
console.log(msg);
if(msg.Message == 'success') {
$('#msg').html('<div class="ui success message" style="display: block;">Erfolgreich eingeloggt.</div>');
location.reload();
} else {
$('#msg').html('<div class="ui error message" style="display: block;">Falsches Passwort.</div>');
}
}
})
}
login();
});
});
});
function login() {
console.log('sumbit');
var pass = $('#password').val();
if(pass == ""){
$('#msg').html('<div class="ui error message" style="display: block;">Bitte gib ein Passwort ein.</div>');
} else {
$.ajax({
url: '/login',
method: 'POST',
data: 'password=' + pass,
success: function (msg) {
console.log(msg);
if(msg.Message == 'success') {
$('#msg').html('<div class="ui success message" style="display: block;">Erfolgreich eingeloggt.</div>');
location.reload();
} else {
$('#msg').html('<div class="ui error message" style="display: block;">Falsches Passwort.</div>');
}
}
})
}
}

Binary file not shown.

View File

@ -0,0 +1,4 @@
.idea/
node_modules
npm-debug.log
dist

View File

@ -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.

View File

@ -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)
})
}

View File

@ -0,0 +1,7 @@
{
"name": "Astilectron",
"description": "Electron-based cross-language application framework",
"devDependencies": {
"electron": "1.6.5"
}
}

View File

@ -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.

View File

@ -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()

View File

@ -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.

View File

@ -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.

BIN
astilectron-deps/vendor/electron/electron vendored Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
astilectron-deps/vendor/electron/libnode.so vendored Executable file

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.

View File

@ -0,0 +1 @@
v1.6.5

Binary file not shown.

1
astilectron-deps/vendor/status.json vendored Normal file
View File

@ -0,0 +1 @@
{"astilectron":{"version":"0.6.0"},"electron":{"version":"1.6.5"}}

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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()
}*/
}

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View File

@ -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">

8
vendor/github.com/asticode/go-astilectron/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
Thumbs.db
.idea/
cover.*
examples/5.single_binary_distribution/vendor.go
examples/test
examples/vendor
testdata/tmp/*

View File

@ -0,0 +1,9 @@
language: go
go:
- 1.8
- tip
matrix:
allow_failures:
- go: tip
script:
go test -v

21
vendor/github.com/asticode/go-astilectron/LICENSE generated vendored Normal file
View File

@ -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.

646
vendor/github.com/asticode/go-astilectron/README.md generated vendored Normal file
View File

@ -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.

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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)

View 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