Added electron browser to allow snapshots of doctests that are stored in lib thumbnail subfolders.

This commit is contained in:
Uwe Oestermeier 2023-05-09 15:40:18 +02:00
parent 9501264f08
commit c8f7e39235
95 changed files with 29101 additions and 509 deletions

13
bin/browser.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
if [ "$1" = "dist" ]
then
if [ -z "$2" ]
then
npm run package-all
else
npm run package-app-$2
fi
else
electron . ./lib/index.html
fi

289
browser/browser.html Normal file
View File

@ -0,0 +1,289 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
html {
height: 100%;
width: 100%;
margin: 0px;
}
::-webkit-scrollbar { display: none; }
body {
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
font-family: sans-serif;
font-size: 22pt;
-webkit-tap-highlight-color: #ccc;
background-color: #DDD;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-hyphens: auto;
hyphens: auto;
/* https://davidwalsh.name/font-smoothing */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-app-region: drag;
}
header {
position: absolute;
width: 100%;
height: 24px;
}
#progressBar {
position: absolute;
background-color: rgb(165, 165, 196);
width: 0%;
height: 24px;
}
#info {
width: 100%;
margin: 3px;
font-size: 16px;
position: absolute;
text-align:center;
}
</style>
<title>
Browser
</title>
</head>
<body style="width: 100%; height: 100%; -webkit-app-region: no-drag">
<header id="header" style="-webkit-app-region: drag">
<div id="progressBar"></div>
<span id="info">Minimal Header</span>
</header>
<main></main>
</body>
<script>
const { ipcRenderer } = require("electron")
let urls = new Set()
let favicons = new Set()
let progress = 0
info.innerHTML = window.location.href
function notify(url) {
if (urls.has(url)) return
console.log(url)
//header.innerHTML += `<p>${url}</p>`
urls.add(url)
}
let colorExtractorCanvas = document.createElement('canvas')
let colorExtractorContext = colorExtractorCanvas.getContext('2d')
let colorExtractorImage = document.createElement('img')
function getColor(url, callback) {
colorExtractorImage.onload = function (e) {
let w = colorExtractorImage.width
let h = colorExtractorImage.height
colorExtractorCanvas.width = w
colorExtractorCanvas.height = h
let offset = Math.max(1, Math.round(0.00032 * w * h))
colorExtractorContext.drawImage(colorExtractorImage, 0, 0, w, h)
let data = colorExtractorContext.getImageData(0, 0, w, h).data
let pixels = {}
let d, add, sum
for (let i = 0; i < data.length; i += 4 * offset) {
d = Math.round(data[i] / 5) * 5 + ',' + Math.round(data[i + 1] / 5) * 5 + ',' + Math.round(data[i + 2] / 5) * 5
add = 1
sum = data[i] + data[i + 1] + data[i + 2]
// very dark or light pixels shouldn't be counted as heavily
if (sum < 310) {
add = 0.35
}
if (sum < 50) {
add = 0.01
}
if (data[i] > 210 || data[i + 1] > 210 || data[i + 2] > 210) {
add = 0.5 - (0.0001 * sum)
}
if (pixels[d]) {
pixels[d] = pixels[d] + add
} else {
pixels[d] = add
}
}
// find the largest pixel set
let largestPixelSet = null
let ct = 0
for (let k in pixels) {
if (k === '255,255,255' || k === '0,0,0') {
pixels[k] *= 0.05
}
if (pixels[k] > ct) {
largestPixelSet = k
ct = pixels[k]
}
}
let res = largestPixelSet.split(',')
for (let i = 0; i < res.length; i++) {
res[i] = parseInt(res[i])
}
callback(res)
}
colorExtractorImage.src = url
}
function getTextColor(bgColor) {
let output = runNetwork(bgColor)
if (output.black > 0.5) {
return 'black'
}
return 'white'
}
var runNetwork = function anonymous(input) {
var net = {
'layers': [{
'r': {},
'g': {},
'b': {}
}, {
'0': {
'bias': 14.176907520571566,
'weights': {
'r': -3.2764240497480652,
'g': -16.90247884718719,
'b': -2.9976364179397814
}
},
'1': {
'bias': 9.086071102351246,
'weights': {
'r': -4.327474143397604,
'g': -15.780660155750773,
'b': 2.879230202567851
}
},
'2': {
'bias': 22.274487339773476,
'weights': {
'r': -3.5830205067960965,
'g': -25.498384261673618,
'b': -6.998329189107962
}
}
}, {
'black': {
'bias': 17.873962570788997,
'weights': {
'0': -15.542217788633987,
'1': -13.377152708685674,
'2': -24.52215186113144
}
}
}],
'outputLookup': true,
'inputLookup': true
}
for (var i = 1; i < net.layers.length; i++) {
var layer = net.layers[i]
var output = {}
for (var id in layer) {
var node = layer[id]
var sum = node.bias
for (var iid in node.weights) {
sum += node.weights[iid] * input[iid]
}
output[id] = (1 / (1 + Math.exp(-sum)))
}
input = output
}
return output
}
function applyColors(backgroundColor, foregroundColor) {
console.log("applyColors", backgroundColor, foregroundColor)
progressBar.style.backgroundColor = backgroundColor
info.style.color = foregroundColor
}
ipcRenderer.on('title', (sender, title) => {
info.innerHTML = title
})
ipcRenderer.on('favicons', (sender, urls) => {
console.log("favicons event", urls)
for (let url of urls) {
if (!favicons.has(url)) {
getColor(url, c => {
let cr = 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')'
let obj = {
r: c[0] / 255,
g: c[1] / 255,
b: c[2] / 255
}
let textclr = getTextColor(obj)
applyColors(cr, textclr)
})
}
favicons.add(url)
}
})
ipcRenderer.on('progress', (sender, amount) => {
console.log("progress event", amount)
if (amount > progress) {
progress = Math.min(amount, 1)
}
progressBar.style.width = Math.round(progress * 100) + '%'
})
ipcRenderer.on('did-start-loading', (sender, url) => {
console.log('did-start-loading', url)
})
ipcRenderer.on('did-get-response-details', (sender, info) => {
let {
status, newURL, originalURL,
httpResponseCode,
requestMethod,
referrer,
headers,
resourceType
} = info
notify(newURL)
notify(originalURL)
//console.log('did-get-response-details', info)
})
ipcRenderer.on('did-get-redirect-request', (sender, info) => {
let { oldURL,
newURL,
isMainFrame,
httpResponseCode,
requestMethod,
referrer,
headers
} = info
notify(newURL)
notify(oldURL)
//console.log('did-get-response-details', info)
})
ipcRenderer.on('did-stop-loading', (sender, info) => {
//console.log('did-stop-loading', info)
})
</script>
</html>

49
browser/carlo.js Normal file
View File

@ -0,0 +1,49 @@
const carlo = require('carlo');
const fse = require('fs-extra');
const urlExists = require('url-exists');
// command line arguments
let path = 'index.html'
process.argv.forEach((value, index, array) => {
if (index === 2) {
path = value
}
});
(async () => {
// Launch the browser.
const opts = {}
// Set path to custom chrome
const chrome = `${__dirname}/../chrome/chrome.exe`
if (fse.pathExistsSync(chrome)) {
opts.executablePath = chrome
}
// Launch app
const app = await carlo.launch(opts)
// Terminate Node.js process on app window closing.
app.on('exit', () => process.exit())
// Tell carlo where your web files are located.
app.serveFolder(`${__dirname}/../`)
// Check if URL exists
urlExists('https://localhost:8443', async (error, exists) => {
if (exists) {
console.info('Serve files via server')
app.serveOrigin('https://localhost:8443') // Optional
} else {
console.info('Serve files from file system')
}
// Expose 'env' function in the web environment.
await app.exposeFunction('env', _ => process.env)
// Navigate to the main page of your app.
console.info('Starting carlo with', path)
await app.load(path)
})
})()

24
browser/i18n.js Normal file
View File

@ -0,0 +1,24 @@
const path = require('path')
const electron = require('electron')
const fs = require('fs')
let loadedLanguage
let app = electron.app ? electron.app : electron.remote.app
module.exports = i18n
function i18n() {
if (fs.existsSync(path.join(__dirname, 'i18n', app.getLocale() + '.js'))) {
loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'i18n', app.getLocale() + '.js'), 'utf8'))
}
else {
loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'i18n', 'en.js'), 'utf8'))
}
}
i18n.prototype.__ = function(phrase) {
let translation = loadedLanguage[phrase]
if (translation === undefined) {
translation = phrase
}
return translation
}

61
browser/i18n/de.js Normal file
View File

@ -0,0 +1,61 @@
{
"edit": "Bearbeiten",
"undo": "Widerrufen",
"redo": "Wiederholen",
"cut": "Ausschneiden",
"copy": "Kopieren",
"paste": "Einsetzen",
"pasteandmatchstyle": "Einsetzen und Stil anpassen",
"delete": "Löschen",
"selectall": "Alles auswählen",
"view": "Darstellung",
"reload": "Seite neu laden",
"forcereload": "Cache löschen und Seite neu laden",
"resetzoom": "Originalgröße",
"zoomin": "Vergrößern",
"zoomout": "Verkleinern",
"togglefullscreen": "Vollbildmodus umschalten",
"minimalpad": "Minimal-Pad",
"multiuserbrowser": "Mehrbenutzer-Browser",
"history": "Verlauf",
"back": "Zurück",
"forward": "Vorwärts",
"home": "Startseite",
"recentlyvisited": "Kürzlich besucht",
"bookmarks": "Lesezeichen",
"localfilesystem": "Lokales Dateisystem",
"testframes": "Testseiten",
"develop": "Entwickler",
"toggledevelopertools": "Webinformationen umschalten",
"openprocessmonitor": "Prozessmonitor öffnen",
"selectfolder": "Datenverzeichnis auswählen...",
"selectfolder.noadmin.ok": "OK",
"selectfolder.noadmin.message": "Keine ausreichenden Berechtigungen",
"selectfolder.noadmin.detail": "Um das Datenverzeichnis zu ändern, muss der IWM Browser mit Administrator-Berechtigungen gestartet werden.",
"selectfolder.warning.next": "Weiter",
"selectfolder.warning.cancel": "Abbrechen",
"selectfolder.warning.message": "Datenverzeichnis vorhanden",
"selectfolder.warning.detail": "Ihr IWM Browser besitzt bereits ein (verlinktes) Datenverzeichnis. Wenn Sie fortfahren, wird das alte Verzeichnis gesichert und ein neues wird erstellt.",
"selectfolder.select.title": "Datenverzeichnis wählen",
"selectfolder.select.buttonLabel": "Auswählen",
"selectfolder.samefolder.ok": "OK",
"selectfolder.samefolder.message": "Ungültiges Datenverzeichnis",
"selectfolder.samefolder.detail.same": "Das alte Datenverzeichnis darf nicht als neues Verzeichnis ausgewählt werden.",
"selectfolder.samefolder.detail.within": "Das neue Datenverzeichnis darf sich nicht innerhalb des alten Verzeichnisses befinden.",
"selectfolder.info.ok": "OK",
"selectfolder.info.message": "Link auf Datenverzeichnis erstellt",
"selectfolder.info.detail": "Der IWM Browser verwendet nun den Ordner \"${0}\" als neues Datenverzeichnis.",
"startserver": "Starte Server",
"stopserver": "Stoppe Server",
"runloadtests": "Starte Ladetests",
"window": "Fenster",
"close": "Fenster schließen",
"minimize": "Im Dock ablegen",
"zoom": "Zoomen",
"front": "Alle nach vorne bringen",
"screenshot": "Bildschirmfoto erstellen",
"help": "Hilfe",
"iwm": "Leibniz-Institut für Wissensmedien",
"about": "Über IWM Browser",
"quit": "IWM Browser beenden"
}

61
browser/i18n/en.js Normal file
View File

@ -0,0 +1,61 @@
{
"edit": "Edit",
"undo": "Undo",
"redo": "Redo",
"cut": "Cut",
"copy": "Copy",
"paste": "Paste",
"pasteandmatchstyle": "Paste and Match Style",
"delete": "Delete",
"selectall": "Select all",
"view": "View",
"reload": "Reload",
"forcereload": "Force Reload",
"resetzoom": "Actual size",
"zoomin": "Zoom in",
"zoomout": "Zoom out",
"togglefullscreen": "Toggle Full Screen",
"minimalpad": "Minimal Pad",
"multiuserbrowser": "Multi-User Browser",
"history": "History",
"back": "Back",
"forward": "Forward",
"home": "Home",
"recentlyvisited": "Recently Visited",
"bookmarks": "Bookmarks",
"localfilesystem": "Local Filesystem",
"testframes": "Test Frames",
"develop": "Develop",
"toggledevelopertools": "Toggle Developer Tools",
"openprocessmonitor": "Open Process Monitor",
"selectfolder": "Select data folder...",
"selectfolder.noadmin.ok": "OK",
"selectfolder.noadmin.message": "Insufficient permissions",
"selectfolder.noadmin.detail": "To change the data directory, the IWM Browser must be started with administrator privileges.",
"selectfolder.warning.next": "Next",
"selectfolder.warning.cancel": "Cancel",
"selectfolder.warning.message": "Data folder exists",
"selectfolder.warning.detail": "Your IWM Browser already has a (linked) data directory. If you continue, the old directory is backed up and a new one is created.",
"selectfolder.select.title": "Select data folder",
"selectfolder.select.buttonLabel": "Select",
"selectfolder.samefolder.ok": "OK",
"selectfolder.samefolder.message": "Invalid data folder",
"selectfolder.samefolder.detail.same": "The old data directory cannot be selected as the new directory.",
"selectfolder.samefolder.detail.within": "The new data directory cannot be inside the old directory.",
"selectfolder.info.ok": "OK",
"selectfolder.info.message": "Created link to data folder",
"selectfolder.info.detail": "The IWM Browser now uses the folder \"${0}\" as the new data folder.",
"startserver": "Start Server",
"stopserver": "Stop Server",
"runloadtests": "Run Load Tests",
"window": "Window",
"close": "Close",
"minimize": "Minimize",
"zoom": "Zoom",
"front": "Bring All to Front",
"screenshot": "Make Screenshot",
"help": "Help",
"iwm": "Leibniz-Institut für Wissensmedien",
"about": "About IWM Browser",
"quit": "Quit IWM Browser"
}

578
browser/main.js Normal file
View File

@ -0,0 +1,578 @@
/* globals require, __dirname, process */
/*eslint no-console: ["error", { allow: ["log"] }]*/
const { app, BrowserWindow, BrowserView, ipcMain, dialog, shell } = require('electron')
const electronLocalshortcut = require('electron-localshortcut')
//const electron = require('electron')
const os = require('os')
const fs = require('fs')
const path = require('path')
const { URL } = require('url')
const Store = require('./store.js')
const { prettyPrint } = require('html')
// Use this constant to start the application in kiosk-mode or in development mode
const DEVELOPMENT = true
// true: Dev-Tools are open
// false (KIOSK-Mode): No application switcher, no menu, no taskbar (or dock on a mac), shortcuts are working
global.multiUserMode = true
global.errorCount = 0
global.stopTestsOnError = false
global.jsonData = { value: null }
// UO: Experimental feature using Native Windows
global.useBrowserView = false
global.useMinimalPad = true
global.menu = null
global.observeTraffic = false
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win
let browsers = new Map() // url: BrowserWindow
const store = new Store({
// We'll call our data file 'user-preferences'
configName: 'user-preferences',
defaults: {
url: `file://${__dirname}/index.html`,
devTools: DEVELOPMENT,
multiUserBrowser: true
}
})
function createWindow() {
if (global.observeTraffic) {
const {session} = require('electron')
session.defaultSession.webRequest.onCompleted((details) => {
console.log("onCompleted", details.url)
})
}
let { screen } = require('electron')
let bounds = store.get('storedBounds')
? store.get('storedBounds')
: screen.getPrimaryDisplay().bounds
// let displays = screen.getAllDisplays()
// let externalDisplay = null
// externalDisplay = displays[displays.length-1]
// const {width, height} =displays[displays.length-1].workAreaSize
// externalDisplay = displays[0]
// const {width, height} =displays[0].workAreaSize
win = new BrowserWindow({
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
fullscreenable: true,
fullscreen: !DEVELOPMENT,
title: 'IWM Browser',
show: false,
kiosk: !DEVELOPMENT,
acceptFirstMouse: true,
webPreferences: {
webSecurity: false,
allowRunningInsecureContent: true,
nodeIntegration: true,
webviewTag: true,
nativeWindowOpen: true,
devTools: true,
preload: path.join(__dirname, './preload.js')
},
icon: path.join(__dirname, 'assets/icons/png/64x64.png')
})
module.exports.win = win
let url = store.get('url')
if (process.argv.length > 2) {
let path = process.argv[2]
url = `file://${__dirname}/../${path}`
console.log('Using process.argv[2]', url)
}
console.log('Using', url)
win.maximize()
// BAD: All other methods don't work (like ensureFileSync, fileExists...)
try {
let settings = require('./settings.json')
console.log('Using settings', `file://${__dirname}/${settings.url}`)
win.loadURL(`file://${__dirname}/${settings.url}`)
} catch (ex) {
win.loadURL(url)
}
const { webContents } = win
//
// if (process.platform === 'win32' && win.isKiosk()) {
// webContents.on('did-finish-load', function() {
// webContents.executeJavaScript('document.body.style.cursor = "none";')
// })
// }
// Add the app menu
let menu = require('./menu.js')
global.menu = menu
// Add global shortcuts
// Esc quits the app
electronLocalshortcut.register('Esc', () => {
app.quit()
})
// Command (Mac) or Control (Win) + K toggles the Kiosk mode
electronLocalshortcut.register('CommandOrControl+K', () => {
if (win) {
win.setKiosk(!win.isKiosk())
}
})
// Show if its ready.
win.once('ready-to-show', () => {
webContents.send('preparePads')
win.show()
})
// Clear cache
webContents.session.clearCache(() => console.log('Cache cleared'))
// Open dev tools when in development mode
if (store.get('devTools')) {
webContents.openDevTools({ mode: 'right' })
} else {
webContents.closeDevTools()
}
webContents.on('devtools-opened', () => {
store.set('devTools', true)
})
webContents.on('devtools-closed', () => {
store.set('devTools', false)
})
webContents.on('did-navigate', (event, url) => {
menu.setHistoryStatus()
})
/* UO: At this point we have no access to the event or link position*/
webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => {
console.log('new-window', global.multiUserMode)
if (global.multiUserMode) {
event.preventDefault()
webContents.send('newPad', url, options.x, options.y)
}
})
// WORKAROUND: On windows, if the app was set to fullscreen, the menubar is not hidden
if (win.isKiosk()) {
win.setMenuBarVisibility(false)
}
win.on('focus', event => {
menu.focus()
})
win.on('blur', event => {
menu.blur()
})
win.on('enter-full-screen', () => {
win.setMenuBarVisibility(false)
})
win.on('leave-full-screen', () => {
win.setMenuBarVisibility(true)
})
win.on('enter-html-full-screen', () => {
win.setMenuBarVisibility(false)
})
win.on('leave-html-full-screen', () => {
win.setMenuBarVisibility(true)
})
win.on('close', () => {
store.set('storedBounds', win.getBounds())
})
// Emitted when the window is closed.
win.on('closed', () => {
app.quit()
})
}
// When work makes progress, show the progress bar
function onProgress(progress) {
// Use values 0 to 1, or -1 to hide the progress bar
try {
win.setProgressBar(progress || -1) // Progress bar works on all platforms
} catch (e) {
if (DEVELOPMENT) console.log(e.message)
}
}
function trySend(target, ...args) {
try {
target.send(...args)
} catch (e) {
if (DEVELOPMENT) console.log(e.message)
}
}
function openBrowserView(url, x, y) {
const useMinBrowser = false // Change this to switch between Min and a custom browser
const minURL = 'file:///Users/uo/devel/min/index.html'
const browserURL = `file://${__dirname}/browser.html`
let width = 640
let height = 1200
let [winWidth, winHeight] = win.getSize()
if (x + width > winWidth) {
x = winWidth - width
}
if (y + height > winHeight) {
y = winHeight - height
}
console.log('open browser view')
let browser = new BrowserWindow({
x,
y,
width,
height,
minWidth: 320,
minHeight: 350,
titleBarStyle: useMinBrowser ? 'hidden-inset' : 'hidden',
frame: process.platform !== 'win32'
})
let browserContents = browser.webContents
browser.setAlwaysOnTop(true)
if (useMinBrowser) {
browserContents.on('did-finish-load', event => {
console.log('did-finish-load', browserContents.getURL())
browserContents.executeJavaScript(
'Object.values(window.webviews.elementMap).map(obj => obj.src)',
result => {
console.log(
'window.webviews',
result,
url,
result.indexOf(url)
)
if (result.indexOf(url) == -1) {
console.log('Adding tab')
browserContents.send('addTab', { url })
}
}
)
})
browser.loadURL(minURL)
} else {
console.log('Loading', browserURL)
browser.loadURL(browserURL)
let view = new BrowserView({
webPreferences: {
nodeIntegration: false,
devTools: true
}
})
//browserContents.openDevTools({mode: 'right'})
browser.setBrowserView(view)
view.setBounds({ x: 0, y: 24, width: width, height: height - 24 })
view.setAutoResize({ width: true, height: true })
let viewContents = view.webContents
let progress = 0
viewContents.on('page-title-set', event => {
console.log('page-title-set', event)
})
viewContents.on('page-favicon-updated', (event, favicons) => {
//console.log("page-favicon-updated", event, favicons)
trySend(browserContents, 'favicons', favicons)
})
viewContents.on('did-start-loading', event => {
onProgress(0)
trySend(browserContents, 'progress', 0)
trySend(browserContents, 'did-start-loading')
//let senderURL = event.sender.getURL() || url
//console.log('did-start-loading', senderURL)
})
viewContents.on(
'did-get-response-details',
(
event,
status,
newURL,
originalURL,
httpResponseCode,
requestMethod,
referrer,
headers,
resourceType
) => {
trySend(browserContents, 'did-get-response-details', {
status,
newURL,
originalURL,
httpResponseCode,
requestMethod,
referrer,
headers,
resourceType
})
progress += 0.01
onProgress(progress)
trySend(browserContents, 'progress', progress)
//console.log('did-get-response-details', newURL)
}
)
viewContents.on(
'did-get-redirect-request',
(
event,
oldURL,
newURL,
isMainFrame,
httpResponseCode,
requestMethod,
referrer,
headers
) => {
trySend(browserContents, 'did-get-redirect-request', {
oldURL,
newURL,
isMainFrame,
httpResponseCode,
requestMethod,
referrer,
headers
})
//console.log('did-get-redirect-request', newURL)
}
)
viewContents.on('did-stop-loading', event => {
//console.log('did-stop-loading', event.sender.getURL())
trySend(browserContents, 'did-stop-loading')
})
viewContents.on('did-finish-load', event => {
//console.log('did-finish-load', event.sender.getURL())
progress = 1
onProgress(progress)
trySend(browserContents, 'progress', progress)
})
viewContents.on('dom-ready', event => {
if (progress < 0.5) {
progress = 0.5
onProgress(progress)
trySend(browserContents, 'progress', progress)
}
viewContents.executeJavaScript('document.title', result => {
trySend(browserContents, 'title', result)
})
//console.log('dom-ready', event.sender.getURL())
})
viewContents.on('new-window', function (event, url) {
event.preventDefault()
console.log('new-window')
openBrowserView(url, x, y)
})
viewContents.loadURL(url)
}
browsers.set(url, browser)
browser.on('closed', e => {
for (let [url, browser] of browsers.entries()) {
if (browser == e.sender) {
browsers.delete(url)
console.log('removed browser view', url)
}
}
})
}
// UO: Experimental call. Opens a Min Browser window or a limited window with a browser view
ipcMain.on('loadBrowserView', (e, opts = {}) => {
let { url, x, y } = opts
openBrowserView(url, x, y)
})
ipcMain.on('multiUserMode', (e, opts = {}) => {
global.multiUserMode = opts
})
ipcMain.on('padContainerLoaded', e => {
win.webContents.send('padContainerAvailable')
})
ipcMain.on('createScreenshot', (e, opts = {}) => {
opts = Object.assign(
{},
{
name: `iwmbrowser-${new Date()
.toISOString()
.replace(/:/g, '-')}.png`,
path: os.tmpdir()
},
opts
)
win.webContents.capturePage(image => {
if (image) {
let file = path.join(opts.path, opts.name)
fs.writeFile(file, image.toPNG(), err => {
if (err) {
//throw err
} else {
console.log(`Screenshot saved: ${file}`)
}
})
}
})
})
ipcMain.on('directoryListing', (e, opts = {}) => {
let { directory, files, folders } = opts
console.log("directoryListing", opts)
try {
let listing = fs.readdirSync(directory)
let result = { directory, files: [], folders: [] }
for (let name of listing) {
if (name.startsWith('.'))
continue
let fullPath = path.join(directory, name)
let stat = fs.lstatSync(fullPath)
if (files && stat.isFile()) {
if (typeof files == 'string' && !files.endsWith(files))
continue
result.files.push(name)
}
if (folders && stat.isDirectory())
result.folders.push(name)
}
e.sender.send('directoryListing', result)
} catch (err) {
let args = { directory, errorMessage: err.message}
e.sender.send('directoryListingError', args)
}
})
ipcMain.on('createTextfile', (e, opts = {}) => {
opts = Object.assign(
{},
{
name: `iwmbrowser-${new Date()
.toISOString()
.replace(/:/g, '-')}.txt`,
path: os.tmpdir(),
text: ''
},
opts
)
let file = path.join(opts.path, opts.name)
fs.writeFile(file, opts.text, err => {
if (err) {
//throw err
} else {
console.log(`Textfile saved: ${file}`)
}
})
})
ipcMain.on('error', e => {
console.log('Received error notification')
global.errorCount += 1
})
ipcMain.on('openExternal', (e, url=null) => {
console.log('Received openExternal', url)
if (url) {
shell.openExternal(url)
}
})
ipcMain.on('save', (e, opts = {}) => {
let { url, html, saveAs, action } = opts
// url must absolute URL
let urlObj = new URL(url)
let pathname = urlObj.pathname
if (saveAs) {
pathname = dialog.showSaveDialog(win, { title: 'Save as:', defaultPath: pathname })
if (typeof pathname == 'undefined')
return
}
try {
console.log("Saving", pathname, action)
html = prettyPrint(html, { indent_size: 4 });
fs.writeFileSync(pathname, html, 'utf-8')
if (saveAs) {
let normalized = pathname.replace(/\\/g, '/')
e.sender.send('savedAs', {url: `file://${normalized}`, action})
}
}
catch (e) {
console.warn('Failed to save the file', pathname)
e.sender.send('saveFailed', pathname)
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on(
'select-client-certificate',
(event, webContents, url, list, callback) => {
console.log('select-client-certificate', url, list)
event.preventDefault()
ipc.once('client-certificate-selected', (event, item) => {
console.log('selected:', item)
callback(item)
})
mainWindow.webContents.send('select-client-certificate', list)
}
)
app.on(
'certificate-error',
(event, webContents, url, error, certificate, callback) => {
console.log('certificate-error', url)
event.preventDefault()
const result = true // TODO: do real validation here
callback(result)
}
)
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})
module.exports = {
store: store
}

629
browser/menu.js Normal file
View File

@ -0,0 +1,629 @@
/* globals require, process */
/*eslint no-console: ["error", { allow: ["log", "error"] }] */
const { Menu, app, shell, dialog } = require('electron')
const fs = require('fs')
const fse = require('fs-extra')
const os = require('os')
const path = require('path')
const { openProcessManager } = require('electron-process-manager')
const main = require('./main.js')
let { thumbnail } = require('./utils.js')
const loadTests = require('./test.js')
const i18n = new (require('./i18n.js'))()
function selectURL(url) {
url = url.replace(/\\/g, '/')
console.log('selectURL', url)
main.win.loadURL(url)
main.store.set('url', url)
}
function findItems(key, value) {
let items = []
for (let i = 0; i < menu.items.length; i++) {
for (let j = 0; j < menu.items[i].submenu.items.length; j++) {
let item = menu.items[i].submenu.items[j]
if (item[key] === value) {
items.push(item)
}
}
}
return items
}
function findItem(key, value) {
return findItems(key, value)[0]
}
function toggleBookmarks(bookmark) {
let items = findItems('class', 'bookmark')
for (let i = 0; i < items.length; i++) {
items[i].checked = false
}
bookmark.checked = true
}
function checkBookmark(url) {
let items = findItems('url', url)
if (items.length === 1) {
toggleBookmarks(items[0])
}
}
function setHistoryStatus() {
const historyBack = findItem('id', 'history-back')
historyBack.enabled = main.win.webContents.canGoBack()
const historyForward = findItem('id', 'history-forward')
historyForward.enabled = main.win.webContents.canGoForward()
}
function showSelectDataFolderDialog(focusedWindow) {
dialog.showOpenDialog(
{
title: i18n.__('selectfolder.select.title'),
buttonLabel: i18n.__('selectfolder.select.buttonLabel'),
properties: ['openDirectory', 'createDirectory', 'noResolveAliases', 'treatPackageAsDirectory']
},
(filePaths) => {
if (filePaths && filePaths.length === 1) {
const varPath = path.join(__dirname, '../var')
// Check if the same folder was used
if (filePaths[0].startsWith(varPath)) {
const same = filePaths[0] === varPath
dialog.showMessageBox(
{
type: 'error',
icon: path.join(__dirname, '../assets/icons/png/512x512-empty.png'),
buttons: [i18n.__('selectfolder.samefolder.ok')],
defaultId: 0,
message: i18n.__('selectfolder.samefolder.message'),
detail: same
? i18n.__('selectfolder.samefolder.detail.same')
: i18n.__('selectfolder.samefolder.detail.within'),
cancelId: 0
},
(response) => {
showSelectDataFolderDialog(focusedWindow)
}
)
} else {
// Backup
if (fse.pathExistsSync(varPath)) {
const varPathBackup = findNextVarFolder()
// Rename old var folder or link
fse.renameSync(varPath, varPathBackup)
} else {
// BUG: Workaround because pathExistsSync return false on existing symbolic links with a missing target
fse.removeSync(varPath)
}
// Add new symlink
main.store.set('dataFolder', filePaths[0])
fs.symlinkSync(filePaths[0], varPath, 'dir')
dialog.showMessageBox(
{
type: 'info',
icon: path.join(__dirname, '../assets/icons/png/link.png'),
buttons: [i18n.__('selectfolder.info.ok')],
defaultId: 0,
message: i18n.__('selectfolder.info.message'),
detail: i18n.__('selectfolder.info.detail').replace(/\$\{0\}/, filePaths[0]),
cancelId: 0
},
(response) => {
if (focusedWindow) focusedWindow.reload()
}
)
}
}
}
)
}
function findNextVarFolder() {
let exists = true
let counter = 0
while (exists) {
counter++
exists = fse.pathExistsSync(path.join(__dirname, `../var${counter}`))
}
return path.join(__dirname, `../var${counter}`)
}
function showFolderBrowser(focusedWindow) {
const varPath = path.join(__dirname, '../var')
const varPathExists = fse.pathExistsSync(varPath)
if (varPathExists) {
dialog.showMessageBox(
{
type: 'warning',
icon: path.join(__dirname, '../assets/icons/png/512x512-empty.png'),
buttons: [i18n.__('selectfolder.warning.next'), i18n.__('selectfolder.warning.cancel')],
defaultId: 1,
message: i18n.__('selectfolder.warning.message'),
detail: i18n.__('selectfolder.warning.detail'),
cancelId: 1
},
(response) => {
if (response === 0) {
showSelectDataFolderDialog(focusedWindow)
}
}
)
} else {
showSelectDataFolderDialog(focusedWindow)
}
}
const template = [
{
label: i18n.__('edit'),
submenu: [
{
role: 'undo',
label: i18n.__('undo')
},
{
role: 'redo',
label: i18n.__('redo')
},
{
type: 'separator'
},
{
role: 'cut',
label: i18n.__('cut')
},
{
role: 'copy',
label: i18n.__('copy')
},
{
role: 'paste',
label: i18n.__('paste')
},
{
role: 'pasteandmatchstyle',
label: i18n.__('pasteandmatchstyle')
},
{
role: 'delete',
label: i18n.__('delete')
},
{
role: 'selectall',
label: i18n.__('selectall')
}
]
},
{
label: i18n.__('view'),
submenu: [
{
label: i18n.__('reload'),
accelerator: 'CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.setVisualZoomLevelLimits(1, 1)
focusedWindow.reload()
}
}
},
{
id: 'forcereload',
label: i18n.__('forcereload'),
accelerator: 'CmdOrCtrl+Shift+R',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.session.clearCache(() => console.log('Cache cleared'))
focusedWindow.webContents.setVisualZoomLevelLimits(1, 1)
focusedWindow.reload()
}
}
},
{
type: 'separator'
},
{
role: 'resetzoom',
label: i18n.__('resetzoom')
},
{
role: 'zoomin',
label: i18n.__('zoomin')
},
{
role: 'zoomout',
label: i18n.__('zoomout')
},
{
type: 'separator'
},
{
id: 'togglefullscreen',
label: i18n.__('togglefullscreen'),
accelerator: process.platform === 'darwin' ? 'Cmd+Ctrl+F' : 'F11',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
}
}
},
{
type: 'separator'
},
{
label: i18n.__('multiuserbrowser'),
accelerator: 'CmdOrCtrl+M',
type: 'checkbox',
checked: true,
click(item, focusedWindow) {
if (focusedWindow) {
main.store.set('multiUserBrowser', item.checked)
global.multiUserMode = item.checked
}
}
},
{
label: i18n.__('minimalpad'),
accelerator: 'CmdOrCtrl+p',
type: 'checkbox',
checked: true,
click(item, focusedWindow) {
if (focusedWindow) {
main.store.set('minimalPad', item.checked)
global.useMinimalPad = item.checked
}
}
}
]
},
{
label: i18n.__('history'),
submenu: [
{
id: 'history-back',
label: i18n.__('back'),
accelerator: 'CmdOrCtrl+Left',
click(item, focusedWindow) {
main.win.webContents.goBack()
}
},
{
id: 'history-forward',
label: i18n.__('forward'),
accelerator: 'CmdOrCtrl+Right',
click(item, focusedWindow) {
main.win.webContents.goForward()
}
},
{
label: i18n.__('home'),
accelerator: 'CmdOrCtrl+Up',
click(item, focusedWindow) {
main.win.webContents.goToIndex(0)
}
},
{
type: 'separator'
},
{
label: i18n.__('recentlyvisited'),
enabled: false
}
]
},
{
label: i18n.__('bookmarks'),
submenu: [
{
label: i18n.__('localfilesystem'),
class: 'bookmark',
type: 'checkbox',
url: `file://${__dirname}/../index.html`,
accelerator: 'CmdOrCtrl+L',
click(item, focusedWindow) {
selectURL(item.url)
toggleBookmarks(item)
}
},
{
label: i18n.__('testframes'),
class: 'bookmark',
type: 'checkbox',
url: `file://${__dirname}/../index.html?test`,
accelerator: 'CmdOrCtrl+T',
click(item, focusedWindow) {
selectURL(item.url)
toggleBookmarks(item)
}
},
{
type: 'separator'
},
{
id: 'localhost',
label: 'https://localhost:8443',
class: 'bookmark',
type: 'checkbox',
enabled: false,
url: 'https://localhost:8443/index.html',
click(item, focusedWindow) {
selectURL(item.url)
toggleBookmarks(item)
}
},
{
id: 'localhost',
label: 'https://localhost:3000',
class: 'bookmark',
type: 'checkbox',
enabled: true,
url: 'https://localhost:3000/index.html',
click(item, focusedWindow) {
selectURL(item.url)
toggleBookmarks(item)
}
},
// {
// label: 'http://tornado.iwm-kmrc.de:8000',
// class: 'bookmark',
// type: 'checkbox',
// url: 'http://tornado.iwm-kmrc.de:8000/index.html',
// click(item, focusedWindow) {
// selectURL(item.url)
// toggleBookmarks(item)
// }
// },
{
label: 'http://rousseau.iwm-kmrc.de/index.html',
class: 'bookmark',
type: 'checkbox',
url: 'http://rousseau.iwm-kmrc.de/index.html',
click(item, focusedWindow) {
selectURL(item.url)
toggleBookmarks(item)
}
}
]
},
{
label: i18n.__('develop'),
submenu: [
{
id: 'toggledevelopertools',
label: i18n.__('toggledevelopertools'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) {
if (focusedWindow) focusedWindow.webContents.toggleDevTools()
}
},
{
label: i18n.__('openprocessmonitor'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+P' : 'Ctrl+Shift+P',
click(item, focusedWindow) {
openProcessManager()
}
},
{
type: 'separator'
},
{
label: i18n.__('selectfolder'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+D' : 'Ctrl+Shift+D',
click(item, focusedWindow) {
if (process.platform === 'win32') {
var exec = require('child_process').exec
exec('NET SESSION', function (err, so, se) {
const admin = se.length === 0 ? true : false
if (admin) {
showFolderBrowser(focusedWindow)
} else {
dialog.showMessageBox({
type: 'error',
icon: path.join(__dirname, '../assets/icons/png/512x512-empty.png'),
buttons: [i18n.__('selectfolder.noadmin.ok')],
message: i18n.__('selectfolder.noadmin.message'),
detail: i18n.__('selectfolder.noadmin.detail')
})
}
})
} else {
showFolderBrowser(focusedWindow)
}
}
},
{
type: 'separator'
},
{
id: 'startserver',
label: i18n.__('startserver'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+S' : 'Ctrl+Shift+S',
click(item, focusedWindow) {
const { server } = require('../server/main.js')
server.start()
item.visible = false
findItem('id', 'stopserver').visible = true
findItem('id', 'localhost').enabled = true
}
},
{
id: 'stopserver',
label: i18n.__('stopserver'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+S' : 'Ctrl+Shift+S',
visible: false,
click(item, focusedWindow) {
const { server } = require('../server/main.js')
server.stop()
item.visible = false
findItem('id', 'startserver').visible = true
findItem('id', 'localhost').enabled = false
}
},
{
type: 'separator'
},
{
label: i18n.__('runloadtests'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+L' : 'Ctrl+Shift+L',
click(item, focusedWindow) {
loadTests(focusedWindow)
}
},
{
type: 'separator'
},
{
label: 'Aktualisiere Tüsch POIs',
click(item, focusedWindow) {
const UpdatePOI = require('../dev/tuesch/bin/menu/update-pois.js')
UpdatePOI.update('./dev/tuesch')
}
}
]
},
{
role: 'window',
label: i18n.__('window'),
submenu: [
{
role: 'close',
label: i18n.__('close')
},
{
role: 'minimize',
label: i18n.__('minimize')
},
{
role: 'zoom',
label: i18n.__('zoom')
},
{
type: 'separator'
},
{
role: 'front',
label: i18n.__('front')
},
{
type: 'separator'
},
{
label: i18n.__('screenshot'),
accelerator: 'CmdOrCtrl+S',
async click(item, focusedWindow) {
if (focusedWindow) {
await focusedWindow.webContents.capturePage().then((image) => {
let screenshotFile = path.join(os.tmpdir(), 'screenshot.png')
console.log('image captured', screenshotFile)
let url = focusedWindow.webContents.getURL()
if (url.startsWith('file://')) {
let normalized = path.normalize(url).replace('.html', '.png')
screenshotFile = normalized.replace('file:', '')
let thumbnailFile = screenshotFile.replace('index.png', 'thumbnail.png')
if (url.endsWith('index.html')) {
thumbnailFile = screenshotFile.replace('index.png', 'thumbnail.png')
} else {
let folderName = path.dirname(screenshotFile)
let baseName = path.basename(screenshotFile)
thumbnailFile = path.join(folderName, 'thumbnails', baseName)
}
fs.writeFile(thumbnailFile, thumbnail(image), (err) => {
if (err) {
throw err
} else {
console.log(`Thumbnail written to ${thumbnailFile}`)
}
})
}
fs.writeFile(screenshotFile, image.toPNG(), (err) => {
if (err) {
throw err
} else {
console.log(`Screenshot written to ${screenshotFile}`)
}
})
})
}
}
},
{
type: 'separator'
}
]
},
{
role: 'help',
label: i18n.__('help'),
submenu: [
{
label: i18n.__('iwm'),
click() {
shell.openExternal('https://www.iwm-tuebingen.de')
}
}
]
}
]
if (process.platform === 'darwin') {
const name = app.getName()
template.unshift({
label: name,
submenu: [
{
role: 'about',
label: i18n.__('about')
},
{
type: 'separator'
},
{
role: 'quit',
label: i18n.__('quit')
}
]
})
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
checkBookmark(main.store.get('url'))
setHistoryStatus()
function focus() {
findItem('id', 'forcereload').enabled = true
findItem('id', 'togglefullscreen').enabled = true
findItem('id', 'toggledevelopertools').enabled = true
}
function blur() {
findItem('id', 'forcereload').enabled = false
findItem('id', 'togglefullscreen').enabled = false
findItem('id', 'toggledevelopertools').enabled = false
}
module.exports = {
menu,
setHistoryStatus,
focus,
blur
}

171
browser/package.js Normal file
View File

@ -0,0 +1,171 @@
/* globals require, __dirname, process */
/*eslint no-console: ["error", { allow: ["log", "info", "warn", "error"] }]*/
const fse = require('fs-extra')
const path = require('path')
const packager = require('electron-packager')
const rebuild = require('electron-rebuild')
// Arguments
//----------------------
let folder = null
if (process.argv.length < 3) {
console.error('Missing command line parameter "folder"!')
process.exit(1)
} else {
folder = process.argv[2]
}
// Settings
//----------------------
let settings = null
const root = path.join(__dirname, '../')
try {
settings = require(`../${folder}/settings.json`)
} catch (e) {
console.error('Cannot read settings.json in folder, does it exist?')
process.exit(1)
}
// Read settings
//----------------------
const title = `--- Build "${settings.name || settings.id}" ---`
const line = Array(title.length + 1).join('-')
console.info(line)
console.info(title)
console.info(line)
// Using folder
//----------------------
const tempFolder = path.join(root, 'temp', settings.id)
console.log(`Using folder ${tempFolder}`)
// Delete temp folder (when last run aborted)
fse.removeSync(tempFolder)
console.log(`Folder ${tempFolder} deleted`)
// Create folder
fse.ensureDirSync(tempFolder)
console.log(`Folder ${tempFolder} created`)
// Create subfolders
const defaultFolders = ['assets', 'browser', 'css', 'lib', 'node_modules', 'server']
console.log(`The folders ${defaultFolders.join(', ')} are included by default`)
const folders = new Set(settings.browser.folders.concat(defaultFolders))
for (let folder of folders) {
console.log(`Copy folder ${folder}`)
const folderOld = path.join(root, folder)
const folderNew = path.join(root, 'temp', settings.id, folder)
fse.copySync(folderOld, folderNew)
}
// Write package.json
//----------------------
let json = {
name: settings.id,
productName: settings.name || settings.id,
version: settings.version,
main: 'browser/main.js',
dependencies: {}
}
// Read and write dependencies
const packageJson = fse.readJsonSync(path.join(root, 'package.json'))
Object.assign(json.dependencies, packageJson.dependencies)
// Add browser dependencies
if (settings.browser.dependencies) {
let dependencies = {}
for (let dependency of settings.browser.dependencies) {
dependencies[dependency] = '*'
}
Object.assign(json.dependencies, dependencies)
}
console.log('Create package.json')
fse.writeJsonSync(path.join(tempFolder, 'package.json'), json, {spaces: 4})
// Write URL to settings.json
//----------------------
console.log('Write URL to browser/settings.json')
fse.writeJsonSync(path.join(tempFolder, 'browser/settings.json'), {url: `../${folder}/index.html`}, {spaces: 4})
// Build with electron-packager
//----------------------
console.log('Start electron-packager')
packager({
dir: `./temp/${settings.id}`,
arch: 'x64',
asar: false,
overwrite: true,
out: './dist/electron',
icon: './assets/icons/icon',
platform: settings.browser.platform || ['darwin', 'win32'],
prune: false,
afterCopy: [(buildPath, electronVersion, platform, arch, callback) => {
console.log(`Rebuild Node.js modules for ${platform}...`)
rebuild.rebuild({buildPath, electronVersion, arch})
.then(() => {
console.log(`...Node.js modules for ${platform} rebuilded`)
callback()
})
.catch(error => {
console.error(`Error: ${error}`)
callback(error)
});
}]
})
.then(appPaths => {
console.log('electron-packager finished')
// Delete temp folder
//----------------------
fse.removeSync(tempFolder)
console.log(`Folder ${tempFolder} deleted`)
// Write data folders
//----------------------
if (settings.browser.data) {
console.log('Copy data folders')
for (let folder of settings.browser.data) {
for (let appPath of appPaths) {
console.log(`Copy folder ${folder} to ${appPath}`)
const source = path.join(root, folder)
const target = path.join(getResourcesPath(root, appPath), folder)
fse.copySync(source, target, {
dereference: true,
filter: item => {
if (settings.browser.dataExtensions && fse.lstatSync(item).isFile() && !settings.browser.dataExtensions.includes(path.extname(item).substring(1).toLowerCase())) {
return false
} else {
return true
}
}
})
}
}
}
// Finished
//----------------------
console.info('Finished')
})
.catch(error => {
console.error(error)
})
function getResourcesPath(root, appPath) {
let resourcesPath = ""
if (/darwin/.test(appPath) || /mas/.test(appPath)) {
resourcesPath = path.join(root, appPath, `${json.productName}.app/Contents/Resources/app`)
} else if (/win32/.test(appPath) || /linux/.test(appPath)) {
resourcesPath = path.join(root, appPath, 'resources/app')
}
return resourcesPath
}

570
browser/pad.js Normal file
View File

@ -0,0 +1,570 @@
const {fileURL} = require('./utils.js')
const path = require('path')
/* A specialization that ignores webview events and thus allows
* webviews to get touch, mouse and wheel events.
*/
class DOMPadContainer extends DOMScatterContainer {
capture(event) {
if (event.target.tagName === 'WEBVIEW' || event.target.classList.contains('interactiveElement'))
return false
return super.capture(event)
}
}
/* A wrapper for a webview that behaves like a virtual tablet browser.
* Uses a DOMScatter to zoom and rotate the virtual browser window.
* The position of buttons and the border size remain constant.
*/
class Pad {
constructor(scatterContainer, {
startScale=1.0, minScale=0.25, maxScale=10.5,
autoBringToFront=true,
url="https://www.iwm-tuebingen.de/www/index.html",
hideOnStart=false,
translatable=true, scalable=true, rotatable=true,
movableX=true,
movableY=true,
rotationDegrees=null,
rotation=null,
onTransform=null,
transformOrigin = 'center center',
// extras which are in part needed
x=0,
y=0,
width=null,
height=null,
resizable=false,
} ={}) {
this.x = x
this.y = y
this.url = url
this.hideOnStart = hideOnStart
this.width = width
this.height = height
this.minScale = minScale
this.maxScale = maxScale
this.scatterContainer = scatterContainer
this.startScale = startScale
this.scale = startScale
this.scalable = scalable
this.rotatable = rotatable
this.rotationDegrees = this.startRotationDegrees
this.transformOrigin = transformOrigin
this.frame = document.createElement('div')
this.frame.classList.add("pad")
this.border = 50 / startScale
Elements.setStyle(this.frame, {
backgroundColor: "#333",
position: "absolute",
display: 'flex',
width: this.width+"px",
height: this.height+"px",
top: 0,
left: 0,
// boxShadow: `10px 10px 10px rgba(0, 0, 0, 0.5)`,
// borderRadius: '10px',
overflow: "visible"})
document.body.appendChild( this.frame)
this.web=document.createElement("webview")
this.webBackground=document.createElement("div")
this.webViewSnapshot=document.createElement("img")
this.overlay = document.createElement('div')
this.loadAnim = document.createElement("div")
this.loadAnim.style.webkitAnimation= "spin 2s linear infinite"
this.loadAnim.style.animation= "spin 2s linear infinite"
this.loadAnim.style.position = "absolute"
document.styleSheets[0].insertRule('\
@keyframes spin {\
from { transform: rotateZ(0deg); }\
to { transform: rotateZ(360deg); }\
}'
)
this.overlay.appendChild(this.loadAnim)
Elements.setStyle(this.web, {
position: "absolute",
overflow: "auto",
border: "1px solid #fff"
})
// this.web.classList.add("interactiveElement")
// this.web.style.pointerEvents="none"
Elements.setStyle(this.webBackground, {
position: "absolute",
overflow: "auto",
background: "white"
})
Elements.setStyle(this.overlay, {
position: "absolute",
overflow: "auto",
background: "white",
opacity: "0.8"
})
Elements.setStyle(this.webViewSnapshot, {
position: "absolute",
overflow: "auto"
})
let timeTouchStart = 0
this.overlayCaptureEvents = document.createElement('div')
// overlay.style.background="white"
this.overlayCaptureEvents.classList.add("interactiveElement")
this.overlayCaptureEvents.addEventListener('touchmove',(e)=>{
e.preventDefault()
e.stopPropagation()
})
this.overlayCaptureEvents.addEventListener('pointerup',(e)=>{
e.preventDefault()
e.stopPropagation()
let p = {x:e.clientX, y:e.clientY}
let webviewPosition = Points.fromPageToNode(this.web,p)
let d = new Date()
if(d.getTime()-timeTouchStart<150)this.web.sendInputEvent({type:'mouseUp', x: webviewPosition.x, y: webviewPosition.y, button:'left', clickCount: 1})
})
this.overlayCaptureEvents.addEventListener('pointerdown',(e)=>{
e.preventDefault()
e.stopPropagation()
this.scatter.bringToFront()
let p = {x:e.clientX, y:e.clientY}
let webviewPosition = Points.fromPageToNode(this.web,p)
let d = new Date()
timeTouchStart = d.getTime()
this.web.sendInputEvent({type:'mouseDown', x: webviewPosition.x, y: webviewPosition.y, button:'left', clickCount: 1})
})
this.overlayCaptureEvents.addEventListener('pointermove',(e)=>{
if(e.pointerType!='mouse'){
let rotation = Angle.radian2degree(this.scatter.rotation);
rotation = (rotation + 360) % 360;
let r = Math.sqrt(Math.pow(e.movementX, 2) + Math.pow(e.movementY, 2));
let phi = Angle.radian2degree(Math.atan2(e.movementX, e.movementY));
phi = ((phi) + 630) % 360;
let rot = ((rotation + 90) + 630) % 360;
let diffAngle = ((0 + rot) + 360) % 360;
let phiCorrected = (phi + diffAngle + 360) % 360;
let deltaX = r * Math.cos(Angle.degree2radian(phiCorrected));
let deltaY = -r * Math.sin(Angle.degree2radian(phiCorrected));
this.web.executeJavaScript("window.scrollTo(scrollX+"+(-1*deltaX)+", scrollY+"+ (-1*deltaY)+")")
}
})
this.overlayCaptureEvents.addEventListener('mousewheel',(e)=>{
console.log("mousewheel",e.deltaY)
// webview.sendInputEvent({type:'mouseWheel', x: 0, y: 0, deltaX: e.deltaX, deltaY: -e.deltaY, canScroll: true })
this.web.executeJavaScript("window.scrollTo(scrollX+"+e.deltaX+", scrollY+"+ e.deltaY+")")
})
this.frame.appendChild(this.webBackground)
this.frame.appendChild(this.web)
this.frame.appendChild(this.webViewSnapshot)
this.frame.appendChild(this.overlay)
if(remote.getGlobal('multiUserMode'))this.frame.appendChild(this.overlayCaptureEvents)
this.webViewSnapshot.style.visibility="hidden"
this.web.src=url
this.web.preload= path.join(__dirname, './preloadPad.js')
this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close")
this.backButton = this.addButton("../assets/icons/svg/left.svg", "go back")
this.forwardButton = this.addButton("../assets/icons/svg/right.svg", "go forward")
this.backButton.style.opacity = 0.5
this.forwardButton.style.opacity = 0.5
/*for (let callback of window.padLoadedHandler) {
callback(this, url)
}*/
this.web.addEventListener('new-window', (e) => {
if(e.url.indexOf("youtube")>-1)return
if (urlPadMap.has(e.url)) {
let childPad = urlPadMap.get(e.url)
childPad.scatter.moveTo(x, y)
return childPad
}
let childPad = new Pad(this.scatterContainer, {
x: this.scatter.position.x+100,
y: this.scatter.position.y+100,
url: e.url,
width: this.scatter.width,
height: this.scatter.height,
scalable: true,
rotatable: true})
urlPadMap.set(e.url, childPad)
for(let callback of window.padLoadedHandler) {
callback(childPad, url)
}
})
this.web.addEventListener('did-navigate', (e) => {
this.enableButtons()
})
this.web.addEventListener('dom-ready',()=>{
//if(this.url.indexOf('local')>-1)this.web.openDevTools()
})
this.web.addEventListener('ipc-message', (e) => {
if(e.channel='webviewPointerDown')this.scatter.bringToFront()
})
this.web.addEventListener('did-start-loading', ()=>{
this.overlay.style.visibility="visible"
let w = this.overlay.offsetWidth
let h = this.overlay.offsetHeight
console.log("did start loading",h,w)
let animationSize = w<h ? w*0.5 : h*0.5
let animationRingWidth = animationSize*0.1;
this.loadAnim.style.border=animationRingWidth+"px solid #f3f3f3"
this.loadAnim.style.borderTop=animationRingWidth+"px solid #ffb18c"
this.loadAnim.style.borderRadius="50%"
this.loadAnim.style.height=animationSize-animationRingWidth*2+"px"
this.loadAnim.style.width=animationSize-animationRingWidth*2+"px"
this.loadAnim.style.top = h*0.25+"px"
this.loadAnim.style.left = w*0.25+"px"
w<h ? this.loadAnim.style.top = 0.5*(h-animationSize)+"px" : this.loadAnim.style.left = 0.5*(w-animationSize)+"px"
})
this.web.addEventListener('did-stop-loading', ()=>{
this.overlay.style.visibility="hidden"
})
/*this.backButton.addEventListener('click', ()=>{
if(this.web.canGoBack())
this.web.goBack()
})
this.forwardButton.addEventListener('click', ()=>{
if(this.web.canGoForward())
this.web.goForward()
})
this.closeButton.addEventListener('click', ()=>{
this.close()
})*/
InteractionMapper.on('tap',this.backButton, e => {
if(this.web.canGoBack())
this.web.goBack()
})
InteractionMapper.on('tap',this.forwardButton, e => {
if(this.web.canGoForward())
this.web.goForward()
})
InteractionMapper.on('tap',this.closeButton, e => {
this.close()
})
this.scatter = new DOMScatter(this.frame, scatterContainer, {
x: this.x,
y: this.y,
startScale: this.startScale,
width: this.width,
height: this.height,
minScale: this.minScale,
maxScale: this.maxScale,
scalable: this.scalable,
resizable: true,
rotatable: this.rotatable})
let img=document.createElement("img")
img.style.width = "70%"
img.style.position = "absolute"
img.style.bottom = "20%"
img.style.right = "20%"
img.style.pointerEvents="none"
img.src = "../../assets/icons/png/flat/resize.png"
this.scatter.resizeButton.appendChild(img)
this.scatter.moveTo({x, y})
this.scatter.bringToFront()
this.scatter.addTransformEventCallback((e) => {
let newBorder = 50 / e.scale
if (newBorder !== this.border) {
this.border = newBorder
this.layout()
}
})
this.layout()
}
rad2degree(alpha){
return alpha * 180 / Math.PI;
}
degree2rad(alpha){
return alpha * Math.PI / 180;
}
close() {
// this.frame.style.display="none"
this.frame.parentNode.removeChild(this.frame)
urlPadMap.delete(this.url)
}
enableButtons() {
this.backButton.style.opacity = (this.web.canGoBack()) ? 1 : 0.5
this.forwardButton.style.opacity = (this.web.canGoForward()) ? 1 : 0.5
}
addButton(src, value) {
let button = document.createElement("img")
button.type = "image"
button.className = "frameButton"
button.style.position = "absolute"
button.src = fileURL(src)
button.value="close"
button.draggable = false
button.classList.add("interactiveElement")
this.frame.appendChild(button)
return button
}
layout() {
let b = this.border
let b2 = b * 2
let b8 = b / 8
let b25 = b / 25
let b15 = b / 15
this.scatter.resizeButton.style.width=b+"px"
this.scatter.resizeButton.style.height=b+"px"
Elements.setStyle(this.frame, {
// borderRadius: b8 + "px",
// boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)`
})
let size = "calc(100% - " + (2*b+2) +"px)"
Elements.setStyle(this.web, {
width: size,
height: size,
margin: b+"px"})
Elements.setStyle(this.webViewSnapshot, {
width: size,
height: size,
margin: b+"px"})
Elements.setStyle(this.webBackground, {
width: size,
height: size,
margin: b+"px"})
Elements.setStyle(this.overlay, {
width: size,
height: size,
margin: b+"px"})
Elements.setStyle(this.overlayCaptureEvents, {
width: size,
height: size,
opacity: 0.0001,
background: "white",
margin: b+"px"})
Elements.setStyle(this.closeButton, {
right: (b * 1.75) + "px",
bottom: "0px",
width: b+"px",
height: b+"px"})
Elements.setStyle(this.backButton, {
left: (b * 0.8) +"px",
bottom: "0px",
width: b+"px",
height: b+"px"})
Elements.setStyle(this.forwardButton, {
left: (this.border + (b * 0.8)) +"px",
bottom: "0px",
width: b+"px",
height: b+"px"})
}
}
class PadFromElement {
constructor(element, scatterContainer, {
startScale=1.0, minScale=0.1, maxScale=1.0,
autoBringToFront=true,
translatable=true, scalable=true, rotatable=true,
movableX=true,
movableY=true,
rotationDegrees=null,
rotation=null,
onTransform=null,
transformOrigin = 'center center',
// extras which are in part needed
x=0,
y=0,
width=null,
height=null,
resizable=false,
} ={}) {
this.element = element
this.x = x
this.y = y
this.width = width
this.height = height
this.minScale = minScale
this.maxScale = maxScale
this.scatterContainer = scatterContainer
this.scale = startScale
this.scalable = scalable
this.rotatable = rotatable
this.rotationDegrees = this.startRotationDegrees
this.transformOrigin = transformOrigin
this.frame = document.createElement('div')
Elements.setStyle(this.frame, {
width: this.width+"px",
height: this.height+"px",
backgroundColor: "#333",
position: "fixed",
top: 0,
left: 0,
overflow: "auto"})
this.closeButton = this.addButton("../assets/icons/svg/cross.svg", "close")
document.body.appendChild( this.frame)
this.border = 50
this.frame.appendChild(this.element)
this.title = document.createElement("div")
this.title.innerHTML = "Titel"
this.title.style.color = "white"
this.frame.appendChild(this.title)
Elements.setStyle(this.title, {
position: "absolute"
})
// this.element.style.overflow = "auto"
// this.element.style.position = "absolute"
this.layout()
this.closeButton.addEventListener('click', ()=>{
this.frame.style.display="none"
})
this.scatter = new DOMScatter(this.frame, scatterContainer, {
x: this.x,
y: this.y,
startScale: this.startScale,
width: this.width,
height: this.height,
minScale: this.minScale,
maxScale: this.maxScale,
scalable: this.scalable,
resizable: true,
rotatable: this.rotatable})
this.scatter.bringToFront()
this.scatter.addTransformEventCallback((e) => {
let newBorder = 50 / e.scale
if (newBorder !== this.border) {
this.border = newBorder
this.layout()
}
})
this.element.addEventListener('pointerdown', (e) => {
this.scatter.bringToFront()
})
}
close() {
// this.frame.style.display="none"
this.frame.parentNode.removeChild(this.frame)
urlPadMap.delete(this.url)
}
addButton(src, value) {
let button = document.createElement("img")
button.type = "image"
button.className = "frameButton"
button.style.position = "absolute"
button.src = fileURL(src)
button.value="close"
button.draggable = false
this.frame.appendChild(button)
return button
}
layout() {
let b = this.border
let b2 = b * 2
let b8 = b / 8
let b25 = b / 25
let b15 = b / 15
this.scatter.resizeButton.style.width=b+"px"
this.scatter.resizeButton.style.height=b+"px"
Elements.setStyle(this.frame, {
// borderRadius: b8 + "px",
// boxShadow: `${b25}px ${b15}px ${b8}px rgba(0, 0, 0, 0.5)`
})
let size = "calc(100% - " + (2*b+2) +"px)"
Elements.setStyle(this.element, {
width: size,
height: size,
top: b+"px",
left: b+"px"})
Elements.setStyle(this.closeButton, {
right: (b * 0.75) + "px",
bottom: "0px",
width: b+"px",
height: b+"px"})
Elements.setStyle(this.title, {
left: (b * 1.5) + "px",
fontSize: (b * 0.8) + "px",
top: (0.1)+"0px"})
}
}
module.exports = { Pad, DOMPadContainer, PadFromElement }

3540
browser/padAccordion.js Normal file

File diff suppressed because it is too large Load Diff

2814
browser/padAccordionOld.js Normal file

File diff suppressed because it is too large Load Diff

1380
browser/padMinimal.js Normal file

File diff suppressed because it is too large Load Diff

231
browser/preload.js Normal file
View File

@ -0,0 +1,231 @@
const { fileURL, loadScript, hideCursor, showCursor } = require('./utils.js')
let { remote } = require('electron')
const webFrame = require('electron').webFrame
// UO: Disable unintended zoom of fullscreen page if user wants to zoom
// only parts like Eyevisit info cards.
console.log('Disable pinch zoom', webFrame)
webFrame.setVisualZoomLevelLimits(1, 1)
let padContainer = null
let hideCursorTimeout = null
let debug = false
let urlPadMap = new Map()
window.urlPadMap = urlPadMap
window.padLoadedHandler = []
window.nodeDirname = __dirname
function pageSize() {
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
width = w.innerWidth || e.clientWidth || g.clientWidth,
height = w.innerHeight || e.clientHeight || g.clientHeight
return [width, height]
}
let size = pageSize()
let pageWidth = size[0]
let pageHeight = size[1]
/* Open a new at x, y position. */
function openPad(url, x, y) {
console.log('openPad')
if (remote.getGlobal('useBrowserView')) {
return ipcRenderer.send('loadBrowserView', { url, x, y })
}
const { Pad } = require('./pad.js')
const { minimalPad } = require('./padMinimal.js')
pad = null
if (urlPadMap.has(url)) {
let pad = urlPadMap.get(url)
pad.scatter.bringToFront()
/*TweenMax.to(pad.frame, 0.5, {
boxShadow: "0 0 25px 5px white", onComplete: () => {
TweenMax.to(pad.frame, 0.5, { boxShadow: "none" })
}
})*/
TweenMax.to(pad.frame, 0.2, {
scale: '1.01',
onComplete: () => {
TweenMax.to(pad.frame, 0.2, { scale: '1' })
}
})
// pad.scatter.moveTo(x, y)
return pad
}
y + 1600 > pageHeight ? (y = pageHeight - 1600) : (y = y)
if (remote.getGlobal('useMinimalPad')) {
pad = new minimalPad(padContainer, {
x: x,
y: y,
url: url,
tabbedView: true,
hasTtitleBar: false,
hideOnStart: false,
startScale: 1,
width: 1000,
height: 1500,
scalable: true,
rotatable: true
})
}
if (!remote.getGlobal('useMinimalPad')) {
pad = new Pad(padContainer, {
x: x,
y: y,
url: url,
hideOnStart: false,
startScale: 1,
width: 1000,
height: 1500,
scalable: true,
rotatable: true
})
}
urlPadMap.set(url, pad)
for (let callback of window.padLoadedHandler) {
callback(pad, url)
}
}
window.padLoadedHandler.push((pad, url) => {
console.log('Add specific behavior')
})
/* According to https://electron.atom.io/docs/faq/
"I can not use jQuery/RequireJS/Meteor/AngularJS in Electron" we
have to rename the symbols in the page before including other libraries.
Remember to use nodeRequire after this point.
*/
window.nodeRequire = require
delete window.require
delete window.exports
delete window.module
/* Create a DOMPadContainer, i.e. a special DOMScatterContainer, as a wrapper
of the document body.
*/
window.addEventListener('load', e => {
console.log('preloading')
// ../iwmlib/dist/iwmlib.3rdparty.js
// loadScript('../iwmlib/lib/3rdparty/preload.js', () => {
loadScript('../../iwmlib/dist/iwmlib.3rdparty.preload.js', () => {
//console.log("../iwmlib/dist/iwmlib.3rdparty.js loaded")
console.log('greensock loaded')
// loadScript('../iwmlib/dist/iwmlib.js', () => {
loadScript('../../iwmlib/dist/iwmlib.js', () => {
console.log('../iwmlib/dist/iwmlib.js loaded')
/* const { Pad, DOMPadContainer } = nodeRequire('./pad.js')
padContainer = new DOMPadContainer(document.body)
window.nodePadContainer = padContainer
ipcRenderer.send('padContainerLoaded') */
/*Register a handler for mousemove events. Hide the cursor a few
*seconds after the last move event.
*/
document.body.addEventListener('mousemove', e => {
showCursor()
clearTimeout(hideCursorTimeout)
hideCursorTimeout = setTimeout(hideCursor, 3000)
})
})
if (debug) {
loadScript('../iwmlib/dist/iwmlib.pixi.js', () => {
console.log('../iwmlib/dist/iwmlib.pixi.js loaded')
const DebugApp = require('./debug.js')
let debugApp = new DebugApp(document.body)
debugApp.setup()
debugApp.run()
})
}
})
})
/* Register a handler for all click events and check whether the link target
opens a new window. If the link target is blank, prevent the default behavior
and create a Pad scatter object instead.
*/
window.addEventListener('click', e => {
let node = e.target
let url = ''
let target = ''
let multiUserMode = false // remote.getGlobal('multiUserMode') // DOMScatter && remote.getGlobal('multiUserMode')
// console.log("click", multiUserMode, remote.getGlobal('multiUserMode'))
if (multiUserMode) {
while (node !== null) {
if (node.tagName === 'A' || node.tagName === 'a') {
if (node.target instanceof SVGAnimatedString) {
url = node.href.baseVal
target = node.target.baseVal
} else {
url = node.href
target = node.target
}
if (target === '_blank') {
e.preventDefault()
openPad(url, e.clientX, e.clientY)
}
return
}
node = node.parentNode
}
}
})
/* Register a handler for contextmenu events and check whether the link target
// opens a new window. If the link target is blank, prevent the default behavior
and show a popup menu with the option to open a pad instead.
*/
window.addEventListener('contextmenu', e => {
let node = e.target
let url = null
if (remote.getGlobal('multiUserMode')) {
while (node !== null) {
if (node.tagName === 'A' || node.tagName === 'a') {
if (node.target instanceof SVGAnimatedString) {
url = node.href.baseVal
} else {
url = node.href
}
e.preventDefault()
let point = { x: e.clientX, y: e.clientY }
PopupMenu.open(
{
'Open new Pad': () => openPad(url, e.clientX, e.clientY)
},
point,
{ fontSize: '0.8em' }
)
return
}
node = node.parentNode
}
}
})
/* Special error handling if the rendere process sends a error notification
log this error on the main console.
*/
let { Console } = require('console')
let debugConsole = new Console(process.stdout, process.stderr)
let { ipcRenderer } = require('electron')
window.addEventListener('error', event => {
debugConsole.error('PAGE ERROR', event.error, event.filename)
debugConsole.error('open', window.location.href)
ipcRenderer.send('error', 1)
})
ipcRenderer.on('newPad', (e, data) => {
openPad(data, 0, 0)
})

83
browser/preloadPad.js Normal file
View File

@ -0,0 +1,83 @@
let { remote } = require('electron')
let { ipcRenderer } = require('electron')
const path = require('path')
const webFrame = require('electron').webFrame
console.log('Disable pinch zoom', webFrame)
webFrame.setVisualZoomLevelLimits(1, 1)
window.nodePath = path
window.nodeDirname = __dirname
window.nodeRequire = require
delete window.require
delete window.exports
delete window.module
window.padLoadedHandler = []
let pointerCounter = 0
window.addEventListener('pointerdown', (e) => {
//e.preventDefault()
// console.log("ipcRenderer.sendToHost('webviewPointerDown')")
ipcRenderer.sendToHost('webviewPointerDown')
})
window.addEventListener('pointerup', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerUp')")
ipcRenderer.sendToHost('webviewPointerUp')
})
window.addEventListener('pointerenter', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerEnter')")
ipcRenderer.sendToHost('webviewPointerEnter')
})
window.addEventListener('pointercancel', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerCancel')")
ipcRenderer.sendToHost('webviewPointerCancel')
})
window.addEventListener('pointerleave', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerLeave')")
ipcRenderer.sendToHost('webviewPointerLeave')
})
window.addEventListener('pointerout', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerOut')")
ipcRenderer.sendToHost('webviewPointerOut')
})
window.addEventListener('pointerover', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerOver')")
ipcRenderer.sendToHost('webviewPointerOver')
})
window.addEventListener('pointermove', (e) => {
// console.log("ipcRenderer.sendToHost('webviewPointerMove')")
ipcRenderer.sendToHost('webviewPointerMove')
})
window.addEventListener('touchmove', (e) => {
// console.log("ipcRenderer.sendToHost('touchmove')")
ipcRenderer.sendToHost('touchMove')
})
window.addEventListener('touchstart', (e) => {
pointerCounter++
// console.log("ipcRenderer.sendToHost('touchstart')")
ipcRenderer.sendToHost('touchStart')
})
window.addEventListener('touchend', (e) => {
pointerCounter--
// console.log("ipcRenderer.sendToHost('touchend')")
ipcRenderer.sendToHost('touchEnd')
})
ipcRenderer.on('overlayEvent', function () {
console.log('hello world From Preload')
})

47
browser/store.js Normal file
View File

@ -0,0 +1,47 @@
const electron = require('electron')
const path = require('path')
const fs = require('fs')
class Store {
constructor(opts) {
// Renderer process has to get `app` module via `remote`, whereas the main process can get it directly
// app.getPath('userData') will return a string of the user's app data directory path.
const userDataPath = (electron.app || electron.remote.app).getPath('userData')
// We'll use the `configName` property to set the file name and path.join to bring it all together as a string
this.path = path.join(userDataPath, opts.configName + '.json')
this.data = parseDataFile(this.path, opts.defaults)
this.data = Object.assign(opts.defaults, this.data)
}
// This will just return the property on the `data` object
get(key) {
return this.data[key]
}
// ...and this will set it
set(key, val) {
this.data[key] = val
// Wait, I thought using the node.js' synchronous APIs was bad form?
// We're not writing a server so there's not nearly the same IO demand on the process
// Also if we used an async API and our app was quit before the asynchronous write had a chance to complete,
// we might lose that data. Note that in a real app, we would try/catch this.
fs.writeFileSync(this.path, JSON.stringify(this.data))
}
}
function parseDataFile(filePath, defaults) {
// We'll try/catch it in case the file doesn't exist yet, which will be the case on the first application run.
// `fs.readFileSync` will return a JSON string which we then parse into a Javascript object
try {
return JSON.parse(fs.readFileSync(filePath))
} catch(error) {
// if there was some kind of error, return the passed in defaults instead.
return defaults
}
}
// expose the class
module.exports = Store

113
browser/test.js Normal file
View File

@ -0,0 +1,113 @@
let fs = require('fs')
let path = require('path')
let {thumbnail} = require('./utils.js')
let pairs = []
let urlMap = new Map()
function isFile(path) {
try {
return !fs.lstatSync(path).isDirectory()
} catch(e) {
if (e.code == 'ENOENT'){
return false
} else {
return false
}
}
}
function listPairs(src, dst, skipFiles=true) {
let dir = fs.readdirSync(src)
for(let name of dir) {
let srcPath = src + name
if (isFile(srcPath) && !skipFiles) {
if (srcPath.endsWith('.html')) {
let dstPath = dst + name.replace(/.html/, '.png')
pairs.push([srcPath, dstPath])
}
}
else {
if (srcPath.endsWith('.'))
continue
let indexPath = srcPath + path.sep + 'index.html'
if (isFile(indexPath)) {
let thumbnailPath = indexPath.replace(/index.html/, 'thumbnail.png')
pairs.push([indexPath, thumbnailPath])
}
}
}
}
function capturePage(focusedWindow, dstPath) {
focusedWindow.capturePage( (image) => {
fs.writeFile(dstPath, thumbnail(image), (err) => {
if (err) throw err;
nextLoadTest(focusedWindow)
})
})
}
function testLoading(focusedWindow, filePath, dstPath) {
let urlPath = filePath.replace(path.sep, '/')
let basePath = `${__dirname}`.replace('/browser', '')
let shortURL = `file://${basePath}/${urlPath}`
let fullURL = `file://${__dirname}/../../${urlPath}`
// console.log({basePath, shortURL, fullURL})
if (focusedWindow) {
urlMap.set(shortURL, dstPath)
focusedWindow.webContents.session.clearCache(() => console.log('Cache cleared'))
focusedWindow.webContents.loadURL(shortURL)
}
}
function listLibPairs() {
let src = "lib" + path.sep
let dst = "lib" + path.sep + 'thumbnails' + path.sep
listPairs(src, dst, false)
}
function listAppPairs() {
let src = "apps" + path.sep
let dst = "apps" + path.sep
listPairs(src, dst)
}
function listSrcPairs() {
let src = "src" + path.sep
let dst = "src" + path.sep
listPairs(src, dst)
}
function nextLoadTest(focusedWindow) {
if (global.errorCount > 0 && global.stopTestsOnError) {
console.log("Test aborted")
return
}
if (pairs.length > 0) {
let [file, image] = pairs.pop()
testLoading(focusedWindow, file, image)
}
else {
console.log("All thumbnails created")
}
}
function loadTests(focusedWindow) {
global.errorCount = 0
focusedWindow.webContents.on('did-finish-load', (e) => {
setTimeout(() => {
let url = e.sender.history[e.sender.history.length-1]
dstPath = urlMap.get(url)
capturePage(focusedWindow, dstPath)
}, 5000)
})
listLibPairs()
listSrcPairs()
listAppPairs()
nextLoadTest(focusedWindow)
}
module.exports = loadTests

38
browser/utils.js Normal file
View File

@ -0,0 +1,38 @@
function fileURL(src) {
let dir = __dirname.replace(/\\/g, '/')
return `file://${dir}/${src}`
}
function loadScript(src, callback) {
let url = fileURL(src)
let script = document.createElement('script')
script.onload = () => {
if (callback) {
callback.call(this, script)
}
}
script.src = url
document.head.appendChild(script)
}
function thumbnail(screenshot) {
return screenshot.resize({ width: 1024 }).toPNG()
}
let hiddenCursor = fileURL('../assets/cursor/cur0000.cur')
let defaultCursor = fileURL('../assets/cursor/cur1054.cur')
function hideCursor() {
// console.log("hideCursor")
document.body.style.cursor = `url('${hiddenCursor}'), default`
}
function showCursor() {
document.body.style.cursor = `url('${defaultCursor}'), default`
// console.log("showCursor")
}
module.exports = {
fileURL, loadScript, thumbnail, hideCursor, showCursor
}

View File

@ -27,6 +27,10 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
h2 {
padding-left: 20px;
}
h3 { h3 {
color: white; color: white;
padding: 4px; padding: 4px;
@ -163,7 +167,6 @@ div.title {
.container { .container {
margin: 0; margin: 0;
padding: 0; padding: 0;
margin-top: 32px;
border: 2pt #000; border: 2pt #000;
min-height: 100%; min-height: 100%;
min-width: 100%; min-width: 100%;

View File

@ -30,11 +30,8 @@
</template> </template>
</head> </head>
<body> <body>
<div id="container" class="container"> <h2><a href="index.html">lib.</a>Doctests</h2>
<a style="position: absolute; left: 22px; top: 12px" target="_blank" href="http://www.iwm-tuebingen.de" <div id="container" class="container"></div>
>IWMLib Doctest</a
>
</div>
<script> <script>
let index = new Index(itemTemplate, [ let index = new Index(itemTemplate, [
['Doctest', 'doctest.html'], ['Doctest', 'doctest.html'],

BIN
lib/interaction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 KiB

BIN
lib/pixi/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

BIN
lib/pixi/application.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
lib/pixi/badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

BIN
lib/pixi/blurfilter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
lib/pixi/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
lib/pixi/buttongroup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

BIN
lib/pixi/coordinates.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

BIN
lib/pixi/deepzoom/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

View File

@ -25,11 +25,11 @@
</template> </template>
</head> </head>
<body> <body>
<div id="container" class="container"> <h2>
<a style="position: absolute; left: 22px; top: 12px" target="_blank" href="http://www.iwm-tuebingen.de" <a href="../../index.html">lib.</a><a href="../index.html">pixi.</a
>IWMLib PIXI DeepZoom</a ><a href="../index.html">deepzoom.</a>Doctests
> </h2>
</div> <div id="container" class="container"></div>
<script> <script>
const index = new Index( const index = new Index(
itemTemplate, itemTemplate,

BIN
lib/pixi/deepzoom/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 148 KiB

BIN
lib/pixi/flipeffect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

BIN
lib/pixi/flippable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

View File

@ -25,11 +25,8 @@
</template> </template>
</head> </head>
<body> <body>
<div id="container" class="container"> <h2><a href="../index.html">lib.</a><a href="index.html">pixi.</a>Doctests</h2>
<a style="position: absolute; left: 22px; top: 12px" target="_blank" href="http://www.iwm-tuebingen.de" <div id="container" class="container"></div>
>IWMLib PIXI Doctests</a
>
</div>
<script> <script>
const index = new Index( const index = new Index(
itemTemplate, itemTemplate,

BIN
lib/pixi/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

BIN
lib/pixi/list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

BIN
lib/pixi/maps/geojson.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

View File

@ -1,112 +1,113 @@
<html> <html>
<head>
<title>PIXI Maps Doctests</title>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta
name="viewport"
content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0"
/>
<link rel="stylesheet" href="../../../css/index.css" />
<head> <script src="../../../dist/iwmlib.js"></script>
<title>PIXI Maps Doctests</title>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport"
content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0" />
<link rel="stylesheet" href="../../../css/index.css">
<script src="../../../dist/iwmlib.js"></script> <template id="itemTemplate">
<a class="wrapper" href="">
<template id="itemTemplate"> <div class="preview">
<a class="wrapper" href=""> <div class="thumbnail-container">
<div class="preview"> <div class="thumbnail">
<div class="thumbnail-container"> <img class="icon" src="thumbnails/notfound.png" />
<div class="thumbnail"> <!-- <iframe src="" frameborder="0"></iframe> -->
<img class="icon" src="thumbnails/notfound.png"> </div>
<!-- <iframe src="" frameborder="0"></iframe> -->
</div> </div>
<div class="title"></div>
</div> </div>
<div class="title"></div> </a>
</div> </template>
</a>
</template>
<style> <style>
body { body {
font-size: 20px; font-size: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#logo { #logo {
left: 0; left: 0;
position: absolute; position: absolute;
top: 30px; top: 30px;
} }
#logo>img { #logo > img {
width: 80px; width: 80px;
} }
header>h1 { header > h1 {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
word-spacing: 0.25em; word-spacing: 0.25em;
margin-top: 0; margin-top: 0;
} }
header>p { header > p {
max-width: 720px; max-width: 720px;
line-height: 1.5em; line-height: 1.5em;
color: rgb(207, 207, 207); color: rgb(207, 207, 207);
} }
header { header {
font-family: "Open Sans", sans-serif; font-family: 'Open Sans', sans-serif;
background-color: #4c4f4f; background-color: #4c4f4f;
color: whitesmoke; color: whitesmoke;
padding: 68px 50px 10px 150px; padding: 68px 50px 10px 150px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
} }
.container { .container {
justify-content: center;
justify-content: center; flex: 1;
height: auto;
min-height: auto;
width: auto;
min-width: auto;
flex: 1; margin: 0 60px;
height: auto; }
min-height: auto; </style>
width: auto; </head>
min-width: auto;
margin: 0 60px; <body>
} <header>
</style> <h1>
</head> <a href="../../index.html">lib.</a><a href="../index.html">pixi.</a
><a href="../index.html">map.</a>Doctests
<body> </h1>
<header> <p>
<a id="logo" target="_blank" href="http://www.iwm-tuebingen.de"> The maps module provides a handy toolkit to easily integrate maps in an application. Create a full
<img src="../../../assets/logos/iwm_logo_2015_twitter.png"> screen map application by using the mapapp. Utilize the GeoLayer-system to integrate maps in an existing
</a> application. Draw graphics onto the map using geographical positions instead of pixel positions with the
<h1>Maps Module</h1> GeoGraphics. Or just use an Overlay to quickly draw icons for each point on a map.
<p>The maps module provides a handy toolkit to easily integrate maps in an application. Create a full screen map </p>
application by using the mapapp. Utilize the GeoLayer-system to integrate maps in an existing application. </header>
Draw graphics onto the map using geographical positions instead of pixel positions with the GeoGraphics. <div id="container" class="container"></div>
Or just use an Overlay to quickly draw icons for each point on a map. <script>
</p> const index = new Index(
</header> itemTemplate,
<div id="container" class="container"> [
['GeoGraphics', 'geographics.html'],
</div> ['GeoJson', 'geojson.html'],
<script> ['GeoMap', 'map.html'],
const index = new Index(itemTemplate, [ ['MapApp', 'mapapp.html'],
["GeoGraphics", "geographics.html"], ['MapProjection', 'mapprojection.html'],
["GeoJson", "geojson.html"], ['MapViewport', 'mapviewport.html'],
["GeoMap", "map.html"], ['Overlay', 'overlay.html'],
["MapApp", "mapapp.html"], ['Scatter', 'scatter.html']
["MapProjection", "mapprojection.html"], ],
["MapViewport", "mapviewport.html"], null
["Overlay", "overlay.html"], )
["Scatter", "scatter.html"] index.load()
], </script>
null) </body>
index.load() </html>
</script>
</body>
</html>

BIN
lib/pixi/maps/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
lib/pixi/maps/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

BIN
lib/pixi/maps/mapapp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
lib/pixi/maps/overlay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

BIN
lib/pixi/maps/scatter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
lib/pixi/message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

BIN
lib/pixi/modal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

BIN
lib/pixi/popover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

BIN
lib/pixi/popup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

BIN
lib/pixi/popupmenu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

BIN
lib/pixi/progress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

BIN
lib/pixi/scatter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

BIN
lib/pixi/scrollview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

BIN
lib/pixi/slider.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

BIN
lib/pixi/stylus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

BIN
lib/pixi/switch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

BIN
lib/pixi/text-transform.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

BIN
lib/pixi/text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 85 KiB

BIN
lib/pixi/tooltip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

BIN
lib/pixi/volatile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 270 KiB

18689
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "iwmlib", "name": "iwmlib",
"version": "2.0.0-beta.1", "version": "2.0.0-beta.1",
"description": "An Open Source library for multi-touch, WebGL powered applications.", "description": "An Open Source library for multi-touch, WebGL powered applications.",
"main": "index.js", "main": "browser/main.js",
"directories": { "directories": {
"example": "examples" "example": "examples"
}, },
@ -45,7 +45,15 @@
"stylelint-config-standard": "^28.0.0" "stylelint-config-standard": "^28.0.0"
}, },
"dependencies": { "dependencies": {
"electron": "^16.2.8",
"electron-localshortcut": "^3.1.0",
"electron-packager": "^13.1.1",
"electron-prebuilt": "^1.4.13",
"electron-prebuilt-compile": "^4.0.0",
"electron-process-manager": "^0.7.0",
"electron-rebuild": "^1.8.5",
"gsap": "^2.1.3", "gsap": "^2.1.3",
"html": "^1.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"optimal-select": "^4.0.1", "optimal-select": "^4.0.1",
"pixi-compressed-textures": "^2.0.5", "pixi-compressed-textures": "^2.0.5",