diff --git a/backend/backend/settings.py b/backend/backend/settings.py index e56e4ab..a1e0883 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -10,30 +10,12 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ import os -import subprocess import dotenv from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - -def _git_commit(): - # deploy/dev/docker-compose.yml bind-mounts the repo's real .git dir at - # /git (separate from BASE_DIR, which only has the backend/ subtree) - - # point git at it explicitly there. Bare-metal dev has no such mount, but - # BASE_DIR sits inside the real checkout so plain rev-parse finds it by - # walking up. In prod, the build context is backend/ alone with no .git - # anywhere, so both fail and we fall back to the GIT_COMMIT build-arg/env - # var (see deploy/prod/Dockerfile.backend and playbook.yml). - cmd = ['git', '--git-dir=/git'] if os.path.isdir('/git') else ['git'] - try: - return subprocess.check_output( - [*cmd, 'rev-parse', '--short', 'HEAD'], cwd=BASE_DIR, stderr=subprocess.DEVNULL - ).decode().strip() - except (subprocess.CalledProcessError, FileNotFoundError, OSError): - return os.environ.get('GIT_COMMIT', 'unknown') - dotenv.load_dotenv(BASE_DIR / '.env') # Quick-start development settings - unsuitable for production @@ -48,7 +30,6 @@ DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true' # Application definition TOOLSHED_VERSION = "0.0.0-dev.0" -GIT_COMMIT = _git_commit() INSTALLED_APPS = [ 'django.contrib.admin', diff --git a/backend/toolshed/api/info.py b/backend/toolshed/api/info.py index a99b398..7395c03 100644 --- a/backend/toolshed/api/info.py +++ b/backend/toolshed/api/info.py @@ -7,14 +7,14 @@ from hostadmin.models import Domain from authentication.signature_auth import SignatureAuthentication from toolshed.models import Tag, Property, Category, InventoryItem from toolshed.serializers import CategorySerializer, PropertySerializer -from backend.settings import TOOLSHED_VERSION, GIT_COMMIT +from backend.settings import TOOLSHED_VERSION @api_view(['GET']) @permission_classes([]) @authentication_classes([]) def get_version(request, format=None): # /version/ - return Response({'version': TOOLSHED_VERSION, 'commit': GIT_COMMIT}) + return Response({'version': TOOLSHED_VERSION}) @api_view(['GET']) diff --git a/deploy/dev/Dockerfile.backend b/deploy/dev/Dockerfile.backend index d1f35f4..1134aba 100644 --- a/deploy/dev/Dockerfile.backend +++ b/deploy/dev/Dockerfile.backend @@ -7,7 +7,7 @@ ENV PYTHONUNBUFFERED 1 # Set work directory WORKDIR /code -RUN mkdir /git + # Install dependencies COPY requirements.txt /code/ RUN pip install --no-cache-dir -r requirements.txt diff --git a/deploy/dev/Dockerfile.frontend b/deploy/dev/Dockerfile.frontend index 02b9e79..07f7c49 100644 --- a/deploy/dev/Dockerfile.frontend +++ b/deploy/dev/Dockerfile.frontend @@ -3,7 +3,6 @@ FROM node:14 # Set work directory WORKDIR /app -RUN mkdir /git # Install app dependencies # A wildcard is used to ensure both package.json AND package-lock.json are copied diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml index 30ed12a..abd20f2 100644 --- a/deploy/dev/docker-compose.yml +++ b/deploy/dev/docker-compose.yml @@ -12,7 +12,6 @@ services: TOOLSHED_SETUP_PATH: /mnt/testdata.py volumes: - ../../backend:/code - - ../../.git:/git:ro - ./instance_a/a.env:/code/.env - ./instance_a/testdata.py:/mnt/testdata.py - ./instance_a/a.sqlite3:/mnt/db.sqlite3 @@ -31,7 +30,6 @@ services: TOOLSHED_SETUP_PATH: /mnt/testdata.py volumes: - ../../backend:/code - - ../../.git:/git:ro - ./instance_b/b.env:/code/.env - ./instance_b/testdata.py:/mnt/testdata.py - ./instance_b/b.sqlite3:/mnt/db.sqlite3 @@ -46,7 +44,6 @@ services: dockerfile: ../deploy/dev/Dockerfile.frontend volumes: - ../../frontend:/app - - ../../.git:/git:ro - /app/node_modules expose: - 5173 diff --git a/deploy/prod/Dockerfile.backend b/deploy/prod/Dockerfile.backend index d64a6fd..b6cf9af 100644 --- a/deploy/prod/Dockerfile.backend +++ b/deploy/prod/Dockerfile.backend @@ -5,15 +5,9 @@ FROM python:3.11-slim -# The build context here is just backend/ (no .git), so settings.py's own -# `git rev-parse` fallback can't find a repo - the actual commit is passed -# in from the real checkout via this build-arg instead (see playbook.yml). -ARG GIT_COMMIT=unknown - ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - DJANGO_SETTINGS_MODULE=backend.settings \ - GIT_COMMIT=$GIT_COMMIT + DJANGO_SETTINGS_MODULE=backend.settings WORKDIR /app diff --git a/deploy/prod/Dockerfile.frontend b/deploy/prod/Dockerfile.frontend index 1def462..12dec8d 100644 --- a/deploy/prod/Dockerfile.frontend +++ b/deploy/prod/Dockerfile.frontend @@ -6,14 +6,6 @@ FROM node:20-alpine AS build WORKDIR /app - -# The build context here is just frontend/ (no .git), so vite.config.js's -# own `git rev-parse` fallback can't find a repo - the actual commit is -# passed in from the real checkout via this build-arg instead (see -# playbook.yml). -ARG GIT_COMMIT=unknown -ENV GIT_COMMIT=$GIT_COMMIT - COPY package.json package-lock.json ./ COPY extras/ ./extras/ RUN npm ci diff --git a/deploy/prod/playbook.yml b/deploy/prod/playbook.yml index aef70b6..d46c406 100644 --- a/deploy/prod/playbook.yml +++ b/deploy/prod/playbook.yml @@ -294,7 +294,6 @@ # pinned commit isn't fetchable from upstream - don't let a broken # submodule block the checkout. recursive: false - register: toolshed_checkout - name: Create toolshed system user ansible.builtin.user: @@ -376,7 +375,6 @@ ansible.builtin.command: cmd: >- docker build -t {{ toolshed_backend_image }}:{{ toolshed_image_tag }} - --build-arg GIT_COMMIT={{ toolshed_checkout.after[:7] }} -f {{ toolshed_src_dir }}/deploy/prod/Dockerfile.backend {{ toolshed_src_dir }}/backend changed_when: true notify: restart backend @@ -464,7 +462,6 @@ version: "{{ toolshed_version | default('stable') }}" force: true recursive: false - register: toolshed_frontend_checkout delegate_to: localhost become: false @@ -472,7 +469,6 @@ ansible.builtin.command: cmd: >- docker build -t {{ toolshed_frontend_image }}:{{ toolshed_image_tag }} - --build-arg GIT_COMMIT={{ toolshed_frontend_checkout.after[:7] }} -f {{ toolshed_frontend_build_src_dir }}/deploy/prod/Dockerfile.frontend {{ toolshed_frontend_build_src_dir }}/frontend changed_when: true delegate_to: localhost diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 3a489d0..d9a1ac2 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -74,22 +74,11 @@ export default { width: 100%; min-width: 0; min-height: 100vh; - margin-left: 260px; transition: margin-left .35s ease-in-out, left .35s ease-in-out, margin-right .35s ease-in-out, right .35s ease-in-out; flex-direction: column; overflow: hidden; } -.main.expanded { - margin-left: 0; -} - -@media (min-width: 1px) and (max-width: 991.98px) { - .main, .main.expanded { - margin-left: 0; - } -} - .navbar-expand { flex-wrap: nowrap; justify-content: flex-start; diff --git a/frontend/src/components/LabelLayoutPreview.vue b/frontend/src/components/LabelLayoutPreview.vue deleted file mode 100644 index 74c97a5..0000000 --- a/frontend/src/components/LabelLayoutPreview.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - - - diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index be71d2a..de5fc9a 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -58,7 +58,6 @@ - @@ -71,37 +70,31 @@ export default { components: { ...BIcons }, - data() { - return { - frontendCommit: __GIT_COMMIT__, - backendCommit: null, - } - }, - computed: { - versionTitle() { - return `frontend build: ${this.frontendCommit}, backend: ${this.backendCommit || 'unknown'}` - } - }, - async mounted() { - try { - const {commit} = await this.$store.dispatch('fetchBackendVersion') - this.backendCommit = commit - } catch (e) { - console.error('could not fetch backend version', e) - } - }, } \ No newline at end of file diff --git a/frontend/src/label-content.js b/frontend/src/label-content.js deleted file mode 100644 index 1bb440a..0000000 --- a/frontend/src/label-content.js +++ /dev/null @@ -1,45 +0,0 @@ -// Turns a {kind, components} prefill into the literal string a print label should show/encode. -// Keeping this keyed by `kind` rather than having each caller build its own string means the -// format for a given kind of label content only has to be gotten right in one place. -export const LABEL_CONTENT_BUILDERS = { - // The self-contained Item URL (see docs/design-in-progress/items-labels.md) - what a - // printed label actually encodes, since scanning it has to resolve the right - // frontend/backend/item with no other context, not just this browser's history. - "item-url": ({user, id}) => `${window.location.origin}/i/${user}/${id}`, -}; - -export function buildLabelContent(prefill) { - if (!prefill) { - return ""; - } - const build = LABEL_CONTENT_BUILDERS[prefill.kind]; - return build ? build(prefill.components) : ""; -} - -// Named fields the field-specific label templates (see label-drawing.js's LABEL_TEMPLATES) draw -// from - keyed by `kind` for the same reason LABEL_CONTENT_BUILDERS is. A field missing from the -// result (rather than present-but-empty) is what LabelLayoutPreview.vue treats as "not available", -// so builders should only include a field once its inputs actually check out. -const LABEL_FIELD_BUILDERS = { - "item-url": ({user, id}) => { - if (!user || !id) { - return {}; - } - return { - itemUrl: `${window.location.origin}/i/${user}/${id}`, - // The compact "owner handle + id" form from docs/design-in-progress/items-labels.md - - // meaningful only where context already makes clear it's a Toolshed item, unlike itemUrl. - itemHandle: `${user}:${id}`, - userHandle: user, - itemId: String(id), - }; - }, -}; - -export function buildLabelFields(prefill) { - if (!prefill) { - return {}; - } - const build = LABEL_FIELD_BUILDERS[prefill.kind]; - return build ? build(prefill.components) : {}; -} diff --git a/frontend/src/label-drawing.js b/frontend/src/label-drawing.js deleted file mode 100644 index c514040..0000000 --- a/frontend/src/label-drawing.js +++ /dev/null @@ -1,329 +0,0 @@ -import QRCode from "qrcode"; - -const TRAILING_PADDING_PX = 3; /* blank columns after the cut, same idea as the leading margin */ -/* A quiet zone narrower than the spec's usual 4 modules: the printer's own - feed margin already keeps the code clear of the tape edge and the cut. */ -const QUIET_ZONE_MODULES = 2; - -/* Sizing a label needs numbers only the driver can supply. */ -export function tapeFromStatus(status) { - const printAreaPx = status?.tape?.printAreaPx; - const dpi = status?.printer?.dpi; - if (!(printAreaPx > 0) || !(dpi > 0)) { - return null; - } - return { - printAreaPx, - dpi, - mediaWidthMm: status.tape.mediaWidthMm, - printLengthPx: status.tape.printLengthPx > 0 ? status.tape.printLengthPx : 0, - /* Brother's documented margin for the mounted tape, in raster columns. */ - leadPx: status.tape.marginsMm - ? Math.round(status.tape.marginsMm * dpi / 25.4) - : TRAILING_PADDING_PX, - }; -} - -/* - Draw the QR code as large as the tape allows, centered in a square, with an - integer number of pixels per module so it stays crisp at printer - resolution rather than blurring at a fractional scale. -*/ -function drawQrLabel(canvas, qr, tape) { - const modules = qr.modules.size + QUIET_ZONE_MODULES * 2; - const maxLength = tape.printLengthPx - ? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX - : Infinity; - const scale = Math.floor(Math.min(tape.printAreaPx, maxLength) / modules); - if (!(scale >= 1)) { - throw new Error("This text needs a bigger QR code than the tape allows — " - + "try a shorter value or a wider tape."); - } - const square = modules * scale; - - const width = tape.printLengthPx || (square + tape.leadPx + TRAILING_PADDING_PX); - canvas.width = width; - canvas.height = tape.printAreaPx; - - const ctx = canvas.getContext("2d", {willReadFrequently: true}); - ctx.fillStyle = "#fff"; - ctx.fillRect(0, 0, width, canvas.height); - - const left = tape.leadPx + Math.floor((width - tape.leadPx - TRAILING_PADDING_PX - square) / 2); - const top = Math.floor((canvas.height - square) / 2); - ctx.fillStyle = "#000"; - for (let row = 0; row < qr.modules.size; row++) { - for (let col = 0; col < qr.modules.size; col++) { - if (qr.modules.get(row, col)) { - ctx.fillRect( - left + (col + QUIET_ZONE_MODULES) * scale, - top + (row + QUIET_ZONE_MODULES) * scale, - scale, scale, - ); - } - } - } -} - -// Each template names which field (see label-content.js's buildLabelFields) feeds its QR code -// and/or its printed text - `null` means that half of the layout is skipped. `text` can also be -// an array of field names, one per printed line, for a stacked multi-line layout. A template is -// only selectable once every field it names is actually available (see templateIsAvailable). -export const LABEL_TEMPLATES = [ - {id: "qr", name: "QR code only", description: "Just the code - smallest label, prints fastest.", - qr: "value", text: null}, - {id: "qr-text", name: "QR code + text", description: "The code with the encoded text printed next to it.", - qr: "value", text: "value"}, - {id: "text", name: "Text only", description: "No code, just the text itself, as large as it fits.", - qr: null, text: "value"}, - {id: "item-handle", name: "Item handle", qr: null, text: "itemHandle", - description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own."}, - {id: "item-url", name: "Item URL", qr: null, text: "itemUrl", - description: "The full item URL as text, with no code - for copying rather than scanning."}, - {id: "owner-handle", name: "Owner handle", qr: null, text: "userHandle", - description: "Just the owning user's handle, as text only."}, - {id: "item-id", name: "Item ID", qr: null, text: "itemId", - description: "Just the bare item id, as text only."}, - {id: "owner-id-text", name: "Owner + item ID", qr: null, text: ["userHandle", "itemId"], - description: "The owner's handle and the item id, as two lines of text - no code."}, - {id: "item-url-qr-handle", name: "Item URL + handle", qr: "itemUrl", text: "itemHandle", - description: "Scannable item URL, with the item's compact handle printed alongside."}, - {id: "item-url-qr-owner", name: "Item URL + owner", qr: "itemUrl", text: "userHandle", - description: "Scannable item URL, with the owner's handle printed alongside."}, - {id: "item-url-qr-id", name: "Item URL + item ID", qr: "itemUrl", text: "itemId", - description: "Scannable item URL, with the bare item id printed alongside."}, - {id: "item-url-qr-owner-id", name: "Item URL + owner + ID", qr: "itemUrl", text: ["userHandle", "itemId"], - description: "Scannable item URL, with the owner's handle and the item id on two lines alongside."}, -]; - -// keysOf/templateIsAvailable/templateContent are the single place that understands the `qr`/ -// `text` field-name shape above (including `text` sometimes being an array) - both -// LabelLayoutPreview.vue's thumbnail grid and Print.vue's big preview resolve a template through -// these rather than each re-implementing the same lookup. -function keysOf(spec) { - if (!spec) { - return []; - } - return Array.isArray(spec) ? spec : [spec]; -} - -export function templateIsAvailable(t, fields) { - return [...keysOf(t.qr), ...keysOf(t.text)].every(key => fields[key] !== undefined); -} - -/* Resolves a template's field names against actual field values. `text` comes back as an array - whenever the template's `text` spec is an array (multi-line), or a plain string otherwise - - drawLabel/drawFallbackLabel below accept either. */ -export function templateContent(t, fields) { - const resolve = (spec) => { - if (!spec) { - return null; - } - return Array.isArray(spec) ? spec.map(key => fields[key]) : fields[spec]; - }; - return {qr: resolve(t.qr), text: resolve(t.text)}; -} - -function measureAtHeight(ctx, text, px) { - ctx.font = `${px}px sans-serif`; - return ctx.measureText(text).width; -} - -/* Picks the largest integer font size (down to a floor) that fits every one of `lines` within - maxWidth, stacked within maxHeight - this is a label, not a paragraph, so each line shrinks to - fit rather than wrapping. */ -function fitTextSize(ctx, lines, maxWidth, maxHeight) { - const minPx = 8; - let px = Math.max(minPx, Math.floor(maxHeight / lines.length)); - while (px > minPx && lines.some(line => measureAtHeight(ctx, line, px) > maxWidth)) { - px -= 1; - } - return px; -} - -/* The qr-text/text-only layouts, tape-fed. Kept separate from drawQrLabel above (rather than - generalizing it) so the plain QR-only path - the common case - is untouched by this. */ -function drawLabelWithText(canvas, tape, qrContent, textContent) { - const qr = qrContent ? QRCode.create(qrContent) : null; - const lines = Array.isArray(textContent) ? textContent : [textContent]; - const availableHeight = tape.printAreaPx; - const maxLength = tape.printLengthPx - ? tape.printLengthPx - tape.leadPx - TRAILING_PADDING_PX - : Infinity; - - let scale = 0, qrSize = 0; - if (qr) { - const modules = qr.modules.size + QUIET_ZONE_MODULES * 2; - // The QR only gets half the length budget on a fixed-length tape, so a long text value - // can't starve it down to unreadable - the rest goes to the text next to it. - const qrBudget = maxLength === Infinity ? Infinity : maxLength / 2; - scale = Math.floor(Math.min(availableHeight, qrBudget) / modules); - if (!(scale >= 1)) { - throw new Error("This text needs a bigger QR code than the tape allows — " - + "try a shorter value, a wider tape, or the text-only layout."); - } - qrSize = modules * scale; - } - - const gap = qr ? Math.round(availableHeight * 0.15) : 0; - const textBudget = maxLength === Infinity ? Infinity : maxLength - qrSize - gap; - if (!(textBudget > 0)) { - throw new Error("No room left for the text next to the QR code on this tape — " - + "try a wider tape or the QR-only layout."); - } - const measureCtx = canvas.getContext("2d"); - const textPx = fitTextSize(measureCtx, lines, textBudget, availableHeight); - const textWidth = Math.max(...lines.map(line => measureAtHeight(measureCtx, line, textPx))); - if (textBudget !== Infinity && textWidth > textBudget) { - throw new Error("This text doesn't fit on this tape even at the smallest readable size — " - + "try a shorter value, a wider tape, or a bigger label."); - } - - const contentWidth = qrSize + gap + textWidth; - const width = tape.printLengthPx || Math.ceil(contentWidth + tape.leadPx + TRAILING_PADDING_PX); - canvas.width = width; - canvas.height = availableHeight; - - const ctx = canvas.getContext("2d", {willReadFrequently: true}); - ctx.fillStyle = "#fff"; - ctx.fillRect(0, 0, width, canvas.height); - ctx.fillStyle = "#000"; - - let cursor = tape.leadPx + Math.floor((width - tape.leadPx - TRAILING_PADDING_PX - contentWidth) / 2); - - if (qr) { - const top = Math.floor((canvas.height - qrSize) / 2); - for (let row = 0; row < qr.modules.size; row++) { - for (let col = 0; col < qr.modules.size; col++) { - if (qr.modules.get(row, col)) { - ctx.fillRect( - cursor + (col + QUIET_ZONE_MODULES) * scale, - top + (row + QUIET_ZONE_MODULES) * scale, - scale, scale, - ); - } - } - } - cursor += qrSize + gap; - } - - ctx.font = `${textPx}px sans-serif`; - ctx.textBaseline = "middle"; - ctx.textAlign = "left"; - // Lines stack as a block vertically centered in the label, rather than each line centered on - // its own - keeps a two-line block reading as one unit instead of drifting apart. - const lineHeight = Math.ceil(textPx * 1.15); - let y = (canvas.height - lineHeight * lines.length) / 2 + lineHeight / 2; - for (const line of lines) { - ctx.fillText(line, cursor, y); - y += lineHeight; - } -} - -/* Dispatches to the right tape-fed layout - drawQrLabel is untouched so the plain QR-only - layout keeps its exact original pixel output. `content` is {qr, text}, each either the string - to encode/print or null/undefined to skip that half of the layout (see LABEL_TEMPLATES). */ -export function drawLabel(canvas, tape, content) { - if (content.qr && !content.text) { - drawQrLabel(canvas, QRCode.create(content.qr), tape); - } else { - drawLabelWithText(canvas, tape, content.qr, content.text); - } -} - -const FALLBACK_SCALE_PX = 8; /* pixels per QR module in the no-webusb preview/PNG */ -const FALLBACK_QUIET_ZONE_MODULES = 4; /* the spec's usual quiet zone - there's no printer feed margin to lean on here */ - -/* Same idea as drawQrLabel, but without a real device to ask for tape dimensions: just a - plain square QR code, sized for a PNG someone downloads and prints some other way. */ -function drawQrSquare(canvas, qr) { - const modules = qr.modules.size + FALLBACK_QUIET_ZONE_MODULES * 2; - const size = modules * FALLBACK_SCALE_PX; - canvas.width = size; - canvas.height = size; - - const ctx = canvas.getContext("2d", {willReadFrequently: true}); - ctx.fillStyle = "#fff"; - ctx.fillRect(0, 0, size, size); - ctx.fillStyle = "#000"; - for (let row = 0; row < qr.modules.size; row++) { - for (let col = 0; col < qr.modules.size; col++) { - if (qr.modules.get(row, col)) { - ctx.fillRect( - (col + FALLBACK_QUIET_ZONE_MODULES) * FALLBACK_SCALE_PX, - (row + FALLBACK_QUIET_ZONE_MODULES) * FALLBACK_SCALE_PX, - FALLBACK_SCALE_PX, FALLBACK_SCALE_PX, - ); - } - } - } -} - -const FALLBACK_LABEL_HEIGHT_PX = 200; /* target content height for the qr-text/text-only fallback layouts */ -const FALLBACK_TEXT_MARGIN_PX = 16; /* left/right margin around a text-only/qr-text fallback label */ - -/* The qr-text/text-only layouts for the no-webusb fallback preview/PNG. There's no real tape - to fit into here, so - unlike drawLabelWithText - the canvas just grows to fit its content. */ -function drawFallbackLabelWithText(canvas, qrContent, textContent) { - const qr = qrContent ? QRCode.create(qrContent) : null; - const lines = Array.isArray(textContent) ? textContent : [textContent]; - - let scale = 0, qrSize = 0; - if (qr) { - const modules = qr.modules.size + FALLBACK_QUIET_ZONE_MODULES * 2; - scale = Math.max(1, Math.floor(FALLBACK_LABEL_HEIGHT_PX / modules)); - qrSize = modules * scale; - } - - const gap = qr ? Math.round(FALLBACK_LABEL_HEIGHT_PX * 0.15) : 0; - const measureCtx = canvas.getContext("2d"); - const textPx = fitTextSize(measureCtx, lines, Infinity, FALLBACK_LABEL_HEIGHT_PX); - const textWidth = Math.max(...lines.map(line => measureAtHeight(measureCtx, line, textPx))); - - const height = Math.max(qrSize, FALLBACK_LABEL_HEIGHT_PX); - const width = qrSize + gap + textWidth + FALLBACK_TEXT_MARGIN_PX * 2; - canvas.width = width; - canvas.height = height; - - const ctx = canvas.getContext("2d", {willReadFrequently: true}); - ctx.fillStyle = "#fff"; - ctx.fillRect(0, 0, width, height); - ctx.fillStyle = "#000"; - - let cursor = FALLBACK_TEXT_MARGIN_PX; - if (qr) { - const top = Math.floor((height - qrSize) / 2); - for (let row = 0; row < qr.modules.size; row++) { - for (let col = 0; col < qr.modules.size; col++) { - if (qr.modules.get(row, col)) { - ctx.fillRect( - cursor + (col + FALLBACK_QUIET_ZONE_MODULES) * scale, - top + (row + FALLBACK_QUIET_ZONE_MODULES) * scale, - scale, scale, - ); - } - } - } - cursor += qrSize + gap; - } - - ctx.font = `${textPx}px sans-serif`; - ctx.textBaseline = "middle"; - ctx.textAlign = "left"; - const lineHeight = Math.ceil(textPx * 1.15); - let y = (height - lineHeight * lines.length) / 2 + lineHeight / 2; - for (const line of lines) { - ctx.fillText(line, cursor, y); - y += lineHeight; - } -} - -/* Dispatches to the right fallback layout - drawQrSquare is untouched so the plain QR-only - layout keeps its exact original pixel output. `content` is {qr, text}, see drawLabel above. */ -export function drawFallbackLabel(canvas, content) { - if (content.qr && !content.text) { - drawQrSquare(canvas, QRCode.create(content.qr)); - } else { - drawFallbackLabelWithText(canvas, content.qr, content.text); - } -} diff --git a/frontend/src/router.js b/frontend/src/router.js index 14ccfb5..0cf663c 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -78,10 +78,7 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, { path: '/print', component: Print, meta: {requiresAuth: true}, - props: route => { - const {kind, ...components} = route.query; - return {prefill: kind ? {kind, components} : null}; - } + props: route => ({prefill: route.query.text}) }, { path: '/search/:query', component: Search, diff --git a/frontend/src/store.js b/frontend/src/store.js index 6befff7..560dee2 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -359,10 +359,6 @@ export default createStore({ async getFriendServers({state, dispatch, commit}, {username}) { return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) }, - async fetchBackendVersion({dispatch, getters}) { - const servers = await dispatch('getHomeServers') - return await servers.get(getters.nullAuth, '/api/version/') - }, async fetchInventoryItems({commit, dispatch, getters}) { const servers = await dispatch('getHomeServers') const items = await servers.get(getters.signAuth, '/api/inventory_items/') diff --git a/frontend/src/views/InventoryDetail.vue b/frontend/src/views/InventoryDetail.vue index 6de684b..780afdb 100644 --- a/frontend/src/views/InventoryDetail.vue +++ b/frontend/src/views/InventoryDetail.vue @@ -47,7 +47,7 @@ Delete @@ -86,6 +86,12 @@ export default { }, location() { return this.storage_locations.find(loc => loc.id === this.item.storage_location) || null + }, + itemUrl() { + // The self-contained Item URL (see docs/design-in-progress/items-labels.md) - what + // a printed label actually encodes, since scanning it has to resolve the right + // frontend/backend/item with no other context, not just this browser's history. + return `${window.location.origin}/i/${this.user}/${this.id}` } }, methods: { diff --git a/frontend/src/views/Print.vue b/frontend/src/views/Print.vue index 40f5b63..b414b85 100644 --- a/frontend/src/views/Print.vue +++ b/frontend/src/views/Print.vue @@ -25,7 +25,7 @@ placeholder="https://example.com/…" autofocus> -
+
@@ -73,7 +73,7 @@ placeholder="https://example.com/…" autofocus>
-
+
@@ -130,13 +130,6 @@
- -
-
- -
-
@@ -145,12 +138,10 @@