Christopher Pitt
Writer and coder, working at ringier.co.za
npm init -y
npm install --save-dev electron
const { app, BrowserWindow } = require("electron")
const path = require("path")
const createWindow = () => {
const mainWindow = new BrowserWindow({
"width": 800,
"height": 600,
})
mainWindow.loadFile("index.html")
}
app.whenReady().then(() => {
createWindow()
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>PixelRobot</title>
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
...UI goes here
</body>
</html>
"scripts": {
"start": "electron ."
}
npm install --save-dev electron-builder
"build": {
"appId": "io.assertchris.pixelrobot",
"productName": "PixelRobot",
"files": [
"build/**/*",
"main.js",
"preload.js",
"renderer.html"
],
"mac": {
"category": "public.app-category.developer-tools"
}
}
"scripts": {
"dist": "npm run build && electron-builder"
},
npm install @capacitor/core
npm install --save-dev @capacitor/cli
npx cap init
npm install @capacitor-community/electron
npx cap add @capacitor-community/electron
npx cap open @capacitor-community/electron
npm install --save-dev tailwindcss @tailwindcss/forms autoprefixer concurrently cssnano postcss postcss-cli
"scripts": {
"start": "concurrently 'npm:watch' 'electron .'",
"watch-css": "postcss source/index.pcss -o build/index.css -w",
"build-css": "NODE_ENV=production postcss source/index.pcss -o build/index.css",
"watch": "concurrently 'npm:watch-css'",
"build": "concurrently 'npm:build-css'"
},
npm install --save-dev esbuild
"scripts": {
...
"watch-js": "esbuild source/index.jsx --bundle --watch --outfile=build/index.js",
"build-js": "NODE_ENV=production esbuild source/index.jsx --bundle --minify --outfile=build/index.js",
"watch": "concurrently 'npm:watch-css' 'npm:watch-js'",
"build": "concurrently 'npm:build-css' 'npm:build-js'",
},
npm install --save-dev esbuild-plugin-postcss2
const esbuild = require('esbuild')
const postCssPlugin = require('esbuild-plugin-postcss2')
const autoprefixer = require('autoprefixer')
const tailwind = require('tailwindcss')
esbuild
.build({
entryPoints: ['source/index.jsx', 'source/index.css'],
bundle: true,
outdir: 'build',
plugins: [
postCssPlugin.default({
plugins: [autoprefixer, tailwind],
}),
],
})
.catch(() => process.exit(1))
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; img-src 'self' data:"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'; img-src 'self' data:"
/>
<link rel="stylesheet" href="build/index.css" />
<title>PixelRobot</title>
</head>
<body class="h-screen">
<div id="app" class="flex w-full h-full text-gray-900"></div>
<script src="build/index.js"></script>
</body>
</html>
{
"id": "controls-16",
"label": "Controls 16",
"sprites": [
{
"id": "analog-1-highlight",
"label": "Highlight Analog Stick",
"width": 16,
"height": 16,
"frames": 9,
"fps": 9,
"pixelated": true,
"preview": {
"still": 4,
"loop": true,
"attachment": "analog-1-highlight.png"
},
"variations": {
"rose": {
"legend": "#f43f5e",
"#fef2f2": "#fff1f2",
"#fee2e2": "#ffe4e6",
"#fecaca": "#fecdd3",
...
}
const [dragEntered, setDragEntered] = useState(false)
const [droppedFiles, setDroppedFiles] = useState(undefined)
useEffect(() => {
const stopPropagation = event => {
event.preventDefault()
event.stopPropagation()
}
const dragEnter = () => {
setDragEntered(true)
}
const dragLeave = () => {
setDragEntered(false)
}
const drop = event => {
stopPropagation(event)
setDroppedFiles(event.dataTransfer.files)
setDragEntered(false)
}
document.addEventListener("dragover", stopPropagation)
document.addEventListener("dragenter", dragEnter)
document.addEventListener("dragleave", dragLeave)
document.addEventListener("drop", drop)
return () => {
document.removeEventListener("dragover", stopPropagation)
document.removeEventListener("dragenter", dragEnter)
document.removeEventListener("dragleave", dragLeave)
document.removeEventListener("drop", drop)
}
}, [setDragEntered])
useEffect(() => {
if (droppedFiles) {
for (const file of droppedFiles) {
const reader = new FileReader()
reader.addEventListener("loadend", async () => {
// ...send this somewhere
})
reader.readAsDataURL(file)
}
}
}, [droppedFiles])
const attempt = require("@assertchris/attempt-promise")
const DecompressZip = require("decompress-zip")
const {
promises: { writeFile, mkdir, readFile, readdir, rmdir },
} = require("fs")
const unzipPack = async folder => {
return new Promise((resolve, reject) => {
const unzipper = new DecompressZip(`${folder}.zip`)
unzipper.on("error", error => {
reject(error)
})
unzipper.on("extract", async () => {
const [readFileError, readFileResult] = await attempt(
readFile(`${folder}/index.json`, "utf8"),
)
if (readFileError) {
reject(readFileError)
return
}
const meta = JSON.parse(readFileResult)
// store image files + send meta data back
})
})
}
const window = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
})
const { contextBridge, ipcRenderer } = require("electron")
contextBridge.exposeInMainWorld("api", {
parsePackFile: async (packFileName, packFileData) => {
return ipcRenderer.invoke("parsePackFile", packFileName, packFileData)
},
putAttachment: async (packId, attachmentId, attachmentData) => {
return ipcRenderer.invoke("putAttachment", packId, attachmentId, attachmentData)
},
fetchAttachment: async (packId, attachmentId) => {
return ipcRenderer.invoke("fetchAttachment", packId, attachmentId)
},
removeAttachments: async packId => {
return ipcRenderer.invoke("removeAttachments", packId)
},
downloadAttachment: async (attachmentId, data) => {
return ipcRenderer.invoke("downloadAttachment", attachmentId, data)
},
})
reader.addEventListener("loadend", async () => {
const { meta, attachments } = await window.api.parsePackFile(
file.name,
reader.result,
)
//...persist data in renderer
})
ipcMain.handle("parsePackFile", async (_, packFileName, packFileData) => {
const temp = app.getPath("temp")
const hash = crypto.createHash("md5").update(packFileName).digest("hex")
const folder = path.join(temp, `pixelrobot-${hash}`)
await mkdir(temp, { recursive: true })
await writeFile(`${folder}.zip`, packFileData.replace("data:application/zip;base64", ""), {
encoding: "base64",
})
await rmdir(folder, { recursive: true })
return unzipPack(folder)
})
import { initial, reducer } from "./reducer"
import actions from "./actions"
import { createContext } from "react"
import creators from "./creators"
import databases from "./databases"
const context = createContext(initial)
export { initial, reducer, actions, creators, context, databases }
import { context as Context, actions, creators, initial, reducer } from "./state"
import { Home, Sheet, Sprite } from "./screens"
import React, { useEffect, useState, useReducer } from "react"
const App = () => {
const [state, dispatch] = useReducer(reducer, initial)
// ...
return (
<Context.Provider value={{ state, dispatch }}>
...
</Context.Provider>
)
}
export { App }
const { state, dispatch } = useContext(context)
const [deleting, setDeleting] = useState(undefined)
const createNewSheet = () => {
dispatch(creators[actions.sheets.create]({ title: "new sheet" }))
}
const deleteSheet = row => {
setDeleting(row)
}
const confirmDeleteSheet = () => {
dispatch(creators[actions.sheets.delete](deleting.doc))
setDeleting(undefined)
}
const cancelDeleteSheet = () => {
setDeleting(undefined)
}
import PouchDB from "pouchdb/dist/pouchdb"
import UpsertPlugin from "pouchdb-upsert/dist/pouchdb.upsert"
PouchDB.plugin(UpsertPlugin)
const packs = new PouchDB("packs")
const settings = new PouchDB("settings")
const sheets = new PouchDB("sheets")
export default { packs, settings, sheets }
const [error, result] = await attempt(
databases.packs.post({
...meta,
createdAt: DateTime.now().toISO(),
}),
)
if (error) {
throw error
}
for (const [name, data] of attachments) {
const [error] = await attempt(window.api.putAttachment(result.id, name, data))
if (error) {
throw error
}
}
By Christopher Pitt