diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4007b47 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +name: Build + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + script: build:linux + artifact: linux + paths: | + dist/*.AppImage + dist/*.deb + dist/*.rpm + - os: windows-latest + script: build:win + artifact: windows + paths: | + dist/*.exe + - os: macos-latest + script: build:mac + artifact: macos + paths: | + dist/*.dmg + dist/*.zip + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run ${{ matrix.script }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: fsprinter-${{ matrix.artifact }} + path: ${{ matrix.paths }} + if-no-files-found: error + + release: + name: Publish release + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts + + - name: Create GitHub release + uses: softprops/action-gh-release@v3 + with: + files: artifacts/**/* + generate_release_notes: true diff --git a/.gitignore b/.gitignore index e280281..076c22b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,14 @@ /nbproject/ /node_modules/ /vendor/ +/out/ +/dist/ package-lock.json pos.txt out/ ticket *.lock -.DS_Store \ No newline at end of file +.DS_Store +.idea +.codex +.claude \ No newline at end of file diff --git a/README.md b/README.md index ce2fc3b..7d617ca 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,63 @@ -# fsprinter -Electron app to connect printers to FacturaScripts. +# FSPrinter +Electron app to connect printers to FacturaScripts a factura.city -## Run from source -``` +## Installation + +```bash npm install +``` + +## Development + +Run the application in development mode: + +```bash npm start ``` -## Build executable +## Build + +The project uses **electron-builder** to create distributable packages. + +### Build for all platforms +```bash +npm run build ``` -npm run make + +### Build for specific platforms + +**Linux** (deb, rpm, AppImage): +```bash +npm run build:linux ``` +**Windows** (installer NSIS and portable): +```bash +npm run build:win +``` + +**macOS** (dmg and zip): +```bash +npm run build:mac +``` + +The compiled files will be generated in the `dist/` directory. + +## macOS: "The application is damaged" + +The macOS build is not signed with an Apple Developer ID, so Gatekeeper +blocks it after download with a misleading "damaged" message. Remove the +quarantine attribute to allow it to run: + +```bash +xattr -dr com.apple.quarantine "/Applications/FSPrinter.app" +``` + +## Project Information + +- **Version:** 1.4.1 +- **Author:** Carlos García Gómez +- **License:** LGPL-3.0 + ## Contact or feedback -https://facturascripts.com/remote-printer \ No newline at end of file +https://facturascripts.com/plugins/tickets diff --git a/main.js b/main.js index d215b61..5c89e1d 100644 --- a/main.js +++ b/main.js @@ -1,23 +1,23 @@ -const { app, BrowserWindow, dialog, ipcMain } = require('electron'); +const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron'); const { exec } = require('child_process'); -const axios = require('axios'); -const fetch = require("node-fetch"); const fs = require('fs'); const os = require('os'); +const path = require('path'); const settings = require('electron-settings'); +app.commandLine.appendSwitch('no-sandbox'); + var win = null; function createWindow() { win = new BrowserWindow({ - width: 600, - height: 620, + width: 430, + height: 550, webPreferences: { contextIsolation: false, - enableRemoteModule: true, nodeIntegration: true } - }); + }) win.removeMenu(); //win.webContents.openDevTools(); @@ -26,12 +26,17 @@ function createWindow() { function prinTesTicket(printerName) { var esc = '\x1B'; //ESC byte in hex notation + var gs = '\x1D'; //GS byte in hex notation var newLine = '\x0A'; //LF byte in hex notation var cmds = esc + "@"; //Initializes the printer (ESC @) + + // Header cmds += esc + '!' + '\x38'; //Emphasized + Double-height + Double-width mode selected (ESC ! (8 + 16 + 32)) 56 dec => 38 hex cmds += 'PRUEBA'; //text to print cmds += newLine + newLine; cmds += esc + '!' + '\x00'; //Character font A selected (ESC ! 0) + + // Sample items cmds += 'COOKIES 5.00'; cmds += newLine; cmds += 'MILK 65 Fl oz 3.78'; @@ -45,25 +50,91 @@ function prinTesTicket(printerName) { cmds += 'CASH TEND 10.00'; cmds += newLine; cmds += 'CASH DUE 0.78'; - cmds += newLine + newLine + newLine + newLine; + cmds += newLine + newLine; + + // Print barcode - Code128 + cmds += esc + 'a' + '\x01'; // Center alignment + cmds += 'CODIGO DE BARRAS:'; + cmds += newLine; + cmds += gs + 'h' + '\x50'; // Set barcode height to 80 dots + cmds += gs + 'w' + '\x02'; // Set barcode width to 2 + cmds += gs + 'H' + '\x02'; // Print HRI (human readable) below barcode + cmds += gs + 'k' + '\x49' + '\x0D' + '1234567890123'; // CODE128 barcode with content + cmds += newLine + newLine; + + // Print QR Code + cmds += 'CODIGO QR:'; + cmds += newLine; + // QR Code: Model + cmds += gs + '(k' + '\x04\x00' + '1A2\x00'; // Function 165: Set model (49=1, 50=2) + // QR Code: Size + cmds += gs + '(k' + '\x03\x00' + '1C' + '\x08'; // Function 167: Set size to 8 + // QR Code: Error correction level + cmds += gs + '(k' + '\x03\x00' + '1E0'; // Function 169: Error correction L + // QR Code: Store data + var qrData = 'https://megacity20.com'; + var qrDataLength = qrData.length + 3; + var pL = String.fromCharCode(qrDataLength % 256); + var pH = String.fromCharCode(Math.floor(qrDataLength / 256)); + cmds += gs + '(k' + pL + pH + '1P0' + qrData; // Function 180: Store data + // QR Code: Print + cmds += gs + '(k' + '\x03\x00' + '1Q0'; // Function 181: Print QR code + cmds += newLine + newLine; - fs.writeFile("ticket", cmds, function (err) { + // Reset alignment + cmds += esc + 'a' + '\x00'; // Left alignment + cmds += newLine + newLine; + + // add cut command (ESC m - partial cut) + cmds += '\x1B\x6D' + newLine; + + // add cash drawer open command (ESC p 0 55 121 - standard command) + cmds += '\x1B\x70\x30\x37\x79' + newLine; + + const ticketPath = path.join(app.getPath('temp'), 'fsprinter_ticket'); + fs.writeFile(ticketPath, cmds, function (err) { if (err) { return console.log(err); } prinTicket(printerName); }); + + // volcamos en consola la ruta del archivo + console.log('Ticket file path:', ticketPath); } function prinTicket(printerName) { - var printCmd = "lp -d " + printerName + " ticket"; + const ticketPath = path.join(app.getPath('temp'), 'fsprinter_ticket'); + let printCmd; + if (os.platform() == 'win32') { - var fullPath = '"' + app.getAppPath() + "\\"; - printCmd = fullPath + 'RawPrint.exe" "' + printerName + '" ticket'; + // Get the resources path properly + let resourcesPath; + if (app.isPackaged) { + // When packaged, unpacked files are in app.asar.unpacked + resourcesPath = path.join(process.resourcesPath, 'app.asar.unpacked'); + } else { + // In development, use the app path directly + resourcesPath = app.getAppPath(); + } + + const rawPrintPath = path.join(resourcesPath, 'RawPrint.exe'); + printCmd = '"' + rawPrintPath + '" "' + printerName + '" "' + ticketPath + '"'; + } else if (os.platform() == 'darwin') { + // macOS: usar lp con ruta absoluta + printCmd = 'lp -d "' + printerName + '" -o raw "' + ticketPath + '"'; + } else { + // Linux + printCmd = 'lp -d "' + printerName + '" "' + ticketPath + '"'; } + console.log('Print command:', printCmd); + exec(printCmd, (error, stdout, stderr) => { + if(stdout) { + console.log(`stdout: ${stdout}`); + } if (error) { console.log(`error: ${error.message}`); dialog.showErrorBox("print error", error.message); @@ -90,46 +161,45 @@ app.on('activate', () => { } }) -ipcMain.on('print-test', (event, arg) => { - prinTesTicket(arg); - event.reply('print-test', 'ok'); +ipcMain.handle('get-printers', async () => { + return await win.webContents.getPrintersAsync(); }) -ipcMain.on('print-2017', (event, arg) => { - console.log('print-2017'); +ipcMain.on('open-external', (event, url) => { + shell.openExternal(url); +}) - fs.writeFile("ticket", arg, function (err) { - if (err) { - return console.log(err); - } +ipcMain.on('reload-window', () => { + if (win) win.reload(); +}) - prinTicket(settings.getSync('printer.name')); - }); +ipcMain.handle('settings-get', async (event, key) => { + return await settings.get(key); +}) - event.reply('print-2017', 'ok'); +ipcMain.handle('settings-set', async (event, key, value) => { + return await settings.set(key, value); }) -ipcMain.on('print-2020', (event, element) => { - console.log('print-2020'); +ipcMain.handle('settings-has', async (event, key) => { + return await settings.has(key); +}) - var ticket = element.text; +ipcMain.handle('settings-unset', async () => { + return await settings.unset(); +}) - // apply cut command - var cutCode = settings.getSync('2020.cut') ?? '27.105'; - var cutComm = cutCode.split('.'); - if (cutComm.length > 0 && element.cortarpapel) { - ticket += String.fromCharCode(...cutComm); - } +ipcMain.on('print-test', (event, arg) => { + prinTesTicket(arg); + event.reply('print-test', 'ok'); +}) - // apply open command - var openCode = settings.getSync('2020.open') ?? '27.112.48'; - var openComm = openCode.split('.'); - if (openComm.length > 0 && element.abrircajon) { - ticket += String.fromCharCode(...openComm); - } +ipcMain.on('send-to-printer', (event, arg) => { + console.log('send-to-printer'); - // print - fs.writeFile("ticket", ticket, function (err) { + const ticketPath = path.join(app.getPath('temp'), 'fsprinter_ticket'); + var text = '\x1B' + '@' + arg; + fs.writeFile(ticketPath, text, function (err) { if (err) { return console.log(err); } @@ -137,22 +207,5 @@ ipcMain.on('print-2020', (event, element) => { prinTicket(settings.getSync('printer.name')); }); - event.reply('print-2020', 'ok'); + event.reply('send-to-printer', 'ok'); }) - -// http server -const http = require('http'); -const requestListener = function (req, res) { - res.writeHead(200); - res.end('fsprinter 0.6.1'); - console.log('http request: ' + req.url); - - // send a message to the main window - if (req.url.substring(0, 11) == '/?terminal=') { - win.webContents.send('http-request-2017', req.url.substring(2)); - } else if (req.url.substring(0, 12) == '/?documento=') { - win.webContents.send('http-request-2020', req.url.substring(12)); - } -} -const server = http.createServer(requestListener); -server.listen(8089); \ No newline at end of file diff --git a/package.json b/package.json index f0edfbe..129b2b2 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,73 @@ { "name": "fsprinter", - "version": "0.6.1", - "description": "Electron app to connect printers to FacturaScripts", + "version": "1.4.0", + "description": "Electron app to connect printers to FacturaScripts and factura.city", "main": "main.js", "scripts": { - "start": "electron-forge start", - "package": "electron-forge package", - "make": "electron-forge make" + "start": "electron . --no-sandbox", + "build": "electron-builder", + "build:linux": "electron-builder --linux", + "build:win": "electron-builder --win", + "build:mac": "electron-builder --mac" }, "keywords": [], - "author": "Carlos García Gómez", + "author": { + "name": "Carlos García Gómez", + "email": "carlos@facturascripts.com" + }, "license": "LGPL-3.0", "devDependencies": { - "@electron-forge/cli": "^6.0.0-beta.54", - "@electron-forge/maker-deb": "^6.0.0-beta.54", - "@electron-forge/maker-rpm": "^6.0.0-beta.54", - "@electron-forge/maker-squirrel": "^6.0.0-beta.54", - "@electron-forge/maker-zip": "^6.0.0-beta.54", - "electron": "^11.2.3" + "electron": "^42", + "electron-builder": "^26.15.6" }, "dependencies": { - "axios": "^0.21.0", - "bootstrap": "^5.0.0", + "axios": "^1.12.2", + "bootstrap": "^5.3.0", + "bootstrap-icons": "^1.11.0", "electron-settings": "^4.0.2", - "electron-squirrel-startup": "^1.0.0", "node-fetch": "^2.6.1" }, - "config": { - "forge": { - "packagerConfig": {}, - "makers": [ - { - "name": "@electron-forge/maker-squirrel", - "config": { - "name": "fsprinter" - } - }, - { - "name": "@electron-forge/maker-zip", - "platforms": [ - "darwin" - ] - }, - { - "name": "@electron-forge/maker-deb", - "config": {} - }, - { - "name": "@electron-forge/maker-rpm", - "config": {} - } + "build": { + "appId": "com.megacity20.fsprinter", + "productName": "FSPrinter", + "directories": { + "output": "dist" + }, + "files": [ + "**/*", + "!node_modules/**/*", + "node_modules/**/*" + ], + "asarUnpack": [ + "node_modules/**/*", + "RawPrint.exe" + ], + "linux": { + "target": [ + "deb", + "rpm", + "AppImage" + ], + "category": "Office" + }, + "win": { + "target": [ + "nsis", + "portable" ] + }, + "mac": { + "target": [ + "dmg", + "zip" + ], + "category": "public.app-category.productivity", + "hardenedRuntime": false, + "gatekeeperAssess": false, + "identity": null } + }, + "allowScripts": { + "electron-winstaller@5.4.0": true } } diff --git a/src/index.html b/src/index.html index 08c8bce..0c6ed79 100644 --- a/src/index.html +++ b/src/index.html @@ -1,150 +1,73 @@ - - fsprinter 0.6.1 + fsprinter 1.4.0 + - -
+
-

Impresora

-
-
-
-
-
+

Impresora

+
Selecciona la impresora de tickets que quieras utilizar.
+
+
-
- +
+
-
-
- - - -
-
-
-
-
-
- -
- Para conectar con FacturaScripts 2021 escribe aquí la url donde tengas FacturaScripts. -
-
-
-
-
- -
- Comprobar cada X segundos. -
-
-
-
-
-
- - -
-
- También necesitas una clave de API. Puedes crearla desde FacturaScripts, - en el menú Administrador > Panel de control > API keys. -
-
-
-
-
- -
- Comando de corte para la impresora. Ejemplo: 27.105 -
-
-
-
-
- -
- Comando de apertura de cajón para la impresora. Ejemplo: 27.112.48 -
-
-
+
+
+
-
-
-
-
-
-
- - -
-
- Para conectar con FacturaScripts 2017 escribe aquí la url donde tengas FacturaScripts. -
-
-
+
+
+
+ + +
-
-
-

Opcional

-

- Si tienes problemas al imprimir, puedes configurar aquí la impresión de todos - los tickets pendientes de un terminal concreto. -

-
-
-
- -
- Número de terminal. -
-
-
-
-
- -
- Comprobar cada X segundos. -
-
-
+
+ + +
+
+ + +
+
+ +
- - -