Compare commits
12
Commits
f09c92f201
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc59cf595e | ||
|
|
7ea03e5bc5 | ||
|
|
61d190ed91 | ||
|
|
46fd345b7f | ||
|
|
4308850a2e | ||
|
|
a58d4b66ea | ||
|
|
ec9278c0dd | ||
|
|
f351212395 | ||
|
|
eafea9f728 | ||
|
|
f8857384fd | ||
|
|
d2900361b3 | ||
|
|
790d716d57 |
No files matched your search
+31
-6
@@ -1,18 +1,43 @@
|
|||||||
= labelprint
|
= labelprint
|
||||||
|
|
||||||
Unified UI for printing labels/stickers:
|
Unified UI for printing labels/receipts:
|
||||||
|
|
||||||
* Native ZPL printing to a Datamax-ONeil thermal barcode printer
|
* Native ZPL printing to a Datamax-ONeil thermal barcode printer
|
||||||
* A proxy/switcher in front of three https://github.com/DL6ER/brother_ql_web[brother_ql_web] instances, one per Brother QL printer
|
* A proxy/switcher in front of three https://github.com/DL6ER/brother_ql_web[brother_ql_web] instances, one per Brother QL printer
|
||||||
|
* ESC/POS receipt printing to an Epson TM-T20II
|
||||||
|
|
||||||
== Routes
|
== Routes
|
||||||
|
|
||||||
* `/` -- upload a PDF and print it (ZPL)
|
* `/` -- home page, links to everything below
|
||||||
* `/upload` -- upload form
|
* `/upload`, `POST /upload` -- upload a PDF/ZPL file and print it (Datamax)
|
||||||
* `/test-print` -- print the bundled test label
|
* `POST /shipping/zpl` -- print raw ZPL text (see "API usage" below)
|
||||||
|
* `/test-print` -- print the bundled Datamax test label
|
||||||
|
* `/epson` -- receipt printing UI (title/body/QR template, or raw ESC/POS mnemonic notation)
|
||||||
|
* `POST /epson/print` -- print a title/body/QR receipt
|
||||||
|
* `POST /epson/raw` -- print already-formatted ESC/POS mnemonic notation (see "API usage" below)
|
||||||
|
* `POST /epson/test-print` -- print an Epson capability test page (text sizes, barcode, QR, raster image)
|
||||||
|
* `/labels` -- shared repository of saved ZPL labels / ESC-POS receipts (list, edit, preview, print, delete)
|
||||||
* `/brother` -- list of Brother QL printers
|
* `/brother` -- list of Brother QL printers
|
||||||
* `/brother/<printer>/*` -- proxied to that printer's brother_ql_web instance (UI and API alike)
|
* `/brother/<printer>/*` -- proxied to that printer's brother_ql_web instance (UI and API alike)
|
||||||
|
|
||||||
|
== API usage (e.g. from Gitea Actions)
|
||||||
|
|
||||||
|
`POST /shipping/zpl` and `POST /epson/raw` are plain, unauthenticated form-POST endpoints -- no session/login needed, and printing without a `save_as`/`commit` param just prints (nothing gets kept in the label repository). That makes labelprint usable as a "dumb" print API for other automation: it doesn't need to know anything about the caller, and the caller doesn't need any labelprint-specific client, just an HTTP form POST.
|
||||||
|
|
||||||
|
Example: `randy/home-gitops` has a workflow (`.gitea/workflows/print-issues.yaml` + `scripts/print_issues.rb`) that fetches its own open issues via the Gitea API and prints a summary on the Epson receipt printer -- the Gitea-specific logic (fetching, formatting) lives entirely in that script, not here:
|
||||||
|
|
||||||
|
[source,ruby]
|
||||||
|
----
|
||||||
|
require 'net/http'
|
||||||
|
|
||||||
|
res = Net::HTTP.post_form(
|
||||||
|
URI('http://rpi-printserver.hv.priesol.net/epson/raw'),
|
||||||
|
'content' => "ESC E 0x01\nHello from CI\nESC E 0x00\nLF"
|
||||||
|
)
|
||||||
|
----
|
||||||
|
|
||||||
|
`content` uses labelprint's mnemonic ESC/POS notation: `ESC`/`GS`/`FS`/`LF`/`CR`/`HT`/`NUL` as command names, `0xNN` as a raw byte, anything else as literal text (CP852-encoded) -- see the `/epson` page for the full reference and worked examples (alignment, bold, barcodes). `POST /shipping/zpl` works the same way but with a `zpl` form field taking plain ZPL instead.
|
||||||
|
|
||||||
== Running
|
== Running
|
||||||
|
|
||||||
[source,shell]
|
[source,shell]
|
||||||
@@ -25,9 +50,9 @@ Requires `config.json` (see printer definitions inside) and a `.session_secret`
|
|||||||
|
|
||||||
== Deployment
|
== Deployment
|
||||||
|
|
||||||
Runs as `sinatra.service` (systemd) on `rpi-printserver.local`, under RVM (`rvm default do bundle exec ruby app.rb` -- always uses whatever ruby is currently the RVM default, no manual bumping needed).
|
Runs as `sinatra.service` (systemd) on `rpi-printserver.hv.priesol.net`, under RVM (`rvm default do bundle exec ruby app.rb` -- always uses whatever ruby is currently the RVM default, no manual bumping needed).
|
||||||
|
|
||||||
`.gitea/workflows/deploy.yaml` deploys on every push to `main`: SSH into `rpi-printserver.local`, `git pull`, `bundle install`, restart the service.
|
`.gitea/workflows/deploy.yaml` deploys on every push to `main`: SSH into `rpi-printserver.hv.priesol.net`, `git pull`, `bundle install`, restart the service.
|
||||||
|
|
||||||
== Dependency updates
|
== Dependency updates
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ require_relative 'lib/label_repository'
|
|||||||
|
|
||||||
set :protection, :except => :frame_options
|
set :protection, :except => :frame_options
|
||||||
set :environment, :production
|
set :environment, :production
|
||||||
set :bind, "::"
|
# nginx reverse-proxies :80 -> here. Loopback-only and an unprivileged port
|
||||||
set :port, 80
|
# means no more cap_net_bind_service dance on every RVM ruby upgrade.
|
||||||
|
set :bind, "127.0.0.1"
|
||||||
|
set :port, 4567
|
||||||
|
|
||||||
if !File.exist?('config.json') || !File.readable?('config.json')
|
if !File.exist?('config.json') || !File.readable?('config.json')
|
||||||
raise RuntimeException, 'Could not read config.json'
|
raise RuntimeException, 'Could not read config.json'
|
||||||
@@ -107,6 +109,56 @@ helpers do
|
|||||||
receipt.print!(PRINTER_DEVICE_EPSON)
|
receipt.print!(PRINTER_DEVICE_EPSON)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Procedural 64x64 checkerboard (8px squares) -- no image library needed,
|
||||||
|
# just to prove GS v 0 raster printing works end to end.
|
||||||
|
def checkerboard_bitmap(size_px = 64, block_px = 8)
|
||||||
|
width_bytes = size_px / 8
|
||||||
|
bytes = "".b
|
||||||
|
size_px.times do |y|
|
||||||
|
row_block = y / block_px
|
||||||
|
width_bytes.times do |xb|
|
||||||
|
byte = 0
|
||||||
|
8.times do |bit|
|
||||||
|
x = xb * 8 + bit
|
||||||
|
col_block = x / block_px
|
||||||
|
byte |= (1 << (7 - bit)) if (row_block + col_block).even?
|
||||||
|
end
|
||||||
|
bytes << byte.chr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
# Demonstrates most of what the printer can do: text sizes/styles, EAN-13,
|
||||||
|
# QR code, and a raster image -- mirrors /test-print's role for the Datamax.
|
||||||
|
def print_epson_test_page
|
||||||
|
receipt = ReceiptBuilder.new
|
||||||
|
receipt.align(:center).bold.size(:big).text("TM-T20II TEST\n").size(:normal).bold(false)
|
||||||
|
receipt.align(:left).text(('-' * 48) + "\n")
|
||||||
|
|
||||||
|
receipt.section_separator('TEXT STYLES')
|
||||||
|
receipt.text("Normal text\n")
|
||||||
|
receipt.bold.text("Bold text\n").bold(false)
|
||||||
|
receipt.underline.text("Underlined text\n").underline(false)
|
||||||
|
receipt.size(:wide).text("Double width\n").size(:normal)
|
||||||
|
receipt.size(:high).text("Double height\n").size(:normal)
|
||||||
|
receipt.size(:big).text("Double both\n").size(:normal)
|
||||||
|
|
||||||
|
receipt.section_separator('BARCODE')
|
||||||
|
receipt.align(:center).ean13('2012345678902')
|
||||||
|
|
||||||
|
receipt.section_separator('QR CODE')
|
||||||
|
receipt.align(:center).qr('https://git.priesol.net/randy/home-gitops')
|
||||||
|
|
||||||
|
receipt.section_separator('IMAGE')
|
||||||
|
receipt.align(:center).image(64, 64, checkerboard_bitmap)
|
||||||
|
|
||||||
|
receipt.align(:left).text(('-' * 48) + "\n")
|
||||||
|
receipt.text("Printed: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
|
receipt.feed(3).cut
|
||||||
|
receipt.print!(PRINTER_DEVICE_EPSON)
|
||||||
|
end
|
||||||
|
|
||||||
ESCPOS_MNEMONICS = {
|
ESCPOS_MNEMONICS = {
|
||||||
'ESC' => "\x1B", 'GS' => "\x1D", 'FS' => "\x1C", 'DLE' => "\x10",
|
'ESC' => "\x1B", 'GS' => "\x1D", 'FS' => "\x1C", 'DLE' => "\x10",
|
||||||
'LF' => "\x0A", 'CR' => "\x0D", 'HT' => "\x09", 'NUL' => "\x00"
|
'LF' => "\x0A", 'CR' => "\x0D", 'HT' => "\x09", 'NUL' => "\x00"
|
||||||
@@ -117,17 +169,23 @@ helpers do
|
|||||||
# Hello # literal text
|
# Hello # literal text
|
||||||
# LF
|
# LF
|
||||||
# GS k 0x03 12345678 0x00
|
# GS k 0x03 12345678 0x00
|
||||||
# Each `#...` trailing comment is stripped, then each line's
|
# A trailing comment is only a comment when "#" is its own
|
||||||
# whitespace-separated tokens become either a known control mnemonic, a
|
# whitespace-separated token -- not just stripped from the first "#"
|
||||||
# raw `0xNN` byte, or literal text (CP852-encoded) -- consecutive literal
|
# anywhere in the line, which would mangle literal text like "#5 Fix
|
||||||
# tokens are rejoined with a single space so plain prose still reads
|
# thing" (an issue reference, not a comment). Each remaining token
|
||||||
# correctly, while whitespace around control/byte tokens is dropped since
|
# becomes a known control mnemonic, a raw `0xNN` byte, or literal text
|
||||||
# those are just for human readability, not printer output.
|
# (CP852-encoded) -- consecutive literal tokens are rejoined with a
|
||||||
|
# single space so plain prose still reads correctly, while whitespace
|
||||||
|
# around control/byte tokens is dropped since those are just for human
|
||||||
|
# readability, not printer output.
|
||||||
def parse_escpos_notation(text)
|
def parse_escpos_notation(text)
|
||||||
bytes = "".b
|
bytes = "".b
|
||||||
text.each_line do |line|
|
text.each_line do |line|
|
||||||
prev_was_text = false
|
prev_was_text = false
|
||||||
line.sub(/#.*/, '').split.each do |token|
|
tokens = line.split
|
||||||
|
comment_at = tokens.index('#')
|
||||||
|
tokens = tokens[0...comment_at] if comment_at
|
||||||
|
tokens.each do |token|
|
||||||
if ESCPOS_MNEMONICS.key?(token)
|
if ESCPOS_MNEMONICS.key?(token)
|
||||||
bytes << ESCPOS_MNEMONICS[token]
|
bytes << ESCPOS_MNEMONICS[token]
|
||||||
prev_was_text = false
|
prev_was_text = false
|
||||||
@@ -307,6 +365,16 @@ get '/epson' do
|
|||||||
erb :epson
|
erb :epson
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post '/epson/test-print' do
|
||||||
|
begin
|
||||||
|
print_epson_test_page
|
||||||
|
flash 'Test page sent to Epson printer', 'success'
|
||||||
|
rescue => e
|
||||||
|
flash "Epson test print failed (#{e.message})", 'danger'
|
||||||
|
end
|
||||||
|
redirect '/epson'
|
||||||
|
end
|
||||||
|
|
||||||
post '/epson/print' do
|
post '/epson/print' do
|
||||||
begin
|
begin
|
||||||
print_receipt(title: params[:title].to_s, body: params[:body].to_s, qr: params[:qr].to_s)
|
print_receipt(title: params[:title].to_s, body: params[:body].to_s, qr: params[:qr].to_s)
|
||||||
@@ -356,27 +424,27 @@ post '/epson/raw' do
|
|||||||
redirect '/epson'
|
redirect '/epson'
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/labels' do
|
get '/repository' do
|
||||||
@labels = LABEL_REPO.all
|
@labels = LABEL_REPO.all
|
||||||
erb :labels
|
erb :repository
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/labels/:id/edit' do
|
get '/repository/:id/edit' do
|
||||||
@label = LABEL_REPO.find(params[:id])
|
@label = LABEL_REPO.find(params[:id])
|
||||||
halt 404, 'unknown label' unless @label
|
halt 404, 'unknown label' unless @label
|
||||||
|
|
||||||
erb :label_edit
|
erb :label_edit
|
||||||
end
|
end
|
||||||
|
|
||||||
post '/labels/:id' do
|
post '/repository/:id' do
|
||||||
label = LABEL_REPO.update(params[:id], name: params[:name].to_s, content: params[:content].to_s)
|
label = LABEL_REPO.update(params[:id], name: params[:name].to_s, content: params[:content].to_s)
|
||||||
halt 404, 'unknown label' unless label
|
halt 404, 'unknown label' unless label
|
||||||
|
|
||||||
flash "Saved '#{label['name']}'", 'success'
|
flash "Saved '#{label['name']}'", 'success'
|
||||||
redirect '/labels'
|
redirect '/repository'
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/labels/:id/preview.png' do
|
get '/repository/:id/preview.png' do
|
||||||
label = LABEL_REPO.find(params[:id])
|
label = LABEL_REPO.find(params[:id])
|
||||||
halt 404 unless label && label['type'] == 'zpl'
|
halt 404 unless label && label['type'] == 'zpl'
|
||||||
|
|
||||||
@@ -387,7 +455,7 @@ get '/labels/:id/preview.png' do
|
|||||||
png
|
png
|
||||||
end
|
end
|
||||||
|
|
||||||
post '/labels/:id/print' do
|
post '/repository/:id/print' do
|
||||||
label = LABEL_REPO.find(params[:id])
|
label = LABEL_REPO.find(params[:id])
|
||||||
halt 404, 'unknown label' unless label
|
halt 404, 'unknown label' unless label
|
||||||
|
|
||||||
@@ -402,13 +470,13 @@ post '/labels/:id/print' do
|
|||||||
rescue => e
|
rescue => e
|
||||||
flash "Print failed (#{e.message})", 'danger'
|
flash "Print failed (#{e.message})", 'danger'
|
||||||
end
|
end
|
||||||
redirect '/labels'
|
redirect '/repository'
|
||||||
end
|
end
|
||||||
|
|
||||||
post '/labels/:id/delete' do
|
post '/repository/:id/delete' do
|
||||||
LABEL_REPO.delete(params[:id])
|
LABEL_REPO.delete(params[:id])
|
||||||
flash 'Label deleted', 'success'
|
flash 'Label deleted', 'success'
|
||||||
redirect '/labels'
|
redirect '/repository'
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/brother' do
|
get '/brother' do
|
||||||
|
|||||||
+36
-2
@@ -65,6 +65,28 @@ class ReceiptBuilder
|
|||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# White-on-black reverse video (GS B n) -- available if a receipt ever
|
||||||
|
# wants a real highlight, but plain "=" rules read better for routine
|
||||||
|
# section breaks than a fully blacked-out line.
|
||||||
|
def invert(enable = true)
|
||||||
|
@buffer << (enable ? "\x1D\x42\x01" : "\x1D\x42\x00")
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
# A single "=" rule with an optional label centered in it, e.g.
|
||||||
|
# section_separator("SECTION") -> "========= SECTION =========".
|
||||||
|
def section_separator(label = '', width: 48)
|
||||||
|
line = if label.empty?
|
||||||
|
'=' * width
|
||||||
|
else
|
||||||
|
mid = " #{label} "
|
||||||
|
pad = [width - mid.length, 0].max
|
||||||
|
left = pad / 2
|
||||||
|
('=' * left) + mid + ('=' * (pad - left))
|
||||||
|
end
|
||||||
|
feed(1).text("#{line}\n").feed(1)
|
||||||
|
end
|
||||||
|
|
||||||
def size(mode)
|
def size(mode)
|
||||||
@buffer << case mode
|
@buffer << case mode
|
||||||
when :big then TXT_BIG
|
when :big then TXT_BIG
|
||||||
@@ -87,12 +109,24 @@ class ReceiptBuilder
|
|||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Native ESC/POS EAN-13
|
# Native ESC/POS EAN-13. height is in dots (GS h) -- the printer's default
|
||||||
def ean13(code)
|
# (~162 dots) prints noticeably tall; 50 is a shorter, still-scannable size.
|
||||||
|
def ean13(code, height: 50)
|
||||||
|
@buffer << "\x1D\x68" << height.chr # GS h n -- set barcode height
|
||||||
@buffer << "\x1D\x6B\x02" << code.b << "\x00"
|
@buffer << "\x1D\x6B\x02" << code.b << "\x00"
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Raster bit image (GS v 0) -- bytes must already be packed 1bpp,
|
||||||
|
# row-major, (width_px/8 rounded up) bytes per row, MSB-first, 1=black.
|
||||||
|
def image(width_px, height_px, bytes)
|
||||||
|
width_bytes = (width_px + 7) / 8
|
||||||
|
@buffer << "\x1D\x76\x30\x00"
|
||||||
|
@buffer << [width_bytes % 256, width_bytes / 256, height_px % 256, height_px / 256].pack('C4')
|
||||||
|
@buffer << bytes
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
def feed(lines = 3)
|
def feed(lines = 3)
|
||||||
@buffer << ("\n" * lines)
|
@buffer << ("\n" * lines)
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="sk">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta http-equiv="refresh" content="5">
|
||||||
|
<title>Print server</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { background: #212529; color: #adb5bd; }
|
||||||
|
.mdi-printer-off { font-size: 4rem; color: #6c757d; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="d-flex align-items-center justify-content-center vh-100">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="mdi mdi-printer-off"></i>
|
||||||
|
<h1 class="mt-3">Tlačiareň spí</h1>
|
||||||
|
<p class="text-muted">Print server práve nabieha alebo sa reštartuje.<br>Stránka sa sama skúsi znova o chvíľu...</p>
|
||||||
|
<div class="spinner-border text-secondary mt-2" role="status"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Installed at /etc/nginx/sites-available/labelprint on rpi-printserver
|
||||||
|
# (symlinked into sites-enabled), not auto-deployed -- kept here just so a
|
||||||
|
# reflash/rebuild isn't a guessing game. nginx owns :80 so the app itself
|
||||||
|
# never needs cap_net_bind_service (which a plain RVM ruby patch upgrade
|
||||||
|
# silently drops, since it's tied to the old binary's path).
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name rpi-printserver.hv.priesol.net;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:4567;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 502 503 504 /502.html;
|
||||||
|
location = /502.html {
|
||||||
|
root /etc/nginx/error_pages;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid d-flex align-items-center">
|
||||||
<a class="navbar-brand" href="/">Print server</a>
|
<a class="navbar-brand" href="/"><i class="mdi mdi-printer"></i> Print server</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<a class="nav-link <%= 'active' if request.path == '/' %>" href="/">Home</a>
|
<a class="nav-link <%= 'active' if request.path == '/' %>" href="/">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= 'active' if request.path.start_with?('/brother') %>" href="/brother">Brother</a>
|
<a class="nav-link border-start <%= 'active' if request.path.start_with?('/brother') %>" href="/brother">Brother</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= 'active' if request.path == '/upload' %>" href="/upload">Shipping labels</a>
|
<a class="nav-link <%= 'active' if request.path == '/upload' %>" href="/upload">Shipping labels</a>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<a class="nav-link <%= 'active' if request.path == '/epson' %>" href="/epson">Receipts</a>
|
<a class="nav-link <%= 'active' if request.path == '/epson' %>" href="/epson">Receipts</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= 'active' if request.path.start_with?('/labels') %>" href="/labels">Labels</a>
|
<a class="nav-link border-start <%= 'active' if request.path.start_with?('/repository') %>" href="/repository">Labels</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<% if request.path.start_with?('/brother') %>
|
<% if request.path.start_with?('/brother') %>
|
||||||
|
|||||||
+5
-1
@@ -15,6 +15,10 @@
|
|||||||
<button class="btn btn-secondary" type="submit">Print</button>
|
<button class="btn btn-secondary" type="submit">Print</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form action="/epson/test-print" method="POST" class="mt-3">
|
||||||
|
<button class="btn btn-outline-secondary" type="submit">Print test page (sizes, barcode, QR, image)</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<hr class="mt-4">
|
<hr class="mt-4">
|
||||||
|
|
||||||
<h5>Free text (ESC/POS mnemonic notation)</h5>
|
<h5>Free text (ESC/POS mnemonic notation)</h5>
|
||||||
@@ -47,4 +51,4 @@ GS V 0x00 # Full cut</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mt-4"><a href="/labels">See saved receipts →</a></p>
|
<p class="mt-4"><a href="/repository">See saved receipts →</a></p>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<span class="badge bg-info text-dark">ESC/POS · Epson</span>
|
<span class="badge bg-info text-dark">ESC/POS · Epson</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<form action="/labels/<%= @label['id'] %>" method="POST" class="mt-3">
|
<form action="/repository/<%= @label['id'] %>" method="POST" class="mt-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input name="name" class="form-control" type="text" id="name" value="<%= @label['name'] %>" />
|
<input name="name" class="form-control" type="text" id="name" value="<%= @label['name'] %>" />
|
||||||
@@ -16,5 +16,5 @@
|
|||||||
<textarea name="content" class="form-control" id="content" rows="12" style="font-family: monospace;"><%= @label['content'] %></textarea>
|
<textarea name="content" class="form-control" id="content" rows="12" style="font-family: monospace;"><%= @label['content'] %></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" type="submit">Save</button>
|
<button class="btn btn-secondary" type="submit">Save</button>
|
||||||
<a href="/labels" class="btn btn-outline-secondary">Cancel</a>
|
<a href="/repository" class="btn btn-outline-secondary">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
+5
-4
@@ -5,12 +5,13 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAOzs7ADl5eUA/Pz8ACsZBgD+/v4AAwMDAPv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWZmZmZmIxERZmZmZmZmERFmZmZmZmYREWZmZoZmZhERZmYUVmZmERFmYREWZmYREWYRERFmZhERZhYREXZmERFmZmYRZmYREWZmZhFmZhERZmZmERFmERFmZmYHEWYREWZmZmZmZhERZmZmZmZmERFmZmZmZmYREWZmZmZmZhHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" />
|
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAOzs7ADl5eUA/Pz8ACsZBgD+/v4AAwMDAPv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWZmZmZmIxERZmZmZmZmERFmZmZmZmYREWZmZoZmZhERZmYUVmZmERFmYREWZmYREWYRERFmZhERZhYREXZmERFmZmYRZmYREWZmZhFmZhERZmZmERFmERFmZmYHEWYREWZmZmZmZhERZmZmZmZmERFmZmZmZmYREWZmZmZmZhHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
|
||||||
<link href="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone.css" rel="stylesheet" type="text/css" />
|
<link href="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone.css" rel="stylesheet" type="text/css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
|
||||||
<%= erb :_navbar, layout: false %>
|
<%= erb :_navbar, layout: false %>
|
||||||
|
<main class="container-fluid px-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<% if @alert_success then %>
|
<% if @alert_success then %>
|
||||||
<div class="alert alert-success"><%=@alert_success%></div>
|
<div class="alert alert-success"><%=@alert_success%></div>
|
||||||
@@ -23,8 +24,8 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script>
|
<script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
Dropzone.options.UploadForm = {
|
Dropzone.options.UploadForm = {
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
<strong class="ms-2"><%= label['name'] %></strong>
|
<strong class="ms-2"><%= label['name'] %></strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<form action="/labels/<%= label['id'] %>/print" method="POST">
|
<form action="/repository/<%= label['id'] %>/print" method="POST">
|
||||||
<button class="btn btn-sm btn-secondary" type="submit">Print</button>
|
<button class="btn btn-sm btn-secondary" type="submit">Print</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="/labels/<%= label['id'] %>/edit" class="btn btn-sm btn-outline-secondary">Edit</a>
|
<a href="/repository/<%= label['id'] %>/edit" class="btn btn-sm btn-outline-secondary">Edit</a>
|
||||||
<form action="/labels/<%= label['id'] %>/delete" method="POST">
|
<form action="/repository/<%= label['id'] %>/delete" method="POST">
|
||||||
<button class="btn btn-sm btn-outline-danger" type="submit">Delete</button>
|
<button class="btn btn-sm btn-outline-danger" type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<% if label['type'] == 'zpl' %>
|
<% if label['type'] == 'zpl' %>
|
||||||
<img src="/labels/<%= label['id'] %>/preview.png" alt="preview" style="max-height: 150px;" onerror="this.remove()" />
|
<img src="/repository/<%= label['id'] %>/preview.png" alt="preview" style="max-height: 150px;" onerror="this.remove()" />
|
||||||
<% else %>
|
<% else %>
|
||||||
<pre class="bg-light p-2 mb-0" style="max-height: 150px; overflow: auto;"><%= label['content'] %></pre>
|
<pre class="bg-light p-2 mb-0" style="max-height: 150px; overflow: auto;"><%= label['content'] %></pre>
|
||||||
<% end %>
|
<% end %>
|
||||||
+1
-1
@@ -33,4 +33,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mt-4"><a href="/labels">See saved labels →</a></p>
|
<p class="mt-4"><a href="/repository">See saved labels →</a></p>
|
||||||
Reference in new issue
Block a user